<?php

class ApiComponent extends Component {

	const ROOT = 'root';
	const STYLESHEET = null;
	const DECLARATION = '<?xml version="1.0" encoding="UTF-8"?>';

	const USE_META = true;
	const FORMAT_XML = true;

	const CURRENT_VERSION = 1;

	const PAGINATION_LIMIT = 300;

	public $time = '19700101000000';
	public $token = null;
	public $format = 'json';
	public $version = 1;
	public $storedRequest = null;
	public $exception = null;

	public $allowedFiles = 'jpe?g|gif|png|pdf|docx?|xlsx?|txt';
	public $allowedFormats = [ 'xml', 'json', 'csv', 'assets' ];

	private $modificationRequests = [];

	/** @var CakeResponse */
	private $responseObject = null;

	public function __construct() {
		if (!empty($this->uses))
			foreach ($this->uses as $model) {
				$this->$model = ClassRegistry::init($model);
			}
	}

	public function setResponseObject($response) {
		$this->responseObject = $response;
	}

	# ~ Startup	 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function setToken($token) {
		$this->token = $token;
	}

	# ~ Initialize - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function initialize(Controller $controller) {
		$this->Controller = $controller;

		if (!$this->responseObject) {
			$this->responseObject = $this->Controller->response;
		}

		$this->requestParams = $this->Controller->request->params;
		$this->namedParams = $this->requestParams['named'];
		$this->LogApi = ClassRegistry::init('LogApi');

		# Set time
		if (!empty($this->namedParams['time'])) {
			$this->time = $this->namedParams['time'];
		}

		# Set format
		if (!empty($this->namedParams['format'])) {
			$this->setFormat($this->namedParams['format']);
		} else if (!empty($this->Controller->request->params['format'])) {
			$this->setFormat($this->Controller->request->params['format']);
		}

		# Set version
		if (!empty($this->namedParams['version'])) {
			$this->version = (int) preg_replace('~[^0-9]~', '', $this->namedParams['version']);
		}
	}

	# ~ Start up - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function startup(Controller $controller) {

		# Validate token
		if ($this->token !== null && (empty($this->namedParams['token']) || $this->namedParams['token'] != $this->token)) {
			$this->build(403);
		}
	}

	# ~ Confirm input  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function validatePost($salt, $expected = [], $data = null) {
		if ($data === null)
			$data = $this->Controller->request->data;

		# Extract token and ip
		$ip = env('REMOTE_ADDR');
		$token = array_pop($data);

		# Check the post
		$safe = [];
		foreach ($expected as $field) {

			# Check that field exists
			if (key($data) != $field) {
				$this->build(400);
			}

			$safe[$field] = array_shift($data);
		}

		# Check for tails
		if (!empty($data)) {
			$this->build(400);
		}

		# Check the hash
		$hash = md5(implode($safe) . $salt . $ip);
		$valid = $token === $hash;

		# Debug
		if (!$valid && Configure::read('debug') > 2 && $ip === '127.0.0.1') {
			die($hash);
		}

		# Check the hash
		return $valid ? $safe : $this->build(403);
	}

	# ~ Handle the received exception  - - - - - - - - - - - - - - - - - - - - - - #
	public function handleException($ex) {
		$development = LOCALHOST || Configure::read('debug');

		if ($ex instanceof InvalidFieldsException) {
			$this->build(null, [
				'success' => false,
				'message' => $ex->text,
				'errors'  => $ex->getErrors()
			]);
		}

		# Soft exception
		if ($ex instanceof APISoftException) {
			$this->build(null, [
					'success' => false,
					'message' => $ex->text,
				] + (array) $ex->additional, [], [ 'code' => $ex->code ]);

			return;
		}

		# Hard exception
		if ($ex instanceof APIHardException) {
			$code = $ex->code;
			$error = $ex->text;

			# Unknown exception
		} else {
			$code = 500;
			$error = $ex->getMessage();
		}

		# Write to log
		$this->exception = $ex->getMessage();
		//CakeLog::write('error', "API error: " . $error . "\n\n" . print_r($this->Controller->request, true));
		CakeLog::write('error', print_r($ex->getErrors(), true));

		# Return response
		$this->build($code, $error);
	}

	# ~ Build  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function build($title, $data = [], $additional = [], $meta = [], $headers = []) {

		// Dirty fix to avoid $title as first param
		if (!empty($title) && !is_string($title)) {
			return $this->build(null, $title, $data, $additional, $meta);
		}

		$development = LOCALHOST || Configure::read('debug');

		# Error
		if (is_numeric($title)) {

			# Debug info
			if ($development && is_string($data)) {
				$meta['debug'] = $data;
			}

			$data = [];
			$meta['error'] = $title;

		} else if (!empty($title)) {
			if (empty($data) && !is_array($data))
				$data = new Object();
			$data = array_merge([ $title => $data ], (array) $additional);
		}

		# Log feed status
		if (!empty($this->storedRequest)) {
			$statusCode = !empty($meta['error']) ? $meta['error'] : 200;
			$this->storedRequest = [
				'id'        => $this->storedRequest,
				'status'    => $statusCode,
				'exception' => $this->exception ];

			$this->LogApi->save(
				$this->storedRequest,
				[
					'callbacks' => false,
					'validate'  => false ]
			);
		}

		# Append meta information
		if (self::USE_META && $additional !== false) {
			$meta = array_merge([
				'ip'        => env('REMOTE_ADDR'),
				'version'   => self::CURRENT_VERSION,
				'cached'    => date('Y-m-d H:i:s'),
				'error'     => null,
				'generated' => "###GENERATED###"
			], $meta);

			# Append debug info
			if ($development && empty($meta['debug'])) {
				$meta['debug'] = null;
			}
		}

		# Meta
		if (self::USE_META && $additional !== false) {
			if (!is_array($data)) {
				$data = [ 'meta' => [ 'error' => $data ] ];
			} else {
				$data['meta'] = $meta;
			}
		}

		# JSON
		if ($this->isJSON()) {
			$type = 'application/json';
			$content = json_encode($data);

			# XML
		} else if ($this->isXML()) {
			$type = 'application/xml';

			# Define element
			$xml = new SimpleXMLElement(self::DECLARATION . self::STYLESHEET . '<' . self::ROOT . '></' . self::ROOT . '>', LIBXML_NOCDATA);

			# Import array to XML and format it
			$this->append($data, $xml);
			$content = $xml->asXML();

			if (self::FORMAT_XML) {
				$dom = new DOMDocument('1.0');
				$dom->preserveWhiteSpace = false;
				$dom->formatOutput = true;
				$dom->loadXML(preg_replace('~([\\x00-\\x09]|[\\x0B-\\x1F])~ e', '', $content));
				$content = $dom->saveXML();
			}

			# Unrecognized format
		} else {
			$this->setFormat('json');
			$this->build(400);
		}

		# Capture data
		$print = '';
		while (ob_get_contents())
			$print .= ob_get_clean();
		$print = trim($print);

		# Se the generated time
		$generated = round((microtime(true) - START_TIME) * 1000);
		$content = preg_replace('~"(\<!\[CDATA\[|")?###GENERATED###("|\]\]\>)?~', $generated, $content);

		# Display debug info
		if (Configure::read('debug') && $print) {
			$content = htmlentities($content);
			echo "{$print}\n\n"; //"div style=\"margin-top: 1em; padding-top: 0.5em; border-top: 1px solid #333; font-weight: bold;\">\n\tHeaders sent in {$file} :{$line}\n</div>\n\n<pre>\n{$content}\n</pre>";

			# Output headers and cotent
		} else {

			# Response code
			if (!empty($meta['error']) && is_numeric($meta['error'])) {
				header("HTTP/1.0 {$meta['error']}");
			}

			# Debug headers
			if ($development) {
				$headers['X-Intellex-Debug'] = !empty($meta['debug']) ? $meta['debug'] : null;
			}

			# Additional headers
			$headers['Content-Type'] = $type . '; charset=utf-8';
			$headers['Content-Length'] = strlen($content);
			$headers['X-Intellex-Generated'] = $generated;
			foreach ($headers as $header => $value)
				header("{$header}: {$value}");

			echo $content;
		}

		exit;
	}

	# ~ Autonomated synchronization	 - - - - - - - - - - - - - - - - - - - - - - - #
	public function synchronize($tables = [], $max = null) {
		$globalImageSize = '';

		# Check params
		if (!$this->time = $this->isoTime($this->time)) {
			return $this->build(400);
		}

		# Current time
		$now = date('Y-m-d H:i:s', START_TIME);
		if ($max !== null) {
			$now = min($now, $max);
		}

		# Get the page
		$page = !empty($this->Controller->request->params['named']['page'])
			? (int) $this->Controller->request->params['named']['page']
			: 0;

		# Response template
		$response = [
			'manifest' => [
				'time'       => $now,
				'pagination' => [
					'count' => 0,
					'limit' => self::PAGINATION_LIMIT,
					'pages' => 1,
					'page'  => 1 ],
				'weight'     => [
					'text'   => 0,
					'binary' => 0,
					'total'  => 0 ] ],
			'tables'   => []
		];

		# Manifest
		$manifest = $page === 1;
		if (!$manifest) {
			unset($response['manifest']);
		}

		# Limit uploads to used tables
		if (isset($tables['uploads']) && !isset($tables['uploads']['conditions']['model']) && empty($tables['uploads']['nolimit'])) {
			foreach (array_keys($tables) as $table) {
				$tables['uploads']['conditions']['model'][] = Inflector::classify($table);
			}

			if (isset($tables['uploads']['size'])) {
				$globalImageSize = $tables['uploads']['size'];
			}
		}

		# Handle single tables
		if (!empty($this->namedParams['table'])) {
			$table = $this->namedParams['table'];
			if (!isset($tables[$table])) {
				$this->build(404);
			}

			$tables = [ $table => $tables[$table] ];
		}

		# Prepare uploads
		$uploads = !empty($this->Controller->request->data['uploads'])
			? json_decode($this->Controller->request->data['uploads'])
			: [];

		# Validate uploads
		if ($uploads === null) {
			$this->build(400, 'Error parsing supplied uploads');
		}

		# Get all tables
		foreach ($tables as $table => $params) {

			if (preg_match('~^uploads-(.*)~', $table, $match)) {
				$table = 'uploads';
				$params['name'] = $match[1];
			}

			# Default params
			$defaults = [
				'cache' => false
			];
			$params = array_merge($defaults, $params);

			# Save fields
			if (empty($params['fields'])) {
				$this->build(400, "ERROR: Table `{$table}` does not have defined fields!");
			}

			if (isset($params['size'])) {
				$tables['uploads'][$table]['size'] = $params['size'];
				unset($params['size']);
			} else {
				$tables['uploads'][$table]['size'] = $globalImageSize;
			}

			$fields = $params['fields'];
			unset($params['fields']);

			# Contains
			$contain = [];
			foreach ($fields as $field) {
				if (preg_match('~^(.*)\?~ Uu', $field, $match)) {
					$contain[] = $match[1] . '.filename';
					$contain[] = $match[1] . '.path';
				}
			}

			# Find
			$model = !empty($params['model']) ? $params['model'] : Inflector::classify($table);
			$params['all'] = true;
			$params['limit'] = $params['cache'] ? (int) $params['cache'] : self::PAGINATION_LIMIT;
			$params['contain'] = $contain;
			$params['conditions']["{$model}.modified >"] = $this->time;
			$params['name'] = isset($params['name']) ? $params['name'] : $table;
			unset($params['cache']);

			# If it is the first page
			if ($manifest) {
				$params['limit'] = null;

			} else if ($page > 0) {
				$params['offset'] = ($page - 1) * self::PAGINATION_LIMIT;
			}

			# Max time
			if ($max !== null) {
				$params['conditions']["{$model}.modified <"] = $max;
			}

			# Get the records
			$this->{"Model{$model}"} = ClassRegistry::init($model);
			$records = $this->{"Model{$model}"}->find('all', $params);

			# Get the total size
			if ($page > 0) {
				$response['manifest']['weight']['text'] += strlen(serialize($records));
				$records = array_slice($records, 0, self::PAGINATION_LIMIT);
			}

			# Get the count
			$count = count($records);

			# If there might be more records, get the number of pages
			if ($manifest && $count == self::PAGINATION_LIMIT) {
				unset($params['limit']);
				$count = $this->{"Model{$model}"}->find('count', $params);

				# Update the response
				$pages = ceil($count * 1.0 / self::PAGINATION_LIMIT);
				$response['manifest']['pagination']['pages'] = max($response['manifest']['pagination']['pages'], $pages);
			}

			# Skip if no records have been found
			if (!$count) {
				continue;
			}

			# Update counters
			if ($manifest) {
				$response['manifest']['pagination']['count'] += $count;
			}

			# Generate output if there is one
			$output = [];
			foreach ($records as $i => $original) {
				$data = [];
				$record = reset($original);

				# Basic info
				$data['id'] = (int) $record['id'];
				$data['type'] = empty($record['is_deleted'])
					? ($record['created'] > $this->time ? 'INSERT' : 'UPDATE')
					: 'DELETE';

				# Send only selected fields
				$data['fields'] = new Object();
				if ($data['type'] != 'DELETE') {
					$data['fields'] = [];
					foreach ($fields as $tagName => $field) {

						# Tag name
						if (is_numeric($tagName)) {
							$tagName = $field;
						}

						# Uploads
						if (strstr($field, '?')) {
							list($upload, $query) = explode('?', $field, 2);
							$tagName = $tagName == $field ? strtolower($upload) : $tagName;

							# Handle requested uploads
							if ($manifest && !empty($uploads->{$model})) {
								foreach ($uploads->{$model} as $file) {
									if ($file->association == $upload) {

										# Get the path
										$path = WWW_ROOT . $original[$upload]['path'] . $original[$upload]['filename'];

										# Add to size
										$response['manifest']['weight']['binary'] += @filesize($path);
									}
								}
							}

							# Direct link
							if ($model === 'Upload' && $upload == 'file') {
								parse_str($query, $query);
								$data['fields'][$tagName] = $this->image($record[$upload], null, null, null, $query);

								# Image
							} else if (!empty($query) && isset($original[$upload])) {
								parse_str($query, $query);
								$source = $original[$upload];

								# Single image
								if (key($source) !== 0) {
									$data['fields'][$tagName] = $this->image($source, null, null, null, $query);

									# Multiple images
								} else {
									$itemTag = Inflector::singularize($tagName) . '_item';
									foreach ($source as $image) {
										if (!empty($image['filename'])) {
											$data['fields'][$tagName][$itemTag][] = $this->image($image, null, null, null, $query);
										}
									}
								}

								# Document
							} else {
								$data['fields'][$tagName] = $this->image($original[$upload]);
							}

							# Polyglot fields
						} else if (substr($field, -2) == '__') {
							foreach ($record["{$field}"] as $locale => $value) {
								$data['fields']["{$tagName}{$locale}"] = $value;
							}

							# GPS locations
						} else if ($tagName == 'gps_location') {
							$data['fields'][$tagName] = str_replace(" ", "", $record[$field]);

							# Normal
						} else {

							$value = $record[$field];

							# Handle field types
							$fieldInfo = $this->{"Model{$model}"}->schema($field);
							if ($fieldInfo)
								switch ($fieldInfo['type']) {
									case 'biginteger':
									case 'integer':
										$value = (int) $value;
										break;

									case 'boolean':
										$value = (bool) $value;
										break;
								}

							$data['fields'][$tagName] = $value;
						}
					}

					# If field is polyglot
					if (isset($record['is_translated'])) {
						foreach ($record['is_translated__'] as $locale => $value) {
							$data['fields']["is_translated__{$locale}"] = $value;
						}
					}

					# If item has activity state
					if (isset($record['is_active'])) {
						$data['fields']["is_active"] = $record['is_active'];
					}

					# If item has ordering field
					if (isset($record['ordering'])) {
						$data['fields']["ordering"] = (int) $record['ordering'];
					}

					# Modified ad created fields
					$data['fields']['modified'] = $record['modified'];
					$data['fields']['created'] = $record['created'];

					# Table specific
					switch ($table) {

						# Upload and Category model
						case 'uploads':
						case 'categories':
							$keys = array_keys($data['fields']);
							foreach ($keys as $i => $key) {

								# Model to table name
								if ($key == 'model') {
									$keys[$i] = 'table';
									$data['fields']['model'] = Inflector::tableize($data['fields']['model']);
								}
							}

							$data['fields'] = array_combine($keys, $data['fields']);

							# Additional tags for uploads
							if ($table == 'uploads') {
								$data['fields']['file'] = FULL_BASE_URL . $data['fields']['file'];

								# Apply custom size if provided.
								if (isset($data['fields']['model']) && !empty($tables['uploads'][$data['fields']['model']]['size'])) {
									$picParams['size'] = $tables['uploads'][$data['fields']['model']]['size'];
									$data['fields']['thumb'] = $this->image($data['fields']['file'], null, null, null, $picParams);
								}
							}

							break;
					}

					# Add to output
					$output[] = $data;

					# Skip deleted items that administrator does not yet have
				} else if ($record['created'] > $this->time) {
					if (!empty($response['changes']))
						$response['changes']--;
					unset($records[$i]);
					continue;
				}
			}

			$response['tables'][] = [
				'name'    => $params['name'],
				'count'   => count($records),
				'size'    => strlen(serialize($records)),
				'records' => $this->isXML() ? [ 'record' => $output ] : $output
			];
		}

		# Append weight
		if ($manifest) {
			$response['manifest']['weight']['total'] = $response['manifest']['weight']['text'] + $response['manifest']['weight']['binary'];
		}

		return $this->executeModificationRequests($response);
	}

	# ~ Append array to xml	 - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function append($array, &$xml, $prev = null) {

		foreach ($array as $key => $value) {

			# Subarray
			if (is_array($value) || is_object($value)) {
				$value = (array) $value;

				# Non-numeric
				if (!is_numeric($key)) {
					if (!is_numeric(key($value))) {
						$subnode = $xml->addChild("{$key}");
					} else {
						$subnode = $xml;
					}

					$this->append($value, $subnode, $key);

					# Numeric
				} else {
					$subnode = $xml->addChild("{$prev}");
					$this->append($value, $subnode, $key);
				}

				# Value
			} else {

				# Numeric indices
				if (is_numeric($key)) {
					$key = $prev;
				}

				# Boolean
				if (is_bool($value) && $key != 'generated') {
					$value = (int) $value;
				}

				# With CDATA
				if ($value != '' && !preg_match("~^[ 0-9:.,-]+$~", $value)) {
					$child = $xml->addChild($key);
					$node = dom_import_simplexml($child);
					$node->appendChild($node->ownerDocument->createCDATASection($value));

					# Plain
				} else {
					$xml->addChild($key, $value);
				}
			}
		}
	}

	# ~ Modify a table from synchronization	 - - - - - - - - - - - - - - - - - - - #
	public function addModificationRequest($tableName, $modification) {
		$this->modificationRequests[] = new ModificationRequest($tableName, $modification);
	}

	# ~ Execute modification requests  - - - - - - - - - - - - - - - - - - - - - - #
	public function executeModificationRequests($response) {

		# Make sure we have any requests
		if (!empty($this->modificationRequests) && !empty($response['tables'])) {
			foreach ($response['tables'] as $t => $table) {

				# Iterate over modification requests
				foreach ($this->modificationRequests as $modification) {
					if ($table['name'] == $modification->getTableName()) {

						# Modify all records in the table
						foreach ($table['records'] as $r => $record) {
							$response['tables'][$t]['records'][$r]['fields'] = $modification->modify($response['tables'][$t]['records'][$r]['fields']);
						}
					}
				}
			}
		}

		return $response;
	}

	# ~ Manicure link  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function image($image, $width = null, $height = null, $zc = null, $params = []) {

		# If empty image
		if (empty($image) || (is_array($image) && empty($image['filename']) && empty($image['file']))) {
			return null;
		}

		# Image to url
		if (is_array($image)) {
			if (!empty($image['file'])) {
				$image = $image['file'];
			} else {
				$image = $image['path'] . $image['filename'];
			}
		}

		# Create full URL
		$params['src'] = FULL_BASE_URL . '/' . trim($image, '/ ');

		# Default size
		if (empty($params['size']) && (!empty($width) || !empty($height))) {
			$params['size'] = $width . 'x' . $height;
		}

		# Zrop
		if (!empty($zc))
			$params['zc'] = $zc;

		# Return original image
		if (array_keys($params) == [ 'src' ]) {
			return $params['src'];
		}

		# Build query
		$query = http_build_query($params);

		# Zoom crop
		if ($zc !== null) {
			if (is_bool($zc)) {
				$zc = (int) $zc;
			}

			$query .= "&zc={$zc}";
		}

		# Select manicure link
		$manicure = !Configure::read('debug') || env('REMOTE_ADDR') != '127.0.0.1'
			? 'http://manicure.services.intellex.rs/'
			: 'http://manicure/';

		return $manicure . '?' . $query;
	}

	# ~ Manicure gallery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function gallery($images, $width = null, $height = null, $zc = null, $params = []) {
		$gallery = [];

		foreach ($images as $image) {
			$gallery['gallery_image'][] = [
				'thumb'    => $this->image($image, 200, 200),
				'fullsize' => $this->image($image)
			];
		}

		return $gallery;
	}

	# ~ Youtube id - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function youtubeId($url) {
		return preg_match('~^.*http://((www\.)?youtu(\.be|be.com)/(embed/|watch\?v=)?)(?P<id>[a-z0-9]+).*$~ i', $url, $match)
			? $match['id']
			: false;
	}

	# ~ Time formating - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function time($time, $format = null, $locale = null) {
		setDefault($locale, (Configure::read('Config.language')));

		# Default formating
		if ($format === null) {
			$format = !preg_match('~^\d{4}-\d{2}-\d{2}$~', $time)
				? '%e. %B %Y. %H:%M:%S'
				: '%e. %B %Y.';
		}

		return preg_replace('~(\b)(ju[ln])i(\b)~', '$1$2$3', strftime($format, strtotime($time)));
	}

	# ~ Change to ISO datetime format  - - - - - - - - - - - - - - - - - - - - - - #
	public function isoTime($time = null) {

		# Default time to supplied in params
		if ($time === null) {
			$time = isset($this->Controller->request['named']['time']) ? $this->Controller->request['named']['time'] : '19700101000000';
		}

		# Validate input
		if (preg_match('~(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})~', preg_replace('~[^0-9]~', '', $time), $match)) {
			return "{$match[1]}-{$match[2]}-{$match[3]} {$match[4]}:{$match[5]}:{$match[6]}";
		}

		return null;
	}

	# ~ Sets the format of the response	 - - - - - - - - - - - - - - - - - - - - - #
	public function setFormat($format) {
		$format = strtolower($format);
		if (in_array($format, $this->allowedFormats)) {
			$this->format = $format;
		}
	}

	# ~ Checks if the format is XML	 - - - - - - - - - - - - - - - - - - - - - - - #
	public function isXML() {
		return $this->format == 'xml';
	}

	# ~ Checks if the format is JSON - - - - - - - - - - - - - - - - - - - - - - - #
	public function isJSON() {
		return $this->format == 'json';
	}

	# ~ Force download of a file - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function downloadHeaders($filename, $encoding = null, $length = null) {
		$headers = [
			'Pragma'              => 'public',
			'Expires'             => '0',
			'Cache-Control'       => 'must-revalidate, post-check=0, pre-check=0',
			'Content-Disposition' => "attachement; filename={$filename}",
		];

		if ($length)
			$headers['Content-Length'] = $length;
		if ($encoding)
			$headers['Content-Transfer-Encoding'] = $encoding;

		return $headers;
	}

}

class ModificationRequest {

	private $tableName;
	private $modification;

	function ModificationRequest($tableName, $modification) {
		$this->modification = $modification;
		$this->tableName = $tableName;
	}

	function modify($data) {
		$func = $this->modification;
		return $func($data);
	}

	function getTableName() {
		return $this->tableName;
	}

}

# Soft API exception
class APISoftException extends Exception {
	public $text, $code, $additional;

	public function __construct($text = null, $code = 200, $additional = []) {
		$this->text = $text;
		$this->code = $code;
		$this->additional = $additional;
	}

}

# Hard API exception
class APIHardException extends Exception {
	public $code, $text;

	public function __construct($code, $text = null) {
		$this->code = $code;
		$this->text = $text;
	}

}

class InvalidFieldsException extends APISoftException {

	var $errors = [];

	public function __construct($message, $errors) {
		parent::__construct($message);
		$this->errors = $errors;
	}

	public function getErrors() {
		return $this->errors;
	}
}
