├── License.md ├── README.md ├── data └── default_relationships.json ├── includes ├── controllers │ ├── core │ │ ├── class-nwsi-db.php │ │ ├── class-nwsi-salesforce-object-manager.php │ │ ├── class-nwsi-salesforce-token-manager.php │ │ ├── class-nwsi-salesforce-worker.php │ │ └── class-nwsi-salesforce.php │ └── utilites │ │ ├── class-nwsi-cryptor.php │ │ └── class-nwsi-utility.php ├── js │ └── nwsi-settings.js ├── libs │ ├── crypto │ │ └── Crypto.php │ └── wp-background-processing │ │ ├── wp-async-request.php │ │ └── wp-background-process.php ├── models │ ├── class-nwsi-order-item-model.php │ ├── class-nwsi-order-model.php │ ├── class-nwsi-product-model.php │ └── interface-nwsi-model.php ├── style │ └── nwsi-settings.css └── views │ ├── class-nwsi-admin-notice.php │ ├── class-nwsi-orders-view.php │ ├── class-nwsi-relationship-form.php │ ├── class-nwsi-relationships-table.php │ └── class-nwsi-settings.php └── neuralab-woocommerce-salesforce-integration.php /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Neuralab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WooCommerce-Salesforce-integration 2 | Salesforce and Woocommerce integration for WordPress. (A syncing engine for all sort of datasets and configurations) 3 | 4 | [![Demo & Setup Video](https://www.neuralab.net/wp-content/uploads/2016/08/WooCommerce-Salesforce-Github.jpg)](https://www.youtube.com/watch?v=W67pldjT_pE "Demo & Setup Video") 5 | 6 | # Installation procedure 7 | 1. Upload the ‘woocommerce-salesforce-integration’ directory to your ‘/wp-content/plugins/’ directory using FTP, SFTP or similar method. 8 | 9 | 2. Activate Neuralab WooCommerce Salesforce Integration plugin from your Wordpress Plugin page. 10 | 11 | 12 | # Setup procedure 13 | 14 | 1. After the plugin activation go to WooCommerce → Settings → Integration tab. There you’ll see two inputs that you need to fill in, Consumer Key and Consumer Secret, and Callback URL which you’ll copy and paste to Salesforce. 15 | 16 | 2. To obtain Consumer Key and Secret, login to your Salesforce account, go to Setup (top right corner) and on the left menu, Build section, choose Create and click on the Apps. You need to create a new Connected App so click on the New button (Connected Apps section). 17 | 18 | 3. Fill all required data and enable OAuth Settings. Give this new app next permissions: 19 | - Access and manage your data (api) 20 | - Perform requests on your behalf at any time (refresh_token, offline_access) 21 | 22 | [![Setup](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Setup-1.png)](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Setup-1.png "Setup") 23 | 24 | 25 | 4. When done, click Save and you’ll be redirect to new page similar to one below. Note that you’ll need to wait 10 to 15 minutes for Salesforce to update changes you made. 26 | 27 | [![Setup](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Setup-2.png)](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Setup-2.png "Setup") 28 | 29 | 5. Copy and paste Consumer Key and Secret to plugin Settings page and click on the Save Changes. 30 | 31 | 6. You’ll be asked to allow the plugin usage of your Salesforce data and after that redirected back to plugins Settings page where you’ll see rest of the interface with default relationships between WooCommerce and Salesforce objects. 32 | 33 | [![Setup](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Setup-3.png)](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Setup-3.png "Setup") 34 | 35 | 7. Edit and activate default relationships and start using the plugin. 36 | 37 | # Standard operations 38 | 39 | ## New relationship between Salesforce and WooCommerce objects 40 | 41 | 1. At the main Settings page (WooCommerce → Settings → Integration), under section New Relationship, choose Salesforce and WooCommerce objects that you want to connect and click on Add button. 42 | 43 | [![Setup](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-1.png)](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-1.png "Setup") 44 | 45 | 2. You’ll be redirect to a new form where you can see the list of Salesforce object fields on the left. For each field you can see type of field like string, boolean, picklist, if it’s required (red asterisk - *) or not, and belonging select of WooCommerce object fields. Make sure that the types of fields match! You can also add custom static values for each field. For example, for the fields type string or textarea it’s a custom string, for integer and double it’s a number, for boolean fields it’s a true or false values, etc. For a field type picklist you can only choose predefined Salesforce values. 46 | 47 | [![Setup](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-2.png)](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-2.png "Setup") 48 | 49 | 3. You’ll also need to define unique fields for new relationships. Unique fields are chosen Salesforce fields that have a role of unique identifier. For example, if we have a Salesforce’s Product object, we can mark Product name and Product code as unique fields because the plugin, before synchronization, checks if the object already exists in the Salesforce by values of unique fields, in this case Product name and Product code. So if Product was synchronized before, plugin will just obtain it’s Salesforce ID and leave the object as is. 50 | 51 | 4. Required Salesforce objects are objects connected to a Salesforce object that we chose for this relationship. Each object can have numerous connections that are required for his creation (you can find object definitions in your Salesforce account). For synchronization to be successful you’ll need to add required objects, mark them as active and create relationships from them. 52 | 53 | [![Setup](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-3.png)](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-3.png "Setup") 54 | 55 | 5. When done with defining relationship, click on Save changes button. 56 | 57 | ## Relationships management 58 | 59 | [![Setup](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-4.png)](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-4.png "Setup") 60 | 61 | On the main Settings page you can see table of already defined relationships. Plugin comes with default relationships between WooCommerce’s Order and Order Item objects, and default Salesforce objects like Order, Account, Product, Price Book, etc. Clicking on a relationship redirects you to a form for editing existing relationship. Already defined relationships can be deleted, activated, or deactivated. Deactivated relationships won’t be considered during the synchronization. 62 | 63 | If option Automatic order sync is checked, each placed order by the customer will be automatically synchronized. Otherwise, you can synchronize orders manually by going to WooCommerce’s orders preview, click on a order you want to synchronize and on right sidebar, you’ll see a metabox with Status and Save and sync order button. 64 | 65 | [![Setup](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-5.png)](https://www.neuralab.net/wp-content/uploads/2016/08/NWSI-Operating-5.png "Setup") 66 | 67 | If synchronization was successful status will change to success, otherwise it’ll change to failed with an error message that describes what went wrong. Usually it’s a missing object dependency or required field. 68 | 69 | 70 | Happy syncing! 71 | 72 | -------------------------------------------------------------------------------- /data/default_relationships.json: -------------------------------------------------------------------------------- 1 | [{"from_object":"Order","from_object_label":"Order","to_object":"Contact","to_object_label":"Contact","relationships":"[ {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_last_name\",\"to\":\"LastName\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_first_name\",\"to\":\"FirstName\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_city\",\"to\":\"OtherCity\"}, {\"source\":\"woocommerce\",\"type\":\"phone\",\"from\":\"billing_phone\",\"to\":\"Phone\"}, {\"source\":\"woocommerce\",\"type\":\"email\",\"from\":\"billing_email\",\"to\":\"Email\"}, {\"source\":\"sf-picklist\",\"type\":\"picklist\",\"from\":\"salesforce\",\"to\":\"LeadSource\",\"value\":\"Web\"} ]","required_sf_objects":"[{\"name\":\"Account\",\"label\":\"Account\",\"id\":\"AccountId\"}]","unique_sf_fields":"[\"Email\"]"},{"from_object":"Order","from_object_label":"Order","to_object":"Account","to_object_label":"Account","relationships":"[ {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_company\",\"to\":\"Name\"}, {\"source\":\"sf-picklist\",\"type\":\"picklist\",\"from\":\"salesforce\",\"to\":\"Type\",\"value\":\"Customer - Channel\"}, {\"source\":\"woocommerce\",\"type\":\"textarea\",\"from\":\"billing_address_1\",\"to\":\"BillingStreet\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_city\",\"to\":\"BillingCity\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_state\",\"to\":\"BillingState\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_postcode\",\"to\":\"BillingPostalCode\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_country\",\"to\":\"BillingCountry\"}, {\"source\":\"woocommerce\",\"type\":\"textarea\",\"from\":\"shipping_address_1\",\"to\":\"ShippingStreet\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"shipping_city\",\"to\":\"ShippingCity\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"shipping_state\",\"to\":\"ShippingState\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"shipping_postcode\",\"to\":\"ShippingPostalCode\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"shipping_country\",\"to\":\"ShippingCountry\"}, {\"source\":\"woocommerce\",\"type\":\"phone\",\"from\":\"billing_phone\",\"to\":\"Phone\"}, {\"source\":\"sf-picklist\",\"type\":\"picklist\",\"from\":\"salesforce\",\"to\":\"Industry\",\"value\":\"Other\"}, {\"source\":\"sf-picklist\",\"type\":\"picklist\",\"from\":\"salesforce\",\"to\":\"Rating\",\"value\":\"Warm\"}, {\"source\":\"sf-picklist\",\"type\":\"picklist\",\"from\":\"salesforce\",\"to\":\"AccountSource\",\"value\":\"Web\"} ]","required_sf_objects":"[]","unique_sf_fields":"[\"Name\"]"},{"from_object":"Order","from_object_label":"Order","to_object":"Order","to_object_label":"Order","relationships":"[ {\"source\":\"woocommerce\",\"type\":\"date\",\"from\":\"order_date\",\"to\":\"EffectiveDate\"}, {\"source\":\"sf-picklist\",\"type\":\"picklist\",\"from\":\"salesforce\",\"to\":\"Status\",\"value\":\"Draft\"}, {\"source\":\"woocommerce\",\"type\":\"textarea\",\"from\":\"custom\",\"to\":\"Description\",\"value\":null}, {\"source\":\"woocommerce\",\"type\":\"textarea\",\"from\":\"billing_address_1\",\"to\":\"BillingStreet\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_city\",\"to\":\"BillingCity\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_state\",\"to\":\"BillingState\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_postcode\",\"to\":\"BillingPostalCode\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"billing_country\",\"to\":\"BillingCountry\"}, {\"source\":\"woocommerce\",\"type\":\"textarea\",\"from\":\"shipping_address_1\",\"to\":\"ShippingStreet\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"shipping_city\",\"to\":\"ShippingCity\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"shipping_state\",\"to\":\"ShippingState\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"shipping_country\",\"to\":\"ShippingCountry\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"order_key\",\"to\":\"Name\"} ]","required_sf_objects":"[{\"name\":\"Account\",\"label\":\"Account\",\"id\":\"AccountId\"},{\"name\":\"Pricebook2\",\"label\":\"Price Book\",\"id\":\"Pricebook2Id\"}]","unique_sf_fields":"[\"\"]"},{"from_object":"Order Product","from_object_label":"Order Product","to_object":"Product2","to_object_label":"Product","relationships":"[ {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"name\",\"to\":\"Name\"}, {\"source\":\"woocommerce\",\"type\":\"string\",\"from\":\"sku\",\"to\":\"ProductCode\"}, {\"source\":\"woocommerce\",\"type\":\"textarea\",\"from\":\"description\",\"to\":\"Description\"}, {\"source\":\"custom\",\"type\":\"boolean\",\"from\":\"custom\",\"to\":\"IsActive\",\"value\":\"true\"} ]","required_sf_objects":"[]","unique_sf_fields":"[\"Name\",\"ProductCode\"]"},{"from_object":"Order Product","from_object_label":"Order Product","to_object":"PricebookEntry","to_object_label":"Pricebook Entry","relationships":"[ {\"source\":\"woocommerce\",\"type\":\"currency\",\"from\":\"regular_price\",\"to\":\"UnitPrice\"}, {\"source\":\"custom\",\"type\":\"boolean\",\"from\":\"custom\",\"to\":\"IsActive\",\"value\":\"true\"} ]","required_sf_objects":"[{\"name\":\"Pricebook2\",\"label\":\"Pricebook2\",\"id\":\"Pricebook2Id\"},{\"name\":\"Product2\",\"label\":\"Product2\",\"id\":\"Product2Id\"}]","unique_sf_fields":"[\"\"]"},{"from_object":"Order Product","from_object_label":"Order Product","to_object":"OrderItem","to_object_label":"Order Item","relationships":"[ {\"source\":\"woocommerce\",\"type\":\"double\",\"from\":\"qty\",\"to\":\"Quantity\"}, {\"source\":\"woocommerce\",\"type\":\"currency\",\"from\":\"sale_price\",\"to\":\"UnitPrice\"}, {\"source\":\"custom\",\"type\":\"date\",\"from\":\"custom\",\"to\":\"ServiceDate\",\"value\":\"current\"}, {\"source\":\"custom\",\"type\":\"string\",\"from\":\"custom\",\"to\":\"Description\",\"value\":\"From testing site\"} ]","required_sf_objects":"[{\"name\":\"PricebookEntry\",\"label\":\"PricebookEntry\",\"id\":\"PricebookEntryId\"},{\"name\":\"Order\",\"label\":\"Order\",\"id\":\"OrderId\"}]","unique_sf_fields":"[\"\"]"},{"from_object":"Order","from_object_label":"Order","to_object":"Pricebook2","to_object_label":"Price Book","relationships":"[ {\"source\":\"custom\",\"type\":\"string\",\"from\":\"custom\",\"to\":\"Name\",\"value\":\"WooCommerce price book\"}, {\"source\":\"custom\",\"type\":\"boolean\",\"from\":\"custom\",\"to\":\"IsActive\",\"value\":\"true\"} ]","required_sf_objects":"[]","unique_sf_fields":"[\"Name\"]"}] 2 | -------------------------------------------------------------------------------- /includes/controllers/core/class-nwsi-db.php: -------------------------------------------------------------------------------- 1 | rel_table_name = $wpdb->prefix . "nwsi_relationships"; 23 | } 24 | 25 | /** 26 | * Create relationship table 27 | */ 28 | public function create_relationship_table() { 29 | global $wpdb; 30 | 31 | $charset_collate = $wpdb->get_charset_collate(); 32 | $query = "CREATE TABLE $this->rel_table_name ( 33 | id MEDIUMINT(9) NOT NULL AUTO_INCREMENT, 34 | hash_key VARCHAR(128) NOT NULL, 35 | relationships TEXT NOT NULL, 36 | from_object VARCHAR(255) NOT NULL, 37 | from_object_label VARCHAR(255), 38 | to_object VARCHAR(255) NOT NULL, 39 | to_object_label VARCHAR(255), 40 | required_sf_objects TEXT, 41 | unique_sf_fields TEXT, 42 | date_updated TIMESTAMP, 43 | date_created TIMESTAMP, 44 | active TINYINT DEFAULT 0, 45 | UNIQUE KEY id (id), 46 | UNIQUE (hash_key) 47 | ) $charset_collate;"; 48 | 49 | require_once( ABSPATH . "wp-admin/includes/upgrade.php" ); 50 | dbDelta( $query ); 51 | } 52 | 53 | /** 54 | * Delete relationship table. 55 | */ 56 | public function delete_relationship_table() { 57 | global $wpdb; 58 | 59 | $wpdb->query( "DROP TABLE IF EXISTS " . $this->rel_table_name ); 60 | } 61 | 62 | /** 63 | * Check if the relationship table is empty. 64 | * 65 | * @return boolean 66 | */ 67 | public function is_relationship_table_empty() { 68 | global $wpdb; 69 | 70 | $query = "SELECT * FROM $this->rel_table_name LIMIT 1"; 71 | 72 | $results = $wpdb->get_results( $query ); 73 | if ( empty( $results ) ) { 74 | return true; 75 | } 76 | return false; 77 | } 78 | 79 | /** 80 | * Save new relationship to the database and return true if successful or 81 | * false otherwise. 82 | * 83 | * @param string $from Name of WooCommerce object. 84 | * @param string $from_label Label of WooCommerce object. 85 | * @param string $to Name of Salesforce object. 86 | * @param string $to_label Label of Salesforce object. 87 | * @param mixed $data Relationships array or relationships JSON string. 88 | * @param string $required_sf_objects Defaults to empty string. 89 | * @param string $unique_sf_fields Defaults to empty string. 90 | * @return boolean 91 | */ 92 | public function save_new_relationship( $from, $from_label, $to, $to_label, $data, $required_sf_objects = "", $unique_sf_fields = "" ) { 93 | if ( empty( $from ) || empty( $to ) || empty( $data ) ) { 94 | return false; 95 | } 96 | 97 | if ( is_array( $data ) ) { 98 | $relationships = $this->relationship_data_to_json( $data ); 99 | } else { 100 | $relationships = $data; 101 | } 102 | 103 | if ( empty( $required_sf_objects ) ) { 104 | $required_sf_objects = $this->get_required_sf_objects( $data ); 105 | } 106 | 107 | if ( empty( $unique_sf_fields ) ) { 108 | $unique_sf_fields = $this->get_unique_sf_fields( $data ); 109 | } 110 | 111 | $key = md5( date('Y-m-d H:i:s') . $from . $to ); 112 | 113 | global $wpdb; 114 | $wpdb->insert( $this->rel_table_name, array( 115 | "from_object" => $from, 116 | "from_object_label" => $from_label, 117 | "to_object" => $to, 118 | "to_object_label" => $to_label, 119 | "relationships" => $relationships, 120 | "hash_key" => $key, 121 | "required_sf_objects" => $required_sf_objects, 122 | "unique_sf_fields" => $unique_sf_fields, 123 | "date_created" => date('Y-m-d H:i:s'), 124 | "date_updated" => date('Y-m-d H:i:s') 125 | ) ); 126 | return true; 127 | } 128 | 129 | /** 130 | * Update relationship and return true if successful or false otherwise. 131 | * 132 | * @param string $key 133 | * @param array $data 134 | * @return boolean 135 | */ 136 | public function update_relationship( $key, $data ) { 137 | $relationships = $this->relationship_data_to_json( $data ); 138 | 139 | $required_sf_objects = $this->get_required_sf_objects( $data ); 140 | $unique_sf_fields = $this->get_unique_sf_fields( $data ); 141 | 142 | if ( empty( $key ) && ( empty( $relationships ) || empty( $required_sf_objects ) || empty( $unique_sf_fields ) ) ) { 143 | return false; 144 | } 145 | 146 | $update_data = array( 147 | "date_updated" => date('Y-m-d H:i:s'), 148 | "required_sf_objects" => $required_sf_objects, 149 | "unique_sf_fields" => $unique_sf_fields, 150 | ); 151 | 152 | if ( !empty( $relationships ) ) { 153 | $update_data["relationships"] = $relationships; 154 | } 155 | 156 | global $wpdb; 157 | $wpdb->update( 158 | $this->rel_table_name, 159 | $update_data, 160 | array( "hash_key" => $key ) 161 | ); 162 | return true; 163 | } 164 | 165 | /** 166 | * Extract unique salesforce fields from the provided array and return them 167 | * as array or string if $to_json is set to true. 168 | * 169 | * @param array $data 170 | * @param string $to_json Defaults to true. 171 | * @return array|string 172 | */ 173 | private function get_unique_sf_fields( $data, $to_json = true ) { 174 | 175 | $unique_sf_fields = array(); 176 | $i = 0; 177 | 178 | while( array_key_exists( "uniqueSfField-" . $i, $data ) ) { 179 | if ( $data["uniqueSfField-" . $i] != "none" ) { 180 | array_push( $unique_sf_fields, $data["uniqueSfField-" . $i] ); 181 | } 182 | $i++; 183 | } 184 | if ( $to_json ) { 185 | return json_encode( $unique_sf_fields ); 186 | } 187 | return $unique_sf_fields; 188 | } 189 | 190 | /** 191 | * Extract required salesforce objects from provided array and return them in 192 | * form of array or string if $to_json is set to true. 193 | * 194 | * @param array $data 195 | * @param string $to_json Defaults to true. 196 | * @return array|string 197 | */ 198 | private function get_required_sf_objects( $data, $to_json = true ) { 199 | 200 | $required_sf_objects = array(); 201 | $i = 0; 202 | 203 | while( array_key_exists( "requiredSfObject-" . $i, $data ) ) { 204 | if ( array_key_exists( "requiredSfObjectIsActive-" . $i, $data ) ) { 205 | $parts = explode( "|", $data["requiredSfObject-" . $i] ); 206 | if ( count( $parts ) < 3 ) { 207 | continue; 208 | } 209 | $required_object = array( 210 | "name" => $parts[0], 211 | "label" => $parts[1], 212 | "id" => $parts[2] 213 | ); 214 | array_push( $required_sf_objects, $required_object ); 215 | } 216 | $i++; 217 | } 218 | if ( $to_json ) { 219 | return json_encode( $required_sf_objects ); 220 | } 221 | return $required_sf_objects; 222 | } 223 | 224 | /** 225 | * Transform relationship array to json string. Returns empty string if no 226 | * relationships provided. 227 | * 228 | * @param array $data 229 | * @return string 230 | */ 231 | private function relationship_data_to_json( $data ) { 232 | 233 | $relationships = array(); 234 | for( $i = 0; $i < intval( $data["numOfFields"] ); $i++ ) { 235 | if ( !empty( $data[ "wcField-" . $i ] ) && $data[ "wcField-" . $i ] != "none" ) { 236 | $temp = array( 237 | "source" => $data[ "wcField-" . $i . "-source" ], 238 | "type" => $data[ "wcField-" . $i . "-type" ], 239 | "from" => $data[ "wcField-" . $i ], 240 | "to" => $data[ "sfField-" . $i ] 241 | ); 242 | 243 | if ( strpos( $data[ "wcField-" . $i ], "custom" ) !== false ) { 244 | $temp["from"] = "custom"; 245 | if ( $temp["type"] == "boolean" ) { 246 | $value = explode( "-", $data[ "wcField-" . $i ] )[1]; 247 | if ( !is_null( $value ) ) { 248 | $temp["value"] = $value; 249 | } 250 | } else if ( $temp["type"] == "date" ) { 251 | $temp["value"] = "current"; 252 | } else { 253 | $temp["value"] = $data[ "wcField-" . $i . "-custom" ]; 254 | } 255 | } else if ( strpos( $data[ "wcField-" . $i . "-source" ], "sf-picklist" ) !== false ) { 256 | $temp["value"] = $data[ "wcField-" . $i ]; 257 | $temp["from"] = "salesforce"; 258 | } 259 | 260 | array_push( $relationships, $temp ); 261 | } 262 | } 263 | 264 | if ( empty( $relationships ) ) { 265 | return ""; 266 | } 267 | 268 | return json_encode( $relationships ); 269 | } 270 | 271 | /** 272 | * Return relationship with provided hash key 273 | * @param string $key 274 | * @return array 275 | */ 276 | public function get_relationship_by_key( $key ) { 277 | global $wpdb; 278 | 279 | $query = "SELECT relationships, from_object, from_object_label, to_object, to_object_label, " 280 | . "required_sf_objects, unique_sf_fields FROM $this->rel_table_name WHERE hash_key=%s"; 281 | 282 | $response = $wpdb->get_results( $wpdb->prepare( $query, $key ) ); 283 | 284 | if ( empty( $response ) ) { 285 | return null; 286 | } else { 287 | return $response[0]; 288 | } 289 | } 290 | 291 | /** 292 | * Return all active relationships. 293 | * 294 | * @return array 295 | */ 296 | public function get_active_relationships() { 297 | global $wpdb; 298 | 299 | $query = "SELECT from_object, to_object, relationships, active, required_sf_objects, unique_sf_fields "; 300 | $query .= "FROM $this->rel_table_name WHERE active=1"; 301 | 302 | return $wpdb->get_results( $query ); 303 | } 304 | 305 | /** 306 | * Return all relationships. 307 | * 308 | * @return array 309 | */ 310 | public function get_relationships() { 311 | global $wpdb; 312 | 313 | $query = "SELECT id, date_created, date_updated, from_object, "; 314 | $query .= "from_object_label, to_object, to_object_label, hash_key, active "; 315 | $query .= "FROM $this->rel_table_name"; 316 | 317 | return $wpdb->get_results( $query ); 318 | } 319 | 320 | /** 321 | * Delete relationships and return true if successful or false otherwise. 322 | * 323 | * @param array $ids 324 | * @return boolean 325 | */ 326 | public function delete_relationships_by_id( $ids ) { 327 | global $wpdb; 328 | 329 | $sql_ids = $this->sanitize_ids( $ids ); 330 | $query = "DELETE FROM $this->rel_table_name WHERE id IN (" . implode( ",", $sql_ids ) . ")"; 331 | 332 | return $wpdb->query( $query ); 333 | } 334 | 335 | /** 336 | * Activate relationships 337 | * @param $ids - array of relationships ids 338 | * @return boolean 339 | */ 340 | public function activate_relationships_by_id( $ids ) { 341 | return $this->set_active_attribute( 1, $ids ); 342 | } 343 | 344 | /** 345 | * Deactivate relationships 346 | * @param array $ids - relationships ids 347 | * @return boolean 348 | */ 349 | public function deactivate_relationships_by_id( $ids ) { 350 | return $this->set_active_attribute( 0, $ids ); 351 | } 352 | 353 | /** 354 | * Set active attribute to given value. 355 | * 356 | * @param int $value 357 | * @param array $ids 358 | * @return boolean 359 | */ 360 | private function set_active_attribute( $value, $ids ) { 361 | global $wpdb; 362 | 363 | $sql_ids = $this->sanitize_ids( $ids ); 364 | $query = "UPDATE $this->rel_table_name SET active=" . $value . " WHERE id IN (" . implode( ",", $sql_ids ) . ")"; 365 | 366 | return $wpdb->query( $query ); 367 | } 368 | 369 | /** 370 | * Return array of sanitized ids. 371 | * 372 | * @param array $ids 373 | * @return array 374 | */ 375 | private function sanitize_ids( $ids ) { 376 | $sql_ids = array(); 377 | for ( $i = 0; $i < count( $ids ); $i++ ) { 378 | array_push( $sql_ids, intval( $ids[$i] ) ); 379 | } 380 | 381 | return $sql_ids; 382 | } 383 | 384 | /** 385 | * Filter raw keys extracted from the database for order, product, etc. 386 | * @param array $keys_raw 387 | * @return array 388 | */ 389 | private function filter_meta_keys( $keys_raw ) { 390 | $keys = array(); 391 | foreach ($keys_raw as $key_container) { 392 | $pos = strpos( $key_container[0], "_" ); 393 | if ( $pos !== false ) { 394 | array_push( $keys, substr_replace( $key_container[0], "", $pos, strlen( "_" ) ) ); 395 | } else { 396 | array_push( $keys, $key_container[0] ); 397 | } 398 | } 399 | return $keys; 400 | } 401 | 402 | /** 403 | * Return WC_Order magic properties from orders in DB. 404 | * 405 | * @return array 406 | */ 407 | public function get_order_meta_keys() { 408 | global $wpdb; 409 | 410 | $query = "SELECT DISTINCT( meta_key ) FROM " . $wpdb->prefix . "postmeta WHERE post_id=("; 411 | $query .= "SELECT ID FROM " . $wpdb->prefix . "posts WHERE post_type='shop_order' "; 412 | $query .= "ORDER BY ID DESC )"; 413 | 414 | $keys_raw = $wpdb->get_results( $query, ARRAY_N ); 415 | return $this->filter_meta_keys( $keys_raw ); 416 | } 417 | 418 | /** 419 | * Return WC_Product magic properties from products in DB. 420 | * 421 | * @return array 422 | */ 423 | public function get_product_meta_keys() { 424 | global $wpdb; 425 | 426 | $query = "SELECT DISTINCT( meta_key ) FROM " . $wpdb->prefix . "postmeta WHERE post_id IN ("; 427 | $query .= "SELECT ID FROM " . $wpdb->prefix . "posts WHERE post_type='product' OR post_type='product_variation')"; 428 | 429 | $keys_raw = $wpdb->get_results( $query, ARRAY_N ); 430 | return $this->filter_meta_keys( $keys_raw ); 431 | } 432 | 433 | /** 434 | * Return WC_Order_Item properties from order item entry in DB. 435 | * 436 | * @return array 437 | */ 438 | public function get_order_item_meta_keys() { 439 | global $wpdb; 440 | 441 | $query = "SELECT DISTINCT ( meta_key ) FROM " . $wpdb->prefix . "woocommerce_order_itemmeta"; 442 | 443 | return $wpdb->get_results( $query ); 444 | } 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /includes/controllers/core/class-nwsi-salesforce-object-manager.php: -------------------------------------------------------------------------------- 1 | get_object( $name, $get_values ); 44 | // if object with the same values of unique field/s exists, return his ID 45 | if ( $get_object_response["success"] ) { 46 | // if an order exists, delete order's items so they can be added after 47 | if ( $name === "Order" ) { 48 | $this->delete_orders_items( $get_object_response["id"] ); 49 | } 50 | return $get_object_response; 51 | } 52 | } 53 | } 54 | 55 | // post new object 56 | $url = $this->instance_url . "/services/data/" . $this->api_version . "/sobjects/" . $name; 57 | $sf_response = $this->get_response( $url, true, "post", json_encode( $values ), "application/json" ); 58 | 59 | if ( !$no_error_handling ) { 60 | $error_response = $this->get_error_status( $sf_response ); 61 | 62 | if ( $error_response == "solved" ) { 63 | $sf_response = $this->get_response( $url, true, "post", json_encode( $values ), "application/json" ); 64 | } else if ( $error_response == "duplicate value" ) { 65 | $object_val = $this->get_object( $name, $values ); 66 | return $object_val; 67 | } else if ( $error_response == "no standard price" ) { 68 | $standard_price_book_id = $this->get_standard_price_book_id(); 69 | 70 | $values["Pricebook2Id"] = $standard_price_book_id; 71 | return $this->create_object( $name, $values, $unique_sf_fields, true ); 72 | } else if ( $error_response == "failed" ) { 73 | // do nothing, for now... 74 | } 75 | } 76 | 77 | if ( isset( $sf_response["success"] ) && $sf_response["success"] ) { 78 | $response["success"] = true; 79 | $response["id"] = $sf_response["id"]; 80 | } else if ( isset( $sf_response["done"] ) && $sf_response["done"] && count( $sf_response["records"] ) > 0 ) { 81 | $response["success"] = true; 82 | $response["id"] = $sf_response["records"][0]["Id"]; 83 | } else { 84 | $response = $this->set_response_error_message( $sf_response ); 85 | } 86 | 87 | return $response; 88 | } 89 | 90 | /** 91 | * Return object's ID that has provided values. 92 | * 93 | * @param string $name 94 | * @param array $values 95 | * @return array 96 | */ 97 | private function get_object( string $name, array $values ) { 98 | $query = "SELECT Id FROM " . $name . " WHERE "; 99 | 100 | $where_query_part = ""; 101 | foreach ( $values as $key => $val ) { 102 | // checking for boolean attributes is not needed! 103 | if ( is_bool( $val ) || $val == "true" || $val == "false" ) { 104 | continue; 105 | } 106 | 107 | if ( !empty( $where_query_part ) ) { 108 | $where_query_part .= " AND "; 109 | } 110 | 111 | if ( is_numeric( $val ) ) { 112 | $where_query_part .= $key . "=" . $val; 113 | } else { 114 | $where_query_part .= $key . "='" . $val . "'"; 115 | } 116 | } 117 | $query .= $where_query_part; 118 | 119 | $url = $this->instance_url . "/services/data/" . $this->api_version . "/query?q=" . urlencode( $query ); 120 | $sf_response = $this->get_response( $url ); 121 | $response = array(); 122 | 123 | if ( isset( $sf_response["done"] ) && $sf_response["done"] && count( $sf_response["records"] ) > 0 ) { 124 | $response["success"] = true; 125 | $response["id"] = $sf_response["records"][0]["Id"]; 126 | } else { 127 | $response = $this->set_response_error_message( $sf_response ); 128 | } 129 | return $response; 130 | } 131 | 132 | /** 133 | * Extract error message and code from salesforce response to appropriate array 134 | * @param array $sf_response 135 | * @return array 136 | */ 137 | private function set_response_error_message( array $sf_response ) { 138 | $response["success"] = false; 139 | if ( !empty( $sf_response[0]["errorCode"] ) && !empty( $sf_response[0]["message"]) ) { 140 | $response["error_code"] = $sf_response[0]["errorCode"]; 141 | $response["error_message"] = $sf_response[0]["message"]; 142 | } else { 143 | $response["error_code"] = "UNKNOWN"; 144 | $response["error_message"] = "Unknown error occurred."; 145 | } 146 | return $response; 147 | } 148 | 149 | /** 150 | * Return Standard Price Book's ID 151 | * @return string 152 | */ 153 | public function get_standard_price_book_id() { 154 | $query = "SELECT Id FROM Pricebook2 WHERE IsStandard=true"; 155 | 156 | $url = $this->instance_url . "/services/data/" . $this->api_version . "/query?q=" . urlencode( $query ); 157 | $response = $this->get_response( $url ); 158 | $error_response = $this->get_error_status( $response ); 159 | if ( $error_response == "solved" ) { 160 | $response = $this->get_response( $url ); 161 | } else if ( $error_response == "failed" ) { 162 | return null; 163 | } 164 | 165 | if ( $response["done"] ) { 166 | return $response["records"][0]["Id"]; 167 | } else { 168 | return null; 169 | } 170 | } 171 | 172 | /** 173 | * Return array of all available Salesforce objects or null in case of failure 174 | * @return array 175 | */ 176 | public function get_all_objects() { 177 | $url = $this->instance_url . "/services/data/" . $this->api_version . "/sobjects/"; 178 | $response = $this->get_response( $url ); 179 | 180 | $error_response = $this->get_error_status( $response ); 181 | if ( $error_response == "solved" ) { 182 | $response = $this->get_response( $url ); 183 | } else if ( $error_response == "failed" ) { 184 | return null; 185 | } 186 | 187 | return $response; 188 | } 189 | 190 | /** 191 | * Return object description or null in case of failure 192 | * @param string $object_name 193 | * @return array 194 | */ 195 | public function get_object_description( $object_name ) { 196 | $url = $this->instance_url . "/services/data/" . $this->api_version . "/sobjects/" . $object_name . "/describe/" ; 197 | $response = $this->get_response( $url ); 198 | 199 | $error_response = $this->get_error_status( $response ); 200 | if ( $error_response == "solved" ) { 201 | $response = $this->get_response( $url ); 202 | } else if ( $error_response == "failed" ) { 203 | return null; 204 | } 205 | 206 | return $response; 207 | } 208 | 209 | /** 210 | * Call API and returns array of products with unit prices 211 | * @return array 212 | */ 213 | private function query_products() { 214 | $query = "SELECT Product2.Id, Product2.Name, Product2.ProductCode, Product2.Description, " 215 | . "Product2.isActiveOnlineProduct__c, PricebookEntry.UnitPrice FROM PricebookEntry"; 216 | 217 | $url = $this->instance_url . "/services/data/" . $this->api_version . "/query?q=" . urlencode( $query ); 218 | 219 | return $this->get_response( $url ); 220 | } 221 | 222 | /** 223 | * Return array of products with unit prices 224 | * @return array or null if failed 225 | */ 226 | public function get_products() { 227 | $response = $this->query_products(); 228 | 229 | $error_response = $this->get_error_status( $response ); 230 | if ( $error_response == "solved" ) { 231 | $response = $this->query_products(); 232 | } else if ( $error_response == "failed" ) { 233 | return null; 234 | } 235 | 236 | try { 237 | $products = array(); 238 | foreach( $response["records"] as $product ) { 239 | if ( !$this->is_product_id_in_array( $product["Product2"]["Id"], $products ) ) { 240 | array_push( $products, array( 241 | "id" => trim( $product["Product2"]["Id"] ), 242 | "name" => $product["Product2"]["Name"], 243 | "code" => $product["Product2"]["ProductCode"], 244 | "unit_price" => $product["UnitPrice"], 245 | "is_active" => $product["Product2"]["isActiveOnlineProduct__c"], 246 | "wand_id" => $product["Product2"]["Product_4D_Wand_ID__c"] 247 | ) ); 248 | } 249 | } 250 | return $products; 251 | } catch( Exception $e ) { 252 | return null; 253 | } 254 | } 255 | 256 | /** 257 | * Return true if there is a provided id multidimensional products array 258 | * @param string $product_id 259 | * @param array $products (2D) 260 | * @return boolean 261 | */ 262 | private function is_product_id_in_array( $product_id, $products ) { 263 | foreach( $products as $product ) { 264 | if ( $product["id"] == $product_id ) { 265 | return true; 266 | } 267 | } 268 | return false; 269 | } 270 | 271 | /** 272 | * Scan API response array for errors and call appropriate handle method if any 273 | * @param array $response 274 | * @return string "solved", "failed", "none", "duplicate value" 275 | */ 276 | private function get_error_status( $response ) { 277 | 278 | if ( empty( $response ) ) { 279 | return "failed"; 280 | } 281 | 282 | if ( array_key_exists( 0, $response ) && array_key_exists( "errorCode", $response[0] ) ) { 283 | if ( $response[0]["errorCode"] == "INVALID_SESSION_ID" ) { 284 | if ( $this->revalidate_token() ) { 285 | return "solved"; 286 | } else { 287 | return "failed"; 288 | } 289 | } else if ( $response[0]["errorCode"] == "DUPLICATE_VALUE" || 290 | $response[0]["errorCode"] == "FIELD_INTEGRITY_EXCEPTION" ) { 291 | return "duplicate value"; 292 | } else if ( $response[0]["errorCode"] == "STANDARD_PRICE_NOT_DEFINED" ) { 293 | return "no standard price"; 294 | } 295 | 296 | return "failed"; 297 | } 298 | 299 | return "none"; 300 | } 301 | 302 | /** 303 | * Delete all OrderItems connected to an Order with given ID 304 | * @param string $order_id 305 | */ 306 | private function delete_orders_items( $order_id ) { 307 | $query = "SELECT Id FROM OrderItem WHERE OrderId='" . $order_id . "'"; 308 | 309 | $url = $this->instance_url . "/services/data/" . $this->api_version . "/query?q=" . urlencode( $query ); 310 | $order_items_response = $this->get_response( $url ); 311 | foreach( $order_items_response["records"] as $order_item ) { 312 | $url_delete = $this->instance_url . "/services/data/" . $this->api_version . "/sobjects/OrderItem/" . urlencode( $order_item["Id"] ); 313 | $this->get_response( $url_delete, true, "delete" ); 314 | } 315 | 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /includes/controllers/core/class-nwsi-salesforce-token-manager.php: -------------------------------------------------------------------------------- 1 | login_uri = get_option("woocommerce_nwsi_login_url"); 28 | 29 | require_once ( NWSI_DIR_PATH . "includes/controllers/utilites/class-nwsi-cryptor.php" ); 30 | $this->cryptor = new NWSI_Cryptor(); 31 | 32 | $this->redirect_uri = admin_url(esc_attr__("admin.php?page=wc-settings&tab=integration§ion=nwsi"), "https"); 33 | $this->access_token = $this->load_access_token(); 34 | $this->refresh_token = $this->load_refresh_token(); 35 | $this->instance_url = get_option( "woocommerce_nwsi_instance_url" ); 36 | 37 | 38 | $settings = get_option( "woocommerce_nwsi_settings", null ); 39 | if ( !empty( $settings ) ) { 40 | $this->consumer_key = $settings["consumer_key"]; 41 | $this->consumer_secret = $settings["consumer_secret"]; 42 | } 43 | } 44 | 45 | /** 46 | * Revalidate token fron Salesforce API 47 | * @return boolean - true in case of succesful revalidation 48 | */ 49 | protected function revalidate_token() { 50 | $params = "grant_type=refresh_token" . 51 | "&client_id=" . urlencode( $this->consumer_key ) . 52 | "&client_secret=" . urlencode( $this->consumer_secret ) . 53 | "&refresh_token=" . urlencode( $this->refresh_token ); 54 | 55 | $url = $this->login_uri . "/services/oauth2/token"; 56 | $response = $this->get_response( $url, false, "post", $params ); 57 | if ( !isset( $response["access_token"] ) || empty( $response["access_token"] ) 58 | || !isset( $response["instance_url"] ) || empty( $response["instance_url"] ) ) { 59 | return false; 60 | } 61 | 62 | $this->access_token = $response["access_token"]; 63 | $this->instance_url = $response["instance_url"]; 64 | 65 | $this->save_access_token( $this->access_token ); 66 | update_option( "woocommerce_nwsi_instance_url", $this->instance_url ); 67 | 68 | return true; 69 | } 70 | 71 | /** 72 | * Call Salesforce API with provided code and saves obtained instance url, 73 | * access and refresh token in DB 74 | * @param string $code 75 | * @return string (access_token_error | instance_url_error | success) 76 | */ 77 | public function get_access_token( $code ) { 78 | $url = $this->login_uri . "/services/oauth2/token"; 79 | 80 | $params = "code=" . $code 81 | . "&grant_type=authorization_code" 82 | . "&client_id=" . $this->consumer_key 83 | . "&client_secret=" . $this->consumer_secret 84 | . "&redirect_uri=" . urlencode( $this->redirect_uri ); 85 | 86 | $response = $this->get_response( $url, false, "get", $params ); 87 | 88 | if ( !isset( $response["access_token"] ) || empty( $response["access_token"] ) ) { 89 | return "access_token_error"; 90 | } 91 | 92 | if ( !isset( $response["instance_url"] ) || empty( $response["instance_url"] ) ) { 93 | return "instance_url_error"; 94 | } 95 | 96 | $this->access_token = $response["access_token"]; 97 | $this->refresh_token = $response["refresh_token"]; 98 | $this->instance_url = $response["instance_url"]; 99 | 100 | $this->save_access_token( $this->access_token ); 101 | $this->save_refresh_token( $this->refresh_token ); 102 | update_option( "woocommerce_nwsi_instance_url", $this->instance_url ); 103 | 104 | $this->update_connection_hash(); 105 | return "success"; 106 | } 107 | 108 | /** 109 | * Create or update a hash string that identifies current used connection 110 | */ 111 | private function update_connection_hash() { 112 | $connection_hash = wp_hash( $this->consumer_key . $this->consumer_secret . $this->login_uri ); 113 | update_option( "woocommerce_nwsi_connection_hash", $connection_hash ); 114 | } 115 | 116 | /** 117 | * Connection string is valid if consumer key, secret, and login URL are the 118 | * same as one when connection hash were created 119 | * @return boolean 120 | */ 121 | public function is_connection_hash_valid() { 122 | $new_connection_hash = wp_hash( $this->consumer_key . $this->consumer_secret . $this->login_uri ); 123 | $old_connection_hash = get_option( "woocommerce_nwsi_connection_hash" ); 124 | 125 | return $new_connection_hash === $old_connection_hash; 126 | } 127 | 128 | /** 129 | * Set login URI used for token management 130 | * @param string $login_uri 131 | */ 132 | public function set_login_uri( $login_uri ) { 133 | if ( !empty( $login_uri ) && is_string( $login_uri ) ) { 134 | $this->login_uri = $login_uri; 135 | } 136 | } 137 | 138 | /** 139 | * Return Salesforce authentication page URL 140 | * @param string $consumer_key 141 | * @param string $consumer_secret 142 | */ 143 | public function redirect_to_salesforce( $consumer_key, $consumer_secret ) { 144 | 145 | if ( empty( $consumer_key ) || empty( $consumer_secret ) ) { 146 | return ""; 147 | } 148 | if ( $this->is_connection_hash_valid() ) { 149 | return ""; 150 | } 151 | 152 | return $this->get_oauth_url( $consumer_key ); 153 | } 154 | 155 | /** 156 | * Return URL string required for obtaining access token 157 | * @param string $consumer_key 158 | * @return string 159 | */ 160 | private function get_oauth_url( $consumer_key ) { 161 | return $this->login_uri 162 | . "/services/oauth2/authorize?response_type=code&client_id=" 163 | . $consumer_key . "&redirect_uri=" . urlencode( $this->redirect_uri ); 164 | } 165 | 166 | /** 167 | * Escape special characters for SOQL 168 | * @param string $str 169 | * @return string 170 | */ 171 | protected function soql_string_literal( $str ) { 172 | // ? & | ! { } [ ] ( ) ^ ~ * : \ " ' + - 173 | $characters = array( 174 | '\\', '?' , '&' , '|' , '!' , '{' , '}' , '[' , ']' , '(' , ')' , '^' , '~' , '*' , ':' , '"' , '\'', '+' , '-' 175 | ); 176 | $replacement = array( 177 | '\\\\', '\?', '\&', '\|', '\!', '\{', '\}', '\[', '\]', '\(', '\)', '\^', '\~', '\*', '\:', '\"', '\\\'', '\+', '\-' 178 | ); 179 | return str_replace( $characters, $replacement, $str ); 180 | } 181 | 182 | /** 183 | * Return true if it has access token. Doesn't mean that it's valid. 184 | * @return boolean 185 | */ 186 | public function has_access_token() { 187 | $access_token = $this->load_access_token(); 188 | if ( empty( $access_token ) ) { 189 | return false; 190 | } else if ( is_bool( $access_token ) ) { 191 | return false; 192 | } else { 193 | return true; 194 | } 195 | } 196 | 197 | /** 198 | * Load and decrypt access token from database 199 | * @return mixed - false if failed or string 200 | */ 201 | public function load_access_token() { 202 | return $this->cryptor->decrypt( get_option( "woocommerce_nwsi_access_token" ), true ); 203 | } 204 | 205 | /** 206 | * Encrypt and save access token to database 207 | * @param string $access_token 208 | */ 209 | public function save_access_token( $access_token ) { 210 | $crypted_token = $this->cryptor->encrypt( $access_token, true ); 211 | update_option( "woocommerce_nwsi_access_token", $crypted_token ); 212 | } 213 | 214 | /** 215 | * Load and decrypt refresh token from database 216 | * @return mixed - false if failed or string 217 | */ 218 | public function load_refresh_token() { 219 | return $this->cryptor->decrypt( get_option( "woocommerce_nwsi_refresh_token" ), true ); 220 | } 221 | 222 | /** 223 | * Encrypt and save refresh token to database 224 | * @param string $refresh_token 225 | */ 226 | public function save_refresh_token( $refresh_token ) { 227 | $crypted_token = $this->cryptor->encrypt( $refresh_token, true ); 228 | update_option( "woocommerce_nwsi_refresh_token", $crypted_token ); 229 | } 230 | 231 | 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /includes/controllers/core/class-nwsi-salesforce-worker.php: -------------------------------------------------------------------------------- 1 | db = new NWSI_DB(); 30 | $this->sf = new NWSI_Salesforce_Object_Manager(); 31 | } 32 | 33 | /** 34 | * Init async order processing procedure 35 | * @param int $order_id 36 | */ 37 | public function process_order( $order_id ) { 38 | $this->data( array( "order_id" => $order_id ) ); 39 | $this->dispatch(); 40 | } 41 | /** 42 | * Extract order id from $_POST, process order and send data to Salesforce 43 | * @override 44 | */ 45 | // protected function handle() { 46 | public function handle() { 47 | if ( array_key_exists( "order_id", $_POST ) && !empty( $_POST["order_id"] ) ) { 48 | $order_id = $_POST["order_id"]; 49 | } else { 50 | return; 51 | } 52 | $this->handle_order( $order_id ); 53 | } 54 | 55 | /** 56 | * Process order and send data to Salesforce. 57 | * 58 | * @param int $order_id 59 | */ 60 | public function handle_order( int $order_id ) { 61 | $is_success = true; 62 | $error_message = array(); 63 | $relationships = $this->db->get_active_relationships(); 64 | 65 | if ( empty( $relationships ) ) { 66 | update_post_meta( $order_id, "_sf_sync_status", "failed" ); 67 | array_push( $error_message, "NWSI: No defined relationships." ); 68 | update_post_meta( $order_id, "_sf_sync_error_message", json_encode( $error_message ) ); 69 | return; 70 | } 71 | 72 | $relationships = $this->prioritize_relationships( $relationships ); 73 | 74 | // contains ids of created objects 75 | $response_ids = array(); 76 | 77 | $order = new NWSI_Order_Model( $order_id ); 78 | $products = $this->get_products_from_order( $order ); 79 | 80 | foreach( $relationships as $relationship ) { 81 | // get relationship connections 82 | $connections = json_decode( $relationship->relationships ); 83 | 84 | if ( $relationship->from_object === "Order" ) { 85 | // process order 86 | $values = $this->get_values( $connections, $order ); 87 | $this->set_dependencies( 88 | $relationship->to_object, $values, 89 | json_decode( $relationship->required_sf_objects ), 90 | $response_ids, $relationship->from_object 91 | ); 92 | 93 | if ( !empty( $values ) ) { 94 | $response = $this->send_to_salesforce( 95 | $relationship->to_object, $values, 96 | json_decode( $relationship->unique_sf_fields ), $response_ids 97 | ); 98 | 99 | if ( !$response["success"] ) { 100 | $is_success = false; 101 | array_push( $error_message, $response["error_message"] ); 102 | break; // no need to continue 103 | } 104 | } 105 | 106 | } else if ( $relationship->from_object === "Order Product" ) { 107 | $i = 0; 108 | foreach( $products as $product ) { 109 | $values = $this->get_values( $connections, $product ); 110 | $this->set_dependencies( $relationship->to_object, $values, 111 | json_decode( $relationship->required_sf_objects ), $response_ids, $relationship->from_object, $i ); 112 | 113 | if ( !empty( $values ) ) { 114 | $response = $this->send_to_salesforce( $relationship->to_object, $values, 115 | json_decode( $relationship->unique_sf_fields ), $response_ids, $i ); 116 | 117 | if ( !$response["success"] ) { 118 | $is_success = false; 119 | array_push( $error_message, $response["error_message"] ); 120 | break; // no need to continue 121 | } 122 | } 123 | $i++; 124 | } 125 | } 126 | } // for each relationship 127 | 128 | // handle order sync response 129 | $this->handle_order_sync_response( $order_id, $is_success, $error_message ); 130 | } 131 | 132 | /** 133 | * Extract and return order items from order object 134 | * @param NWSI_Order_Model $order 135 | * @return array - array of NWSI_Product_Model 136 | */ 137 | private function get_products_from_order( $order ) { 138 | $product_items = $order->get_items(); 139 | 140 | // prepare order items/products 141 | $products = array(); 142 | foreach( $product_items as $product_item ) { 143 | // process order product 144 | $product = new NWSI_Product_Model( $product_item["product_id"] ); 145 | $product->set_order_product_meta_data( $product_item["item_meta"] ); 146 | 147 | array_push( $products, $product ); 148 | } 149 | 150 | return $products; 151 | } 152 | 153 | /** 154 | * Save sync status and error messages to order meta data 155 | * @param int $order_id 156 | * @param boolean $is_successful 157 | * @param array $error_message 158 | */ 159 | private function handle_order_sync_response( $order_id, $is_successful, $error_message ) { 160 | if ( $is_successful ) { 161 | update_post_meta( $order_id, "_sf_sync_status", "success" ); 162 | } else { 163 | update_post_meta( $order_id, "_sf_sync_status", "failed" ); 164 | update_post_meta( $order_id, "_sf_sync_error_message", json_encode( $error_message ) ); 165 | } 166 | } 167 | 168 | /** 169 | * Send values to given object via Salesforce API 170 | * @param string $to_object 171 | * @param array $values 172 | * @param array $unique_sf_fields 173 | * @param array $response_ids (reference) 174 | * @param int $id_index - in case we've multiple sf objects of the same type 175 | * @return array - [success, error_message] 176 | */ 177 | private function send_to_salesforce( $to_object, $values, $unique_sf_fields, &$response_ids, $id_index = null ) { 178 | $response = array(); 179 | 180 | $sf_response = $this->sf->create_object( $to_object, $values, $unique_sf_fields ); 181 | 182 | // obtain SF response ID if any 183 | if ( $sf_response["success"] ) { 184 | $response["success"] = true; 185 | if ( is_null( $id_index ) ) { 186 | $response_ids[ $to_object ] = $sf_response["id"]; 187 | // echo $to_object . ": " . $response_ids[ $to_object ] . "\n"; 188 | } else { 189 | $response_ids[ $to_object ][ $id_index ] = $sf_response["id"]; 190 | // echo $to_object . ", " . $id_index . ": " . $response_ids[ $to_object ][ $id_index ] . "\n"; 191 | } 192 | } else { 193 | $response["success"] = false; 194 | $response["error_message"] = $sf_response["error_code"] . " (" . $to_object . "): " . $sf_response["error_message"]; 195 | } 196 | 197 | return $response; 198 | } 199 | 200 | /** 201 | * Check and return true if date is in Y-m-d format 202 | * @param string $date 203 | * @return boolean 204 | */ 205 | private function is_correct_date_format( $date ) { 206 | if ( preg_match( "/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date ) ) { 207 | return true; 208 | } else { 209 | return false; 210 | } 211 | } 212 | 213 | /** 214 | * Return populated array with field names and values 215 | * @param array $connections 216 | * @param NWSI_Model $item 217 | * @return array 218 | */ 219 | private function get_values( $connections, $item ) { 220 | $values = array(); 221 | foreach( $connections as $connection ) { 222 | 223 | if ( $connection->source == "woocommerce" ) { 224 | $value = $item->get( $connection->from ); 225 | // validation 226 | if ( $connection->type == "boolean" && !is_bool( $value ) ) { 227 | $value = null; 228 | } else if ( in_array( $connection->type, array( "double", "currency", "number", "percent" ) ) 229 | && !is_numeric( $value ) ) { 230 | $value = null; 231 | } else if ( $connection->type == "email" && !filter_var( $value, FILTER_VALIDATE_EMAIL) === false ) { 232 | $value = null; 233 | } else if ( $connection->type == "date" ) { 234 | if ( !$this->is_correct_date_format( $value ) ) { 235 | try { 236 | $value = explode( " ", $value )[0]; 237 | if ( !$this->is_correct_date_format( $value ) ) { 238 | $value = date( "Y-m-d" ); 239 | } 240 | } catch( Exception $exc ) { 241 | // not user friendly fallback but it will solved any required dates 242 | $value = date( "Y-m-d" ); 243 | } 244 | } 245 | } else if ( !is_string( $value ) ) { 246 | $value = null; 247 | } 248 | 249 | } else if ( $connection->source == "sf-picklist" || $connection->source == "custom" ) { 250 | if ( $connection->type == "date" && $connection->value == "current" ) { 251 | $value = date( "Y-m-d" ); 252 | } else { 253 | $value = $connection->value; 254 | } 255 | } 256 | 257 | if ( !empty( $value ) ) { 258 | if ( array_key_exists( $connection->to, $values ) ) { 259 | $values[ $connection->to ] .= ", " . $value; 260 | } else { 261 | $values[ $connection->to ] = $value; 262 | } 263 | } 264 | } 265 | return $values; 266 | } 267 | 268 | /** 269 | * Check dependecies and update values array if needed 270 | * @param string $to_object 271 | * @param array $response_ids 272 | * @param array $values (reference) 273 | * @param array $required_sf_objects 274 | * @param string $from_object 275 | * @param int $id_index - in case we've multiple sf objects of the same type 276 | */ 277 | private function set_dependencies( $to_object, &$values, $required_sf_objects, $response_ids, $from_object, $id_index = null ) { 278 | foreach( $required_sf_objects as $required_sf_object ) { 279 | if ( is_array( $response_ids[ $required_sf_object->name ] ) ) { 280 | if ( empty( $id_index ) ) { 281 | $values[ $required_sf_object->id ] = $response_ids[ $required_sf_object->name ][0]; 282 | } else { 283 | $values[ $required_sf_object->id ] = $response_ids[ $required_sf_object->name ][ $id_index ]; 284 | } 285 | } else { 286 | $values[ $required_sf_object->id ] = $response_ids[ $required_sf_object->name ]; 287 | } 288 | } 289 | } 290 | 291 | /** 292 | * Sort relationships by Salesforce object dependencies 293 | * @param array $relationships 294 | * @return array 295 | */ 296 | private function prioritize_relationships( $relationships ) { 297 | $prioritized_relationships = $relationships; 298 | 299 | for ( $i = 0; $i < sizeof( $relationships ); $i++ ) { 300 | $required_objects = json_decode( $relationships[$i]->required_sf_objects ); 301 | foreach( $required_objects as $required_object ) { 302 | for ( $j = 0; $j < sizeof( $relationships ); $j++ ) { 303 | if ( $i == $j ) { 304 | continue; 305 | } 306 | if ( $required_object->name == $relationships[$j]->to_object ) { 307 | $new_position = $this->get_relationship_index_in_array( $prioritized_relationships, $required_object->name ); 308 | if ( $new_position != -1 ) { // required object exists in array 309 | $current_position = $this->get_relationship_index_in_array( $prioritized_relationships, $relationships[$i]->to_object ); 310 | if ( $new_position > $current_position ) { // object that depends is located before in array 311 | 312 | $temp = array_splice( $prioritized_relationships, $current_position, 1 ); 313 | array_splice( $prioritized_relationships, $new_position, 0, $temp ); 314 | } 315 | } 316 | } 317 | } 318 | } // foreach 319 | } 320 | return $prioritized_relationships; 321 | } 322 | 323 | /** 324 | * Return position of object in relationships array with the same to_object 325 | * value or -1 in case of no matching object 326 | * @param array $relationships 327 | * @param string $to_object 328 | * @return int 329 | */ 330 | private function get_relationship_index_in_array( $relationships, $to_object ) { 331 | for( $i = 0; $i < sizeof( $relationships ); $i++ ) { 332 | if ( $relationships[$i]->to_object == $to_object ) { 333 | return $i; 334 | } 335 | } 336 | return -1; 337 | } 338 | 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /includes/controllers/core/class-nwsi-salesforce.php: -------------------------------------------------------------------------------- 1 | "1.0", 25 | "timeout" => 5, 26 | "redirection" => 5, 27 | "blocking" => true, 28 | "cookies" => array() 29 | ); 30 | 31 | if ( $authorization ) { 32 | $http_header["Authorization"] = "OAuth " . $this->access_token; 33 | } 34 | 35 | if ( $content_type != "" ) { 36 | $http_header["Content-Type"] = $content_type; 37 | } 38 | 39 | if ( !empty( $http_header ) ) { 40 | $args["headers"] = $http_header; 41 | } 42 | 43 | // get response 44 | switch( strtolower( $type ) ) { 45 | case "get": 46 | $args["method"] = "GET"; 47 | if ( $params != "" ) { 48 | $url .= "?" . $params; 49 | } 50 | $response = wp_remote_get( $url, $args ); 51 | break; 52 | case "post": 53 | $args["method"] = "POST"; 54 | if ( $params != "" ) { 55 | $args["body"] = $params; 56 | } 57 | $response = wp_remote_post( $url, $args ); 58 | break; 59 | case "delete": 60 | $args["method"] = "DELETE"; 61 | if ( $params != "" ) { 62 | $args["body"] = $params; 63 | } 64 | $response = wp_remote_post( $url, $args ); 65 | break; 66 | default: 67 | return array(); 68 | } 69 | try { 70 | if ( is_array( $response ) && !empty( $response["body"] ) ) { 71 | return json_decode( $response["body"], true ); 72 | } else { 73 | return array(); 74 | } 75 | } catch( Exception $exc ) { 76 | return array(); 77 | } 78 | } 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /includes/controllers/utilites/class-nwsi-cryptor.php: -------------------------------------------------------------------------------- 1 | key = unpack( "H*", mb_strimwidth( $plain_key, 0, 8 ) )[1]; 19 | } 20 | 21 | /** 22 | * Return encrypted data or false in case of failure 23 | * @param string $data 24 | * @param boolean $encode - set to true for base64 encoding 25 | * @return mixed - boolean or string 26 | */ 27 | public function encrypt( $data, $encode = false ) { 28 | try { 29 | 30 | $encrypted_data = Crypto::Encrypt( $data, $this->key ); 31 | 32 | if ( $encode ) { 33 | return base64_encode( $encrypted_data ); 34 | } else { 35 | return $encrypted_data; 36 | } 37 | 38 | } catch ( Exception $ex ) { 39 | return false; 40 | } 41 | } 42 | 43 | /** 44 | * Return decrypted data or false in case of failure 45 | * @param string $data 46 | * @param boolean $encoded - set to true if $data is base64 encoded 47 | * @return mixed 48 | */ 49 | public function decrypt( $data, $encoded = false ) { 50 | try { 51 | if ( $encoded ) { 52 | $data = base64_decode( $data ); 53 | } 54 | return Crypto::Decrypt( $data, $this->key ); 55 | 56 | } catch ( Exception $ex ) { 57 | return false; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /includes/controllers/utilites/class-nwsi-utility.php: -------------------------------------------------------------------------------- 1 | = 2 ) { 12 | if ( URLParts[1].indexOf("page=wc-settings") !== -1 13 | && URLParts[1].indexOf("tab=integration") !== -1 14 | && URLParts[1].indexOf("section=nwsi") !== -1 ) { 15 | 16 | var URLBase = URLParts[0]; 17 | var GETParams = URLParts[1].split("&"); 18 | var filteredGETParams = ""; 19 | 20 | var isFirst = true; 21 | $.each( GETParams, function(index, param) { 22 | if ( param.indexOf("status=") === -1 && param.indexOf("source=") === -1 ) { 23 | if ( isFirst ) { 24 | isFirst = false; 25 | filteredGETParams += "?"; 26 | } else { 27 | filteredGETParams += "&"; 28 | } 29 | filteredGETParams += param; 30 | } 31 | }); 32 | 33 | if ( filteredGETParams.length > 0 && window.history.replaceState ) { 34 | window.history.replaceState( {}, null, URLBase + filteredGETParams ); 35 | } 36 | } 37 | } 38 | 39 | // if (window.history.replaceState) { 40 | // //prevents browser from storing history with each change: 41 | // window.history.replaceState(statedata, title, url); 42 | // } 43 | } 44 | /** 45 | * Add new relationship after "Add" button click 46 | */ 47 | $( "#nwsi-add-new-rel" ).click( function( e ) { 48 | e.preventDefault(); 49 | 50 | window.location.replace( window.location.href + "&rel=new" + 51 | "&from=" + encodeURIComponent( $( "#nwsi-rel-from-wc" ).val() ) + 52 | "&from_label=" + encodeURIComponent( $("#nwsi-rel-from-wc option:selected").text().trim() ) + 53 | "&to=" + encodeURIComponent( $( "#nwsi-rel-to-sf" ).val() ) + 54 | "&to_label=" + encodeURIComponent( $( "#nwsi-rel-to-sf option:selected" ).text() ) 55 | ); 56 | 57 | } ); 58 | 59 | /** 60 | * Create new select and checkbox element for requires salesforce objects 61 | */ 62 | $( "#nwsi-add-new-required-sf-object" ).click( function(e) { 63 | e.preventDefault(); 64 | 65 | var container = $( "#nwsi-required-sf-objects > tbody" ); 66 | var selectNum = $( "#nwsi-required-sf-objects select" ).size(); 67 | var defaultSelectElement = $("select[name='defaultRequiredSfObject']" ); 68 | 69 | var output = ""; 70 | // checkbox 71 | var newCheckbox = document.createElement( "input" ); 72 | $( newCheckbox ) 73 | .attr( "name", "requiredSfObjectIsActive-" + String( selectNum ) ) 74 | .attr( "type", "checkbox" ); 75 | 76 | output += $( newCheckbox )[0].outerHTML; 77 | output += ""; 78 | 79 | // select 80 | var newSelect = document.createElement( "select" ); 81 | $( newSelect ) 82 | .attr( "name", "requiredSfObject-" + String( selectNum ) ) 83 | .html( defaultSelectElement.html() ); 84 | 85 | output += $( newSelect )[0].outerHTML; 86 | output += ""; 87 | 88 | container.append( output ); 89 | 90 | } ); 91 | 92 | /** 93 | * Create new input for unique ID 94 | */ 95 | $( "#nwsi-add-new-unique-sf-field" ).click( function( e ) { 96 | e.preventDefault(); 97 | 98 | var uniqueSfField0 = $( "select[name='uniqueSfField-0']" ); 99 | var container = $( "#nwsi-unique-sf-fields" ); 100 | var selectNum = $( "#nwsi-unique-sf-fields select" ).size(); 101 | 102 | container.append( " + " ); 103 | 104 | var newSelect = document.createElement("select"); 105 | $( newSelect ) 106 | .attr( "name", "uniqueSfField-" + String( selectNum ) ) 107 | .html( uniqueSfField0.html() ) 108 | .val( "none" ) 109 | .appendTo( container ); 110 | 111 | } ); 112 | 113 | /** 114 | * Manage custom fields 115 | */ 116 | $( "select[name^='wcField-']" ).change( function(e) { 117 | 118 | var selectedVal = $( this ).val(); 119 | if ( selectedVal === undefined ) { 120 | return; 121 | } 122 | 123 | var parent = $( this ).parent(); 124 | var name = $( this ).attr( "name" ); 125 | 126 | if ( selectedVal.indexOf( "custom" ) > -1 ) { 127 | // set hidden source input 128 | $( "input[name='" + name + "-source']" ).val( "custom" ); 129 | // create custom field 130 | if ( selectedVal === "custom-value" ) { 131 | var fieldType = $( "input[name='" + name + "-type']" ).val(); 132 | var inputType = "text"; 133 | if ( fieldType === "double" || fieldType === "integer" ) { 134 | inputType = "number"; 135 | } 136 | parent.append( "
" ); 137 | } 138 | } else { 139 | // delete custom input if user selects another option 140 | parent.find( "input[name='" + name + "-custom']" ).remove(); 141 | parent.find( "br" ).remove(); 142 | } 143 | 144 | } ); 145 | 146 | } )( jQuery ); 147 | -------------------------------------------------------------------------------- /includes/libs/crypto/Crypto.php: -------------------------------------------------------------------------------- 1 | 255 * $digest_length) { 325 | throw new CannotPerformOperationException(); 326 | } 327 | 328 | // "if [salt] not provided, is set to a string of HashLen zeroes." 329 | if (is_null($salt)) { 330 | $salt = str_repeat("\x00", $digest_length); 331 | } 332 | 333 | // HKDF-Extract: 334 | // PRK = HMAC-Hash(salt, IKM) 335 | // The salt is the HMAC key. 336 | $prk = hash_hmac($hash, $ikm, $salt, true); 337 | 338 | // HKDF-Expand: 339 | 340 | // This check is useless, but it serves as a reminder to the spec. 341 | if (self::our_strlen($prk) < $digest_length) { 342 | throw new CannotPerformOperationException(); 343 | } 344 | 345 | // T(0) = '' 346 | $t = ''; 347 | $last_block = ''; 348 | for ($block_index = 1; self::our_strlen($t) < $length; $block_index++) { 349 | // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) 350 | $last_block = hash_hmac( 351 | $hash, 352 | $last_block . $info . chr($block_index), 353 | $prk, 354 | true 355 | ); 356 | // T = T(1) | T(2) | T(3) | ... | T(N) 357 | $t .= $last_block; 358 | } 359 | 360 | // ORM = first L octets of T 361 | $orm = self::our_substr($t, 0, $length); 362 | if ($orm === FALSE) { 363 | throw new CannotPerformOperationException(); 364 | } 365 | return $orm; 366 | } 367 | 368 | private static function VerifyHMAC($correct_hmac, $message, $key) 369 | { 370 | $message_hmac = hash_hmac(self::HASH_FUNCTION, $message, $key, true); 371 | 372 | // We can't just compare the strings with '==', since it would make 373 | // timing attacks possible. We could use the XOR-OR constant-time 374 | // comparison algorithm, but I'm not sure if that's good enough way up 375 | // here in an interpreted language. So we use the method of HMACing the 376 | // strings we want to compare with a random key, then comparing those. 377 | 378 | // NOTE: This leaks information when the strings are not the same 379 | // length, but they should always be the same length here. Enforce it: 380 | if (self::our_strlen($correct_hmac) !== self::our_strlen($message_hmac)) { 381 | throw new CannotPerformOperationException(); 382 | } 383 | 384 | $blind = self::CreateNewRandomKey(); 385 | $message_compare = hash_hmac(self::HASH_FUNCTION, $message_hmac, $blind); 386 | $correct_compare = hash_hmac(self::HASH_FUNCTION, $correct_hmac, $blind); 387 | return $correct_compare === $message_compare; 388 | } 389 | 390 | private static function TestEncryptDecrypt() 391 | { 392 | $key = self::CreateNewRandomKey(); 393 | $data = "EnCrYpT EvErYThInG\x00\x00"; 394 | 395 | // Make sure encrypting then decrypting doesn't change the message. 396 | $ciphertext = self::Encrypt($data, $key); 397 | try { 398 | $decrypted = self::Decrypt($ciphertext, $key); 399 | } catch (InvalidCiphertextException $ex) { 400 | // It's important to catch this and change it into a 401 | // CryptoTestFailedException, otherwise a test failure could trick 402 | // the user into thinking it's just an invalid ciphertext! 403 | throw new CryptoTestFailedException(); 404 | } 405 | if($decrypted !== $data) 406 | { 407 | throw new CryptoTestFailedException(); 408 | } 409 | 410 | // Modifying the ciphertext: Appending a string. 411 | try { 412 | self::Decrypt($ciphertext . "a", $key); 413 | throw new CryptoTestFailedException(); 414 | } catch (InvalidCiphertextException $e) { /* expected */ } 415 | 416 | // Modifying the ciphertext: Changing an IV byte. 417 | try { 418 | $ciphertext[0] = chr((ord($ciphertext[0]) + 1) % 256); 419 | self::Decrypt($ciphertext, $key); 420 | throw new CryptoTestFailedException(); 421 | } catch (InvalidCiphertextException $e) { /* expected */ } 422 | 423 | // Decrypting with the wrong key. 424 | $key = self::CreateNewRandomKey(); 425 | $data = "abcdef"; 426 | $ciphertext = self::Encrypt($data, $key); 427 | $wrong_key = self::CreateNewRandomKey(); 428 | try { 429 | self::Decrypt($ciphertext, $wrong_key); 430 | throw new CryptoTestFailedException(); 431 | } catch (InvalidCiphertextException $e) { /* expected */ } 432 | 433 | // Ciphertext too small (shorter than HMAC). 434 | $key = self::CreateNewRandomKey(); 435 | $ciphertext = str_repeat("A", self::MAC_BYTE_SIZE - 1); 436 | try { 437 | self::Decrypt($ciphertext, $key); 438 | throw new CryptoTestFailedException(); 439 | } catch (InvalidCiphertextException $e) { /* expected */ } 440 | } 441 | 442 | private static function HKDFTestVector() 443 | { 444 | // HKDF test vectors from RFC 5869 445 | 446 | // Test Case 1 447 | $ikm = str_repeat("\x0b", 22); 448 | $salt = self::hexToBytes("000102030405060708090a0b0c"); 449 | $info = self::hexToBytes("f0f1f2f3f4f5f6f7f8f9"); 450 | $length = 42; 451 | $okm = self::hexToBytes( 452 | "3cb25f25faacd57a90434f64d0362f2a" . 453 | "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" . 454 | "34007208d5b887185865" 455 | ); 456 | $computed_okm = self::HKDF("sha256", $ikm, $length, $info, $salt); 457 | if ($computed_okm !== $okm) { 458 | throw new CryptoTestFailedException(); 459 | } 460 | 461 | // Test Case 7 462 | $ikm = str_repeat("\x0c", 22); 463 | $length = 42; 464 | $okm = self::hexToBytes( 465 | "2c91117204d745f3500d636a62f64f0a" . 466 | "b3bae548aa53d423b0d1f27ebba6f5e5" . 467 | "673a081d70cce7acfc48" 468 | ); 469 | $computed_okm = self::HKDF("sha1", $ikm, $length); 470 | if ($computed_okm !== $okm) { 471 | throw new CryptoTestFailedException(); 472 | } 473 | 474 | } 475 | 476 | private static function HMACTestVector() 477 | { 478 | // HMAC test vector From RFC 4231 (Test Case 1) 479 | $key = str_repeat("\x0b", 20); 480 | $data = "Hi There"; 481 | $correct = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"; 482 | if (hash_hmac(self::HASH_FUNCTION, $data, $key) != $correct) { 483 | throw new CryptoTestFailedException(); 484 | } 485 | } 486 | 487 | private static function AESTestVector() 488 | { 489 | // AES CBC mode test vector from NIST SP 800-38A 490 | $key = self::hexToBytes("2b7e151628aed2a6abf7158809cf4f3c"); 491 | $iv = self::hexToBytes("000102030405060708090a0b0c0d0e0f"); 492 | $plaintext = self::hexToBytes( 493 | "6bc1bee22e409f96e93d7e117393172a" . 494 | "ae2d8a571e03ac9c9eb76fac45af8e51" . 495 | "30c81c46a35ce411e5fbc1191a0a52ef" . 496 | "f69f2445df4f9b17ad2b417be66c3710" 497 | ); 498 | $ciphertext = self::hexToBytes( 499 | "7649abac8119b246cee98e9b12e9197d" . 500 | "5086cb9b507219ee95db113a917678b2" . 501 | "73bed6b8e3c1743b7116e69e22229516" . 502 | "3ff1caa1681fac09120eca307586e1a7" . 503 | /* Block due to padding. Not from NIST test vector. 504 | Padding Block: 10101010101010101010101010101010 505 | Ciphertext: 3ff1caa1681fac09120eca307586e1a7 506 | (+) 2fe1dab1780fbc19021eda206596f1b7 507 | AES 8cb82807230e1321d3fae00d18cc2012 508 | 509 | */ 510 | "8cb82807230e1321d3fae00d18cc2012" 511 | ); 512 | 513 | $computed_ciphertext = self::PlainEncrypt($plaintext, $key, $iv); 514 | if ($computed_ciphertext !== $ciphertext) { 515 | throw new CryptoTestFailedException(); 516 | } 517 | 518 | $computed_plaintext = self::PlainDecrypt($ciphertext, $key, $iv); 519 | if ($computed_plaintext !== $plaintext) { 520 | throw new CryptoTestFailedException(); 521 | } 522 | } 523 | 524 | /* WARNING: Do not call this function on secrets. It creates side channels. */ 525 | private static function hexToBytes($hex_string) 526 | { 527 | return pack("H*", $hex_string); 528 | } 529 | 530 | private static function EnsureConstantExists($name) 531 | { 532 | if (!defined($name)) { 533 | throw new CannotPerformOperationException(); 534 | } 535 | } 536 | 537 | private static function EnsureFunctionExists($name) 538 | { 539 | if (!function_exists($name)) { 540 | throw new CannotPerformOperationException(); 541 | } 542 | } 543 | 544 | /* 545 | * We need these strlen() and substr() functions because when 546 | * 'mbstring.func_overload' is set in php.ini, the standard strlen() and 547 | * substr() are replaced by mb_strlen() and mb_substr(). 548 | */ 549 | 550 | private static function our_strlen($str) 551 | { 552 | if (function_exists('mb_strlen')) { 553 | $length = mb_strlen($str, '8bit'); 554 | if ($length === FALSE) { 555 | throw new CannotPerformOperationException(); 556 | } 557 | return $length; 558 | } else { 559 | return strlen($str); 560 | } 561 | } 562 | 563 | private static function our_substr($str, $start, $length = NULL) 564 | { 565 | if (function_exists('mb_substr')) 566 | { 567 | // mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP 568 | // 5.3, so we have to find the length ourselves. 569 | if (!isset($length)) { 570 | if ($start >= 0) { 571 | $length = self::our_strlen($str) - $start; 572 | } else { 573 | $length = -$start; 574 | } 575 | } 576 | 577 | return mb_substr($str, $start, $length, '8bit'); 578 | } 579 | 580 | // Unlike mb_substr(), substr() doesn't accept NULL for length 581 | if (isset($length)) { 582 | return substr($str, $start, $length); 583 | } else { 584 | return substr($str, $start); 585 | } 586 | } 587 | 588 | } 589 | 590 | /* 591 | * We want to catch all uncaught exceptions that come from the Crypto class, 592 | * since by default, PHP will leak the key in the stack trace from an uncaught 593 | * exception. This is a really ugly hack, but I think it's justified. 594 | * 595 | * Everything up to handler() getting called should be reliable, so this should 596 | * reliably suppress the stack traces. The rest is just a bonus so that we don't 597 | * make it impossible to debug other exceptions. 598 | * 599 | * This bit of code was adapted from: http://stackoverflow.com/a/7939492 600 | */ 601 | 602 | class CryptoExceptionHandler 603 | { 604 | private $rethrow = NULL; 605 | 606 | public function __construct() 607 | { 608 | set_exception_handler(array($this, "handler")); 609 | } 610 | 611 | public function handler($ex) 612 | { 613 | if ( 614 | $ex instanceof InvalidCiphertextException || 615 | $ex instanceof CannotPerformOperationException || 616 | $ex instanceof CryptoTestFailedException 617 | ) { 618 | echo "FATAL ERROR: Uncaught crypto exception. Suppresssing output.\n"; 619 | } else { 620 | /* Re-throw the exception in the destructor. */ 621 | $this->rethrow = $ex; 622 | } 623 | } 624 | 625 | public function __destruct() { 626 | if ($this->rethrow) { 627 | throw $this->rethrow; 628 | } 629 | } 630 | } 631 | 632 | $crypto_exception_handler_object_dont_touch_me = new CryptoExceptionHandler(); 633 | 634 | -------------------------------------------------------------------------------- /includes/libs/wp-background-processing/wp-async-request.php: -------------------------------------------------------------------------------- 1 | identifier = $this->prefix . '_' . $this->action; 31 | 32 | add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); 33 | add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); 34 | } 35 | 36 | /** 37 | * Set data used during the request 38 | * 39 | * @param array $data 40 | * 41 | * @return $this 42 | */ 43 | public function data( $data ) { 44 | $this->data = $data; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Dispatch the async request 51 | * 52 | * @return array|WP_Error 53 | */ 54 | public function dispatch() { 55 | $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); 56 | $args = $this->get_post_args(); 57 | 58 | return wp_remote_post( esc_url_raw( $url ), $args ); 59 | } 60 | 61 | /** 62 | * Get query args 63 | * 64 | * @return array 65 | */ 66 | protected function get_query_args() { 67 | if ( property_exists( $this, 'query_args' ) ) { 68 | return $this->query_args; 69 | } 70 | 71 | return array( 72 | 'action' => $this->identifier, 73 | 'nonce' => wp_create_nonce( $this->identifier ), 74 | ); 75 | } 76 | 77 | /** 78 | * Get query URL 79 | * 80 | * @return string 81 | */ 82 | protected function get_query_url() { 83 | if ( property_exists( $this, 'query_url' ) ) { 84 | return $this->query_url; 85 | } 86 | 87 | return admin_url( 'admin-ajax.php' ); 88 | } 89 | 90 | /** 91 | * Get post args 92 | * 93 | * @return array 94 | */ 95 | protected function get_post_args() { 96 | if ( property_exists( $this, 'post_args' ) ) { 97 | return $this->post_args; 98 | } 99 | 100 | return array( 101 | 'timeout' => 4.01, 102 | 'blocking' => true, 103 | 'body' => $this->data, 104 | 'cookies' => $_COOKIE, 105 | 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), 106 | ); 107 | } 108 | 109 | /** 110 | * Maybe handle 111 | * 112 | * Check for correct nonce and pass to handler. 113 | */ 114 | public function maybe_handle() { 115 | check_ajax_referer( $this->identifier, 'nonce' ); 116 | 117 | $this->handle(); 118 | 119 | wp_die(); 120 | } 121 | 122 | /** 123 | * Handle 124 | * 125 | * Override this method to perform any actions required 126 | * during the async request. 127 | */ 128 | abstract protected function handle(); 129 | 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /includes/libs/wp-background-processing/wp-background-process.php: -------------------------------------------------------------------------------- 1 | cron_hook_identifier = $this->identifier . '_cron'; 35 | $this->cron_interval_identifier = $this->identifier . '_cron_interval'; 36 | 37 | add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); 38 | add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); 39 | } 40 | 41 | /** 42 | * Dispatch 43 | * 44 | * @return array|WP_Error 45 | */ 46 | public function dispatch() { 47 | // Schedule the cron healthcheck 48 | $this->schedule_event(); 49 | 50 | // Perform remote post 51 | parent::dispatch(); 52 | } 53 | 54 | /** 55 | * Push to queue 56 | * 57 | * @param mixed $data 58 | * 59 | * @return $this 60 | */ 61 | public function push_to_queue( $data ) { 62 | $this->data[] = $data; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Save queue 69 | * 70 | * @return $this 71 | */ 72 | public function save() { 73 | $key = $this->generate_key(); 74 | 75 | if ( ! empty( $this->data ) ) { 76 | update_site_option( $key, $this->data ); 77 | } 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Update queue 84 | * 85 | * @param string $key 86 | * @param array $data 87 | * 88 | * @return $this 89 | */ 90 | public function update( $key, $data ) { 91 | if ( ! empty( $data ) ) { 92 | update_site_option( $key, $data ); 93 | } 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Delete queue 100 | * 101 | * @param string $key 102 | * 103 | * @return $this 104 | */ 105 | public function delete( $key ) { 106 | delete_site_option( $key ); 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Generate key 113 | * 114 | * Generates a unique key based on microtime. Queue items are 115 | * given a unique key so that they can be merged upon save. 116 | * 117 | * @param int $length 118 | * 119 | * @return string 120 | */ 121 | protected function generate_key( $length = 64 ) { 122 | $unique = md5( microtime() . rand() ); 123 | $prepend = $this->identifier . '_batch_'; 124 | 125 | return substr( $prepend . $unique, 0, $length ); 126 | } 127 | 128 | /** 129 | * Maybe process queue 130 | * 131 | * Checks whether data exists within the queue and that 132 | * the process is not already running. 133 | */ 134 | public function maybe_handle() { 135 | if ( $this->is_process_running() ) { 136 | // Background process already running 137 | wp_die(); 138 | } 139 | 140 | if ( $this->is_queue_empty() ) { 141 | // No data to process 142 | wp_die(); 143 | } 144 | 145 | check_ajax_referer( $this->identifier, 'nonce' ); 146 | 147 | $this->handle(); 148 | 149 | wp_die(); 150 | } 151 | 152 | /** 153 | * Is queue empty 154 | * 155 | * @return bool 156 | */ 157 | protected function is_queue_empty() { 158 | global $wpdb; 159 | 160 | $table = $wpdb->options; 161 | $column = 'option_name'; 162 | 163 | if ( is_multisite() ) { 164 | $table = $wpdb->sitemeta; 165 | $column = 'meta_key'; 166 | } 167 | 168 | $key = $this->identifier . '_batch_%'; 169 | 170 | $count = $wpdb->get_var( $wpdb->prepare( " 171 | SELECT COUNT(*) 172 | FROM {$table} 173 | WHERE {$column} LIKE %s 174 | ", $key ) ); 175 | 176 | return ( $count > 0 ) ? false : true; 177 | } 178 | 179 | /** 180 | * Is process running 181 | * 182 | * Check whether the current process is already running 183 | * in a background process. 184 | */ 185 | protected function is_process_running() { 186 | if ( get_site_transient( $this->identifier . '_process_lock' ) ) { 187 | // Process already running 188 | return true; 189 | } 190 | 191 | return false; 192 | } 193 | 194 | /** 195 | * Lock process 196 | * 197 | * Lock the process so that multiple instances can't run simultaneously. 198 | * Override if applicable, but the duration should be greater than that 199 | * defined in the time_exceeded() method. 200 | */ 201 | protected function lock_process() { 202 | $this->start_time = time(); // Set start time of current process 203 | 204 | $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute 205 | $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); 206 | 207 | set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); 208 | } 209 | 210 | /** 211 | * Unlock process 212 | * 213 | * Unlock the process so that other instances can spawn. 214 | * 215 | * @return $this 216 | */ 217 | protected function unlock_process() { 218 | delete_site_transient( $this->identifier . '_process_lock' ); 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Get batch 225 | * 226 | * @return stdClass Return the first batch from the queue 227 | */ 228 | protected function get_batch() { 229 | global $wpdb; 230 | 231 | $table = $wpdb->options; 232 | $column = 'option_name'; 233 | $key_column = 'option_id'; 234 | $value_column = 'option_value'; 235 | 236 | if ( is_multisite() ) { 237 | $table = $wpdb->sitemeta; 238 | $column = 'meta_key'; 239 | $key_column = 'meta_id'; 240 | $value_column = 'meta_value'; 241 | } 242 | 243 | $key = $this->identifier . '_batch_%'; 244 | 245 | $query = $wpdb->get_row( $wpdb->prepare( " 246 | SELECT * 247 | FROM {$table} 248 | WHERE {$column} LIKE %s 249 | ORDER BY {$key_column} ASC 250 | LIMIT 1 251 | ", $key ) ); 252 | 253 | $batch = new stdClass(); 254 | $batch->key = $query->$column; 255 | $batch->data = maybe_unserialize( $query->$value_column ); 256 | 257 | return $batch; 258 | } 259 | 260 | /** 261 | * Handle 262 | * 263 | * Pass each queue item to the task handler, while remaining 264 | * within server memory and time limit constraints. 265 | */ 266 | protected function handle() { 267 | $this->lock_process(); 268 | 269 | do { 270 | $batch = $this->get_batch(); 271 | 272 | foreach ( $batch->data as $key => $value ) { 273 | $task = $this->task( $value ); 274 | 275 | if ( false !== $task ) { 276 | $batch->data[ $key ] = $task; 277 | } else { 278 | unset( $batch->data[ $key ] ); 279 | } 280 | 281 | if ( $this->time_exceeded() || $this->memory_exceeded() ) { 282 | // Batch limits reached 283 | break; 284 | } 285 | } 286 | 287 | // Update or delete current batch 288 | if ( ! empty( $batch->data ) ) { 289 | $this->update( $batch->key, $batch->data ); 290 | } else { 291 | $this->delete( $batch->key ); 292 | } 293 | } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); 294 | 295 | $this->unlock_process(); 296 | 297 | // Start next batch or complete process 298 | if ( ! $this->is_queue_empty() ) { 299 | $this->dispatch(); 300 | } else { 301 | $this->complete(); 302 | } 303 | 304 | wp_die(); 305 | } 306 | 307 | /** 308 | * Memory exceeded 309 | * 310 | * Ensures the batch process never exceeds 90% 311 | * of the maximum WordPress memory. 312 | * 313 | * @return bool 314 | */ 315 | protected function memory_exceeded() { 316 | $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory 317 | $current_memory = memory_get_usage( true ); 318 | $return = false; 319 | 320 | if ( $current_memory >= $memory_limit ) { 321 | $return = true; 322 | } 323 | 324 | return apply_filters( $this->identifier . '_memory_exceeded', $return ); 325 | } 326 | 327 | /** 328 | * Get memory limit 329 | * 330 | * @return int 331 | */ 332 | protected function get_memory_limit() { 333 | if ( function_exists( 'ini_get' ) ) { 334 | $memory_limit = ini_get( 'memory_limit' ); 335 | } else { 336 | // Sensible default 337 | $memory_limit = '128M'; 338 | } 339 | 340 | if ( ! $memory_limit || -1 == $memory_limit ) { 341 | // Unlimited, set to 32GB 342 | $memory_limit = '32000M'; 343 | } 344 | 345 | return intval( $memory_limit ) * 1024 * 1024; 346 | } 347 | 348 | /** 349 | * Time exceeded 350 | * 351 | * Ensures the batch never exceeds a sensible time limit. 352 | * A timeout limit of 30s is common on shared hosting. 353 | * 354 | * @return bool 355 | */ 356 | protected function time_exceeded() { 357 | $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds 358 | $return = false; 359 | 360 | if ( time() >= $finish ) { 361 | $return = true; 362 | } 363 | 364 | return apply_filters( $this->identifier . '_time_exceeded', $return ); 365 | } 366 | 367 | /** 368 | * Complete 369 | * 370 | * Override if applicable, but ensure that the below actions are 371 | * performed, or, call parent::complete(). 372 | */ 373 | protected function complete() { 374 | // Unschedule the cron healthcheck 375 | $this->clear_scheduled_event(); 376 | } 377 | 378 | /** 379 | * Schedule cron healthcheck 380 | * 381 | * @param $schedules 382 | * 383 | * @return mixed 384 | */ 385 | public function schedule_cron_healthcheck( $schedules ) { 386 | $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); 387 | 388 | if ( property_exists( $this, 'cron_interval' ) ) { 389 | $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval_identifier ); 390 | } 391 | 392 | // Adds every 5 minutes to the existing schedules. 393 | $schedules[ $this->identifier . '_cron_interval' ] = array( 394 | 'interval' => MINUTE_IN_SECONDS * $interval, 395 | 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), 396 | ); 397 | 398 | return $schedules; 399 | } 400 | 401 | /** 402 | * Handle cron healthcheck 403 | * 404 | * Restart the background process if not already running 405 | * and data exists in the queue. 406 | */ 407 | public function handle_cron_healthcheck() { 408 | if ( $this->is_process_running() ) { 409 | // Background process already running 410 | exit; 411 | } 412 | 413 | if ( $this->is_queue_empty() ) { 414 | // No data to process 415 | $this->clear_scheduled_event(); 416 | exit; 417 | } 418 | 419 | $this->handle(); 420 | 421 | exit; 422 | } 423 | 424 | /** 425 | * Schedule event 426 | */ 427 | protected function schedule_event() { 428 | if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { 429 | wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); 430 | } 431 | } 432 | 433 | /** 434 | * Clear scheduled event 435 | */ 436 | protected function clear_scheduled_event() { 437 | $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); 438 | 439 | if ( $timestamp ) { 440 | wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); 441 | } 442 | } 443 | 444 | /** 445 | * Task 446 | * 447 | * Override this method to perform any actions required on each 448 | * queue item. Return the modified item for further processing 449 | * in the next pass through. Or, return false to remove the 450 | * item from the queue. 451 | * 452 | * @param mixed $item Queue item to iterate over 453 | * 454 | * @return mixed 455 | */ 456 | abstract protected function task( $item ); 457 | 458 | } 459 | } -------------------------------------------------------------------------------- /includes/models/class-nwsi-order-item-model.php: -------------------------------------------------------------------------------- 1 | get_data(); 40 | $data_keys = $this->get_data_keys(); 41 | 42 | foreach ( $data_keys as $data_key ) { 43 | if ( isset( $data[$data_key] ) && is_array( $data[$data_key] ) ) { 44 | $data_key_index = array_search( $data_key, $data_keys ); 45 | if ( $data_key_index !== false ) { 46 | unset( $data_keys[$data_key_index] ); 47 | } 48 | } 49 | } 50 | 51 | $include_db_keys = false; 52 | if ( has_filter( "nwsi_include_order_item_keys_from_database" ) ) { 53 | $include_db_keys = (bool) apply_filters( "nwsi_include_order_item_keys_from_database" ); 54 | } 55 | 56 | if ( $include_db_keys ) { 57 | // combine with order meta keys from the database 58 | require_once( NWSI_DIR_PATH . "includes/controllers/core/class-nwsi-db.php" ); 59 | $db = new NWSI_DB(); 60 | $keys = array_merge( $data_keys, $db->get_order_item_meta_keys() ); 61 | } else { 62 | $keys = $data_keys; 63 | } 64 | 65 | $unique_keys = array_unique( $keys ); 66 | sort( $unique_keys, SORT_STRING ); 67 | 68 | if ( has_filter( "nwsi_order_item_property_keys" ) ) { 69 | $unique_keys = (array) apply_filters( "nwsi_order_item_property_keys", $unique_keys ); 70 | } 71 | 72 | return $unique_keys; 73 | } 74 | 75 | /** 76 | * Return property value. 77 | * 78 | * @since 0.9.2 79 | * @param string $property_name 80 | * @return string 81 | */ 82 | public function get( $property_name ) { 83 | $value = null; 84 | if ( method_exists( $this, "get_" . $property_name ) ) { 85 | $value = $this->{"get_" . $property_name}(); 86 | } 87 | 88 | if ( has_filter( "nwsi_get_order_item_property_key_" . $property_name ) ) { 89 | return apply_filters( "nwsi_get_order_item_property_key_" . $property_name, $value, $this ); 90 | } else { 91 | return $value; 92 | } 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /includes/models/class-nwsi-order-model.php: -------------------------------------------------------------------------------- 1 | get_data(); 40 | $data_keys = $this->get_data_keys(); 41 | 42 | // keys which hold subarrays in $data 43 | $parent_keys = array( "shipping", "billing" ); 44 | for ( $i = 0; $i < count( $data_keys ) ; $i++ ) { 45 | foreach ( $parent_keys as $parent_key ) { 46 | if ( isset( $data_keys[$i] ) && $parent_key === $data_keys[$i] ) { 47 | unset( $data_keys[$i] ); 48 | foreach ( $data[$parent_key] as $child_key => $child_value ) { 49 | array_push( $data_keys, $parent_key . "_" . $child_key ); 50 | } 51 | } 52 | } 53 | } 54 | 55 | $include_db_keys = false; 56 | if ( has_filter( "nwsi_include_order_keys_from_database" ) ) { 57 | $include_db_keys = (bool) apply_filters( "nwsi_include_order_keys_from_database" ); 58 | } 59 | 60 | if ( $include_db_keys ) { 61 | // combine with order meta keys from the database 62 | require_once( NWSI_DIR_PATH . "includes/controllers/core/class-nwsi-db.php" ); 63 | $db = new NWSI_DB(); 64 | $keys = array_merge( $data_keys, $db->get_order_meta_keys() ); 65 | } else { 66 | $keys = $data_keys; 67 | } 68 | 69 | $unique_keys = array_unique( $keys ); 70 | sort( $unique_keys, SORT_STRING ); 71 | 72 | if ( has_filter( "nwsi_order_property_keys" ) ) { 73 | $unique_keys = (array) apply_filters( "nwsi_order_property_keys", $unique_keys ); 74 | } 75 | 76 | return $unique_keys; 77 | } 78 | 79 | /** 80 | * Return property value. 81 | * 82 | * @since 0.1 83 | * @param string $property_name 84 | * @return string 85 | */ 86 | public function get( $property_name ) { 87 | $value = null; 88 | if ( method_exists( $this, "get_" . $property_name ) ) { 89 | $value = $this->{"get_" . $property_name}(); 90 | } 91 | 92 | if ( has_filter( "nwsi_get_order_property_key_" . $property_name ) ) { 93 | return apply_filters( "nwsi_get_order_property_key_" . $property_name, $value, $this ); 94 | } else { 95 | return $value; 96 | } 97 | } 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /includes/models/class-nwsi-product-model.php: -------------------------------------------------------------------------------- 1 | order_product_meta_data = $meta_data; 41 | } 42 | 43 | /** 44 | * Return product properties from latest product 45 | * @return array 46 | */ 47 | public function get_product_meta_keys() { 48 | require_once( NWSI_DIR_PATH . "includes/controllers/core/class-nwsi-db.php" ); 49 | 50 | $db = new NWSI_DB(); 51 | $meta_keys = $db->get_product_meta_keys(); 52 | 53 | if ( empty( $items ) ) { 54 | // fallback if there's no products in DB 55 | $items = $this->get_default_product_meta_keys(); 56 | } 57 | return $meta_keys; 58 | } 59 | 60 | /** 61 | * Return product meta keys. 62 | * 63 | * @since 0.9.1 64 | * @return array 65 | */ 66 | public function get_property_keys() { 67 | $data = $this->get_data(); 68 | $data_keys = $this->get_data_keys(); 69 | 70 | foreach ( $data_keys as $data_key ) { 71 | if ( isset( $data[$data_key] ) && is_array( $data[$data_key] ) ) { 72 | $data_key_index = array_search( $data_key, $data_keys ); 73 | if ( $data_key_index !== false ) { 74 | unset( $data_keys[$data_key_index] ); 75 | } 76 | } 77 | } 78 | 79 | $include_db_keys = false; 80 | if ( has_filter( "nwsi_include_product_keys_from_database" ) ) { 81 | $include_db_keys = (bool) apply_filters( "nwsi_include_product_keys_from_database" ); 82 | } 83 | 84 | if ( $include_db_keys ) { 85 | // combine with product meta keys from the database 86 | require_once( NWSI_DIR_PATH . "includes/controllers/core/class-nwsi-db.php" ); 87 | $db = new NWSI_DB(); 88 | $keys = array_merge( $data_keys, $db->get_product_meta_keys() ); 89 | } else { 90 | $keys = $data_keys; 91 | } 92 | 93 | $unique_keys = array_unique( $keys ); 94 | sort( $unique_keys, SORT_STRING ); 95 | 96 | if ( has_filter( "nwsi_product_property_keys" ) ) { 97 | $unique_keys = (array) apply_filters( "nwsi_product_property_keys", $unique_keys ); 98 | } 99 | 100 | return $unique_keys; 101 | } 102 | 103 | /** 104 | * Return property value. 105 | * 106 | * @since 0.9.1 107 | * @param string $property_name 108 | * @return string 109 | */ 110 | public function get( $property_name ) { 111 | $value = null; 112 | if ( method_exists( $this, "get_" . $property_name ) ) { 113 | $value = $this->{"get_" . $property_name}(); 114 | } 115 | 116 | if ( has_filter( "nwsi_get_product_property_key_" . $property_name ) ) { 117 | return apply_filters( "nwsi_get_product_property_key_" . $property_name, $value, $this ); 118 | } else { 119 | return $value; 120 | } 121 | } 122 | 123 | } 124 | } 125 | ?> 126 | -------------------------------------------------------------------------------- /includes/models/interface-nwsi-model.php: -------------------------------------------------------------------------------- 1 | 59 |
60 |

61 |
62 | display_admin_notice( "Relationship successfully edited!", "success" ); 70 | } 71 | 72 | /** 73 | * Admin notice HTML for successfully creating the relationship 74 | */ 75 | public function display_rel_new_success() { 76 | $this->display_admin_notice( "Relationship successfully created!", "success" ); 77 | } 78 | 79 | /** 80 | * Admin notice HTML for failed relationship update 81 | */ 82 | public function display_rel_edit_error() { 83 | $this->display_admin_notice( "Something went wrong while updating the relationship, please try again!", "error" ); 84 | } 85 | 86 | /** 87 | * Admin notice HTML for failed creation of new relationship 88 | */ 89 | public function display_rel_new_error() { 90 | $this->display_admin_notice( "Something went wrong while creating new relationship, please try again!", "error" ); 91 | } 92 | 93 | /** 94 | * Admin notice HTML for successfully obtained tokens 95 | */ 96 | public function display_access_token_success() { 97 | $this->display_admin_notice( "Access token successfully obtained!", "success" ); 98 | } 99 | 100 | /** 101 | * Admin notice HTML for no access found error 102 | */ 103 | public function display_access_token_error() { 104 | $this->display_admin_notice( "Access token missing from response, please try again!", "error" ); 105 | } 106 | 107 | /** 108 | * Admin notice HTML for no instance url found error 109 | */ 110 | public function display_instance_url_error() { 111 | $this->display_admin_notice( "Instance URL missing from response, please try again!", "error" ); 112 | } 113 | 114 | /** 115 | * Admin notice HTML for failed response to access token request 116 | */ 117 | public function display_token_url_call_error() { 118 | $this->display_admin_notice( "Call to token URL failed, please try again!", "error" ); 119 | } 120 | 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /includes/views/class-nwsi-orders-view.php: -------------------------------------------------------------------------------- 1 | handle_order( $order_id ); 49 | } 50 | 51 | /** 52 | * Create new sync column in orders preview. 53 | * 54 | * @param array $columns 55 | * @return array 56 | */ 57 | public function manage_orders_columns( array $columns ) { 58 | $new_columns = array(); 59 | foreach( $columns as $key => $value ) { 60 | if ( $key == "order_actions" ) { 61 | $new_columns["salesforce_sync"] = __( "Salesforce sync status", "woocommerce-integration-nwsi" ); 62 | } 63 | $new_columns[ $key ] = $value; 64 | } 65 | 66 | return $new_columns; 67 | } 68 | 69 | /** 70 | * Insert value for salesforce sync column in orders preview. 71 | * 72 | * @param string $column 73 | * @param int $post_id 74 | */ 75 | public function order_column_salesforce_sync( $column, $post_id ) { 76 | if ( $column == "salesforce_sync" ) { 77 | $status = get_post_meta( $post_id, "_sf_sync_status", true ); 78 | if ( empty( $status ) ) { 79 | echo "none"; 80 | } else { 81 | echo $status; 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Create custom meta box in order preview for salesforce sync status. 88 | */ 89 | public function add_sf_order_meta_box() { 90 | add_meta_box( 91 | "woocommerce_display_sf_order_meta_box_fields", 92 | __( "Salesforce Sync", "woocommerce-integration-nwsi" ), 93 | array( $this, "display_sf_order_meta_box_fields" ), 94 | "shop_order", 95 | "side", 96 | "default" 97 | ); 98 | } 99 | 100 | /** 101 | * Echo HTML for meta box in order preview for salesforce sync status. 102 | */ 103 | public function display_sf_order_meta_box_fields() { 104 | global $post; 105 | 106 | $status = get_post_meta( $post->ID, "_sf_sync_status", true ); 107 | if ( empty( $status ) ) { 108 | $status = "none"; 109 | } 110 | echo "" . __( "Status", "woocommerce-integration-nwsi" ) . ": " . $status . "
"; 111 | 112 | if ( $status == "failed" ) { 113 | $error_messages_txt = ""; 114 | $error_messages = get_post_meta( $post->ID, "_sf_sync_error_message", true ); 115 | 116 | try { 117 | $error_messages = json_decode( $error_messages ); 118 | $counter = 1; 119 | foreach( $error_messages as $error_message ) { 120 | $error_messages_txt .= $counter++ . ". " . $error_message . "\n"; 121 | } 122 | } catch( Exception $ex ) { 123 | $error_messages_txt = ""; 124 | } 125 | 126 | echo "" . __( "Error message", "woocommerce-integration-nwsi" ) . ": " . "
"; 127 | echo ""; 128 | } 129 | 130 | wp_nonce_field( "nwsi_sync_product", "nwsi_sync_product_nonce" ); 131 | echo "
"; 132 | echo ""; 133 | } 134 | 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /includes/views/class-nwsi-relationship-form.php: -------------------------------------------------------------------------------- 1 | sf = $sf; 27 | } 28 | 29 | /** 30 | * Echo form for editing the existing relationship provided as a parameter. 31 | * Existing relationship should contain properties: 32 | * [relationships] => array 33 | * [from_object] => string 34 | * [from_object_label] => string 35 | * [to_object] => string 36 | * [to_object_label] => string 37 | * [required_sf_objects] => array 38 | * [unique_sf_fields] => array 39 | * 40 | * @param stdClass $rel Relationship object. 41 | */ 42 | public function display_existing( stdClass $rel ) { 43 | $this->display_blank( 44 | $rel->from_object, $rel->from_object_label, 45 | $rel->to_object, $rel->to_object_label, 46 | json_decode( $rel->relationships ), 47 | json_decode( $rel->required_sf_objects ), 48 | json_decode( $rel->unique_sf_fields ) 49 | ); 50 | } 51 | 52 | /** 53 | * Echo HTML form for creating new relationships. 54 | * 55 | * @param string $from WooCommerce object name. 56 | * @param string $from_label WooCommerce object label. 57 | * @param string $to Salesforce object name. 58 | * @param string $to_label Salesforce object label. 59 | * @param array $relationships Existing relationship values, defaults to empty array. 60 | * @param array $required_sf_objects Array of required SF objects, defaults to empty array. 61 | * @param array $unique_sf_fields Array of unique SF fields, defaults to empty array. 62 | */ 63 | public function display_blank( string $from, string $from_label, string $to, string $to_label, array $relationships = array(), array $required_sf_objects = array(), array $unique_sf_fields = array() ) { 64 | $wc_object_description = $this->get_wc_object_description( $from ); 65 | $sf_object_description = $this->sf->get_object_description( $to ); 66 | 67 | $this->display_title( empty( $relationships ) ); 68 | 69 | $this->display_main_section( 70 | $from_label, 71 | $to_label, 72 | $sf_object_description, 73 | $wc_object_description, 74 | $relationships 75 | ); 76 | 77 | $this->display_unique_section( $unique_sf_fields, $sf_object_description ); 78 | $this->display_required_objects_section( $sf_object_description, $required_sf_objects, $to, $to_label ); 79 | 80 | wp_enqueue_script( "nwsi-settings-js", NWSI_DIR_URL . "includes/js/nwsi-settings.js", array( "jquery" ) ); 81 | } 82 | 83 | /** 84 | * Echo form title. 85 | * 86 | * @param boolean $is_new 87 | */ 88 | private function display_title( $is_new ) { 89 | ?> 90 |

91 | 98 |

99 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 128 | 129 | 144 | 206 | 207 | 208 | 209 | 210 | 211 |
(Salesforce) (WooCommerce)
130 | *"; 138 | $required = true; 139 | } 140 | ?> 141 | 142 | " value="" /> 143 | 145 | to == $sf_field["name"] ) { 149 | 150 | if ( $relationship->from == "salesforce" ) { 151 | $selected = $relationship->value; 152 | } else if ( $relationship->from == "custom" ) { 153 | if ( $relationship->type == "date" ) { 154 | $selected = "custom-current-date"; 155 | } else if ( $relationship->type == "boolean" ) { 156 | $selected = "custom-" . $relationship->value; 157 | } else { 158 | $selected = "custom-value"; 159 | } 160 | } else { 161 | $selected = $relationship->from; 162 | } 163 | 164 | if ( property_exists( $relationship, "source" ) ) { 165 | $source = $relationship->source; 166 | } else { 167 | $source = "woocommerce"; 168 | } 169 | 170 | if ( property_exists( $relationship, "type" ) ) { 171 | $type = $relationship->type; 172 | } 173 | 174 | if ( property_exists( $relationship, "value" ) ) { 175 | $value = $relationship->value; 176 | } 177 | break; 178 | } 179 | } 180 | ?> 181 | " value="" /> 182 | generate_wc_select_element( $wc_object_description, "wcField-" . $i, $selected, $required, $sf_field["type"] ); 186 | 187 | if ( $source == "custom" && !in_array( $type, array( "boolean", "date" ) ) ) { 188 | $input_type = ( $type == "double" ) ? "number" : "text"; 189 | ?> 190 |
191 | " value="" /> 192 | 196 | " value="" /> 197 | generate_sf_picklist_select_element( $sf_field["picklistValues"], "wcField-" . $i, $selected, $required ); 200 | ?> 201 | " value="sf-picklist" /> 202 | 205 |
212 | 213 | 225 |
226 |

Unique fields

227 |
228 | 229 | generate_sf_select_element( $sf_object_description["fields"], "uniqueSfField-0", "" ); 232 | } else { 233 | for ( $i = 0; $i < sizeof( $unique_sf_fields ); $i++ ) { 234 | echo $this->generate_sf_select_element( $sf_object_description["fields"], "uniqueSfField-" . $i, $unique_sf_fields[$i] ); 235 | if ( $i != sizeof( $unique_sf_fields ) - 1 ) { 236 | echo " + "; 237 | } 238 | } 239 | } 240 | ?> 241 | 242 |
243 |
244 | 245 |
246 | 266 | 277 | sf->get_all_objects(); 292 | $sf_objects = array(); 293 | 294 | // prepare array of possible required salesforce objects 295 | foreach( $sf_object_description["fields"] as $sf_field ) { 296 | if ( $sf_field["type"] == "reference" && $sf_field["createable"] && !$sf_field["defaultedOnCreate"] ) { 297 | // user is by default included 298 | if( $sf_field["referenceTo"][0] != $to && $sf_field["referenceTo"][0] != "User" ) { 299 | array_push( $sf_objects, array( 300 | "name" => $sf_field["referenceTo"][0], 301 | "label" => $sf_field["referenceTo"][0], 302 | "id" => $sf_field["name"] 303 | ) ); 304 | } 305 | } 306 | } 307 | foreach( $sf_objects_raw["sobjects"] as $sf_object_raw ) { 308 | if( !$sf_object_raw["deprecatedAndHidden"] && $sf_object_raw["createable"] && $sf_object_raw["updateable"] && $sf_object_raw["deletable"] ) { 309 | array_push( $sf_objects, array( 310 | "name" => $sf_object_raw["name"], 311 | "label" => $sf_object_raw["label"], 312 | "id" => $sf_object_raw["name"] . "Id" 313 | ) ); 314 | } 315 | } 316 | 317 | $counter = 0; 318 | ?> 319 |
320 | 321 | display_required_objects_select_element( "defaultRequiredSfObject", $sf_objects, "", "", true ); ?> 322 |

323 | 324 |

325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 340 | 341 | 342 | 343 |
335 | display_required_objects_select_element( "requiredSfObject-" . $counter, $sf_objects, $required_sf_object->name, $required_sf_object->id ); 337 | $counter++; 338 | ?> 339 |
344 |
345 | 346 |
347 |
348 | generate_select_element( $fields, $name, $selected, true ); 361 | } 362 | 363 | /** 364 | * Return select HTML element with given options from the SF field picklist. 365 | * 366 | * @param array $fields Salesforce picklist fields. 367 | * @param string $name Name of the select element. 368 | * @param string $selected Value of the selected option/field. 369 | * @param boolean $required Is select element required, defaults to false. 370 | * @return string Representing HTML select element. 371 | */ 372 | private function generate_sf_picklist_select_element( array $fields, string $name, string $selected, bool $required = false ) { 373 | return $this->generate_select_element( $fields, $name, $selected, false, $required ); 374 | } 375 | 376 | /** 377 | * Return select HTML element with given name and options from WooCommerce. 378 | * 379 | * @param array $field_names WooCommerce object field names. 380 | * @param string $name Name of the select element. 381 | * @param string $selected Name of the selected option. 382 | * @param boolean $required Is select element required, defaults to false. 383 | * @param string $type Type of corresponding SF field, defaults to "". 384 | * @return string Representing HTML element. 385 | */ 386 | private function generate_wc_select_element( array $field_names, string $name, string $selected, bool $required = false, string $type = "" ) { 387 | $fields = array(); 388 | foreach( $field_names as $field_name ) { 389 | array_push( $fields, array( 390 | "name" => $field_name, 391 | "label" => ucwords( str_replace( "_", " ", $field_name ) ) 392 | ) ); 393 | } 394 | return $this->generate_select_element( $fields, $name, $selected, false, $required, $type ); 395 | } 396 | 397 | /** 398 | * Return an option HTML element as a string. 399 | * 400 | * @param string $name 401 | * @param string $value 402 | * @param boolean $is_selected 403 | * @return string 404 | */ 405 | private function generate_option_element( string $name, string $value, bool $is_selected = false ) { 406 | $option_element = ""; 411 | } 412 | 413 | /** 414 | * Return select HTML element 415 | * @param array $fields Array of objects with option names and values. 416 | * @param string $name Name of the select element. 417 | * @param string $selected Name of the selected option, defaults to "". 418 | * @param boolean $ignore_refrences True to ignore references fields, defaults to false. 419 | * @param boolean $required Is select element required, defaults to false. 420 | * @param string $type Type of corresponding SF field, defaults to "". 421 | * @return string Representing HTML element. 422 | */ 423 | private function generate_select_element( array $fields, string $name, string $selected = "", bool $ignore_refrences = false, bool $required = false, string $type = "" ) { 424 | 425 | $select_element = ""; 430 | $select_element .= ""; 431 | 432 | if ( !empty( $type ) ) { 433 | if ( in_array( $type, array( "string", "double", "integer", "url", "phone", "textarea" ) ) ) { 434 | array_push( $fields, array( "name" => "custom-value", "label" => "Custom value" ) ); 435 | } else if ( $type == "date" ) { 436 | array_push( $fields, array( "name" => "custom-current-date", "label" => "Current Date" ) ); 437 | } else if ( $type == "boolean" ) { 438 | array_push( $fields, array( "name" => "custom-true", "label" => "True" ) ); 439 | array_push( $fields, array( "name" => "custom-false", "label" => "False" ) ); 440 | } 441 | } 442 | 443 | foreach( $fields as $field ) { 444 | // field check 445 | if ( $ignore_refrences ) { 446 | if ( !$field["createable"] || $field["deprecatedAndHidden"] || $field["type"] == "reference" ) { 447 | continue; 448 | } 449 | } 450 | 451 | if( array_key_exists( "active", $field ) && !$field["active"] ) { 452 | continue; 453 | } 454 | 455 | if ( array_key_exists( "value", $field ) && !array_key_exists( "name", $field ) ) { 456 | $field["name"] = $field["value"]; 457 | } 458 | 459 | if ( $selected == $field["name"] ) { 460 | $select_element .= $this->generate_option_element( $field["label"], $field["name"], true ); 461 | } else { 462 | $select_element .= $this->generate_option_element( $field["label"], $field["name"]); 463 | } 464 | } 465 | 466 | $select_element .= ""; 467 | return $select_element; 468 | } 469 | 470 | /** 471 | * Return NWSI model property keys (attribute names). 472 | * 473 | * @param string $type Name of the model. 474 | * @return array 475 | */ 476 | private function get_wc_object_description( string $type ) { 477 | switch( strtolower( $type ) ) { 478 | case "product": 479 | case "order product": 480 | $model = new NWSI_Product_Model(); 481 | break; 482 | case "order": 483 | $model = new NWSI_Order_Model(); 484 | break; 485 | case "order_item": 486 | case "order item": 487 | $model = new NWSI_Order_Item_Model(); 488 | break; 489 | default: 490 | return null; 491 | } 492 | return $model->get_property_keys(); 493 | } 494 | 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /includes/views/class-nwsi-relationships-table.php: -------------------------------------------------------------------------------- 1 | _args["plural"] = "nwsi_relationships_table"; 19 | 20 | $this->process_bulk_action(); 21 | } 22 | 23 | /** 24 | * Set items array. 25 | * 26 | * @param array $items 27 | */ 28 | public function set_items( array $items ) { 29 | $this->items = $items; 30 | } 31 | 32 | /** 33 | * Prepare items for the table to process. 34 | * 35 | * @override 36 | */ 37 | public function prepare_items() { 38 | $columns = $this->get_columns(); 39 | $hidden = $this->get_hidden_columns(); 40 | $sortable = $this->get_sortable_columns(); 41 | 42 | $this->_column_headers = array( $columns, $hidden, $sortable ); 43 | } 44 | 45 | /** 46 | * Generate the table navigation above or below the table. 47 | * 48 | * @param string $which Is it "top" or "bottom". 49 | */ 50 | protected function display_tablenav( $which ) { 51 | ?> 52 |
53 | has_items() ): ?> 54 |
55 | bulk_actions( $which ); ?> 56 |
57 | 58 | extra_tablenav( $which ); ?> 59 | pagination( $which ); ?> 60 | 61 |
62 |
63 | "action_label", 70 | * ... 71 | * ] 72 | * 73 | * @override 74 | * @return array 75 | */ 76 | public function get_bulk_actions() { 77 | return array( 78 | "delete" => __( "Delete", "woocommerce-integration-nwsi" ), 79 | "activate" => __( "Activate", "woocommerce-integration-nwsi" ), 80 | "deactivate" => __( "Deactivate", "woocommerce-integration-nwsi" ), 81 | ); 82 | } 83 | 84 | /** 85 | * Process choosen bulk action. 86 | * 87 | * @override 88 | */ 89 | public function process_bulk_action() { 90 | if ( !array_key_exists( "bulk", $_POST ) || empty( $_POST["bulk"] ) ) { 91 | return; 92 | } 93 | 94 | // security check! 95 | if ( isset( $_POST["_wpnonce"] ) && !empty( $_POST["_wpnonce"] ) ) { 96 | $nonce = filter_input( INPUT_POST, "_wpnonce", FILTER_SANITIZE_STRING ); 97 | if ( !wp_verify_nonce( $nonce, "woocommerce-settings" ) ) { 98 | return; 99 | } 100 | } 101 | 102 | require_once ( NWSI_DIR_PATH . "includes/controllers/core/class-nwsi-db.php" ); 103 | $db = new NWSI_DB(); 104 | 105 | $bulk = $_POST["bulk"]; 106 | switch( $this->current_action() ) { 107 | case "delete": 108 | $db->delete_relationships_by_id( $bulk ); 109 | break; 110 | case "activate": 111 | $db->activate_relationships_by_id( $bulk ); 112 | break; 113 | case "deactivate": 114 | $db->deactivate_relationships_by_id( $bulk ); 115 | break; 116 | default: 117 | break; 118 | } 119 | return; 120 | } 121 | 122 | /** 123 | * Return HTML for row checkbox 124 | * @override 125 | * @param array $item 126 | * @return string 127 | */ 128 | protected function column_cb( $item ) { 129 | return sprintf( 130 | '', $item["id"] 131 | ); 132 | } 133 | 134 | /** 135 | * Define columns to use in listing table 136 | * @override 137 | * @return array 138 | */ 139 | public function get_columns() { 140 | $columns = array( 141 | "cb" => "", 142 | "id" => __( "ID", "woocommerce-integration-nwsi" ), 143 | "relationship" => __( "Relationship (SF - WC)", "woocommerce-integration-nwsi" ), 144 | "date-created" => __( "Date Created", "woocommerce-integration-nwsi" ), 145 | "date-updated" => __( "Date Updated", "woocommerce-integration-nwsi" ), 146 | "active" => __( "Is Active", "woocommerce-integration-nwsi" ), 147 | ); 148 | 149 | return $columns; 150 | } 151 | 152 | /** 153 | * Define what data to show on each column of the table 154 | * @override 155 | * @param array $item 156 | * @param string $column_name Current column name. 157 | * @return mixed 158 | */ 159 | public function column_default( $item, $column_name ) { 160 | switch( $column_name ) { 161 | case "id": 162 | case "relationship": 163 | case "date-created": 164 | case "date-updated": 165 | case "active": 166 | return $item[ $column_name ]; 167 | default: 168 | return print_r( $item, true ) ; 169 | } 170 | } 171 | 172 | /** 173 | * Define which columns are hidden. 174 | * 175 | * @override 176 | * @return array 177 | */ 178 | public function get_hidden_columns() { 179 | return array( "id" ); 180 | } 181 | 182 | /** 183 | * Define the sortable columns. 184 | * 185 | * @override 186 | * @return array 187 | */ 188 | public function get_sortable_columns() { 189 | return array(); 190 | } 191 | 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /includes/views/class-nwsi-settings.php: -------------------------------------------------------------------------------- 1 | init(); 52 | $this->process_requests(); 53 | } 54 | 55 | /** 56 | * Initialize class attributes. 57 | */ 58 | public function init() { 59 | $this->id = "nwsi"; 60 | $this->method_title = __( "Salesforce", "woocommerce-integration-nwsi" ); 61 | 62 | $this->sf = new NWSI_Salesforce_Object_Manager(); 63 | $this->relationship_form = new NWSI_Relationships_Form( $this->sf ); 64 | $this->db = new NWSI_DB(); 65 | $this->utility = new NWSI_Utility(); 66 | $this->admin_notice = new NWSI_Admin_Notice(); 67 | } 68 | 69 | /** 70 | * Process GET and POST requests. 71 | */ 72 | private function process_requests() { 73 | if ( !array_key_exists( "rel", $_GET ) ) { 74 | if ( !empty( $_POST["save"] ) ) { 75 | $this->update_additional_settings($_POST); 76 | 77 | if ( array_key_exists( "woocommerce_nwsi_consumer_secret", $_POST ) 78 | && array_key_exists( "woocommerce_nwsi_consumer_key", $_POST ) ) { 79 | 80 | $redirect_uri = $this->sf->redirect_to_salesforce( 81 | $_POST["woocommerce_nwsi_consumer_key"], 82 | $_POST["woocommerce_nwsi_consumer_secret"] 83 | ); 84 | 85 | if ( !empty( $redirect_uri ) ) { 86 | header( "Location: " . $redirect_uri ); 87 | } 88 | } 89 | } 90 | 91 | if ( array_key_exists( "code", $_GET ) && !empty( $_GET["code"] ) ) { 92 | $redirect_uri = $this->obtain_access_token( $_GET["code"] ); 93 | header( "Location: " . $redirect_uri ); 94 | } 95 | 96 | $this->init_form_fields(); 97 | $this->init_settings(); 98 | 99 | if ( array_key_exists( "status", $_GET ) && !empty( $_GET["status"] ) ) { 100 | if ( !empty( $_GET["source"] ) && $_GET["source"] == "access_token" ) { 101 | $this->admin_notice->display_access_token_notice( $_GET["status"] ); 102 | } else { 103 | $this->admin_notice->display_relationship_notice( $_GET["status"] ); 104 | } 105 | } 106 | 107 | add_action( "woocommerce_update_options_integration_" . $this->id, array( $this, "process_admin_options" ) ); 108 | } else if ( !empty( $_GET["rel"] ) && !empty( $_POST ) ) { 109 | 110 | $status = $this->manage_relationship_process( $_GET["rel"] ); 111 | $redirect_uri = admin_url("admin.php", "https") 112 | . "?page=" . $_GET["page"] . "&tab=" . $_GET["tab"] 113 | . "§ion=" . $_GET["section"] . "&status=" . $status; 114 | 115 | header( "Location: " . $redirect_uri ); 116 | } 117 | } 118 | 119 | /** 120 | * Update options such as automatic order sync and login URL. 121 | * 122 | * @param array $data Usually $_POST. 123 | */ 124 | private function update_additional_settings( $data ) { 125 | // update automatic order sync option 126 | if ( empty( $data["automatic_order_sync"] ) ) { 127 | update_option( "woocommerce_nwsi_automatic_order_sync", "0" ); 128 | } else { 129 | update_option( "woocommerce_nwsi_automatic_order_sync", "1" ); 130 | } 131 | // update login url 132 | if ( !empty( $data["woocommerce_nwsi_login_url"] ) ) { 133 | $login_url = $data["woocommerce_nwsi_login_url"]; 134 | if ( substr( $login_url, -1 ) === "/" ) { 135 | $login_url = esc_attr__( rtrim( trim( $login_url ), "/" ) ); 136 | } 137 | $this->sf->set_login_uri( $login_url ); 138 | update_option( "woocommerce_nwsi_login_url", $login_url ); 139 | } 140 | } 141 | 142 | /** 143 | * Save new or update existing relationship. 144 | * 145 | * @param string $rel_type Type of relationship (new or existing). 146 | * @return string Status. 147 | */ 148 | private function manage_relationship_process( $rel_type ) { 149 | $status = ""; 150 | 151 | if ( $rel_type == "new" ) { 152 | $response = $this->db->save_new_relationship( $_GET["from"], $_GET["from_label"], $_GET["to"], $_GET["to_label"], $_POST ); 153 | if ( $response ) { 154 | $status = "rel_new_success"; 155 | } else { 156 | $status = "rel_new_fail"; 157 | } 158 | } else if ( $rel_type == "existing" ) { 159 | $response = $this->db->update_relationship( $_GET["key"], $_POST ); 160 | if ( $response ) { 161 | $status = "rel_edit_success"; 162 | } else { 163 | $status = "rel_edit_fail"; 164 | } 165 | } 166 | return $status; 167 | } 168 | 169 | /** 170 | * Obtain access token and display corresponding admin notice. 171 | * 172 | * @param string $code Obtained after user login. 173 | * @return string Redirect URL. 174 | */ 175 | private function obtain_access_token( $code ) { 176 | $status = $this->sf->get_access_token( $code ); 177 | 178 | $redirect_uri = admin_url( "admin.php", "https" ) 179 | . "?page=" . $_GET["page"] . "&tab=" . $_GET["tab"] 180 | . "§ion=" . $_GET["section"] 181 | . "&status=" . $status . "&source=access_token"; 182 | 183 | return $redirect_uri; 184 | } 185 | 186 | /** 187 | * Display form for choosing Salesforce and WooCommerce objects for 188 | * new relationship. 189 | */ 190 | private function display_add_new_relationship_form() { 191 | $sf_objects = $this->sf->get_all_objects(); 192 | ?> 193 | 194 | 195 | 196 | 199 | 208 | 209 | 210 | 213 | 226 | 227 | 228 | 233 | 234 | 235 |
197 | 198 | 200 | 207 |
211 | 212 | 214 | 225 |
229 | 232 |
236 | 248 |
249 | 250 | /> 251 |
252 | 260 |
261 |

262 | display_add_new_relationship_form(); ?> 263 | 264 |
265 |

266 | display_automatic_sync_settings(); ?> 267 |
268 |
269 | relationships_table = new NWSI_Relationships_Table(); 271 | $this->relationships_table->process_bulk_action(); 272 | $relationships = $this->db->get_relationships(); 273 | 274 | $date_time_format = get_option( "date_format" ) . " " . get_option( "time_format" ); 275 | $data = array(); 276 | foreach( $relationships as $relationship ) { 277 | $temp = array(); 278 | 279 | $temp["id"] = $relationship->id; 280 | $temp["date-created"] = date( $date_time_format, strtotime( $relationship->date_created ) ); 281 | $temp["date-updated"] = date( $date_time_format, strtotime( $relationship->date_updated ) ); 282 | $temp["active"] = ( intval( $relationship->active ) == 1 ) ? "Yes" : "No"; 283 | $temp["relationship"] = "" . "" 286 | . $relationship->to_object_label . " - " . $relationship->from_object_label . " "; 287 | 288 | array_push( $data, $temp ); 289 | } 290 | 291 | $this->relationships_table->set_items( $data ); 292 | $this->relationships_table->prepare_items(); 293 | $this->relationships_table->display(); 294 | 295 | ?> 296 |
297 |
298 | relationship_form->display_blank( $_GET["from"], $from_label, $_GET["to"], $to_label ); 322 | 323 | } else if ( array_key_exists( "key", $_GET ) && !empty( $_GET["key"] ) ) { 324 | // form for already existing relationship 325 | $this->relationship_form->display_existing( $this->db->get_relationship_by_key( $_GET["key"] ) ); 326 | } 327 | 328 | } else { 329 | // default view 330 | parent::admin_options(); 331 | $this->display_additional_settings_fields(); 332 | 333 | if ( $this->sf->has_access_token() ) { 334 | $this->display_default_settings_page(); 335 | 336 | wp_enqueue_script( "nwsi-settings-js", plugins_url("../js/nwsi-settings.js", __FILE__ ), array( "jquery" ) ); 337 | wp_localize_script( "nwsi-settings-js", "ajaxObject", array( "url" => admin_url( "admin-ajax.php" ) ) ); 338 | } 339 | } 340 | } 341 | 342 | /** 343 | * Echo HTML for additional settings fields such as callback and login URL 344 | */ 345 | private function display_additional_settings_fields() { 346 | $login_url = get_option( "woocommerce_nwsi_login_url" ); 347 | if ( !$login_url ) { 348 | $login_url = ""; 349 | } 350 | ?> 351 | 352 | 353 | 356 | 362 | 363 | 364 | 367 | 373 | 374 |
354 | 355 | 357 |
358 | 359 | 360 |
361 |
365 | 366 | 368 |
369 | 370 | " > 371 |
372 |
375 | form_fields = array( 384 | "consumer_key" => array( 385 | "title" => __( "Consumer key", "woocommerce-integration-nwsi" ), 386 | "type" => "text", 387 | "default" => "" 388 | ), 389 | "consumer_secret" => array( 390 | "title" => __( "Consumer secret", "woocommerce-integration-nwsi" ), 391 | "type" => "password", 392 | "default" => "" 393 | ) 394 | ); 395 | } 396 | 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /neuralab-woocommerce-salesforce-integration.php: -------------------------------------------------------------------------------- 1 | parent_base === "plugins" ) { 44 | ?> 45 |
46 |

WooCommerce before activating the Neuralab WooCommerce SalesForce Integration plugin!", "woocommerce-integration-nwsi" ); ?>

47 |
48 | define_plugin_constants(); 84 | 85 | require_once( "includes/controllers/core/class-nwsi-salesforce-object-manager.php" ); 86 | require_once( "includes/controllers/core/class-nwsi-salesforce-worker.php" ); 87 | require_once( "includes/controllers/utilites/class-nwsi-utility.php" ); 88 | require_once( "includes/views/class-nwsi-settings.php" ); 89 | 90 | add_filter( "woocommerce_integrations", array( $this, "add_integration_section" ) ); 91 | 92 | if ( is_admin() ) { 93 | require_once( "includes/views/class-nwsi-orders-view.php" ); 94 | $orders_view = new NWSI_Orders_View(); 95 | $orders_view->register_hooks(); 96 | 97 | //TODO: Find a better way of doing this 98 | add_action('admin_enqueue_scripts', function() { 99 | wp_enqueue_style( "nwsi-settings-style", plugins_url( "/includes/style/nwsi-settings.css", __FILE__ ) ); 100 | }); 101 | } 102 | 103 | $this->worker = new NWSI_Salesforce_Worker(); 104 | // add_action( "woocommerce_checkout_order_processed", array( $this, "process_order" ), 10, 1 ); 105 | add_action( "woocommerce_thankyou", array( $this, "process_order" ), 90, 1 ); 106 | 107 | } 108 | 109 | /** 110 | * Define all the constants used in plugin. 111 | */ 112 | private static function define_plugin_constants() { 113 | if ( !defined( "NWSI_DIR_NAME" ) ) { 114 | define( "NWSI_DIR_NAME", basename( __DIR__ ) ); 115 | } 116 | 117 | if ( !defined( "NWSI_DIR_PATH" ) ) { 118 | define( "NWSI_DIR_PATH", plugin_dir_path( __FILE__ ) ); 119 | } 120 | 121 | if ( !defined( "NWSI_DIR_URL" ) ) { 122 | define( "NWSI_DIR_URL", plugins_url( "/", __FILE__ ) ); 123 | } 124 | } 125 | 126 | /** 127 | * Process order. 128 | * @param int $order_id 129 | */ 130 | public function process_order( $order_id ) { 131 | if ( !empty( get_option( "woocommerce_nwsi_automatic_order_sync" ) ) ) { 132 | $this->worker->process_order( $order_id ); 133 | } 134 | } 135 | 136 | /** 137 | * Create plugins table and triggers in DB. 138 | */ 139 | public static function install() { 140 | if ( !current_user_can( "activate_plugins" ) ) { 141 | return; 142 | } 143 | 144 | NWSI_Main::define_plugin_constants(); 145 | update_option( "woocommerce_nwsi_login_url", "https://login.salesforce.com" ); 146 | 147 | require_once( "includes/controllers/core/class-nwsi-db.php" ); 148 | $db = new NWSI_DB(); 149 | $db->create_relationship_table(); 150 | 151 | if ( $db->is_relationship_table_empty() ) { 152 | // insert default relationships 153 | require_once( "includes/controllers/utilites/class-nwsi-utility.php" ); 154 | $utility = new NWSI_Utility(); 155 | $relationships = $utility->load_from_file( "default_relationships.json", "data" ); 156 | 157 | if ( !empty( $relationships ) ) { 158 | foreach( $relationships as $relationship ) { 159 | $db->save_new_relationship( $relationship->from_object, $relationship->from_object_label, 160 | $relationship->to_object, $relationship->to_object_label, $relationship->relationships, 161 | $relationship->required_sf_objects, $relationship->unique_sf_fields ); 162 | } 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * Delete plugin tables and related WP options. 169 | */ 170 | public static function uninstall() { 171 | if ( !current_user_can( "activate_plugins" ) ) { 172 | return; 173 | } 174 | 175 | delete_option( "woocommerce_nwsi_settings" ); 176 | delete_option( "woocommerce_nwsi_access_token" ); 177 | delete_option( "woocommerce_nwsi_refresh_token" ); 178 | delete_option( "woocommerce_nwsi_instance_url" ); 179 | delete_option( "woocommerce_nwsi_automatic_order_sync" ); 180 | delete_option( "woocommerce_nwsi_connection_hash" ); 181 | delete_option( "woocommerce_nwsi_login_url" ); 182 | 183 | require_once( "includes/controllers/core/class-nwsi-db.php" ); 184 | $db = new NWSI_DB(); 185 | $db->delete_relationship_table(); 186 | 187 | // clear any cached data that has been removed 188 | wp_cache_flush(); 189 | } 190 | 191 | /** 192 | * Return integrations array with new section element. Needed for 193 | * woocommerce_integrations filter hook. 194 | * 195 | * @param array $integrations 196 | * @return array 197 | */ 198 | public function add_integration_section( $integrations ) { 199 | $integrations[] = "NWSI_Settings"; 200 | return $integrations; 201 | } 202 | 203 | /** 204 | * Return class instance. 205 | * 206 | * @return NWSI_Main 207 | */ 208 | public static function get_instance() { 209 | if ( is_null( self::$instance ) ) { 210 | self::$instance = new self; 211 | } 212 | return self::$instance; 213 | } 214 | 215 | /** 216 | * Cloning is forbidden. 217 | */ 218 | public function __clone() { 219 | _doing_it_wrong( __FUNCTION__, __( "Cloning is forbidden!", "woocommerce-integration-nwsi" ), "4.0" ); 220 | } 221 | 222 | /** 223 | * Unserializing instances of this class is forbidden. 224 | */ 225 | public function __wakeup() { 226 | _doing_it_wrong( __FUNCTION__, __( "Unserializing instances is forbidden!", "woocommerce-integration-nwsi" ), "4.0" ); 227 | } 228 | 229 | } 230 | } 231 | 232 | register_activation_hook( __FILE__, array( "NWSI_Main", "install" ) ); 233 | register_uninstall_hook( __FILE__, array( "NWSI_Main", "uninstall" ) ); 234 | 235 | add_action( "plugins_loaded", array( "NWSI_Main", "get_instance" ), 0 ); 236 | } else { 237 | add_action( "admin_notices", "nwsi_admin_notice_missing_woocommerce" ); 238 | } 239 | --------------------------------------------------------------------------------