* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ /** * @todo : Create typed exception for more finer errors check */ class WebserviceOutputBuilderCore { /** * @var int constant */ public const VIEW_LIST = 1; public const VIEW_DETAILS = 2; protected $wsUrl; protected $output; /** @var WebserviceOutputInterface|WebserviceOutputXML|WebserviceOutputJSON|null */ public $objectRender; protected $wsResource; protected $depth = 0; protected $schemaToDisplay; protected $fieldsToDisplay; protected $specificFields = []; protected $virtualFields = []; protected $statusInt; protected $wsParamOverrides; protected static $_cache_ws_parameters = []; /* Header properties */ protected $headerParams = [ 'Access-Time' => 0, 'X-Powered-By' => 0, 'PSWS-Version' => 0, 'Content-Type' => 0, ]; /** * @var string Status header sent at return */ protected $status; public function __construct($ws_url) { $this->statusInt = 200; $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 200 OK'; $this->wsUrl = $ws_url; $this->wsParamOverrides = []; } /** * Set the render object for set the output format. * Set the Content-type for the http header. * * @param WebserviceOutputInterface $obj_render * @throw WebserviceException if the object render is not an instance of WebserviceOutputInterface * * @return $this * * @throws WebserviceException */ public function setObjectRender(WebserviceOutputInterface $obj_render) { if (!$obj_render instanceof WebserviceOutputInterface) { throw new WebserviceException('Obj_render param must be an WebserviceOutputInterface object type', [83, 500]); } $this->objectRender = $obj_render; $this->objectRender->setWsUrl($this->wsUrl); if ($this->objectRender->getContentType()) { $this->setHeaderParams('Content-Type', $this->objectRender->getContentType()); } return $this; } /** * getter. * * @return WebserviceOutputInterface */ public function getObjectRender() { return $this->objectRender; } /** * Need to have the resource list to get the class name for an entity, * To build. * * @param array $resources * * @return $this */ public function setWsResources($resources) { $this->wsResource = $resources; return $this; } /** * This method return an array with each http header params for a content. * This check each required params. * * If this method is overrided don't forget to check required specific params (for xml etc...) * * @return array */ public function buildHeader() { $return = []; $return[] = $this->status; foreach ($this->headerParams as $key => $param) { $return[] = trim($key) . ': ' . $param; } return $return; } /** * @param string $key The normalized key expected for an http response * @param string $value * * @return $this * * @throws WebserviceException If the key or the value are corrupted (use Validate::isCleanHtml method) */ public function setHeaderParams($key, $value) { if (!Validate::isCleanHtml($key) || !Validate::isCleanHtml($value)) { throw new WebserviceException('the key or your value is corrupted.', [94, 500]); } $this->headerParams[$key] = $value; return $this; } /** * @param string|null $key if null get all header params otherwise the params specified by the key * @throw WebserviceException if the key is corrupted (use Validate::isCleanHtml method) * @throw WebserviceException if the asked key does'nt exists. * * @return array|int */ public function getHeaderParams($key = null) { if (null !== $key) { if (!Validate::isCleanHtml($key)) { throw new WebserviceException('the key you write is a corrupted text.', [95, 500]); } if (!array_key_exists($key, $this->headerParams)) { throw new WebserviceException(sprintf('The key %s does\'nt exist', $key), [96, 500]); } $return = $this->headerParams[$key]; } else { $return = $this->headerParams; } return $return; } /** * Delete all Header parameters previously set. * * @return $this */ public function resetHeaderParams() { $this->headerParams = []; return $this; } /** * @return string the normalized status for http request */ public function getStatus() { return $this->status; } public function getStatusInt() { return $this->statusInt; } /** * Set the return header status. * * @param int $num the Http status code */ public function setStatus($num) { $this->statusInt = (int) $num; switch ($num) { case 200: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 200 OK'; break; case 201: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 201 Created'; break; case 204: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 204 No Content'; break; case 304: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified'; break; case 400: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'; break; case 401: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized'; break; case 403: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'; break; case 404: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'; break; case 405: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed'; break; case 500: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error'; break; case 501: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 501 Not Implemented'; break; case 503: $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 503 Service Unavailable'; break; } } /** * Build errors output using an error array. * * @param array $errors * * @return string output in the format specified by WebserviceOutputBuilder::objectRender */ public function getErrors($errors) { $str_output = ''; if (!empty($errors)) { if (isset($this->objectRender)) { $str_output = $this->objectRender->renderErrorsHeader(); foreach ($errors as $error) { if (is_array($error)) { $str_output .= $this->objectRender->renderErrors($error[1], $error[0]); } else { $str_output .= $this->objectRender->renderErrors($error); } } $str_output .= $this->objectRender->renderErrorsFooter(); $str_output = $this->objectRender->overrideContent($str_output); } else { $str_output = '
' . print_r($errors, true) . '
'; } } return $str_output; } /** * Build the resource list in the output format specified by WebserviceOutputBuilder::objectRender. * * @param array $key_permissions * * @return string */ public function getResourcesList($key_permissions) { if (null === $this->wsResource) { throw new WebserviceException('You must set web service resource for get the resources list.', [82, 500]); } $output = ''; $more_attr = ['shopName' => htmlspecialchars(Configuration::get('PS_SHOP_NAME'))]; $output .= $this->objectRender->renderNodeHeader('api', [], $more_attr); foreach ($this->wsResource as $resourceName => $resource) { if (in_array($resourceName, array_keys($key_permissions))) { $more_attr = [ 'xlink_resource' => $this->wsUrl . $resourceName, 'get' => (in_array('GET', $key_permissions[$resourceName]) ? 'true' : 'false'), 'put' => (in_array('PUT', $key_permissions[$resourceName]) ? 'true' : 'false'), 'post' => (in_array('POST', $key_permissions[$resourceName]) ? 'true' : 'false'), 'patch' => (in_array('PATCH', $key_permissions[$resourceName]) ? 'true' : 'false'), 'delete' => (in_array('DELETE', $key_permissions[$resourceName]) ? 'true' : 'false'), 'head' => (in_array('HEAD', $key_permissions[$resourceName]) ? 'true' : 'false'), ]; $output .= $this->objectRender->renderNodeHeader($resourceName, [], $more_attr); $output .= $this->objectRender->renderNodeHeader('description', [], $more_attr); $output .= $resource['description']; $output .= $this->objectRender->renderNodeFooter('description', []); if (!isset($resource['specific_management']) || !$resource['specific_management']) { $more_attr_schema = [ 'xlink_resource' => $this->wsUrl . $resourceName . '?schema=blank', 'type' => 'blank', ]; $output .= $this->objectRender->renderNodeHeader('schema', [], $more_attr_schema, false); $more_attr_schema = [ 'xlink_resource' => $this->wsUrl . $resourceName . '?schema=synopsis', 'type' => 'synopsis', ]; $output .= $this->objectRender->renderNodeHeader('schema', [], $more_attr_schema, false); } $output .= $this->objectRender->renderNodeFooter($resourceName, []); } } $output .= $this->objectRender->renderNodeFooter('api', []); $output = $this->objectRender->overrideContent($output); return $output; } public function registerOverrideWSParameters($wsrObject, $method) { $this->wsParamOverrides[] = ['object' => $wsrObject, 'method' => $method]; } /** * Method is used for each content type * Different content types are : * - list of entities, * - tree diagram of entity details (full or minimum), * - schema (synopsis & blank),. * * @param array $objects each object created by entity asked * * @see WebserviceOutputBuilder::executeEntityGetAndHead * * @param string|null $schema_to_display if null display the entities list or entity details * @param string|array $fields_to_display the fields allow for the output * @param int $depth depth for the tree diagram output * @param int $type_of_view use the 2 constants WebserviceOutputBuilder::VIEW_LIST WebserviceOutputBuilder::VIEW_DETAILS * * @return string in the output format specified by WebserviceOutputBuilder::objectRender */ public function getContent($objects, $schema_to_display = null, $fields_to_display = 'minimum', $depth = 0, $type_of_view = self::VIEW_LIST, $override = true) { $this->fieldsToDisplay = $fields_to_display; $this->depth = $depth; $output = ''; if ($schema_to_display != null) { $this->schemaToDisplay = $schema_to_display; $this->objectRender->setSchemaToDisplay($this->schemaToDisplay); // If a shema is asked the view must be an details type $type_of_view = self::VIEW_DETAILS; } $class = get_class($objects['empty']); if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) { WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $objects['empty']->getWebserviceParameters(); } $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class]; foreach ($this->wsParamOverrides as $p) { $object = $p['object']; $ws_params = $object->{$p['method']}($ws_params); } // If a list is asked, need to wrap with a plural node if ($type_of_view === self::VIEW_LIST) { $output .= $this->setIndent($depth) . $this->objectRender->renderNodeHeader($ws_params['objectsNodeName'], $ws_params); } if (null === $this->schemaToDisplay) { foreach ($objects as $key => $object) { if ($key !== 'empty') { if ($this->fieldsToDisplay === 'minimum') { $output .= $this->renderEntityMinimum($object, $depth); } else { $output .= $this->renderEntity($object, $depth); } } } } else { $output .= $this->renderSchema($objects['empty'], $ws_params); } // If a list is asked, need to wrap with a plural node if ($type_of_view === self::VIEW_LIST) { $output .= $this->setIndent($depth) . $this->objectRender->renderNodeFooter($ws_params['objectsNodeName'], $ws_params); } if ($override) { $output = $this->objectRender->overrideContent($output); } return $output; } /** * Create the tree diagram with no details. * * @param ObjectModel $object create by the entity * @param int $depth the depth for the tree diagram * * @return string */ public function renderEntityMinimum($object, $depth) { $class = get_class($object); if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) { WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $object->getWebserviceParameters(); } $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class]; $more_attr['id'] = $object->id; $more_attr['xlink_resource'] = $this->wsUrl . $ws_params['objectsNodeName'] . '/' . $object->id; $output = $this->setIndent($depth) . $this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params, $more_attr, false); return $output; } /** * Build a schema blank or synopsis. * * @param ObjectModel $object create by the entity * @param array $ws_params webserviceParams from the entity * * @return string */ protected function renderSchema($object, $ws_params) { $output = $this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params); foreach ($ws_params['fields'] as $field_name => $field) { $output .= $this->renderField($object, $ws_params, $field_name, $field, 0); } if (isset($ws_params['associations']) && count($ws_params['associations']) > 0) { $this->fieldsToDisplay = 'full'; $output .= $this->renderAssociations($object, 0, $ws_params['associations'], $ws_params); } $output .= $this->objectRender->renderNodeFooter($ws_params['objectNodeName'], $ws_params); return $output; } /** * Build the entity detail. * * @param ObjectModel $object create by the entity * @param int $depth the depth for the tree diagram * * @return string */ public function renderEntity($object, $depth) { $output = ''; $class = get_class($object); if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) { WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $object->getWebserviceParameters(); } $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class]; foreach ($this->wsParamOverrides as $p) { $o = $p['object']; $ws_params = $o->{$p['method']}($ws_params); } $output .= $this->setIndent($depth) . $this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params); if (!empty($object->id)) { // This to add virtual Fields for a particular entity. $virtual_fields = $this->addVirtualFields($ws_params['objectsNodeName'], $object); if (!empty($virtual_fields)) { $ws_params['fields'] = array_merge($ws_params['fields'], $virtual_fields); } foreach ($ws_params['fields'] as $field_name => $field) { if ($this->fieldsToDisplay === 'full' || array_key_exists($field_name, $this->fieldsToDisplay)) { $field['object_id'] = $object->id; $field['entity_name'] = $ws_params['objectNodeName']; $field['entities_name'] = $ws_params['objectsNodeName']; $output .= $this->renderField($object, $ws_params, $field_name, $field, $depth); } } } $subexists = false; if (is_array($this->fieldsToDisplay)) { foreach ($this->fieldsToDisplay as $fields) { if (is_array($fields)) { $subexists = true; } } } if (isset($ws_params['associations']) && ($this->fieldsToDisplay == 'full' || $subexists)) { $output .= $this->renderAssociations($object, $depth, $ws_params['associations'], $ws_params); } $output .= $this->setIndent($depth) . $this->objectRender->renderNodeFooter($ws_params['objectNodeName'], $ws_params); return $output; } /** * Build a field and use recursivity depend on the depth parameter. * * @param ObjectModel $object create by the entity * @param array $ws_params webserviceParams from the entity * @param string $field_name * @param array $field * @param int $depth * * @return string */ protected function renderField($object, $ws_params, $field_name, $field, $depth) { $output = ''; $show_field = true; if (isset($ws_params['hidden_fields']) && in_array($field_name, $ws_params['hidden_fields'])) { return ''; } if ($this->schemaToDisplay === 'synopsis') { $field['synopsis_details'] = $this->getSynopsisDetails($field); if ($field_name === 'id') { $show_field = false; } } if ($this->schemaToDisplay === 'blank') { if (isset($field['setter']) && !$field['setter']) { $show_field = false; } } // don't set any value for a schema if (isset($field['synopsis_details']) || $this->schemaToDisplay === 'blank') { $field['value'] = ''; if (isset($field['xlink_resource'])) { unset($field['xlink_resource']); } } elseif (isset($field['getter']) && $object != null && method_exists($object, $field['getter'])) { $field['value'] = $object->{$field['getter']}(); } elseif (!isset($field['value'])) { $field['value'] = $object->$field_name; } // this apply specific function for a particular field on a choosen entity $field = $this->overrideSpecificField($ws_params['objectsNodeName'], $field_name, $field, $object, $ws_params); // don't display informations for a not existant id if (substr($field['sqlId'], 0, 3) == 'id_' && !$field['value']) { if ($field['value'] === null) { $field['value'] = ''; } // delete the xlink except for schemas if (isset($field['xlink_resource']) && null === $this->schemaToDisplay) { unset($field['xlink_resource']); } } // set "id" for each node name which display the id of the entity if ($field_name === 'id') { $field['sqlId'] = 'id'; } // don't display the node id for a synopsis schema if ($show_field) { $output .= $this->setIndent($depth - 1) . $this->objectRender->renderField($field); } return $output; } /** * @param ObjectModel $object * @param int $depth * @param array $associations * @param array $ws_params * * @return string */ protected function renderAssociations($object, $depth, $associations, $ws_params) { $output = $this->objectRender->renderAssociationWrapperHeader(); foreach ($associations as $assoc_name => $association) { if ($this->fieldsToDisplay == 'full' || is_array($this->fieldsToDisplay) && array_key_exists($assoc_name, $this->fieldsToDisplay)) { $getter = $association['getter']; $objects_assoc = []; $fields_assoc = []; if (isset($association['fields'])) { $fields_assoc = $association['fields']; } $parent_details = [ 'object_id' => $object->id, 'entity_name' => $ws_params['objectNodeName'], 'entities_name' => $ws_params['objectsNodeName'], ]; if (is_array($getter)) { $association_resources = call_user_func($getter, $object); if (is_array($association_resources) && !empty($association_resources)) { foreach ($association_resources as $association_resource) { $objects_assoc[] = $association_resource; } } } else { if (method_exists($object, $getter) && null === $this->schemaToDisplay) { $association_resources = $object->$getter(); if (is_array($association_resources) && !empty($association_resources)) { foreach ($association_resources as $association_resource) { $objects_assoc[] = $association_resource; } } } else { $objects_assoc[] = ''; } } $class_name = null; if (isset($this->wsResource[$assoc_name]['class']) && class_exists($this->wsResource[$assoc_name]['class'], true)) { $class_name = $this->wsResource[$assoc_name]['class']; } $output_details = ''; foreach ($objects_assoc as $object_assoc) { if ($depth == 0 || $class_name === null) { $value = null; if (!empty($object_assoc)) { $value = $object_assoc; } if (empty($fields_assoc)) { $fields_assoc = [['id' => ($value['id'] ?? null)]]; } $output_details .= $this->renderFlatAssociation($object, $depth, $assoc_name, $association['resource'], $fields_assoc, $value, $parent_details); } else { foreach ($object_assoc as $id) { if ($class_name !== null) { $child_object = new $class_name($id); $output_details .= $this->renderEntity($child_object, ($depth - 2 ? 0 : $depth - 2)); } } } } if ($output_details != '') { $output .= $this->setIndent($depth) . $this->objectRender->renderAssociationHeader($object, $ws_params, $assoc_name); $output .= $output_details; $output .= $this->setIndent($depth) . $this->objectRender->renderAssociationFooter($object, $ws_params, $assoc_name); } else { $output .= $this->setIndent($depth) . $this->objectRender->renderAssociationHeader($object, $ws_params, $assoc_name, true); } } } $output .= $this->objectRender->renderAssociationWrapperFooter(); return $output; } protected function renderFlatAssociation($object, $depth, $assoc_name, $resource_name, $fields_assoc, $object_assoc, $parent_details) { $output = ''; $more_attr = []; if (isset($this->wsResource[$assoc_name]) && null === $this->schemaToDisplay) { if ($assoc_name == 'images') { if ($parent_details['entities_name'] == 'combinations') { $more_attr['xlink_resource'] = $this->wsUrl . $assoc_name . '/products/' . $object->id_product . '/' . $object_assoc['id']; } else { $more_attr['xlink_resource'] = $this->wsUrl . $assoc_name . '/' . $parent_details['entities_name'] . '/' . $parent_details['object_id'] . '/' . $object_assoc['id']; } } else { $more_attr['xlink_resource'] = $this->wsUrl . $assoc_name . '/' . $object_assoc['id']; } } $output .= $this->setIndent($depth - 1) . $this->objectRender->renderNodeHeader($resource_name, [], $more_attr); foreach ($fields_assoc as $field_name => $field) { if (!is_array($this->fieldsToDisplay) || in_array($field_name, $this->fieldsToDisplay[$assoc_name])) { if (isset($field['id']) && !isset($field['sqlId'])) { $field['sqlId'] = 'id'; $field['value'] = isset($object_assoc['id']) ? $object_assoc['id'] : null; } elseif (!isset($field['sqlId'])) { $field['sqlId'] = $field_name ?: 'id'; $field['value'] = isset($object_assoc[$field_name]) ? $object_assoc[$field_name] : null; } $field['entities_name'] = $assoc_name; $field['entity_name'] = $resource_name; if (null !== $this->schemaToDisplay) { $field['synopsis_details'] = $this->getSynopsisDetails($field); } $field['is_association'] = true; $output .= $this->setIndent($depth - 1) . $this->objectRender->renderField($field); } } $output .= $this->setIndent($depth - 1) . $this->objectRender->renderNodeFooter($resource_name, []); return $output; } public function setIndent($depth) { $string = ''; $number_of_tabs = $this->depth - $depth; for ($i = 0; $i < $number_of_tabs; ++$i) { $string .= "\t"; } return $string; } public function getSynopsisDetails($field) { $arr_details = []; if (array_key_exists('required', $field) && $field['required']) { $arr_details['required'] = 'true'; } if (array_key_exists('maxSize', $field) && $field['maxSize']) { $arr_details['maxSize'] = $field['maxSize']; } if (array_key_exists('validateMethod', $field) && $field['validateMethod']) { $arr_details['format'] = $field['validateMethod']; } if (array_key_exists('setter', $field) && !$field['setter']) { $arr_details['readOnly'] = 'true'; } return $arr_details; } /** * @param string|object $object * @param string $method * @param string $field_name * @param string $entity_name * * @return $this * * @throws Exception * @throws WebserviceException */ public function setSpecificField($object, $method, $field_name, $entity_name) { try { $this->validateObjectAndMethod($object, $method); } catch (WebserviceException $e) { throw $e; } $this->specificFields[$field_name] = ['entity' => $entity_name, 'object' => $object, 'method' => $method, 'type' => gettype($object)]; return $this; } protected function validateObjectAndMethod($object, $method) { if (is_string($object) && !class_exists($object)) { throw new WebserviceException('The object you want to set in ' . __METHOD__ . ' is not allowed.', [98, 500]); } if (!method_exists($object, $method)) { throw new WebserviceException('The method you want to set in ' . __METHOD__ . ' is not allowed.', [99, 500]); } } public function getSpecificField() { return $this->specificFields; } protected function overrideSpecificField($entity_name, $field_name, $field, $entity_object, $ws_params) { if (array_key_exists($field_name, $this->specificFields) && $this->specificFields[$field_name]['entity'] == $entity_name) { if ($this->specificFields[$field_name]['type'] == 'string') { $object = new $this->specificFields[$field_name]['object'](); } elseif ($this->specificFields[$field_name]['type'] == 'object') { $object = $this->specificFields[$field_name]['object']; } if (isset($object) && is_object($object)) { $field = $object->{$this->specificFields[$field_name]['method']}($field, $entity_object, $ws_params); } } return $field; } public function setVirtualField($object, $method, $entity_name, $parameters) { try { $this->validateObjectAndMethod($object, $method); } catch (WebserviceException $e) { throw $e; } $this->virtualFields[$entity_name][] = ['parameters' => $parameters, 'object' => $object, 'method' => $method, 'type' => gettype($object)]; } public function getVirtualFields() { return $this->virtualFields; } public function addVirtualFields($entity_name, $entity_object) { $arr_return = []; $virtual_fields = $this->getVirtualFields(); if (array_key_exists($entity_name, $virtual_fields)) { foreach ($virtual_fields[$entity_name] as $function_infos) { if ($function_infos['type'] == 'string') { $object = new $function_infos['object'](); } elseif ($function_infos['type'] == 'object') { $object = $function_infos['object']; } if (isset($object) && is_object($object)) { $return_fields = $object->{$function_infos['method']}($entity_object, $function_infos['parameters']); foreach ($return_fields as $field_name => $value) { if (Validate::isConfigName($field_name)) { $arr_return[$field_name] = $value; } else { throw new WebserviceException('Name for the virtual field is not allow', [128, 400]); } } } } } return $arr_return; } public function setFieldsToDisplay($fields) { $this->fieldsToDisplay = $fields; } }