app=$app; $this->intern = true; } public function ImportList() { $msg = $this->app->erp->base64_url_encode('
Sie können hier die Shops einstellen
'); header('Location: index.php?module=onlineshops&action=list&msg='.$msg); exit; } /** * This function returns the number of orders which have not yet been imported */ public function ImportGetAuftraegeAnzahl() { // Query the API to get new orders, filtered by the order status as specifed by the user. // We set per_page to 100 - this could lead to a situation where there are more than // 100 new Orders, but we still only return 100. // Array containing additional settings, namely 'ab_nummer' (containting the next order number to get) // and 'holeallestati' (an integer) $tmp = $this->CatchRemoteCommand('data'); // Only orders having an order number greater or equal than this should be fetched. null otherwise $number_from = empty($tmp['ab_nummer']) ? null : (int)$tmp['ab_nummer']; // pending orders will be fetched into this array. it's length is returned at the end of the funciton $pendingOrders = array(); if ($number_from) { // Number-based import is selected // The WooCommerce API doenst allow for a proper "greater than id n" request. // we fake this behavior by creating an array that contains 'many' (~ 1000) consecutive // ids that are greater than $from_number and use this array with the 'include' property // of the WooCommerce API $number_to = $number_from+800; if(!empty($tmp['bis_nummer'])){ $number_to = $tmp['bis_nummer']; } $fakeGreaterThanIds = range($number_from, $number_to); $pendingOrders = $this->client->get('orders', [ 'per_page' => 100, 'include' => implode(",",$fakeGreaterThanIds), ]); } else { // fetch posts by status $pendingOrders = $this->client->get('orders', [ 'status' => array_map('trim',explode(';', $this->statusPending)), 'per_page' => 100 ]); } return (!empty($pendingOrders)?count($pendingOrders):0); } /** * Calling this function queries the api for pending orders and returns them * as an array. * * TODO: Only one single order is returned per invocation of this function. * Given that we have to perform an exteremly expensive external HTTP call * every time we call this function and could easily process more than one * order this seems very bad performance-wise. */ public function ImportGetAuftrag() { // Array containing additional settings, namely 'ab_nummer' (containting the next order number to get) // and 'holeallestati' (an integer) $tmp = $this->CatchRemoteCommand('data'); // Only orders having an order number greater or equal than this should be fetched. null otherwise $number_from = empty($tmp['ab_nummer']) ? null : (int)$tmp['ab_nummer']; // pending orders will be fetched into this array. it's length is returned at the end of the funciton $pendingOrders = array(); if ($number_from) { // Number-based import is selected // The WooCommerce API doenst allow for a proper "greater than id n" request. // we fake this behavior by creating an array that contains 'many' (~ 1000) consecutive // ids that are greater than $from_number and use this array with the 'include' property // of the WooCommerce API $number_to = $number_from+800; if(!empty($tmp['bis_nummer'])){ $number_to = $tmp['bis_nummer']; } $fakeGreaterThanIds = range($number_from, $number_to); $pendingOrders = $this->client->get('orders', [ 'per_page' => 20, 'include' => implode(',',$fakeGreaterThanIds), 'order' => 'asc', 'orderby' => 'id' ]); } else { // fetch posts by status $pendingOrders = $this->client->get('orders', [ 'status' => array_map('trim',explode(';', $this->statusPending)), 'per_page' => 20, 'order' => 'asc', 'orderby' => 'id' ]); } // Return an empty array in case there are no orders to import if ((!empty($pendingOrders)?count($pendingOrders):0) === 0) { return null; } $tmp = []; foreach ($pendingOrders as $pendingOrder){ $wcOrder = $pendingOrder; $order = $this->parseOrder($wcOrder); if (is_null($wcOrder)) { continue; } $tmp[] = [ 'id' => $order['auftrag'], 'sessionid' => '', 'logdatei' => '', 'warenkorb' => base64_encode(serialize($order)), ]; } return $tmp; } // This function searches the wcOrder for the specified WC Meta key // and returns it if found, null otherise public function get_wc_meta($wcOrder, $meta_key) { $value = null; foreach ($wcOrder->meta_data as $meta) { if ($meta->key == $meta_key) { $value = $meta->value; break; } } return $value; } // Parse the given WooCommerce order, return a Xentral array-represented order. // Overload this method whenever additional attributes are required. public function parseOrder($wcOrder) { $order = array(); $order['auftragsdaten'] = $wcOrder; $isBillingCompany = !self::emptyString( $wcOrder->billing->company ); $isShippingCompany = !self::emptyString( $wcOrder->shipping->company ); $seperateShippingAddress = !self::compareObjects( $wcOrder->billing, $wcOrder->shipping, ['first_name', 'second_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country'] ); if ($isBillingCompany) { $order['name'] = $wcOrder->billing->company; $order['anrede'] = 'firma'; $order['ansprechpartner'] = $wcOrder->billing->first_name . ' ' . $wcOrder->billing->last_name; } else { $order['name'] = $wcOrder->billing->first_name . ' ' . $wcOrder->billing->last_name; // Retrieve title from meta data // This is not a standard WC Feature! Should work with the very popuplar "WooCommerce germanized" plugin though $meta_title = $this->get_wc_meta($wcOrder, "_billing_title"); if (!is_null($meta_title)) { $order['anrede'] = ($meta_title == 'mrs') ? 'frau' : 'herr'; } } if(!empty($wcOrder->subshop)){ $order['subshop'] = $wcOrder->subshop; } // General order properties and billing address $order['auftrag'] = $wcOrder->id; $order['order'] = json_decode(json_encode($wcOrder), true); $order['strasse'] = $wcOrder->billing->address_1; if(!empty($wcOrder->billing->address_2)){ $order['adresszusatz'] = $wcOrder->billing->address_2; } $order['plz'] = $wcOrder->billing->postcode; $order['ort'] = $wcOrder->billing->city; $order['land'] = $wcOrder->billing->country; $order['email'] = $wcOrder->billing->email; $order['telefon'] = $wcOrder->billing->phone; $order['bestelldatum'] = $wcOrder->date_created; $order['gesamtsumme'] = $wcOrder->total; $order['transaktionsnummer'] = $wcOrder->transaction_id; $order['onlinebestellnummer'] = $wcOrder->number; $order['versandkostenbrutto'] = $wcOrder->shipping_total + $wcOrder->shipping_tax; $order['internebemerkung'] = $wcOrder->customer_note; if(!empty((string)$wcOrder->currency)){ $warenkorb['waehrung'] = (string)$wcOrder->currency; } // // // Coupon Codes // $discount_total = (float)$wcOrder->discount_total; $discount_tax = (float)$wcOrder->discount_tax; if($discount_total != 0) { // Discount was applied to this order // Calculate coupon amount if($discount_tax == 0) { // Tax calculations are not enabled for any used coupon $order['rabattnetto'] = -abs((float)$discount_total); } else{ // At least one used coupon has tax calculations enabled $order['rabattbrutto'] = -abs((float)$discount_total); $order['rabattbrutto'] += -abs((float)$discount_tax); } // Set coupon name $couponLine = $wcOrder->coupon_lines; // Check if we have a valid coupon line just to be sure and set the coupon name if($couponLine && is_array($couponLine) && isset($couponLine[0]) && (String)$couponLine[0]->code !== '') { $order['rabattname'] = (String)$couponLine[0]->code; } } $seperateShippingAddress = !self::compareObjects( $wcOrder->billing, $wcOrder->shipping, ['first_name', 'second_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country'] ); if ($seperateShippingAddress) { $order['abweichendelieferadresse'] = '1'; if ($isShippingCompany) { $order['lieferadresse_name'] = $wcOrder->shipping->company; $order['lieferadresse_ansprechpartner'] = $wcOrder->shipping->first_name . ' ' . $wcOrder->shipping->last_name; } else { $order['lieferadresse_name'] = $wcOrder->shipping->first_name . ' ' . $wcOrder->shipping->last_name; } $order['lieferadresse_strasse'] = $wcOrder->shipping->address_1; if(!empty($wcOrder->shipping->address_2)){ $order['lieferadresse_adresszusatz'] = $wcOrder->shipping->address_2; } $order['lieferadresse_plz'] = $wcOrder->shipping->postcode; $order['lieferadresse_ort'] = $wcOrder->shipping->city; $order['lieferadresse_land'] = $wcOrder->shipping->country; } // VAT stuff $vatId = $this->get_wc_meta($wcOrder, "_billing_ustid"); if (!is_null($vatId) && !self::emptyString($vatId)) { $order['ustid'] = $vatId; } foreach ($wcOrder->line_items as $wcOrderItem){ $order['articlelist'][] = $this->parseItem($wcOrderItem); } $order['zahlungsweise'] = $wcOrder->payment_method; $order['lieferung'] = $wcOrder->shipping_lines[0]->method_id; return $order; } function parseItem($wcOrderItem){ // The WC API doesnt expose the net price of a single product in the get order endpoint. // We could query each individual product and get the price, but that would result in a // huge amount of HTTP requests. // // We could use the price_netto attribute in the order item, but we get a higher precision using // this custom calculation as we have access to the exact `total_tax` amount. // Passing the net value of the line eliminates rounding issues if the position happens to have large quantites switch ($this->priceType){ case 'grosscalculated': $priceValue = ((float)$wcOrderItem->subtotal + (float)$wcOrderItem->subtotal_tax) / (float)$wcOrderItem->quantity; $priceType = 'price'; break; case 'netcalculated': default: $priceType = 'price_netto'; $priceValue = (float)$wcOrderItem->subtotal / (float)$wcOrderItem->quantity; break; } $orderItem = array(); $orderItem['articleid'] = $wcOrderItem->sku; $orderItem['name'] = $wcOrderItem->name; $orderItem[$priceType] = $priceValue; $orderItem['quantity'] = $wcOrderItem->quantity; // The item could be a variable product in which case we have to retrieve the sku of the variation product if (!empty($wcOrderItem->variation_id)) { $variation_product_sku = $this->getSKUByShopId($wcOrderItem->id,$wcOrderItem->variation_id); if (!empty($variation_product_sku)) { $orderItem['articleid'] = $variation_product_sku; } } return $orderItem; } /** * Sets the Order status to processing, meaning we've successfully imported * the order into our DB. This prevents the order from beeing imported again. */ public function ImportDeleteAuftrag() { $orderId = $this->CatchRemoteCommand('data')['auftrag']; if (!empty($orderId)) { $this->client->put('orders/'.$orderId, [ 'status' => $this->statusProcessing, ]); } return 'ok'; } /** * Updates the order status once payment and shipping are set to ok. * Also updates the order with the shipping tracking code * @return string * @throws WCHttpClientException */ public function ImportUpdateAuftrag() { $tmp = $this->CatchRemoteCommand('data'); $orderId = $tmp['auftrag']; $paymentOk = $tmp['zahlung']; $shippingOk = $tmp['versand']; $trackingCode = $tmp['tracking']; $carrier = $tmp['versandart']; if ($paymentOk === 'ok' || $paymentOk === '1'){ $paymentOk = true; } if ($shippingOk === 'ok' || $shippingOk === '1'){ $shippingOk = true; } if (!empty($trackingCode)) { $this->client->post('orders/'.$orderId.'/notes', [ 'note' => 'Tracking Code: ' . $trackingCode ]); $this->WooCommerceLog("Tracking Code Rückmeldung für Auftrag: $orderId", $trackingCode); } if ($paymentOk && $shippingOk) { $updateData = [ 'status' => $this->statusCompleted, 'meta_data' => [ [ 'key' => 'tracking_code', 'value' => $trackingCode ], [ 'key' => 'shipping_carrier', 'value' => $carrier ] ], ]; $this->client->put('orders/'.$orderId, $updateData); $this->WooCommerceLog("Statusrückmeldung 'completed' für Auftrag: $orderId",$this->statusCompleted ); } return 'ok'; } /** * This function syncs the current stock to the remote WooCommerce shop * @return int * @throws WCHttpClientException */ public function ImportSendListLager() { $tmp = $this->CatchRemoteCommand('data'); $anzahl = 0; $ctmp = (!empty($tmp)?count($tmp):0); for($i=0;$i<$ctmp;$i++) { // Get important values from input data $artikel = $tmp[$i]['artikel']; if($artikel === 'ignore') { continue; } $nummer = $tmp[$i]['nummer']; if(!empty($tmp[$i]['artikelnummer_fremdnummern'][0]['nummer'])){ $nummer = $tmp[$i]['artikelnummer_fremdnummern'][0]['nummer']; } $lageranzahl = $tmp[$i]['anzahl_lager']; $pseudolager = trim($tmp[$i]['pseudolager']); $inaktiv = $tmp[$i]['inaktiv']; $status ='publish'; // Do some computations, sanitize input if($pseudolager !== ''){ $lageranzahl = $pseudolager; } if($tmp[$i]['ausverkauft']){ $lageranzahl = 0; } if($inaktiv){ $status = 'private'; } // get the product id that WooCommerce uses to represent the current article $remoteIdInformation = $this->getShopIdBySKU($nummer); if (empty($remoteIdInformation['id'])) { // The online shop doesnt know this article, write to log and continue with next product $this->WooCommerceLog("Artikel $nummer wurde im Online-Shop nicht gefunden! Falsche Artikelnummer im Shop hinterlegt?"); continue; } // Sync settings to online store $updateProductParams = [ 'manage_stock' => true, 'status' => $status, 'stock_quantity' => $lageranzahl // WooCommerce doesnt have a standard property for the other values, we're ignoring them ]; if($remoteIdInformation['isvariant']){ $result = $this->client->put('products/' . $remoteIdInformation['parent'].'/variations/'. $remoteIdInformation['id'], $updateProductParams); }else{ $result = $this->client->put('products/' . $remoteIdInformation['id'], $updateProductParams); } $this->WooCommerceLog("WooCommerce Lagerzahlenübertragung für Artikel: $nummer / $remoteIdInformation[id] - Anzahl: $lageranzahl", $result); $anzahl++; } return $anzahl; } public function ImportStorniereAuftrag() { $orderId = $this->CatchRemoteCommand('data')['auftrag']; if (!empty($orderId)) { $this->client->put('orders/'.$orderId, [ 'status' => 'cancelled', ]); } else { return 'failed'; } return 'ok'; } public function ImportSendList() { $tmp = $this->catchRemoteCommand('data'); $anzahl = 0; for($i=0;$i<(!empty($tmp)?count($tmp):0);$i++){ $artikel = $tmp[$i]['artikel']; $nummer = $tmp[$i]['nummer']; if(!empty($tmp[$i]['artikelnummer_fremdnummern'][0]['nummer'])){ $nummer = $tmp[$i]['artikelnummer_fremdnummern'][0]['nummer']; } $laststock = $tmp[$i]['restmenge']; $inaktiv = $tmp[$i]['inaktiv']; $shippingtime = $tmp[$i]['lieferzeitmanuell']; $hersteller = $tmp[$i]['hersteller']; $herstellerlink = $tmp[$i]['herstellerlink']; $name_de = $tmp[$i]['name_de']; $name_en = $tmp[$i]['name_en']; $description = html_entity_decode($tmp[$i]['uebersicht_de']); $description_en = html_entity_decode($tmp[$i]['uebersicht_en']); $preis = $tmp[$i]['preis']; $kurzbeschreibung = $tmp[$i]['kurztext_de']; $weight_kg = $tmp[$i]['gewicht']; $dim_length = $tmp[$i]['laenge']; $dim_width = $tmp[$i]['breite']; $dim_height = $tmp[$i]['hoehe']; // Sanitize dimensions if (self::emptyString($weight_kg)) $weight_kg = null; if (self::emptyString($dim_length)) $dim_length = null; if (self::emptyString($dim_width)) $dim_width = null; if (self::emptyString($dim_height)) $dim_height = null; $meta_desc = $tmp[$i]['metadescription_de']; $meta_title = $tmp[$i]['metatitle_de']; $pseudopreis = $tmp[$i]['pseudopreis'];//*1.19; if($pseudopreis <= $preis)$pseudopreis = $preis; $steuersatz = $tmp[$i]['steuersatz']; if($steuersatz > 1.10){ $steuersatz = 'normal'; } else{ $steuersatz = 'ermaessigt'; } $lageranzahl = $tmp[$i]['anzahl_lager']; $pseudolager = trim($tmp[$i]['pseudolager']); if($pseudolager > 0) $lageranzahl=$pseudolager; if($tmp[$i]['ausverkauft']=="1"){ $lageranzahl=0; $laststock="1"; } if($inaktiv)$aktiv=0; else $aktiv=1; $product_id = 0; if($laststock!="1") $laststock=0; $remoteIdInformation = $this->getShopIdBySKU($nummer); $product_id = $remoteIdInformation['id']; $commonMetaData = [ ['key' => '_yoast_wpseo_metadesc', 'value' => $meta_desc], ['key' => '_yoast_wpseo_title', 'value' => $meta_title], ]; // Attributes that are used for both updating an existing product as well as creating a new one $commonProductAtts = [ 'name' => $name_de, 'description' => $description, 'status'=>($aktiv?'publish':'private'), 'regular_price' => number_format($pseudopreis,2,'.',''), 'sale_price' => number_format($preis,2,'.',''), 'short_description' => $kurzbeschreibung, 'weight' => $weight_kg, 'dimensions' => [ 'length' => $dim_length, 'width' => $dim_width, 'height' => $dim_height ], 'meta_data' => $commonMetaData, ]; if($lageranzahl===0){ $commonProductAtts['stock_status'] = 'outofstock'; $commonProductAtts['manage_stock'] = true; } elseif($lageranzahl===''){ $commonProductAtts['stock_status'] = 'instock'; $commonProductAtts['manage_stock'] = false; } else{ $commonProductAtts['stock_status'] = 'instock'; $commonProductAtts['manage_stock'] = true; } if($lageranzahl!=='') { $commonProductAtts['stock_quantity'] = (int)$lageranzahl; } if(!is_null($product_id)) { // Such a product already appears to exist, so we update it $this->client->put('products/'.$product_id, array_merge([ ], $commonProductAtts)); $this->WooCommerceLog("WooCommerce Artikel geändert für Artikel: $nummer / $product_id"); } else{ // create a new product $product_id = $this->client->post('products/', array_merge([ 'sku' => $nummer, ], $commonProductAtts))->id; $this->WooCommerceLog("WooCommerce neuer Artikel angelegt: $nummer"); } // TODO: Kategoriebaum und Bilder werden noch nicht uebertragen // if(isset($tmp[$i]['kompletter_kategorienbaum'])){ // $baum = $tmp[$i]['kompletter_kategorienbaum']; // $this->updateKategorieBaum($baum); // } // if(isset($tmp[$i]['Dateien'])){ // $dateien = $tmp[$i]['Dateien']; // $this->save_images($dateien, $product_id); // } // Update the associated product categories $chosenCats = array(); if(isset($tmp[$i]['kategorien']) || isset($tmp[$i]['kategoriename'])){ $kategorien = $tmp[$i]['kategorien']; if (!($kategorien) && !self::emptyString($tmp[$i]['kategoriename'])) { $kategorien = array( array( 'name' => $tmp[$i]['kategoriename'], ) ); } if((!empty($kategorien)?count($kategorien):0)>0){ // Retrive all WC categories via API $allWooCommerceCategories = $this->client->get('products/categories', ['per_page' => '100']); $searchWpCategories = []; foreach($allWooCommerceCategories as $a){ $searchWpCategories[$a->id] = $a->name; } // searchWPCategories is an assoc array of type WCCatId(Int) -> WCCatName(string) // Iterate over the categories that are choosen in xentral foreach($kategorien as $k => $v){ $wawi_cat_name = $v['name']; $wcCatId = null; // If WC has a matching category. We match based on name! if(array_search($wawi_cat_name,array_values($searchWpCategories)) !== false) { // get id of that WC Category $wcCatId = array_search($wawi_cat_name,$searchWpCategories); } else { // No matching category exists $wcCatId = $this->client->post('products/categories', [ 'name' => $wawi_cat_name, ])->id; } if ($wcCatId) { // update category. We first retrieve the product and append the new product category, not replace the entire category array. $alreadyAssignedWCCats = $this->client->get('products/'.$product_id, [ 'per_page' => 1, ])->categories; // Get ids of existing categories $existingCategoryIds = []; foreach ($alreadyAssignedWCCats as $cat) { $existingCategoryIds[] = $cat->id; } $allCatIds = array_merge($existingCategoryIds, array($wcCatId)); // prepare data to be in correct format for WC api. should be individual items with key 'id' and id as value $allCatIdsWCAPIRep = array(); foreach($allCatIds as $id) { $allCatIdsWCAPIRep[] = ['id' => $id]; } // Update category assignment $this->client->put('products/'.$product_id, [ 'categories' => $allCatIdsWCAPIRep, ]); $chosenCats[] = $wcCatId; } } } } $anzahl++; } return $anzahl; // return array($product_id,$anzahl,$nummer,$steuersatz, $preis); } /** * Checks the connection to the WooCommerce API by trying a simple API request * * @return string */ public function ImportAuth() { try { $orders = $this->client->get('orders', ['per_page' => '1']); return 'success'; } catch (Exception $e) { return 'failed: Keine Verbindung zur API - ' . $e->getMessage(); } } /** * This is called by class.remote.php, initializes some class variables from the DB * @param [type] $shopid [description] * @param [type] $data [description] * @return [type] [description] * @throws WCHttpClientException */ public function getKonfig($shopid, $data) { $this->shopid = $shopid; $this->data = $data; $preferences_json = $this->app->DB->Select("SELECT einstellungen_json FROM shopexport WHERE id = '$this->shopid' LIMIT 1"); if($preferences_json){ $preferences = json_decode($preferences_json,true); } $this->protokoll = $preferences['felder']['protokoll']; $ImportWooCommerceApiSecret = $preferences['felder']['ImportWoocommerceApiSecret']; $ImportWooCommerceApiKey = $preferences['felder']['ImportWoocommerceApiKey']; $ImportWooCommerceApiUrl = $preferences['felder']['ImportWoocommerceApiUrl']; $this->statusPending = $preferences['felder']['statusPending']; $this->statusProcessing = $preferences['felder']['statusProcessing']; $this->statusCompleted = $preferences['felder']['statusCompleted']; $this->priceType = $preferences['felder']['priceType']; $this->url = $ImportWooCommerceApiUrl; $this->client = new WCClient( //URL des WooCommerce Rest Servers $ImportWooCommerceApiUrl, //WooCommerce API Key $ImportWooCommerceApiKey, //WooCommerce API Secret $ImportWooCommerceApiSecret, ["query_string_auth" => true] ); } /** * @param array $shopArr * @param array $postData * * @return array */ public function updateShopexportArr($shopArr, $postData) { $shopArr['demomodus'] = 0; $shopArr['anzgleichzeitig'] = 1; $shopArr['cronjobaktiv'] = 1; return $shopArr; } /** * @return JsonResponse|null */ public function AuthByAssistent() { $ImportWooCommerceApiKey = $this->app->Secure->GetPOST('ImportWoocommerceApiKey'); $ImportWooCommerceApiSecret = $this->app->Secure->GetPOST('ImportWoocommerceApiSecret'); $ImportWooCommerceApiUrl = $this->app->Secure->GetPOST('ImportWoocommerceApiUrl'); if(empty($ImportWooCommerceApiUrl)) { return new JsonResponse(['error' => 'Bitte die API-Url angeben'], JsonResponse::HTTP_BAD_REQUEST); } if(empty($ImportWooCommerceApiKey)) { return new JsonResponse(['error' => 'Bitte den API-Key angeben'], JsonResponse::HTTP_BAD_REQUEST); } if(empty($ImportWooCommerceApiSecret)) { return new JsonResponse(['error' => 'Bitte das API-Secret angeben'], JsonResponse::HTTP_BAD_REQUEST); } $this->client = new WCClient( $ImportWooCommerceApiUrl, $ImportWooCommerceApiKey, $ImportWooCommerceApiSecret, ['query_string_auth' => true] ); $auth = $this->ImportAuth(); if ($auth !== 'success') { return new JsonResponse(['error' => $auth], JsonResponse::HTTP_BAD_REQUEST); } return null; } /** * @return array[] */ public function getCreateForm() { return [ [ 'id' => 0, 'name' => 'urls', 'inputs' => [ [ 'label' => 'API Url', 'type' => 'text', 'name' => 'ImportWoocommerceApiUrl', 'validation' => true, ], ], ], [ 'id' => 1, 'name' => 'username', 'inputs' => [ [ 'label' => 'API Key', 'type' => 'text', 'name' => 'ImportWoocommerceApiKey', 'validation' => true, ], ], ], [ 'id' => 2, 'name' => 'password', 'inputs' => [ [ 'label' => 'API Secret', 'type' => 'password', 'name' => 'ImportWoocommerceApiSecret', 'validation' => true, ], ], ], ]; } /** * Returns the WooCommerce Product Id of a product given the SKU (= Xentral arikelnummer) * * @param string $sku Artikelnummer * * @return array|null The WooCommerce product id of the given product, null if such a product does not exist * @throws WCHttpClientException */ private function getShopIdBySKU($sku) { // Retrieve the product with the given sku. // Note: We limit the result set to 1 (per_page=1), so this doesnt work // if there are multiple products with the same sku. should not happen in practice anyway $product = $this->client->get('products', ['sku' => $sku, 'per_page' => 1]); // We look at the first product in the array. // We may get an empty array, in that case null is returned if (isset($product[0])){ return [ 'id' => $product[0]->id, 'parent' => $product[0]->parent_id, 'isvariant' => !empty($product[0]->parent_id)]; } return null; } private function getSKUByShopId($articleid, $variationid) { $product = $this->client->get("products/$articleid/variations/$variationid"); // We look at the first product in the array. // We may get an empty array, in that case null is returned if (!empty($product)) return $product->sku; return null; } public function EinstellungenStruktur() { return array( 'ausblenden'=>array('abholmodus'=>array('zeitbereich')), 'archiv'=>array('ab_nummer'), 'felder'=>array( 'protokoll'=>array('typ'=>'checkbox','bezeichnung'=>'Protokollierung im Logfile:'), 'ImportWoocommerceApiKey'=>array('typ'=>'text','bezeichnung'=>'{|API Key:','size'=>60), 'ImportWoocommerceApiSecret'=>array('typ'=>'text','bezeichnung'=>'{|API Secret|}:','size'=>60), 'ImportWoocommerceApiUrl'=>array('typ'=>'text','bezeichnung'=>'{|API Url|}:','size'=>40), 'statusPending'=>array('typ'=>'text','bezeichnung'=>'{|Statusname Bestellung offen|}:','size'=>40, 'default' => 'pending', 'info' => '({|ggfs. getrennt durch ";": pending;on-hold|})'), 'statusProcessing'=>array('typ'=>'text','bezeichnung'=>'{|Statusname Bestellung in Bearbeitung|}:','size'=>10, 'default' => 'processing'), 'statusCompleted'=>array('typ'=>'text','bezeichnung'=>'{|Statusname Bestellung fertig|}:','size'=>10, 'default' => 'completed'), 'priceType'=>array('typ'=>'select','bezeichnung'=>'{|Preisberechnungsgrundlage bei Auftragsimport|}','optionen'=>array('netcalculated'=>'{|Nettopreis zurückrechnen (Standard)|}','grosscalculated'=>'{|Bruttopreis zurückrechnen|}')), )); } /** * Writes data to the syslog * @param [type] $nachricht message that will be logged * @param string $dump php array or object, printed using print_r */ public function WooCommerceLog($nachricht, $dump = '') { if($this->protokoll){ $this->app->erp->LogFile($nachricht, print_r($dump, true)); } } /** * Compares two Objects and returns true if every variable in items * is the same in $a and $b * @param Obj $a [description] * @param Obj $b [description] * @param array $items [description] * @return Bool [description] */ protected static function compareObjects($a, $b, $items) { foreach($items as $v) { if (property_exists($a, $v) && property_exists($b, $v)) { if ($a->$v != $b->$v) { //print_r($v); return false; } } } return true; } /** * Returns true when the string entered is empty, after stripping whitespace * @param String $string input * @return String output */ protected static function emptyString($string) { return (strlen(trim($string)) == 0); } } class WCClient { /** * WooCommerce REST API WCClient version. */ const VERSION = '3.0.0'; /** * HttpClient instance. * * @var WCHttpClient */ public $http; /** * Initialize client. * * @param string $url Store URL. * @param string $consumerKey Consumer key. * @param string $consumerSecret Consumer secret. * @param array $options WCOptions (version, timeout, verify_ssl). * * @throws WCHttpClientException */ public function __construct($url, $consumerKey, $consumerSecret, $options = []) { $this->http = new WCHttpClient($url, $consumerKey, $consumerSecret, $options); } /** * POST method. * * @param string $endpoint API endpoint. * @param array $data WCRequest data. * * @throws WCHttpClientException * * @return array */ public function post($endpoint, $data) { return $this->http->request($endpoint, 'POST', $data); } /** * PUT method. * * @param string $endpoint API endpoint. * @param array $data WCRequest data. * * @throws WCHttpClientException * * @return array */ public function put($endpoint, $data) { return $this->http->request($endpoint, 'PUT', $data); } /** * GET method. * * @param string $endpoint API endpoint. * @param array $parameters WCRequest parameters. * * @throws WCHttpClientException * * @return array */ public function get($endpoint, $parameters = []) { return $this->http->request($endpoint, 'GET', [], $parameters); } /** * DELETE method. * * @param string $endpoint API endpoint. * @param array $parameters WCRequest parameters. * * @throws WCHttpClientException * * @return array */ public function delete($endpoint, $parameters = []) { return $this->http->request($endpoint, 'DELETE', [], $parameters); } /** * OPTIONS method. * * @param string $endpoint API endpoint. * * @throws WCHttpClientException * * @return array */ public function options($endpoint) { return $this->http->request($endpoint, 'OPTIONS'); } } class WCResponse { /** * WCResponse code. * * @var int */ private $code; /** * WCResponse headers. * * @var array */ private $headers; /** * WCResponse body. * * @var string */ private $body; /** * Initialize response. * * @param int $code WCResponse code. * @param array $headers WCResponse headers. * @param string $body WCResponse body. */ public function __construct($code = 0, $headers = [], $body = '') { $this->code = $code; $this->headers = $headers; $this->body = $body; } /** * Set code. * * @param int $code WCResponse code. */ public function setCode($code) { $this->code = (int) $code; } /** * Set headers. * * @param array $headers WCResponse headers. */ public function setHeaders($headers) { $this->headers = $headers; } /** * Set body. * * @param string $body WCResponse body. */ public function setBody($body) { $this->body = $body; } /** * Get code. * * @return int */ public function getCode() { return $this->code; } /** * Get headers. * * @return array $headers WCResponse headers. */ public function getHeaders() { return $this->headers; } /** * Get body. * * @return string $body WCResponse body. */ public function getBody() { return $this->body; } } class WCOptions { /** * Default WooCommerce REST API version. */ const VERSION = 'wc/v3'; /** * Default request timeout. */ const TIMEOUT = 30; /** * Default WP API prefix. * Including leading and trailing slashes. */ const WP_API_PREFIX = '/wp-json/'; /** * Default User Agent. * No version number. */ const USER_AGENT = 'WooCommerce API Client-PHP'; /** * WCOptions. * * @var array */ private $options; /** * Initialize HTTP client options. * * @param array $options Client options. */ public function __construct($options) { $this->options = $options; } /** * Get API version. * * @return string */ public function getVersion() { return isset($this->options['version']) ? $this->options['version'] : self::VERSION; } /** * Check if need to verify SSL. * * @return bool */ public function verifySsl() { return isset($this->options['verify_ssl']) ? (bool) $this->options['verify_ssl'] : true; } /** * Get timeout. * * @return int */ public function getTimeout() { return isset($this->options['timeout']) ? (int) $this->options['timeout'] : self::TIMEOUT; } /** * Basic Authentication as query string. * Some old servers are not able to use CURLOPT_USERPWD. * * @return bool */ public function isQueryStringAuth() { return isset($this->options['query_string_auth']) ? (bool) $this->options['query_string_auth'] : false; } /** * Check if is WP REST API. * * @return bool */ public function isWPAPI() { return isset($this->options['wp_api']) ? (bool) $this->options['wp_api'] : true; } /** * Custom API Prefix for WP API. * * @return string */ public function apiPrefix() { return isset($this->options['wp_api_prefix']) ? $this->options['wp_api_prefix'] : self::WP_API_PREFIX; } /** * oAuth timestamp. * * @return string */ public function oauthTimestamp() { return isset($this->options['oauth_timestamp']) ? $this->options['oauth_timestamp'] : \time(); } /** * Custom user agent. * * @return string */ public function userAgent() { return isset($this->options['user_agent']) ? $this->options['user_agent'] : self::USER_AGENT; } /** * Get follow redirects * * @return bool */ public function getFollowRedirects() { return isset($this->options['follow_redirects']) ? (bool) $this->options['follow_redirects'] : false; } } class WCRequest { /** * WCRequest url. * * @var string */ private $url; /** * WCRequest method. * * @var string */ private $method; /** * WCRequest paramenters. * * @var array */ private $parameters; /** * WCRequest headers. * * @var array */ private $headers; /** * WCRequest body. * * @var string */ private $body; /** * Initialize request. * * @param string $url WCRequest url. * @param string $method WCRequest method. * @param array $parameters WCRequest paramenters. * @param array $headers WCRequest headers. * @param string $body WCRequest body. */ public function __construct($url = '', $method = 'POST', $parameters = [], $headers = [], $body = '') { $this->url = $url; $this->method = $method; $this->parameters = $parameters; $this->headers = $headers; $this->body = $body; } /** * Set url. * * @param string $url WCRequest url. */ public function setUrl($url) { $this->url = $url; } /** * Set method. * * @param string $method WCRequest method. */ public function setMethod($method) { $this->method = $method; } /** * Set parameters. * * @param array $parameters WCRequest paramenters. */ public function setParameters($parameters) { $this->parameters = $parameters; } /** * Set headers. * * @param array $headers WCRequest headers. */ public function setHeaders($headers) { $this->headers = $headers; } /** * Set body. * * @param string $body WCRequest body. */ public function setBody($body) { $this->body = $body; } /** * Get url. * * @return string */ public function getUrl() { return $this->url; } /** * Get method. * * @return string */ public function getMethod() { return $this->method; } /** * Get parameters. * * @return array */ public function getParameters() { return $this->parameters; } /** * Get headers. * * @return array */ public function getHeaders() { return $this->headers; } /** * Get raw headers. * * @return array */ public function getRawHeaders() { $headers = []; foreach ($this->headers as $key => $value) { $headers[] = $key . ': ' . $value; } return $headers; } /** * Get body. * * @return string */ public function getBody() { return $this->body; } } class WCOAuth { /** * OAuth signature method algorithm. */ const HASH_ALGORITHM = 'SHA256'; /** * API endpoint URL. * * @var string */ protected $url; /** * Consumer key. * * @var string */ protected $consumerKey; /** * Consumer secret. * * @var string */ protected $consumerSecret; /** * API version. * * @var string */ protected $apiVersion; /** * WCRequest method. * * @var string */ protected $method; /** * WCRequest parameters. * * @var array */ protected $parameters; /** * Timestamp. * * @var string */ protected $timestamp; /** * Initialize oAuth class. * * @param string $url Store URL. * @param string $consumerKey Consumer key. * @param string $consumerSecret Consumer Secret. * @param string $method WCRequest method. * @param string $apiVersion API version. * @param array $parameters WCRequest parameters. * @param string $timestamp Timestamp. */ public function __construct( $url, $consumerKey, $consumerSecret, $apiVersion, $method, $parameters = [], $timestamp = '' ) { $this->url = $url; $this->consumerKey = $consumerKey; $this->consumerSecret = $consumerSecret; $this->apiVersion = $apiVersion; $this->method = $method; $this->parameters = $parameters; $this->timestamp = $timestamp; } /** * Encode according to RFC 3986. * * @param string|array $value Value to be normalized. * * @return string */ //TODO Rückgbabetyp prüfen protected function encode($value) { if (is_array($value)) { return array_map([$this, 'encode'], $value); } else { return str_replace(['+', '%7E'], [' ', '~'], rawurlencode($value)); } } /** * Normalize parameters. * * @param array $parameters Parameters to normalize. * * @return array */ protected function normalizeParameters($parameters) { $normalized = []; foreach ($parameters as $key => $value) { // Percent symbols (%) must be double-encoded. $key = $this->encode($key); $value = $this->encode($value); $normalized[$key] = $value; } return $normalized; } /** * Process filters. * * @param array $parameters WCRequest parameters. * * @return array */ protected function processFilters($parameters) { if (isset($parameters['filter'])) { $filters = $parameters['filter']; unset($parameters['filter']); foreach ($filters as $filter => $value) { $parameters['filter[' . $filter . ']'] = $value; } } return $parameters; } /** * Get secret. * * @return string */ protected function getSecret() { $secret = $this->consumerSecret; // Fix secret for v3 or later. if (!\in_array($this->apiVersion, ['v1', 'v2'])) { $secret .= '&'; } return $secret; } /** * Generate oAuth1.0 signature. * * @param array $parameters WCRequest parameters including oauth. * * @return string */ protected function generateOauthSignature($parameters) { $baseRequestUri = rawurlencode($this->url); // Extract filters. $parameters = $this->processFilters($parameters); // Normalize parameter key/values and sort them. $parameters = $this->normalizeParameters($parameters); uksort($parameters, 'strcmp'); // Set query string. $queryString = implode('%26', $this->joinWithEqualsSign($parameters)); // Join with ampersand. $stringToSign = $this->method . '&' . $baseRequestUri . '&' . $queryString; $secret = $this->getSecret(); return base64_encode(hash_hmac(self::HASH_ALGORITHM, $stringToSign, $secret, true)); } /** * Creates an array of urlencoded strings out of each array key/value pairs. * * @param array $params Array of parameters to convert. * @param array $queryParams Array to extend. * @param string $key Optional Array key to append * @return string Array of urlencoded strings */ protected function joinWithEqualsSign($params, $queryParams = [], $key = '') { foreach ($params as $paramKey => $paramValue) { if ($key) { $paramKey = $key . '%5B' . $paramKey . '%5D'; // Handle multi-dimensional array. } if (is_array($paramValue)) { //TODO Typ prüfen $queryParams = $this->joinWithEqualsSign($paramValue, $queryParams, $paramKey); } else { $string = $paramKey . '=' . $paramValue; // Join with equals sign. $queryParams[] = $this->encode($string); } } return $queryParams; } /** * Sort parameters. * * @param array $parameters Parameters to sort in byte-order. * * @return array */ protected function getSortedParameters($parameters) { uksort($parameters, 'strcmp'); foreach ($parameters as $key => $value) { if (is_array($value)) { uksort($parameters[$key], 'strcmp'); } } return $parameters; } /** * Get oAuth1.0 parameters. * * @return string */ public function getParameters() { $parameters = \array_merge($this->parameters, [ 'oauth_consumer_key' => $this->consumerKey, 'oauth_timestamp' => $this->timestamp, 'oauth_nonce' => \sha1(\microtime()), 'oauth_signature_method' => 'HMAC-' . self::HASH_ALGORITHM, ]); // The parameters above must be included in the signature generation. $parameters['oauth_signature'] = $this->generateOauthSignature($parameters); //TODO Typ prüfen return $this->getSortedParameters($parameters); } } class WCHttpClientException extends \Exception { /** * WCRequest. * * @var WCRequest */ private $request; /** * WCResponse. * * @var WCResponse */ private $response; /** * Initialize exception. * * @param string $message Error message. * @param int $code Error code. * @param WCRequest $request Request data. * @param WCResponse $response Response data. */ public function __construct($message, $code, WCRequest $request, WCResponse $response) { parent::__construct($message, $code); $this->request = $request; $this->response = $response; } /** * Get request data. * * @return WCRequest */ public function getRequest() { return $this->request; } /** * Get response data. * * @return WCResponse */ public function getResponse() { return $this->response; } } class WCHttpClient { /** * cURL handle. * * @var resource */ protected $ch; /** * Store API URL. * * @var string */ protected $url; /** * Consumer key. * * @var string */ protected $consumerKey; /** * Consumer secret. * * @var string */ protected $consumerSecret; /** * WCClient options. * * @var WCOptions */ protected $options; /** * WCRequest. * * @var WCRequest */ private $request; /** * WCResponse. * * @var WCResponse */ private $response; /** * WCResponse headers. * * @var string */ private $responseHeaders; /** * Initialize HTTP client. * * @param string $url Store URL. * @param string $consumerKey Consumer key. * @param string $consumerSecret Consumer Secret. * @param array $options WCClient options. * * @throws WCHttpClientException */ public function __construct($url, $consumerKey, $consumerSecret, $options) { if (!function_exists('curl_version')) { throw new WCHttpClientException('cURL is NOT installed on this server', -1, new WCRequest(), new WCResponse()); } $this->options = new WCOptions($options); $this->url = $this->buildApiUrl($url); $this->consumerKey = $consumerKey; $this->consumerSecret = $consumerSecret; } /** * Check if is under SSL. * * @return bool */ protected function isSsl() { return strpos($this->url,'https://')===0; } /** * Build API URL. * * @param string $url Store URL. * * @return string */ protected function buildApiUrl($url) { $api = $this->options->isWPAPI() ? $this->options->apiPrefix() : '/wc-api/'; return rtrim($url, '/') . $api . $this->options->getVersion() . '/'; } /** * Build URL. * * @param string $url URL. * @param array $parameters Query string parameters. * * @return string */ protected function buildUrlQuery($url, $parameters = []) { if (!empty($parameters)) { $url .= '?' . http_build_query($parameters); } return $url; } /** * Authenticate. * * @param string $url WCRequest URL. * @param string $method WCRequest method. * @param array $parameters WCRequest parameters. * * @return array */ protected function authenticate($url, $method, $parameters = []) { // Setup authentication. if ($this->isSsl()) { $basicAuth = new WCBasicAuth( $this->ch, $this->consumerKey, $this->consumerSecret, $this->options->isQueryStringAuth(), $parameters ); $parameters = $basicAuth->getParameters(); } else { $oAuth = new WCOAuth( $url, $this->consumerKey, $this->consumerSecret, $this->options->getVersion(), $method, $parameters, $this->options->oauthTimestamp() ); //TODO Typ prüfen $parameters = $oAuth->getParameters(); } return $parameters; } /** * Setup method. * * @param string $method WCRequest method. */ protected function setupMethod($method) { if ('POST' === $method) { curl_setopt($this->ch, CURLOPT_POST, true); } elseif (in_array($method, ['PUT', 'DELETE', 'OPTIONS'])) { curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method); } } /** * Get request headers. * * @param bool $sendData If request send data or not. * * @return array */ protected function getRequestHeaders($sendData = false) { $headers = [ 'Accept' => 'application/json', 'User-Agent' => $this->options->userAgent() . '/' . WCClient::VERSION, ]; if ($sendData) { $headers['Content-Type'] = 'application/json;charset=utf-8'; } return $headers; } /** * Create request. * * @param string $endpoint WCRequest endpoint. * @param string $method WCRequest method. * @param array $data WCRequest data. * @param array $parameters WCRequest parameters. * * @return WCRequest */ protected function createRequest($endpoint, $method, $data = [], $parameters = []) { $body = ''; $url = $this->url . $endpoint; $hasData = !empty($data); // Setup authentication. $parameters = $this->authenticate($url, $method, $parameters); // Setup method. $this->setupMethod($method); // Include post fields. if ($hasData) { $body = json_encode($data); curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); } $this->request = new WCRequest( $this->buildUrlQuery($url, $parameters), $method, $parameters, $this->getRequestHeaders($hasData), $body ); return $this->getRequest(); } /** * Get response headers. * * @return array */ protected function getResponseHeaders() { $headers = []; $lines = explode("\n", $this->responseHeaders); $lines = array_filter($lines, 'trim'); foreach ($lines as $index => $line) { // Remove HTTP/xxx params. if (strpos($line, ': ') === false) { continue; } list($key, $value) = explode(': ', $line); $headers[$key] = isset($headers[$key]) ? $headers[$key] . ', ' . trim($value) : trim($value); } return $headers; } /** * Create response. * * @return WCResponse */ protected function createResponse() { // Set response headers. $this->responseHeaders = ''; curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($_, $headers) { $this->responseHeaders .= $headers; return strlen($headers); }); // Get response data. $body = curl_exec($this->ch); $code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE); $headers = $this->getResponseHeaders(); // Register response. $this->response = new WCResponse($code, $headers, $body); return $this->getResponse(); } /** * Set default cURL settings. */ protected function setDefaultCurlSettings() { $verifySsl = $this->options->verifySsl(); $timeout = $this->options->getTimeout(); $followRedirects = $this->options->getFollowRedirects(); curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, $verifySsl); if (!$verifySsl) { curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, $verifySsl); } if ($followRedirects) { curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true); } curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $timeout); curl_setopt($this->ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->request->getRawHeaders()); curl_setopt($this->ch, CURLOPT_URL, $this->request->getUrl()); } /** * Look for errors in the request. * * @throws WCHttpClientException * * @param array $parsedResponse Parsed body response. */ protected function lookForErrors($parsedResponse) { // Any non-200/201/202 response code indicates an error. if (!in_array($this->response->getCode(), ['200', '201', '202'])) { $errors = isset($parsedResponse->errors) ? $parsedResponse->errors : $parsedResponse; $errorMessage = ''; $errorCode = ''; if (is_array($errors)) { $errorMessage = $errors[0]->message; $errorCode = $errors[0]->code; } elseif (isset($errors->message, $errors->code)) { $errorMessage = $errors->message; $errorCode = $errors->code; } throw new WCHttpClientException( sprintf('Error: %s [%s]', $errorMessage, $errorCode), $this->response->getCode(), $this->request, $this->response ); } } /** * Process response. * * @throws WCHttpClientException * @return array */ protected function processResponse() { $body = $this->response->getBody(); // Look for UTF-8 BOM and remove. if (0 === strpos(bin2hex(substr($body, 0, 4)), 'efbbbf')) { $body = substr($body, 3); } $parsedResponse = json_decode($body); // Test if return a valid JSON. if (JSON_ERROR_NONE !== json_last_error()) { $message = function_exists('json_last_error_msg') ? json_last_error_msg() : 'Invalid JSON returned'; throw new WCHttpClientException( sprintf('JSON ERROR: %s', $message), $this->response->getCode(), $this->request, $this->response ); } $this->lookForErrors($parsedResponse); return $parsedResponse; } /** * Make requests. * * @param string $endpoint WCRequest endpoint. * @param string $method WCRequest method. * @param array $data WCRequest data. * @param array $parameters WCRequest parameters. * * @throws WCHttpClientException * * @return array */ public function request($endpoint, $method, $data = [], $parameters = []) { // Initialize cURL. $this->ch = curl_init(); // Set request args. $request = $this->createRequest($endpoint, $method, $data, $parameters); // Default cURL settings. $this->setDefaultCurlSettings(); // Get response. $response = $this->createResponse(); // Check for cURL errors. if (curl_errno($this->ch)) { throw new WCHttpClientException('cURL Error: ' . \curl_error($this->ch), 0, $request, $response); } curl_close($this->ch); return $this->processResponse(); } /** * Get request data. * * @return WCRequest */ public function getRequest() { return $this->request; } /** * Get response data. * * @return WCResponse */ public function getResponse() { return $this->response; } } class WCBasicAuth { /** * cURL handle. * * @var resource */ protected $ch; /** * Consumer key. * * @var string */ protected $consumerKey; /** * Consumer secret. * * @var string */ protected $consumerSecret; /** * Do query string auth. * * @var bool */ protected $doQueryString; /** * WCRequest parameters. * * @var array */ protected $parameters; /** * Initialize Basic Authentication class. * * @param resource $ch cURL handle. * @param string $consumerKey Consumer key. * @param string $consumerSecret Consumer Secret. * @param bool $doQueryString Do or not query string auth. * @param array $parameters WCRequest parameters. */ public function __construct($ch, $consumerKey, $consumerSecret, $doQueryString, $parameters = []) { $this->ch = $ch; $this->consumerKey = $consumerKey; $this->consumerSecret = $consumerSecret; $this->doQueryString = $doQueryString; $this->parameters = $parameters; $this->processAuth(); } /** * Process auth. */ protected function processAuth() { if ($this->doQueryString) { $this->parameters['consumer_key'] = $this->consumerKey; $this->parameters['consumer_secret'] = $this->consumerSecret; } else { \curl_setopt($this->ch, CURLOPT_USERPWD, $this->consumerKey . ':' . $this->consumerSecret); } } /** * Get parameters. * * @return array */ public function getParameters() { return $this->parameters; } }