diff --git a/assets/css/activation.css b/assets/css/activation.css index 6b78b8f6130..d11bc8d1d1a 100644 --- a/assets/css/activation.css +++ b/assets/css/activation.css @@ -1 +1 @@ -.woocommerce-message{overflow:hidden;position:relative;border-left-color:#cc99c2!important}.woocommerce-message a.button-primary,p.woocommerce-actions a.button-primary{background:#cc99c2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);border-color:#b366a4;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);color:#fff;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.woocommerce-message a.button-primary:hover,p.woocommerce-actions a.button-primary:hover{background:#bb77ae;border-color:#aa559a;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15)}.woocommerce-message a.button-primary:active,p.woocommerce-actions a.button-primary:active{background:#aa559a;border-color:#aa559a}.woocommerce-message a.woocommerce-message-close,p.woocommerce-actions a.woocommerce-message-close{position:absolute;top:10px;right:10px;padding:10px 15px 10px 21px;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:absolute;top:8px;left:0;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out}.woocommerce-message a.button-primary,.woocommerce-message a.button-secondary,p.woocommerce-actions a.button-primary,p.woocommerce-actions a.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}div.woocommerce-legacy-shipping-notice,div.woocommerce-no-shipping-methods-notice{overflow:hidden;padding:1px 12px}div.woocommerce-legacy-shipping-notice p,div.woocommerce-no-shipping-methods-notice p{position:relative;z-index:1;max-width:700px;line-height:1.5em;margin:12px 0}div.woocommerce-legacy-shipping-notice p.main,div.woocommerce-no-shipping-methods-notice p.main{font-size:1.1em}div.woocommerce-legacy-shipping-notice:before,div.woocommerce-no-shipping-methods-notice:before{content:"\e01b";font-family:WooCommerce;text-align:center;line-height:1;color:#F7F1F6;display:block;width:1em;font-size:20em;top:36px;right:12px;position:absolute} \ No newline at end of file +div.woocommerce-message{overflow:hidden;position:relative;border-left-color:#cc99c2!important}div.woocommerce-message p{max-width:700px}.woocommerce-message a.button-primary,p.woocommerce-actions a.button-primary{background:#cc99c2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);border-color:#b366a4;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);color:#fff;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.woocommerce-message a.button-primary:hover,p.woocommerce-actions a.button-primary:hover{background:#bb77ae;border-color:#aa559a;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15)}.woocommerce-message a.button-primary:active,p.woocommerce-actions a.button-primary:active{background:#aa559a;border-color:#aa559a}.woocommerce-message a.woocommerce-message-close,p.woocommerce-actions a.woocommerce-message-close{position:absolute;top:0;right:0;padding:10px 15px 10px 21px;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:absolute;top:8px;left:0;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out}.woocommerce-message a.button-primary,.woocommerce-message a.button-secondary,p.woocommerce-actions a.button-primary,p.woocommerce-actions a.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}div.woocommerce-legacy-shipping-notice,div.woocommerce-no-shipping-methods-notice{overflow:hidden;padding:1px 12px}div.woocommerce-legacy-shipping-notice p,div.woocommerce-no-shipping-methods-notice p{position:relative;z-index:1;max-width:700px;line-height:1.5em;margin:12px 0}div.woocommerce-legacy-shipping-notice p.main,div.woocommerce-no-shipping-methods-notice p.main{font-size:1.1em}div.woocommerce-legacy-shipping-notice:before,div.woocommerce-no-shipping-methods-notice:before{content:"\e01b";font-family:WooCommerce;text-align:center;line-height:1;color:#F7F1F6;display:block;width:1em;font-size:20em;top:36px;right:12px;position:absolute} \ No newline at end of file diff --git a/assets/css/activation.scss b/assets/css/activation.scss index 44d88a207ed..569a38b9efb 100644 --- a/assets/css/activation.scss +++ b/assets/css/activation.scss @@ -11,10 +11,13 @@ /** * Styling begins */ -.woocommerce-message { +div.woocommerce-message { overflow: hidden; position: relative; border-left-color: #cc99c2 !important; + p { + max-width: 700px; + } } p.woocommerce-actions, @@ -42,8 +45,8 @@ p.woocommerce-actions, a.woocommerce-message-close { position: absolute; - top: 10px; - right: 10px; + top: 0; + right: 0; padding: 10px 15px 10px 21px; font-size: 13px; line-height: 1.23076923; diff --git a/assets/css/wc-setup.css b/assets/css/wc-setup.css index 4708a819ef8..58ba9a1d20d 100644 --- a/assets/css/wc-setup.css +++ b/assets/css/wc-setup.css @@ -1 +1 @@ -.wc-setup-content p,.wc-setup-content table{font-size:1em;line-height:1.75em;color:#666}body{margin:100px auto 24px;box-shadow:none;background:#f1f1f1;padding:0}#wc-logo{border:0;margin:0 0 24px;padding:0;text-align:center}#wc-logo img{max-width:50%}.wc-setup-content{box-shadow:0 1px 3px rgba(0,0,0,.13);padding:24px 24px 0;background:#fff;overflow:hidden;zoom:1}.wc-setup-content h1,.wc-setup-content h2,.wc-setup-content h3,.wc-setup-content table{margin:0 0 24px;border:0;padding:0;color:#666;clear:none}.wc-setup-content p{margin:0 0 24px}.wc-setup-content a{color:#a16696}.wc-setup-content a:focus,.wc-setup-content a:hover{color:#111}.wc-setup-content .form-table th{width:35%;vertical-align:top;font-weight:400}.wc-setup-content .form-table td{vertical-align:top}.wc-setup-content .form-table td input,.wc-setup-content .form-table td select{width:100%;box-sizing:border-box}.wc-setup-content .form-table td input[size]{width:auto}.wc-setup-content .form-table td .description{line-height:1.5em;display:block;margin-top:.25em;color:#999;font-style:italic}.wc-setup-content .form-table td .input-checkbox,.wc-setup-content .form-table td .input-radio{width:auto;box-sizing:inherit;padding:inherit;margin:0 .5em 0 0;box-shadow:none}.wc-setup-content .form-table .section_title td{padding:0}.wc-setup-content .form-table .section_title td h2,.wc-setup-content .form-table .section_title td p{margin:12px 0 0}.wc-setup-content .form-table td,.wc-setup-content .form-table th{padding:12px 0;margin:0;border:0}.wc-setup-content .form-table td:first-child,.wc-setup-content .form-table th:first-child{padding-right:1em}.wc-setup-content table.tax-rates{width:100%;font-size:.92em}.wc-setup-content table.tax-rates th{padding:0;text-align:center;width:auto;vertical-align:middle}.wc-setup-content table.tax-rates td{border:1px solid #f5f5f5;padding:6px;text-align:center;vertical-align:middle}.wc-setup-content table.tax-rates td input{outline:0;border:0;padding:0;box-shadow:none;text-align:center;width:100%}.wc-setup-content table.tax-rates .add,.wc-setup-content table.tax-rates .remove{padding:1em 0 0 1em;line-height:1em;width:0;height:0;display:inline-block;font-size:1em;overflow:hidden}.wc-setup-content table.tax-rates td.sort{cursor:move;color:#ccc}.wc-setup-content table.tax-rates td.sort:before{content:"\f333";font-family:dashicons}.wc-setup-content table.tax-rates td.readonly{background:#f5f5f5}.wc-setup-content table.tax-rates .add{margin:6px 0 0;position:relative}.wc-setup-content table.tax-rates .add:before{content:"\f502";font-family:dashicons;position:absolute;left:0;top:0}.wc-setup-content table.tax-rates .remove{margin:0;position:relative}.wc-setup-content table.tax-rates .remove:before{content:"\f182";font-family:dashicons;position:absolute;left:0;top:0}.wc-setup-content .wc-setup-pages{width:100%;border-top:1px solid #eee}.wc-setup-content .wc-setup-pages thead th{display:none}.wc-setup-content .wc-setup-pages .page-name{width:30%;font-weight:700}.wc-setup-content .wc-setup-pages td,.wc-setup-content .wc-setup-pages th{padding:14px 0;border-bottom:1px solid #eee}.wc-setup-content .wc-setup-pages td:first-child,.wc-setup-content .wc-setup-pages th:first-child{padding-right:9px}.wc-setup-content .wc-setup-pages th{padding-top:0}.wc-setup-content .wc-setup-pages .page-options p{color:#777;margin:6px 0 0 24px;line-height:1.75em}.wc-setup-content .wc-setup-pages .page-options p input{vertical-align:middle;margin:1px 0 0;height:1.75em;width:1.75em;line-height:1.75em}.wc-setup-content .wc-setup-pages .page-options p label{line-height:1}@media screen and (max-width:782px){.wc-setup-content .form-table tbody th{width:auto}}.wc-setup-content .twitter-share-button{float:right}.wc-setup-content .wc-setup-next-steps{overflow:hidden;margin:0 0 24px}.wc-setup-content .wc-setup-next-steps h2{margin-bottom:12px}.wc-setup-content .wc-setup-next-steps .wc-setup-next-steps-first{float:left;width:50%;box-sizing:border-box}.wc-setup-content .wc-setup-next-steps .wc-setup-next-steps-last{float:right;width:50%;box-sizing:border-box}.wc-setup-content .wc-setup-next-steps ul{padding:0 2em 0 0;list-style:none;margin:0 0 -.75em}.wc-setup-content .wc-setup-next-steps ul li a{display:block;padding:0 0 .75em}.wc-setup-content .wc-setup-next-steps ul .setup-product a{background-color:#a16696;border-color:#a16696;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);font-size:1em;height:auto;line-height:1.75em;margin:0 0 .75em;opacity:1;padding:1em;text-align:center;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-setup-content .wc-setup-next-steps ul li a:before{color:#82878c;font:400 20px/1 dashicons;speak:none;display:inline-block;padding:0 10px 0 0;top:1px;position:relative;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none!important;vertical-align:top}.wc-setup-content .wc-setup-next-steps ul .learn-more a:before{content:"\f105"}.wc-setup-content .wc-setup-next-steps ul .video-walkthrough a:before{content:"\f126"}.wc-setup-content .wc-setup-next-steps ul {content:"\f118"}.wc-setup-content .wc-setup-next-steps ul .newsletter a:before{content:"\f465"}.wc-setup-content .updated,.wc-setup-content .woocommerce-tracker{padding:24px 24px 0;margin:0 0 24px;overflow:hidden;background:#f5f5f5}.wc-setup-content .updated p,.wc-setup-content .woocommerce-tracker p{padding:0;margin:0 0 12px}.wc-setup-content .updated p:last-child,.wc-setup-content .woocommerce-tracker p:last-child{margin:0 0 24px}.wc-setup-steps{padding:0 0 24px;margin:0;list-style:none;overflow:hidden;color:#ccc;width:100%;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex}.wc-setup-steps li{width:20%;float:left;padding:0 0 .8em;margin:0;text-align:center;position:relative;border-bottom:4px solid #ccc;line-height:1.4em}.wc-setup-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}.wc-setup-steps li.active{border-color:#a16696;color:#a16696}.wc-setup-steps li.active:before{border-color:#a16696}.wc-setup-steps li.done{border-color:#a16696;color:#a16696}.wc-setup-steps li.done:before{border-color:#a16696;background:#a16696}.wc-setup .wc-setup-actions{overflow:hidden}.wc-setup .wc-setup-actions .button{float:right;font-size:1.25em;padding:.5em 1em;line-height:1em;margin-right:.5em;height:auto}.wc-setup .wc-setup-actions .button-primary{background-color:#a16696;border-color:#a16696;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);float:right;margin:0;opacity:1;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-return-to-dashboard{font-size:.85em;color:#b5b5b5;margin:1.18em 0;display:block;text-align:center} +.wc-setup-content p,.wc-setup-content table{font-size:1em;line-height:1.75em;color:#666}body{margin:100px auto 24px;box-shadow:none;background:#f1f1f1;padding:0}#wc-logo{border:0;margin:0 0 24px;padding:0;text-align:center}#wc-logo img{max-width:50%}.wc-setup-content{box-shadow:0 1px 3px rgba(0,0,0,.13);padding:24px 24px 0;background:#fff;overflow:hidden;zoom:1}.wc-setup-content h1,.wc-setup-content h2,.wc-setup-content h3,.wc-setup-content table{margin:0 0 24px;border:0;padding:0;color:#666;clear:none}.wc-setup-content p{margin:0 0 24px}.wc-setup-content a{color:#a16696}.wc-setup-content a:focus,.wc-setup-content a:hover{color:#111}.wc-setup-content .form-table th{width:35%;vertical-align:top;font-weight:400}.wc-setup-content .form-table td{vertical-align:top}.wc-setup-content .form-table td input,.wc-setup-content .form-table td select{width:100%;box-sizing:border-box}.wc-setup-content .form-table td input[size]{width:auto}.wc-setup-content .form-table td .description{line-height:1.5em;display:block;margin-top:.25em;color:#999;font-style:italic}.wc-setup-content .form-table td .input-checkbox,.wc-setup-content .form-table td .input-radio{width:auto;box-sizing:inherit;padding:inherit;margin:0 .5em 0 0;box-shadow:none}.wc-setup-content .form-table .section_title td{padding:0}.wc-setup-content .form-table .section_title td h2,.wc-setup-content .form-table .section_title td p{margin:12px 0 0}.wc-setup-content .form-table td,.wc-setup-content .form-table th{padding:12px 0;margin:0;border:0}.wc-setup-content .form-table td:first-child,.wc-setup-content .form-table th:first-child{padding-right:1em}.wc-setup-content table.tax-rates{width:100%;font-size:.92em}.wc-setup-content table.tax-rates th{padding:0;text-align:center;width:auto;vertical-align:middle}.wc-setup-content table.tax-rates td{border:1px solid #f5f5f5;padding:6px;text-align:center;vertical-align:middle}.wc-setup-content table.tax-rates td input{outline:0;border:0;padding:0;box-shadow:none;text-align:center;width:100%}.wc-setup-content table.tax-rates .add,.wc-setup-content table.tax-rates .remove{padding:1em 0 0 1em;line-height:1em;width:0;height:0;display:inline-block;font-size:1em;overflow:hidden}.wc-setup-content table.tax-rates td.sort{cursor:move;color:#ccc}.wc-setup-content table.tax-rates td.sort:before{content:"\f333";font-family:dashicons}.wc-setup-content table.tax-rates td.readonly{background:#f5f5f5}.wc-setup-content table.tax-rates .add{margin:6px 0 0;position:relative}.wc-setup-content table.tax-rates .add:before{content:"\f502";font-family:dashicons;position:absolute;left:0;top:0}.wc-setup-content table.tax-rates .remove{margin:0;position:relative}.wc-setup-content table.tax-rates .remove:before{content:"\f182";font-family:dashicons;position:absolute;left:0;top:0}.wc-setup-content .wc-setup-pages{width:100%;border-top:1px solid #eee}.wc-setup-content .wc-setup-pages thead th{display:none}.wc-setup-content .wc-setup-pages .page-name{width:30%;font-weight:700}.wc-setup-content .wc-setup-pages td,.wc-setup-content .wc-setup-pages th{padding:14px 0;border-bottom:1px solid #eee}.wc-setup-content .wc-setup-pages td:first-child,.wc-setup-content .wc-setup-pages th:first-child{padding-right:9px}.wc-setup-content .wc-setup-pages th{padding-top:0}.wc-setup-content .wc-setup-pages .page-options p{color:#777;margin:6px 0 0 24px;line-height:1.75em}.wc-setup-content .wc-setup-pages .page-options p input{vertical-align:middle;margin:1px 0 0;height:1.75em;width:1.75em;line-height:1.75em}.wc-setup-content .wc-setup-pages .page-options p label{line-height:1}@media screen and (max-width:782px){.wc-setup-content .form-table tbody th{width:auto}}.wc-setup-content .twitter-share-button{float:right}.wc-setup-content .wc-setup-next-steps{overflow:hidden;margin:0 0 24px}.wc-setup-content .wc-setup-next-steps h2{margin-bottom:12px}.wc-setup-content .wc-setup-next-steps .wc-setup-next-steps-first{float:left;width:50%;box-sizing:border-box}.wc-setup-content .wc-setup-next-steps .wc-setup-next-steps-last{float:right;width:50%;box-sizing:border-box}.wc-setup-content .wc-setup-next-steps ul{padding:0 2em 0 0;list-style:none;margin:0 0 -.75em}.wc-setup-content .wc-setup-next-steps ul li a{display:block;padding:0 0 .75em}.wc-setup-content .wc-setup-next-steps ul .setup-product a{background-color:#a16696;border-color:#a16696;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);font-size:1em;height:auto;line-height:1.75em;margin:0 0 .75em;opacity:1;padding:1em;text-align:center;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-setup-content .wc-setup-next-steps ul li a:before{color:#82878c;font:400 20px/1 dashicons;speak:none;display:inline-block;padding:0 10px 0 0;top:1px;position:relative;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none!important;vertical-align:top}.wc-setup-content .wc-setup-next-steps ul .learn-more a:before{content:"\f105"}.wc-setup-content .wc-setup-next-steps ul .video-walkthrough a:before{content:"\f126"}.wc-setup-content .wc-setup-next-steps ul .newsletter a:before{content:"\f465"}.wc-setup-content .updated,.wc-setup-content .woocommerce-tracker{padding:24px 24px 0;margin:0 0 24px;overflow:hidden;background:#f5f5f5}.wc-setup-content .updated p,.wc-setup-content .woocommerce-tracker p{padding:0;margin:0 0 12px}.wc-setup-content .updated p:last-child,.wc-setup-content .woocommerce-tracker p:last-child{margin:0 0 24px}.wc-setup-steps{padding:0 0 24px;margin:0;list-style:none;overflow:hidden;color:#ccc;width:100%;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex}.wc-setup-steps li{width:20%;float:left;padding:0 0 .8em;margin:0;text-align:center;position:relative;border-bottom:4px solid #ccc;line-height:1.4em}.wc-setup-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}.wc-setup-steps li.active{border-color:#a16696;color:#a16696}.wc-setup-steps li.active:before{border-color:#a16696}.wc-setup-steps li.done{border-color:#a16696;color:#a16696}.wc-setup-steps li.done:before{border-color:#a16696;background:#a16696}.wc-setup .wc-setup-actions{overflow:hidden}.wc-setup .wc-setup-actions .button{float:right;font-size:1.25em;padding:.5em 1em;line-height:1em;margin-right:.5em;height:auto}.wc-setup .wc-setup-actions .button-primary{background-color:#a16696;border-color:#a16696;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);float:right;margin:0;opacity:1;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-return-to-dashboard{font-size:.85em;color:#b5b5b5;margin:1.18em 0;display:block;text-align:center}ul.wc-wizard-payment-gateways{border:1px solid #eee;border-bottom-width:2px;padding:0;margin:0 0 1em;list-style:none;border-radius:4px}ul.wc-wizard-payment-gateways li.wc-wizard-gateway{padding:1.5em 1em 1em 4em;border-top:1px solid #eee;list-style:none;cursor:pointer;color:#555;box-sizing:border-box;clear:both}ul.wc-wizard-payment-gateways li.wc-wizard-gateway:first-child{border-top:0}ul.wc-wizard-payment-gateways li.wc-wizard-gateway.featured{width:50%;float:left;clear:none;border-right:1px solid #eee;border-top:0}ul.wc-wizard-payment-gateways li.wc-wizard-gateway.featured-row-last{border-right:0}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-settings{display:none;margin-bottom:0;cursor:default}ul.wc-wizard-payment-gateways li.wc-wizard-gateway.checked{background:#F7F1F6}ul.wc-wizard-payment-gateways li.wc-wizard-gateway.checked .wc-wizard-gateway-settings{display:table}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-description{font-style:italic}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-description p,ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable{margin:0 0 .5em}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable label{display:block;font-weight:700;vertical-align:middle}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable label img{max-height:2em;vertical-align:middle}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable label:before{content:'';background:#fff;border:2px solid #eee;display:inline-block;float:left;vertical-align:middle;width:2em;height:2em;padding:0;box-sizing:border-box;line-height:1.8em;text-align:center;margin:0 1em 0 -3em;border-radius:4px}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable input{opacity:0;width:0;position:absolute}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable input:checked+label:before{content:"\f147";font-family:dashicons;color:#fff;border-color:#a46497;background:#a46497}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable input:focus+label{outline:#ddd solid 1px} \ No newline at end of file diff --git a/assets/css/wc-setup.scss b/assets/css/wc-setup.scss index 93151f6b000..8560ad38cb1 100644 --- a/assets/css/wc-setup.scss +++ b/assets/css/wc-setup.scss @@ -367,3 +367,108 @@ body { display: block; text-align: center; } + +ul.wc-wizard-payment-gateways { + border: 1px solid #eee; + border-bottom-width: 2px; + padding: 0; + margin: 0 0 1em; + list-style: none outside; + border-radius: 4px; + + li.wc-wizard-gateway { + padding: 1.5em 1em 1em 4em; + border-top: 1px solid #eee; + list-style: none outside; + cursor: pointer; + color: #555; + box-sizing: border-box; + clear: both; + + &:first-child { + border-top: 0; + } + + &.featured { + width: 50%; + float: left; + clear: none; + border-right: 1px solid #eee; + border-top: 0; + } + + &.featured-row-last { + border-right: 0; + } + + .wc-wizard-gateway-settings { + display: none; + margin-bottom: 0; + cursor: default; + } + + &.checked { + background: #F7F1F6; + + .wc-wizard-gateway-settings { + display: table; + } + } + + .wc-wizard-gateway-description { + font-style: italic; + + p { + margin: 0 0 .5em; + } + } + + .wc-wizard-gateway-enable { + margin: 0 0 .5em; + + label { + display: block; + font-weight: bold; + vertical-align: middle; + + img { + max-height: 2em; + vertical-align: middle; + } + } + label:before { + content: ''; + background: #fff; + border: 2px solid #eee; + display: inline-block; + float: left; + vertical-align: middle; + width: 2em; + height: 2em; + padding: 0; + box-sizing: border-box; + line-height: 1.8em; + text-align: center; + margin: 0 1em 0 -3em; + border-radius: 4px; + } + input { + opacity: 0; + width: 0; + position: absolute; + + &:checked + label:before { + content: "\f147"; + font-family: 'dashicons'; + color: #fff; + border-color: #a46497; + background: #a46497; + } + + &:focus + label { + outline: 1px solid #ddd; /* focus style */ + } + } + } + } +} diff --git a/assets/images/paypal-braintree.png b/assets/images/paypal-braintree.png new file mode 100644 index 00000000000..2eb31c6136f Binary files /dev/null and b/assets/images/paypal-braintree.png differ diff --git a/assets/images/paypal.png b/assets/images/paypal.png new file mode 100644 index 00000000000..a892ac1d876 Binary files /dev/null and b/assets/images/paypal.png differ diff --git a/assets/images/stripe.png b/assets/images/stripe.png new file mode 100644 index 00000000000..9d0ad7a535a Binary files /dev/null and b/assets/images/stripe.png differ diff --git a/assets/js/admin/wc-setup.js b/assets/js/admin/wc-setup.js index 9ab675215de..96145eaac7f 100644 --- a/assets/js/admin/wc-setup.js +++ b/assets/js/admin/wc-setup.js @@ -46,4 +46,21 @@ jQuery( function( $ ) { return true; } ); + $( '.wc-wizard-payment-gateways' ).on( 'change', '.wc-wizard-gateway-enable input', function() { + if ( $( this ).is( ':checked' ) ) { + $( this ).closest( 'li' ).addClass( 'checked' ); + } else { + $( this ).closest( 'li' ).removeClass( 'checked' ); + } + } ); + + $( '.wc-wizard-payment-gateways' ).on( 'click', 'li.wc-wizard-gateway', function() { + var $enabled = $( this ).find( '.wc-wizard-gateway-enable input' ); + + $enabled.prop( 'checked', ! $enabled.prop( 'checked' ) ).change(); + } ); + + $( '.wc-wizard-payment-gateways' ).on( 'click', 'li.wc-wizard-gateway table, li.wc-wizard-gateway a', function( e ) { + e.stopPropagation(); + } ); } ); diff --git a/assets/js/admin/wc-setup.min.js b/assets/js/admin/wc-setup.min.js index fdb451c1978..5dcaf11b467 100644 --- a/assets/js/admin/wc-setup.min.js +++ b/assets/js/admin/wc-setup.min.js @@ -1 +1 @@ -jQuery(function(a){var b=a.parseJSON(wc_setup_params.locale_info);a('select[name="store_location"]').change(function(){var c=a(this).val(),d=c.split(":")[0],e=b[d],f=["thousand_sep","decimal_sep","num_decimals","currency_pos"];e?a.each(e,function(b,c){a(':input[name="'+b+'"]').val(c).change(),-1!==a.inArray(b,f)&&a(':input[name="'+b+'"]').closest("tr").hide()}):(a(':input[name="currency_pos"]').closest("tr").show(),a(':input[name="thousand_sep"]').closest("tr").show(),a(':input[name="decimal_sep"]').closest("tr").show(),a(':input[name="num_decimals"]').closest("tr").show())}).change(),a('input[name="woocommerce_calc_taxes"]').change(function(){a(this).is(":checked")?(a(':input[name="woocommerce_prices_include_tax"], :input[name="woocommerce_import_tax_rates"]').closest("tr").show(),a("tr.tax-rates").show()):(a(':input[name="woocommerce_prices_include_tax"], :input[name="woocommerce_import_tax_rates"]').closest("tr").hide(),a("tr.tax-rates").hide())}).change(),a(".button-next").on("click",function(){return a(".wc-setup-content").block({message:null,overlayCSS:{background:"#fff",opacity:.6}}),!0})}); \ No newline at end of file +jQuery(function(a){var b=a.parseJSON(wc_setup_params.locale_info);a('select[name="store_location"]').change(function(){var c=a(this).val(),d=c.split(":")[0],e=b[d],f=["thousand_sep","decimal_sep","num_decimals","currency_pos"];e?a.each(e,function(b,c){a(':input[name="'+b+'"]').val(c).change(),-1!==a.inArray(b,f)&&a(':input[name="'+b+'"]').closest("tr").hide()}):(a(':input[name="currency_pos"]').closest("tr").show(),a(':input[name="thousand_sep"]').closest("tr").show(),a(':input[name="decimal_sep"]').closest("tr").show(),a(':input[name="num_decimals"]').closest("tr").show())}).change(),a('input[name="woocommerce_calc_taxes"]').change(function(){a(this).is(":checked")?(a(':input[name="woocommerce_prices_include_tax"], :input[name="woocommerce_import_tax_rates"]').closest("tr").show(),a("tr.tax-rates").show()):(a(':input[name="woocommerce_prices_include_tax"], :input[name="woocommerce_import_tax_rates"]').closest("tr").hide(),a("tr.tax-rates").hide())}).change(),a(".button-next").on("click",function(){return a(".wc-setup-content").block({message:null,overlayCSS:{background:"#fff",opacity:.6}}),!0}),a(".wc-wizard-payment-gateways").on("change",".wc-wizard-gateway-enable input",function(){a(this).is(":checked")?a(this).closest("li").addClass("checked"):a(this).closest("li").removeClass("checked")}),a(".wc-wizard-payment-gateways").on("click","li.wc-wizard-gateway",function(){var b=a(this).find(".wc-wizard-gateway-enable input");b.prop("checked",!b.prop("checked")).change()}),a(".wc-wizard-payment-gateways").on("click","li.wc-wizard-gateway table, li.wc-wizard-gateway a",function(a){a.stopPropagation()})}); \ No newline at end of file diff --git a/assets/js/frontend/single-product.js b/assets/js/frontend/single-product.js index 698d03ea67a..65578194dbe 100644 --- a/assets/js/frontend/single-product.js +++ b/assets/js/frontend/single-product.js @@ -7,8 +7,8 @@ jQuery( function( $ ) { } // Tabs - $( '.wc-tabs-wrapper, .woocommerce-tabs' ) - .on( 'init', function() { + $( 'body' ) + .on( 'init', '.wc-tabs-wrapper, .woocommerce-tabs', function() { $( '.wc-tab, .woocommerce-tabs .panel:not(.panel .panel)' ).hide(); var hash = window.location.hash; @@ -35,17 +35,15 @@ jQuery( function( $ ) { $tab.closest( 'li' ).addClass( 'active' ); $tabs_wrapper.find( $tab.attr( 'href' ) ).show(); }) - .trigger( 'init' ); - - $( 'a.woocommerce-review-link' ).click( function() { - $( '.reviews_tab a' ).click(); - return true; - }); - - // Star ratings for comments - $( '#rating' ).hide().before( '

12345

' ); - - $( 'body' ) + // Review link + .on( 'click', 'a.woocommerce-review-link', function() { + $( '.reviews_tab a' ).click(); + return true; + }) + // Star ratings for comments + .on( 'init', '#rating', function() { + $( '#rating' ).hide().before( '

12345

' ); + }) .on( 'click', '#respond p.stars a', function() { var $star = $( this ), $rating = $( this ).closest( '#respond' ).find( '#rating' ), @@ -68,4 +66,7 @@ jQuery( function( $ ) { return false; } }); + + //Init Tabs and Star Ratings + $( '.wc-tabs-wrapper, .woocommerce-tabs, #rating' ).trigger( 'init' ); }); diff --git a/assets/js/frontend/single-product.min.js b/assets/js/frontend/single-product.min.js index ddce18dec6b..7f9cce13f12 100644 --- a/assets/js/frontend/single-product.min.js +++ b/assets/js/frontend/single-product.min.js @@ -1 +1 @@ -jQuery(function(a){return"undefined"==typeof wc_single_product_params?!1:(a(".wc-tabs-wrapper, .woocommerce-tabs").on("init",function(){a(".wc-tab, .woocommerce-tabs .panel:not(.panel .panel)").hide();var b=window.location.hash,c=window.location.href,d=a(this).find(".wc-tabs, ul.tabs").first();b.toLowerCase().indexOf("comment-")>=0||"#reviews"===b||"#tab-reviews"===b?d.find("li.reviews_tab a").click():c.indexOf("comment-page-")>0||c.indexOf("cpage=")>0?d.find("li.reviews_tab a").click():d.find("li:first a").click()}).on("click",".wc-tabs li a, ul.tabs li a",function(b){b.preventDefault();var c=a(this),d=c.closest(".wc-tabs-wrapper, .woocommerce-tabs"),e=d.find(".wc-tabs, ul.tabs");e.find("li").removeClass("active"),d.find(".wc-tab, .panel:not(.panel .panel)").hide(),c.closest("li").addClass("active"),d.find(c.attr("href")).show()}).trigger("init"),a("a.woocommerce-review-link").click(function(){return a(".reviews_tab a").click(),!0}),a("#rating").hide().before('

12345

'),void a("body").on("click","#respond p.stars a",function(){var b=a(this),c=a(this).closest("#respond").find("#rating"),d=a(this).closest(".stars");return c.val(b.text()),b.siblings("a").removeClass("active"),b.addClass("active"),d.addClass("selected"),!1}).on("click","#respond #submit",function(){var b=a(this).closest("#respond").find("#rating"),c=b.val();return b.length>0&&!c&&"yes"===wc_single_product_params.review_rating_required?(window.alert(wc_single_product_params.i18n_required_rating_text),!1):void 0}))}); \ No newline at end of file +jQuery(function(a){return"undefined"==typeof wc_single_product_params?!1:(a("body").on("init",".wc-tabs-wrapper, .woocommerce-tabs",function(){a(".wc-tab, .woocommerce-tabs .panel:not(.panel .panel)").hide();var b=window.location.hash,c=window.location.href,d=a(this).find(".wc-tabs, ul.tabs").first();b.toLowerCase().indexOf("comment-")>=0||"#reviews"===b||"#tab-reviews"===b?d.find("li.reviews_tab a").click():c.indexOf("comment-page-")>0||c.indexOf("cpage=")>0?d.find("li.reviews_tab a").click():d.find("li:first a").click()}).on("click",".wc-tabs li a, ul.tabs li a",function(b){b.preventDefault();var c=a(this),d=c.closest(".wc-tabs-wrapper, .woocommerce-tabs"),e=d.find(".wc-tabs, ul.tabs");e.find("li").removeClass("active"),d.find(".wc-tab, .panel:not(.panel .panel)").hide(),c.closest("li").addClass("active"),d.find(c.attr("href")).show()}).on("click","a.woocommerce-review-link",function(){return a(".reviews_tab a").click(),!0}).on("init","#rating",function(){a("#rating").hide().before('

12345

')}).on("click","#respond p.stars a",function(){var b=a(this),c=a(this).closest("#respond").find("#rating"),d=a(this).closest(".stars");return c.val(b.text()),b.siblings("a").removeClass("active"),b.addClass("active"),d.addClass("selected"),!1}).on("click","#respond #submit",function(){var b=a(this).closest("#respond").find("#rating"),c=b.val();return b.length>0&&!c&&"yes"===wc_single_product_params.review_rating_required?(window.alert(wc_single_product_params.i18n_required_rating_text),!1):void 0}),void a(".wc-tabs-wrapper, .woocommerce-tabs, #rating").trigger("init"))}); \ No newline at end of file diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php new file mode 100644 index 00000000000..bb3a96f7497 --- /dev/null +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -0,0 +1,678 @@ +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( $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( $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; + } + + /** + * 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( $id ) || empty( $post->ID ) || $this->post_type !== $post->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'] ) ) { + 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; + $schema = $this->get_item_schema(); + $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 object $post Inserted object (not a WP_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 + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return true; + } + + /** + * Delete post. + * + * @param WP_Post $post + */ + 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( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $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; + } + + $schema = $this->get_item_schema(); + + $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 object $post Inserted object (not a WP_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 ( 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 ) || $this->post_type !== $post->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 ) ) { + 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 ) { + 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 ) { + 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 ) { + 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. + * @return array Links for the given post. + */ + protected function prepare_links( $post ) { + $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 + * @param WP_REST_Request $request + * @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'; + } + + 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', + ); + $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', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + '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', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + $post_type_obj = get_post_type_object( $this->post_type ); + if ( $post_type_obj->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to those of particular parent ids.', 'woocommerce' ), + 'type' => 'array', + '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', + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + } + + $params['slug'] = array( + 'description' => __( 'Limit result set to posts with a specific slug.', 'woocommerce', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + $params['filter'] = array( + '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 + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + return true; + } +} diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php new file mode 100644 index 00000000000..4b7ec89d9b4 --- /dev/null +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -0,0 +1,750 @@ +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( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + )); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 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' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_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 cannot create new resource.', '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_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 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 cannot delete 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 ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Check permissions for a single term. + if ( $id = intval( $request['id'] ) ) { + $term = get_term( $id, $taxonomy ); + + if ( ! $term || $term->taxonomy !== $taxonomy ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't 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 ); + $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'] >= $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 pagation 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 = add_query_arg( $request->get_query_params(), rest_url( '/' . $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 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 ) ); + } + + $parent = get_term( (int) $request['parent'], $taxonomy ); + + if ( ! $parent ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Parent resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $args['parent'] = $parent->term_id; + } + + $term = wp_insert_term( $name, $taxonomy, $args ); + if ( is_wp_error( $term ) ) { + + // If we're going to inform the client that the term exists, give them the identifier + // they can actually use. + if ( ( $term_id = $term->get_error_data( 'term_exists' ) ) ) { + $existing_term = get_term( $term_id, $taxonomy ); + $term->add_data( $existing_term->term_id, 'term_exists' ); + } + + return $term; + } + + $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 ) ) { + 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 ) ); + } + + $parent = get_term( (int) $request['parent'], $taxonomy ); + + if ( ! $parent ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Parent resource doesn't exist.", 'woocommerce' ), array( 'status' => 400 ) ); + } + + $prepared_args['parent'] = $parent->term_id; + } + + // 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 ); + $term = get_term( (int) $request['id'], $taxonomy ); + $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 ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + + $retval = wp_delete_term( $term->term_id, $term->taxonomy ); + if ( ! $retval ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single term is deleted via the REST API. + * + * @param WP_Term $term The deleted term. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param object $term Term object. + * @param WP_REST_Request $request Full details about the request. + * @return array Links for the given term. + */ + protected function prepare_links( $term, $request ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + + if ( ! empty( $request['attribute_id'] ) ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + + if ( $term->parent ) { + $parent_term = get_term( (int) $term->parent, $term->taxonomy ); + if ( $parent_term ) { + $links['up'] = array( + 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ), + ); + } + } + + return $links; + } + + /** + * Update term meta fields. + * + * @param WP_Term $term + * @param WP_REST_Request $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()`. + * @return array List of term objects. (Total count in `$this->total_terms`). + */ + protected function get_terms_for_product( $prepared_args ) { + $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' ) ) ) { + 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(); + + if ( '' !== $this->taxonomy ) { + $taxonomy = get_taxonomy( $this->taxonomy ); + } else { + $taxonomy = new stdClass(); + $taxonomy->hierarchical = true; + } + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + if ( ! $taxonomy->hierarchical ) { + $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', + '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', + ); + if ( $taxonomy->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific parent.', '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 ) { + // Check if taxonomy is defined. + // Prevents check for attribute taxonomy more than one time for each query. + if ( '' !== $this->taxonomy ) { + return $this->taxonomy; + } + + if ( ! empty( $request['attribute_id'] ) ) { + $taxonomy = wc_attribute_taxonomy_name_by_id( (int) $request['attribute_id'] ); + + $this->taxonomy = $taxonomy; + } + + return $this->taxonomy; + } +} diff --git a/includes/admin/class-wc-admin-notices.php b/includes/admin/class-wc-admin-notices.php index ff154db506e..52f766ec3fe 100644 --- a/includes/admin/class-wc-admin-notices.php +++ b/includes/admin/class-wc-admin-notices.php @@ -28,6 +28,7 @@ class WC_Admin_Notices { 'theme_support' => 'theme_check_notice', 'legacy_shipping' => 'legacy_shipping_notice', 'no_shipping_methods' => 'no_shipping_methods_notice', + 'simplify_commerce' => 'simplify_commerce_notice', ); /** @@ -57,12 +58,19 @@ class WC_Admin_Notices { if ( ! current_theme_supports( 'woocommerce' ) && ! in_array( get_option( 'template' ), wc_get_core_supported_themes() ) ) { self::add_notice( 'theme_support' ); } + + $simplify_options = get_option( 'woocommerce_simplify_commerce_settings', array() ); + + if ( ! class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) && ! empty( $simplify_options['enabled'] ) && 'yes' === $simplify_options['enabled'] ) { + WC_Admin_Notices::add_notice( 'simplify_commerce' ); + } + self::add_notice( 'template_files' ); } /** * Show a notice. - * @param string $name + * @param string $name */ public static function add_notice( $name ) { $notices = array_unique( array_merge( get_option( 'woocommerce_admin_notices', array() ), array( $name ) ) ); @@ -76,6 +84,7 @@ class WC_Admin_Notices { public static function remove_notice( $name ) { $notices = array_diff( get_option( 'woocommerce_admin_notices', array() ), array( $name ) ); update_option( 'woocommerce_admin_notices', $notices ); + delete_option( 'woocommerce_admin_notice_' . $name ); } /** @@ -117,6 +126,36 @@ class WC_Admin_Notices { foreach ( $notices as $notice ) { if ( ! empty( $this->core_notices[ $notice ] ) && apply_filters( 'woocommerce_show_admin_notice', true, $notice ) ) { add_action( 'admin_notices', array( $this, $this->core_notices[ $notice ] ) ); + } else { + add_action( 'admin_notices', array( $this, 'output_custom_notices' ) ); + } + } + } + } + + /** + * Add a custom notice. + * @param string $name + * @param string $notice_html + */ + public static function add_custom_notice( $name, $notice_html ) { + self::add_notice( $name ); + update_option( 'woocommerce_admin_notice_' . $name, wp_kses_post( $notice_html ) ); + } + + /** + * Output any stored custom notices. + */ + public function output_custom_notices() { + $notices = get_option( 'woocommerce_admin_notices', array() ); + if ( $notices ) { + foreach ( $notices as $notice ) { + if ( empty( $this->core_notices[ $notice ] ) ) { + $notice_html = get_option( 'woocommerce_admin_notice_' . $notice ); + + if ( $notice_html ) { + include( 'views/html-notice-custom.php' ); + } } } } @@ -225,6 +264,19 @@ class WC_Admin_Notices { } } } + + /** + * Simplify Commerce is being removed from core. + */ + public function simplify_commerce_notice() { + if ( class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) ) { + self::remove_notice( 'simplify_commerce' ); + return; + } + if ( empty( $_GET['action'] ) ) { + include( 'views/html-notice-simplify-commerce.php' ); + } + } } new WC_Admin_Notices(); diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php index fe658a3e6ae..8077288b257 100644 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ b/includes/admin/class-wc-admin-setup-wizard.php @@ -557,56 +557,132 @@ class WC_Admin_Setup_Wizard { exit; } + /** + * Simple array of gateways to show in wizard. + * @return array + */ + protected function get_wizard_payment_gateways() { + $gateways = array( + 'paypal-braintree' => array( + 'name' => __( 'PayPal by Braintree', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/paypal-braintree.png', + 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s paypal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), + 'class' => 'featured featured-row-last', + 'repo-slug' => 'woocommerce-gateway-paypal-powered-by-braintree', + ), + 'paypal-ec' => array( + 'name' => __( 'PayPal Express Checkout', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/paypal.png', + 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), + 'class' => 'featured featured-row-last', + 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', + ), + 'stripe' => array( + 'name' => __( 'Stripe', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/stripe.png', + 'description' => sprintf( __( 'A modern and robust way to accept credit card payments on your store. %sLearn more about Stripe%s.', 'woocommerce' ), '', '' ), + 'class' => 'featured featured-row-first', + 'repo-slug' => 'woocommerce-gateway-stripe', + ), + 'paypal' => array( + 'name' => __( 'PayPal Standard', 'woocommerce' ), + 'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ), + 'image' => '', + 'class' => '', + 'settings' => array( + 'email' => array( + 'label' => __( 'PayPal email address', 'woocommerce' ), + 'type' => 'email', + 'value' => get_option( 'admin_email' ), + 'placeholder' => __( 'PayPal email address', 'woocommerce' ), + ), + ), + ), + 'cheque' => array( + 'name' => __( 'Cheque Payments', 'woocommerce' ), + 'description' => __( 'An simple offline gateway that lets you accept Cheque payment.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + 'bacs' => array( + 'name' => __( 'Bank Transfer (BACS) Payments', 'woocommerce' ), + 'description' => __( 'An simple offline gateway that lets you accept BACS payment.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + 'cod' => array( + 'name' => __( 'Cash on Delivery', 'woocommerce' ), + 'description' => __( 'An simple offline gateway that lets you accept cash on delivery.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ) + ); + + $country = WC()->countries->get_base_country(); + + if ( 'US' === $country ) { + unset( $gateways['paypal-ec'] ); + } else { + unset( $gateways['paypal-braintree'] ); + } + + if ( ! current_user_can( 'install_plugins' ) ) { + unset( $gateways['paypal-braintree'] ); + unset( $gateways['paypal-ec'] ); + unset( $gateways['stripe'] ); + } + + return $gateways; + } + /** * Payments Step. */ public function wc_setup_payments() { - $paypal_settings = array_filter( (array) get_option( 'woocommerce_paypal_settings', array() ) ); - $cheque_settings = array_filter( (array) get_option( 'woocommerce_cheque_settings', array() ) ); - $cod_settings = array_filter( (array) get_option( 'woocommerce_cod_settings', array() ) ); - $bacs_settings = array_filter( (array) get_option( 'woocommerce_bacs_settings', array() ) ); + $gateways = $this->get_wizard_payment_gateways(); ?>

-
+

', '', '' ); ?>

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

-

-
- -
-

-

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

@@ -622,25 +698,27 @@ class WC_Admin_Setup_Wizard { public function wc_setup_payments_save() { check_admin_referer( 'wc-setup' ); - $paypal_settings = array_filter( (array) get_option( 'woocommerce_paypal_settings', array() ) ); - $cheque_settings = array_filter( (array) get_option( 'woocommerce_cheque_settings', array() ) ); - $cod_settings = array_filter( (array) get_option( 'woocommerce_cod_settings', array() ) ); - $bacs_settings = array_filter( (array) get_option( 'woocommerce_bacs_settings', array() ) ); + $gateways = $this->get_wizard_payment_gateways(); - $paypal_settings['enabled'] = ! empty( $_POST['woocommerce_paypal_email'] ) ? 'yes' : 'no'; - $cheque_settings['enabled'] = isset( $_POST['woocommerce_enable_cheque'] ) ? 'yes' : 'no'; - $cod_settings['enabled'] = isset( $_POST['woocommerce_enable_cod'] ) ? 'yes' : 'no'; - $bacs_settings['enabled'] = isset( $_POST['woocommerce_enable_bacs'] ) ? 'yes' : 'no'; + foreach ( $gateways as $gateway_id => $gateway ) { + // If repo-slug is defined, download and install plugin from .org. + if ( ! empty( $gateway['repo-slug'] ) && ! empty( $_POST[ 'wc-wizard-gateway-' . $gateway_id . '-enabled' ] ) ) { + wp_schedule_single_event( time() + 10, 'woocommerce_plugin_background_installer', array( $gateway_id, $gateway ) ); + } - if ( ! empty( $_POST['woocommerce_paypal_email'] ) ) { - $paypal_settings['email'] = wc_clean( $_POST['woocommerce_paypal_email'] ); + $settings_key = 'woocommerce_' . $gateway_id . '_settings'; + $settings = array_filter( (array) get_option( $settings_key, array() ) ); + $settings['enabled'] = ! empty( $_POST[ 'wc-wizard-gateway-' . $gateway_id . '-enabled' ] ) ? 'yes' : 'no'; + + if ( ! empty( $gateway['settings'] ) ) { + foreach ( $gateway['settings'] as $setting_id => $setting ) { + $settings[ $setting_id ] = wc_clean( $_POST[ $gateway_id . '_' . $setting_id ] ); + } + } + + update_option( $settings_key, $settings ); } - update_option( 'woocommerce_paypal_settings', $paypal_settings ); - update_option( 'woocommerce_cheque_settings', $cheque_settings ); - update_option( 'woocommerce_cod_settings', $cod_settings ); - update_option( 'woocommerce_bacs_settings', $bacs_settings ); - wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); exit; } diff --git a/includes/admin/views/html-notice-custom.php b/includes/admin/views/html-notice-custom.php new file mode 100644 index 00000000000..4a020d7afdf --- /dev/null +++ b/includes/admin/views/html-notice-custom.php @@ -0,0 +1,14 @@ + +

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

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

+ +

+
diff --git a/includes/api/class-wc-rest-authentication.php b/includes/api/class-wc-rest-authentication.php new file mode 100644 index 00000000000..6d5049be133 --- /dev/null +++ b/includes/api/class-wc-rest-authentication.php @@ -0,0 +1,412 @@ +perform_basic_authentication(); + } else { + return $this->perform_oauth_authentication(); + } + } + + /** + * Check for authentication error. + * + * @param WP_Error|null|bool $error + * @return WP_Error|null|bool + */ + public function check_authentication_error( $error ) { + global $wc_rest_authentication_error; + + // Passthrough other errors. + if ( ! empty( $error ) ) { + return $error; + } + + return $wc_rest_authentication_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() { + global $wc_rest_authentication_error; + + $user = null; + $consumer_key = ''; + $consumer_secret = ''; + + // If the $_GET parameters are present, use those first. + if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) { + $consumer_key = $_GET['consumer_key']; + $consumer_secret = $_GET['consumer_secret']; + } + + // If the above is not present, we will do full basic auth. + if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { + $consumer_key = $_SERVER['PHP_AUTH_USER']; + $consumer_secret = $_SERVER['PHP_AUTH_PW']; + } + + // Stop if don't have any key. + if ( ! $consumer_key || ! $consumer_secret ) { + return false; + } + + // Get user data. + $user = $this->get_user_data_by_consumer_key( $consumer_key ); + if ( empty( $user ) ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); + + return false; + } + + // Validate user secret. + if ( ! hash_equals( $user->consumer_secret, $consumer_secret ) ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); + + return false; + } + + // Check API Key permissions. + if ( ! $this->check_permissions( $user->permissions ) ) { + return false; + } + + // Update last access. + $this->update_last_access( $user->key_id ); + + return $user->user_id; + } + + /** + * 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() { + global $wc_rest_authentication_error; + + $params = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); + + // Check for required OAuth parameters. + foreach ( $params as $param ) { + if ( empty( $_GET[ $param ] ) ) { + return false; + } + } + + // Fetch WP user by consumer key + $user = $this->get_user_data_by_consumer_key( $_GET['oauth_consumer_key'] ); + + if ( empty( $user ) ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); + + return false; + } + + // Perform OAuth validation. + $wc_rest_authentication_error = $this->check_oauth_signature( $user, $_GET ); + if ( is_wp_error( $wc_rest_authentication_error ) ) { + return false; + } + + $wc_rest_authentication_error = $this->check_oauth_timestamp_and_nonce( $user, $_GET['oauth_timestamp'], $_GET['oauth_nonce'] ); + if ( is_wp_error( $wc_rest_authentication_error ) ) { + return false; + } + + // Check API Key permissions. + if ( ! $this->check_permissions( $user->permissions ) ) { + return false; + } + + // Update last access. + $this->update_last_access( $user->key_id ); + + return $user->user_id; + } + + /** + * Verify that the consumer-provided request signature matches our generated signature, + * this ensures the consumer has a valid key/secret. + * + * @param stdClass $user + * @param array $params The request parameters. + * @return null|WP_Error + */ + private function check_oauth_signature( $user, $params ) { + $http_method = strtoupper( $_SERVER['REQUEST_METHOD'] ); + $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); + $wp_base = get_home_url( null, '/', 'relative' ); + if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) { + $request_path = substr( $request_path, strlen( $wp_base ) ); + } + $base_request_uri = rawurlencode( get_home_url( null, $request_path ) ); + + // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature. + $consumer_signature = rawurldecode( $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_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 ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - signature method is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); + $secret = $user->consumer_secret . '&'; + $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); + + if ( ! hash_equals( $signature, $consumer_signature ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + return true; + } + + /** + * 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. + * + * @see rawurlencode() + * @param array $parameters Un-normalized pararmeters. + * @return array Normalized parameters. + */ + private function normalize_parameters( $parameters ) { + $keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) ); + $values = wc_rest_urlencode_rfc3986( array_values( $parameters ) ); + $parameters = array_combine( $keys, $values ); + + return $parameters; + } + + /** + * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where + * an attacker could attempt to re-send an intercepted request at a later time. + * + * - A timestamp is valid if it is within 15 minutes of now. + * - A nonce is valid if it has not been used within the last 15 minutes. + * + * @param stdClass $user + * @param int $timestamp the unix timestamp for when the request was made + * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated + * @return bool|WP_Error + */ + private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) { + global $wpdb; + + $valid_window = 15 * 60; // 15 minute window. + + if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $used_nonces = maybe_unserialize( $user->nonces ); + + if ( empty( $used_nonces ) ) { + $used_nonces = array(); + } + + if ( in_array( $nonce, $used_nonces ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $used_nonces[ $timestamp ] = $nonce; + + // Remove expired nonces. + foreach ( $used_nonces as $nonce_timestamp => $nonce ) { + if ( $nonce_timestamp < ( time() - $valid_window ) ) { + unset( $used_nonces[ $nonce_timestamp ] ); + } + } + + $used_nonces = maybe_serialize( $used_nonces ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'nonces' => $used_nonces ), + array( 'key_id' => $user->key_id ), + array( '%s' ), + array( '%d' ) + ); + + return true; + } + + /** + * Return the user data for the given consumer_key. + * + * @param string $consumer_key + * @return array + */ + private function get_user_data_by_consumer_key( $consumer_key ) { + global $wpdb; + + $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); + $user = $wpdb->get_row( $wpdb->prepare( " + SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces + FROM {$wpdb->prefix}woocommerce_api_keys + WHERE consumer_key = %s + ", $consumer_key ) ); + + return $user; + } + + /** + * Check that the API keys provided have the proper key-specific permissions to either read or write API resources. + * + * @param string $permissions + * @return bool + */ + private function check_permissions( $permissions ) { + global $wc_rest_authentication_error; + + $valid = true; + + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) { + return false; + } + + switch ( $_SERVER['REQUEST_METHOD'] ) { + + case 'HEAD' : + case 'GET' : + if ( 'read' !== $permissions && 'read_write' !== $permissions ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'woocommerce' ), array( 'status' => 401 ) ); + $valid = false; + } + break; + + case 'POST' : + case 'PUT' : + case 'PATCH' : + case 'DELETE' : + if ( 'write' !== $permissions && 'read_write' !== $permissions ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'woocommerce' ), array( 'status' => 401 ) ); + $valid = false; + } + break; + } + + return $valid; + } + + /** + * Updated API Key last access datetime. + * + * @param int $key_id + */ + private function update_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' ) + ); + } + + /** + * 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 ) { + global $wc_rest_authentication_error; + + if ( is_wp_error( $wc_rest_authentication_error ) && is_ssl() ) { + $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; + } +} + +new WC_REST_Authentication(); diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php new file mode 100644 index 00000000000..580518964ee --- /dev/null +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -0,0 +1,569 @@ +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( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 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' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + global $wpdb; + + if ( ! empty( $request['code'] ) ) { + $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $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 ) { + global $wpdb; + + // Get the coupon code. + $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $post->ID ) ); + + $coupon = new WC_Coupon( $code ); + + $data = array( + 'id' => $coupon->id, + 'code' => $coupon->code, + 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), + 'discount_type' => $coupon->type, + 'description' => $post->post_excerpt, + 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), + 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? wc_rest_prepare_date_response( $coupon->expiry_date ) : null, + 'usage_count' => (int) $coupon->usage_count, + 'individual_use' => ( 'yes' === $coupon->individual_use ), + 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), + 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), + 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, + 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, + 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, + 'free_shipping' => $coupon->enable_free_shipping(), + 'product_categories' => array_map( 'absint', (array) $coupon->product_categories ), + 'excluded_product_categories' => array_map( 'absint', (array) $coupon->exclude_product_categories ), + 'exclude_sale_items' => $coupon->exclude_sale_items(), + 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), + 'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ), + 'email_restrictions' => $coupon->customer_email, + 'used_by' => $coupon->get_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 ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $post ) ); + + /** + * 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. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + global $wpdb; + + $data = new stdClass; + + // ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + $schema = $this->get_item_schema(); + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->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 ) ); + } + } + + // Code. + if ( ! empty( $schema['properties']['code'] ) && ! empty( $request['code'] ) ) { + $coupon_code = apply_filters( 'woocommerce_coupon_code', $request['code'] ); + $id = isset( $data->ID ) ? $data->ID : 0; + + // Check for duplicate coupon codes. + $coupon_found = $wpdb->get_var( $wpdb->prepare( " + SELECT $wpdb->posts.ID + FROM $wpdb->posts + WHERE $wpdb->posts.post_type = 'shop_coupon' + AND $wpdb->posts.post_status = 'publish' + AND $wpdb->posts.post_title = '%s' + AND $wpdb->posts.ID != %s + ", $coupon_code, $id ) ); + + if ( $coupon_found ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data->post_title = $coupon_code; + } + + // Content. + $data->post_content = ''; + + // Excerpt. + if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['description'] ) ) { + $data->post_excerpt = wp_filter_post_kses( $request['description'] ); + } + + // Post type. + $data->post_type = $this->post_type; + + // Post status. + $data->post_status = 'publish'; + + // 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 ); + } + + /** + * Expiry date format. + * + * @param string $expiry_date + * @return string + */ + protected function get_coupon_expiry_date( $expiry_date ) { + if ( '' != $expiry_date ) { + return date( 'Y-m-d', strtotime( $expiry_date ) ); + } + + return ''; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + $data = $request->get_json_params(); + + $defaults = array( + 'discount_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' => '', + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'email_restrictions' => array(), + 'description' => '' + ); + + $data = wp_parse_args( $data, $defaults ); + + // Set coupon meta. + update_post_meta( $post->ID, 'discount_type', $data['discount_type'] ); + update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); + update_post_meta( $post->ID, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); + update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); + update_post_meta( $post->ID, 'usage_limit', absint( $data['usage_limit'] ) ); + update_post_meta( $post->ID, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); + update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); + update_post_meta( $post->ID, 'usage_count', absint( $data['usage_count'] ) ); + update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); + update_post_meta( $post->ID, 'free_shipping', ( true === $data['free_shipping'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $data['product_categories'] ) ) ); + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $data['excluded_product_categories'] ) ) ); + update_post_meta( $post->ID, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); + update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $data['email_restrictions'] ) ) ); + + return true; + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + if ( isset( $request['amount'] ) ) { + update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $request['amount'] ) ); + } + + if ( isset( $request['individual_use'] ) ) { + update_post_meta( $post->ID, 'individual_use', ( true === $request['individual_use'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['product_ids'] ) ) { + update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $request['product_ids'] ) ) ) ); + } + + if ( isset( $request['exclude_product_ids'] ) ) { + update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $request['exclude_product_ids'] ) ) ) ); + } + + if ( isset( $request['usage_limit'] ) ) { + update_post_meta( $post->ID, 'usage_limit', absint( $request['usage_limit'] ) ); + } + + if ( isset( $request['usage_limit_per_user'] ) ) { + update_post_meta( $post->ID, 'usage_limit_per_user', absint( $request['usage_limit_per_user'] ) ); + } + + if ( isset( $request['limit_usage_to_x_items'] ) ) { + update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $request['limit_usage_to_x_items'] ) ); + } + + if ( isset( $request['usage_count'] ) ) { + update_post_meta( $post->ID, 'usage_count', absint( $request['usage_count'] ) ); + } + + if ( isset( $request['expiry_date'] ) ) { + update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $request['expiry_date'] ) ) ); + } + + if ( isset( $request['free_shipping'] ) ) { + update_post_meta( $post->ID, 'free_shipping', ( true === $request['free_shipping'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['product_categories'] ) ) { + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $request['product_categories'] ) ) ); + } + + if ( isset( $request['excluded_product_categories'] ) ) { + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $request['excluded_product_categories'] ) ) ); + } + + if ( isset( $request['exclude_sale_items'] ) ) { + update_post_meta( $post->ID, 'exclude_sale_items', ( true === $request['exclude_sale_items'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['minimum_amount'] ) ) { + update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $request['minimum_amount'] ) ); + } + + if ( isset( $request['maximum_amount'] ) ) { + update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $request['maximum_amount'] ) ); + } + + if ( isset( $request['email_restrictions'] ) ) { + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $request['email_restrictions'] ) ) ); + } + + return true; + } + + /** + * 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', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'amount' => array( + 'description' => __( 'The amount of discount.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'expiry_date' => array( + 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'individual_use' => array( + 'description' => __( 'Whether coupon can only be used individually.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( "List of product ID's the coupon can be used on.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'exclude_product_ids' => array( + 'description' => __( "List of product ID's the coupon cannot be used on.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be user 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' => __( 'Define if can be applied for free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'product_categories' => array( + 'description' => __( "List of category ID's the coupon applies to.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_categories' => array( + 'description' => __( "List of category ID's the coupon does not apply to.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'Define if should not apply when have sale items.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'email_restrictions' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'used_by' => array( + 'description' => __( 'List of user IDs who have used the coupon.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema );; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['code'] = array( + 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-customer-downloads-controller.php b/includes/api/class-wc-rest-customer-downloads-controller.php new file mode 100644 index 00000000000..2b5deed07d4 --- /dev/null +++ b/includes/api/class-wc-rest-customer-downloads-controller.php @@ -0,0 +1,243 @@ +/downloads endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Customers controller class. + * + * @package WooCommerce/API + * @extends WP_REST_Controller + */ +class WC_REST_Customer_Downloads_Controller extends WP_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( + 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 doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_user_permissions( 'read', $customer->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']; + + $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' => __( 'Amount of downloads remaining.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires' => array( + 'description' => __( "The date when the download access expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File details', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php new file mode 100644 index 00000000000..0b3a6ecde4c --- /dev/null +++ b/includes/api/class-wc-rest-customers-controller.php @@ -0,0 +1,903 @@ +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, + ), + 'username' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), + ), + 'password' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 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' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array(), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/me', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_current_item' ), + 'args' => array( + 'context' => array(), + ), + '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 ) { + 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 boolean + */ + 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 boolean + */ + 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 boolean + */ + 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; + } + + /** + * Get all customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $prepared_args = array(); + $prepared_args['exclude'] = $request['exclude']; + $prepared_args['include'] = $request['include']; + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'ID', + 'include' => 'include', + 'name' => 'display_name', + 'registered_date' => 'registered', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['search'] = $request['search']; + + if ( '' !== $prepared_args['search'] ) { + $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; + } + + // Filter by email. + if ( ! empty( $request['email'] ) ) { + $prepared_args['search'] = $request['email']; + $prepared_args['search_columns'] = array( 'user_email' ); + } + + // Filter by role. + if ( 'all' !== $request['role'] ) { + $prepared_args['role'] = $request['role']; + } + + /** + * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. + * + * @see https://developer.wordpress.org/reference/classes/wp_user_query/ + * + * @param array $prepared_args Array of arguments for WP_User_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); + + $query = new WP_User_Query( $prepared_args ); + + $users = array(); + foreach ( $query->results as $user ) { + $data = $this->prepare_item_for_response( $user, $request ); + $users[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $users ); + + // Store pagation values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $prepared_args['fields'] = 'ID'; + + $total_users = $query->get_total(); + if ( $total_users < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'] ); + unset( $prepared_args['offset'] ); + $count_query = new WP_User_Query( $prepared_args ); + $total_users = $count_query->get_total(); + } + $response->header( 'X-WP-Total', (int) $total_users ); + $max_pages = ceil( $total_users / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Sets the username. + $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; + + // Sets the password. + $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; + + // Create customer. + $customer_id = wc_create_new_customer( $request['email'], $request['username'], $request['password'] );; + if ( is_wp_error( $customer_id ) ) { + return $customer_id; + } + + $customer = get_user_by( 'id', $customer_id ); + + $this->update_additional_fields_for_object( $customer, $request ); + + // Add customer data. + $this->update_customer_meta_fields( $customer, $request ); + + /** + * 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', $customer, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $customer, $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_id ) ) ); + + return $response; + } + + /** + * 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']; + $customer = get_userdata( $id ); + + if ( empty( $id ) || empty( $customer->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $customer = $this->prepare_item_for_response( $customer, $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 ) { + $id = (int) $request['id']; + $customer = get_userdata( $id ); + + if ( ! $customer ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->user_email ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $request['username'] ) && $request['username'] !== $customer->user_login ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable", 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Customer email. + if ( isset( $request['email'] ) ) { + wp_update_user( array( 'ID' => $customer->ID, 'user_email' => sanitize_email( $request['email'] ) ) ); + } + + // Customer password. + if ( isset( $request['password'] ) ) { + wp_update_user( array( 'ID' => $customer->ID, 'user_pass' => wc_clean( $request['password'] ) ) ); + } + + $this->update_additional_fields_for_object( $customer, $request ); + + // Update customer data. + $this->update_customer_meta_fields( $customer, $request ); + + /** + * 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', $customer, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $customer, $request ); + $response = rest_ensure_response( $response ); + return $response; + } + + /** + * 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 ) ); + } + + $customer = get_userdata( $id ); + if ( ! $customer ) { + 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( $customer, $request ); + + /** Include admin customer functions to get access to wp_delete_user() */ + require_once ABSPATH . 'wp-admin/includes/user.php'; + + $result = wp_delete_user( $id, $reassign ); + + if ( ! $result ) { + return 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 $customer The customer 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', $customer, $response, $request ); + + return $response; + } + + /** + * Get the current customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_current_item( $request ) { + $id = get_current_user_id(); + if ( empty( $id ) ) { + return new WP_Error( 'woocommerce_rest_not_logged_in', __( 'You are not currently logged in.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + 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() ) ); + } + + $customer = wp_get_current_user(); + $response = $this->prepare_item_for_response( $customer, $request ); + $response = rest_ensure_response( $response ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) ); + $response->set_status( 302 ); + + return $response; + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $customer Customer object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $customer, $request ) { + $last_order = wc_get_customer_last_order( $customer->ID ); + + $data = array( + 'id' => $customer->ID, + 'date_created' => wc_rest_prepare_date_response( $customer->user_registered ), + 'date_modified' => $customer->last_update ? wc_rest_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, + 'email' => $customer->user_email, + 'first_name' => $customer->first_name, + 'last_name' => $customer->last_name, + 'username' => $customer->user_login, + 'last_order' => array( + 'id' => is_object( $last_order ) ? $last_order->id : null, + 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->post->post_date_gmt ) : null + ), + 'orders_count' => wc_get_customer_order_count( $customer->ID ), + 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ), + 'avatar_url' => wc_get_customer_avatar_url( $customer->customer_email ), + 'billing_address' => array( + 'first_name' => $customer->billing_first_name, + 'last_name' => $customer->billing_last_name, + 'company' => $customer->billing_company, + 'address_1' => $customer->billing_address_1, + 'address_2' => $customer->billing_address_2, + 'city' => $customer->billing_city, + 'state' => $customer->billing_state, + 'postcode' => $customer->billing_postcode, + 'country' => $customer->billing_country, + 'email' => $customer->billing_email, + 'phone' => $customer->billing_phone, + ), + 'shipping_address' => array( + 'first_name' => $customer->shipping_first_name, + 'last_name' => $customer->shipping_last_name, + 'company' => $customer->shipping_company, + 'address_1' => $customer->shipping_address_1, + 'address_2' => $customer->shipping_address_2, + 'city' => $customer->shipping_city, + 'state' => $customer->shipping_state, + 'postcode' => $customer->shipping_postcode, + 'country' => $customer->shipping_country, + ), + ); + + $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( $customer ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $customer User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $customer, $request ); + } + + /** + * Update customer meta fields. + * + * @param WP_User $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'] ) ) { + update_user_meta( $customer->ID, 'first_name', wc_clean( $request['first_name'] ) ); + } + + // Customer last name. + if ( isset( $request['last_name'] ) ) { + update_user_meta( $customer->ID, 'last_name', wc_clean( $request['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $request['billing_address'] ) ) { + foreach ( array_keys( $schema['properties']['billing_address']['properties'] ) as $address ) { + if ( isset( $request['billing_address'][ $address ] ) ) { + update_user_meta( $customer->ID, 'billing_' . $address, wc_clean( $request['billing_address'][ $address ] ) ); + } + } + } + + // Customer shipping address. + if ( isset( $request['shipping_address'] ) ) { + foreach ( array_keys( $schema['properties']['shipping_address']['properties'] ) as $address ) { + if ( isset( $request['shipping_address'][ $address ] ) ) { + update_user_meta( $customer->ID, 'shipping_' . $address, wc_clean( $request['shipping_address'][ $address ] ) ); + } + } + } + } + + /** + * 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, in the site's timezone.", '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, + ), + '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' => __( 'UTC DateTime of the customer last order.', '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' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing_address' => 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_address' => 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', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'name', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'include', + 'name', + 'registered_date', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['email'] = array( + 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['role'] = array( + 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'customer', + 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), + 'validate_callback' => 'rest_validate_request_arg', + ); + return $params; + } +} diff --git a/includes/api/class-wc-rest-exception.php b/includes/api/class-wc-rest-exception.php new file mode 100644 index 00000000000..a059493dba1 --- /dev/null +++ b/includes/api/class-wc-rest-exception.php @@ -0,0 +1,19 @@ +/notes endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Notes controller class. + * + * @package WooCommerce/API + * @extends WP_REST_Controller + */ +class WC_REST_Order_Notes_Controller extends WP_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/notes'; + + /** + * Register the routes for order notes. + */ + 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( + 'note' => array( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 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, + '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( 'shop_order', '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 boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'shop_order', '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 ) { + $post = get_post( (int) $request['order_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'shop_order', '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 delete a order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + $post = get_post( (int) $request['order_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'shop_order', '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; + } + + /** + * Get order notes from an order. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $id = (int) $request['id']; + $order = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || 'shop_order' !== $order->post_type ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $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 ); + + $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'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || 'shop_order' !== $order->post_type ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $order = wc_get_order( $order ); + + // 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->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 = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || 'shop_order' !== $order->post_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->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 = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || 'shop_order' !== $order->post_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->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $note, $request ); + + $result = wp_delete_comment( $note->comment_ID, true );; + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a order note is deleted or trashed via the REST API. + * + * @param WP_Comment $note The deleted or trashed order note. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request ); + + return $response; + } + + /** + * Prepare a single order note output for response. + * + * @param WP_Comment $note Order note object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $note, $request ) { + $data = array( + 'id' => $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' => 'tax', + 'type' => 'order_note', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'note' => array( + 'description' => __( 'Order note.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_note' => array( + 'description' => __( 'Shows/define if the note is only for reference or for the customer (the user will be notified).', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php new file mode 100644 index 00000000000..a9d51d4c6c8 --- /dev/null +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -0,0 +1,519 @@ +/refunds endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Posts_Controller + */ +class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_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( + 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, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 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, + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array(), + ), + ), + '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_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + global $wpdb; + + $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 || intval( $refund->post->post_parent ) !== intval( $order->id ) ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $dp = $request['dp']; + + $data = array( + 'id' => $refund->id, + 'date_created' => wc_rest_prepare_date_response( $refund->date ), + 'amount' => wc_format_decimal( $refund->get_refund_amount(), $dp ), + 'reason' => $refund->get_refund_reason(), + 'line_items' => array(), + ); + + // Add line items. + foreach ( $refund->get_items() as $item_id => $item ) { + $product = $refund->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $product->id; + $variation_id = $product->variation_id; + $product_sku = $product->get_sku(); + } + + $meta = new WC_Order_Item_Meta( $item, $product ); + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta['key'], + 'label' => $formatted_meta['label'], + 'value' => $formatted_meta['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 ) ); + + /** + * 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. + * @return array Links for the given order refund. + */ + protected function prepare_links( $refund ) { + $order_id = $refund->post->post_parent; + $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->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 + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + $args['post_status'] = 'any'; + + 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'] ) ) { + 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 ); + } + + $api_refund = is_bool( $request['api_refund'] ) ? $request['api_refund'] : true; + + $data = array( + 'order_id' => $order_data->ID, + 'amount' => $request['amount'], + 'line_items' => $request['line_items'], + ); + + // Create the refund. + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_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_data ); + + if ( isset( $payment_gateways[ $order->payment_method ] ) && $payment_gateways[ $order->payment_method ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->payment_method ]->process_refund( $order_id, $refund->get_refund_amount(), $refund->get_refund_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + return new WP_Error( 'woocommerce_rest_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + $post = get_post( $refund->id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_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' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line total tax.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + '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, + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['dp'] = array( + 'default' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php new file mode 100644 index 00000000000..813b5d67583 --- /dev/null +++ b/includes/api/class-wc-rest-orders-controller.php @@ -0,0 +1,1711 @@ +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( + 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' ), + ), + 'reassign' => array(), + ), + ), + 'schema' => array( $this, 'get_public_item_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 ) { + global $wpdb; + + $order = wc_get_order( $post ); + $dp = $request['dp']; + + $data = array( + 'id' => $order->id, + 'parent_id' => $post->post_parent, + 'status' => $order->get_status(), + 'order_key' => $order->order_key, + 'currency' => $order->get_order_currency(), + 'version' => $order->order_version, + 'prices_include_tax' => $order->prices_include_tax, + 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), + 'customer_id' => $order->get_user_id(), + 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'discount_tax' => wc_format_decimal( $order->cart_discount_tax, $dp ), + 'shipping_total' => wc_format_decimal( $order->get_total_shipping(), $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->payment_method, + 'payment_method_title' => $order->payment_method_title, + 'transaction_id' => $order->get_transaction_id(), + 'customer_ip_address' => $order->customer_ip_address, + 'customer_user_agent' => $order->customer_user_agent, + 'created_via' => $order->created_via, + 'customer_note' => $order->customer_note, + 'date_completed' => wc_rest_prepare_date_response( $order->completed_date, true ), + 'date_paid' => $order->paid_date, + 'cart_hash' => $order->cart_hash, + 'line_items' => array(), + 'tax_lines' => array(), + 'shipping_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // Add addresses. + $data['billing'] = $order->get_address( 'billing' ); + $data['shipping'] = $order->get_address( 'shipping' ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $order->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $product->id; + $variation_id = $product->variation_id; + $product_sku = $product->get_sku(); + } + + $meta = new WC_Order_Item_Meta( $item, $product ); + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta['key'], + 'label' => $formatted_meta['label'], + 'value' => $formatted_meta['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 = maybe_unserialize( $shipping_item['taxes'] ); + + if ( ! empty( $shipping_taxes ) ) { + $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes ), $dp ); + + foreach ( $shipping_taxes 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' => '', + ); + } + + 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; + } + + $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 ) ); + + /** + * 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. + * @return array Links for the given order. + */ + protected function prepare_links( $order ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->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->post->post_parent ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->post->post_parent ) ), + ); + } + + 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 ( ! empty( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + 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; + } + + return $args; + } + + /** + * Create order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + wc_transaction_query( 'start' ); + + try { + // Make sure customer exists. + if ( 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $order = wc_create_order( array( + 'status' => $request['status'], + 'customer_id' => $request['customer_id'], + 'customer_note' => $request['customer_note'], + 'created_via' => 'rest-api', + ) ); + + if ( is_wp_error( $order ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_order', sprintf( __( 'Cannot create order: %s.', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // Set addresses. + if ( is_array( $request['billing'] ) ) { + $this->update_address( $order, $request['billing'], 'billing' ); + } + if ( is_array( $request['shipping'] ) ) { + $this->update_address( $order, $request['shipping'], 'shipping' ); + } + + // Set currency. + update_post_meta( $order->id, '_order_currency', $request['currency'] ); + + // Set lines. + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + if ( is_array( $request[ $line ] ) ) { + foreach ( $request[ $line ] as $item ) { + $set_item = 'set_' . $line_type; + $new_item = $this->$set_item( $order, $item, 'create' ); + } + } + } + + // Calculate totals and set them. + $order->calculate_totals(); + + // Set payment method. + if ( ! empty( $request['payment_method'] ) ) { + update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); + } + if ( ! empty( $request['payment_method_title'] ) ) { + update_post_meta( $order->id, '_payment_method_title', $request['payment_method'] ); + } + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // Set meta data. + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + $this->update_meta_data( $order->id, $request['meta_data'] ); + } + + wc_transaction_query( 'commit' ); + + return $order->id; + } catch ( WC_REST_Exception $e ) { + wc_transaction_query( 'rollback' ); + + 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' ) { + $fields = $order->get_address( $type ); + + foreach ( array_keys( $fields ) as $field ) { + if ( isset( $posted[ $field ] ) ) { + $fields[ $field ] = $posted[ $field ]; + } + } + + // Set address. + $order->set_address( $fields, $type ); + + // Update user meta. + if ( $order->get_user_id() ) { + foreach ( $fields as $key => $value ) { + update_user_meta( $order->get_user_id(), $type . '_' . $key, $value ); + } + } + } + + /** + * Create or update a line item. + * + * @param WC_Order $order Order data. + * @param array $item Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_line_item( $order, $item, $action = 'create' ) { + $creating = 'create' === $action; + $item_args = array(); + + // Product is always required. + if ( empty( $item['product_id'] ) && empty( $item['sku'] ) && empty( $item['variation_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $item['product_id'] ) ) { + $product_id = (int) $item['product_id']; + } else if ( ! empty( $item['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $item['sku'] ); + } else if ( ! empty( $item['variation_id'] ) ) { + $product_id = (int) $item['variation_id']; + } + + // When updating, ensure product ID provided matches. + if ( 'update' === $action && ! empty( $item['id'] ) ) { + $item_product_id = (int) wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = (int) wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $product_id !== $item_product_id && $product_id !== $item_variation_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or variation ID provided does not match this line item.', 'woocommerce' ), 400 ); + } + } + + $product = wc_get_product( $product_id ); + + // Must be a valid WC_Product. + if ( ! is_object( $product ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // Quantity must be positive float. + if ( isset( $item['quantity'] ) && 0 >= floatval( $item['quantity'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_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_REST_Exception( 'woocommerce_rest_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + // Get variation attributes. + if ( method_exists( $product, 'get_variation_attributes' ) ) { + $item_args['variation'] = $product->get_variation_attributes(); + } + + // Quantity. + if ( isset( $item['quantity'] ) ) { + $item_args['qty'] = $item['quantity']; + } + + // Total. + if ( isset( $item['total'] ) ) { + $item_args['totals']['total'] = floatval( $item['total'] ); + } + + // Total tax. + if ( isset( $item['total_tax'] ) ) { + $item_args['totals']['tax'] = floatval( $item['total_tax'] ); + } + + // Subtotal. + if ( isset( $item['subtotal'] ) ) { + $item_args['totals']['subtotal'] = floatval( $item['subtotal'] ); + } + + // Subtotal tax. + if ( isset( $item['subtotal_tax'] ) ) { + $item_args['totals']['subtotal_tax'] = floatval( $item['subtotal_tax'] ); + } + + if ( $creating ) { + $item_id = $order->add_product( $product, $item_args['qty'], $item_args ); + if ( ! $item_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } else { + $item_id = $order->update_product( $item['id'], $product, $item_args ); + if ( ! $item_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_line_item', __( 'Cannot update line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order shipping method. + * + * @param WC_Order $order Order data. + * @param array $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_shipping( $order, $shipping, $action ) { + // Total must be a positive float. + if ( ! empty( $shipping['total'] ) && 0 > floatval( $shipping['total'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + // Method ID is required. + if ( empty( $shipping['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_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'] ); + + $shipping_id = $order->add_shipping( $rate ); + + if ( ! $shipping_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_shipping', __( 'Cannot create shipping method, try again.', 'woocommerce' ), 500 ); + } + + } else { + $shipping_args = array(); + + if ( isset( $shipping['method_id'] ) ) { + $shipping_args['method_id'] = $shipping['method_id']; + } + + if ( isset( $shipping['method_title'] ) ) { + $shipping_args['method_title'] = $shipping['method_title']; + } + + if ( isset( $shipping['total'] ) ) { + $shipping_args['cost'] = floatval( $shipping['total'] ); + } + + $shipping_id = $order->update_shipping( $shipping['id'], $shipping_args ); + + if ( ! $shipping_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee. + * + * @param WC_Order $order Order data. + * @param array $fee Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_fee( $order, $fee, $action ) { + if ( 'create' === $action ) { + + // Fee name is required. + if ( empty( $fee['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + + $fee_data = new stdClass(); + $fee_data->id = sanitize_title( $fee['name'] ); + $fee_data->name = $fee['name']; + $fee_data->amount = isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0; + $fee_data->taxable = false; + $fee_data->tax = 0; + $fee_data->tax_data = array(); + $fee_data->tax_class = ''; + + // If taxable, tax class and total are required. + if ( isset( $fee['tax_status'] ) && 'taxable' === $fee['tax_status'] ) { + + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $fee_data->taxable = true; + $fee_data->tax_class = $fee['tax_class']; + + if ( isset( $fee['total_tax'] ) ) { + $fee_data->tax = isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0; + } + } + + $fee_id = $order->add_fee( $fee_data ); + + if ( ! $fee_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_fee', __( 'Cannot create fee, try again.', 'woocommerce' ), 500 ); + } + + } else { + $fee_args = array(); + + if ( isset( $fee['name'] ) ) { + $fee_args['name'] = $fee['name']; + } + + if ( isset( $fee['tax_class'] ) ) { + $fee_args['tax_class'] = $fee['tax_class']; + } + + if ( isset( $fee['total'] ) ) { + $fee_args['line_total'] = floatval( $fee['total'] ); + } + + if ( isset( $fee['total_tax'] ) ) { + $fee_args['line_tax'] = floatval( $fee['total_tax'] ); + } + + $fee_id = $order->update_fee( $fee['id'], $fee_args ); + + if ( ! $fee_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon. + * + * @param WC_Order $order Order data. + * @param array $coupon Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_coupon( $order, $coupon, $action ) { + // Coupon discount must be positive float. + if ( isset( $coupon['discount'] ) && 0 > floatval( $coupon['discount'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_total', __( 'Coupon discount must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + // Coupon code is required. + if ( empty( $coupon['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $coupon_id = $order->add_coupon( $coupon['code'], floatval( $coupon['discount'] ) ); + + if ( ! $coupon_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_order_coupon', __( 'Cannot create coupon, try again.', 'woocommerce' ), 500 ); + } + + } else { + $coupon_args = array(); + + if ( isset( $coupon['code'] ) ) { + $coupon_args['code'] = $coupon['code']; + } + + if ( isset( $coupon['discount'] ) ) { + $coupon_args['discount_amount'] = floatval( $coupon['discount'] ); + } + + $coupon_id = $order->update_coupon( $coupon['id'], $coupon_args ); + + if ( ! $coupon_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Helper method to add/update meta data, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @param WC_Order $order Order data. + * @param array $meta_data Meta data in array( 'meta_key' => 'meta_value' ) format. + */ + protected function update_meta_data( $order_id, $meta_data ) { + foreach ( $meta_data 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 ); + } + } + } + + /** + * 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'] ) ) { + 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; + } + + // Clear transients. + wc_delete_shop_order_transients( $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 object $post Inserted object (not a WP_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; + } + + /** + * 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 $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_REST_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->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 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * 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', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Update order. + * + * @param WP_REST_Request $request Full details about the request. + * @param WP_Post $post Post data. + * @return int|WP_Error + */ + protected function update_order( $request, $post ) { + try { + $update_totals = false; + $order = wc_get_order( $post ); + $order_args = array( 'order_id' => $order->id ); + + // Customer note. + if ( isset( $request['customer_note'] ) ) { + $order_args['customer_note'] = $request['customer_note']; + } + + // Customer ID. + if ( isset( $request['customer_id'] ) && $request['customer_id'] != $order->get_user_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 ); + } + + update_post_meta( $order->id, '_customer_user', $request['customer_id'] ); + } + + // Update addresses. + if ( is_array( $request['billing'] ) ) { + $this->update_address( $order, $request['billing'], 'billing' ); + } + if ( is_array( $request['shipping'] ) ) { + $this->update_address( $order, $request['shipping'], 'shipping' ); + } + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + if ( isset( $request[ $line ] ) && is_array( $request[ $line ] ) ) { + $update_totals = true; + foreach ( $request[ $line ] as $item ) { + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID is required.', 'woocommerce' ), 400 ); + } + + // 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' ); + } + } + } + } + + // Set payment method. + if ( ! empty( $request['payment_method'] ) ) { + update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); + } + if ( ! empty( $request['payment_method_title'] ) ) { + update_post_meta( $order->id, '_payment_method_title', $request['payment_method'] ); + } + if ( $order->needs_payment() && isset( $request['set_paid'] ) && true === $request['set_paid'] ) { + $order->payment_complete( ! empty( $request['transaction_id'] ) ? $request['transaction_id'] : '' ); + } + + // Set order currency. + if ( isset( $request['currency'] ) ) { + update_post_meta( $order->id, '_order_currency', $request['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update meta data. + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + $this->update_meta_data( $order->id, $request['meta_data'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $request['status'] ) ) { + $order->update_status( $request['status'], isset( $request['status_note'] ) ? $request['status_note'] : '' ); + } + + return $order->id; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * 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 ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $order_id = $this->update_order( $request, $post ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Clear transients. + wc_delete_shop_order_transients( $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 object $post Inserted object (not a WP_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 order statuses. + * + * @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, + ), + '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 when the order was made.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'Shows if 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, in the site's timezone.", '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, + ), + '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' => 'array', + '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' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + 'writeonly' => true, + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'boolean', + '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 has been 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' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line total tax.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + '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, + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + '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' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'integer', + '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 total tax.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + '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' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total tax (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 total tax.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + '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' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-product-attribute-terms-controller.php b/includes/api/class-wc-rest-product-attribute-terms-controller.php new file mode 100644 index 00000000000..4941c9cf773 --- /dev/null +++ b/includes/api/class-wc-rest-product-attribute-terms-controller.php @@ -0,0 +1,118 @@ +/terms endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Attribute Terms controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Terms_Controller + */ +class WC_REST_Product_Attribute_Terms_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'; + + /** + * Prepare a single product attribute term output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + '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 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', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'woocommerce' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-product-attributes-controller.php b/includes/api/class-wc-rest-product-attributes-controller.php new file mode 100644 index 00000000000..0b5eb2beeec --- /dev/null +++ b/includes/api/class-wc-rest-product-attributes-controller.php @@ -0,0 +1,621 @@ +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( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + )); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 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' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_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 doesn't 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 doesn't 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 doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all attributes. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $attributes = wc_get_attribute_taxonomies(); + $data = array(); + foreach ( $attributes as $attribute_obj ) { + $attribute = $this->prepare_item_for_response( $attribute_obj, $request ); + $attribute = $this->prepare_response_for_collection( $attribute ); + $data[] = $attribute; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + global $wpdb; + + $args = array( + 'attribute_label' => $request['name'], + 'attribute_name' => $request['slug'], + 'attribute_type' => $request['type'], + 'attribute_orderby' => $request['order_by'], + 'attribute_public' => $request['has_archives'], + ); + + // Set the attribute slug. + if ( empty( $args['attribute_name'] ) ) { + $args['attribute_name'] = wc_sanitize_taxonomy_name( stripslashes( $args['attribute_label'] ) ); + } else { + $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); + } + + $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], true ); + if ( is_wp_error( $valid_slug ) ) { + return $valid_slug; + } + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $args, + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for errors. + if ( is_wp_error( $insert ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', $insert->get_error_message(), array( 'status' => 400 ) ); + } + + $attribute = $this->get_attribute( $wpdb->insert_id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $this->update_additional_fields_for_object( $attribute, $request ); + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return $response; + } + + /** + * Get a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function get_item( $request ) { + global $wpdb; + + $attribute = $this->get_attribute( $request['id'] ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $response = $this->prepare_item_for_response( $attribute, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Update a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $format = array( '%s', '%s', '%s', '%s', '%d' ); + $args = array( + 'attribute_label' => $request['name'], + 'attribute_name' => $request['slug'], + 'attribute_type' => $request['type'], + 'attribute_orderby' => $request['order_by'], + 'attribute_public' => $request['has_archives'], + ); + + $i = 0; + foreach ( $args as $key => $value ) { + if ( empty( $value ) && ! is_bool( $value ) ) { + unset( $args[ $key ] ); + unset( $format[ $i ] ); + } + + $i++; + } + + // Set the attribute slug. + if ( ! empty( $args['attribute_name'] ) ) { + $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); + + $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], true ); + if ( is_wp_error( $valid_slug ) ) { + return $valid_slug; + } + } + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $args, + array( 'attribute_id' => $id ), + $format, + array( '%d' ) + ); + + // Checks for errors. + if ( false === $update ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Could not edit the attribute', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $attribute = $this->get_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $this->update_additional_fields_for_object( $attribute, $request ); + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + global $wpdb; + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $attribute = $this->get_attribute( $request['id'] ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $attribute->attribute_id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute->attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + /** + * Fires after a single attribute is deleted via the REST API. + * + * @param stdObject $attribute The deleted attribute. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request ); + + // Fires woocommerce_attribute_deleted hook. + do_action( 'woocommerce_attribute_deleted', $attribute->attribute_id, $attribute->attribute_name, $taxonomy ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return $response; + } + + /** + * Prepare a single product attribute output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item->attribute_id, + 'name' => $item->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $item->attribute_name ), + 'type' => $item->attribute_type, + 'order_by' => $item->attribute_orderby, + 'has_archives' => (bool) $item->attribute_public, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter a attribute item returned from the API. + * + * Allows modification of the product attribute data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original attribute object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $attribute Attribute object. + * @param WP_REST_Request $request Full details about the request. + * @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 int|WP_Error + */ + protected function get_taxonomy( $request ) { + if ( '' !== $this->attribute ) { + return $this->attribute; + } + + if ( $request['id'] ) { + $name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] ); + + $this->attribute = $name; + } + + return $this->attribute; + } + + /** + * Get attribute data. + * + * @param int $id Attribute ID. + * @return stdClass|WP_Error + */ + protected function get_attribute( $id ) { + global $wpdb; + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + return new WP_Error( 'woocommerce_rest_attribute_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $attribute; + } + + /** + * Validate attribute slug. + * + * @param string $slug + * @param bool $new_data + * @return bool|WP_Error + */ + protected function validate_attribute_slug( $slug, $new_data = true ) { + if ( strlen( $slug ) >= 28 ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max).', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } else if ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } else if ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } + + return true; + } +} diff --git a/includes/api/class-wc-rest-product-categories-controller.php b/includes/api/class-wc-rest-product-categories-controller.php new file mode 100644 index 00000000000..a9194844859 --- /dev/null +++ b/includes/api/class-wc-rest-product-categories-controller.php @@ -0,0 +1,194 @@ +term_id, 'display_type' ); + + // Get category image. + $image = ''; + if ( $image_id = get_woocommerce_term_meta( $item->term_id, 'thumbnail_id' ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $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' => $image ? esc_url( $image ) : '', + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Update term meta fields. + * + * @param WP_Term $term + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + update_woocommerce_term_meta( $id, 'display_type', $request['display'] ); + + if ( ! empty( $request['image'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image'] ) ); + + if ( is_wp_error( $upload ) ) { + return $upload; + } + + $image_id = wc_rest_set_uploaded_image_as_attachment( $upload ); + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_woocommerce_term_meta( $id, 'thumbnail_id', $image_id ); + } + } + + 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 URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'woocommerce' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-product-reviews-controller.php b/includes/api/class-wc-rest-product-reviews-controller.php new file mode 100644 index 00000000000..118e8b01875 --- /dev/null +++ b/includes/api/class-wc-rest-product-reviews-controller.php @@ -0,0 +1,271 @@ +/reviews endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Products controller class. + * + * @package WooCommerce/API + * @extends WP_REST_Controller + */ +class WC_REST_Product_Reviews_Controller extends WP_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( + 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( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( '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 webhook develivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['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; + } + + /** + * Get all reviews from a product. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $product = get_post( (int) $request['product_id'] ); + + if ( empty( $product->post_type ) || 'product' !== $product->post_type ) { + 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 = get_post( (int) $request['product_id'] ); + + if ( empty( $product->post_type ) || 'product' !== $product->post_type ) { + 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 ) !== intval( $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; + } + + /** + * 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 ), + 'reviewer_name' => $review->comment_author, + 'reviewer_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 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' ), + '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, + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'reviewer_name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'reviewer_email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/class-wc-rest-product-shipping-classes-controller.php b/includes/api/class-wc-rest-product-shipping-classes-controller.php new file mode 100644 index 00000000000..ddd5121208d --- /dev/null +++ b/includes/api/class-wc-rest-product-shipping-classes-controller.php @@ -0,0 +1,140 @@ + (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'parent' => (int) $item->parent, + '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', + ), + ), + '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', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'woocommerce' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-product-tags-controller.php b/includes/api/class-wc-rest-product-tags-controller.php new file mode 100644 index 00000000000..494de9526b2 --- /dev/null +++ b/includes/api/class-wc-rest-product-tags-controller.php @@ -0,0 +1,134 @@ + (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Tag's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'woocommerce' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php new file mode 100644 index 00000000000..53bee01fbc9 --- /dev/null +++ b/includes/api/class-wc-rest-products-controller.php @@ -0,0 +1,2553 @@ +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( + 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' ), + ), + 'reassign' => array(), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @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_type' => 'type', + '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 ] ) ) { + $terms = explode( ',', $request[ $key ] ); + + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $terms, + ); + } + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names() ) ) { + $terms = explode( ',', $request['attribute_term'] ); + + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $terms, + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $request['sku'], + 'compare' => '=' + ); + + $args['post_type'] = array( 'product', 'product_variation' ); + } + + return $args; + } + + /** + * Get the downloads for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_files() 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 + * @param string $taxonomy + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wp_get_post_terms( $product->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 + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + if ( $product->is_type( 'variation' ) ) { + if ( has_post_thumbnail( $product->get_variation_id() ) ) { + // Add variation image if set. + $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() ); + } elseif ( has_post_thumbnail( $product->id ) ) { + // Otherwise use the parent product featured image if set. + $attachment_ids[] = get_post_thumbnail_id( $product->id ); + } + } else { + // Add featured image. + if ( has_post_thumbnail( $product->id ) ) { + $attachment_ids[] = get_post_thumbnail_id( $product->id ); + } + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_ids() ); + } + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + '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 ), + '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, + 'date_created' => wc_rest_prepare_date_response( time() ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( time() ), + 'src' => wc_placeholder_img_src(), + 'title' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get default attributes. + * + * @param WC_Product $product + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( (array) get_post_meta( $product->id, '_default_attributes', true ) as $key => $value ) { + $default[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $key ) ), + 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ), + 'option' => $value, + ); + } + } + + return $default; + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product + * @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 ) { + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + $attributes[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), + 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ), + 'option' => $attribute, + ); + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + // Taxonomy-based attributes are comma-separated, others are pipe (|) separated. + if ( $attribute['is_taxonomy'] ) { + $options = explode( ',', $product->get_attribute( $attribute['name'] ) ); + } else { + $options = explode( '|', $product->get_attribute( $attribute['name'] ) ); + } + + $attributes[] = array( + 'name' => wc_attribute_label( $attribute['name'], $product ), + 'slug' => str_replace( 'pa_', '', $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => array_map( 'trim', $options ), + ); + } + } + + return $attributes; + } + + /** + * Get product menu order. + * + * @param WC_Product $product + * @return int + */ + protected function get_product_menu_order( $product ) { + $menu_order = $product->get_post_data()->menu_order; + + if ( $product->is_type( 'variation' ) ) { + $variation = get_post( $product->get_variation_id() ); + $menu_order = $variation->menu_order; + } + + return $menu_order; + } + + /** + * Get product data. + * + * @param WC_Product $product + * @return array + */ + protected function get_product_data( $product ) { + $data = array( + 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, + 'name' => $product->get_title(), + 'slug' => $product->get_post_data()->post_name, + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_post_data()->post_modified_gmt ), + 'type' => $product->product_type, + 'status' => $product->get_post_data()->post_status, + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->visibility, + 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), + '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->sale_price_dates_from ? date( 'Y-m-d', $product->sale_price_dates_from ) : '', + 'date_on_sale_to' => $product->sale_price_dates_to ? date( 'Y-m-d', $product->sale_price_dates_to ) : '', + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale(), + 'purchasable' => $product->is_purchasable(), + 'total_sales' => (int) get_post_meta( $product->id, 'total_sales', true ), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => (int) $product->download_limit, + 'download_expiry' => (int) $product->download_expiry, + 'download_type' => $product->download_type ? $product->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->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' => (int) $product->get_shipping_class_id(), + 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => (int) $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsells() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), + 'parent_id' => $product->is_type( 'variation' ) ? $product->parent->id : $product->get_post_data()->post_parent, + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->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' => $this->get_product_menu_order( $product ), + ); + + return $data; + } + + /** + * Get an individual variation's data. + * + * @param WC_Product $product + * @return array + */ + protected function get_variation_data( $product ) { + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + $variation = $product->get_child( $child_id ); + if ( ! $variation->exists() ) { + continue; + } + + $variations[] = array( + 'id' => $variation->get_variation_id(), + 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), + '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->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', + 'date_on_sale_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', + 'on_sale' => $variation->is_on_sale(), + 'purchasable' => $variation->is_purchasable(), + 'virtual' => $variation->is_virtual(), + 'downloadable' => $variation->is_downloadable(), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $variation->download_limit, + 'download_expiry' => (int) $variation->download_expiry, + '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->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 $data + */ + 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 ) ); + + /** + * 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. + * @return array Links for the given product. + */ + protected function prepare_links( $product ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( $product->is_type( 'variation' ) && $product->parent ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->parent ) ), + ); + } elseif ( $product->is_type( 'simple' ) && ! empty( $product->post->post_parent ) ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->post->post_parent ) ), + ); + } + + 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 ) { + $data = new stdClass; + + // ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $data->post_title = wp_filter_post_kses( $request['name'] ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $data->post_content = wp_filter_post_kses( $request['description'] ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $data->post_excerpt = wp_filter_post_kses( $request['short_description'] ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $data->post_status = get_post_status_object( $request['status'] ) ? $request['status'] : 'draft'; + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $data->post_name = $request['slug']; + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $data->menu_order = (int) $request['menu_order']; + } + + // Comment status. + if ( ! empty( $request['reviews_allowed'] ) ) { + $data->comment_status = $request['reviews_allowed'] ? 'open' : 'closed'; + } + + // Only when creating products. + if ( empty( $request['id'] ) ) { + // Post type. + $data->post_type = $this->post_type; + + // 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 ); + } + + /** + * Get attribute taxonomy by slug. + * + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $taxonomies as $key => $tax ) { + if ( $slug === $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * Save product images. + * + * @param WC_Product $product + * @param array $images + * @throws WC_REST_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 = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->id ); + } + + set_post_thumbnail( $product->id, $attachment_id ); + } else { + $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 ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->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 ) ) { + update_post_meta( $product->id, '_product_image_gallery', implode( ',', $gallery ) ); + } + } else { + delete_post_thumbnail( $product->id ); + update_post_meta( $product->id, '_product_image_gallery', '' ); + } + } + + /** + * Save product shipping data. + * + * @param WC_Product $product + * @param array $data + */ + private function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + update_post_meta( $product->id, '_weight', '' ); + update_post_meta( $product->id, '_length', '' ); + update_post_meta( $product->id, '_width', '' ); + update_post_meta( $product->id, '_height', '' ); + } else { + if ( isset( $data['weight'] ) ) { + update_post_meta( $product->id, '_weight', '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + update_post_meta( $product->id, '_height', '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + update_post_meta( $product->id, '_width', '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + update_post_meta( $product->id, '_length', '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + wp_set_object_terms( $product->id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); + } + } + + /** + * Save downloadable files. + * + * @param WC_Product $product + * @param array $downloads + * @param int $variation_id + */ + private function save_downloadable_files( $product, $downloads, $variation_id = 0 ) { + $files = array(); + + // File paths will be stored in an array keyed off md5(file path). + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( ! isset( $file['file'] ) ) { + continue; + } + + $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; + + if ( 0 === strpos( $file['file'], 'http' ) ) { + $file_url = esc_url_raw( $file['file'] ); + } else { + $file_url = wc_clean( $file['file'] ); + } + + $files[ md5( $file_url ) ] = array( + 'name' => $file_name, + 'file' => $file_url, + ); + } + + // Grant permission to any newly added files on any existing orders for this product prior to saving. + do_action( 'woocommerce_process_product_file_download_paths', $product->id, $variation_id, $files ); + + $id = ( 0 === $variation_id ) ? $product->id : $variation_id; + + update_post_meta( $id, '_downloadable_files', $files ); + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product + * @param array $terms + * @param string $taxonomy + * @return array + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + $term_ids = array_unique( array_map( 'intval', $term_ids ) ); + + wp_set_object_terms( $product->id, $term_ids, 'product_' . $taxonomy ); + + return $terms; + } + + /** + * Save product meta. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_product_meta( $product, $request ) { + global $wpdb; + + // Product Type. + $product_type = null; + if ( isset( $request['type'] ) ) { + $product_type = wc_clean( $request['type'] ); + wp_set_object_terms( $product->id, $product_type, 'product_type' ); + } else { + $_product_type = get_the_terms( $product->id, 'product_type' ); + if ( is_array( $_product_type ) ) { + $_product_type = current( $_product_type ); + $product_type = $_product_type->slug; + } + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + update_post_meta( $product->id, '_virtual', true === $request['virtual'] ? 'yes' : 'no' ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + update_post_meta( $product->id, '_tax_status', wc_clean( $request['tax_status'] ) ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + update_post_meta( $product->id, '_tax_class', wc_clean( $request['tax_class'] ) ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + update_post_meta( $product->id, '_visibility', wc_clean( $request['catalog_visibility'] ) ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + update_post_meta( $product->id, '_purchase_note', wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + update_post_meta( $product->id, '_featured', true === $request['featured'] ? 'yes' : 'no' ); + } + + // Shipping data. + $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $sku = get_post_meta( $product->id, '_sku', true ); + $new_sku = wc_clean( $request['sku'] ); + + if ( '' === $new_sku ) { + update_post_meta( $product->id, '_sku', '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->id, $new_sku ); + if ( ! $unique_sku ) { + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + update_post_meta( $product->id, '_sku', $new_sku ); + } + } else { + update_post_meta( $product->id, '_sku', '' ); + } + } + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['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 ) { + + 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->id, $values, $taxonomy ); + } + + if ( $values ) { + // Add attribute to array, but don't set values. + $attributes[ $taxonomy ] = array( + 'name' => $taxonomy, + 'value' => '', + 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, + 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, + 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, + 'is_taxonomy' => $is_taxonomy + ); + } + + } elseif ( isset( $attribute['options'] ) ) { + // Array based. + if ( is_array( $attribute['options'] ) ) { + $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); + + // Text based, separate by pipe. + } else { + $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attributes[ $attribute_slug ] = array( + 'name' => wc_clean( $attribute['name'] ), + 'value' => $values, + 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, + 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, + 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, + 'is_taxonomy' => $is_taxonomy + ); + } + } + + if ( ! function_exists( 'attributes_cmp' ) ) { + function attributes_cmp( $a, $b ) { + if ( $a['position'] === $b['position'] ) { + return 0; + } + + return ( $a['position'] < $b['position'] ) ? -1 : 1; + } + } + uasort( $attributes, 'attributes_cmp' ); + + update_post_meta( $product->id, '_product_attributes', $attributes ); + } + + // Sales and prices. + if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + update_post_meta( $product->id, '_regular_price', '' ); + update_post_meta( $product->id, '_sale_price', '' ); + update_post_meta( $product->id, '_sale_price_dates_from', '' ); + update_post_meta( $product->id, '_sale_price_dates_to', '' ); + update_post_meta( $product->id, '_price', '' ); + + } else { + + // Regular Price + if ( isset( $request['regular_price'] ) ) { + $regular_price = ( '' === $request['regular_price'] ) ? '' : $request['regular_price']; + } else { + $regular_price = get_post_meta( $product->id, '_regular_price', true ); + } + + // Sale Price + if ( isset( $request['sale_price'] ) ) { + $sale_price = ( '' === $request['sale_price'] ) ? '' : $request['sale_price']; + } else { + $sale_price = get_post_meta( $product->id, '_sale_price', true ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $date_from = $request['date_on_sale_from']; + } else { + $date_from = get_post_meta( $product->id, '_sale_price_dates_from', true ); + $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $date_to = $request['date_on_sale_to']; + } else { + $date_to = get_post_meta( $product->id, '_sale_price_dates_to', true ); + $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); + } + + _wc_save_product_price( $product->id, $regular_price, $sale_price, $date_from, $date_to ); + } + + // Product parent ID for groups. + $parent_id = 0; + if ( isset( $request['parent_id'] ) ) { + $parent_id = wp_update_post( array( 'ID' => $product->id, 'post_parent' => absint( $request['parent_id'] ) ) ); + } + + // Update parent if grouped so price sorting works and stays in sync with the cheapest child. + if ( $parent_id > 0 || 'grouped' === $product_type ) { + + $clear_parent_ids = array(); + + if ( $parent_id > 0 ) { + $clear_parent_ids[] = $parent_id; + } + + if ( 'grouped' === $product_type ) { + $clear_parent_ids[] = $product->id; + } + + if ( $clear_parent_ids ) { + foreach ( $clear_parent_ids as $clear_id ) { + + $children_by_price = get_posts( array( + 'post_parent' => $clear_id, + 'orderby' => 'meta_value_num', + 'order' => 'asc', + 'meta_key' => '_price', + 'posts_per_page' => 1, + 'post_type' => 'product', + 'fields' => 'ids' + ) ); + + if ( $children_by_price ) { + foreach ( $children_by_price as $child ) { + $child_price = get_post_meta( $child, '_price', true ); + update_post_meta( $clear_id, '_price', $child_price ); + } + } + } + } + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + update_post_meta( $product->id, '_sold_individually', true === $request['sold_individually'] ? 'yes' : '' ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = get_post_meta( $product->id, '_stock_status', true ); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $manage_stock = ( true === $request['manage_stock'] ) ? 'yes' : 'no'; + update_post_meta( $product->id, '_manage_stock', $manage_stock ); + } else { + $manage_stock = get_post_meta( $product->id, '_manage_stock', true ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $backorders = $request['backorders']; + update_post_meta( $product->id, '_backorders', $backorders ); + } else { + $backorders = get_post_meta( $product->id, '_backorders', true ); + } + + if ( 'grouped' === $product_type ) { + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', 'no' ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, $stock_status ); + } elseif ( 'external' === $product_type ) { + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', 'no' ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, 'instock' ); + } elseif ( 'variable' === $product_type ) { + update_post_meta( $product->id, '_stock', '' ); + } elseif ( 'yes' === $manage_stock ) { + update_post_meta( $product->id, '_backorders', $backorders ); + + wc_update_product_stock_status( $product->id, $stock_status ); + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + wc_update_product_stock( $product->id, wc_stock_amount( $request['stock_quantity'] ) ); + } else if ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( get_post_meta( $product->id, '_stock', true ) ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + + wc_update_product_stock( $product->id, wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', $backorders ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, $stock_status ); + } + + } elseif ( 'variable' !== $product_type ) { + wc_update_product_stock_status( $product->id, $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; + } + } + + update_post_meta( $product->id, '_upsell_ids', $upsells ); + } else { + delete_post_meta( $product->id, '_upsell_ids' ); + } + } + + // 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; + } + } + + update_post_meta( $product->id, '_crosssell_ids', $crosssells ); + } else { + delete_post_meta( $product->id, '_crosssell_ids' ); + } + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $is_downloadable = true === $request['downloadable'] ? 'yes' : 'no'; + update_post_meta( $product->id, '_downloadable', $is_downloadable ); + } else { + $is_downloadable = get_post_meta( $product->id, '_downloadable', true ); + } + + // Downloadable options. + if ( 'yes' === $is_downloadable ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + update_post_meta( $product->id, '_download_limit', ( '' === $request['download_limit'] ) ? '' : absint( $request['download_limit'] ) ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + update_post_meta( $product->id, '_download_expiry', ( '' === $request['download_expiry'] ) ? '' : absint( $request['download_expiry'] ) ); + } + + // Download type. + if ( isset( $request['download_type'] ) ) { + update_post_meta( $product->id, '_download_type', wc_clean( $request['download_type'] ) ); + } + } + + // Product url and button text for external products. + if ( 'external' === $product_type ) { + if ( isset( $request['external_url'] ) ) { + update_post_meta( $product->id, '_product_url', wc_clean( $request['external_url'] ) ); + } + + if ( isset( $request['button_text'] ) ) { + update_post_meta( $product->id, '_button_text', wc_clean( $request['button_text'] ) ); + } + } + + return true; + } + + /** + * Save variations. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_variations_data( $product, $request ) { + global $wpdb; + + $variations = $request['variations']; + $attributes = (array) maybe_unserialize( get_post_meta( $product->id, '_product_attributes', true ) ); + + foreach ( $variations as $menu_order => $variation ) { + $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; + + // Generate a useful post title. + $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $product->id ) ) ); + + // Update or Add post. + if ( ! $variation_id ) { + $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; + + $new_variation = array( + 'post_title' => $variation_post_title, + 'post_content' => '', + 'post_status' => $post_status, + 'post_author' => get_current_user_id(), + 'post_parent' => $product->id, + 'post_type' => 'product_variation', + 'menu_order' => $menu_order, + ); + + $variation_id = wp_insert_post( $new_variation ); + + do_action( 'woocommerce_create_product_variation', $variation_id ); + } else { + $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); + if ( isset( $variation['visible'] ) ) { + $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; + $update_variation['post_status'] = $post_status; + } + + $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); + + do_action( 'woocommerce_update_product_variation', $variation_id ); + } + + // Stop with we don't have a variation ID. + if ( is_wp_error( $variation_id ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_save_product_variation', $variation_id->get_error_message(), 400 ); + } + + // SKU. + if ( isset( $variation['sku'] ) ) { + $sku = get_post_meta( $variation_id, '_sku', true ); + $new_sku = wc_clean( $variation['sku'] ); + + if ( '' === $new_sku ) { + update_post_meta( $variation_id, '_sku', '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); + if ( ! $unique_sku ) { + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + update_post_meta( $variation_id, '_sku', $new_sku ); + } + } else { + update_post_meta( $variation_id, '_sku', '' ); + } + } + } + + // Thumbnail. + if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { + $image = current( $variation['image'] ); + if ( $image && is_array( $image ) ) { + if ( isset( $image['position'] ) && isset( $image['src'] ) && 0 === $image['position'] ) { + $upload = wc_rest_upload_image_from_url( wc_clean( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->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 title if present. + if ( ! empty( $image['title'] ) ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + + update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); + } + } else { + delete_post_meta( $variation_id, '_thumbnail_id' ); + } + } + + // Virtual variation. + if ( isset( $variation['virtual'] ) ) { + $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_virtual', $is_virtual ); + } + + // Downloadable variation. + if ( isset( $variation['downloadable'] ) ) { + $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_downloadable', $is_downloadable ); + } else { + $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); + } + + // Shipping data. + $this->save_product_shipping_data( $variation_id, $variation ); + + // Stock handling. + if ( isset( $variation['manage_stock'] ) ) { + $manage_stock = ( true === $variation['manage_stock'] ) ? 'yes' : 'no'; + } else { + $manage_stock = get_post_meta( $variation_id, '_manage_stock', true ); + } + + update_post_meta( $variation_id, '_manage_stock', '' === $manage_stock ? 'no' : $manage_stock ); + + if ( isset( $variation['in_stock'] ) ) { + $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = get_post_meta( $variation_id, '_stock_status', true ); + } + + wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status ); + + if ( 'yes' === $manage_stock ) { + $backorders = get_post_meta( $variation_id, '_backorders', true ); + + if ( isset( $variation['backorders'] ) ) { + $backorders = $variation['backorders']; + } + + update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders ); + + if ( isset( $variation['stock_quantity'] ) ) { + wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) ); + } else if ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( get_post_meta( $variation_id, '_stock', true ) ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + + wc_update_product_stock( $variation_id, wc_stock_amount( $stock_quantity ) ); + } + } else { + delete_post_meta( $variation_id, '_backorders' ); + delete_post_meta( $variation_id, '_stock' ); + } + + // Regular Price. + if ( isset( $variation['regular_price'] ) ) { + $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price']; + } else { + $regular_price = get_post_meta( $variation_id, '_regular_price', true ); + } + + // Sale Price. + if ( isset( $variation['sale_price'] ) ) { + $sale_price = ( '' === $variation['sale_price'] ) ? '' : $variation['sale_price']; + } else { + $sale_price = get_post_meta( $variation_id, '_sale_price', true ); + } + + if ( isset( $variation['date_on_sale_from'] ) ) { + $date_from = $variation['date_on_sale_from']; + } else { + $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true ); + $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); + } + + if ( isset( $variation['date_on_sale_to'] ) ) { + $date_to = $variation['date_on_sale_to']; + } else { + $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true ); + $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); + } + + _wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to ); + + // Tax class. + if ( isset( $variation['tax_class'] ) ) { + if ( $variation['tax_class'] !== 'parent' ) { + update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); + } else { + delete_post_meta( $variation_id, '_tax_class' ); + } + } + + // Downloads. + if ( 'yes' === $is_downloadable ) { + // Downloadable files. + if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { + $this->save_downloadable_files( $product->id, $variation['downloads'], $variation_id ); + } + + // Download limit. + if ( isset( $variation['download_limit'] ) ) { + $download_limit = absint( $variation['download_limit'] ); + update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit ); + } + + // Download expiry. + if ( isset( $variation['download_expiry'] ) ) { + $download_expiry = absint( $variation['download_expiry'] ); + update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry ); + } + } else { + update_post_meta( $variation_id, '_download_limit', '' ); + update_post_meta( $variation_id, '_download_expiry', '' ); + update_post_meta( $variation_id, '_downloadable_files', '' ); + } + + // Description. + if ( isset( $variation['description'] ) ) { + update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) ); + } + + // Update taxonomies. + if ( isset( $variation['attributes'] ) ) { + $updated_attribute_keys = array(); + + foreach ( $variation['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 = 'attribute_' . sanitize_title( $_attribute['name'] ); + $updated_attribute_keys[] = $_attribute_key; + + 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'] ) ) : ''; + } + + update_post_meta( $variation_id, $_attribute_key, $_attribute_value ); + } + } + + // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. + $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); + + foreach ( $delete_attribute_keys as $key ) { + delete_post_meta( $variation_id, $key ); + } + } + + do_action( 'woocommerce_rest_save_product_variation', $variation_id, $menu_order, $variation ); + } + + // Update parent if variable so price sorting works and stays in sync with the cheapest child. + WC_Product_Variable::sync( $product->id ); + + // 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'] ) ) { + $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; + } + } + } + } + + update_post_meta( $product->id, '_default_attributes', $default_attributes ); + } + + return true; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + try { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $this->save_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $this->save_product_meta( $product, $request ); + + // Save variations. + if ( isset( $request['type'] ) && 'variable' === $request['type'] && isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations_data( $product, $request ); + } + + return true; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + try { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $this->save_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $this->save_product_meta( $product, $request ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations_data( $product, $request ); + } else { + // Just sync variations. + WC_Product_Variable::sync( $product->id ); + WC_Product_Variable::sync_stock_status( $product->id ); + } + } + + return true; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear cache/transients. + * + * @param WP_Post $post Post data. + */ + public function clear_transients( $post ) { + wc_delete_product_transients( $post->ID ); + } + + /** + * Delete post. + * + * @param WP_Post $post + */ + protected function delete_post( $post ) { + // Delete product attachments. + $attachments = get_children( array( + 'post_parent' => $post->ID, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product. + wp_delete_post( $post->ID, true ); + } + + /** + * Get the Product'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, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_keys( get_post_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( 'End data of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'File MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Amount of times the product can be downloaded.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days that the customer has up to be able to download the product.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + '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', 'application', 'music' ), + '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 a product is on backorder.', '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( + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), get_option( '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', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + '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' ), + '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' ), + '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' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + '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' ), + 'properties' => array( + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + 'slug' => array( + 'description' => __( 'Attribute slug.', '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' ), + 'properties' => array( + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Attribute slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected term name of the attribute.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + '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' => __( 'Product 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 product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( 'End data of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + '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, + ), + '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' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'File MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Amount of times the product can be downloaded.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days that the customer has up to be able to download the product.', '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 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 a product is on backorder.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'weight' => array( + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), get_option( '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' => __( 'Varition image data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + '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' ), + 'properties' => array( + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + 'slug' => array( + 'description' => __( 'Attribute slug.', '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' ), + ), + ), + ), + ), + ), + 'grouped_products_ids' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + '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['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), array_keys( get_post_statuses() ) ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['type'] = array( + 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_types() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( + 'description' => __( 'Limit result set to products assigned a specific category.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + '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 (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-report-sales-controller.php b/includes/api/class-wc-rest-report-sales-controller.php new file mode 100644 index 00000000000..fa2c5e94f78 --- /dev/null +++ b/includes/api/class-wc-rest-report-sales-controller.php @@ -0,0 +1,395 @@ +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' => 'object', + '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( + 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'date_max' => array( + 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + ); + } +} diff --git a/includes/api/class-wc-rest-report-top-sellers-controller.php b/includes/api/class-wc-rest-report-top-sellers-controller.php new file mode 100644 index 00000000000..5fee090d14b --- /dev/null +++ b/includes/api/class-wc-rest-report-top-sellers-controller.php @@ -0,0 +1,174 @@ + $request['period'], + 'date_min' => $request['date_min'], + 'date_max' => $request['date_max'], + ); + $this->setup_report( $filter ); + + $report_data = $this->report->get_order_report_data( array( + 'data' => array( + '_product_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => '', + 'name' => 'product_id', + ), + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ) + ), + 'order_by' => 'order_item_qty DESC', + 'group_by' => 'product_id', + 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + $top_sellers = array(); + + foreach ( $report_data as $item ) { + $product = wc_get_product( $item->product_id ); + + if ( $product ) { + $top_sellers[] = array( + 'title' => $product->get_title(), + '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( + 'title' => $top_seller->title, + '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( + 'title' => array( + 'description' => __( 'Product title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Total number of purchases.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-reports-controller.php b/includes/api/class-wc-rest-reports-controller.php new file mode 100644 index 00000000000..1a2062e08f6 --- /dev/null +++ b/includes/api/class-wc-rest-reports-controller.php @@ -0,0 +1,174 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read reports. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all reports. + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $reports = array( + array( + 'slug' => 'sales', + 'description' => __( 'List of sales reports.', 'woocommerce' ), + ), + array( + 'slug' => 'top_sellers', + 'description' => __( 'List of top sellers products.', 'woocommerce' ), + ), + ); + + foreach ( $reports as $report ) { + $item = $this->prepare_item_for_response( (object) $report, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'description' => $report->description, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ) ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human-readable description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/class-wc-rest-tax-classes-controller.php b/includes/api/class-wc-rest-tax-classes-controller.php new file mode 100644 index 00000000000..046090f275b --- /dev/null +++ b/includes/api/class-wc-rest-tax-classes-controller.php @@ -0,0 +1,356 @@ +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( + 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' => __( '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 tax classes.', '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 boolean + */ + 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 boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all tax classes. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $tax_classes = array(); + + // Add standard class. + $tax_classes[] = array( + 'slug' => 'standard', + 'name' => __( 'Standard Rate', 'woocommerce' ), + ); + + $classes = WC_Tax::get_tax_classes(); + + foreach ( $classes as $class ) { + $tax_classes[] = array( + 'slug' => sanitize_title( $class ), + 'name' => $class, + ); + } + + $data = array(); + foreach ( $tax_classes as $tax_class ) { + $class = $this->prepare_item_for_response( $tax_class, $request ); + $class = $this->prepare_response_for_collection( $class ); + $data[] = $class; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $exists = false; + $classes = WC_Tax::get_tax_classes(); + $tax_class = array( + 'slug' => sanitize_title( $request['name'] ), + 'name' => $request['name'], + ); + + // Check if class exists. + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $tax_class['slug'] ) { + $exists = true; + break; + } + } + + // Return error if tax class already exists. + if ( $exists ) { + return new WP_Error( 'woocommerce_rest_tax_class_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Add the new class. + $classes[] = $tax_class['name']; + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + $this->update_additional_fields_for_object( $tax_class, $request ); + + /** + * Fires after a tax class is created or updated via the REST API. + * + * @param stdClass $tax_class Data used to create the tax class. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax class, false when updating tax class. + */ + do_action( 'woocommerce_rest_insert_tax_class', (object) $tax_class, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax_class, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $tax_class['slug'] ) ) ); + + return $response; + } + + /** + * Delete a single tax class. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $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_class = array( + 'slug' => sanitize_title( $request['slug'] ), + 'name' => '', + ); + $classes = WC_Tax::get_tax_classes(); + $deleted = false; + + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $tax_class['slug'] ) { + $tax_class['name'] = $class; + unset( $classes[ $key ] ); + $deleted = true; + break; + } + } + + if ( ! $deleted ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + // Delete tax rate locations locations from the selected class. + $wpdb->query( $wpdb->prepare( " + DELETE locations.* + FROM {$wpdb->prefix}woocommerce_tax_rate_locations AS locations + INNER JOIN + {$wpdb->prefix}woocommerce_tax_rates AS rates + ON rates.tax_rate_id = locations.tax_rate_id + WHERE rates.tax_rate_class = '%s' + ", $tax_class['slug'] ) ); + + // Delete tax rates in the selected class. + $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_class' => $tax_class['slug'] ), array( '%s' ) ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax_class, $request ); + + /** + * Fires after a tax class is deleted via the REST API. + * + * @param stdClass $tax_class The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', (object) $tax_class, $response, $request ); + + return $response; + } + + /** + * Prepare a single tax class output for response. + * + * @param array $tax_class Tax class data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax_class, $request ) { + $data = $tax_class; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links() ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax_class Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, (object) $tax_class, $request ); + } + + /** + * Prepare links for the request. + * + * @return array Links for the given tax class. + */ + protected function prepare_links() { + $links = array( + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Tax Classes schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax_class', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Tax class name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/class-wc-rest-taxes-controller.php b/includes/api/class-wc-rest-taxes-controller.php new file mode 100644 index 00000000000..530319e40e4 --- /dev/null +++ b/includes/api/class-wc-rest-taxes-controller.php @@ -0,0 +1,671 @@ +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( + 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' => __( '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 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 taxes.', '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 boolean + */ + 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 boolean + */ + 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 boolean + */ + 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 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['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' => 'tax_rate_id', + 'order' => 'tax_rate_order', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['class'] = $request['class']; + + /** + * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. + * + * @param array $prepared_args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); + + $query = " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE 1 = 1 + "; + + // Filter by tax class. + if ( ! empty( $prepared_args['class'] ) ) { + $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; + $query .= " AND tax_rate_class = '$class'"; + } + + // Order tax rates. + $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) ); + + // Pagination. + $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] ); + + // Query taxes. + $results = $wpdb->get_results( $query . $order_by . $pagination ); + + $taxes = array(); + foreach ( $results as $tax ) { + $data = $this->prepare_item_for_response( $tax, $request ); + $taxes[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $taxes ); + + // Store pagation values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + // Query only for ids. + $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); + + // Calcule totals. + $total_taxes = (int) $wpdb->num_rows; + $response->header( 'X-WP-Total', (int) $total_taxes ); + $max_pages = ceil( $total_taxes / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * 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 ) ); + } + + $data = array( + 'tax_rate_country' => $request['country'], + 'tax_rate_state' => $request['state'], + 'tax_rate' => $request['rate'], + 'tax_rate_name' => $request['name'], + 'tax_rate_priority' => (int) $request['priority'], + 'tax_rate_compound' => (int) $request['compound'], + 'tax_rate_shipping' => (int) $request['shipping'], + 'tax_rate_order' => (int) $request['order'], + 'tax_rate_class' => 'standard' !== $request['class'] ? $request['class'] : '', + ); + + // Create tax rate. + $id = WC_Tax::_insert_tax_rate( $data ); + + // Add locales. + if ( ! empty( $request['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); + } + if ( ! empty( $request['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + $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, $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']; + $current_tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $current_tax ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $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 ) { + $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); + + if ( ! isset( $request[ $key ] ) ) { + continue; + } + + $value = $request[ $key ]; + + // Fix compund and shipping values. + if ( in_array( $key, array( 'compound', 'shipping' ) ) ) { + $value = (int) $request[ $key ]; + } + + // Test new data against current data. + if ( $current_tax->$field === $value ) { + continue; + } + + $data[ $field ] = $request[ $key ]; + } + + // Update tax rate. + WC_Tax::_update_tax_rate( $id, $data ); + + // Update locales. + if ( ! isset( $request['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); + } + + if ( ! isset( $request['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + + return $response; + } + + /** + * Delete a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a tax is deleted via the REST API. + * + * @param stdClass $tax The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); + + return $response; + } + + /** + * Prepare a single tax output for response. + * + * @param stdClass $tax Tax object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax, $request ) { + global $wpdb; + + $id = (int) $tax->tax_rate_id; + $data = array( + 'id' => $id, + 'country' => $tax->tax_rate_country, + 'state' => $tax->tax_rate_state, + 'postcode' => '', + 'city' => '', + 'rate' => $tax->tax_rate, + 'name' => $tax->tax_rate_name, + 'priority' => (int) $tax->tax_rate_priority, + 'compound' => (bool) $tax->tax_rate_compound, + 'shipping' => (bool) $tax->tax_rate_shipping, + 'order' => (int) $tax->tax_rate_order, + 'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard', + ); + + // Get locales from a tax rate. + $locales = $wpdb->get_results( $wpdb->prepare( " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", $id ) ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $data[ $locale->location_type ] = $locale->location_code; + } + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $tax ) ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $tax Tax object. + * @return array Links for the given tax. + */ + protected function prepare_links( $tax ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Taxes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'country' => array( + 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'State code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postcode/ZIP.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'rate' => array( + 'description' => __( 'Tax rate.', 'woocommerce' ), + 'type' => 'float', + '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' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), + '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 = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + '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' => 'order', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'order', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['class'] = array( + 'description' => __( 'Sort by tax class.', 'woocommerce' ), + 'enum' => array_merge( array( 'standard' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), + 'sanitize_callback' => 'sanitize_title', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php new file mode 100644 index 00000000000..55948b7d726 --- /dev/null +++ b/includes/api/class-wc-rest-webhook-deliveries.php @@ -0,0 +1,307 @@ +/deliveries endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Webhook Deliveries controller class. + * + * @package WooCommerce/API + * @extends WP_REST_Controller + */ +class WC_REST_Webhook_Deliveries_Controller extends WP_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( + 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( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'shop_webhook', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a webhook develivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['webhook_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'shop_webhook', 'read', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all webhook deliveries. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $webhook = new WC_Webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $logs = $webhook->get_delivery_logs(); + + $data = array(); + foreach ( $logs as $log ) { + $delivery = $this->prepare_item_for_response( (object) $log, $request ); + $delivery = $this->prepare_response_for_collection( $delivery ); + $data[] = $delivery; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single webhook delivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $webhook = new WC_Webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $log = $webhook->get_delivery_log( $id ); + + if ( empty( $id ) || empty( $log ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( (object) $log, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + /** + * Prepare a single webhook delivery output for response. + * + * @param stdClass $log Delivery log object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $log, $request ) { + $data = (array) $log; + + // Add timestamp. + $data['date_created'] = wc_rest_prepare_date_response( $log->comment->comment_date_gmt ); + + // Remove comment object. + unset( $data['comment'] ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $log ) ); + + /** + * Filter webhook delivery object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $log Delivery log object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $log Delivery log object. + * @return array Links for the given webhook delivery. + */ + protected function prepare_links( $log ) { + $webhook_id = (int) $log->request_headers['X-WC-Webhook-ID']; + $base = str_replace( '(?P[\d]+)', $webhook_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/webhooks/%d', $this->namespace, $webhook_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook_delivery', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'duration' => array( + 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'summary' => array( + 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_url' => array( + 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_headers' => array( + 'description' => __( '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, + ), + '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, + ), + 'response_body' => array( + 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/class-wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php new file mode 100644 index 00000000000..229eb15d47c --- /dev/null +++ b/includes/api/class-wc-rest-webhooks-controller.php @@ -0,0 +1,555 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for webhooks. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'topic' => array( + 'required' => true, + ), + 'delivery_url' => array( + 'required' => true, + ), + 'secret' => array( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 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' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * 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'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + // Validate topic. + if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Validate delivery URL. + if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $post->post_type = $this->post_type; + $post_id = wp_insert_post( $post, true ); + + if ( is_wp_error( $post_id ) ) { + + if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + $post->ID = $post_id; + + $webhook = new WC_Webhook( $post_id ); + + // Set topic. + $webhook->set_topic( $request['topic'] ); + + // Set delivery URL. + $webhook->set_delivery_url( $request['delivery_url'] ); + + // Set secret. + $webhook->set_secret( $request['secret'] ); + + // Set status. + if ( ! empty( $request['status'] ) ) { + $webhook->update_status( $request['status'] ); + } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); + + // Send ping. + $webhook->deliver_ping(); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return $response; + } + + /** + * Update a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $webhook = new WC_Webhook( $id ); + + // Update topic. + if ( ! empty( $request['topic'] ) ) { + if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { + $webhook->set_topic( $request['topic'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update delivery URL. + if ( ! empty( $request['delivery_url'] ) ) { + if ( wc_is_valid_url( $request['delivery_url'] ) ) { + $webhook->set_delivery_url( $request['delivery_url'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update secret. + if ( ! empty( $request['secret'] ) ) { + $webhook->set_secret( $request['secret'] ); + } + + // Update status. + if ( ! empty( $request['status'] ) ) { + $webhook->update_status( $request['status'] ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. + $post_id = wp_update_post( (array) $post, true ); + if ( is_wp_error( $post_id ) ) { + if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + $result = wp_delete_post( $id, true ); + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param object $post The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return $response; + } + + /** + * Prepare a single webhook for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + global $wpdb; + + $data = new stdClass; + + // Post ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + $schema = $this->get_item_schema(); + + // 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' ) ) ); + + // Post author. + $data->post_author = get_current_user_id(); + + // Post password. + $password = strlen( uniqid( 'webhook_' ) ); + $data->post_password = $password > 20 ? substr( $password, 0, 20 ) : $password; + + // Post status. + $data->post_status = 'publish'; + } else { + + // Allow edit post title. + if ( ! empty( $request['name'] ) ) { + $data->post_title = $request['name']; + } + } + + // Comment status. + $data->comment_status = 'closed'; + + // Ping status. + $data->ping_status = 'closed'; + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param stdClass $data An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + + /** + * Prepare a single webhook output for response. + * + * @param WP_Post $webhook Webhook object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $post, $request ) { + $id = (int) $post->ID; + $webhook = new WC_Webhook( $id ); + $data = array( + 'id' => $webhook->id, + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'date_created' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $post ) ); + + /** + * Filter webhook object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Webhook $webhook Webhook object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + switch ( $request['status'] ) { + case 'active' : + $args['post_status'] = 'publish'; + break; + case 'paused' : + $args['post_status'] = 'draft'; + break; + case 'disabled' : + $args['post_status'] = 'pending'; + break; + default : + $args['post_status'] = 'any'; + break; + } + + return $args; + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Webhook status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'active', + 'enum' => array( 'active', 'paused', 'disabled' ), + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wc_is_webhook_valid_topic', + ), + ), + 'topic' => array( + 'description' => __( 'Webhook topic.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'resource' => array( + 'description' => __( 'Webhook resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'event' => array( + 'description' => __( 'Webhook event.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'hooks' => array( + 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + '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 the current API user's consumer secret if not provided.", 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'edit' ), + 'writeonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'all', + 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'all', 'active', 'paused', 'disabled' ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-api-authentication.php b/includes/api/legacy/v1/class-wc-api-authentication.php similarity index 100% rename from includes/api/v1/class-wc-api-authentication.php rename to includes/api/legacy/v1/class-wc-api-authentication.php diff --git a/includes/api/v1/class-wc-api-coupons.php b/includes/api/legacy/v1/class-wc-api-coupons.php similarity index 100% rename from includes/api/v1/class-wc-api-coupons.php rename to includes/api/legacy/v1/class-wc-api-coupons.php diff --git a/includes/api/v1/class-wc-api-customers.php b/includes/api/legacy/v1/class-wc-api-customers.php similarity index 100% rename from includes/api/v1/class-wc-api-customers.php rename to includes/api/legacy/v1/class-wc-api-customers.php diff --git a/includes/api/v1/class-wc-api-json-handler.php b/includes/api/legacy/v1/class-wc-api-json-handler.php similarity index 100% rename from includes/api/v1/class-wc-api-json-handler.php rename to includes/api/legacy/v1/class-wc-api-json-handler.php diff --git a/includes/api/v1/class-wc-api-orders.php b/includes/api/legacy/v1/class-wc-api-orders.php similarity index 100% rename from includes/api/v1/class-wc-api-orders.php rename to includes/api/legacy/v1/class-wc-api-orders.php diff --git a/includes/api/v1/class-wc-api-products.php b/includes/api/legacy/v1/class-wc-api-products.php similarity index 100% rename from includes/api/v1/class-wc-api-products.php rename to includes/api/legacy/v1/class-wc-api-products.php diff --git a/includes/api/v1/class-wc-api-reports.php b/includes/api/legacy/v1/class-wc-api-reports.php similarity index 100% rename from includes/api/v1/class-wc-api-reports.php rename to includes/api/legacy/v1/class-wc-api-reports.php diff --git a/includes/api/v1/class-wc-api-resource.php b/includes/api/legacy/v1/class-wc-api-resource.php similarity index 100% rename from includes/api/v1/class-wc-api-resource.php rename to includes/api/legacy/v1/class-wc-api-resource.php diff --git a/includes/api/v1/class-wc-api-server.php b/includes/api/legacy/v1/class-wc-api-server.php similarity index 100% rename from includes/api/v1/class-wc-api-server.php rename to includes/api/legacy/v1/class-wc-api-server.php diff --git a/includes/api/v1/class-wc-api-xml-handler.php b/includes/api/legacy/v1/class-wc-api-xml-handler.php similarity index 100% rename from includes/api/v1/class-wc-api-xml-handler.php rename to includes/api/legacy/v1/class-wc-api-xml-handler.php diff --git a/includes/api/v1/interface-wc-api-handler.php b/includes/api/legacy/v1/interface-wc-api-handler.php similarity index 100% rename from includes/api/v1/interface-wc-api-handler.php rename to includes/api/legacy/v1/interface-wc-api-handler.php diff --git a/includes/api/v2/class-wc-api-authentication.php b/includes/api/legacy/v2/class-wc-api-authentication.php similarity index 100% rename from includes/api/v2/class-wc-api-authentication.php rename to includes/api/legacy/v2/class-wc-api-authentication.php diff --git a/includes/api/class-wc-api-coupons.php b/includes/api/legacy/v2/class-wc-api-coupons.php similarity index 100% rename from includes/api/class-wc-api-coupons.php rename to includes/api/legacy/v2/class-wc-api-coupons.php diff --git a/includes/api/v2/class-wc-api-customers.php b/includes/api/legacy/v2/class-wc-api-customers.php similarity index 100% rename from includes/api/v2/class-wc-api-customers.php rename to includes/api/legacy/v2/class-wc-api-customers.php diff --git a/includes/api/class-wc-api-exception.php b/includes/api/legacy/v2/class-wc-api-exception.php similarity index 100% rename from includes/api/class-wc-api-exception.php rename to includes/api/legacy/v2/class-wc-api-exception.php diff --git a/includes/api/class-wc-api-json-handler.php b/includes/api/legacy/v2/class-wc-api-json-handler.php similarity index 100% rename from includes/api/class-wc-api-json-handler.php rename to includes/api/legacy/v2/class-wc-api-json-handler.php diff --git a/includes/api/v2/class-wc-api-orders.php b/includes/api/legacy/v2/class-wc-api-orders.php similarity index 100% rename from includes/api/v2/class-wc-api-orders.php rename to includes/api/legacy/v2/class-wc-api-orders.php diff --git a/includes/api/v2/class-wc-api-products.php b/includes/api/legacy/v2/class-wc-api-products.php similarity index 100% rename from includes/api/v2/class-wc-api-products.php rename to includes/api/legacy/v2/class-wc-api-products.php diff --git a/includes/api/class-wc-api-reports.php b/includes/api/legacy/v2/class-wc-api-reports.php similarity index 100% rename from includes/api/class-wc-api-reports.php rename to includes/api/legacy/v2/class-wc-api-reports.php diff --git a/includes/api/v2/class-wc-api-resource.php b/includes/api/legacy/v2/class-wc-api-resource.php similarity index 100% rename from includes/api/v2/class-wc-api-resource.php rename to includes/api/legacy/v2/class-wc-api-resource.php diff --git a/includes/api/v2/class-wc-api-server.php b/includes/api/legacy/v2/class-wc-api-server.php similarity index 100% rename from includes/api/v2/class-wc-api-server.php rename to includes/api/legacy/v2/class-wc-api-server.php diff --git a/includes/api/v2/class-wc-api-webhooks.php b/includes/api/legacy/v2/class-wc-api-webhooks.php similarity index 100% rename from includes/api/v2/class-wc-api-webhooks.php rename to includes/api/legacy/v2/class-wc-api-webhooks.php diff --git a/includes/api/interface-wc-api-handler.php b/includes/api/legacy/v2/interface-wc-api-handler.php similarity index 100% rename from includes/api/interface-wc-api-handler.php rename to includes/api/legacy/v2/interface-wc-api-handler.php diff --git a/includes/api/class-wc-api-authentication.php b/includes/api/legacy/v3/class-wc-api-authentication.php similarity index 100% rename from includes/api/class-wc-api-authentication.php rename to includes/api/legacy/v3/class-wc-api-authentication.php diff --git a/includes/api/v2/class-wc-api-coupons.php b/includes/api/legacy/v3/class-wc-api-coupons.php similarity index 100% rename from includes/api/v2/class-wc-api-coupons.php rename to includes/api/legacy/v3/class-wc-api-coupons.php diff --git a/includes/api/class-wc-api-customers.php b/includes/api/legacy/v3/class-wc-api-customers.php similarity index 100% rename from includes/api/class-wc-api-customers.php rename to includes/api/legacy/v3/class-wc-api-customers.php diff --git a/includes/api/v2/class-wc-api-exception.php b/includes/api/legacy/v3/class-wc-api-exception.php similarity index 100% rename from includes/api/v2/class-wc-api-exception.php rename to includes/api/legacy/v3/class-wc-api-exception.php diff --git a/includes/api/v2/class-wc-api-json-handler.php b/includes/api/legacy/v3/class-wc-api-json-handler.php similarity index 100% rename from includes/api/v2/class-wc-api-json-handler.php rename to includes/api/legacy/v3/class-wc-api-json-handler.php diff --git a/includes/api/class-wc-api-orders.php b/includes/api/legacy/v3/class-wc-api-orders.php similarity index 100% rename from includes/api/class-wc-api-orders.php rename to includes/api/legacy/v3/class-wc-api-orders.php diff --git a/includes/api/class-wc-api-products.php b/includes/api/legacy/v3/class-wc-api-products.php similarity index 100% rename from includes/api/class-wc-api-products.php rename to includes/api/legacy/v3/class-wc-api-products.php diff --git a/includes/api/v2/class-wc-api-reports.php b/includes/api/legacy/v3/class-wc-api-reports.php similarity index 100% rename from includes/api/v2/class-wc-api-reports.php rename to includes/api/legacy/v3/class-wc-api-reports.php diff --git a/includes/api/class-wc-api-resource.php b/includes/api/legacy/v3/class-wc-api-resource.php similarity index 100% rename from includes/api/class-wc-api-resource.php rename to includes/api/legacy/v3/class-wc-api-resource.php diff --git a/includes/api/class-wc-api-server.php b/includes/api/legacy/v3/class-wc-api-server.php similarity index 100% rename from includes/api/class-wc-api-server.php rename to includes/api/legacy/v3/class-wc-api-server.php diff --git a/includes/api/class-wc-api-taxes.php b/includes/api/legacy/v3/class-wc-api-taxes.php similarity index 100% rename from includes/api/class-wc-api-taxes.php rename to includes/api/legacy/v3/class-wc-api-taxes.php diff --git a/includes/api/class-wc-api-webhooks.php b/includes/api/legacy/v3/class-wc-api-webhooks.php similarity index 100% rename from includes/api/class-wc-api-webhooks.php rename to includes/api/legacy/v3/class-wc-api-webhooks.php diff --git a/includes/api/v2/interface-wc-api-handler.php b/includes/api/legacy/v3/interface-wc-api-handler.php similarity index 100% rename from includes/api/v2/interface-wc-api-handler.php rename to includes/api/legacy/v3/interface-wc-api-handler.php diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 1ef5e831715..c4907860161 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -22,6 +22,7 @@ class WC_API { * This is the major version for the REST API and takes * first-order position in endpoint URLs. * + * @deprecated 2.6.0 * @var string */ const VERSION = '3.1.0'; @@ -29,6 +30,7 @@ class WC_API { /** * The REST API server. * + * @deprecated 2.6.0 * @var WC_API_Server */ public $server; @@ -36,6 +38,7 @@ class WC_API { /** * REST API authentication class instance. * + * @deprecated 2.6.0 * @var WC_API_Authentication */ public $authentication; @@ -47,20 +50,23 @@ class WC_API { * @return WC_API */ public function __construct() { - // add query vars + // Add query vars. add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); - // register API endpoints + // Register API endpoints. add_action( 'init', array( $this, 'add_endpoint' ), 0 ); - // handle REST API requests + // Handle REST API requests. add_action( 'parse_request', array( $this, 'handle_rest_api_requests' ), 0 ); - // handle wc-api endpoint requests + // Handle wc-api endpoint requests. add_action( 'parse_request', array( $this, 'handle_api_requests' ), 0 ); - // Ensure payment gateways are initialized in time for API requests + // Ensure payment gateways are initialized in time for API requests. add_action( 'woocommerce_api_request', array( 'WC_Payment_Gateways', 'instance' ), 0 ); + + // WP REST API. + $this->rest_api_init(); } /** @@ -72,8 +78,9 @@ class WC_API { */ public function add_query_vars( $vars ) { $vars[] = 'wc-api'; - $vars[] = 'wc-api-version'; - $vars[] = 'wc-api-route'; + $vars[] = 'wc-api-version'; // Deprecated since 2.6.0. + $vars[] = 'wc-api-route'; // Deprecated since 2.6.0. + return $vars; } @@ -84,11 +91,11 @@ class WC_API { */ public static function add_endpoint() { - // REST API + // REST API, deprecated since 2.6.0. add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' ); add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' ); - // WC API for payment gateway IPNs, etc + // WC API for payment gateway IPNs, etc. add_rewrite_endpoint( 'wc-api', EP_ALL ); } @@ -97,6 +104,7 @@ class WC_API { * Handle REST API requests. * * @since 2.2 + * @deprecated 2.6.0 */ public function handle_rest_api_requests() { global $wp; @@ -109,13 +117,13 @@ class WC_API { $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; } - // REST API request + // REST API request. if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) { define( 'WC_API_REQUEST', true ); define( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) ); - // legacy v1 API request + // Legacy v1 API request. if ( 1 === WC_API_REQUEST_VERSION ) { $this->handle_v1_rest_api_request(); } else if ( 2 === WC_API_REQUEST_VERSION ) { @@ -125,10 +133,10 @@ class WC_API { $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] ); - // load API resource classes + // load API resource classes. $this->register_resources( $this->server ); - // Fire off the request + // Fire off the request. $this->server->serve_request(); } @@ -140,29 +148,30 @@ class WC_API { * Include required files for REST API request. * * @since 2.1 + * @deprecated 2.6.0 */ public function includes() { - // API server / response handlers - include_once( 'api/class-wc-api-exception.php' ); - include_once( 'api/class-wc-api-server.php' ); - include_once( 'api/interface-wc-api-handler.php' ); - include_once( 'api/class-wc-api-json-handler.php' ); + // API server / response handlers. + include_once( 'api/legacy/v3/class-wc-api-exception.php' ); + include_once( 'api/legacy/v3/class-wc-api-server.php' ); + include_once( 'api/legacy/v3/interface-wc-api-handler.php' ); + include_once( 'api/legacy/v3/class-wc-api-json-handler.php' ); - // authentication - include_once( 'api/class-wc-api-authentication.php' ); + // Authentication. + include_once( 'api/legacy/v3/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); - include_once( 'api/class-wc-api-resource.php' ); - include_once( 'api/class-wc-api-coupons.php' ); - include_once( 'api/class-wc-api-customers.php' ); - include_once( 'api/class-wc-api-orders.php' ); - include_once( 'api/class-wc-api-products.php' ); - include_once( 'api/class-wc-api-reports.php' ); - include_once( 'api/class-wc-api-taxes.php' ); - include_once( 'api/class-wc-api-webhooks.php' ); + include_once( 'api/legacy/v3/class-wc-api-resource.php' ); + include_once( 'api/legacy/v3/class-wc-api-coupons.php' ); + include_once( 'api/legacy/v3/class-wc-api-customers.php' ); + include_once( 'api/legacy/v3/class-wc-api-orders.php' ); + include_once( 'api/legacy/v3/class-wc-api-products.php' ); + include_once( 'api/legacy/v3/class-wc-api-reports.php' ); + include_once( 'api/legacy/v3/class-wc-api-taxes.php' ); + include_once( 'api/legacy/v3/class-wc-api-webhooks.php' ); - // allow plugins to load other response handlers or resource classes + // Allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); } @@ -170,6 +179,7 @@ class WC_API { * Register available API resources. * * @since 2.1 + * @deprecated 2.6.0 * @param WC_API_Server $server the REST server */ public function register_resources( $server ) { @@ -196,31 +206,32 @@ class WC_API { * 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( 'api/v1/class-wc-api-server.php' ); - include_once( 'api/v1/interface-wc-api-handler.php' ); - include_once( 'api/v1/class-wc-api-json-handler.php' ); - include_once( 'api/v1/class-wc-api-xml-handler.php' ); + // Include legacy required files for v1 REST API request. + include_once( 'api/legacy/v1/class-wc-api-server.php' ); + include_once( 'api/legacy/v1/interface-wc-api-handler.php' ); + include_once( 'api/legacy/v1/class-wc-api-json-handler.php' ); + include_once( 'api/legacy/v1/class-wc-api-xml-handler.php' ); - include_once( 'api/v1/class-wc-api-authentication.php' ); + include_once( 'api/legacy/v1/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); - include_once( 'api/v1/class-wc-api-resource.php' ); - include_once( 'api/v1/class-wc-api-coupons.php' ); - include_once( 'api/v1/class-wc-api-customers.php' ); - include_once( 'api/v1/class-wc-api-orders.php' ); - include_once( 'api/v1/class-wc-api-products.php' ); - include_once( 'api/v1/class-wc-api-reports.php' ); + include_once( 'api/legacy/v1/class-wc-api-resource.php' ); + include_once( 'api/legacy/v1/class-wc-api-coupons.php' ); + include_once( 'api/legacy/v1/class-wc-api-customers.php' ); + include_once( 'api/legacy/v1/class-wc-api-orders.php' ); + include_once( 'api/legacy/v1/class-wc-api-products.php' ); + include_once( 'api/legacy/v1/class-wc-api-reports.php' ); - // allow plugins to load other response handlers or resource classes + // 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 + // Register available resources for legacy v1 REST API request. $api_classes = apply_filters( 'woocommerce_api_classes', array( 'WC_API_Customers', @@ -235,7 +246,7 @@ class WC_API { $this->$api_class = new $api_class( $this->server ); } - // Fire off the request + // Fire off the request. $this->server->serve_request(); } @@ -243,30 +254,31 @@ class WC_API { * Handle legacy v2 REST API requests. * * @since 2.4 + * @deprecated 2.6.0 */ private function handle_v2_rest_api_request() { - include_once( 'api/v2/class-wc-api-exception.php' ); - include_once( 'api/v2/class-wc-api-server.php' ); - include_once( 'api/v2/interface-wc-api-handler.php' ); - include_once( 'api/v2/class-wc-api-json-handler.php' ); + include_once( 'api/legacy/v2/class-wc-api-exception.php' ); + include_once( 'api/legacy/v2/class-wc-api-server.php' ); + include_once( 'api/legacy/v2/interface-wc-api-handler.php' ); + include_once( 'api/legacy/v2/class-wc-api-json-handler.php' ); - include_once( 'api/v2/class-wc-api-authentication.php' ); + include_once( 'api/legacy/v2/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); - include_once( 'api/v2/class-wc-api-resource.php' ); - include_once( 'api/v2/class-wc-api-coupons.php' ); - include_once( 'api/v2/class-wc-api-customers.php' ); - include_once( 'api/v2/class-wc-api-orders.php' ); - include_once( 'api/v2/class-wc-api-products.php' ); - include_once( 'api/v2/class-wc-api-reports.php' ); - include_once( 'api/v2/class-wc-api-webhooks.php' ); + include_once( 'api/legacy/v2/class-wc-api-resource.php' ); + include_once( 'api/legacy/v2/class-wc-api-coupons.php' ); + include_once( 'api/legacy/v2/class-wc-api-customers.php' ); + include_once( 'api/legacy/v2/class-wc-api-orders.php' ); + include_once( 'api/legacy/v2/class-wc-api-products.php' ); + include_once( 'api/legacy/v2/class-wc-api-reports.php' ); + include_once( 'api/legacy/v2/class-wc-api-webhooks.php' ); - // allow plugins to load other response handlers or resource classes + // 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 + // Register available resources for legacy v2 REST API request. $api_classes = apply_filters( 'woocommerce_api_classes', array( 'WC_API_Customers', @@ -282,15 +294,15 @@ class WC_API { $this->$api_class = new $api_class( $this->server ); } - // Fire off the request + // Fire off the request. $this->server->serve_request(); } /** * API request - Trigger any API requests. * - * @since 2.0 - * @version 2.4 + * @since 2.0 + * @version 2.4 */ public function handle_api_requests() { global $wp; @@ -299,30 +311,132 @@ class WC_API { $wp->query_vars['wc-api'] = $_GET['wc-api']; } - // wc-api endpoint requests + // wc-api endpoint requests. if ( ! empty( $wp->query_vars['wc-api'] ) ) { - // Buffer, we won't want any output here + // Buffer, we won't want any output here. ob_start(); - // No cache headers + // No cache headers. nocache_headers(); - // Clean the API request + // Clean the API request. $api_request = strtolower( wc_clean( $wp->query_vars['wc-api'] ) ); - // Trigger generic action before request hook + // Trigger generic action before request hook. do_action( 'woocommerce_api_request', $api_request ); - // Is there actually something hooked into this API request? If not trigger 400 - Bad request + // Is there actually something hooked into this API request? If not trigger 400 - Bad request. status_header( has_action( 'woocommerce_api_' . $api_request ) ? 200 : 400 ); - // Trigger an action which plugins can hook into to fulfill the request + // Trigger an action which plugins can hook into to fulfill the request. do_action( 'woocommerce_api_' . $api_request ); - // Done, clear buffer and exit + // Done, clear buffer and exit. ob_end_clean(); - die('-1'); + die( '-1' ); + } + } + + /** + * Init WP REST API. + * + * @since 2.6.0 + */ + private function rest_api_init() { + global $wp_version; + + // REST API was included starting WordPress 4.4. + if ( version_compare( $wp_version, 4.4, '<' ) ) { + return; + } + + $this->rest_api_includes(); + + // Init REST API routes. + add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); + } + + /** + * Include REST API classes. + * + * @since 2.6.0 + */ + private function rest_api_includes() { + // Exception handler. + if ( ! class_exists( 'WC_API_Exception' ) ) { + include_once( 'api/legacy/v3/class-wc-api-exception.php' ); + } + include_once( 'api/class-wc-rest-exception.php' ); + + // Authentication. + include_once( 'api/class-wc-rest-authentication.php' ); + + // WP-API classes and functions. + include_once( 'vendor/wp-rest-functions.php' ); + if ( ! class_exists( 'WP_REST_Controller' ) ) { + include_once( 'vendor/class-wp-rest-controller.php' ); + } + + // Abstract controllers. + include_once( 'abstracts/abstract-wc-rest-posts-controller.php' ); + include_once( 'abstracts/abstract-wc-rest-terms-controller.php' ); + + // REST API controllers. + include_once( 'api/class-wc-rest-coupons-controller.php' ); + include_once( 'api/class-wc-rest-customer-downloads-controller.php' ); + include_once( 'api/class-wc-rest-customers-controller.php' ); + include_once( 'api/class-wc-rest-order-notes-controller.php' ); + include_once( 'api/class-wc-rest-order-refunds-controller.php' ); + include_once( 'api/class-wc-rest-orders-controller.php' ); + include_once( 'api/class-wc-rest-product-attribute-terms-controller.php' ); + include_once( 'api/class-wc-rest-product-attributes-controller.php' ); + include_once( 'api/class-wc-rest-product-categories-controller.php' ); + include_once( 'api/class-wc-rest-product-reviews-controller.php' ); + include_once( 'api/class-wc-rest-product-shipping-classes-controller.php' ); + include_once( 'api/class-wc-rest-product-tags-controller.php' ); + include_once( 'api/class-wc-rest-products-controller.php' ); + include_once( 'api/class-wc-rest-report-sales-controller.php' ); + include_once( 'api/class-wc-rest-report-top-sellers-controller.php' ); + include_once( 'api/class-wc-rest-reports-controller.php' ); + include_once( 'api/class-wc-rest-tax-classes-controller.php' ); + include_once( 'api/class-wc-rest-taxes-controller.php' ); + include_once( 'api/class-wc-rest-webhook-deliveries.php' ); + include_once( 'api/class-wc-rest-webhooks-controller.php' ); + } + + /** + * Register REST API routes. + * + * @since 2.6.0 + */ + public function register_rest_routes() { + $controllers = array( + 'WC_REST_Coupons_Controller', + 'WC_REST_Customer_Downloads_Controller', + 'WC_REST_Customers_Controller', + 'WC_REST_Order_Notes_Controller', + 'WC_REST_Order_Refunds_Controller', + 'WC_REST_Orders_Controller', + 'WC_REST_Product_Attribute_Terms_Controller', + 'WC_REST_Product_Attributes_Controller', + 'WC_REST_Product_Categories_Controller', + 'WC_REST_Product_Reviews_Controller', + 'WC_REST_Product_Shipping_Classes_Controller', + 'WC_REST_Product_Tags_Controller', + 'WC_REST_Products_Controller', + 'WC_REST_Report_Sales_Controller', + 'WC_REST_Report_Top_Sellers_Controller', + 'WC_REST_Reports_Controller', + 'WC_REST_Tax_Classes_Controller', + 'WC_REST_Taxes_Controller', + 'WC_REST_Webhook_Deliveries_Controller', + 'WC_REST_Webhooks_Controller', + ); + + foreach ( $controllers as $controller ) { + $this->$controller = new $controller(); + $this->$controller->register_routes(); } } } diff --git a/includes/class-wc-auth.php b/includes/class-wc-auth.php index b64274bf26d..2276771995f 100644 --- a/includes/class-wc-auth.php +++ b/includes/class-wc-auth.php @@ -134,14 +134,29 @@ class WC_Auth { $url = wc_get_endpoint_url( 'wc-auth/v' . self::VERSION, $endpoint, home_url( '/' ) ); return add_query_arg( array( - 'app_name' => wc_clean( $data['app_name'] ), - 'user_id' => wc_clean( $data['user_id'] ), - 'return_url' => urlencode( $data['return_url'] ), - 'callback_url' => urlencode( $data['callback_url'] ), - 'scope' => wc_clean( $data['scope'] ), + 'app_name' => wc_clean( $data['app_name'] ), + 'user_id' => wc_clean( $data['user_id'] ), + 'return_url' => urlencode( $this->get_formatted_url( $data['return_url'] ) ), + 'callback_url' => urlencode( $this->get_formatted_url( $data['callback_url'] ) ), + 'scope' => wc_clean( $data['scope'] ), ), $url ); } + /** + * Decode and format a URL. + * @param string $url + * @return array + */ + protected function get_formatted_url( $url ) { + $url = urldecode( $url ); + + if ( ! strstr( $url, '://' ) ) { + $url = 'https://' . $url; + } + + return $url; + } + /** * Make validation. * @@ -167,12 +182,16 @@ class WC_Auth { } foreach ( array( 'return_url', 'callback_url' ) as $param ) { - if ( false === filter_var( urldecode( $_REQUEST[ $param ] ), FILTER_VALIDATE_URL ) ) { + $param = $this->get_formatted_url( $_REQUEST[ $param ] ); + + if ( false === filter_var( $param, FILTER_VALIDATE_URL ) ) { throw new Exception( sprintf( __( 'The %s is not a valid URL', 'woocommerce' ), $param ) ); } } - if ( 0 !== stripos( urldecode( $_REQUEST['callback_url'] ), 'https://' ) ) { + $callback_url = $this->get_formatted_url( $_REQUEST['callback_url'] ); + + if ( 0 !== stripos( $callback_url, 'https://' ) ) { throw new Exception( __( 'The callback_url need to be over SSL', 'woocommerce' ) ); } } @@ -248,7 +267,7 @@ class WC_Auth { ) ); - $response = wp_safe_remote_post( esc_url_raw( urldecode( $url ) ), $params ); + $response = wp_safe_remote_post( esc_url_raw( $url ), $params ); if ( is_wp_error( $response ) ) { throw new Exception( $response->get_error_message() ); @@ -305,7 +324,7 @@ class WC_Auth { if ( 'login' == $route && ! is_user_logged_in() ) { wc_get_template( 'auth/form-login.php', array( 'app_name' => $_REQUEST['app_name'], - 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), urldecode( $_REQUEST['return_url'] ) ), + 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ), 'redirect_url' => $this->build_url( $_REQUEST, 'authorize' ), ) ); @@ -325,7 +344,7 @@ class WC_Auth { } else if ( 'authorize' == $route && current_user_can( 'manage_woocommerce' ) ) { wc_get_template( 'auth/form-grant-access.php', array( 'app_name' => $_REQUEST['app_name'], - 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), urldecode( $_REQUEST['return_url'] ) ), + 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ), 'scope' => $this->get_i18n_scope( wc_clean( $_REQUEST['scope'] ) ), 'permissions' => $this->get_permissions_in_scope( wc_clean( $_REQUEST['scope'] ) ), 'granted_url' => wp_nonce_url( $this->build_url( $_REQUEST, 'access_granted' ), 'wc_auth_grant_access', 'wc_auth_nonce' ), @@ -341,10 +360,10 @@ class WC_Auth { } $consumer_data = $this->create_keys( $_REQUEST['app_name'], $_REQUEST['user_id'], $_REQUEST['scope'] ); - $response = $this->post_consumer_data( $consumer_data, $_REQUEST['callback_url'] ); + $response = $this->post_consumer_data( $consumer_data, $this->get_formatted_url( $_REQUEST['callback_url'] ) ); if ( $response ) { - wp_redirect( esc_url_raw( add_query_arg( array( 'success' => 1, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), urldecode( $_REQUEST['return_url'] ) ) ) ); + wp_redirect( esc_url_raw( add_query_arg( array( 'success' => 1, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ) ) ); exit; } } else { diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php index 3ea1676c154..4d8f01ffd2b 100644 --- a/includes/class-wc-install.php +++ b/includes/class-wc-install.php @@ -41,6 +41,7 @@ class WC_Install { add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 ); add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) ); add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) ); + add_action( 'woocommerce_plugin_background_installer', array( __CLASS__, 'background_installer' ), 10, 2 ); } /** @@ -820,6 +821,151 @@ CREATE TABLE {$wpdb->prefix}woocommerce_termmeta ( return $tables; } + + /** + * Get slug from path + * @param string $key + * @return string + */ + private static function format_plugin_slug( $key ) { + $slug = explode( '/', $key ); + $slug = explode( '.', end( $slug ) ); + return $slug[0]; + } + + /** + * Install a plugin from .org in the background via a cron job (used by + * installer - opt in). + * @param string $plugin_to_install_id + * @param array $plugin_to_install + * @since 2.6.0 + */ + public static function background_installer( $plugin_to_install_id, $plugin_to_install ) { + if ( ! empty( $plugin_to_install['repo-slug'] ) ) { + require_once( ABSPATH . 'wp-admin/includes/file.php' ); + require_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); + require_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin; + $upgrader = new WP_Upgrader( $skin ); + $installed_plugins = array_map( array( __CLASS__, 'format_plugin_slug' ), array_keys( get_plugins() ) ); + $plugin_slug = $plugin_to_install['repo-slug']; + $plugin = $plugin_slug . '/' . $plugin_slug . '.php'; + $installed = false; + $activate = false; + + // See if the plugin is installed already + if ( in_array( $plugin_to_install['repo-slug'], $installed_plugins ) ) { + $installed = true; + $activate = ! is_plugin_active( $plugin ); + } + + // Install this thing! + if ( ! $installed ) { + // Suppress feedback + ob_start(); + + try { + $plugin_information = plugins_api( 'plugin_information', array( + 'slug' => $plugin_to_install['repo-slug'], + 'fields' => array( + 'short_description' => false, + 'sections' => false, + 'requires' => false, + 'rating' => false, + 'ratings' => false, + 'downloaded' => false, + 'last_updated' => false, + 'added' => false, + 'tags' => false, + 'homepage' => false, + 'donate_link' => false, + 'author_profile' => false, + 'author' => false, + ), + ) ); + + if ( is_wp_error( $plugin_information ) ) { + throw new Exception( $plugin_information->get_error_message() ); + } + + $package = $plugin_information->download_link; + $download = $upgrader->download_package( $package ); + + if ( is_wp_error( $download ) ) { + throw new Exception( $download->get_error_message() ); + } + + $working_dir = $upgrader->unpack_package( $download, true ); + + if ( is_wp_error( $working_dir ) ) { + throw new Exception( $working_dir->get_error_message() ); + } + + $result = $upgrader->install_package( array( + 'source' => $working_dir, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => false, + 'abort_if_destination_exists' => false, + 'clear_working' => true, + 'hook_extra' => array( + 'type' => 'plugin', + 'action' => 'install', + ), + ) ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + $activate = true; + + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + __( '%s could not be installed (%s). %sPlease install it manually by clicking here.%s', 'woocommerce' ), + $plugin_to_install['name'], + $e->getMessage(), + '', + '' + ) + ); + } + + // Discard feedback + ob_end_clean(); + } + + wp_clean_plugins_cache(); + + // Activate this thing + if ( $activate ) { + try { + $result = activate_plugin( $plugin ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + __( '%s could not be activated (%s). %sPlease activate it manually via the plugins screen.%s', 'woocommerce' ), + $plugin_to_install['name'], + $e->getMessage() . '"' . $plugin . '"', + '', + '' + ) + ); + } + } + } + } } WC_Install::init(); diff --git a/includes/class-wc-logger.php b/includes/class-wc-logger.php index fb48c63680f..d9a8eb8f831 100644 --- a/includes/class-wc-logger.php +++ b/includes/class-wc-logger.php @@ -7,11 +7,11 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Allows log files to be written to for debugging purposes * - * @class WC_Logger - * @version 1.6.4 - * @package WooCommerce/Classes - * @category Class - * @author WooThemes + * @class WC_Logger + * @version 1.6.4 + * @package WooCommerce/Classes + * @category Class + * @author WooThemes */ class WC_Logger { @@ -30,7 +30,6 @@ class WC_Logger { $this->_handles = array(); } - /** * Destructor. */ @@ -40,54 +39,75 @@ class WC_Logger { } } - /** * Open log file for writing. * * @access private - * @param mixed $handle + * + * @param string $handle + * @param string $mode + * * @return bool success */ - private function open( $handle ) { + private function open( $handle, $mode = 'a' ) { if ( isset( $this->_handles[ $handle ] ) ) { return true; } - if ( $this->_handles[ $handle ] = @fopen( wc_get_log_file_path( $handle ), 'a' ) ) { + if ( $this->_handles[ $handle ] = @fopen( wc_get_log_file_path( $handle ), $mode ) ) { return true; } return false; } - /** * Add a log entry to chosen file. * * @param string $handle * @param string $message + * + * @return bool */ public function add( $handle, $message ) { + $result = false; + if ( $this->open( $handle ) && is_resource( $this->_handles[ $handle ] ) ) { - $time = date_i18n( 'm-d-Y @ H:i:s -' ); // Grab Time - @fwrite( $this->_handles[ $handle ], $time . " " . $message . "\n" ); + $time = date_i18n( 'm-d-Y @ H:i:s -' ); // Grab Time + $result = @fwrite( $this->_handles[ $handle ], $time . " " . $message . "\n" ); } do_action( 'woocommerce_log_add', $handle, $message ); - } + return false !== $result; + } /** * Clear entries from chosen file. * - * @param mixed $handle + * @param string $handle + * + * @return bool */ public function clear( $handle ) { - if ( $this->open( $handle ) && is_resource( $this->_handles[ $handle ] ) ) { - @ftruncate( $this->_handles[ $handle ], 0 ); + $result = false; + + // Close the file if it's already open. + if ( is_resource( $this->_handles[ $handle ] ) ) { + @fclose( $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; } } diff --git a/includes/class-wc-order-item-meta.php b/includes/class-wc-order-item-meta.php index 522ec449aeb..941b728e07d 100644 --- a/includes/class-wc-order-item-meta.php +++ b/includes/class-wc-order-item-meta.php @@ -116,24 +116,26 @@ class WC_Order_Item_Meta { 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 ) ) { + 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 ); + $term = get_term_by( 'slug', $meta_value, $attribute_key ); + if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { - $meta->value = $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 ), + 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value ), ); } } diff --git a/includes/class-wc-payment-gateways.php b/includes/class-wc-payment-gateways.php index eb20438d925..4afccce6d91 100644 --- a/includes/class-wc-payment-gateways.php +++ b/includes/class-wc-payment-gateways.php @@ -78,17 +78,18 @@ class WC_Payment_Gateways { 'WC_Gateway_Paypal', ); - $simplify_countries = (array) apply_filters( 'woocommerce_gateway_simplify_commerce_supported_countries', array( 'US', 'IE' ) ); + /** + * Simplify Commerce is @deprecated in 2.6.0. Only load when enabled. + */ + if ( ! class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) && in_array( WC()->countries->get_base_country(), apply_filters( 'woocommerce_gateway_simplify_commerce_supported_countries', array( 'US', 'IE' ) ) ) ) { + $simplify_options = get_option( 'woocommerce_simplify_commerce_settings', array() ); - if ( in_array( WC()->countries->get_base_country(), $simplify_countries ) ) { - if ( class_exists( 'WC_Subscriptions_Order' ) || class_exists( 'WC_Pre_Orders_Order' ) ) { - if ( ! function_exists( 'wcs_create_renewal_order' ) ) { // Subscriptions < 2.0 - $load_gateways[] = 'WC_Addons_Gateway_Simplify_Commerce_Deprecated'; - } else { + if ( ! empty( $simplify_options['enabled'] ) && 'yes' === $simplify_options['enabled'] ) { + if ( function_exists( 'wcs_create_renewal_order' ) ) { $load_gateways[] = 'WC_Addons_Gateway_Simplify_Commerce'; + } else { + $load_gateways[] = 'WC_Gateway_Simplify_Commerce'; } - } else { - $load_gateways[] = 'WC_Gateway_Simplify_Commerce'; } } diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php index 0085ab37480..3f6144db8b7 100644 --- a/includes/class-wc-tax.php +++ b/includes/class-wc-tax.php @@ -727,18 +727,19 @@ class WC_Tax { * @since 2.5.0 * @access private * - * @param int $tax_rate_id + * @param int $tax_rate_id + * @param string $output_type * * @return array */ - public static function _get_tax_rate( $tax_rate_id ) { + 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 ), ARRAY_A ); + ", $tax_rate_id ), $output_type ); } /** diff --git a/includes/gateways/simplify-commerce-deprecated/class-wc-addons-gateway-simplify-commerce-deprecated.php b/includes/gateways/simplify-commerce-deprecated/class-wc-addons-gateway-simplify-commerce-deprecated.php deleted file mode 100644 index ef7b4e89397..00000000000 --- a/includes/gateways/simplify-commerce-deprecated/class-wc-addons-gateway-simplify-commerce-deprecated.php +++ /dev/null @@ -1,177 +0,0 @@ -id, array( $this, 'process_scheduled_subscription_payment' ), 10, 3 ); - add_filter( 'woocommerce_subscriptions_renewal_order_meta_query', array( $this, 'remove_renewal_order_meta' ), 10, 4 ); - add_action( 'woocommerce_subscriptions_changed_failing_payment_method_' . $this->id, array( $this, 'change_failing_payment_method' ), 10, 3 ); - } - } - - /** - * Store the customer and card IDs on the order and subscriptions in the order. - * - * @param int $order_id - * @param string $customer_id - * @return array - */ - protected function save_subscription_meta( $order_id, $customer_id ) { - update_post_meta( $order_id, '_simplify_customer_id', wc_clean( $customer_id ) ); - } - - /** - * process_subscription_payment function. - * - * @param WC_order $order - * @param integer $amount (default: 0) - * @uses Simplify_BadRequestException - * @return bool|WP_Error - */ - public function process_subscription_payment( $order, $amount = 0 ) { - if ( 0 == $amount ) { - // Payment complete - $order->payment_complete(); - - return true; - } - - if ( $amount * 100 < 50 ) { - return new WP_Error( 'simplify_error', __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce' ) ); - } - - $order_items = $order->get_items(); - $order_item = array_shift( $order_items ); - $subscription_name = sprintf( __( '%s - Subscription for "%s"', 'woocommerce' ), esc_html( get_bloginfo( 'name', 'display' ) ), $order_item['name'] ) . ' ' . sprintf( __( '(Order #%s)', 'woocommerce' ), $order->get_order_number() ); - - $customer_id = get_post_meta( $order->id, '_simplify_customer_id', true ); - - if ( ! $customer_id ) { - return new WP_Error( 'simplify_error', __( 'Customer not found', 'woocommerce' ) ); - } - - try { - // Charge the customer - $payment = Simplify_Payment::createPayment( array( - 'amount' => $amount * 100, // In cents. - 'customer' => $customer_id, - 'description' => trim( substr( $subscription_name, 0, 1024 ) ), - 'currency' => strtoupper( get_woocommerce_currency() ), - 'reference' => $order->id, - 'card.addressCity' => $order->billing_city, - 'card.addressCountry' => $order->billing_country, - 'card.addressLine1' => $order->billing_address_1, - 'card.addressLine2' => $order->billing_address_2, - 'card.addressState' => $order->billing_state, - 'card.addressZip' => $order->billing_postcode - ) ); - - } catch ( Exception $e ) { - - $error_message = $e->getMessage(); - - if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { - $error_message = ''; - foreach ( $e->getFieldErrors() as $error ) { - $error_message .= ' ' . $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')'; - } - } - - $order->add_order_note( sprintf( __( 'Simplify payment error: %s', 'woocommerce' ), $error_message ) ); - - return new WP_Error( 'simplify_payment_declined', $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - - if ( 'APPROVED' == $payment->paymentStatus ) { - // Payment complete - $order->payment_complete( $payment->id ); - - // Add order note - $order->add_order_note( sprintf( __( 'Simplify payment approved (ID: %s, Auth Code: %s)', 'woocommerce' ), $payment->id, $payment->authCode ) ); - - return true; - } else { - $order->add_order_note( __( 'Simplify payment declined', 'woocommerce' ) ); - - return new WP_Error( 'simplify_payment_declined', __( 'Payment was declined - please try another card.', 'woocommerce' ) ); - } - } - - /** - * process_scheduled_subscription_payment function. - * - * @param float $amount_to_charge The amount to charge. - * @param WC_Order $order The WC_Order object of the order which the subscription was purchased in. - * @param int $product_id The ID of the subscription product for which this payment relates. - */ - public function process_scheduled_subscription_payment( $amount_to_charge, $order, $product_id ) { - $result = $this->process_subscription_payment( $order, $amount_to_charge ); - - if ( is_wp_error( $result ) ) { - WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $order, $product_id ); - } else { - WC_Subscriptions_Manager::process_subscription_payments_on_order( $order ); - } - } - - /** - * Don't transfer customer meta when creating a parent renewal order. - * - * @param string $order_meta_query MySQL query for pulling the metadata - * @param int $original_order_id Post ID of the order being used to purchased the subscription being renewed - * @param int $renewal_order_id Post ID of the order created for renewing the subscription - * @param string $new_order_role The role the renewal order is taking, one of 'parent' or 'child' - * @return string - */ - public function remove_renewal_order_meta( $order_meta_query, $original_order_id, $renewal_order_id, $new_order_role ) { - if ( 'parent' == $new_order_role ) { - $order_meta_query .= " AND `meta_key` NOT LIKE '_simplify_customer_id' "; - } - - return $order_meta_query; - } - - /** - * Check if order contains subscriptions. - * - * @param int $order_id - * @return bool - */ - protected function order_contains_subscription( $order_id ) { - return class_exists( 'WC_Subscriptions_Order' ) && ( WC_Subscriptions_Order::order_contains_subscription( $order_id ) || WC_Subscriptions_Renewal_Order::is_renewal( $order_id ) ); - } - - /** - * Update the customer_id for a subscription after using Simplify to complete a payment to make up for. - * an automatic renewal payment which previously failed. - * - * @param WC_Order $original_order The original order in which the subscription was purchased. - * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment). - * @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key() - */ - public function change_failing_payment_method( $original_order, $renewal_order, $subscription_key ) { - $new_customer_id = get_post_meta( $renewal_order->id, '_simplify_customer_id', true ); - - update_post_meta( $original_order->id, '_simplify_customer_id', $new_customer_id ); - } -} diff --git a/includes/vendor/class-wp-rest-controller.php b/includes/vendor/class-wp-rest-controller.php new file mode 100644 index 00000000000..4d54072c36e --- /dev/null +++ b/includes/vendor/class-wp-rest-controller.php @@ -0,0 +1,478 @@ + 405 ) ); + } + + /** + * Get a collection of items. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Check if a given request has access to get a specific item. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Get one item from the collection. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Check if a given request has access to create items. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Create one item from the collection. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Check if a given request has access to update a specific item. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Update one item from the collection. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Check if a given request has access to delete a specific item. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Delete one item from the collection. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepare the item for create or update operation. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|object $prepared_item + */ + protected function prepare_item_for_database( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepare the item for the REST response. + * + * @param mixed $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepare a response for inserting into a collection. + * + * @param WP_REST_Response $response Response object. + * @return array Response data, ready for insertion into collection data. + */ + public function prepare_response_for_collection( $response ) { + if ( ! ( $response instanceof WP_REST_Response ) ) { + return $response; + } + + $data = (array) $response->get_data(); + $links = WP_REST_Server::get_response_links( $response ); + if ( ! empty( $links ) ) { + $data['_links'] = $links; + } + + return $data; + } + + /** + * Filter a response based on the context defined in the schema. + * + * @param array $data + * @param string $context + * @return array + */ + public function filter_response_by_context( $data, $context ) { + + $schema = $this->get_item_schema(); + foreach ( $data as $key => $value ) { + if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) { + continue; + } + + if ( ! in_array( $context, $schema['properties'][ $key ]['context'] ) ) { + unset( $data[ $key ] ); + } + + if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) { + foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) { + if ( empty( $details['context'] ) ) { + continue; + } + if ( ! in_array( $context, $details['context'] ) ) { + if ( isset( $data[ $key ][ $attribute ] ) ) { + unset( $data[ $key ][ $attribute ] ); + } + } + } + } + } + + return $data; + } + + /** + * Get the item's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + return $this->add_additional_fields_schema( array() ); + } + + /** + * Get the item's schema for display / public consumption purposes. + * + * @return array + */ + public function get_public_item_schema() { + + $schema = $this->get_item_schema(); + + foreach ( $schema['properties'] as &$property ) { + if ( isset( $property['arg_options'] ) ) { + unset( $property['arg_options'] ); + } + } + + return $schema; + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param(), + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ), + 'search' => array( + 'description' => 'Limit results to those matching a string.', + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + ); + } + + /** + * Get the magical context param. + * + * Ensures consistent description between endpoints, and populates enum from schema. + * + * @param array $args + * @return array + */ + public function get_context_param( $args = array() ) { + $param_details = array( + 'description' => 'Scope under which the request is made; determines fields present in response.', + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $schema = $this->get_item_schema(); + if ( empty( $schema['properties'] ) ) { + return array_merge( $param_details, $args ); + } + $contexts = array(); + foreach ( $schema['properties'] as $key => $attributes ) { + if ( ! empty( $attributes['context'] ) ) { + $contexts = array_merge( $contexts, $attributes['context'] ); + } + } + if ( ! empty( $contexts ) ) { + $param_details['enum'] = array_unique( $contexts ); + rsort( $param_details['enum'] ); + } + return array_merge( $param_details, $args ); + } + + /** + * Add the values from additional fields to a data object. + * + * @param array $object + * @param WP_REST_Request $request + * @return array modified object with additional fields. + */ + protected function add_additional_fields_to_object( $object, $request ) { + + $additional_fields = $this->get_additional_fields(); + + foreach ( $additional_fields as $field_name => $field_options ) { + + if ( ! $field_options['get_callback'] ) { + continue; + } + + $object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() ); + } + + return $object; + } + + /** + * Update the values of additional fields added to a data object. + * + * @param array $object + * @param WP_REST_Request $request + */ + protected function update_additional_fields_for_object( $object, $request ) { + + $additional_fields = $this->get_additional_fields(); + + foreach ( $additional_fields as $field_name => $field_options ) { + + if ( ! $field_options['update_callback'] ) { + continue; + } + + // Don't run the update callbacks if the data wasn't passed in the request. + if ( ! isset( $request[ $field_name ] ) ) { + continue; + } + + call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() ); + } + } + + /** + * 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. + */ + 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']; + } + + return $schema; + } + + /** + * Get all the registered additional fields for a given object-type. + * + * @param string $object_type + * @return array + */ + protected function get_additional_fields( $object_type = null ) { + + if ( ! $object_type ) { + $object_type = $this->get_object_type(); + } + + if ( ! $object_type ) { + return array(); + } + + global $wp_rest_additional_fields; + + if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) { + return array(); + } + + return $wp_rest_additional_fields[ $object_type ]; + } + + /** + * Get the object type this controller is responsible for managing. + * + * @return string + */ + protected function get_object_type() { + $schema = $this->get_item_schema(); + + if ( ! $schema || ! isset( $schema['title'] ) ) { + return null; + } + + return $schema['title']; + } + + /** + * Get an array of endpoint arguments from the item schema for the controller. + * + * @param string $method HTTP method of the request. The arguments + * for `CREATABLE` requests are checked for required + * values and may fall-back to a given default, this + * is not done on `EDITABLE` requests. Default is + * WP_REST_Server::CREATABLE. + * @return array $endpoint_args + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + + $schema = $this->get_item_schema(); + $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array(); + $endpoint_args = array(); + + foreach ( $schema_properties as $field_id => $params ) { + + // Arguments specified as `readonly` are not allowed to be set. + if ( ! empty( $params['readonly'] ) ) { + continue; + } + + $endpoint_args[ $field_id ] = array( + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'rest_sanitize_request_arg', + ); + + if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) { + $endpoint_args[ $field_id ]['default'] = $params['default']; + } + + if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) { + $endpoint_args[ $field_id ]['required'] = true; + } + + foreach ( array( 'type', 'format', 'enum' ) as $schema_prop ) { + if ( isset( $params[ $schema_prop ] ) ) { + $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ]; + } + } + + // Merge in any options provided by the schema property. + if ( isset( $params['arg_options'] ) ) { + + // Only use required / default from arg_options on CREATABLE endpoints. + if ( WP_REST_Server::CREATABLE !== $method ) { + $params['arg_options'] = array_diff_key( $params['arg_options'], array( 'required' => '', 'default' => '' ) ); + } + + $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] ); + } + } + + return $endpoint_args; + } + +} diff --git a/includes/vendor/wp-rest-functions.php b/includes/vendor/wp-rest-functions.php new file mode 100644 index 00000000000..c5d1a4a6f3b --- /dev/null +++ b/includes/vendor/wp-rest-functions.php @@ -0,0 +1,198 @@ + null, + 'update_callback' => null, + 'schema' => null, + ); + + $args = wp_parse_args( $args, $defaults ); + + global $wp_rest_additional_fields; + + $object_types = (array) $object_type; + + foreach ( $object_types as $object_type ) { + $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args; + } + } +} + +if ( ! function_exists( 'register_api_field' ) ) { + /** + * Backwards compat shim + */ + function register_api_field( $object_type, $attributes, $args = array() ) { + _deprecated_function( 'register_api_field', 'WPAPI-2.0', 'register_rest_field' ); + register_rest_field( $object_type, $attributes, $args ); + } +} + +if ( ! function_exists( 'rest_validate_request_arg' ) ) { + /** + * Validate a request argument based on details registered to the route. + * + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param + * @return WP_Error|boolean + */ + function rest_validate_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 ( ! empty( $args['enum'] ) ) { + if ( ! in_array( $value, $args['enum'] ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not one of %s', 'woocommerce' ), $param, implode( ', ', $args['enum'] ) ) ); + } + } + + if ( 'integer' === $args['type'] && ! is_numeric( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s', 'woocommerce' ), $param, 'integer' ) ); + } + + if ( 'string' === $args['type'] && ! is_string( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s', 'woocommerce' ), $param, 'string' ) ); + } + + if ( isset( $args['format'] ) ) { + switch ( $args['format'] ) { + case 'date-time' : + if ( ! rest_parse_date( $value ) ) { + return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.', 'woocommerce' ) ); + } + break; + + case 'email' : + if ( ! is_email( $value ) ) { + return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.', 'woocommerce' ) ); + } + break; + } + } + + if ( in_array( $args['type'], array( 'numeric', 'integer' ) ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) { + if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) { + if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be greater than %d (exclusive)', 'woocommerce' ), $param, $args['minimum'] ) ); + } else if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be greater than %d (inclusive)', 'woocommerce' ), $param, $args['minimum'] ) ); + } + } else if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { + if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be less than %d (exclusive)', 'woocommerce' ), $param, $args['maximum'] ) ); + } else if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be less than %d (inclusive)', 'woocommerce' ), $param, $args['maximum'] ) ); + } + } else if ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) { + if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { + if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (exclusive) and %d (exclusive)', 'woocommerce' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } else if ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { + if ( $value >= $args['maximum'] || $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (inclusive) and %d (exclusive)', 'woocommerce' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } else if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { + if ( $value > $args['maximum'] || $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (exclusive) and %d (inclusive)', 'woocommerce' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } else if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { + if ( $value > $args['maximum'] || $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (inclusive) and %d (inclusive)', 'woocommerce' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } + } + } + + return true; + } +} + +if ( ! function_exists( 'rest_sanitize_request_arg' ) ) { + /** + * Sanitize a request argument based on details registered to the route. + * + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param + * @return mixed + */ + function rest_sanitize_request_arg( $value, $request, $param ) { + + $attributes = $request->get_attributes(); + if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { + return $value; + } + $args = $attributes['args'][ $param ]; + + if ( 'integer' === $args['type'] ) { + return (int) $value; + } + + if ( isset( $args['format'] ) ) { + switch ( $args['format'] ) { + case 'date-time' : + return sanitize_text_field( $value ); + + case 'email' : + /* + * sanitize_email() validates, which would be unexpected + */ + return sanitize_text_field( $value ); + + case 'uri' : + return esc_url_raw( $value ); + } + } + + return $value; + } + +} diff --git a/includes/wc-core-functions.php b/includes/wc-core-functions.php index 3d36a586270..5274fdc0ac4 100644 --- a/includes/wc-core-functions.php +++ b/includes/wc-core-functions.php @@ -26,6 +26,7 @@ include( 'wc-product-functions.php' ); include( 'wc-account-functions.php' ); include( 'wc-term-functions.php' ); include( 'wc-attribute-functions.php' ); +include( 'wc-rest-functions.php' ); /** * Filters on data used in admin and frontend. diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php new file mode 100644 index 00000000000..17112578306 --- /dev/null +++ b/includes/wc-rest-functions.php @@ -0,0 +1,295 @@ + 400 ) ); + } + + // Ensure url is valid. + $image_url = esc_url_raw( $image_url ); + + // Get the file. + $response = wp_safe_remote_get( $image_url, array( + 'timeout' => 10 + ) ); + + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + return new WP_Error( 'woocommerce_rest_invalid_remote_image_url', sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); + } + + // Ensure we have a file name and type. + if ( ! $wp_filetype['type'] ) { + $headers = wp_remote_retrieve_headers( $response ); + if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) { + $disposition = end( explode( 'filename=', $headers['content-disposition'] ) ); + $disposition = sanitize_file_name( $disposition ); + $file_name = $disposition; + } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) { + $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] ); + } + unset( $headers ); + } + + // Upload the file. + $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) ); + + if ( $upload['error'] ) { + return new WP_Error( 'woocommerce_rest_image_upload_error', $upload['error'], array( 'status' => 400 ) ); + } + + // Get filesize. + $filesize = filesize( $upload['file'] ); + + if ( 0 == $filesize ) { + @unlink( $upload['file'] ); + unset( $upload ); + + return new WP_Error( 'woocommerce_rest_image_upload_file_error', __( 'Zero size file downloaded.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + do_action( 'woocommerce_rest_api_uploaded_image_from_url', $upload, $image_url ); + + return $upload; +} + +/** + * 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' ); + } + + if ( $image_meta = wp_read_image_metadata( $upload['file'] ) ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = $image_meta['title']; + } + if ( trim( $image_meta['caption'] ) ) { + $content = $image_meta['caption']; + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title, + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; +} + +/** + * Validate reports request arguments. + * + * @since 2.6.0 + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param + * @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 ) ) { + return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%s is not of type %s.', 'woocommerce' ), $param, 'string' ) ); + } + + if ( 'data' === $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 ); + } else { + // Percent symbols (%) must be double-encoded. + return str_replace( '%', '%25', rawurlencode( rawurldecode( $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', + ); + + 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' => 'edit_users', + 'edit' => 'edit_users', + 'delete' => 'delete_users', + ); + + $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', + ); + + $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', + 'attributes' => 'manage_product_terms', + ); + + $permission = current_user_can( $objects[ $object ] ); + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, 0, $object ); +} + diff --git a/includes/wc-user-functions.php b/includes/wc-user-functions.php index a7fa0bda74d..3005692b3e1 100644 --- a/includes/wc-user-functions.php +++ b/includes/wc-user-functions.php @@ -622,3 +622,51 @@ function wc_set_user_last_update_time( $user_id ) { 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 ) { + global $wpdb; + + $customer_id = absint( $customer_id ); + + $id = $wpdb->get_var( "SELECT id + FROM $wpdb->posts AS posts + LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id + WHERE meta.meta_key = '_customer_user' + AND meta.meta_value = {$customer_id} + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) + ORDER BY posts.ID DESC + " ); + + return wc_get_order( $id ); +} + +/** + * Wrapper for @see get_avatar() which doesn't simply return + * the URL so we need to pluck it from the HTML img tag. + * + * Kudos to https://github.com/WP-API/WP-API for offering a better solution. + * + * @since 2.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 ) { + $avatar_html = get_avatar( $email ); + + // Get the URL of the avatar from the provided HTML. + preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches ); + + if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { + return esc_url_raw( $matches[1] ); + } + + return null; +} diff --git a/templates/order/order-details.php b/templates/order/order-details.php index 1f0085aa50c..d3cb56ebad9 100644 --- a/templates/order/order-details.php +++ b/templates/order/order-details.php @@ -13,7 +13,7 @@ * @see http://docs.woothemes.com/document/template-structure/ * @author WooThemes * @package WooCommerce/Templates - * @version 2.5.3 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -37,15 +37,14 @@ $show_customer_details = is_user_logged_in() && $order->get_user_id() === get_cu get_items() as $item_id => $item ) { $product = apply_filters( 'woocommerce_order_item_product', $order->get_product_from_item( $item ), $item ); - $purchase_note = get_post_meta( $product->id, '_purchase_note', true ); 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' => $purchase_note, - 'product' => $product, + 'order' => $order, + 'item_id' => $item_id, + 'item' => $item, + 'show_purchase_note' => $show_purchase_note, + 'purchase_note' => $product ? get_post_meta( $product->id, '_purchase_note', true ) : '', + 'product' => $product, ) ); } ?>