[177a560] | 1 | <?php |
---|
| 2 | |
---|
| 3 | /** |
---|
| 4 | * @file |
---|
| 5 | * Definition of FeedsSourceInterface and FeedsSource class. |
---|
| 6 | */ |
---|
| 7 | |
---|
| 8 | /** |
---|
| 9 | * Declares an interface for a class that defines default values and form |
---|
| 10 | * descriptions for a FeedSource. |
---|
| 11 | */ |
---|
| 12 | interface FeedsSourceInterface { |
---|
| 13 | |
---|
| 14 | /** |
---|
| 15 | * Crutch: for ease of use, we implement FeedsSourceInterface for every |
---|
| 16 | * plugin, but then we need to have a handle which plugin actually implements |
---|
| 17 | * a source. |
---|
| 18 | * |
---|
| 19 | * @see FeedsPlugin class. |
---|
| 20 | * |
---|
| 21 | * @return |
---|
| 22 | * TRUE if a plugin handles source specific configuration, FALSE otherwise. |
---|
| 23 | */ |
---|
| 24 | public function hasSourceConfig(); |
---|
| 25 | |
---|
| 26 | /** |
---|
| 27 | * Return an associative array of default values. |
---|
| 28 | */ |
---|
| 29 | public function sourceDefaults(); |
---|
| 30 | |
---|
| 31 | /** |
---|
| 32 | * Return a Form API form array that defines a form configuring values. Keys |
---|
| 33 | * correspond to the keys of the return value of sourceDefaults(). |
---|
| 34 | */ |
---|
| 35 | public function sourceForm($source_config); |
---|
| 36 | |
---|
| 37 | /** |
---|
| 38 | * Validate user entered values submitted by sourceForm(). |
---|
| 39 | */ |
---|
| 40 | public function sourceFormValidate(&$source_config); |
---|
| 41 | |
---|
| 42 | /** |
---|
| 43 | * A source is being saved. |
---|
| 44 | */ |
---|
| 45 | public function sourceSave(FeedsSource $source); |
---|
| 46 | |
---|
| 47 | /** |
---|
| 48 | * A source is being deleted. |
---|
| 49 | */ |
---|
| 50 | public function sourceDelete(FeedsSource $source); |
---|
| 51 | } |
---|
| 52 | |
---|
| 53 | /** |
---|
| 54 | * This class encapsulates a source of a feed. It stores where the feed can be |
---|
| 55 | * found and how to import it. |
---|
| 56 | * |
---|
| 57 | * Information on how to import a feed is encapsulated in a FeedsImporter object |
---|
| 58 | * which is identified by the common id of the FeedsSource and the |
---|
| 59 | * FeedsImporter. More than one FeedsSource can use the same FeedsImporter |
---|
| 60 | * therefore a FeedsImporter never holds a pointer to a FeedsSource object, nor |
---|
| 61 | * does it hold any other information for a particular FeedsSource object. |
---|
| 62 | * |
---|
| 63 | * Classes extending FeedsPlugin can implement a sourceForm to expose |
---|
| 64 | * configuration for a FeedsSource object. This is for instance how FeedsFetcher |
---|
| 65 | * exposes a text field for a feed URL or how FeedsCSVParser exposes a select |
---|
| 66 | * field for choosing between colon or semicolon delimiters. |
---|
| 67 | * |
---|
| 68 | * It is important that a FeedsPlugin does not directly hold information about |
---|
| 69 | * a source but leave all storage up to FeedsSource. An instance of a |
---|
| 70 | * FeedsPlugin class only exists once per FeedsImporter configuration, while an |
---|
| 71 | * instance of a FeedsSource class exists once per feed_nid to be imported. |
---|
| 72 | * |
---|
| 73 | * As with FeedsImporter, the idea with FeedsSource is that it can be used |
---|
| 74 | * without actually saving the object to the database. |
---|
| 75 | */ |
---|
| 76 | class FeedsSource extends FeedsConfigurable { |
---|
| 77 | |
---|
| 78 | // Contains the node id of the feed this source info object is attached to. |
---|
| 79 | // Equals 0 if not attached to any node - i. e. if used on a |
---|
| 80 | // standalone import form within Feeds or by other API users. |
---|
| 81 | protected $feed_nid; |
---|
| 82 | |
---|
| 83 | // The FeedsImporter object that this source is expected to be used with. |
---|
| 84 | protected $importer; |
---|
| 85 | |
---|
| 86 | // A FeedsBatch object. NULL if there is no active batch. |
---|
| 87 | protected $batch; |
---|
| 88 | |
---|
| 89 | /** |
---|
| 90 | * Instantiate a unique object per class/id/feed_nid. Don't use |
---|
| 91 | * directly, use feeds_source() instead. |
---|
| 92 | */ |
---|
| 93 | public static function instance($importer_id, $feed_nid) { |
---|
| 94 | $class = variable_get('feeds_source_class', 'FeedsSource'); |
---|
| 95 | static $instances = array(); |
---|
| 96 | if (!isset($instances[$class][$importer_id][$feed_nid])) { |
---|
| 97 | $instances[$class][$importer_id][$feed_nid] = new $class($importer_id, $feed_nid); |
---|
| 98 | } |
---|
| 99 | return $instances[$class][$importer_id][$feed_nid]; |
---|
| 100 | } |
---|
| 101 | |
---|
| 102 | /** |
---|
| 103 | * Constructor. |
---|
| 104 | */ |
---|
| 105 | protected function __construct($importer_id, $feed_nid) { |
---|
| 106 | $this->feed_nid = $feed_nid; |
---|
| 107 | $this->importer = feeds_importer($importer_id); |
---|
| 108 | parent::__construct($importer_id); |
---|
| 109 | $this->load(); |
---|
| 110 | } |
---|
| 111 | |
---|
| 112 | /** |
---|
| 113 | * Preview = fetch and parse a feed. |
---|
| 114 | * |
---|
| 115 | * @return |
---|
| 116 | * FeedsImportBatch object, fetched and parsed. |
---|
| 117 | * |
---|
| 118 | * @throws |
---|
| 119 | * Throws Exception if an error occurs when fetching or parsing. |
---|
| 120 | */ |
---|
| 121 | public function preview() { |
---|
| 122 | $this->batch = $this->importer->fetcher->fetch($this); |
---|
| 123 | $this->importer->parser->parse($this->batch, $this); |
---|
| 124 | module_invoke_all('feeds_after_parse', $this->importer, $this); |
---|
| 125 | $batch = $this->batch; |
---|
| 126 | unset($this->batch); |
---|
| 127 | return $batch; |
---|
| 128 | } |
---|
| 129 | |
---|
| 130 | /** |
---|
| 131 | * Import a feed: execute fetching, parsing and processing stage. |
---|
| 132 | * |
---|
| 133 | * @return |
---|
| 134 | * FEEDS_BATCH_COMPLETE if the import process finished. A decimal between |
---|
| 135 | * 0.0 and 0.9 periodic if import is still in progress. |
---|
| 136 | * |
---|
| 137 | * @throws |
---|
| 138 | * Throws Exception if an error occurs when importing. |
---|
| 139 | */ |
---|
| 140 | public function import() { |
---|
| 141 | try { |
---|
| 142 | if (!$this->batch || !($this->batch instanceof FeedsImportBatch)) { |
---|
| 143 | $this->batch = $this->importer->fetcher->fetch($this); |
---|
| 144 | $this->importer->parser->parse($this->batch, $this); |
---|
| 145 | module_invoke_all('feeds_after_parse', $this->importer, $this); |
---|
| 146 | } |
---|
| 147 | $this->importer->processor->process($this->batch, $this); |
---|
| 148 | $result = $this->batch->getProgress(); |
---|
| 149 | if ($result == FEEDS_BATCH_COMPLETE) { |
---|
| 150 | unset($this->batch); |
---|
| 151 | module_invoke_all('feeds_after_import', $this->importer, $this); |
---|
| 152 | } |
---|
| 153 | } |
---|
| 154 | catch (Exception $e) { |
---|
| 155 | unset($this->batch); |
---|
| 156 | $this->save(); |
---|
| 157 | throw $e; |
---|
| 158 | } |
---|
| 159 | $this->save(); |
---|
| 160 | return $result; |
---|
| 161 | } |
---|
| 162 | |
---|
| 163 | /** |
---|
| 164 | * Remove all items from a feed. |
---|
| 165 | * |
---|
| 166 | * @return |
---|
| 167 | * FEEDS_BATCH_COMPLETE if the clearing process finished. A decimal between |
---|
| 168 | * 0.0 and 0.9 periodic if clearing is still in progress. |
---|
| 169 | * |
---|
| 170 | * @throws |
---|
| 171 | * Throws Exception if an error occurs when clearing. |
---|
| 172 | */ |
---|
| 173 | public function clear() { |
---|
| 174 | try { |
---|
| 175 | $this->importer->fetcher->clear($this); |
---|
| 176 | $this->importer->parser->clear($this); |
---|
| 177 | if (!$this->batch || !($this->batch instanceof FeedsClearBatch)) { |
---|
| 178 | $this->batch = new FeedsClearBatch(); |
---|
| 179 | } |
---|
| 180 | $this->importer->processor->clear($this->batch, $this); |
---|
| 181 | $result = $this->batch->getProgress(); |
---|
| 182 | if ($result == FEEDS_BATCH_COMPLETE) { |
---|
| 183 | unset($this->batch); |
---|
| 184 | module_invoke_all('feeds_after_clear', $this->importer, $this); |
---|
| 185 | } |
---|
| 186 | } |
---|
| 187 | catch (Exception $e) { |
---|
| 188 | unset($this->batch); |
---|
| 189 | $this->save(); |
---|
| 190 | throw $e; |
---|
| 191 | } |
---|
| 192 | $this->save(); |
---|
| 193 | return $result; |
---|
| 194 | } |
---|
| 195 | |
---|
| 196 | /** |
---|
| 197 | * Schedule this source. |
---|
| 198 | */ |
---|
| 199 | public function schedule() { |
---|
| 200 | // Check whether any fetcher is overriding the import period. |
---|
| 201 | $period = $this->importer->config['import_period']; |
---|
| 202 | $fetcher_period = $this->importer->fetcher->importPeriod($this); |
---|
| 203 | if (is_numeric($fetcher_period)) { |
---|
| 204 | $period = $fetcher_period; |
---|
| 205 | } |
---|
| 206 | $job = array( |
---|
| 207 | 'callback' => 'feeds_source_import', |
---|
| 208 | 'type' => $this->id, |
---|
| 209 | 'id' => $this->feed_nid, |
---|
| 210 | // Schedule as soon as possible if a batch is active. |
---|
| 211 | 'period' => !empty($this->batch) ? 0 : $period, |
---|
| 212 | 'periodic' => TRUE, |
---|
| 213 | ); |
---|
| 214 | if ($job['period'] != FEEDS_SCHEDULE_NEVER) { |
---|
| 215 | job_scheduler()->set($job); |
---|
| 216 | } |
---|
| 217 | else { |
---|
| 218 | job_scheduler()->remove($job); |
---|
| 219 | } |
---|
| 220 | } |
---|
| 221 | |
---|
| 222 | /** |
---|
| 223 | * Save configuration. |
---|
| 224 | */ |
---|
| 225 | public function save() { |
---|
| 226 | $config = $this->getConfig(); |
---|
| 227 | // Alert implementers of FeedsSourceInterface to the fact that we're saving. |
---|
| 228 | foreach ($this->importer->plugin_types as $type) { |
---|
| 229 | $this->importer->$type->sourceSave($this); |
---|
| 230 | } |
---|
| 231 | // Store the source property of the fetcher in a separate column so that we |
---|
| 232 | // can do fast lookups on it. |
---|
| 233 | $source = ''; |
---|
| 234 | if (isset($config[get_class($this->importer->fetcher)]['source'])) { |
---|
| 235 | $source = $config[get_class($this->importer->fetcher)]['source']; |
---|
| 236 | } |
---|
| 237 | $object = array( |
---|
| 238 | 'id' => $this->id, |
---|
| 239 | 'feed_nid' => $this->feed_nid, |
---|
| 240 | 'config' => $config, |
---|
| 241 | 'source' => $source, |
---|
| 242 | 'batch' => isset($this->batch) ? $this->batch : FALSE, |
---|
| 243 | ); |
---|
| 244 | if (db_result(db_query_range("SELECT 1 FROM {feeds_source} WHERE id = '%s' AND feed_nid = %d", $this->id, $this->feed_nid, 0, 1))) { |
---|
| 245 | drupal_write_record('feeds_source', $object, array('id', 'feed_nid')); |
---|
| 246 | } |
---|
| 247 | else { |
---|
| 248 | drupal_write_record('feeds_source', $object); |
---|
| 249 | } |
---|
| 250 | } |
---|
| 251 | |
---|
| 252 | /** |
---|
| 253 | * Load configuration and unpack. |
---|
| 254 | * |
---|
| 255 | * @todo Patch CTools to move constants from export.inc to ctools.module. |
---|
| 256 | */ |
---|
| 257 | public function load() { |
---|
| 258 | if ($record = db_fetch_object(db_query("SELECT config, batch FROM {feeds_source} WHERE id = '%s' AND feed_nid = %d", $this->id, $this->feed_nid))) { |
---|
| 259 | // While FeedsSource cannot be exported, we still use CTool's export.inc |
---|
| 260 | // export definitions. |
---|
| 261 | ctools_include('export'); |
---|
| 262 | $this->export_type = EXPORT_IN_DATABASE; |
---|
| 263 | $this->config = unserialize($record->config); |
---|
| 264 | $this->batch = unserialize($record->batch); |
---|
| 265 | } |
---|
| 266 | } |
---|
| 267 | |
---|
| 268 | /** |
---|
| 269 | * Delete configuration. Removes configuration information |
---|
| 270 | * from database, does not delete configuration itself. |
---|
| 271 | */ |
---|
| 272 | public function delete() { |
---|
| 273 | // Alert implementers of FeedsSourceInterface to the fact that we're |
---|
| 274 | // deleting. |
---|
| 275 | foreach ($this->importer->plugin_types as $type) { |
---|
| 276 | $this->importer->$type->sourceDelete($this); |
---|
| 277 | } |
---|
| 278 | db_query("DELETE FROM {feeds_source} WHERE id = '%s' AND feed_nid = %d", $this->id, $this->feed_nid); |
---|
| 279 | // Remove from schedule. |
---|
| 280 | $job = array( |
---|
| 281 | 'callback' => 'feeds_source_import', |
---|
| 282 | 'type' => $this->id, |
---|
| 283 | 'id' => $this->feed_nid, |
---|
| 284 | ); |
---|
| 285 | job_scheduler()->remove($job); |
---|
| 286 | } |
---|
| 287 | |
---|
| 288 | /** |
---|
| 289 | * Only return source if configuration is persistent and valid. |
---|
| 290 | * |
---|
| 291 | * @see FeedsConfigurable::existing(). |
---|
| 292 | */ |
---|
| 293 | public function existing() { |
---|
| 294 | // If there is no feed nid given, there must be no content type specified. |
---|
| 295 | // If there is a feed nid given, there must be a content type specified. |
---|
| 296 | // Ensure that importer is persistent (= defined in code or DB). |
---|
| 297 | // Ensure that source is persistent (= defined in DB). |
---|
| 298 | if ((empty($this->feed_nid) && empty($this->importer->config['content_type'])) || |
---|
| 299 | (!empty($this->feed_nid) && !empty($this->importer->config['content_type']))) { |
---|
| 300 | $this->importer->existing(); |
---|
| 301 | return parent::existing(); |
---|
| 302 | } |
---|
| 303 | throw new FeedsNotExistingException(t('Source configuration not valid.')); |
---|
| 304 | } |
---|
| 305 | |
---|
| 306 | /** |
---|
| 307 | * Convenience function. Returns the configuration for a specific class. |
---|
| 308 | * |
---|
| 309 | * @param FeedsSourceInterface $client |
---|
| 310 | * An object that is an implementer of FeedsSourceInterface. |
---|
| 311 | * |
---|
| 312 | * @return |
---|
| 313 | * An array stored for $client. |
---|
| 314 | */ |
---|
| 315 | public function getConfigFor(FeedsSourceInterface $client) { |
---|
| 316 | $class = get_class($client); |
---|
| 317 | return isset($this->config[$class]) ? $this->config[$class] : $client->sourceDefaults(); |
---|
| 318 | } |
---|
| 319 | |
---|
| 320 | /** |
---|
| 321 | * Return defaults for feed configuration. |
---|
| 322 | */ |
---|
| 323 | public function configDefaults() { |
---|
| 324 | // Collect information from plugins. |
---|
| 325 | $defaults = array(); |
---|
| 326 | foreach ($this->importer->plugin_types as $type) { |
---|
| 327 | if ($this->importer->$type->hasSourceConfig()) { |
---|
| 328 | $defaults[get_class($this->importer->$type)] = $this->importer->$type->sourceDefaults(); |
---|
| 329 | } |
---|
| 330 | } |
---|
| 331 | return $defaults; |
---|
| 332 | } |
---|
| 333 | |
---|
| 334 | /** |
---|
| 335 | * Override parent::configForm(). |
---|
| 336 | */ |
---|
| 337 | public function configForm(&$form_state) { |
---|
| 338 | // Collect information from plugins. |
---|
| 339 | $form = array(); |
---|
| 340 | foreach ($this->importer->plugin_types as $type) { |
---|
| 341 | if ($this->importer->$type->hasSourceConfig()) { |
---|
| 342 | $class = get_class($this->importer->$type); |
---|
| 343 | $form[$class] = $this->importer->$type->sourceForm($this->config[$class]); |
---|
| 344 | $form[$class]['#tree'] = TRUE; |
---|
| 345 | } |
---|
| 346 | } |
---|
| 347 | return $form; |
---|
| 348 | } |
---|
| 349 | |
---|
| 350 | /** |
---|
| 351 | * Override parent::configFormValidate(). |
---|
| 352 | */ |
---|
| 353 | public function configFormValidate(&$values) { |
---|
| 354 | foreach ($this->importer->plugin_types as $type) { |
---|
| 355 | $class = get_class($this->importer->$type); |
---|
| 356 | if (isset($values[$class]) && $this->importer->$type->hasSourceConfig()) { |
---|
| 357 | $this->importer->$type->sourceFormValidate($values[$class]); |
---|
| 358 | } |
---|
| 359 | } |
---|
| 360 | } |
---|
| 361 | } |
---|