| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792 |
- <?php
- /*
- * This file is part of the MODX Revolution package.
- *
- * Copyright (c) MODX, LLC
- *
- * For complete copyright and license information, see the COPYRIGHT and LICENSE
- * files found in the top-level directory of this distribution.
- *
- */
- /**
- * Abstract controller class for modRestService; all REST controllers must extend this class to be properly
- * implemented.
- *
- * @package modx
- * @subpackage rest
- */
- abstract class modRestController {
- /** @var modX $modx The modX instance */
- public $modx;
- /** @var array $config An array of configuration properties, passed from modRestService */
- public $config = array();
- /** @var array $properties An array of request parameters passed */
- public $properties = array();
- /** @var array $headers An array of HTTP headers passed */
- public $headers = array();
- /** @var string $primaryKeyField The primary key field for this controller; useful when automating REST calls */
- public $primaryKeyField = 'id';
- /** @var array $errors An array of errors that may have occurred for this controller */
- public $errors = array();
- /** @var string $errorMessage A generic error message for this response */
- public $errorMessage = '';
- /** @var boolean $protected Whether or not this controller is "protected" - meaning whether or not verifyAuthentication will be called*/
- protected $protected = true;
- /** @var \modRestServiceRequest $request The request object passed to this controller */
- protected $request;
- /** @var string $response The response being sent by this controller */
- protected $response;
- /** @var string $responseStatus The response status being sent by this controller */
- protected $responseStatus;
- /**
- * The following options are used if the default get/put/post/delete methods are not overridden. They automate
- * the display and manipulation of data based on the classKey that is specified on the controller class, allowing
- * for quick and easy controller creation based on standard CRUD concepts.
- */
- /** @var string $classKey The xPDO class to use */
- public $classKey;
- /** @var string $classAlias The alias of the class when used in the getList method */
- public $classAlias;
- /** @var string $defaultSortField The default field to sort by in the getList method */
- public $defaultSortField = 'name';
- /** @var string $defaultSortDirection The default direction to sort in the getList method */
- public $defaultSortDirection = 'ASC';
- /** @var int $defaultLimit The default number of records to return in the getList method */
- public $defaultLimit = 20;
- /** @var int $defaultOffset The default offset in the getList method */
- public $defaultOffset = 0;
- /** @var xPDOObject $object */
- public $object;
- /** @var array $searchFields Optional. An array of fields to use when the search parameter is passed */
- public $searchFields = array();
- /** @var array $postRequiredFields An array of required field keys that must be passed for POST requests */
- public $postRequiredFields = array();
- /** @var array $postRequiredRelatedObjects An array of classKey/field pairings for checking related objects on POST */
- public $postRequiredRelatedObjects = array();
- /** @var string $postMethod The method on the object to call for POST requests */
- public $postMethod = 'save';
- /** @var array $putRequiredFields An array of required field keys that must be passed for PUT requests */
- public $putRequiredFields = array();
- /** @var array $postRequiredRelatedObjects An array of classKey/field pairings for checking related objects on PUT */
- public $putRequiredRelatedObjects = array();
- /** @var string $putMethod The method on the object to call for PUT requests */
- public $putMethod = 'save';
- /** @var array $deleteRequiredFields An array of required field keys that must be passed for DELETE requests */
- public $deleteRequiredFields = array();
- /** @var string $deleteMethod The method on the object to call for DELETE requests */
- public $deleteMethod = 'remove';
- /** @var array $allowedMethods An array of allowed request methods */
- public $allowedMethods = array('GET', 'POST', 'PUT', 'DELETE');
- /** @var array $allowedMethods An array of allowed request methods */
- public $allowedHeaders = array('Content-Type');
- /**
- * @param modX $modx The modX instance
- * @param modRestServiceRequest $request The rest service request class instance
- * @param array $config An array of configuration properties, passed through from modRestService
- */
- public function __construct(modX $modx,modRestServiceRequest $request,array $config = array()) {
- $this->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;
- }
- }
|