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 @@
1
+ <?php
2
+ if ( ! class_exists( 'WC_Abstract_Privacy' ) ) {
3
+ return;
4
+ }
5
+
6
+ class WC_ShipStation_Privacy extends WC_Abstract_Privacy {
7
+ /**
8
+ * Constructor
9
+ *
10
+ */
11
+ public function __construct() {
12
+ parent::__construct( __( 'ShipStation', 'woocommerce-shipstation' ) );
13
+
14
+ $this->add_exporter( 'woocommerce-shipstation-order-data', __( 'WooCommerce ShipStation Order Data', 'woocommerce-shipstation' ), array( $this, 'order_data_exporter' ) );
15
+
16
+ $this->add_eraser( 'woocommerce-shipstation-order-data', __( 'WooCommerce ShipStation Data', 'woocommerce-shipstation' ), array( $this, 'order_data_eraser' ) );
17
+ }
18
+
19
+ /**
20
+ * Returns a list of orders.
21
+ *
22
+ * @param string $email_address
23
+ * @param int $page
24
+ *
25
+ * @return array WP_Post
26
+ */
27
+ protected function get_orders( $email_address, $page ) {
28
+ $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
29
+
30
+ $order_query = array(
31
+ 'limit' => 10,
32
+ 'page' => $page,
33
+ );
34
+
35
+ if ( $user instanceof WP_User ) {
36
+ $order_query['customer_id'] = (int) $user->ID;
37
+ } else {
38
+ $order_query['billing_email'] = $email_address;
39
+ }
40
+
41
+ return wc_get_orders( $order_query );
42
+ }
43
+
44
+ /**
45
+ * Gets the message of the privacy to display.
46
+ *
47
+ */
48
+ public function get_privacy_message() {
49
+ /* translators: 1: URL to documentation */
50
+ return wpautop( sprintf( __( 'By using this extension, you may be storing personal data or sharing data with an external service. <a href="%s" target="_blank">Learn more about how this works, including what you may want to include in your privacy policy.</a>', 'woocommerce-shipstation' ), 'https://docs.woocommerce.com/document/privacy-shipping/#woocommerce-shipstation' ) );
51
+ }
52
+
53
+ /**
54
+ * Handle exporting data for Orders.
55
+ *
56
+ * @param string $email_address E-mail address to export.
57
+ * @param int $page Pagination of data.
58
+ *
59
+ * @return array
60
+ */
61
+ public function order_data_exporter( $email_address, $page = 1 ) {
62
+ $done = false;
63
+ $data_to_export = array();
64
+
65
+ $orders = $this->get_orders( $email_address, (int) $page );
66
+
67
+ $done = true;
68
+
69
+ if ( 0 < count( $orders ) ) {
70
+ foreach ( $orders as $order ) {
71
+ $data_to_export[] = array(
72
+ 'group_id' => 'woocommerce_orders',
73
+ 'group_label' => __( 'Orders', 'woocommerce-shipstation' ),
74
+ 'item_id' => 'order-' . $order->get_id(),
75
+ 'data' => array(
76
+ array(
77
+ 'name' => __( 'ShipStation tracking provider', 'woocommerce-shipstation' ),
78
+ 'value' => get_post_meta( $order->get_id(), '_tracking_provider', true ),
79
+ ),
80
+ array(
81
+ 'name' => __( 'ShipStation tracking number', 'woocommerce-shipstation' ),
82
+ 'value' => get_post_meta( $order->get_id(), '_tracking_number', true ),
83
+ ),
84
+ array(
85
+ 'name' => __( 'ShipStation date shipped', 'woocommerce-shipstation' ),
86
+ 'value' => get_post_meta( $order->get_id(), '_date_shipped', true ) ? date( 'Y-m-d H:i:s', get_post_meta( $order->get_id(), '_date_shipped', true ) ) : '',
87
+ ),
88
+ ),
89
+ );
90
+ }
91
+
92
+ $done = 10 > count( $orders );
93
+ }
94
+
95
+ return array(
96
+ 'data' => $data_to_export,
97
+ 'done' => $done,
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Finds and erases order data by email address.
103
+ *
104
+ * @since 3.4.0
105
+ * @param string $email_address The user email address.
106
+ * @param int $page Page.
107
+ * @return array An array of personal data in name value pairs
108
+ */
109
+ public function order_data_eraser( $email_address, $page ) {
110
+ $orders = $this->get_orders( $email_address, (int) $page );
111
+
112
+ $items_removed = false;
113
+ $items_retained = false;
114
+ $messages = array();
115
+
116
+ foreach ( (array) $orders as $order ) {
117
+ $order = wc_get_order( $order->get_id() );
118
+
119
+ list( $removed, $retained, $msgs ) = $this->maybe_handle_order( $order );
120
+ $items_removed |= $removed;
121
+ $items_retained |= $retained;
122
+ $messages = array_merge( $messages, $msgs );
123
+ }
124
+
125
+ // Tell core if we have more orders to work on still
126
+ $done = count( $orders ) < 10;
127
+
128
+ return array(
129
+ 'items_removed' => $items_removed,
130
+ 'items_retained' => $items_retained,
131
+ 'messages' => $messages,
132
+ 'done' => $done,
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Handle eraser of data tied to Orders
138
+ *
139
+ * @param WC_Order $order
140
+ * @return array
141
+ */
142
+ protected function maybe_handle_order( $order ) {
143
+ $order_id = $order->get_id();
144
+ $item_count = get_post_meta( $order->get_id(), '_shipstation_shipped_item_count', true );
145
+ $tracking_provider = get_post_meta( $order->get_id(), '_tracking_provider', true );
146
+ $tracking_number = get_post_meta( $order->get_id(), '_tracking_number', true );
147
+ $date_shipped = get_post_meta( $order->get_id(), '_date_shipped', true );
148
+ $shipstation_exported = get_post_meta( $order->get_id(), '_shipstation_exported', true );
149
+
150
+ if ( empty( $item_count ) && empty( $tracking_provider ) && empty( $tracking_number )
151
+ && empty( $date_shipped ) && empty( $shipstation_exported ) ) {
152
+ return array( false, false, array() );
153
+ }
154
+
155
+ delete_post_meta( $order->get_id(), '_shipstation_shipped_item_count' );
156
+ delete_post_meta( $order->get_id(), '_tracking_provider' );
157
+ delete_post_meta( $order->get_id(), '_tracking_number' );
158
+ delete_post_meta( $order->get_id(), '_date_shipped' );
159
+ delete_post_meta( $order->get_id(), '_shipstation_exported' );
160
+
161
+ return array( true, false, array( __( 'ShipStation Order Data Erased.', 'woocommerce-shipstation' ) ) );
162
+ }
163
+ }
164
+
165
+ new WC_ShipStation_Privacy();
includes/data/data-settings.php ADDED
@@ -0,0 +1,72 @@
1
+ <?php
2
+
3
+ $statuses = wc_get_order_statuses();
4
+
5
+ // When integration loaded custom statuses is not loaded yet, so we need to
6
+ // merge it manually.
7
+ if ( function_exists( 'wc_order_status_manager' ) ) {
8
+ $query = new WP_Query( array(
9
+ 'post_type' => 'wc_order_status',
10
+ 'post_status' => 'publish',
11
+ 'posts_per_page' => -1,
12
+ 'suppress_filters' => 1,
13
+ 'orderby' => 'menu_order',
14
+ 'order' => 'ASC',
15
+ ) );
16
+
17
+ $filtered_statuses = array();
18
+ foreach ( $query->posts as $status ) {
19
+ $filtered_statuses[ 'wc-' . $status->post_name ] = $status->post_title;
20
+ }
21
+ $statuses = array_merge( $statuses, $filtered_statuses );
22
+
23
+ wp_reset_postdata();
24
+ }
25
+
26
+ foreach ( $statuses as $key => $value ) {
27
+ $statuses[ $key ] = str_replace( 'wc-', '', $key );
28
+ }
29
+
30
+ $fields = array(
31
+ 'auth_key' => array(
32
+ 'title' => __( 'Authentication Key', 'woocommerce-shipstation' ),
33
+ 'description' => __( 'Copy and paste this key into ShipStation during setup.', 'woocommerce-shipstation' ),
34
+ 'default' => '',
35
+ 'type' => 'text',
36
+ 'desc_tip' => __( 'This is the <code>Auth Key</code> you set in ShipStation and allows ShipStation to communicate with your store.', 'woocommerce-shipstation' ),
37
+ 'custom_attributes' => array(
38
+ 'readonly' => 'readonly',
39
+ ),
40
+ 'value' => WC_ShipStation_Integration::$auth_key,
41
+ ),
42
+ 'export_statuses' => array(
43
+ 'title' => __( 'Export Order Statuses&hellip;', 'woocommerce-shipstation' ),
44
+ 'type' => 'multiselect',
45
+ 'options' => $statuses,
46
+ 'class' => 'chosen_select',
47
+ 'css' => 'width: 450px;',
48
+ 'description' => __( 'Define the order statuses you wish to export to ShipStation.', 'woocommerce' ),
49
+ 'desc_tip' => true,
50
+ 'custom_attributes' => array(
51
+ 'data-placeholder' => __( 'Select Order Statuses', 'woocommerce' ),
52
+ ),
53
+ ),
54
+ 'shipped_status' => array(
55
+ 'title' => __( 'Shipped Order Status&hellip;', 'woocommerce-shipstation' ),
56
+ 'type' => 'select',
57
+ 'options' => $statuses,
58
+ 'description' => __( 'Define the order status you wish to update to once an order has been shipping via ShipStation. By default this is "Completed".', 'woocommerce' ),
59
+ 'desc_tip' => true,
60
+ 'default' => 'wc-completed',
61
+ ),
62
+ 'logging_enabled' => array(
63
+ 'title' => __( 'Logging', 'woocommerce-shipstation' ),
64
+ 'label' => __( 'Enable Logging', 'woocommerce-shipstation' ),
65
+ 'type' => 'checkbox',
66
+ 'description' => __( 'Log all API interations.', 'woocommerce' ),
67
+ 'desc_tip' => true,
68
+ 'default' => 'yes',
69
+ ),
70
+ );
71
+
72
+ return $fields;
readme.txt ADDED
@@ -0,0 +1,52 @@
1
+ === WooCommerce ShipStation Gateway ===
2
+ Contributors: automattic, royho, akeda, mattyza, bor0, woothemes, dwainm, laurendavissmith001
3
+ Tags: shipping, woocommerce, automattic
4
+ Requires at least: 4.4
5
+ Tested up to: 4.9
6
+ Requires PHP: 5.6
7
+ Stable tag: 4.1.23
8
+ License: GPLv3
9
+ License URI: https://www.gnu.org/licenses/gpl-3.0.html
10
+
11
+ The official WooCommerce ShipStation plugin helps store owners integrate WooCommerce with ShipStation and expedite the shipping process.
12
+
13
+ == Description ==
14
+
15
+ ShipStation’s sophisticated automation features help you shave many hours off your fulfillment process. Print wirelessly and share your printer with ease thanks to ShipStation Connect. Run your business on-the-go with ShipStation Mobile, the industry’s only mobile app (free for iOS and Android) and do everything from creating orders to printing labels and emailing return labels all from your phone or tablet.
16
+
17
+ = Why choose ShipStation? =
18
+
19
+ ShipStation is a web-based shipping solution that streamlines the order fulfillment process for online retailers, handling everything from order import and batch label creation to customer communication. Advanced customization features allow ShipStation to fit businesses with any number of users or locations.
20
+
21
+ == Frequently Asked Questions ==
22
+
23
+ = Does ShipStation provide real-time shipping quotes that can be used at checkout? =
24
+
25
+ No. Store owners need a real-time shipping quote extension such as USPS, FedEx, UPS, etc. or have an alternate way to show shipping quotes (e.g., Flat rate charge).
26
+
27
+ = Does ShipStation send data when not being used (e.g., Free Shipping)? =
28
+
29
+ Yes, there isn’t conditional exporting. If the data is there, we export it!
30
+
31
+ = Why do multiple line items in an order on the WooCommerce side get combined when they reach ShipStation? =
32
+
33
+ This is most likely because unique Product SKUs have not been configured for each product and variation in the Store. To ensure that order line items show up correctly in ShipStation, we recommend assigning a unique SKU to each product as well as each variation within a product.
34
+
35
+ = Why do multiple line items in an order on the WooCommerce side get combined when they reach ShipStation? =
36
+
37
+ This is most likely because unique Product SKUs have not been configured for each product and variation in the Store. To ensure that order line items show up correctly in ShipStation, we recommend assigning a unique SKU to each product as well as each variation within a product.
38
+
39
+ = Where can I find documentation? =
40
+
41
+ For help setting up and configuring, please refer to our [user guide](https://docs.woocommerce.com/document/shipstation-for-woocommerce)
42
+
43
+ = Where can I get support or talk to other users? =
44
+
45
+ If you get stuck, you can ask for help in the Plugin Forum.
46
+
47
+ == Changelog ==
48
+
49
+ = 2018-09-12 - version 4.1.23 =
50
+ * Fix - Use correct textdomain on some strings.
51
+ * Tweak - Rework settings notice to correctly provide setup instructions.
52
+ * Tweak - Coding standards and making the plugin ready for wordpress.org.
woocommerce-shipstation.php ADDED
@@ -0,0 +1,87 @@
1
+ <?php
2
+ /**
3
+ * Plugin Name: WooCommerce - ShipStation Integration
4
+ * Plugin URI: https://woocommerce.com/products/shipstation-integration/
5
+ * Version: 4.1.23
6
+ * Description: Adds ShipStation label printing support to WooCommerce. Requires server DomDocument support.
7
+ * Author: WooCommerce
8
+ * Author URI: https://woocommerce.com/
9
+ * Text Domain: woocommerce-shipstation
10
+ * WC tested up to: 3.4
11
+ * WC requires at least: 2.6
12
+ *
13
+ */
14
+
15
+ if ( ! defined( 'ABSPATH' ) ) {
16
+ exit;
17
+ }
18
+
19
+ /**
20
+ * Include shipstation class.
21
+ *
22
+ * @since 1.0.0
23
+ */
24
+ function woocommerce_shipstation_init() {
25
+ define( 'WC_SHIPSTATION_VERSION', '4.1.23' );
26
+ define( 'WC_SHIPSTATION_FILE', __FILE__ );
27
+
28
+ if ( ! defined( 'WC_SHIPSTATION_EXPORT_LIMIT' ) ) {
29
+ define( 'WC_SHIPSTATION_EXPORT_LIMIT', 100 );
30
+ }
31
+
32
+ load_plugin_textdomain( 'woocommerce-shipstation', false, basename( dirname( __FILE__ ) ) . '/languages' );
33
+
34
+ include_once( 'includes/class-wc-shipstation-integration.php' );
35
+ include_once( 'includes/class-wc-shipstation-privacy.php' );
36
+ }
37
+
38
+ add_action( 'plugins_loaded', 'woocommerce_shipstation_init' );
39
+
40
+ /**
41
+ * Define integration.
42
+ *
43
+ * @since 1.0.0
44
+ *
45
+ * @param array $integrations Integrations.
46
+ * @return array Integrations.
47
+ */
48
+ function woocommerce_shipstation_load_integration( $integrations ) {
49
+ $integrations[] = 'WC_ShipStation_Integration';
50
+
51
+ return $integrations;
52
+ }
53
+
54
+ add_filter( 'woocommerce_integrations', 'woocommerce_shipstation_load_integration' );
55
+
56
+ /**
57
+ * Listen for API requests.
58
+ *
59
+ * @since 1.0.0
60
+ */
61
+ function woocommerce_shipstation_api() {
62
+ include_once( 'includes/class-wc-shipstation-api.php' );
63
+ }
64
+
65
+ add_action( 'woocommerce_api_wc_shipstation', 'woocommerce_shipstation_api' );
66
+
67
+ /**
68
+ * Added ShipStation custom plugin action links.
69
+ *
70
+ * @since 4.1.17
71
+ * @version 4.1.17
72
+ *
73
+ * @param array $links Links.
74
+ *
75
+ * @return array Links.
76
+ */
77
+ function woocommerce_shipstation_api_plugin_action_links( $links ) {
78
+ $setting_link = admin_url( 'admin.php?page=wc-settings&tab=integration&section=shipstation' );
79
+ $plugin_links = array(
80
+ '<a href="' . $setting_link . '">' . __( 'Settings', 'woocommerce-shipstation' ) . '</a>',
81
+ '<a href="https://woocommerce.com/my-account/tickets">' . __( 'Support', 'woocommerce-shipstation' ) . '</a>',
82
+ '<a href="https://docs.woocommerce.com/document/shipstation-for-woocommerce/">' . __( 'Docs', 'woocommerce-shipstation' ) . '</a>',
83
+ );
84
+
85
+ return array_merge( $plugin_links, $links );
86
+ }
87
+ add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'woocommerce_shipstation_api_plugin_action_links' );