/classes/webservice/WebserviceRequest.php
PHP | 1089 lines | 723 code | 79 blank | 287 comment | 159 complexity | e149f0c555ca717d852536ed267df3e7 MD5 | raw file
1<?php
2/*
3* 2007-2015 PrestaShop
4*
5* NOTICE OF LICENSE
6*
7* This source file is subject to the Open Software License (OSL 3.0)
8* that is bundled with this package in the file LICENSE.txt.
9* It is also available through the world-wide-web at this URL:
10* http://opensource.org/licenses/osl-3.0.php
11* If you did not receive a copy of the license and are unable to
12* obtain it through the world-wide-web, please send an email
13* to license@prestashop.com so we can send you a copy immediately.
14*
15* DISCLAIMER
16*
17* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
18* versions in the future. If you wish to customize PrestaShop for your
19* needs please refer to http://www.prestashop.com for more information.
20*
21* @author Prestashop SA <contact@prestashop.com>
22* @copyright 2007-2010 Prestashop SA
23* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
24* International Registered Trademark & Property of PrestaShop SA
25*/
26
27class WebserviceRequestCore
28{
29 const HTTP_GET = 1;
30 const HTTP_POST = 2;
31 const HTTP_PUT = 4;
32
33 protected $_available_languages = null;
34 /**
35 * Errors triggered at execution
36 * @var array
37 */
38 public $errors = array();
39
40 /**
41 * Set if return should display content or not
42 * @var bool
43 */
44 protected $_outputEnabled = true;
45
46 /**
47 * Set if the management is specific or if it is classic (entity management)
48 * @var WebserviceSpecificManagementImages|WebserviceSpecificManagementSearch|false
49 */
50 protected $objectSpecificManagement = false;
51
52 /**
53 * Base PrestaShop webservice URL
54 * @var string
55 */
56 public $wsUrl;
57
58 /**
59 * PrestaShop Webservice Documentation URL
60 * @var string
61 */
62 protected $_docUrl = 'http://doc.prestashop.com/display/PS16/Using+the+PrestaShop+Web+Service';
63
64 /**
65 * Set if the authentication key was checked
66 * @var bool
67 */
68 protected $_authenticated = false;
69
70 /**
71 * HTTP Method to support
72 * @var string
73 */
74 public $method;
75
76 /**
77 * The segment of the URL
78 * @var array
79 */
80 public $urlSegment = array();
81
82 /**
83 * The segment list of the URL after the "api" segment
84 * @var array
85 */
86 public $urlFragments = array();
87
88 /**
89 * The time in microseconds of the start of the execution of the web service request
90 * @var int
91 */
92 protected $_startTime = 0;
93
94 /**
95 * The list of each resources manageable via web service
96 * @var array
97 */
98 public $resourceList;
99
100 /**
101 * The configuration parameters of the current resource
102 * @var array
103 */
104 public $resourceConfiguration;
105
106 /**
107 * The permissions for the current key
108 * @var array
109 */
110 public $keyPermissions;
111
112 /**
113 * The XML string to display if web service call succeed
114 * @var string
115 */
116 protected $specificOutput = '';
117
118 /**
119 * The list of objects to display
120 * @var array
121 */
122 public $objects;
123
124 /**
125 * The current object to support, it extends the PrestaShop ObjectModel
126 * @var ObjectModel
127 */
128 protected $_object;
129
130 /**
131 * The schema to display.
132 * If null, no schema have to be displayed and normal management has to be performed
133 * @var string
134 */
135 public $schemaToDisplay;
136
137 /**
138 * The fields to display. These fields will be displayed when retrieving objects
139 * @var string
140 */
141 public $fieldsToDisplay = 'minimum';
142
143 /**
144 * If we are in PUT or POST case, we use this attribute to store the xml string value during process
145 * @var string
146 */
147 protected $_inputXml;
148
149 /**
150 * Object instance for singleton
151 * @var WebserviceRequest
152 */
153 protected static $_instance;
154
155 /**
156 * Key used for authentication
157 * @var string
158 */
159 protected $_key;
160
161 /**
162 * This is used to have a deeper tree diagram.
163 * @var int
164 */
165 public $depth = 0;
166
167 /**
168 * Name of the output format
169 * @var string
170 */
171 protected $outputFormat = 'xml';
172
173 /**
174 * The object to build the output.
175 * @var WebserviceOutputBuilder
176 */
177 protected $objOutput;
178
179 /**
180 * Save the class name for override used in getInstance()
181 * @var string
182 */
183 public static $ws_current_classname;
184
185
186 public static $shopIDs;
187
188
189 public function getOutputEnabled()
190 {
191 return $this->_outputEnabled;
192 }
193
194 public function setOutputEnabled($bool)
195 {
196 if (Validate::isBool($bool)) {
197 $this->_outputEnabled = $bool;
198 }
199 return $this;
200 }
201
202 /**
203 * Get WebserviceRequest object instance (Singleton)
204 *
205 * @return object WebserviceRequest instance
206 */
207 public static function getInstance()
208 {
209 if (!isset(self::$_instance)) {
210 self::$_instance = new WebserviceRequest::$ws_current_classname();
211 }
212 return self::$_instance;
213 }
214
215 /*
216 protected function getOutputObject($type)
217 {
218 switch ($type)
219 {
220 case 'XML' :
221 default :
222 $obj_render = new WebserviceOutputXML();
223 break;
224 }
225 return $obj_render;
226 }
227 */
228 protected function getOutputObject($type)
229 {
230 // set header param in header or as get param
231 $headers = self::getallheaders();
232 if (isset($headers['Io-Format'])) {
233 $type = $headers['Io-Format'];
234 } elseif (isset($headers['Output-Format'])) {
235 $type = $headers['Output-Format'];
236 } elseif (isset($_GET['output_format'])) {
237 $type = $_GET['output_format'];
238 } elseif (isset($_GET['io_format'])) {
239 $type = $_GET['io_format'];
240 }
241 $this->outputFormat = $type;
242 switch ($type) {
243 case 'JSON' :
244 require_once dirname(__FILE__).'/WebserviceOutputJSON.php';
245 $obj_render = new WebserviceOutputJSON();
246 break;
247 case 'XML' :
248 default :
249 $obj_render = new WebserviceOutputXML();
250 break;
251 }
252 return $obj_render;
253 }
254
255 public static function getResources()
256 {
257 $resources = array(
258 'addresses' => array('description' => 'The Customer, Manufacturer and Customer addresses','class' => 'Address'),
259 'carriers' => array('description' => 'The Carriers','class' => 'Carrier'),
260 'carts' => array('description' => 'Customer\'s carts', 'class' => 'Cart'),
261 'cart_rules' => array('description' => 'Cart rules management', 'class' => 'CartRule'),
262 'categories' => array('description' => 'The product categories','class' => 'Category'),
263 'combinations' => array('description' => 'The product combinations','class' => 'Combination'),
264 'configurations' => array('description' => 'Shop configuration', 'class' => 'Configuration'),
265 'contacts' => array('description' => 'Shop contacts','class' => 'Contact'),
266 'countries' => array('description' => 'The countries','class' => 'Country'),
267 'currencies' => array('description' => 'The currencies', 'class' => 'Currency'),
268 'customers' => array('description' => 'The e-shop\'s customers','class' => 'Customer'),
269 'customer_threads' => array('description' => 'Customer services threads','class' => 'CustomerThread'),
270 'customer_messages' => array('description' => 'Customer services messages','class' => 'CustomerMessage'),
271 'deliveries' => array('description' => 'Product delivery', 'class' => 'Delivery'),
272 'groups' => array('description' => 'The customer\'s groups','class' => 'Group'),
273 'guests' => array('description' => 'The guests', 'class' => 'Guest'),
274 'images' => array('description' => 'The images', 'specific_management' => true),
275 'image_types' => array('description' => 'The image types', 'class' => 'ImageType'),
276 'languages' => array('description' => 'Shop languages', 'class' => 'Language'),
277 'manufacturers' => array('description' => 'The product manufacturers','class' => 'Manufacturer'),
278 'order_carriers' => array('description' => 'The Order carriers','class' => 'OrderCarrier'),
279 'order_details' => array('description' => 'Details of an order', 'class' => 'OrderDetail'),
280 'order_discounts' => array('description' => 'Discounts of an order', 'class' => 'OrderDiscount'),
281 'order_histories' => array('description' => 'The Order histories','class' => 'OrderHistory'),
282 'order_invoices' => array('description' => 'The Order invoices','class' => 'OrderInvoice'),
283 'orders' => array('description' => 'The Customers orders','class' => 'Order'),
284 'order_payments' => array('description' => 'The Order payments','class' => 'OrderPayment'),
285 'order_states' => array('description' => 'The Order statuses','class' => 'OrderState'),
286 'order_slip' => array('description' => 'The Order slips', 'class' => 'OrderSlip'),
287 'price_ranges' => array('description' => 'Price ranges', 'class' => 'RangePrice'),
288 'product_features' => array('description' => 'The product features','class' => 'Feature'),
289 'product_feature_values' => array('description' => 'The product feature values','class' => 'FeatureValue'),
290 'product_options' => array('description' => 'The product options','class' => 'AttributeGroup'),
291 'product_option_values' => array('description' => 'The product options value','class' => 'Attribute'),
292 'products' => array('description' => 'The products','class' => 'Product'),
293 'states' => array('description' => 'The available states of countries','class' => 'State'),
294 'stores' => array('description' => 'The stores', 'class' => 'Store'),
295 'suppliers' => array('description' => 'The product suppliers','class' => 'Supplier'),
296 'tags' => array('description' => 'The Products tags','class' => 'Tag'),
297 'translated_configurations' => array('description' => 'Shop configuration', 'class' => 'TranslatedConfiguration'),
298 'weight_ranges' => array('description' => 'Weight ranges', 'class' => 'RangeWeight'),
299 'zones' => array('description' => 'The Countries zones','class' => 'Zone'),
300 'employees' => array('description' => 'The Employees', 'class' => 'Employee'),
301 'search' => array('description' => 'Search', 'specific_management' => true, 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
302 'content_management_system' => array('description' => 'Content management system', 'class' => 'CMS'),
303 'shops' => array('description' => 'Shops from multi-shop feature', 'class' => 'Shop'),
304 'shop_groups' => array('description' => 'Shop groups from multi-shop feature', 'class' => 'ShopGroup'),
305 'taxes' => array('description' => 'The tax rate', 'class' => 'Tax'),
306 'stock_movements' => array('description' => 'Stock movements', 'class' => 'StockMvtWS', 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
307 'stock_movement_reasons' => array('description' => 'Stock movement reason', 'class' => 'StockMvtReason'),
308 'warehouses' => array('description' => 'Warehouses', 'class' => 'Warehouse', 'forbidden_method' => array('DELETE')),
309 'stocks' => array('description' => 'Stocks', 'class' => 'Stock', 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
310 'stock_availables' => array('description' => 'Available quantities', 'class' => 'StockAvailable', 'forbidden_method' => array('POST', 'DELETE')),
311 'warehouse_product_locations' => array('description' => 'Location of products in warehouses', 'class' => 'WarehouseProductLocation', 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
312 'supply_orders' => array('description' => 'Supply Orders', 'class' => 'SupplyOrder', 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
313 'supply_order_details' => array('description' => 'Supply Order Details', 'class' => 'SupplyOrderDetail', 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
314 'supply_order_states' => array('description' => 'Supply Order Statuses', 'class' => 'SupplyOrderState', 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
315 'supply_order_histories' => array('description' => 'Supply Order Histories', 'class' => 'SupplyOrderHistory', 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
316 'supply_order_receipt_histories' => array('description' => 'Supply Order Receipt Histories', 'class' => 'SupplyOrderReceiptHistory', 'forbidden_method' => array('PUT', 'POST', 'DELETE')),
317 'product_suppliers' => array('description' => 'Product Suppliers', 'class' => 'ProductSupplier'),
318 'tax_rules' => array('description' => 'Tax rules entity', 'class' => 'TaxRule'),
319 'tax_rule_groups' => array('description' => 'Tax rule groups', 'class' => 'TaxRulesGroup'),
320 'specific_prices' => array('description' => 'Specific price management', 'class' => 'SpecificPrice'),
321 'specific_price_rules' => array('description' => 'Specific price management', 'class' => 'SpecificPriceRule'),
322 'shop_urls' => array('description' => 'Shop URLs from multi-shop feature', 'class' => 'ShopUrl'),
323 'product_customization_fields' => array('description' => 'Customization Field', 'class' => 'CustomizationField'),
324 'customizations' => array('description' => 'Customization values', 'class' => 'Customization'),
325 );
326 ksort($resources);
327 return $resources;
328 }
329
330 /* @todo Check how get parameters */
331 /* @todo : set this method out */
332 /**
333 * This method is used for calculate the price for products on the output details
334 *
335 * @param $field
336 * @param $entity_object
337 * @param $ws_params
338 * @return array field parameters.
339 */
340 public function getPriceForProduct($field, $entity_object, $ws_params)
341 {
342 if (is_int($entity_object->id)) {
343 $arr_return = $this->specificPriceForProduct($entity_object, array('default_price'=>''));
344 $field['value'] = $arr_return['default_price']['value'];
345 }
346 return $field;
347 }
348
349 /* @todo : set this method out */
350 /**
351 * This method is used for calculate the price for products on a virtual fields
352 *
353 * @param $entity_object
354 * @param array $parameters
355 * @return array
356 */
357 public function specificPriceForProduct($entity_object, $parameters)
358 {
359 foreach (array_keys($parameters) as $name) {
360 $parameters[$name]['object_id'] = $entity_object->id;
361 }
362 $arr_return = $this->specificPriceCalculation($parameters);
363 return $arr_return;
364 }
365
366 public function specificPriceCalculation($parameters)
367 {
368 $arr_return = array();
369 foreach ($parameters as $name => $value) {
370 $id_shop = (int)Context::getContext()->shop->id;
371 $id_country = (int)(isset($value['country']) ? $value['country'] : (Configuration::get('PS_COUNTRY_DEFAULT')));
372 $id_state = (int)(isset($value['state']) ? $value['state'] : 0);
373 $id_currency = (int)(isset($value['currency']) ? $value['currency'] : Configuration::get('PS_CURRENCY_DEFAULT'));
374 $id_group = (int)(isset($value['group']) ? $value['group'] : (int)Configuration::get('PS_CUSTOMER_GROUP'));
375 $quantity = (int)(isset($value['quantity']) ? $value['quantity'] : 1);
376 $use_tax = (int)(isset($value['use_tax']) ? $value['use_tax'] : Configuration::get('PS_TAX'));
377 $decimals = (int)(isset($value['decimals']) ? $value['decimals'] : Configuration::get('PS_PRICE_ROUND_MODE'));
378 $id_product_attribute = (int)(isset($value['product_attribute']) ? $value['product_attribute'] : null);
379 $only_reduc = (int)(isset($value['only_reduction']) ? $value['only_reduction'] : false);
380 $use_reduc = (int)(isset($value['use_reduction']) ? $value['use_reduction'] : true);
381 $use_ecotax = (int)(isset($value['use_ecotax']) ? $value['use_ecotax'] : Configuration::get('PS_USE_ECOTAX'));
382 $specific_price_output = null;
383 $id_county = (int)(isset($value['county']) ? $value['county'] : 0);
384 $return_value = Product::priceCalculation($id_shop, $value['object_id'], $id_product_attribute, $id_country, $id_state, $id_county, $id_currency, $id_group, $quantity,
385 $use_tax, $decimals, $only_reduc, $use_reduc, $use_ecotax, $specific_price_output, null);
386 $arr_return[$name] = array('sqlId'=>strtolower($name), 'value'=>sprintf('%f', $return_value));
387 }
388 return $arr_return;
389 }
390 /* @todo : set this method out */
391 /**
392 * This method is used for calculate the price for products on a virtual fields
393 *
394 * @param $entity_object
395 * @param array $parameters
396 * @return array
397 */
398 public function specificPriceForCombination($entity_object, $parameters)
399 {
400 foreach (array_keys($parameters) as $name) {
401 $parameters[$name]['object_id'] = $entity_object->id_product;
402 $parameters[$name]['product_attribute'] = $entity_object->id;
403 }
404 $arr_return = $this->specificPriceCalculation($parameters);
405 return $arr_return;
406 }
407
408 /**
409 * Start Webservice request
410 * Check webservice activation
411 * Check autentication
412 * Check resource
413 * Check HTTP Method
414 * Execute the action
415 * Display the result
416 *
417 * @param string $key
418 * @param string $method
419 * @param string $url
420 * @param string $params
421 * @param string $inputXml
422 *
423 * @return array Returns an array of results (headers, content, type of resource...)
424 */
425 public function fetch($key, $method, $url, $params, $bad_class_name, $inputXml = null)
426 {
427 // Time logger
428 $this->_startTime = microtime(true);
429 $this->objects = array();
430
431 // Error handler
432 set_error_handler(array($this, 'webserviceErrorHandler'));
433 ini_set('html_errors', 'off');
434
435 // Two global vars, for compatibility with the PS core...
436 global $webservice_call, $display_errors;
437 $webservice_call = true;
438 $display_errors = strtolower(ini_get('display_errors')) != 'off';
439 // __PS_BASE_URI__ is from Shop::$current_base_uri
440 $this->wsUrl = Tools::getHttpHost(true).__PS_BASE_URI__.'api/';
441 // set the output object which manage the content and header structure and informations
442 $this->objOutput = new WebserviceOutputBuilder($this->wsUrl);
443
444 $this->_key = trim($key);
445
446 $this->outputFormat = isset($params['output_format']) ? $params['output_format'] : $this->outputFormat;
447 // Set the render object to build the output on the asked format (XML, JSON, CSV, ...)
448 $this->objOutput->setObjectRender($this->getOutputObject($this->outputFormat));
449 $this->params = $params;
450 // Check webservice activation and request authentication
451 if ($this->webserviceChecks()) {
452 if ($bad_class_name) {
453 $this->setError(500, 'Class "'.htmlspecialchars($bad_class_name).'" not found. Please update the class_name field in the webservice_account table.', 126);
454 }
455 // parse request url
456 $this->method = $method;
457 $this->urlSegment = explode('/', $url);
458 $this->urlFragments = $params;
459 $this->_inputXml = $inputXml;
460 $this->depth = isset($this->urlFragments['depth']) ? (int)$this->urlFragments['depth'] : $this->depth;
461
462 try {
463 // Method below set a particular fonction to use on the price field for products entity
464 // @see WebserviceRequest::getPriceForProduct() method
465 // @see WebserviceOutputBuilder::setSpecificField() method
466 //$this->objOutput->setSpecificField($this, 'getPriceForProduct', 'price', 'products');
467 if (isset($this->urlFragments['price'])) {
468 $this->objOutput->setVirtualField($this, 'specificPriceForCombination', 'combinations', $this->urlFragments['price']);
469 $this->objOutput->setVirtualField($this, 'specificPriceForProduct', 'products', $this->urlFragments['price']);
470 }
471 } catch (Exception $e) {
472 $this->setError(500, $e->getMessage(), $e->getCode());
473 }
474
475 if (isset($this->urlFragments['language'])) {
476 $this->_available_languages = $this->filterLanguage();
477 } else {
478 $this->_available_languages = Language::getIDs();
479 }
480
481 if (empty($this->_available_languages)) {
482 $this->setError(400, 'language is not available', 81);
483 }
484
485 // Need to set available languages for the render object.
486 // Thus we can filter i18n field for the output
487 // @see WebserviceOutputXML::renderField() method for example
488 $this->objOutput->objectRender->setLanguages($this->_available_languages);
489
490 // check method and resource
491 if (empty($this->errors) && $this->checkResource() && $this->checkHTTPMethod()) {
492 // The resource list is necessary for build the output
493 $this->objOutput->setWsResources($this->resourceList);
494
495 // if the resource is a core entity...
496 if (!isset($this->resourceList[$this->urlSegment[0]]['specific_management']) || !$this->resourceList[$this->urlSegment[0]]['specific_management']) {
497
498 // load resource configuration
499 if ($this->urlSegment[0] != '') {
500 /** @var ObjectModel $object */
501 $object = new $this->resourceList[$this->urlSegment[0]]['class']();
502 if (isset($this->resourceList[$this->urlSegment[0]]['parameters_attribute'])) {
503 $this->resourceConfiguration = $object->getWebserviceParameters($this->resourceList[$this->urlSegment[0]]['parameters_attribute']);
504 } else {
505 $this->resourceConfiguration = $object->getWebserviceParameters();
506 }
507 }
508 $success = false;
509 // execute the action
510 switch ($this->method) {
511 case 'GET':
512 case 'HEAD':
513 if ($this->executeEntityGetAndHead()) {
514 $success = true;
515 }
516 break;
517 case 'POST':
518 if ($this->executeEntityPost()) {
519 $success = true;
520 }
521 break;
522 case 'PUT':
523 if ($this->executeEntityPut()) {
524 $success = true;
525 }
526 break;
527 case 'DELETE':
528 $this->executeEntityDelete();
529 break;
530 }
531 // Need to set an object for the WebserviceOutputBuilder object in any case
532 // because schema need to get webserviceParameters of this object
533 if (isset($object)) {
534 $this->objects['empty'] = $object;
535 }
536 }
537 // if the management is specific
538 else {
539 $specificObjectName = 'WebserviceSpecificManagement'.ucfirst(Tools::toCamelCase($this->urlSegment[0]));
540 if (!class_exists($specificObjectName)) {
541 $this->setError(501, sprintf('The specific management class is not implemented for the "%s" entity.', $this->urlSegment[0]), 124);
542 } else {
543 $this->objectSpecificManagement = new $specificObjectName();
544 $this->objectSpecificManagement->setObjectOutput($this->objOutput)
545 ->setWsObject($this);
546
547 try {
548 $this->objectSpecificManagement->manage();
549 } catch (WebserviceException $e) {
550 if ($e->getType() == WebserviceException::DID_YOU_MEAN) {
551 $this->setErrorDidYouMean($e->getStatus(), $e->getMessage(), $e->getWrongValue(), $e->getAvailableValues(), $e->getCode());
552 } elseif ($e->getType() == WebserviceException::SIMPLE) {
553 $this->setError($e->getStatus(), $e->getMessage(), $e->getCode());
554 }
555 }
556 }
557 }
558 }
559 }
560 $return = $this->returnOutput();
561 unset($webservice_call);
562 unset($display_errors);
563 return $return;
564 }
565
566 protected function webserviceChecks()
567 {
568 return ($this->isActivated() && $this->authenticate() && $this->groupShopExists($this->params) && $this->shopExists($this->params) && $this->shopHasRight($this->_key));
569 }
570
571 /**
572 * Set a webservice error
573 *
574 * @param int $status
575 * @param string $label
576 * @param int $code
577 * @return void
578 */
579 public function setError($status, $label, $code)
580 {
581 global $display_errors;
582 if (!isset($display_errors)) {
583 $display_errors = strtolower(ini_get('display_errors')) != 'off';
584 }
585 if (isset($this->objOutput)) {
586 $this->objOutput->setStatus($status);
587 }
588 $this->errors[] = $display_errors ? array($code, $label) : 'Internal error. To see this error please display the PHP errors.';
589 }
590
591 /**
592 * Set a webservice error and propose a new value near from the available values
593 *
594 * @param int $num
595 * @param string $label
596 * @param array $value
597 * @param array $values
598 * @param int $code
599 * @return void
600 */
601 public function setErrorDidYouMean($num, $label, $value, $available_values, $code)
602 {
603 $this->setError($num, $label.'. Did you mean: "'.$this->getClosest($value, $available_values).'"?'.(count($available_values) > 1 ? ' The full list is: "'.implode('", "', $available_values).'"' : ''), $code);
604 }
605
606 /**
607 * Return the nearest value picked in the values list
608 *
609 * @param string $input
610 * @param array $words
611 * @return string
612 */
613 protected function getClosest($input, $words)
614 {
615 $shortest = -1;
616 foreach ($words as $word) {
617 $lev = levenshtein($input, $word);
618 if ($lev == 0) {
619 $closest = $word;
620 $shortest = 0;
621 break;
622 }
623 if ($lev <= $shortest || $shortest < 0) {
624 $closest = $word;
625 $shortest = $lev;
626 }
627 }
628 return $closest;
629 }
630
631 /**
632 * Used to replace the default PHP error handler, in order to display PHP errors in a XML format
633 *
634 * @param string $errno contains the level of the error raised, as an integer
635 * @param array $errstr contains the error message, as a string
636 * @param array $errfile errfile, which contains the filename that the error was raised in, as a string
637 * @param array $errline errline, which contains the line number the error was raised at, as an integer
638 * @return bool Always return true to avoid the default PHP error handler
639 */
640 public function webserviceErrorHandler($errno, $errstr, $errfile, $errline)
641 {
642 $display_errors = strtolower(ini_get('display_errors')) != 'off';
643 if (!(error_reporting() & $errno) || $display_errors) {
644 return;
645 }
646
647 $errortype = array(
648 E_ERROR => 'Error',
649 E_WARNING => 'Warning',
650 E_PARSE => 'Parse',
651 E_NOTICE => 'Notice',
652 E_CORE_ERROR => 'Core Error',
653 E_CORE_WARNING => 'Core Warning',
654 E_COMPILE_ERROR => 'Compile Error',
655 E_COMPILE_WARNING => 'Compile Warning',
656 E_USER_ERROR => 'Error',
657 E_USER_WARNING => 'User warning',
658 E_USER_NOTICE => 'User notice',
659 E_STRICT => 'Runtime Notice',
660 E_RECOVERABLE_ERROR => 'Recoverable error'
661 );
662 $type = (isset($errortype[$errno]) ? $errortype[$errno] : 'Unknown error');
663 Tools::error_log('[PHP '.$type.' #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')');
664
665 switch ($errno) {
666 case E_ERROR:
667 WebserviceRequest::getInstance()->setError(500, '[PHP Error #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 2);
668 break;
669 case E_WARNING:
670 WebserviceRequest::getInstance()->setError(500, '[PHP Warning #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 3);
671 break;
672 case E_PARSE:
673 WebserviceRequest::getInstance()->setError(500, '[PHP Parse #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 4);
674 break;
675 case E_NOTICE:
676 WebserviceRequest::getInstance()->setError(500, '[PHP Notice #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 5);
677 break;
678 case E_CORE_ERROR:
679 WebserviceRequest::getInstance()->setError(500, '[PHP Core #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 6);
680 break;
681 case E_CORE_WARNING:
682 WebserviceRequest::getInstance()->setError(500, '[PHP Core warning #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 7);
683 break;
684 case E_COMPILE_ERROR:
685 WebserviceRequest::getInstance()->setError(500, '[PHP Compile #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 8);
686 break;
687 case E_COMPILE_WARNING:
688 WebserviceRequest::getInstance()->setError(500, '[PHP Compile warning #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 9);
689 break;
690 case E_USER_ERROR:
691 WebserviceRequest::getInstance()->setError(500, '[PHP Error #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 10);
692 break;
693 case E_USER_WARNING:
694 WebserviceRequest::getInstance()->setError(500, '[PHP User warning #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 11);
695 break;
696 case E_USER_NOTICE:
697 WebserviceRequest::getInstance()->setError(500, '[PHP User notice #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 12);
698 break;
699 case E_STRICT:
700 WebserviceRequest::getInstance()->setError(500, '[PHP Strict #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 13);
701 break;
702 case E_RECOVERABLE_ERROR:
703 WebserviceRequest::getInstance()->setError(500, '[PHP Recoverable error #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 14);
704 break;
705 default:
706 WebserviceRequest::getInstance()->setError(500, '[PHP Unknown error #'.$errno.'] '.$errstr.' ('.$errfile.', line '.$errline.')', 15);
707 }
708 return true;
709 }
710
711 /**
712 * Check if there is one or more error
713 *
714 * @return bool
715 */
716 protected function hasErrors()
717 {
718 return (bool)$this->errors;
719 }
720
721 /**
722 * Check request authentication
723 *
724 * @return bool
725 */
726 protected function authenticate()
727 {
728 if (!$this->hasErrors()) {
729 if (is_null($this->_key)) {
730 $this->setError(401, 'Please enter the authentication key as the login. No password required', 16);
731 } else {
732 if (empty($this->_key)) {
733 $this->setError(401, 'Authentication key is empty', 17);
734 } elseif (strlen($this->_key) != '32') {
735 $this->setError(401, 'Invalid authentication key format', 18);
736 } else {
737 if (WebserviceKey::isKeyActive($this->_key)) {
738 $this->keyPermissions = WebserviceKey::getPermissionForAccount($this->_key);
739 } else {
740 $this->setError(401, 'Authentification key is not active', 20);
741 }
742
743 if (!$this->keyPermissions) {
744 $this->setError(401, 'No permission for this authentication key', 21);
745 }
746 }
747 }
748 if ($this->hasErrors()) {
749 header('WWW-Authenticate: Basic realm="Welcome to PrestaShop Webservice, please enter the authentication key as the login. No password required."');
750 $this->objOutput->setStatus(401);
751 return false;
752 } else {
753 // only now we can say the access is authenticated
754 $this->_authenticated = true;
755 return true;
756 }
757 }
758 }
759
760 /**
761 * Check webservice activation
762 *
763 * @return bool
764 */
765 protected function isActivated()
766 {
767 if (!Configuration::get('PS_WEBSERVICE')) {
768 $this->setError(503, 'The PrestaShop webservice is disabled. Please activate it in the PrestaShop Back Office', 22);
769 return false;
770 }
771 return true;
772 }
773
774 protected function shopHasRight($key)
775 {
776 $sql = 'SELECT 1
777 FROM '._DB_PREFIX_.'webservice_account wsa LEFT JOIN '._DB_PREFIX_.'webservice_account_shop wsas ON (wsa.id_webservice_account = wsas.id_webservice_account)
778 WHERE wsa.key = \''.pSQL($key).'\'';
779
780 foreach (self::$shopIDs as $id_shop) {
781 $OR[] = ' wsas.id_shop = '.(int)$id_shop.' ';
782 }
783 $sql .= ' AND ('.implode('OR', $OR).') ';
784 if (!Db::getInstance()->getValue($sql)) {
785 $this->setError(403, 'No permission for this key on this shop', 132);
786 return false;
787 }
788 return true;
789 }
790
791 protected function shopExists($params)
792 {
793 if (count(self::$shopIDs)) {
794 return true;
795 }
796
797 if (isset($params['id_shop'])) {
798 if ($params['id_shop'] != 'all' && is_numeric($params['id_shop'])) {
799 Shop::setContext(Shop::CONTEXT_SHOP, (int)$params['id_shop']);
800 self::$shopIDs[] = (int)$params['id_shop'];
801 return true;
802 } elseif ($params['id_shop'] == 'all') {
803 Shop::setContext(Shop::CONTEXT_ALL);
804 self::$shopIDs = Shop::getShops(true, null, true);
805 return true;
806 }
807 } else {
808 self::$shopIDs[] = Context::getContext()->shop->id;
809 return true;
810 }
811 $this->setError(404, 'This shop id does not exist', 999);
812 return false;
813 }
814
815 protected function groupShopExists($params)
816 {
817 if (isset($params['id_group_shop']) && is_numeric($params['id_group_shop'])) {
818 Shop::setContext(Shop::CONTEXT_GROUP, (int)$params['id_group_shop']);
819 self::$shopIDs = Shop::getShops(true, (int)$params['id_group_shop'], true);
820 if (count(self::$shopIDs) == 0) {
821 // @FIXME Set ErrorCode !
822 $this->setError(500, 'This group shop doesn\'t have shops', 999);
823 return false;
824 }
825 }
826 // id_group_shop isn't mandatory
827 return true;
828 }
829
830 /**
831 * Check HTTP method
832 *
833 * @return bool
834 */
835 protected function checkHTTPMethod()
836 {
837 if (!in_array($this->method, array('GET', 'POST', 'PUT', 'DELETE', 'HEAD'))) {
838 $this->setError(405, 'Method '.$this->method.' is not valid', 23);
839 } elseif (isset($this->urlSegment[0]) && isset($this->resourceList[$this->urlSegment[0]]['forbidden_method']) && in_array($this->method, $this->resourceList[$this->urlSegment[0]]['forbidden_method'])) {
840 $this->setError(405, 'Method '.$this->method.' is not allowed for the resource '.$this->urlSegment[0], 101);
841 } elseif ($this->urlSegment[0] && !in_array($this->method, $this->keyPermissions[$this->urlSegment[0]])) {
842 $this->setError(405, 'Method '.$this->method.' is not allowed for the resource '.$this->urlSegment[0].' with this authentication key', 25);
843 } else {
844 return true;
845 }
846 return false;
847 }
848
849 /**
850 * Check resource validity
851 *
852 * @return bool
853 */
854 protected function checkResource()
855 {
856 $this->resourceList = $this->getResources();
857 $resourceNames = array_keys($this->resourceList);
858 if ($this->urlSegment[0] == '') {
859 $this->resourceConfiguration['objectsNodeName'] = 'resources';
860 } elseif (in_array($this->urlSegment[0], $resourceNames)) {
861 if (!in_array($this->urlSegment[0], array_keys($this->keyPermissions))) {
862 $this->setError(401, 'Resource of type "'.$this->urlSegment[0].'" is not allowed with this authentication key', 26);
863 return false;
864 }
865 } else {
866 $this->setErrorDidYouMean(400, 'Resource of type "'.$this->urlSegment[0].'" does not exists', $this->urlSegment[0], $resourceNames, 27);
867 return false;
868 }
869 return true;
870 }
871
872
873 protected function setObjects()
874 {
875 $objects = array();
876 $arr_avoid_id = array();
877 $ids = array();
878 if (isset($this->urlFragments['id'])) {
879 preg_match('#^\[(.*)\]$#Ui', $this->urlFragments['id'], $matches);
880 if (count($matches) > 1) {
881 $ids = explode(',', $matches[1]);
882 }
883 } else {
884 $ids[] = (int)$this->urlSegment[1];
885 }
886 if (!empty($ids)) {
887 foreach ($ids as $id) {
888 $object = new $this->resourceConfiguration['retrieveData']['className']((int)$id);
889 if (!$object->id) {
890 $arr_avoid_id[] = $id;
891 } else {
892 $objects[] = $object;
893 }
894 }
895 }
896
897 if (!empty($arr_avoid_id) || empty($ids)) {
898 $this->setError(404, 'Id(s) not exists: '.implode(', ', $arr_avoid_id), 87);
899 $this->_outputEnabled = true;
900 }
901 }
902
903 protected function parseDisplayFields($str)
904 {
905 $bracket_level = 0;
906 $part = array();
907 $tmp = '';
908 $str_len = strlen($str);
909 for ($i = 0; $i < $str_len; $i++) {
910 if ($str[$i] == ',' && $bracket_level == 0) {
911 $part[] = $tmp;
912 $tmp = '';
913 } else {
914 $tmp .= $str[$i];
915 }
916 if ($str[$i] == '[') {
917 $bracket_level++;
918 }
919 if ($str[$i] == ']') {
920 $bracket_level--;
921 }
922 }
923 if ($tmp != '') {
924 $part[] = $tmp;
925 }
926 $fields = array();
927 foreach ($part as $str) {
928 $field_name = trim(substr($str, 0, (strpos($str, '[') === false ? strlen($str) : strpos($str, '['))));
929 if (!isset($fields[$field_name])) {
930 $fields[$field_name] = null;
931 }
932 if (strpos($str, '[') !== false) {
933 $sub_fields = substr($str, strpos($str, '[') + 1, strlen($str) - strpos($str, '[') - 2);
934 $tmp_array = array();
935 if (strpos($sub_fields, ',') !== false) {
936 $tmp_array = explode(',', $sub_fields);
937 } else {
938 $tmp_array = array($sub_fields);
939 }
940 $fields[$field_name] = (is_array($fields[$field_name])) ? array_merge($fields[$field_name], $tmp_array) : $tmp_array;
941 }
942 }
943 return $fields;
944 }
945
946 public function setFieldsToDisplay()
947 {
948 // set the fields to display in the list : "full", "minimum", "field_1", "field_1,field_2,field_3"
949 if (isset($this->urlFragments['display'])) {
950 $this->fieldsToDisplay = $this->urlFragments['display'];
951 if ($this->fieldsToDisplay != 'full' && $this->fieldsToDisplay != 'minimum') {
952 preg_match('#^\[(.*)\]$#Ui', $this->fieldsToDisplay, $matches);
953 if (count($matches)) {
954 $error = false;
955 $fieldsToTest = $this->parseDisplayFields($matches[1]);
956 foreach ($fieldsToTest as $field_name => $part) {
957 // in case it is not an association
958 if (!is_array($part)) {
959 // We have to allow new specific field for price calculation too
960 $error = (!isset($this->resourceConfiguration['fields'][$field_name]) && !isset($this->urlFragments['price'][$field_name]));
961 } else {
962 // if this association does not exists
963 if (!array_key_exists($field_name, $this->resourceConfiguration['associations'])) {
964 $error = true;
965 }
966
967 foreach ($part as $field) {
968 if ($field != 'id' && !array_key_exists($field, $this->resourceConfiguration['associations'][$field_name]['fields'])) {
969 $error = true;
970 break;
971 }
972 }
973 }
974 if ($error) {
975 $this->setError(400, 'Unable to display this field "'.$field_name.(is_array($part) ? ' (details : '.var_export($part, true).')' : '').'". However, these are available: '.implode(', ', array_keys($this->resourceConfiguration['fields'])), 35);
976 return false;
977 }
978 }
979 $this->fieldsToDisplay = $fieldsToTest;
980 } else {
981 $this->setError(400, 'The \'display\' syntax is wrong. You can set \'full\' or \'[field_1,field_2,field_3,...]\'. These are available: '.implode(', ', array_keys($this->resourceConfiguration['fields'])), 36);
982 return false;
983 }
984 }
985 }
986 return true;
987 }
988
989 protected function manageFilters()
990 {
991 // filtered fields which can not use filters : hidden_fields
992 $available_filters = array();
993 // filtered i18n fields which can use filters
994 $i18n_available_filters = array();
995 foreach ($this->resourceConfiguration['fields'] as $fieldName => $field) {
996 if ((!isset($this->resourceConfiguration['hidden_fields']) ||
997 (isset($this->resourceConfiguration['hidden_fields']) && !in_array($fieldName, $this->resourceConfiguration['hidden_fields'])))) {
998 if ((!isset($field['i18n']) ||
999 (isset($field['i18n']) && !$field['i18n']))) {
1000 $available_filters[] = $fieldName;
1001 } else {
1002 $i18n_available_filters[] = $fieldName;
1003 }
1004 }
1005 }
1006
1007 // Date feature : date=1
1008 if (!empty($this->urlFragments['date']) && $this->urlFragments['date']) {
1009 if (!in_array('date_add', $available_filters)) {
1010 $available_filters[] = 'date_add';
1011 }
1012 if (!in_array('date_upd', $available_filters)) {
1013 $available_filters[] = 'date_upd';
1014 }
1015 if (!array_key_exists('date_add', $this->resourceConfiguration['fields'])) {
1016 $this->resourceConfiguration['fields']['date_add'] = array('sqlId' => 'date_add');
1017 }
1018 if (!array_key_exists('date_upd', $this->resourceConfiguration['fields'])) {
1019 $this->resourceConfiguration['fields']['date_upd'] = array('sqlId' => 'date_upd');
1020 }
1021 } else {
1022 foreach ($available_filters as $key => $value) {
1023 if ($value == 'date_add' || $value == 'date_upd') {
1024 unset($available_filters[$key]);
1025 }
1026 }
1027 }
1028
1029 //construct SQL filter
1030 $sql_filter = '';
1031 $sql_join = '';
1032 if ($this->urlFragments) {
1033 $schema = 'schema';
1034 // if we have to display the schema
1035 if (isset($this->urlFragments[$schema])) {
1036 if ($this->urlFragments[$schema] == 'blank' || $this->urlFragments[$schema] == 'synopsis') {
1037 $this->schemaToDisplay = $this->urlFragments[$schema];
1038 return true;
1039 } else {
1040 $this->setError(400, 'Please select a schema of type \'synopsis\' to get the whole schema informations (which fields are required, which kind of content...) or \'blank\' to get an empty schema to fill before using POST request', 28);
1041 return false;
1042 }
1043 } else {
1044 // if there are filters
1045 if (isset($this->urlFragments['filter'])) {
1046 foreach ($this->urlFragments['filter'] as $field => $url_param) {
1047 if ($field != 'sort' && $field != 'limit') {
1048 if (!in_array($field, $available_filters)) {
1049 // if there are linked tables
1050 if (isset($this->resourceConfiguration['linked_tables']) && isset($this->resourceConfiguration['linked_tables'][$field])) {
1051 // contruct SQL join for linked tables
1052 $sql_join .= 'LEFT JOIN `'.bqSQL(_DB_PREFIX_.$this->resourceConfiguration['linked_tables'][$field]['table']).'` '.bqSQL($field).' ON (main.`'.bqSQL($this->resourceConfiguration['fields']['id']['sqlId']).'` = '.bqSQL($field).'.`'.bqSQL($this->resourceConfiguration['fields']['id']['sqlId']).'`)'."\n";
1053
1054 // construct SQL filter for linked tables
1055 foreach ($url_param as $field2 => $value) {
1056 if (isset($this->resourceConfiguration['linked_tables'][$field]['fields'][$field2])) {
1057 $linked_field = $this->resourceConfiguration['linked_tables'][$field]['fields'][$field2];
1058 $sql_filter .= $this->getSQLRetrieveFilter($linked_field['sqlId'], $value, $field.'.');
1059 } else {
1060 $list = array_keys($this->resourceConfiguration['linked_tables'][$field]['fields']);
1061 $this->setErrorDidYouMean(400, 'This filter does not exist for this linked table', $field2, $list, 29);
1062 return false;
1063 }
1064 }
1065 } elseif ($url_param != '' && in_array($field, $i18n_available_filters)) {
1066 if (!is_array($url_param)) {
1067 $url_param = array($url_param);
1068 }
1069 $sql_join .= 'LEFT JOIN `'.bqSQL(_DB_PREFIX_.$this->resourceConfiguration['retrieveData']['table']).'_lang` AS main_i18n ON (main.`'.pSQL($this->resourceConfiguration['fields']['id']['sqlId']).'` = main_i18n.`'.bqSQL($this->resourceConfiguration['fields']['id']['sqlId']).'`)'."\n";
1070 foreach ($url_param as $field2 => $value) {
1071 $linked_field = $this->resourceConfiguration['fields'][$field];
1072 $sql_filter .= $this->getSQLRetrieveFilter($linked_field['sqlId'], $value, 'main_i18n.');
1073 $language_filter = '['.implode('|', $this->_available_languages).']';
1074 $sql_filter .= $this->getSQLRetrieveFilter('id_lang', $language_filter, 'main_i18n.');
1075 }
1076 }
1077 // if there are filters on linked tables but there are no linked table
1078 elseif (is_array($url_param)) {
1079 if (isset($this->resourceConfiguration['linked_tables'])) {
1080 $this->setErrorDidYouMean(400, 'This linked table does not exist', $field, array_keys($this->resourceConfiguration['linked_tables']), 30);
1081 } else {
1082 $this->setError(400, 'There is no existing linked table for this resource', 31);
1083 }
1084 return false;
1085 } else {
1086 $this->setErrorDidYouMean(400, 'This filter does not exist', $field, $available_filters, 32);
1087 return false;
1088 }
1089