replacements = $replacements; } /** * Search and replace variables in string. * * Replaces all placeholders in upper or lower case within single or double curly brackets * @param string $message * * E.G.: Would convert "hello {customer}." to "hello awesome people." * if array to __construct was like ['customer' => 'awesome people'] * * @return string */ public function handle($message) { if (!is_string($message)) { $type = gettype($message); throw new RuntimeException(sprintf( 'Expected message as string, got %s.', $type )); } if (empty($this->replacements)) { return $message; } foreach ($this->replacements as $key => $value) { $value = (string) $value; // key with one bracket $key = '{' . trim($key, '{}') . '}'; $message = str_replace([$key, strtolower($key)], [$value, $value], $message); // key with two brackets $key = '{' . $key . '}'; $message = str_replace([$key, strtolower($key)], [$value, $value], $message); } return $message; } } /** * This abstract class is designed to simplify a new 'delivery tool' * * Class AbstractVersandart */ abstract class AbstractVersandartParcelone extends Versanddienstleister { public $einstellungen = []; public $export_drucker; public $paketmarke_drucker; /** * Copied from other 'versandart modules', added * the RuntimeException for json decode. * * AbstractVersandart constructor. * * @param app_t $app * @param int $id */ final public function __construct($app, $id) { $this->id = $id; $this->app = &$app; $settings = $this->app->DB->Select("SELECT einstellungen_json FROM versandarten WHERE id = '$id' LIMIT 1"); $this->paketmarke_drucker = $this->app->DB->Select("SELECT paketmarke_drucker FROM versandarten WHERE id = '$id' LIMIT 1"); $this->export_drucker = $this->app->DB->Select("SELECT export_drucker FROM versandarten WHERE id = '$id' LIMIT 1"); if ($settings) { $settings = json_decode($settings, true); if (json_last_error()) { throw new RuntimeException(sprintf( 'JSON decode failed in %s with %s.', get_class($this), json_last_error_msg() )); } } else { $settings = []; } $this->einstellungen = $settings; $this->ctrHook(); } /** * Just a hook run if the ctr had his job done. * * @return void */ abstract protected function ctrHook(); /** * * Called via 'versandzentrum' * Thanks to 'sevensenders.php' * * @param int|string $id * @param $sid * * @return array */ public function PaketmarkeDrucken($id, $sid) { $adressdaten = $this->GetAdressdaten($id, $sid); $ret = $this->Paketmarke($sid, $id, '', false, $adressdaten); if ($sid !== 'lieferschein') { return $ret; } $deliveryNoteArr = $this->app->DB->SelectRow("SELECT adresse, versandart, projekt FROM lieferschein WHERE id = '$id' LIMIT 1"); $adresse = $deliveryNoteArr['adresse']; $project = $deliveryNoteArr['projekt']; $versandart = $deliveryNoteArr['versandart']; $addressValidation = 2; if ($ret) { $addressValidation = 1; } $tracking = null; // $tracking = $this->tracking; // $this->tracking is not set in 'sevensenders.php' if (isset($adressdaten['tracking'])) { $tracking = $adressdaten['tracking']; } $this->app->DB->Insert("INSERT INTO versand (versandunternehmen, tracking, versendet_am,abgeschlossen,lieferschein,freigegeben,firma,adresse,projekt,paketmarkegedruckt,adressvalidation) VALUES ('$versandart','$tracking',NOW(),1,'$id',1,'1','$adresse','$project',1,'$addressValidation') "); if ($addressValidation === 1) { $this->app->erp->LieferscheinProtokoll($id, 'Paketmarke automatisch gedruckt'); } elseif ($addressValidation === 2) { $this->app->erp->LieferscheinProtokoll($id, 'automatisches Paketmarke Drucken fehlgeschlagen'); } return $ret; } /** * This method is the 'main' part of all delivery tools. * * Available via Lager -> Lieferschein * index.php?module=lieferschein&action=paketmarke&id={id} * or Lager -> Versandzentrum * index.php?module=versanderzeugen&action=frankieren&id={id} * or Lager -> Retoure * index.php?module=retoure&action=paketmarke&id={id} * * called via erpapi->Paketmarke($parsetarget,$sid="",$zusatz="",$typ="DHL") as: * $error = $obj->Paketmarke($sid!=''?$sid:'lieferschein',($sid=='versand'?$id:$tid), $parsetarget, $error); * * Reads the address data and package data via '$this->app->Secure->GetPOST' or from '$adressdaten' array. * * @param string $doctyp 'lieferschein' / 'versand' / 'retoure' * @param string|int $id '1' * @param string $target '#TAB1' * @param bool $error * @param null|array $adressdaten * * @return array */ final public function Paketmarke($doctyp, $id, $target = '', $error = false, &$adressdaten = null) { if (is_string($id) && is_numeric($id)) { $id = (int)$id; } if (!is_int($id)) { $type = gettype($id); throw new ArgumentTypeException('Expected id as integer, got ' . $type); } if (!is_string($doctyp)) { $type = gettype($doctyp); throw new ArgumentTypeException('Expected doctyp as string, got ' . $type); } $allowedTypes = ['lieferschein', 'versand', 'retoure']; if ($adressdaten === null) { $doctyp = $this->getModuleName($doctyp, $allowedTypes); } if (!in_array($doctyp, $allowedTypes, true)) { throw new RuntimeException('Only \'Lieferschein\' is supported, got ' . $doctyp); } $this->validateSettings(); if (is_array($adressdaten) && !empty($adressdaten)) { $address = [ 'name' => $adressdaten['name'], 'name2' => $adressdaten['name2'], 'name3' => $adressdaten['name3'], 'street' => $adressdaten['strassekomplett'], 'street_no' => $adressdaten['hausnummer'], 'plz' => $adressdaten['plz'], 'ort' => $adressdaten['ort'], 'email' => $adressdaten['email'], 'phone' => $adressdaten['phone'], 'land' => $adressdaten['land'], // bundesstaat ]; // $anzahl = (int)isset($adressdaten["anzahl"])?$adressdaten["anzahl"]:0; // $nummeraufbeleg = "";//$this->app->Secure->GetPOST("nummeraufbeleg"); // if ($anzahl <= 0 || !is_int($anzahl)) $anzahl = 1; // $laenge = isset($adressdaten["laenge"])?$adressdaten["laenge"]:''; // $breite = isset($adressdaten["breite"])?$adressdaten["breite"]:''; // $hoehe = isset($adressdaten["hoehe"])?$adressdaten["hoehe"]:''; $cash_on_delivery = isset($adressdaten['Nachnahme']) ? $adressdaten['Nachnahme'] : 0; $packageData = [ 'kg1' => $adressdaten['standardkg'], 'drucken' => '1', 'anders' => '', 'tracking_again' => '', 'module' => $doctyp, 'versandmit' => '', // $this->app->Secure->GetPOST('versandmit'), 'trackingsubmit' => '', // $this->app->Secure->GetPOST('trackingsubmit'), 'versandmitbutton' => '', // $this->app->Secure->GetPOST('versandmitbutton'), 'tracking' => '', // $this->app->Secure->GetPOST('tracking'), 'trackingsubmitcancel' => '', // $this->app->Secure->GetPOST('trackingsubmitcancel'), 'retourenlabel' => '', // $this->app->Secure->GetPOST('retourenlabel'), 'nachnahme' => (int)$cash_on_delivery, 'product' => '', ]; } else { $address = [ 'name' => $this->app->Secure->GetPOST('name'), 'name2' => $this->app->Secure->GetPOST('name2'), 'name3' => $this->app->Secure->GetPOST('name3'), 'street' => $this->app->Secure->GetPOST('strasse'), 'street_no' => $this->app->Secure->GetPOST('hausnummer'), 'plz' => $this->app->Secure->GetPOST('plz'), 'ort' => $this->app->Secure->GetPOST('ort'), 'email' => $this->app->Secure->GetPOST('email'), 'phone' => $this->app->Secure->GetPOST('phone'), 'land' => $this->app->Secure->GetPOST('land'), ]; $packageData = [ 'kg1' => $this->app->Secure->GetPOST('kg1'), 'drucken' => $this->app->Secure->GetPOST('drucken'), 'anders' => $this->app->Secure->GetPOST('anders'), 'tracking_again' => $this->app->Secure->GetGET('tracking_again'), 'module' => $this->app->Secure->GetGET('module'), 'versandmit' => $this->app->Secure->GetPOST('versandmit'), 'trackingsubmit' => $this->app->Secure->GetPOST('trackingsubmit'), 'versandmitbutton' => $this->app->Secure->GetPOST('versandmitbutton'), 'tracking' => $this->app->Secure->GetPOST('tracking'), 'trackingsubmitcancel' => $this->app->Secure->GetPOST('trackingsubmitcancel'), 'retourenlabel' => $this->app->Secure->GetPOST('retourenlabel'), // 'product' => $this->app->Secure->GetPOST('products'), 'nachnahme' => $this->app->Secure->GetPOST('nachnahme'), ]; } // $packageData['clients_reference'] = $this->app->Secure->GetPOST('clients_reference'); // $packageData['shipment_reference'] = $this->app->Secure->GetPOST('shipment_reference'); $packageData['service'] = $this->app->Secure->GetPOST('service'); /* * This is in 'sevensenders.php' only called, if $addressdata === null. But why? */ $this->setNachnahmeCheckbox($doctyp, $id); $ret = []; if (!empty($address)) { try { // throw new RuntimeException('Ooops - data missing'); $this->createPaketmarke($doctyp, $id, $target, $error, $address, $packageData); } catch (Exception $e) { $ret[] = $e->getMessage(); } } if ($target) { $this->parseTemplate($target); } return $ret; } /** * Get the module name. * * @param string $doctype * @param array $allowedTypes * * @return string */ private function getModuleName($doctype, $allowedTypes) { // // in sevensenders: // if($adressdaten === null){ // $module = $this->app->Secure->GetGET('module'); // }else{ // $module = $doctyp; // } $tmp = $this->app->Secure->GetGET('module'); if (is_array($tmp)) { $tmp = array_filter($tmp); $tmp = array_filter($tmp, 'is_string'); if (array_key_exists(0, $tmp)) { $tmp = $tmp[0]; } else { $tmp = ''; } } if (in_array($tmp, $allowedTypes, true)) { return (string)$tmp; } return $doctype; } /** * Check if all settings are set and not empty. * Uses $this->settingsStructure() to get all * required settings. Empty settings are not allowed! * Respects select options (drop-down menus). Uses * the shown labels in the exceptions. * * @throws RuntimeException */ protected function validateSettings() { $settings = $this->EinstellungenStruktur(); foreach ($settings as $key => $setting) { $name = rtrim($setting['bezeichnung'], ':'); if (!array_key_exists($key, $this->einstellungen)) { if (array_key_exists('optional', $setting) && $setting['optional'] === true) { continue; } if (array_key_exists('typ', $setting) && strtolower((string)$setting['typ']) === 'checkbox') { continue; } if (array_key_exists('default', $setting)) { $value = $setting['default']; // if default value is empty, don't run other validations. $this->einstellungen[$key] = $value; if (empty($value)) { continue; } } } if (!array_key_exists($key, $this->einstellungen)) { throw new RuntimeException(sprintf( 'Setting \'%s\' is missing.', $name )); } $value = $this->einstellungen[$key]; /* * Check 'size' argument for maximum length. */ if (array_key_exists('maxLength', $setting) && is_numeric($setting['maxLength'])) { $maxLength = (int) $setting['maxLength']; if ($maxLength > 0 && strlen($value) > $maxLength) { throw new RuntimeException(sprintf( 'Setting \'%s\' raised it\'s maximum length of %s.', $name, $maxLength )); } } /* * Respect select fields. */ if (array_key_exists('optionen', $setting) && $setting['type'] === 'select') { $options = $setting['optionen']; $keys = array_keys($options); if (!in_array($value, $keys, true)) { $values = array_values($options); $options = implode(', ', $values); throw new RuntimeException(sprintf( 'Setting \'%s\' is not allowed, allowed values are: %s.', $name, $options )); } continue; } if (array_key_exists('regex', $setting) && is_string($setting['regex'])) { $pattern = $setting['regex']; $pattern = trim($pattern, '/'); $pattern = ltrim($pattern, '^'); $pattern = rtrim($pattern, '$'); $pattern = '/^' . $pattern . '$/'; $match = preg_match($pattern, $value); if ($match === false) { throw new RuntimeException(sprintf( 'Invalid regex pattern for \'%s\'.', $name )); } if ($match !== 1) { throw new RuntimeException(sprintf( 'Setting \'%s\' does not match it\'s regex pattern.', $name )); } } } } /** * Print the created file. * * @param string $filename * @param string $content * @param int $versandId * * @throws RuntimeException */ protected function printFile($filename, $content, $versandId) { $printer = $this->getPrinter(); if (!$printer) { throw new RuntimeException('No printer configured.'); } if (!is_string($filename)) { $type = gettype($filename); throw new RuntimeException(sprintf( 'Expected filename as string, got %s.', $type )); } if (empty($filename)) { throw new RuntimeException( 'Empty filename is not supported.' ); } if (!is_string($content)) { $type = gettype($content); throw new RuntimeException(sprintf( 'Expected content as string, got %s.', $type )); } if (empty($content)) { throw new RuntimeException(sprintf( 'Empty content for document %s is not supported.', $filename )); } $tmpPath = $this->app->erp->GetTMP(); $full = $tmpPath . $filename; if (!file_put_contents($full, $content)) { throw new RuntimeException(sprintf( 'Could not write file \'%s\'.', $filename )); } $spoolerId = $this->app->printer->Drucken($printer, $full); unlink($full); if($versandId && $spoolerId) { $this->app->DB->Update( sprintf( 'UPDATE versand SET lastspooler_id = %d, lastprinter = %d WHERE id = %d', $spoolerId, $printer, $versandId ) ); } } /** * Return the html structure displayed in the * 'versandarten' module to add some extra * input fields for api keys etc. * * Displayed via class 'Versanddienstleister' located * in ../class.versanddienstleister.php * * @return array */ abstract protected function EinstellungenStruktur(); /** * Set the 'nachnahme' checkbox field. * * Thanks to 'sevensenders.php' * * @param string $doctyp * @param int|string $id */ private function setNachnahmeCheckbox($doctyp, $id) { if ($doctyp === 'lieferschein') { $lieferschein = $id; } elseif ($doctyp === 'retoure') { $lieferschein = $this->app->DB->Select("SELECT lieferschein FROM retoure WHERE id='$id' LIMIT 1"); } else { $lieferschein = $this->app->DB->Select("SELECT lieferschein FROM versand WHERE id='$id' LIMIT 1"); if ($lieferschein <= 0) { $lieferschein = $id; } } $rechnung = $this->app->DB->Select("SELECT id FROM rechnung WHERE lieferschein='$lieferschein' LIMIT 1"); $zahlungsweise = $this->app->DB->Select("SELECT zahlungsweise FROM rechnung WHERE id='$rechnung' LIMIT 1"); if ($zahlungsweise === 'nachnahme') { $this->app->Tpl->Set('NACHNAHME', 'checked="checked"'); } } /** * Create the package labels. * * Called via Paketmarke or PaketmarkeDrucken * * @param string $doctyp * @param string $id * @param string $target * @param bool $error * @param array $adressdaten * @param array $packageData * * @return array list of error messages */ abstract protected function createPaketmarke($doctyp, $id, $target, $error, $adressdaten, $packageData); /** * Parse the template. * * @param string $target * * @return void */ abstract protected function parseTemplate($target); /** * May change the tracking id before inserting into the db. * * @param string $tracking * * @return string */ public function TrackingReplace($tracking) { return $tracking; } /** * Extract the child's name and create * a default module name via 'ucfirst'. * * E.G. converts child's class name * 'Versandart_parcelone' to 'Parcelone'. * * @return string */ public function GetBezeichnung() { $c = get_class($this); $c = explode('_', $c); $c = array_filter($c); $c = array_pop($c); $c = ucfirst($c); return $c; } /** * Load the data given by the current document type and it's id. * * @param string $documentTyp on of 'lieferschein', 'versand' or 'retoure' * @param int $id * * @throws RuntimeException * * @return array */ protected function getDocumentByID($documentTyp, $id) { /* * var $documentTyp may be: * - 'lieferschein' * - 'versand' * - 'retoure' * * - 'auftrag' */ $documentTyp = $this->app->DB->real_escape_string($documentTyp); $sql = sprintf('SELECT * FROM %s WHERE id = %s LIMIT 1', $documentTyp, $id); $data = $this->app->DB->SelectArr($sql); $error = $this->app->DB->error(); if ($error) { $error = htmlspecialchars($error); $msg = sprintf('SQL SelectArr error: \'%s\', query was \'%s\'', $error, $sql); throw new RuntimeException($msg); } if (!is_array($data) || !array_key_exists(0, $data) || !is_array($data[0]) || !$data[0]) { throw new RuntimeException(sprintf( 'No data for document %s with id %s found.', $documentTyp, $id )); } $data = $data[0]; return $data; } /** * Return the 'standard Paketmarkendrucker'. * * Checks GetPOST: drucken * Checks GetGET: tracking_again * * @return int */ protected function getPrinter() { if (is_numeric($this->paketmarke_drucker) && $this->paketmarke_drucker) { return $this->paketmarke_drucker; } $printer = (int) $this->app->erp->GetStandardPaketmarkendrucker(); if ($printer) { return $printer; } return $this->export_drucker; } /** * Get the current user name as escaped string. * * @return string */ protected function getUserName() { $user = $this->app->User->GetName(); $user = $this->app->DB->real_escape_string($user); return $user; } /** * Load an address via it's id. * * @param string|int $id * @param string|array $fields Filter these columns, default all * * @throws RuntimeException * * @return array */ protected function loadAddress($id, $fields = '*') { if (is_string($id) && is_numeric($id)) { $id = (int)$id; } if (!is_int($id)) { $type = gettype($id); throw new ArgumentTypeException('Expected addressID as int, got ' . $type); } $fields = (array)$fields; $fields = array_values($fields); $fields = array_filter($fields); $fields = array_filter($fields, 'is_string'); if (in_array('*', $fields, true)) { $fields = '*'; } else { $tmp = []; foreach ($fields as $field) { $tmp[] = $this->app->DB->real_escape_string($field); } $fields = implode(', ', $tmp); } $sql = sprintf('SELECT %s FROM adresse WHERE id =\'%s\'', $fields, $id); $query = $this->app->DB->Query($sql); $address = $query->fetch_array(MYSQLI_ASSOC); $error = $this->app->DB->error(); if ($error) { $error = htmlspecialchars($error); $msg = sprintf('SQL query error: \'%s\', query was \'%s\'', $error, $sql); throw new RuntimeException($msg); } if (!is_array($address)) { $type = gettype($address); throw new ArgumentTypeException('Expected address as array, got ' . $type); } return $address; } /** * Get the package weight from user input. * * @param array $packageData * * @throws RuntimeException * * @return float */ protected function getWeight($packageData) { if (!is_array($packageData)) { $type = gettype($packageData); throw new ArgumentTypeException('Expected package data as array, got ' . $type); } if (!array_key_exists('kg1', $packageData)) { throw new RuntimeException('Weight (kg1) is missing.'); } $weight = $packageData['kg1']; if (empty($weight) && $weight !== '0') { // wtf?! why is '0' string empty? throw new RuntimeException('The package weight is required.'); } if (is_string($weight)) { $weight = str_replace(',', '.', $weight); if (is_numeric($weight)) { $weight = (float)$weight; } } if (!is_float($weight)) { $type = gettype($weight); throw new ArgumentTypeException('Expected weight as float, got ' . $type); } if ($weight < 0) { throw new RuntimeException('A negative package weight is not supported.'); } return $weight; } /** * Build an html select element. * * @param string $nameID Name and ID of select element. * @param array $select associative array * @param string|int|null|array $selected * * @return string */ protected function buildSelectForm($nameID, $select, $selected = null) { $options = []; $nameID = htmlspecialchars($nameID); $options[] = sprintf(''; return implode('', $options); } } class ArgumentTypeException extends RuntimeException implements ParcelOneExceptionInterface { } class ResponseException extends RuntimeException implements ParcelOneExceptionInterface { } class NotImplementedException extends RuntimeException implements ParcelOneExceptionInterface { } class EmptyResponseException extends ResponseException implements ParcelOneExceptionInterface { } class MissingArgumentException extends RuntimeException implements ParcelOneExceptionInterface { } class MissingResponseFieldException extends ResponseException implements ParcelOneExceptionInterface { /** * MissingResponseFieldException constructor. * * Change the exception message -> wrap it, so its enough to pass only the missed argument name. * * @param string $message * @param int $code * @param Throwable|null $previous * * @return static */ final public static function fromFieldName($message, $code = 0, Throwable $previous = null) { if (!$message || !is_string($message)) { $message = 'A field is missing.'; } $message = sprintf('The field %s is missing.', $message); return new static($message, $code, $previous); } } class SoapExtensionMissingException extends RuntimeException implements ParcelOneExceptionInterface { } if (!class_exists('SoapHeader')) { /** * This is just a fix, if php comes without the soap extension. * * So the following header classes can extend this class without raising an unhandled exception just by loading this * file. A check if the soap extension exists is located in Versandart_parcelone::EinstellungenStruktur() so the * user cannot setup this delivery service. In addition, a check is located in * AbstractParcelOneRequest::__construct() so it's not possible to execute a soap request if the extension is * missing. * * Class SoapHeader */ class SoapHeader { public function __construct($namespace, $name, $data = null, $mustunderstand = false, $actor = '') { } } } /** * Define some constants if the soap extension is not available so at least the class instantiation runs as expected. * * @url: https://www.php.net/manual/en/soap.constants.php */ defined('SOAP_1_1') or define('SOAP_1_1', 1); defined('WSDL_CACHE_NONE') or define('WSDL_CACHE_NONE', 0); defined('SOAP_COMPRESSION_GZIP') or define('SOAP_COMPRESSION_GZIP', 0); defined('SOAP_COMPRESSION_ACCEPT') or define('SOAP_COMPRESSION_ACCEPT', 32); /** * Headers from documentation package. * * classes: * - AuthHeader * - APIKeyHeader * - CultureHeader * * Thanks to Jörk Sternsdorff from * Awiwe Solutions GmbH */ class AuthHeader extends SoapHeader { private $wss_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'; private $wsu_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'; /** * AuthHeader constructor. * * @param string $user * @param string $pass */ public function __construct($user, $pass) { $created = gmdate('Y-m-d\TH:i:s\Z'); $nonce = mt_rand(); $passdigest = base64_encode(pack('H*', sha1(pack('H*', $nonce) . pack('a*', $created) . pack('a*', $pass)))); $auth = new stdClass(); $auth->Username = new SoapVar($user, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns); $auth->Password = new SoapVar($pass, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns); $auth->Nonce = new SoapVar($passdigest, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns); $auth->Created = new SoapVar($created, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wsu_ns); $username_token = new stdClass(); $username_token->UsernameToken = new SoapVar($auth, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns); $security_sv = new SoapVar( new SoapVar($username_token, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns), SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'Security', $this->wss_ns); parent::__construct($this->wss_ns, 'Security', $security_sv, true); } } class APIKeyHeader extends SoapHeader { public function __construct($apiKey) { parent::__construct('apikey', 'apikey', $apiKey, false); } } class CultureHeader extends SoapHeader { public function __construct($culture) { parent::__construct('culture', 'culture', $culture, false); } } /** * Class AbstractParcelOneRequest * * This class holds only some functionality to * build up the soap request. It supports some * methods to set required header. * Nothing more. * * @package ParcelOne */ abstract class AbstractParcelOneRequest { /* * Support only PA1 as carrier. */ const DEFAULT_CARRIER = 'PA1'; /** * this Key is used to identify this software and not the customer. */ const API_KEY = '0641C551-D23A-43BB-87A9-626DAE7FFE00'; /** * The default software attribute is injected into every request. * It's possible to 'override' this constant in child classes. */ const SOFTWARE = 'Xentral_ERP_Software'; const SERVICE = 'https://productionapi.awiwe.solutions/version4/shippingwcf/ShippingWCF.svc?wsdl'; const SANDBOX_SERVICE = 'https://sandboxapi.awiwe.solutions/version4/shippingwcfsandbox/shippingWCF.svc?wsdl'; const ENDPOINT = 'https://productionapi.awiwe.solutions/version4/shippingwcf/ShippingWCF.svc/Shippingwcf'; const SANDBOX_ENDPOINT = 'https://sandboxapi.awiwe.solutions/version4/shippingwcfsandbox/shippingWCF.svc/ShippingWCF'; /** * @var AbstractParcelOneRequest|ParcelOneRequest */ static private $instance = null; /** * The Mandator ID * * ID at PARCEL.ONE, usually '1', if only one mandator. * * @var int 1 */ protected $mandator = 1; /** * The Consigner ID * * ID at PARCEL.ONE, usually "1", if only one consigner. * * @var int */ protected $consigner = 1; /** * The carrier selected as second step in the settings. * * @var string default PA1 for Parcel.One */ protected $carrier = 'PA1'; /** * @var string The product chosen in the settings. */ protected $product = ''; /** * Options for the SoapClient constructor. * * Note: this property is private. Also child classes have to use * the setOption / deleteOption methods. * * @var array */ private $options = [ 'soap_version' => SOAP_1_1, 'exceptions' => true, 'trace' => false, 'cache_wsdl' => WSDL_CACHE_NONE, 'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP, 'location' => 'https://shippingwcf.awiwe.net/Shippingwcf.svc/Shippingwcf', 'connection_timeout' => 300 ]; /** * @var array of RequestHeaderInterface */ private $soapHeaders = []; /** * @var string */ private $url; /** * Cache here the created soap client. * * @var SoapClient */ private $client = null; /** * ParcelOneRequest constructor. * * @param bool $production * @param int $mandator * @param int $consigner */ final private function __construct($production = false, $mandator = 1, $consigner = 1) { if (!class_exists('SoapClient')) { throw new SoapExtensionMissingException('SOAP support is not configured.'); } $this->mandator = $mandator; $this->consigner = $consigner; $this->url = self::SANDBOX_SERVICE; $this->options['location'] = self::SANDBOX_ENDPOINT; if ($production) { $this->url = self::SERVICE; $this->options['location'] = self::ENDPOINT; } } /** * Get the singleton. * * @param array $settings * @return AbstractParcelOneRequest|ParcelOneRequest */ final public static function getInstance($settings = []) { if (self::$instance !== null) { if (empty(self::$instance->carrier) && array_key_exists('carrier', $settings)) { self::$instance->carrier = $settings['carrier']; } if (empty(self::$instance->product) && array_key_exists('product', $settings)) { self::$instance->product = $settings['product']; } return self::$instance; } $required = ['kdnr', 'password', 'sandbox']; foreach ($required as $requirenment) { if (!array_key_exists($requirenment, $settings)) { throw new MissingArgumentException(sprintf( 'Setting %s is missing.', $requirenment )); } } $required = array_flip($required); $settings = array_intersect_key($settings, $required); $production = true; if (array_key_exists('sandbox', $settings)) { $production = $settings['sandbox'] !== '1'; } if (!array_key_exists('mandator', $settings)) { $settings['mandator'] = 1; } $mandator = (int)$settings['mandator']; $mandator = max(1, $mandator); if (!array_key_exists('consigner', $settings)) { $settings['consigner'] = 1; } $consigner = (int)$settings['consigner']; $consigner = max(1, $consigner); if (!array_key_exists('country', $settings)) { $settings['country'] = 'de-DE'; } $instance = new static($production, $mandator, $consigner); $instance ->addSoapHeader(new AuthHeader($settings['kdnr'], $settings['password'])) ->addSoapHeader(new CultureHeader($settings['country'])) ->addSoapHeader(new APIKeyHeader(self::API_KEY)); self::$instance = $instance; return $instance; } /** * Don't allow 'on the fly' calls. * * Only the implemented SOAP methods are supported. * * @param string $name * @param array $arguments * * @throws NotImplementedException * * @return mixed */ final public function __call($name, $arguments) { $class = get_class($this); $message = sprintf( 'Method %s->%s is not implemented.', $class, $name ); throw new NotImplementedException($message); } /** * Make one call to the PARCEL.ONE API * * @param string $name * @param array $arguments * * @throws SoapFault * * @throws EmptyResponseException * @return stdClass|mixed */ final protected function call($name, $arguments = null) { $client = $this->getClient(); if (!is_string($name)) { $type = gettype($name); throw new InvalidArgumentException(sprintf( 'Expected method name as string, got %s.', $type )); } if ($arguments === null) { $arguments = []; } if (!is_array($arguments)) { $type = gettype($arguments); throw new InvalidArgumentException(sprintf( 'Expected %s\'s arguments as array, got %s.', $name, $type )); } $arguments['Software'] = self::SOFTWARE; /* * Do some magic ;) */ $callback = [$client, $name]; // $response = call_user_func($callback, $arguments); $response = $callback($arguments); if (empty($response)) { $message = sprintf('Method %s returned empty body.', $name); throw new EmptyResponseException($message); } return $response; } /** * Return the SOAP client. * * Set it up, if not available now. * * @throws SoapFault * * @return SoapClient */ final protected function getClient() { $client = $this->client; if ($client === null) { $client = new SoapClient($this->url, $this->options); $client->__setSoapHeaders($this->soapHeaders); $this->client = $client; } return $this->client; } /** * Convert the stdClass object into an array. * * This method uses recursive calls. * * Thanks to https://stackoverflow.com/a/18576919 * * @param stdClass|array $array * * @return array */ final protected function convert2array($array) { if (is_array($array)) { foreach ($array as $key => $value) { if (is_array($value)) { $array[$key] = $this->convert2array($value); } if ($value instanceof stdClass) { $array[$key] = $this->convert2array((array)$value); } } } if ($array instanceof stdClass) { return $this->convert2array((array)$array); } return $array; } /** * @param SoapHeader $header * * @return $this */ private function addSoapHeader(SoapHeader $header) { $this->client = null; $this->soapHeaders[] = $header; return $this; } } /** * Class ParcelOneRequest * * Implement here all API methods we support. */ class ParcelOneRequest extends AbstractParcelOneRequest { /** * Get available Products for a mandator and Carrier * * @param int|string $level : [0, 1, 2, ..] * - 0 = all levels returned; * - 1 = only 1 level returned (only products), * - >=2 = 2 levels returned (products and services info) * * @throws SoapFault * * @return array|stdClass */ public function getProducts($level = 1) { $level = is_numeric($level) ? $level : 1; $level = max(0, (int)$level); $arguments = [ 'Mandator' => $this->mandator, // string, as field in settings? 'level' => $level, // int 'CEP' => $this->carrier, // string Carrier abbreviation (UPS, PA1 for Parcel One, ...). ]; $response = $this->call(__FUNCTION__, $arguments); if (!isset($response->getProductsResult)) { throw MissingResponseFieldException::fromFieldName('getProductsResult'); } if (!isset($response->getProductsResult->Product)) { throw MissingResponseFieldException::fromFieldName('getProductsResult->Product'); } $response = $response->getProductsResult->Product; $response = $this->convert2array($response); $products = []; // get as first the default product foreach ($response as $product) { if (array_key_exists('Default', $product) && $product['Default']) { $id = $product['ProductID']; $name = $product['ProductName']; $products[$id] = $name; } } // then add all other products foreach ($response as $product) { if (array_key_exists('Default', $product) && !$product['Default']) { $id = $product['ProductID']; $name = $product['ProductName']; $products[$id] = $name; } } return $products; } /** * Register Forward and Return Shipments. * * @param $shippingData * * @throws SoapFault * * @return array */ public function registerShipments($shippingData) { $default = [ // MandatorID: required - Mandator ID at PARCEL.ONE, usually "1", if only one mandator. 'MandatorID' => $this->mandator, // ConsignerID: required - Consigner ID at PARCEL.ONE, usually "1", if only one consigner. 'ConsignerID' => $this->consigner, // CEPID: required - Carrier specification, possible values so far: UPS, PA1, DHL. 'CEPID' => $this->carrier, // Software: required - Software specification of client, if possible with version number. 'Software' => self::SOFTWARE, /* * PrintDocuments: required - Flag to indicate, * if Documents (for DHL also Export Documents) should be returned with this request * (0=no, 1=yes). */ 'PrintDocuments' => 1, // DocumentFormat: required - only Format.Type = "PDF" needed. 'DocumentFormat' => ['Type' => 'PDF'], // LabelFormat: required - only Format.Type = either "GIF" or "PDF" needed. 'LabelFormat' => ['Type' => 'PDF'], // PrintLabel: required - Flag to indicate, if Label should be returned with this request (0=no, 1=yes). 'PrintLabel' => 1, ]; $shippingData = array_merge($shippingData, $default); $shippingData = ['ShippingData' => [$shippingData]]; $response = $this->call(__FUNCTION__, $shippingData); if (!isset($response->registerShipmentsResult)) { throw MissingResponseFieldException::fromFieldName('registerShipmentsResult'); } $response = $this->convert2array($response); if (!isset($response['registerShipmentsResult'])) { throw new ResponseException('Missing registerShipmentsResult response field.'); } $response = $response['registerShipmentsResult']; if (!isset($response['ShipmentResult'])) { throw new ResponseException('Missing ShipmentResult response field.'); } $response = $response['ShipmentResult']; foreach ($response['ActionResult']['Errors'] as $e) { if (!is_array($e)) { continue; } if (!array_key_exists('Message', $e) || !$e['Message']) { continue; } $msg = $e['Message']; if (array_key_exists('StatusCode', $e) && $e['StatusCode']) { $msg .= ' (' . (string)$e['StatusCode'] . ')'; } throw new ResponseException($msg); } return $response; } /** * Get available Carriers for a mandator, optionally filtered by countries list. * * CEP[] getCEPs(string Mandator, int level, String[] Countries); * * @param int|string level: [0, 1, 2, ..] * 0 = all levels returned; * 1 = only 1 level returned (only products), * >=2 = 2 levels returned (products and services info) * * @throws SoapFault * * @return array|stdClass */ public function getCEPs($level = 1) { $arguments = [ 'Mandator' => $this->mandator, 'level' => max(1, (int) $level), ]; $response = $this->call(__FUNCTION__, $arguments); if (!isset($response->getCEPsResult)) { throw new ResponseException('Missing getCEPsResult field.'); } if (!isset($response->getCEPsResult->CEP)) { throw new ResponseException('Missing CEP field.'); } $response = $response->getCEPsResult->CEP; $response = $this->convert2array($response); return $response; } /** * @throws SoapFault */ public function getServices() { $arguments = [ 'Mandator' => $this->mandator, 'CEP' => $this->carrier, 'Product' => $this->product, ]; $response = $this->call(__FUNCTION__, $arguments); if (!isset($response->getServicesResult)) { throw MissingResponseFieldException::fromFieldName('getServicesResult'); } if (!isset($response->getServicesResult->Service)) { throw MissingResponseFieldException::fromFieldName('getServicesResult::Service.'); } $response = $response->getServicesResult->Service; $response = $this->convert2array($response); $services = [ '' => 'Kein Service' ]; // Search for the default service foreach ($response as $service) { if (array_key_exists('Default', $service) && $service['Default']) { $id = $service['ServiceID']; $name = $service['ServiceName']; $services[$id] = $name; } } // Append all other services foreach ($response as $service) { if (array_key_exists('Default', $service) && ! $service['Default']) { $id = $service['ServiceID']; $name = $service['ServiceName']; $services[$id] = $name; } } return $services; } // todo: implement here the necessary methods } /** * API from PARCEL.ONE * * Note: the class name is expected as: * * Versandart_{filename} * * Where {filename} is lowercase and without the '.php' extension. * * @url: https://parcel.one/en * @url: https://parcel.one/en/api */ class Versandart_parcelone extends AbstractVersandartParcelone { /** * @inheritDoc */ public function GetBezeichnung() { return 'PARCEL.ONE'; } protected function ctrHook() { $current = [ 'kdnr' => $this->app->Secure->GetPOST('kdnr'), 'password' => $this->app->Secure->GetPOST('password'), 'carrier_product' => $this->app->Secure->GetPOST('carrier_product'), ]; $current = array_filter($current); // $this->einstellungen += $current; $this->einstellungen = array_merge($this->einstellungen, $current); if (array_key_exists('carrier_product', $this->einstellungen)) { list($carrier, $product) = explode('.', $this->einstellungen['carrier_product']); $this->einstellungen['carrier'] = $carrier; $this->einstellungen['product'] = $product; } } /** * @inheritDoc */ protected function EinstellungenStruktur() { if (!class_exists('SoapClient')) { $message = 'PHP SOAP Extension ist nicht konfiguriert. Diese Versandart kann nicht genutzt werden.'; $this->app->Tpl->Add('MESSAGE', sprintf('