1<?php
2/**
3 * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
4 * Atom feeds for websites that don't have one.
5 *
6 * For the full license information, please view the UNLICENSE file distributed
7 * with this source code.
8 *
9 * @package	Core
10 * @license	http://unlicense.org/ UNLICENSE
11 * @link	https://github.com/rss-bridge/rss-bridge
12 */
13
14/**
15 * An abstract class for bridges
16 *
17 * This class implements {@see BridgeInterface} with most common functions in
18 * order to reduce code duplication. Bridges should inherit from this class
19 * instead of implementing the interface manually.
20 *
21 * @todo Move constants to the interface (this is supported by PHP)
22 * @todo Change visibility of constants to protected
23 * @todo Return `self` on more functions to allow chaining
24 * @todo Add specification for PARAMETERS ()
25 * @todo Add specification for $items
26 */
27abstract class BridgeAbstract implements BridgeInterface {
28
29	/**
30	 * Name of the bridge
31	 *
32	 * Use {@see BridgeAbstract::getName()} to read this parameter
33	 */
34	const NAME = 'Unnamed bridge';
35
36	/**
37	 * URI to the site the bridge is intended to be used for.
38	 *
39	 * Use {@see BridgeAbstract::getURI()} to read this parameter
40	 */
41	const URI = '';
42
43	/**
44	 * A brief description of what the bridge can do
45	 *
46	 * Use {@see BridgeAbstract::getDescription()} to read this parameter
47	 */
48	const DESCRIPTION = 'No description provided';
49
50	/**
51	 * The name of the maintainer. Multiple maintainers can be separated by comma
52	 *
53	 * Use {@see BridgeAbstract::getMaintainer()} to read this parameter
54	 */
55	const MAINTAINER = 'No maintainer';
56
57	/**
58	 * The default cache timeout for the bridge
59	 *
60	 * Use {@see BridgeAbstract::getCacheTimeout()} to read this parameter
61	 */
62	const CACHE_TIMEOUT = 3600;
63
64	/**
65	 * Configuration for the bridge
66	 *
67	 * Use {@see BridgeAbstract::getConfiguration()} to read this parameter
68	 */
69	const CONFIGURATION = array();
70
71	/**
72	 * Parameters for the bridge
73	 *
74	 * Use {@see BridgeAbstract::getParameters()} to read this parameter
75	 */
76	const PARAMETERS = array();
77
78	/**
79	 * Holds the list of items collected by the bridge
80	 *
81	 * Items must be collected by {@see BridgeInterface::collectData()}
82	 *
83	 * Use {@see BridgeAbstract::getItems()} to access items.
84	 *
85	 * @var array
86	 */
87	protected $items = array();
88
89	/**
90	 * Holds the list of input parameters used by the bridge
91	 *
92	 * Do not access this parameter directly!
93	 * Use {@see BridgeAbstract::setInputs()} and {@see BridgeAbstract::getInput()} instead!
94	 *
95	 * @var array
96	 */
97	protected $inputs = array();
98
99	/**
100	 * Holds the name of the queried context
101	 *
102	 * @var string
103	 */
104	protected $queriedContext = '';
105
106	/** {@inheritdoc} */
107	public function getItems(){
108		return $this->items;
109	}
110
111	/**
112	 * Sets the input values for a given context.
113	 *
114	 * @param array $inputs Associative array of inputs
115	 * @param string $queriedContext The context name
116	 * @return void
117	 */
118	protected function setInputs(array $inputs, $queriedContext){
119		// Import and assign all inputs to their context
120		foreach($inputs as $name => $value) {
121			foreach(static::PARAMETERS as $context => $set) {
122				if(array_key_exists($name, static::PARAMETERS[$context])) {
123					$this->inputs[$context][$name]['value'] = $value;
124				}
125			}
126		}
127
128		// Apply default values to missing data
129		$contexts = array($queriedContext);
130		if(array_key_exists('global', static::PARAMETERS)) {
131			$contexts[] = 'global';
132		}
133
134		foreach($contexts as $context) {
135			foreach(static::PARAMETERS[$context] as $name => $properties) {
136				if(isset($this->inputs[$context][$name]['value'])) {
137					continue;
138				}
139
140				$type = isset($properties['type']) ? $properties['type'] : 'text';
141
142				switch($type) {
143				case 'checkbox':
144					if(!isset($properties['defaultValue'])) {
145						$this->inputs[$context][$name]['value'] = false;
146					} else {
147						$this->inputs[$context][$name]['value'] = $properties['defaultValue'];
148					}
149					break;
150				case 'list':
151					if(!isset($properties['defaultValue'])) {
152						$firstItem = reset($properties['values']);
153						if(is_array($firstItem)) {
154							$firstItem = reset($firstItem);
155						}
156						$this->inputs[$context][$name]['value'] = $firstItem;
157					} else {
158						$this->inputs[$context][$name]['value'] = $properties['defaultValue'];
159					}
160					break;
161				default:
162					if(isset($properties['defaultValue'])) {
163						$this->inputs[$context][$name]['value'] = $properties['defaultValue'];
164					}
165					break;
166				}
167			}
168		}
169
170		// Copy global parameter values to the guessed context
171		if(array_key_exists('global', static::PARAMETERS)) {
172			foreach(static::PARAMETERS['global'] as $name => $properties) {
173				if(isset($inputs[$name])) {
174					$value = $inputs[$name];
175				} elseif(isset($properties['defaultValue'])) {
176					$value = $properties['defaultValue'];
177				} else {
178					continue;
179				}
180				$this->inputs[$queriedContext][$name]['value'] = $value;
181			}
182		}
183
184		// Only keep guessed context parameters values
185		if(isset($this->inputs[$queriedContext])) {
186			$this->inputs = array($queriedContext => $this->inputs[$queriedContext]);
187		} else {
188			$this->inputs = array();
189		}
190	}
191
192	/**
193	 * Set inputs for the bridge
194	 *
195	 * Returns errors and aborts execution if the provided input parameters are
196	 * invalid.
197	 *
198	 * @param array List of input parameters. Each element in this list must
199	 * relate to an item in {@see BridgeAbstract::PARAMETERS}
200	 * @return void
201	 */
202	public function setDatas(array $inputs){
203
204		if(isset($inputs['context'])) { // Context hinting (optional)
205			$this->queriedContext = $inputs['context'];
206			unset($inputs['context']);
207		}
208
209		if(empty(static::PARAMETERS)) {
210
211			if(!empty($inputs)) {
212				returnClientError('Invalid parameters value(s)');
213			}
214
215			return;
216
217		}
218
219		$validator = new ParameterValidator();
220
221		if(!$validator->validateData($inputs, static::PARAMETERS)) {
222			$parameters = array_map(
223				function($i){ return $i['name']; }, // Just display parameter names
224				$validator->getInvalidParameters()
225			);
226
227			returnClientError(
228				'Invalid parameters value(s): '
229				. implode(', ', $parameters)
230			);
231		}
232
233		// Guess the context from input data
234		if(empty($this->queriedContext)) {
235			$this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS);
236		}
237
238		if(is_null($this->queriedContext)) {
239			returnClientError('Required parameter(s) missing');
240		} elseif($this->queriedContext === false) {
241			returnClientError('Mixed context parameters');
242		}
243
244		$this->setInputs($inputs, $this->queriedContext);
245
246	}
247
248	/**
249	 * Loads configuration for the bridge
250	 *
251	 * Returns errors and aborts execution if the provided configuration is
252	 * invalid.
253	 *
254	 * @return void
255	 */
256	public function loadConfiguration() {
257		foreach(static::CONFIGURATION as $optionName => $optionValue) {
258
259			$configurationOption = Configuration::getConfig(get_class($this), $optionName);
260
261			if($configurationOption !== null) {
262				$this->configuration[$optionName] = $configurationOption;
263				continue;
264			}
265
266			if(isset($optionValue['required']) && $optionValue['required'] === true) {
267				returnServerError(
268					'Missing configuration option: '
269					. $optionName
270				);
271			} elseif(isset($optionValue['defaultValue'])) {
272				$this->configuration[$optionName] = $optionValue['defaultValue'];
273			}
274
275		}
276	}
277
278	/**
279	 * Returns the value for the provided input
280	 *
281	 * @param string $input The input name
282	 * @return mixed|null The input value or null if the input is not defined
283	 */
284	protected function getInput($input){
285		if(!isset($this->inputs[$this->queriedContext][$input]['value'])) {
286			return null;
287		}
288		return $this->inputs[$this->queriedContext][$input]['value'];
289	}
290
291	/**
292	 * Returns the value for the selected configuration
293	 *
294	 * @param string $input The option name
295	 * @return mixed|null The option value or null if the input is not defined
296	 */
297	public function getOption($name){
298		if(!isset($this->configuration[$name])) {
299			return null;
300		}
301		return $this->configuration[$name];
302	}
303
304	/** {@inheritdoc} */
305	public function getDescription(){
306		return static::DESCRIPTION;
307	}
308
309	/** {@inheritdoc} */
310	public function getMaintainer(){
311		return static::MAINTAINER;
312	}
313
314	/** {@inheritdoc} */
315	public function getName(){
316		return static::NAME;
317	}
318
319	/** {@inheritdoc} */
320	public function getIcon(){
321		return static::URI . '/favicon.ico';
322	}
323
324	/** {@inheritdoc} */
325	public function getConfiguration(){
326		return static::CONFIGURATION;
327	}
328
329	/** {@inheritdoc} */
330	public function getParameters(){
331		return static::PARAMETERS;
332	}
333
334	/** {@inheritdoc} */
335	public function getURI(){
336		return static::URI;
337	}
338
339	/** {@inheritdoc} */
340	public function getCacheTimeout(){
341		return static::CACHE_TIMEOUT;
342	}
343
344	/** {@inheritdoc} */
345	public function detectParameters($url){
346		$regex = '/^(https?:\/\/)?(www\.)?(.+?)(\/)?$/';
347		if(empty(static::PARAMETERS)
348		&& preg_match($regex, $url, $urlMatches) > 0
349		&& preg_match($regex, static::URI, $bridgeUriMatches) > 0
350		&& $urlMatches[3] === $bridgeUriMatches[3]) {
351			return array();
352		} else {
353			return null;
354		}
355	}
356}
357