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