modx =& $modx; $this->request =& $request; $this->config = array_merge($this->config,$config); } /** * Get a configuration option for this controller * * @param string $key * @param mixed $default * @return mixed */ public function getOption($key,$default = null) { return array_key_exists($key,$this->config) ? $this->config[$key] : $default; } /** * Initialize the controller */ public function initialize() {} /** * Override to verify authentication on this specific controller. Useful for managing permissions. * * @return boolean */ public function verifyAuthentication() { return true; } /** * Return whether or not this controller is set to be protected * @final * @return bool */ final public function isProtected() { return $this->protected; } /** * Check for any empty fields * * @param array $fields * @param boolean $setFieldError * @return bool|string */ public function checkRequiredFields(array $fields = array(),$setFieldError = true) { $missing = array(); foreach ($fields as $field) { $value = $this->getProperty($field); $isEmptyString = empty($value) && $value !== "0"; $isNotBase = !is_int($value) && !is_bool($value); if ($isEmptyString && $isNotBase) { $missing[] = $field; if ($setFieldError) { $this->addFieldError($field,$this->modx->lexicon('rest.err_field_required')); } } } if (!empty($missing)) { return $this->modx->lexicon('rest.err_fields_required',array( 'fields' => implode(', ',$missing), )); } return true; } /** * Check to ensure the existence of required related objects on the passed request * * @param array $pairs An array of arrays in the format: 'field' => 'classKey' * @return boolean */ public function checkRequiredRelatedObjects(array $pairs = array()) { $passed = true; foreach ($pairs as $field => $classKey) { if (!empty($classKey) && !empty($field)) { $relatedObject = $this->modx->getObject($classKey,$this->getProperty($field)); if (empty($relatedObject)) { $objectName = substr($classKey,2); $this->addFieldError($field,$this->modx->lexicon('err.obj_nf',array('name' => $objectName))); $passed = false; } } } return $passed; } /** * Get a REQUEST property for the controller * * @param string $key * @param mixed $default * @return mixed */ public function getProperty($key,$default =null) { $value = $default; if (array_key_exists($key,$this->properties)) { $value = $this->properties[$key]; } return $value; } /** * Set a request property for the controller * * @param string $key * @param string $value */ public function setProperty($key,$value) { $this->properties[$key] = $value; } /** * Unset a request property for the controller * @param string $key */ public function unsetProperty($key) { unset($this->properties[$key]); } /** * Get the request properties for the controller * @return array */ public function getProperties() { return $this->properties; } /** * Set a collection of properties for the controller * * @param array $properties An array of properties * @param bool $merge Optionally, only merge properties in if this is true */ public function setProperties(array $properties = array(),$merge = false) { $this->properties = $merge ? array_merge($this->properties,$properties) : $properties; } /** * Set the HTTP request headers for this controller * * @param array $headers An array of headers * @param bool $merge Optionally, only merge headers in if this is true */ public function setHeaders(array $headers = array(),$merge = false) { $this->headers = $merge ? array_merge($this->headers,$headers) : $headers; } /** * Get the request headers for this controller * @return array */ final public function getHeaders() { return $this->headers; } /** * Return a success message for this controller, with an optional return object * * @param string $message Optional. The success response message. * @param array|xPDOObject $object Optional. An xPDOObject or array to send as the return object. * @param int $status Optional. The status code to send. */ public function success($message = '',$object = array(),$status = null) { if (empty($status)) $status = $this->getOption('defaultSuccessStatusCode',200); $this->process(true,$message,$object,$status); } /** * Return a failure message for this controller, with an optional return object. Will also automatically * send errors in an errors root node if any are found. * * @param string $message Optional. The failure response message. * @param array|xPDOObject $object Optional. An xPDOObject or array to send as the return object. * @param int $status Optional. The status code to send. */ public function failure($message = '',$object = array(),$status = null) { if (empty($status)) $status = $this->getOption('defaultFailureStatusCode',200); $this->process(false,$message,$object,$status); } /** * Process the response and format in the proper response format. * * @param bool $success Whether or not this response is successful. * @param string $message Optional. The response message. * @param array|xPDOObject $object Optional. The response return object. * @param int $status Optional. The response code. */ protected function process($success = true,$message = '',$object = array(),$status = 200) { $response = array( $this->getOption('responseSuccessKey','success') => $success, $this->getOption('responseMessageKey','message') => $message, $this->getOption('responseObjectKey','object') => is_object($object) ? $object->toArray() : $object, 'code' => $status ); if (empty($success) && !empty($this->errors)) { $response[$this->getOption('responseErrorsKey','errors')] = $this->errors; } $this->modx->log(modX::LOG_LEVEL_DEBUG,'[REST] Sending REST response: '.print_r($response,true)); $this->response = $response; $this->responseStatus = empty($status) ? (empty($success) ? $this->getOption('defaultFailureStatusCode',200) : $this->getOption('defaultSuccessStatusCode',200)) : $status; } /** * Return the response status code * @return string */ final public function getResponseStatus() { return $this->responseStatus; } /** * Return the response payload * * @return string */ final public function getResponse() { return $this->response; } /** * Get any errors that may have been set on this controller * * @return array */ public function getErrors() { return $this->errors; } /** * See if any errors have been set during the request on this controller * * @return bool */ public function hasErrors() { return !empty($this->errors) || !empty($this->errorMessage); } /** * Set an error for a specific field * @param string $k The key of the field to set * @param string $v The error message to set * @param boolean $append Whether or not to append the error message or overwrite it */ public function addFieldError($k,$v,$append = true) { if ($append && !empty($this->errors[$k])) { $separator = $this->getOption('errorMessageSeparator',' '); $this->errors[$k] .= $separator.$v; } else { $this->errors[$k] = $v; } } /** * Remove an error from a field * * @param string $k */ public function removeFieldError($k) { unset($this->errors[$k]); } /** * Set the general error message * * @param string $message */ public function setErrorMessage($message) { $this->errorMessage = $message; } /** * Clear the general error message */ public function clearErrorMessage() { $this->errorMessage = ''; } /** * Set a default value for a property on this controller request. * @param string $k The key of the field * @param mixed $v The default value to set * @param bool $useNotEmpty Whether or not to use empty() for checking set status * @return boolean True if the default was used */ public function setDefault($k,$v,$useNotEmpty = false) { $isSet = false; if ($useNotEmpty) { if (!empty($this->properties[$k])) $isSet = true; } else if (array_key_exists($k,$this->properties)) { $isSet = true; } if (!$isSet) { $this->properties[$k] = $v; } return !$isSet; } /** * Set an array of default values for properties on this controller request * @param array $array * @param bool $useNotEmpty */ public function setDefaults(array $array,$useNotEmpty = false) { foreach ($array as $k => $v) { $this->setDefault($k,$v,$useNotEmpty); } } /** * Output a collection of objects as a list. * * @param array $list * @param int|boolean $total * @param int $status */ public function collection($list = array(),$total = false,$status = null) { if (empty($status)) $status = $this->getOption('defaultSuccessStatusCode',200); if ($total === false) { $total = count($list); } $this->response = array( $this->getOption('collectionResultsKey','results') => $list, $this->getOption('collectionTotalKey','total') => $total, ); $this->responseStatus = $status; } /** * Route GET requests * @return array */ public function get() { $pk = $this->getProperty($this->primaryKeyField); if (empty($pk)) { return $this->getList(); } return $this->read($pk); } /** * Abstract method for routing GET requests without a primary key passed. Must be defined in your derivative * controller. Handles fetching of collections of objects. * * @abstract * @return array */ public function getList() { $this->getProperties(); $c = $this->modx->newQuery($this->classKey); $c = $this->addSearchQuery($c); $c = $this->prepareListQueryBeforeCount($c); $total = $this->modx->getCount($this->classKey,$c); $alias = !empty($this->classAlias) ? $this->classAlias : $this->classKey; $c->select($this->modx->getSelectColumns($this->classKey,$alias)); $c = $this->prepareListQueryAfterCount($c); $c->sortby($this->getProperty($this->getOption('propertySort','sort'),$this->defaultSortField),$this->getProperty($this->getOption('propertySortDir','dir'),$this->defaultSortDirection)); $limit = $this->getProperty($this->getOption('propertyLimit','limit'),$this->defaultLimit); if (empty($limit)) $limit = $this->defaultLimit; $c->limit($limit,$this->getProperty($this->getOption('propertyOffset','start'),$this->defaultOffset)); $objects = $this->modx->getCollection($this->classKey,$c); if (empty($objects)) $objects = array(); $list = array(); /** @var xPDOObject $object */ foreach ($objects as $object) { $list[] = $this->prepareListObject($object); } return $this->collection($list,$total); } /** * Add a search query to listing calls * * @param xPDOQuery $c * @return xPDOQuery */ protected function addSearchQuery(xPDOQuery $c) { $search = $this->getProperty($this->getOption('propertySearch','search'),false); if (!empty($search) && !empty($this->searchFields)) { $searchQuery = array(); $i = 0; foreach ($this->searchFields as $searchField) { $or = $i > 0 ? 'OR:' : ''; $searchQuery[$or.$searchField.':LIKE'] = '%'.$search.'%'; $i++; } if (!empty($searchQuery)) { $c->where($searchQuery); } } return $c; } /** * Allows manipulation of the query object before the COUNT statement is called on listing calls. Override to * provide custom functionality. * * @param xPDOQuery $c * @return xPDOQuery */ protected function prepareListQueryBeforeCount(xPDOQuery $c) { return $c; } /** * Allows manipulation of the query object after the COUNT statement is called on listing calls. Override to * provide custom functionality. * * @param xPDOQuery $c * @return xPDOQuery */ protected function prepareListQueryAfterCount(xPDOQuery $c) { return $c; } /** * Returns an array of field-value pairs for the object when listing. Override to provide custom functionality. * * @param xPDOObject $object The current iterated object * @return array An array of field-value pairs of data */ protected function prepareListObject(xPDOObject $object) { return $object->toArray(); } /** * Get the criteria for the getObject call for GET/PUT/DELETE requests * @param mixed $id * @return array */ public function getPrimaryKeyCriteria($id) { return array($this->primaryKeyField => $id); } /** * Abstract method for routing GET requests with a primary key passed. Must be defined in your derivative * controller. * @abstract * @param $id */ public function read($id) { if (empty($id)) { return $this->failure($this->modx->lexicon('rest.err_field_ns',array( 'field' => $this->primaryKeyField, ))); } /** @var xPDOObject $object */ $c = $this->getPrimaryKeyCriteria($id); $this->object = $this->modx->getObject($this->classKey,$c); if (empty($this->object)) { return $this->failure($this->modx->lexicon('rest.err_obj_nf',array( 'class_key' => $this->classKey, ))); } $objectArray = $this->object->toArray(); $afterRead = $this->afterRead($objectArray); if ($afterRead !== true && $afterRead !== null) { return $this->failure($afterRead === false ? $this->errorMessage : $afterRead); } return $this->success('',$objectArray); } /** * Fires after reading the object. Override to provide custom functionality. * * @param array $objectArray A reference to the outputting array * @return boolean|string Either return true/false or a string message */ public function afterRead(array &$objectArray) { return !$this->hasErrors(); } /** * Method for routing POST requests. Can be overridden; default behavior automates xPDOObject, class-based requests. * @abstract */ public function post() { $properties = $this->getProperties(); if (!empty($this->postRequiredFields)) { if (!$this->checkRequiredFields($this->postRequiredFields)) { return $this->failure($this->modx->lexicon('error')); } } if (!empty($this->postRequiredRelatedObjects)) { if (!$this->checkRequiredRelatedObjects($this->postRequiredRelatedObjects)) { return $this->failure(); } } /** @var xPDOObject $object */ $this->object = $this->modx->newObject($this->classKey); $this->object->fromArray($properties); $beforePost = $this->beforePost(); if ($beforePost !== true && $beforePost !== null) { return $this->failure($beforePost === false ? $this->errorMessage : $beforePost); } if (!$this->object->{$this->postMethod}()) { $this->setObjectErrors(); if ($this->hasErrors()) { return $this->failure(); } else { return $this->failure($this->modx->lexicon('rest.err_class_save',array( 'class_key' => $this->classKey, ))); } } $objectArray = $this->object->toArray(); $this->afterPost($objectArray); return $this->success('',$objectArray); } /** * Fires before saving the new object. Override to provide custom functionality. * @return boolean */ public function beforePost() { return !$this->hasErrors(); } /** * Fires after saving the new object. Override to provide custom functionality. * * @param array $objectArray A reference to the outputting array */ public function afterPost(array &$objectArray) {} /** * Handles updating of objects * @return array */ public function put() { $id = $this->getProperty($this->primaryKeyField,false); if (empty($id)) { return $this->failure($this->modx->lexicon('rest.err_field_ns',array( 'field' => $this->primaryKeyField, ))); } $c = $this->getPrimaryKeyCriteria($id); $this->object = $this->modx->getObject($this->classKey,$c); if (empty($this->object)) { return $this->failure($this->modx->lexicon('rest.err_obj_nf',array( 'class_key' => $this->classKey, ))); } if (!empty($this->putRequiredFields)) { if (!$this->checkRequiredFields($this->putRequiredFields)) { return $this->failure(); } } if (!empty($this->putRequiredRelatedObjects)) { if (!$this->checkRequiredRelatedObjects($this->putRequiredRelatedObjects)) { return $this->failure(); } } $this->object->fromArray($this->getProperties()); $beforePut = $this->beforePut(); if ($beforePut !== true && $beforePut !== null) { return $this->failure($beforePut === false ? $this->errorMessage : $beforePut); } if (!$this->object->{$this->putMethod}()) { $this->setObjectErrors(); if ($this->hasErrors()) { return $this->failure(); } else { return $this->failure($this->modx->lexicon('rest.err_class_save',array( 'class_key' => $this->classKey, ))); } } $objectArray = $this->object->toArray(); $this->afterPut($objectArray); return $this->success('',$objectArray); } /** * Fires before saving an existing object. Override to provide custom functionality. * @return boolean */ public function beforePut() { return !$this->hasErrors(); } /** * Fires after saving an existing object. Override to provide custom functionality. * @param array $objectArray A reference to the outputting array */ public function afterPut(array &$objectArray) {} /** * Handle DELETE requests * @return array */ public function delete() { $id = $this->getProperty($this->primaryKeyField,false); if (empty($id)) { return $this->failure($this->modx->lexicon('rest.err_field_ns',array( 'field' => $this->primaryKeyField, ))); } $c = $this->getPrimaryKeyCriteria($id); $this->object = $this->modx->getObject($this->classKey,$c); if (empty($this->object)) { return $this->failure($this->modx->lexicon('rest.err_obj_nf',array( 'class_key' => $this->classKey, ))); } if (!empty($this->deleteRequiredFields)) { if (!$this->checkRequiredFields($this->deleteRequiredFields)) { return $this->failure(); } } $this->object->fromArray($this->getProperties()); $beforeDelete = $this->beforeDelete(); if ($beforeDelete !== true) { return $this->failure($beforeDelete === false ? $this->errorMessage : $beforeDelete); } if (!$this->object->{$this->deleteMethod}()) { $this->setObjectErrors(); return $this->failure($this->modx->lexicon('rest.err_class_remove',array( 'class_key' => $this->classKey, ))); } $objectArray = $this->object->toArray(); $this->afterDelete($objectArray); return $this->success('',$objectArray); } /** * Fires before deleting an existing object. Override to provide custom functionality. * @return boolean */ public function beforeDelete() { return !$this->hasErrors(); } /** * Fires after deleting an existing object. Override to provide custom functionality. * * @param array $objectArray */ public function afterDelete(array &$objectArray) {} /** * Handle OPTIONS requests * @return array */ public function options() { header('Access-Control-Allow-Methods: ' . implode(', ', $this->allowedMethods)); header('Access-Control-Allow-Headers: ' . implode(', ', $this->allowedHeaders)); $this->responseStatus = 200; } /** * Set object-specific model-layer errors for classes that implement the getErrors/addFieldError methods */ public function setObjectErrors() { if (method_exists($this->object,'getErrors')) { $errors = $this->object->getErrors(); foreach ($errors as $k => $msg) { $this->addFieldError($k,$msg); } } } /** * If an object is set, set the object fields with the passed values * * @param object $object * @param array $values * @return mixed */ public function setObjectFields(&$object,array $values = array()) { foreach ($values as $key => $value) { if (is_array($value)) { foreach ($value as $k => $v) { $object->{$key[$k]} = $v; } } else { $object->{$key} = $value; } } return $object; } }