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 | } |
---|