WooCommerce ShipStation Gateway - Version 4.1.23

Version Description

Download this release

Release Info

Developer bor0
Plugin Icon 128x128 WooCommerce ShipStation Gateway
Version 4.1.23
Comparing to
See all releases

Version 4.1.23

assets/css/admin.css ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .shipstation-logo {
2
+ width: 180px;
3
+ margin: 0 0 -16px -16px;
4
+ }
5
+
6
+ .shipstation-external-link:after {
7
+ font-family: "dashicons";
8
+ content: "\f504";
9
+ display: inline-block;
10
+ vertical-align: bottom;
11
+ }
assets/images/shipstation-logo-blue.png ADDED
Binary file
changelog.txt ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *** ShipStation for WooCommerce ***
2
+
3
+ = 2018-09-12 - version 4.1.23 =
4
+ * Fix - Use correct textdomain on some strings.
5
+ * Tweak - Rework settings notice to correctly provide setup instructions.
6
+ * Tweak - Coding standards and making the plugin ready for wordpress.org.
7
+
8
+ = 2018-05-24 - version 4.1.22 =
9
+ * Fix - Order timestamp issue.
10
+
11
+ = 2018-05-23 - version 4.1.21 =
12
+ * Fix - Privacy policy updates.
13
+
14
+ = 2018-05-23 - version 4.1.20 =
15
+ * Fix - Paid date not showing actual payment date, but Order Date instead.
16
+ * Update - Privacy policy notification.
17
+ * Update - Export/erasure hooks added.
18
+ * Update - WC 3.4 compatibility.
19
+
20
+ = 2017-12-15 - version 4.1.19 =
21
+ * Fix - WC 3.3 compatibility.
22
+
23
+ = 2017-07-18 - version 4.1.18 =
24
+ * Fix - Update the order status to complete if XML from ShipStation is not present in request's body. Also log the request information.
25
+ * Fix - Adjusted text domain for two strings so that they are now translateable.
26
+
27
+ = 2017-07-06 - version 4.1.17 =
28
+ * Fix - Issue when a server couldn't read ShipNotify's XML posted in request's body, nothing is updated in the order.
29
+ * Tweak - Added setting, docs, and support links in plugin action links.
30
+
31
+ = 2017-06-14 - version 4.1.16 =
32
+ * Fix - Issue where legacy code for converting sequential order numbers still used.
33
+ * Fix - Make sure to not count non shippable item when get notified from ShipStation.
34
+
35
+ = 2017-05-12 - version 4.1.15 =
36
+ * Fix - Ensure some orders from previous version of ShipStation are able to be found on notifications.
37
+
38
+ = 2017-05-11 - version 4.1.14 =
39
+ * Fix - Possible error when order is not found during shipment notification.
40
+ * Tweak - Order numbers are now sent via own XML field and will not display in invoice.
41
+
42
+ = 2017-05-05 - version 4.1.13 =
43
+ * Fix - WC30 date/time not displaying correctly.
44
+ * Fix - Tax amount discrenpancy when sent to Shipstation.
45
+ * Fix - When using split orders, order does not get updated in WooCommerce.
46
+ * Tweak - Sequential Numbers Pro compatibility.
47
+ * Add - Exported order note when the order has been exported.
48
+
49
+ = 2017-05-02 - version 4.1.12 =
50
+ * Fix - Product attributes not passing to Shipstation under certain conditions.
51
+
52
+ = 2017-05-01 - version 4.1.11 =
53
+ * Fix - Export error due to WC30 incompatibility.
54
+
55
+ = 2017-04-10 - version 4.1.10 =
56
+ * Fix - Allow additional characters to be used for shipping service name
57
+
58
+ = 2017-04-06 - version 4.1.9 =
59
+ * Fix - Additional updates for WC 3.0 compatibility
60
+
61
+ = 2017-04-03 - version 4.1.8 =
62
+ * Fix - PHP 7 compatibility
63
+ * Fix - Update for WC 3.0 compatibility
64
+
65
+ = 2016-10-03 - version 4.1.7 =
66
+ * Fix - Digital products are also sent through.
67
+ * Fix - Checkout add on fee not being sent through.
68
+
69
+ = 2016-08-15 - version 4.1.6 =
70
+ * Tweak - Added filter for ShipNotify order ID
71
+ * Tweak - Send payment method ShipStation
72
+ * Fix - Issue where fee items not be exported to ShipStation
73
+
74
+ = 2016-02-24 - version 4.1.5 =
75
+ * Fix - Compatibility issue with WC Order Status Manager
76
+
77
+ = 2016-01-25 - version 4.1.4 =
78
+ * Fix - Compatibility issue with woocommerce-sequential-order-numbers-pro version 1-9-0
79
+
80
+ = 2015-09-23 - version 4.1.3 =
81
+ * Fix - Allow copy/paste from API key field in firefox
82
+
83
+ = 2015-08-21 - version 4.1.2 =
84
+ * Fix - Send pre-discount unit price.
85
+
86
+ = 2015-08-06 - version 4.1.1 =
87
+ * Fix - Send UnitPrice as single product total-
88
+ * Tweak - Date parsing.
89
+
90
+ = 2015-06-24 - version 4.1.0 =
91
+ * Fix - Sanitize XML response.
92
+ * Fix - Prevent API requests being callable when not authenticated.
93
+ * Fix - Prevent caching.
94
+ * Tweak - Use hash_equals to compare keys.
95
+ * Tweak - Send total discount to ShipStation.
96
+
97
+ = 2015-05-12 - version 4.0.9 =
98
+ * Tweak - woocommerce_shipstation_export_order filter.
99
+ * Tweak - Exclude system notes.
100
+ * Tweak - Custom field value filters.
101
+
102
+ = 2015-04-03 - version 4.0.8 =
103
+ * Fix - Don't automatically set to $is_customer_note to true
104
+
105
+ = 2015-03-12 - version 4.0.7 =
106
+ * Check if $product exists before checking if needs_shipping in export.
107
+
108
+ = 2015-01-16 - version 4.0.6 =
109
+ * Send negative discount.
110
+
111
+ = 2015-01-08 - version 4.0.5 =
112
+ * Export query based on post_modified_gmt rather than post_date_gmt
113
+
114
+ = 2014-11-19 - version 4.0.4 =
115
+ * Fix compatibility with Sequential order numbers.
116
+
117
+ = 2014-11-13 - version 4.0.3 =
118
+ * Extra logging in ShipNotify.
119
+ * Fixed completing orders with multiple lines.
120
+
121
+ = 2014-11-13 - version 4.0.2 =
122
+ * Order results by date.
123
+ * Enforce minimum page 1.
124
+ * Removed check to see if orders need shipping to prevent issues with offset/max pages. Exports all orders.
125
+
126
+ = 2014-11-12 - version 4.0.1 =
127
+ * Added 'pages' node to XML feed so ShipStation knows how many pages of results are present.
128
+
129
+ = 2014-11-01 - version 4.0.0 =
130
+ * Completely refactored by WooThemes!
131
+ * Supports split orders (only completes the order once all items are shipped).
132
+ * Exports orders (from statuses you define).
133
+ * Excludes orders and items which do not require shipping.
134
+ * Simplified setup process; just requires an auth key.
135
+ * Exports order-level discounts as line items.
includes/api-requests/class-wc-safe-domdocument.php ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * Drop in replacement for DOMDocument that is secure against XML eXternal Entity (XXE) Injection.
8
+ * Bails if any DOCTYPE is found
9
+ *
10
+ * Comments in quotes come from the DOMDocument documentation: http://php.net/manual/en/class.domdocument.php
11
+ */
12
+ class WC_Safe_DOMDocument extends DOMDocument {
13
+ /**
14
+ * When called non-statically (as an object method) with malicious data, no Exception is thrown, but the object is emptied of all DOM nodes.
15
+ *
16
+ * @param string $filename The path to the XML document.
17
+ * @param int $options Bitwise OR of the libxml option constants. http://us3.php.net/manual/en/libxml.constants.php
18
+ *
19
+ * @return bool|DOMDocument true on success, false on failure. If called statically (E_STRICT error), returns DOMDocument on success.
20
+ */
21
+ public function load( $filename, $options = 0 ) {
22
+ if ( '' === $filename ) {
23
+ // "If an empty string is passed as the filename or an empty file is named, a warning will be generated."
24
+ // "This warning is not generated by libxml and cannot be handled using libxml's error handling functions."
25
+ trigger_error( 'WC_Safe_DOMDocument::load(): Empty string supplied as input', E_USER_WARNING );
26
+ return false;
27
+ }
28
+
29
+ if ( ! is_file( $filename ) || ! is_readable( $filename ) ) {
30
+ // This warning probably would have been generated by libxml and could have been handled handled using libxml's error handling functions.
31
+ // In WC_Safe_DOMDocument, however, we catch it before libxml, so it can't.
32
+ // The alternative is to let file_get_contents() handle the error, but that's annoying.
33
+ trigger_error( 'WC_Safe_DOMDocument::load(): I/O warning : failed to load external entity "' . $filename . '"', E_USER_WARNING );
34
+ return false;
35
+ }
36
+
37
+ if ( is_object( $this ) ) {
38
+ return $this->loadXML( file_get_contents( $filename ), $options );
39
+ } else {
40
+ // "This method *may* be called statically, but will issue an E_STRICT error."
41
+ return self::loadXML( file_get_contents( $filename ), $options );
42
+ }
43
+ }
44
+
45
+ /**
46
+ * When called non-statically (as an object method) with malicious data, no Exception is thrown, but the object is emptied of all DOM nodes.
47
+ *
48
+ * @param string $source The string containing the XML.
49
+ * @param int $options Bitwise OR of the libxml option constants. http://us3.php.net/manual/en/libxml.constants.php
50
+ *
51
+ * @return bool|DOMDocument true on success, false on failure. If called statically (E_STRICT error), returns DOMDocument on success.
52
+ */
53
+ public function loadXML( $source, $options = 0 ) {
54
+ if ( '' === $source ) {
55
+ // "If an empty string is passed as the source, a warning will be generated."
56
+ // "This warning is not generated by libxml and cannot be handled using libxml's error handling functions."
57
+ trigger_error( 'WC_Safe_DOMDocument::loadXML(): Empty string supplied as input', E_USER_WARNING );
58
+ return false;
59
+ }
60
+
61
+ $old = null;
62
+
63
+ if ( function_exists( 'libxml_disable_entity_loader' ) ) {
64
+ $old = libxml_disable_entity_loader( true );
65
+ }
66
+
67
+ $return = parent::loadXML( $source, $options );
68
+
69
+ if ( ! is_null( $old ) ) {
70
+ libxml_disable_entity_loader( $old );
71
+ }
72
+
73
+ if ( ! $return ) {
74
+ return $return;
75
+ }
76
+
77
+ // "This method *may* be called statically, but will issue an E_STRICT error."
78
+ $is_this = is_object( $this );
79
+
80
+ $object = $is_this ? $this : $return;
81
+
82
+ if ( isset( $object->doctype ) ) {
83
+ if ( $is_this ) {
84
+ // Get rid of the dangerous input by removing *all* nodes
85
+ while ( $this->firstChild ) {
86
+ $this->removeChild( $this->firstChild );
87
+ }
88
+ }
89
+
90
+ trigger_error( 'WC_Safe_DOMDocument::loadXML(): Unsafe DOCTYPE Detected', E_USER_WARNING );
91
+
92
+ return false;
93
+ }
94
+
95
+ return $return;
96
+ }
97
+ }
98
+
includes/api-requests/class-wc-shipstation-api-export.php ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! defined( 'ABSPATH' ) ) {
4
+ exit; // Exit if accessed directly
5
+ }
6
+
7
+ /**
8
+ * WC_Shipstation_API_Export Class
9
+ */
10
+ class WC_Shipstation_API_Export extends WC_Shipstation_API_Request {
11
+
12
+ /**
13
+ * Constructor
14
+ */
15
+ public function __construct() {
16
+ if ( ! WC_Shipstation_API::authenticated() ) {
17
+ exit;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Do the request
23
+ */
24
+ public function request() {
25
+ global $wpdb;
26
+
27
+ $this->validate_input( array( 'start_date', 'end_date' ) );
28
+
29
+ header( 'Content-Type: text/xml' );
30
+ $xml = new DOMDocument( '1.0', 'utf-8' );
31
+ $xml->formatOutput = true;
32
+ $page = max( 1, isset( $_GET['page'] ) ? absint( $_GET['page'] ) : 1 );
33
+ $exported = 0;
34
+ $tz_offset = get_option( 'gmt_offset' ) * 3600;
35
+ $raw_start_date = wc_clean( urldecode( $_GET['start_date'] ) );
36
+ $raw_end_date = wc_clean( urldecode( $_GET['end_date'] ) );
37
+
38
+ // Parse start and end date
39
+ if ( $raw_start_date && false === strtotime( $raw_start_date ) ) {
40
+ $month = substr( $raw_start_date, 0, 2 );
41
+ $day = substr( $raw_start_date, 2, 2 );
42
+ $year = substr( $raw_start_date, 4, 4 );
43
+ $time = substr( $raw_start_date, 9, 4 );
44
+ $start_date = gmdate( 'Y-m-d H:i:s', strtotime( $year . '-' . $month . '-' . $day . ' ' . $time ) );
45
+ } else {
46
+ $start_date = gmdate( 'Y-m-d H:i:s', strtotime( $raw_start_date ) );
47
+ }
48
+
49
+ if ( $raw_end_date && false === strtotime( $raw_end_date ) ) {
50
+ $month = substr( $raw_end_date, 0, 2 );
51
+ $day = substr( $raw_end_date, 2, 2 );
52
+ $year = substr( $raw_end_date, 4, 4 );
53
+ $time = substr( $raw_end_date, 9, 4 );
54
+ $end_date = gmdate( 'Y-m-d H:i:s', strtotime( $year . '-' . $month . '-' . $day . ' ' . $time ) );
55
+ } else {
56
+ $end_date = gmdate( 'Y-m-d H:i:s', strtotime( $raw_end_date ) );
57
+ }
58
+
59
+ if ( version_compare( WC_VERSION, '3.1', '>=' ) ) {
60
+ $order_ids = wc_get_orders( array(
61
+ 'date_modified' => $start_date . '...' . $end_date,
62
+ 'type' => 'shop_order',
63
+ 'status' => WC_ShipStation_Integration::$export_statuses,
64
+ 'return' => 'ids',
65
+ 'orderby' => 'date_modified',
66
+ 'order' => 'DESC',
67
+ 'paged' => $page,
68
+ 'limit' => WC_SHIPSTATION_EXPORT_LIMIT,
69
+ ) );
70
+ $order_ids = array_map( function( $order_or_id ) {
71
+ return is_a( $order_or_id, 'WC_Order' ) ? $order_or_id->get_id() : $order_or_id;
72
+ }, $order_ids );
73
+ } else {
74
+ $order_ids = $wpdb->get_col(
75
+ $wpdb->prepare( "
76
+ SELECT ID FROM {$wpdb->posts}
77
+ WHERE post_type = 'shop_order'
78
+ AND post_status IN ( '" . implode( "','", WC_ShipStation_Integration::$export_statuses ) . "' )
79
+ AND %s <= post_modified_gmt
80
+ AND post_modified_gmt <= %s
81
+ ORDER BY post_modified_gmt DESC
82
+ LIMIT %d, %d
83
+ ",
84
+ $start_date,
85
+ $end_date,
86
+ WC_SHIPSTATION_EXPORT_LIMIT * ( $page - 1 ),
87
+ WC_SHIPSTATION_EXPORT_LIMIT
88
+ )
89
+ );
90
+ }
91
+
92
+ // Figure out how to retrieve this using WC Query class.
93
+ $max_results = $wpdb->get_var(
94
+ $wpdb->prepare( "
95
+ SELECT COUNT(ID) FROM {$wpdb->posts}
96
+ WHERE post_type = 'shop_order'
97
+ AND post_status IN ( '" . implode( "','", WC_ShipStation_Integration::$export_statuses ) . "' )
98
+ AND %s <= post_modified_gmt
99
+ AND post_modified_gmt <= %s
100
+ ",
101
+ $start_date,
102
+ $end_date
103
+ )
104
+ );
105
+
106
+ $orders_xml = $xml->createElement( 'Orders' );
107
+
108
+ foreach ( $order_ids as $order_id ) {
109
+ if ( ! apply_filters( 'woocommerce_shipstation_export_order', true, $order_id ) ) {
110
+ continue;
111
+ }
112
+
113
+ $order = wc_get_order( $order_id );
114
+ $order_xml = $xml->createElement( 'Order' );
115
+ $wc_gte_30 = version_compare( WC_VERSION, '3.0', '>=' );// gte greater than or equal to 3.0
116
+ $formatted_order_number = ltrim( $order->get_order_number(), '#' );
117
+ $this->xml_append( $order_xml, 'OrderNumber', $formatted_order_number );
118
+ $this->xml_append( $order_xml, 'OrderID', $order_id );
119
+
120
+ if ( $wc_gte_30 ) {
121
+ // Sequence of date ordering: date paid > date completed > date created
122
+ $order_timestamp = $order->get_date_paid() ?: $order->get_date_completed() ?: $order->get_date_created();
123
+ $order_timestamp = $order_timestamp->getOffsetTimestamp();
124
+ } else {
125
+ $order_timestamp = $order->order_date;
126
+ }
127
+
128
+ $order_timestamp -= $tz_offset;
129
+ $this->xml_append( $order_xml, 'OrderDate', gmdate( 'm/d/Y H:i', $order_timestamp ), false );
130
+ $this->xml_append( $order_xml, 'OrderStatus', $order->get_status() );
131
+ $this->xml_append( $order_xml, 'PaymentMethod', $wc_gte_30 ? $order->get_payment_method() : $order->payment_method );
132
+ $this->xml_append( $order_xml, 'OrderPaymentMethodTitle', $wc_gte_30 ? $order->get_payment_method_title() : $order->payment_method_title );
133
+ $last_modified = strtotime( $wc_gte_30 ? $order->get_date_modified()->date( 'm/d/Y H:i' ) : $order->modified_date ) - $tz_offset;
134
+ $this->xml_append( $order_xml, 'LastModified', gmdate( 'm/d/Y H:i', $last_modified ), false );
135
+ $this->xml_append( $order_xml, 'ShippingMethod', implode( ' | ', $this->get_shipping_methods( $order ) ) );
136
+
137
+ $this->xml_append( $order_xml, 'OrderTotal', $order->get_total(), false );
138
+ $this->xml_append( $order_xml, 'TaxAmount', wc_round_tax_total( $order->get_total_tax() ), false );
139
+
140
+ if ( class_exists( 'WC_COG' ) ) {
141
+ $this->xml_append( $order_xml, 'CostOfGoods', wc_format_decimal( $order->wc_cog_order_total_cost ), false );
142
+ }
143
+
144
+ $this->xml_append( $order_xml, 'ShippingAmount', $wc_gte_30 ? $order->get_shipping_total() : $order->get_total_shipping(), false );
145
+ $this->xml_append( $order_xml, 'CustomerNotes', $wc_gte_30 ? $order->get_customer_note() : $order->customer_note );
146
+ $this->xml_append( $order_xml, 'InternalNotes', implode( ' | ', $this->get_order_notes( $order ) ) );
147
+
148
+ // Custom fields - 1 is used for coupon codes
149
+ $this->xml_append( $order_xml, 'CustomField1', implode( ' | ', $order->get_used_coupons() ) );
150
+
151
+ // Custom fields 2 and 3 can be mapped to a custom field via the following filters
152
+ $meta_key = apply_filters( 'woocommerce_shipstation_export_custom_field_2', '' );
153
+ if ( $meta_key ) {
154
+ $this->xml_append( $order_xml, 'CustomField2', apply_filters( 'woocommerce_shipstation_export_custom_field_2_value', get_post_meta( $order_id, $meta_key, true ), $order_id ) );
155
+ }
156
+
157
+ $meta_key = apply_filters( 'woocommerce_shipstation_export_custom_field_3', '' );
158
+ if ( $meta_key ) {
159
+ $this->xml_append( $order_xml, 'CustomField3', apply_filters( 'woocommerce_shipstation_export_custom_field_3_value', get_post_meta( $order_id, $meta_key, true ), $order_id ) );
160
+ }
161
+
162
+ // Customer data
163
+ $customer_xml = $xml->createElement( 'Customer' );
164
+ $this->xml_append( $customer_xml, 'CustomerCode', $wc_gte_30 ? $order->get_billing_email() : $order->billing_email );
165
+
166
+ $billto_xml = $xml->createElement( 'BillTo' );
167
+ $this->xml_append( $billto_xml, 'Name', ( $wc_gte_30 ? $order->get_billing_first_name() : $order->billing_first_name ) . ' ' . ( $wc_gte_30 ? $order->get_billing_last_name() : $order->billing_last_name ) );
168
+ $this->xml_append( $billto_xml, 'Company', $wc_gte_30 ? $order->get_billing_company() : $order->billing_company );
169
+ $this->xml_append( $billto_xml, 'Phone', $wc_gte_30 ? $order->get_billing_phone() : $order->billing_phone );
170
+ $this->xml_append( $billto_xml, 'Email', $wc_gte_30 ? $order->get_billing_email() : $order->billing_email );
171
+ $customer_xml->appendChild( $billto_xml );
172
+
173
+ $shipto_xml = $xml->createElement( 'ShipTo' );
174
+
175
+ $shipping_country = $wc_gte_30 ? $order->get_shipping_country() : $order->shipping_country;
176
+ if ( empty( $shipping_country ) ) {
177
+ $name = ( $wc_gte_30 ? $order->get_billing_first_name() : $order->billing_first_name ) . ' ' . ( $wc_gte_30 ? $order->get_billing_last_name() : $order->billing_last_name );
178
+ $this->xml_append( $shipto_xml, 'Name', $name );
179
+ $this->xml_append( $shipto_xml, 'Company', $wc_gte_30 ? $order->get_billing_company() : $order->billing_company );
180
+ $this->xml_append( $shipto_xml, 'Address1', $wc_gte_30 ? $order->get_billing_address_1() : $order->billing_address_1 );
181
+ $this->xml_append( $shipto_xml, 'Address2', $wc_gte_30 ? $order->get_billing_address_2() : $order->billing_address_2 );
182
+ $this->xml_append( $shipto_xml, 'City', $wc_gte_30 ? $order->get_billing_city() : $order->billing_city );
183
+ $this->xml_append( $shipto_xml, 'State', $wc_gte_30 ? $order->get_billing_state() : $order->billing_state );
184
+ $this->xml_append( $shipto_xml, 'PostalCode', $wc_gte_30 ? $order->get_billing_postcode() : $order->billing_postcode );
185
+ $this->xml_append( $shipto_xml, 'Country', $wc_gte_30 ? $order->get_billing_country() : $order->billing_country );
186
+ $this->xml_append( $shipto_xml, 'Phone', $wc_gte_30 ? $order->get_billing_phone() : $order->billing_phone );
187
+ } else {
188
+ $name = ( $wc_gte_30 ? $order->get_shipping_first_name() : $order->shipping_first_name ) . ' ' . ( $wc_gte_30 ? $order->get_shipping_last_name() : $order->shipping_last_name );
189
+ $this->xml_append( $shipto_xml, 'Name', $name );
190
+ $this->xml_append( $shipto_xml, 'Company', $wc_gte_30 ? $order->get_shipping_company() : $order->shipping_company );
191
+ $this->xml_append( $shipto_xml, 'Address1', $wc_gte_30 ? $order->get_shipping_address_1() : $order->shipping_address_1 );
192
+ $this->xml_append( $shipto_xml, 'Address2', $wc_gte_30 ? $order->get_shipping_address_2() : $order->shipping_address_2 );
193
+ $this->xml_append( $shipto_xml, 'City', $wc_gte_30 ? $order->get_shipping_city() : $order->shipping_city );
194
+ $this->xml_append( $shipto_xml, 'State', $wc_gte_30 ? $order->get_shipping_state() : $order->shipping_state );
195
+ $this->xml_append( $shipto_xml, 'PostalCode', $wc_gte_30 ? $order->get_shipping_postcode() : $order->shipping_postcode );
196
+ $this->xml_append( $shipto_xml, 'Country', $wc_gte_30 ? $order->get_shipping_country() : $order->shipping_country );
197
+ $this->xml_append( $shipto_xml, 'Phone', $wc_gte_30 ? $order->get_billing_phone() : $order->billing_phone );
198
+ }
199
+ $customer_xml->appendChild( $shipto_xml );
200
+
201
+ $order_xml->appendChild( $customer_xml );
202
+
203
+ // Item data
204
+ $found_item = false;
205
+ $items_xml = $xml->createElement( 'Items' );
206
+ // Merge arrays without loosing indexes.
207
+ $order_items = $order->get_items() + $order->get_items( 'fee' );
208
+ foreach ( $order_items as $item_id => $item ) {
209
+ if ( $wc_gte_30 ) {
210
+ $product = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : false;
211
+ } else {
212
+ $product = $order->get_product_from_item( $item );
213
+ }
214
+ $item_needs_no_shipping = ! $product || ! $product->needs_shipping();
215
+ $item_not_a_fee = 'fee' !== $item['type'];
216
+ if ( $item_needs_no_shipping && $item_not_a_fee ) {
217
+ continue;
218
+ }
219
+
220
+ $found_item = true;
221
+ $item_xml = $xml->createElement( 'Item' );
222
+ $this->xml_append( $item_xml, 'LineItemID', $item_id );
223
+
224
+ if ( 'fee' === $item['type'] ) {
225
+ $this->xml_append( $item_xml, 'Name', $item['name'] );
226
+ $this->xml_append( $item_xml, 'Quantity', 1, false );
227
+ $this->xml_append( $item_xml, 'UnitPrice', $order->get_item_total( $item, false, true ), false );
228
+ }
229
+
230
+ // handle product specific data
231
+ if ( $product && $product->needs_shipping() ) {
232
+ $this->xml_append( $item_xml, 'SKU', $product->get_sku() );
233
+ $this->xml_append( $item_xml, 'Name', $product->get_title() );
234
+ // image data
235
+ $image_id = $product->get_image_id();
236
+ $image_url = $image_id ? current( wp_get_attachment_image_src( $image_id, 'shop_thumbnail' ) ) : '';
237
+ $this->xml_append( $item_xml, 'ImageUrl', $image_url );
238
+
239
+ $this->xml_append( $item_xml, 'Weight', wc_get_weight( $product->get_weight(), 'oz' ), false );
240
+ $this->xml_append( $item_xml, 'WeightUnits', 'Ounces', false );
241
+ $this->xml_append( $item_xml, 'Quantity', $item['qty'], false );
242
+ $this->xml_append( $item_xml, 'UnitPrice', $order->get_item_subtotal( $item, false, true ), false );
243
+ }
244
+
245
+ if ( $item['item_meta'] ) {
246
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
247
+ $item_meta = new WC_Order_Item_Meta( $item, $product );
248
+ $formatted_meta = $item_meta->get_formatted( '_' );
249
+ } else {
250
+ add_filter( 'woocommerce_is_attribute_in_product_name', '__return_false' );
251
+ $formatted_meta = $item->get_formatted_meta_data();
252
+ }
253
+
254
+ if ( ! empty( $formatted_meta ) ) {
255
+ $options_xml = $xml->createElement( 'Options' );
256
+
257
+ foreach ( $formatted_meta as $meta_key => $meta ) {
258
+ $option_xml = $xml->createElement( 'Option' );
259
+
260
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
261
+ $this->xml_append( $option_xml, 'Name', $meta['label'] );
262
+ $this->xml_append( $option_xml, 'Value', $meta['value'] );
263
+ } else {
264
+ $this->xml_append( $option_xml, 'Name', $meta->display_key );
265
+ $this->xml_append( $option_xml, 'Value', wp_strip_all_tags( $meta->display_value ) );
266
+ }
267
+
268
+ $options_xml->appendChild( $option_xml );
269
+ }
270
+
271
+ $item_xml->appendChild( $options_xml );
272
+ }
273
+ }
274
+
275
+ $items_xml->appendChild( $item_xml );
276
+ }
277
+
278
+ if ( ! $found_item ) {
279
+ continue;
280
+ }
281
+
282
+ // Append cart level discount line
283
+ if ( $order->get_total_discount() ) {
284
+ $item_xml = $xml->createElement( 'Item' );
285
+ $this->xml_append( $item_xml, 'SKU', 'total-discount' );
286
+ $this->xml_append( $item_xml, 'Name', __( 'Total Discount', 'woocommerce-shipstation' ) );
287
+ $this->xml_append( $item_xml, 'Adjustment', 'true', false );
288
+ $this->xml_append( $item_xml, 'Quantity', 1, false );
289
+ $this->xml_append( $item_xml, 'UnitPrice', $order->get_total_discount() * -1, false );
290
+ $items_xml->appendChild( $item_xml );
291
+ }
292
+
293
+ // Append items XML
294
+ $order_xml->appendChild( $items_xml );
295
+ $orders_xml->appendChild( $order_xml );
296
+
297
+ $exported ++;
298
+
299
+ // Add order note to indicate it has been exported to Shipstation.
300
+ if ( 'yes' !== get_post_meta( $order_id, '_shipstation_exported', true ) ) {
301
+ $order->add_order_note( __( 'Order has been exported to Shipstation', 'woocommerce-shipstation' ) );
302
+ update_post_meta( $order_id, '_shipstation_exported', 'yes' );
303
+ }
304
+ }
305
+
306
+ $orders_xml->setAttribute( 'page', $page );
307
+ $orders_xml->setAttribute( 'pages', ceil( $max_results / WC_SHIPSTATION_EXPORT_LIMIT ) );
308
+ $xml->appendChild( $orders_xml );
309
+ echo $xml->saveXML();
310
+
311
+ /* translators: 1: total count */
312
+ $this->log( sprintf( __( 'Exported %s orders', 'woocommerce-shipstation' ), $exported ) );
313
+ }
314
+
315
+ /**
316
+ * Get shipping method names
317
+ * @param WC_Order $order
318
+ * @return array
319
+ */
320
+ private function get_shipping_methods( $order ) {
321
+ $shipping_methods = $order->get_shipping_methods();
322
+ $shipping_method_names = array();
323
+
324
+ foreach ( $shipping_methods as $shipping_method ) {
325
+ // Replace non-AlNum characters with space
326
+ $method_name = preg_replace( '/[^A-Za-z0-9 \-\.\_,]/', '', $shipping_method['name'] );
327
+ $shipping_method_names[] = $method_name;
328
+ }
329
+
330
+ return $shipping_method_names;
331
+ }
332
+
333
+ /**
334
+ * Get Order Notes
335
+ * @param WC_Order $order
336
+ * @return array
337
+ */
338
+ private function get_order_notes( $order ) {
339
+ $args = array(
340
+ 'post_id' => version_compare( WC_VERSION, '3.0.0', '>=' ) ? $order->get_id() : $order->id,
341
+ 'approve' => 'approve',
342
+ 'type' => 'order_note',
343
+ );
344
+
345
+ remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
346
+
347
+ $notes = get_comments( $args );
348
+
349
+ add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
350
+
351
+ $order_notes = array();
352
+
353
+ foreach ( $notes as $note ) {
354
+ if ( 'WooCommerce' !== $note->comment_author ) {
355
+ $order_notes[] = $note->comment_content;
356
+ }
357
+ }
358
+
359
+ return $order_notes;
360
+ }
361
+
362
+ /**
363
+ * Append XML as cdata
364
+ */
365
+ private function xml_append( $append_to, $name, $value, $cdata = true ) {
366
+ $data = $append_to->appendChild( $append_to->ownerDocument->createElement( $name ) );
367
+ if ( $cdata ) {
368
+ $data->appendChild( $append_to->ownerDocument->createCDATASection( $value ) );
369
+ } else {
370
+ $data->appendChild( $append_to->ownerDocument->createTextNode( $value ) );
371
+ }
372
+ }
373
+ }
374
+
375
+ return new WC_Shipstation_API_Export();
includes/api-requests/class-wc-shipstation-api-request.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! defined( 'ABSPATH' ) ) {
4
+ exit; // Exit if accessed directly
5
+ }
6
+
7
+ /**
8
+ * WC_Shipstation_API_Request Class
9
+ */
10
+ abstract class WC_Shipstation_API_Request {
11
+
12
+ /**
13
+ * Stores logger class
14
+ * @var WC_Logger
15
+ */
16
+ private $log = null;
17
+
18
+ /**
19
+ * Log something
20
+ * @param string $message
21
+ */
22
+ public function log( $message ) {
23
+ if ( 'no' === WC_ShipStation_Integration::$logging_enabled ) {
24
+ return;
25
+ }
26
+ if ( is_null( $this->log ) ) {
27
+ $this->log = new WC_Logger();
28
+ }
29
+ $this->log->add( 'shipstation', $message );
30
+ }
31
+
32
+ /**
33
+ * Run the request
34
+ */
35
+ public function request() {}
36
+
37
+ /**
38
+ * Validate data
39
+ * @param array $required_fields fields to look for
40
+ */
41
+ function validate_input( $required_fields ) {
42
+ foreach ( $required_fields as $required ) {
43
+ if ( empty( $_GET[ $required ] ) ) {
44
+ /* translators: 1: field name */
45
+ $this->trigger_error( sprintf( __( 'Missing required param: %s', 'woocommerce-shipstation' ), $required ) );
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Trigger and log an error
52
+ * @param string $message
53
+ */
54
+ public function trigger_error( $message ) {
55
+ $this->log( $message );
56
+ wp_send_json_error( $message );
57
+ }
58
+ }
59
+
includes/api-requests/class-wc-shipstation-api-shipnotify.php ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! defined( 'ABSPATH' ) ) {
4
+ exit; // Exit if accessed directly
5
+ }
6
+
7
+ /**
8
+ * WC_Shipstation_API_Shipnotify Class
9
+ */
10
+ class WC_Shipstation_API_Shipnotify extends WC_Shipstation_API_Request {
11
+
12
+ /**
13
+ * Constructor.
14
+ */
15
+ public function __construct() {
16
+ if ( ! WC_Shipstation_API::authenticated() ) {
17
+ exit;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * See how many items in the order need shipping.
23
+ *
24
+ * @param WC_Order $order Order object.
25
+ *
26
+ * @return int
27
+ */
28
+ private function order_items_to_ship_count( $order ) {
29
+ $needs_shipping = 0;
30
+
31
+ foreach ( $order->get_items() as $item_id => $item ) {
32
+ $product = $order->get_product_from_item( $item );
33
+
34
+ if ( is_a( $product, 'WC_Product' ) && $product->needs_shipping() ) {
35
+ $needs_shipping += $item['qty'];
36
+ }
37
+ }
38
+
39
+ return $needs_shipping;
40
+ }
41
+
42
+ /**
43
+ * Check whether a given item ID is shippable item.
44
+ *
45
+ * @since 4.1.16
46
+ * @version 4.1.16
47
+ *
48
+ * @param WC_Order $order Order object.
49
+ * @param int $item_id Item ID.
50
+ *
51
+ * @return bool Returns true if item is shippable product.
52
+ */
53
+ private function is_shippable_item( $order, $item_id ) {
54
+ if ( version_compare( WC_VERSION, '3.0', '>=' ) ) {
55
+ $item = $order->get_item( $item_id );
56
+ if ( ! is_callable( array( $item, 'get_product' ) ) ) {
57
+ return false;
58
+ }
59
+
60
+ $product = $item->get_product();
61
+ } else {
62
+ $items = $order->get_items();
63
+ if ( ! isset( $items[ $item_id ] ) ) {
64
+ return false;
65
+ }
66
+
67
+ $product = $order->get_product_from_item( $items[ $item_id ] );
68
+ }
69
+
70
+ if ( ! $product ) {
71
+ return false;
72
+ }
73
+
74
+ return $product->needs_shipping();
75
+ }
76
+
77
+ /**
78
+ * Get the order ID from the order number.
79
+ *
80
+ * @param string $order_number Order number.
81
+ * @return integer
82
+ */
83
+ private function get_order_id( $order_number ) {
84
+ // Try to match an order number in brackets.
85
+ preg_match( '/\((.*?)\)/', $order_number, $matches );
86
+ if ( is_array( $matches ) && isset( $matches[1] ) ) {
87
+ $order_id = $matches[1];
88
+
89
+ } elseif ( function_exists( 'wc_sequential_order_numbers' ) ) {
90
+ // Try to convert number for Sequential Order Number.
91
+ $order_id = wc_sequential_order_numbers()->find_order_by_order_number( $order_number );
92
+
93
+ } elseif ( function_exists( 'wc_seq_order_number_pro' ) ) {
94
+ // Try to convert number for Sequential Order Number Pro.
95
+ $order_id = wc_seq_order_number_pro()->find_order_by_order_number( $order_number );
96
+
97
+ } else {
98
+ // Default to not converting order number.
99
+ $order_id = $order_number;
100
+ }
101
+
102
+ if ( 0 === $order_id ) {
103
+ $order_id = $order_number;
104
+ }
105
+
106
+ return apply_filters( 'woocommerce_shipstation_get_order_id', absint( $order_id ) );
107
+ }
108
+
109
+ /**
110
+ * Retrieves the raw request data (body).
111
+ *
112
+ * `$HTTP_RAW_POST_DATA` is deprecated in PHP 5.6 and removed in PHP 5.7,
113
+ * it's used here for server that has issue with reading `php://input`
114
+ * stream.
115
+ *
116
+ * @since 4.1.17
117
+ * @version 4.1.17
118
+ *
119
+ * @return string Raw request data.
120
+ */
121
+ private function get_raw_post_data() {
122
+ global $HTTP_RAW_POST_DATA;
123
+
124
+ if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
125
+ $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
126
+ }
127
+
128
+ return $HTTP_RAW_POST_DATA;
129
+ }
130
+
131
+ /**
132
+ * Get Parsed XML response.
133
+ *
134
+ * @param string $xml XML.
135
+ * @return string|bool
136
+ */
137
+ private function get_parsed_xml( $xml ) {
138
+ if ( ! class_exists( 'WC_Safe_DOMDocument' ) ) {
139
+ include_once( 'class-wc-safe-domdocument.php' );
140
+ }
141
+
142
+ libxml_use_internal_errors( true );
143
+
144
+ $dom = new WC_Safe_DOMDocument;
145
+ $success = $dom->loadXML( $xml );
146
+
147
+ if ( ! $success ) {
148
+ $this->log( 'wpcom_safe_simplexml_load_string(): Error loading XML string' );
149
+ return false;
150
+ }
151
+
152
+ if ( isset( $dom->doctype ) ) {
153
+ $this->log( 'wpcom_safe_simplexml_import_dom(): Unsafe DOCTYPE Detected' );
154
+ return false;
155
+ }
156
+
157
+ return simplexml_import_dom( $dom, 'SimpleXMLElement' );
158
+ }
159
+
160
+ /**
161
+ * Handling the request.
162
+ *
163
+ * @since 1.0.0
164
+ * @version 4.1.18
165
+ */
166
+ public function request() {
167
+
168
+ $this->validate_input( array( 'order_number', 'carrier' ) );
169
+
170
+ $timestamp = current_time( 'timestamp' );
171
+ $shipstation_xml = $this->get_raw_post_data();
172
+ $shipped_items = array();
173
+ $shipped_item_count = 0;
174
+ $order_shipped = false;
175
+ $xml_order_id = 0;
176
+
177
+ $can_parse_xml = true;
178
+
179
+ if ( empty( $shipstation_xml ) ) {
180
+ $can_parse_xml = false;
181
+ $this->log( __( 'Missing ShipNotify XML input.', 'woocommerce-shipstation' ) );
182
+
183
+ // For unknown reason raw post data can be empty. Log all requests
184
+ // information might help figuring out the culprit.
185
+ //
186
+ // @see https://github.com/woocommerce/woocommerce-shipstation/issues/80.
187
+ $this->log( '$_REQUEST: ' . print_r( $_REQUEST, true ) );
188
+ }
189
+
190
+ if ( ! function_exists( 'simplexml_import_dom' ) ) {
191
+ $can_parse_xml = false;
192
+ $this->log( __( 'Missing SimpleXML extension for parsing ShipStation XML.', 'woocommerce-shipstation' ) );
193
+ }
194
+
195
+ // Try to parse XML first since it can contain the real OrderID.
196
+ if ( $can_parse_xml ) {
197
+ $this->log( __( 'ShipNotify XML: ', 'woocommerce-shipstation' ) . print_r( $shipstation_xml, true ) );
198
+
199
+ $xml = $this->get_parsed_xml( $shipstation_xml );
200
+
201
+ if ( ! $xml ) {
202
+ $this->log( __( 'Cannot parse XML', 'woocommerce-shipstation' ) );
203
+ status_header( 500 );
204
+ }
205
+
206
+ if ( isset( $xml->ShipDate ) ) {
207
+ $timestamp = strtotime( (string) $xml->ShipDate );
208
+ }
209
+
210
+ if ( isset( $xml->OrderID ) && $_GET['order_number'] !== (string) $xml->OrderID ) {
211
+ $xml_order_id = (int) $xml->OrderID;
212
+ }
213
+ }
214
+
215
+ // Get real order ID from XML otherwise try to convert it from the order number.
216
+ $order_id = ! $xml_order_id ? $this->get_order_id( wc_clean( $_GET['order_number'] ) ) : $xml_order_id;
217
+ $tracking_number = empty( $_GET['tracking_number'] ) ? '' : wc_clean( $_GET['tracking_number'] );
218
+ $carrier = empty( $_GET['carrier'] ) ? '' : wc_clean( $_GET['carrier'] );
219
+ $order = wc_get_order( $order_id );
220
+
221
+ if ( false === $order || ! is_object( $order ) ) {
222
+ /* translators: 1: order id */
223
+ $this->log( sprintf( __( 'Order %s can not be found.', 'woocommerce-shipstation' ), $order_id ) );
224
+ exit;
225
+ }
226
+
227
+ // Get real order ID from order object.
228
+ $order_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->id : $order->get_id();
229
+ if ( empty( $order_id ) ) {
230
+ /* translators: 1: order id */
231
+ $this->log( sprintf( __( 'Invalid order ID: %s', 'woocommerce-shipstation' ), $order_id ) );
232
+ exit;
233
+ }
234
+
235
+ // Maybe parse items from posted XML (if exists).
236
+ if ( $can_parse_xml && isset( $xml->Items ) ) {
237
+ $items = $xml->Items;
238
+ if ( $items ) {
239
+ foreach ( $items->Item as $item ) {
240
+ $this->log( __( 'ShipNotify Item: ', 'woocommerce-shipstation' ) . print_r( $item, true ) );
241
+
242
+ $item_sku = wc_clean( (string) $item->SKU );
243
+ $item_name = wc_clean( (string) $item->Name );
244
+ $qty_shipped = absint( $item->Quantity );
245
+
246
+ if ( $item_sku ) {
247
+ $item_sku = ' (' . $item_sku . ')';
248
+ }
249
+
250
+ $item_id = wc_clean( (int) $item->LineItemID );
251
+ if ( ! $this->is_shippable_item( $order, $item_id ) ) {
252
+ /* translators: 1: item name */
253
+ $this->log( sprintf( __( 'Item %s is not shippable product. Skipping.', 'woocommerce-shipstation' ), $item_name ) );
254
+ continue;
255
+ }
256
+
257
+ $shipped_item_count += $qty_shipped;
258
+ $shipped_items[] = $item_name . $item_sku . ' x ' . $qty_shipped;
259
+ }
260
+ }
261
+ }
262
+
263
+ // Number of items in WC order.
264
+ $total_item_count = $this->order_items_to_ship_count( $order );
265
+
266
+ // If we have a list of shipped items, we can customise the note + see
267
+ // if the order is not yet complete.
268
+ if ( sizeof( $shipped_items ) > 0 ) {
269
+ $order_note = sprintf(
270
+ /* translators: 1) shipped items 2) carrier's name 3) shipped date, 4) tracking number */
271
+ __( '%1$s shipped via %2$s on %3$s with tracking number %4$s.', 'woocommerce-shipstation' ),
272
+ esc_html( implode( ', ', $shipped_items ) ),
273
+ esc_html( $carrier ),
274
+ date_i18n( get_option( 'date_format' ), $timestamp ),
275
+ $tracking_number
276
+ );
277
+
278
+ $current_shipped_items = max( (int) get_post_meta( $order_id, '_shipstation_shipped_item_count', true ), 0 );
279
+
280
+ if ( ( $current_shipped_items + $shipped_item_count ) >= $total_item_count ) {
281
+ $order_shipped = true;
282
+ }
283
+
284
+ $this->log(
285
+ sprintf(
286
+ /* translators: 1) number of shipped items 2) total shipped items 3) order ID */
287
+ __( 'Shipped %1$d out of %2$d items in order %3$s', 'woocommerce-shipstation' ),
288
+ $shipped_item_count,
289
+ $total_item_count,
290
+ $order_id
291
+ )
292
+ );
293
+
294
+ update_post_meta( $order_id, '_shipstation_shipped_item_count', $current_shipped_items + $shipped_item_count );
295
+
296
+ } else {
297
+ // If we don't have items from SS and order items in WC, or cannot parse
298
+ // the XML, just complete the order as a whole.
299
+ $order_shipped = 0 === $total_item_count || ! $can_parse_xml;
300
+
301
+ $order_note = sprintf(
302
+ /* translators: 1) carrier's name 2) shipped date, 3) tracking number */
303
+ __( 'Items shipped via %1$s on %2$s with tracking number %3$s (Shipstation).', 'woocommerce-shipstation' ),
304
+ esc_html( $carrier ),
305
+ date_i18n( get_option( 'date_format' ), $timestamp ),
306
+ $tracking_number
307
+ );
308
+
309
+ /* translators: 1: order id */
310
+ $this->log( sprintf( __( 'No items found - shipping entire order %d.', 'woocommerce-shipstation' ), $order_id ) );
311
+ }
312
+
313
+ // Tracking information - WC Shipment Tracking extension.
314
+ if ( class_exists( 'WC_Shipment_Tracking' ) ) {
315
+ if ( function_exists( 'wc_st_add_tracking_number' ) ) {
316
+ wc_st_add_tracking_number( $order_id, $tracking_number, strtolower( $carrier ), $timestamp );
317
+ } else {
318
+ // You're using Shipment Tracking < 1.4.0. Please update!
319
+ update_post_meta( $order_id, '_tracking_provider', strtolower( $carrier ) );
320
+ update_post_meta( $order_id, '_tracking_number', $tracking_number );
321
+ update_post_meta( $order_id, '_date_shipped', $timestamp );
322
+ }
323
+
324
+ $is_customer_note = 0;
325
+ } else {
326
+ $is_customer_note = 1;
327
+ }
328
+
329
+ $order->add_order_note( $order_note, $is_customer_note );
330
+
331
+ // Update order status.
332
+ if ( $order_shipped ) {
333
+ $order->update_status( WC_ShipStation_Integration::$shipped_status );
334
+
335
+ /* translators: 1) order ID 2) shipment status */
336
+ $this->log( sprintf( __( 'Updated order %1$s to status %2$s', 'woocommerce-shipstation' ), $order_id, WC_ShipStation_Integration::$shipped_status ) );
337
+ }
338
+
339
+ // Trigger action for other integrations.
340
+ do_action( 'woocommerce_shipstation_shipnotify', $order, array(
341
+ 'tracking_number' => $tracking_number,
342
+ 'carrier' => $carrier,
343
+ 'ship_date' => $timestamp,
344
+ 'xml' => $shipstation_xml,
345
+ ) );
346
+
347
+ status_header( 200 );
348
+ }
349
+ }
350
+
351
+ return new WC_Shipstation_API_Shipnotify();
includes/class-wc-shipstation-api.php ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! defined( 'ABSPATH' ) ) {
4
+ exit; // Exit if accessed directly
5
+ }
6
+
7
+ include_once( 'api-requests/class-wc-shipstation-api-request.php' );
8
+
9
+ /**
10
+ * WC_Shipstation_API Class
11
+ */
12
+ class WC_Shipstation_API extends WC_Shipstation_API_Request {
13
+
14
+ /** @var boolean Stores whether or not shipstation has been authenticated */
15
+ private static $authenticated = false;
16
+
17
+ /**
18
+ * Constructor
19
+ */
20
+ public function __construct() {
21
+ nocache_headers();
22
+
23
+ if ( ! defined( 'DONOTCACHEPAGE' ) ) {
24
+ define( 'DONOTCACHEPAGE', 'true' );
25
+ }
26
+
27
+ if ( ! defined( 'DONOTCACHEOBJECT' ) ) {
28
+ define( 'DONOTCACHEOBJECT', 'true' );
29
+ }
30
+
31
+ if ( ! defined( 'DONOTCACHEDB' ) ) {
32
+ define( 'DONOTCACHEDB', 'true' );
33
+ }
34
+
35
+ self::$authenticated = false;
36
+
37
+ $this->request();
38
+ }
39
+
40
+ /**
41
+ * Has API been authenticated?
42
+ * @return bool
43
+ */
44
+ public static function authenticated() {
45
+ return self::$authenticated;
46
+ }
47
+
48
+ /**
49
+ * Handle the request
50
+ */
51
+ public function request() {
52
+ if ( empty( $_GET['auth_key'] ) ) {
53
+ $this->trigger_error( __( 'Authentication key is required!', 'woocommerce-shipstation' ) );
54
+ }
55
+
56
+ if ( ! hash_equals( sanitize_text_field( $_GET['auth_key'] ), WC_ShipStation_Integration::$auth_key ) ) {
57
+ $this->trigger_error( __( 'Invalid authentication key', 'woocommerce-shipstation' ) );
58
+ }
59
+
60
+ $request = $_GET;
61
+
62
+ if ( isset( $request['action'] ) ) {
63
+ $this->request = array_map( 'sanitize_text_field', $request );
64
+ } else {
65
+ $this->trigger_error( __( 'Invalid request', 'woocommerce-shipstation' ) );
66
+ }
67
+
68
+ self::$authenticated = true;
69
+
70
+ if ( in_array( $this->request['action'], array( 'export', 'shipnotify' ) ) ) {
71
+ /* translators: 1: query string */
72
+ $this->log( sprintf( __( 'Input params: %s', 'woocommerce-shipstation' ), http_build_query( $this->request ) ) );
73
+ $request_class = include( 'api-requests/class-wc-shipstation-api-' . $this->request['action'] . '.php' );
74
+ $request_class->request();
75
+ } else {
76
+ $this->trigger_error( __( 'Invalid request', 'woocommerce-shipstation' ) );
77
+ }
78
+
79
+ exit;
80
+ }
81
+ }
82
+
83
+ new WC_Shipstation_API();
84
+
includes/class-wc-shipstation-integration.php ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( ! defined( 'ABSPATH' ) ) {
4
+ exit; // Exit if accessed directly
5
+ }
6
+
7
+ /**
8
+ * WC_ShipStation_Integration Class
9
+ */
10
+ class WC_ShipStation_Integration extends WC_Integration {
11
+
12
+ public static $auth_key = null;
13
+ public static $export_statuses = array();
14
+ public static $logging_enabled = true;
15
+ public static $shipped_status = null;
16
+
17
+ /**
18
+ * Constructor
19
+ */
20
+ public function __construct() {
21
+ $this->id = 'shipstation';
22
+ $this->method_title = __( 'ShipStation', 'woocommerce-shipstation' );
23
+ $this->method_description = __( 'ShipStation allows you to retrieve &amp; manage orders, then print labels &amp; packing slips with ease.', 'woocommerce-shipstation' );
24
+
25
+ if ( ! get_option( 'woocommerce_shipstation_auth_key', false ) ) {
26
+ update_option( 'woocommerce_shipstation_auth_key', $this->generate_key() );
27
+ }
28
+
29
+ // Load admin form
30
+ $this->init_form_fields();
31
+
32
+ // Load settings
33
+ $this->init_settings();
34
+
35
+ self::$auth_key = get_option( 'woocommerce_shipstation_auth_key', false );
36
+ self::$export_statuses = $this->get_option( 'export_statuses', array( 'wc-processing', 'wc-on-hold', 'wc-completed', 'wc-cancelled' ) );
37
+ self::$logging_enabled = 'yes' === $this->get_option( 'logging_enabled', 'yes' );
38
+ self::$shipped_status = $this->get_option( 'shipped_status', 'wc-completed' );
39
+
40
+ // Force saved value
41
+ $this->settings['auth_key'] = self::$auth_key;
42
+
43
+ // Hooks
44
+ add_action( 'woocommerce_update_options_integration_shipstation', array( $this, 'process_admin_options' ) );
45
+ add_filter( 'woocommerce_subscriptions_renewal_order_meta_query', array( $this, 'subscriptions_renewal_order_meta_query' ), 10, 4 );
46
+
47
+ $settings_notice_dismissed = get_user_meta( get_current_user_id(), 'dismissed_shipstation-setup_notice' );
48
+
49
+ if ( ! $settings_notice_dismissed ) {
50
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
51
+ add_action( 'admin_notices', array( $this, 'settings_notice' ) );
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Enqueue admin scripts/styles
57
+ */
58
+ public function enqueue_scripts() {
59
+ wp_enqueue_style( 'shipstation-admin', plugins_url( 'assets/css/admin.css', dirname( __FILE__ ) ) );
60
+ }
61
+
62
+ /**
63
+ * Generate a key
64
+ * @return string
65
+ */
66
+ public function generate_key() {
67
+ $to_hash = get_current_user_id() . date( 'U' ) . mt_rand();
68
+ return 'WCSS-' . hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
69
+ }
70
+
71
+ /**
72
+ * Init integration form fields
73
+ */
74
+ public function init_form_fields() {
75
+ $this->form_fields = include( 'data/data-settings.php' );
76
+ }
77
+
78
+ /**
79
+ * Prevents WooCommerce Subscriptions from copying across certain meta keys to renewal orders.
80
+ * @param array $order_meta_query
81
+ * @param int $original_order_id
82
+ * @param int $renewal_order_id
83
+ * @param string $new_order_role
84
+ * @return array
85
+ */
86
+ public function subscriptions_renewal_order_meta_query( $order_meta_query, $original_order_id, $renewal_order_id, $new_order_role ) {
87
+ if ( 'parent' == $new_order_role ) {
88
+ $order_meta_query .= ' AND `meta_key` NOT IN ('
89
+ . "'_tracking_provider', "
90
+ . "'_tracking_number', "
91
+ . "'_date_shipped', "
92
+ . "'_order_custtrackurl', "
93
+ . "'_order_custcompname', "
94
+ . "'_order_trackno', "
95
+ . "'_order_trackurl' )";
96
+ }
97
+ return $order_meta_query;
98
+ }
99
+
100
+ /**
101
+ * Settings prompt
102
+ */
103
+ public function settings_notice() {
104
+ if ( ! empty( $_GET['tab'] ) && 'integration' === $_GET['tab'] ) {
105
+ return;
106
+ }
107
+
108
+ $logo_title = __( 'ShipStation logo', 'woocommerce-shipstation' );
109
+ ?>
110
+ <div id="message" class="updated woocommerce-message shipstation-setup">
111
+ <img class="shipstation-logo" alt="<?php echo esc_attr( $logo_title ); ?>" title="<?php echo esc_attr( $logo_title ); ?>" src="<?php echo plugins_url( 'assets/images/shipstation-logo-blue.png', dirname( __FILE__ ) ); ?>" />
112
+ <a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'shipstation-setup' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php _e( 'Dismiss', 'woocommerce' ); ?></a>
113
+ <p>
114
+ <?php
115
+ printf(
116
+ wp_kses(
117
+ /* translators: %s: ShipStation URL */
118
+ __( 'To begin printing shipping labels with ShipStation head over to <a class="shipstation-external-link" href="%s" target="_blank">ShipStation.com</a> and log in or create a new account.', 'woocommerce-shipstation' ),
119
+ array(
120
+ 'a' => array(
121
+ 'class' => array(),
122
+ 'href' => array(),
123
+ 'target' => array(),
124
+ ),
125
+ )
126
+ ),
127
+ 'https://www.shipstation.com/'
128
+ );
129
+ ?>
130
+ </p>
131
+ <p>
132
+ <?php
133
+ printf(
134
+ wp_kses(
135
+ /* translators: %s: ShipStation Auth Key */
136
+ __( 'After logging in, add a selling channel for WooCommerce and use your Auth Key (<code>%s</code>) to connect your store.', 'woocommerce-shipstation' ),
137
+ array( 'code' => array() )
138
+ ),
139
+ self::$auth_key
140
+ );
141
+ ?>
142
+ </p>
143
+ <p><?php esc_html_e( "Once connected you're good to go!", 'woocommerce-shipstation' ); ?></p>
144
+ <hr>
145
+ <p>
146
+ <?php
147
+ printf(
148
+ wp_kses(
149
+ /* translators: %1$s: ShipStation plugin settings URL, %2$s: ShipStation documentation URL */
150
+ __( 'You can find other settings for this extension <a href="%1$s">here</a> and view the documentation <a href="%2$s">here</a>.', 'woocommerce-shipstation' ),
151
+ array(
152
+ 'a' => array(
153
+ 'href' => array(),
154
+ ),
155
+ )
156
+ ),
157
+ admin_url( 'admin.php?page=wc-settings&tab=integration&section=shipstation' ),
158
+ 'https://docs.woocommerce.com/document/shipstation-for-woocommerce/'
159
+ );
160
+ ?>
161
+ </p>
162
+ </div>
163
+ <?php
164
+ }
165
+ }
166
+
includes/class-wc-shipstation-privacy.php ADDED
@@ -0,0 +1,165 @@