* @copyright 2007-2014 PrestaShop SA * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) * International Registered Trademark & Property of PrestaShop SA */ /** * @todo : Create typed exception for more finer errors check */ class WebserviceOutputBuilderCore { /** * * @var int constant */ const VIEW_LIST = 1; const VIEW_DETAILS = 2; protected $wsUrl; protected $output; public $objectRender; protected $wsResource; protected $depth = 0; protected $schemaToDisplay; protected $fieldsToDisplay; protected $specificFields = array(); protected $virtualFields = array(); protected $statusInt; protected $wsParamOverrides; protected static $_cache_ws_parameters = array(); // Header properties protected $headerParams = array( '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 = array(); } /** * 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 */ public function setObjectRender(WebserviceOutputInterface $obj_render) { if (!$obj_render instanceof WebserviceOutputInterface) throw new WebserviceException('Obj_render param must be an WebserviceOutputInterface object type', array(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 = array(); $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 * @throw WebserviceException if the key or the value are corrupted * (use Validate::isCleanHtml method) * @return $this */ public function setHeaderParams($key, $value) { if (!Validate::isCleanHtml($key) OR !Validate::isCleanHtml($value)) throw new WebserviceException('the key or your value is corrupted.', array(94, 500)); $this->headerParams[$key] = $value; return $this; } /** * @param null|string $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|string */ public function getHeaderParams($key = null) { $return = ''; if (!is_null($key)) { if (!Validate::isCleanHtml($key)) throw new WebserviceException('the key you write is a corrupted text.', array(95, 500)); if (!array_key_exists($key, $this->headerParams)) throw new WebserviceException(sprintf('The key %s does\'nt exist', $key), array(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 = array(); 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 * @return void */ 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) { 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 $key_permissions * @return string */ public function getResourcesList($key_permissions) { if (is_null($this->wsResource)) throw new WebserviceException ('You must set web service resource for get the resources list.', array(82, 500)); $output = ''; $more_attr = array('shop_name' => htmlspecialchars(Configuration::get('PS_SHOP_NAME'))); $output .= $this->objectRender->renderNodeHeader('api', array(), $more_attr); foreach ($this->wsResource as $resourceName => $resource) { if (in_array($resourceName, array_keys($key_permissions))) { $more_attr = array( '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'), 'delete' => (in_array('DELETE', $key_permissions[$resourceName]) ? 'true' : 'false'), 'head' => (in_array('HEAD', $key_permissions[$resourceName]) ? 'true' : 'false'), ); $output .= $this->objectRender->renderNodeHeader($resourceName, array(), $more_attr); $output .= $this->objectRender->renderNodeHeader('description', array(), $more_attr); $output .= $resource['description']; $output .= $this->objectRender->renderNodeFooter('description', array()); if (!isset($resource['specific_management']) || !$resource['specific_management']) { $more_attr_schema = array( 'xlink_resource' => $this->wsUrl.$resourceName.'?schema=blank', 'type' => 'blank', ); $output .= $this->objectRender->renderNodeHeader('schema', array(), $more_attr_schema, false); $more_attr_schema = array( 'xlink_resource' => $this->wsUrl.$resourceName.'?schema=synopsis', 'type' => 'synopsis', ); $output .= $this->objectRender->renderNodeHeader('schema', array(), $more_attr_schema, false); } $output .= $this->objectRender->renderNodeFooter($resourceName, array()); } } $output .= $this->objectRender->renderNodeFooter('api', array()); $output = $this->objectRender->overrideContent($output); return $output; } public function registerOverrideWSParameters($wsrObject, $method) { $this->wsParamOverrides[] = array('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 null|string $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 (is_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 ($object->id != 0) { // 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']) && is_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 $object * @param $depth * @param $associations * @param $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 = array(); $fields_assoc = array(); if (isset($association['fields'])) $fields_assoc = $association['fields']; $parent_details = array( '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) && is_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 = array(array('id' => $value['id'])); $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 = array(); if (isset($this->wsResource[$assoc_name]) && is_null($this->schemaToDisplay)) { if ($assoc_name == 'images') $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, array(), $more_attr); foreach ($fields_assoc as $field_name=>$field) { if (!is_array($this->fieldsToDisplay) || in_array($field_name, $this->fieldsToDisplay[$assoc_name])) { if ($field_name == 'id' && !isset($field['sqlId'])) { $field['sqlId'] = 'id'; $field['value'] = $object_assoc['id']; } elseif (!isset($field['sqlId'])) { $field['sqlId'] = $field_name; $field['value'] = $object_assoc[$field_name]; } $field['entities_name'] = $assoc_name; $field['entity_name'] = $resource_name; if (!is_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, array()); 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 * @return $this */ public function setSpecificField($object, $method, $field_name, $entity_name) { try { $this->validateObjectAndMethod($object, $method); } catch (WebserviceException $e) { throw $e; } $this->specificFields[$field_name] = array('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.', array(98, 500)); if (!method_exists($object, $method)) throw new WebserviceException ('The method you want to set in '.__METHOD__.' is not allowed.', array(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']; $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][] = array('parameters' => $parameters, 'object' => $object, 'method' => $method, 'type' => gettype($object)); } public function getVirtualFields() { return $this->virtualFields; } public function addVirtualFields($entity_name, $entity_object) { $arr_return = array(); $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']; $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', array(128, 400)); } } } return $arr_return; } public function setFieldsToDisplay($fields) { $this->fieldsToDisplay = $fields; } }