├── 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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 = "
(Salesforce) | 116 |(WooCommerce) | 117 | 118 | 119 | 120 | 128 |
---|---|
130 | *"; 138 | $required = true; 139 | } 140 | ?> 141 | 142 | " value="" /> 143 | | 144 |
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 | |
206 |
197 | 198 | | 199 |200 | 207 | | 208 |
---|---|
211 | 212 | | 213 |214 | 225 | | 226 |
229 | 232 | | 233 |
354 | 355 | | 356 |357 | 361 | | 362 |
---|---|
365 | 366 | | 367 |368 | 372 | | 373 |
WooCommerce before activating the Neuralab WooCommerce SalesForce Integration plugin!", "woocommerce-integration-nwsi" ); ?>
47 |