1 | <?php |
---|
2 | |
---|
3 | /** |
---|
4 | * @file |
---|
5 | * Class definition of FeedsNodeProcessor. |
---|
6 | */ |
---|
7 | |
---|
8 | // Create or delete FEEDS_NODE_BATCH_SIZE at a time. |
---|
9 | define('FEEDS_NODE_BATCH_SIZE', 50); |
---|
10 | |
---|
11 | // Deprecated. Use FEEDS_SKIPE_EXISTING, FEEDS_REPLACE_EXISTNG, |
---|
12 | // FEEDS_UPDATE_EXISTING instead. |
---|
13 | define('FEEDS_NODE_SKIP_EXISTING', 0); |
---|
14 | define('FEEDS_NODE_REPLACE_EXISTING', 1); |
---|
15 | define('FEEDS_NODE_UPDATE_EXISTING', 2); |
---|
16 | |
---|
17 | /** |
---|
18 | * Creates nodes from feed items. |
---|
19 | */ |
---|
20 | class FeedsNodeProcessor extends FeedsProcessor { |
---|
21 | |
---|
22 | /** |
---|
23 | * Implementation of FeedsProcessor::process(). |
---|
24 | */ |
---|
25 | public function process(FeedsImportBatch $batch, FeedsSource $source) { |
---|
26 | |
---|
27 | // Keep track of processed items in this pass, set total number of items. |
---|
28 | $processed = 0; |
---|
29 | $batch_size = variable_get('feeds_node_batch_size', FEEDS_NODE_BATCH_SIZE); |
---|
30 | if (!$batch->getTotal(FEEDS_PROCESSING)) { |
---|
31 | $batch->setTotal(FEEDS_PROCESSING, count($batch->items)); |
---|
32 | } |
---|
33 | |
---|
34 | while ($item = $batch->shiftItem()) { |
---|
35 | |
---|
36 | // Create/update if item does not exist or update existing is enabled. |
---|
37 | if (!($nid = $this->existingItemId($batch, $source)) || ($this->config['update_existing'] != FEEDS_SKIP_EXISTING)) { |
---|
38 | // Only proceed if item has actually changed. |
---|
39 | $hash = $this->hash($item); |
---|
40 | if (!empty($nid) && $hash == $this->getHash($nid)) { |
---|
41 | continue; |
---|
42 | } |
---|
43 | |
---|
44 | $node = $this->buildNode($nid, $source->feed_nid); |
---|
45 | $node->feeds_node_item->hash = $hash; |
---|
46 | |
---|
47 | // Map and save node. If errors occur don't stop but report them. |
---|
48 | try { |
---|
49 | $this->map($batch, $node); |
---|
50 | if ($this->config['authorize']) { |
---|
51 | if (empty($node->nid)) { |
---|
52 | $op = 'create'; |
---|
53 | } |
---|
54 | else { |
---|
55 | $op = 'update'; |
---|
56 | } |
---|
57 | $account = user_load($node->uid); |
---|
58 | if (!node_access($op, $node, $account)) { |
---|
59 | throw new Exception('User ' . $account->uid . ' not authorized to ' . $op . ' content type ' . $node->type); |
---|
60 | } |
---|
61 | } |
---|
62 | node_save($node); |
---|
63 | if (!empty($nid)) { |
---|
64 | $batch->updated++; |
---|
65 | } |
---|
66 | else { |
---|
67 | $batch->created++; |
---|
68 | } |
---|
69 | } |
---|
70 | catch (Exception $e) { |
---|
71 | drupal_set_message($e->getMessage(), 'warning'); |
---|
72 | watchdog('feeds', $e->getMessage(), array(), WATCHDOG_WARNING); |
---|
73 | } |
---|
74 | } |
---|
75 | |
---|
76 | $processed++; |
---|
77 | if ($processed >= $batch_size) { |
---|
78 | $total = $batch->getTotal(FEEDS_PROCESSING); |
---|
79 | $batch->setProgress(FEEDS_PROCESSING, $total - count($batch->items)); |
---|
80 | return; |
---|
81 | } |
---|
82 | } |
---|
83 | |
---|
84 | // Set messages. |
---|
85 | if ($batch->created) { |
---|
86 | drupal_set_message(format_plural($batch->created, 'Created @number @type node.', 'Created @number @type nodes.', array('@number' => $batch->created, '@type' => node_get_types('name', $this->config['content_type'])))); |
---|
87 | } |
---|
88 | if ($batch->updated) { |
---|
89 | drupal_set_message(format_plural($batch->updated, 'Updated @number @type node.', 'Updated @number @type nodes.', array('@number' => $batch->updated, '@type' => node_get_types('name', $this->config['content_type'])))); |
---|
90 | } |
---|
91 | if (!$batch->created && !$batch->updated) { |
---|
92 | drupal_set_message(t('There is no new content.')); |
---|
93 | } |
---|
94 | $batch->setProgress(FEEDS_PROCESSING, FEEDS_BATCH_COMPLETE); |
---|
95 | } |
---|
96 | |
---|
97 | /** |
---|
98 | * Implementation of FeedsProcessor::clear(). |
---|
99 | */ |
---|
100 | public function clear(FeedsBatch $batch, FeedsSource $source) { |
---|
101 | if (!$batch->getTotal(FEEDS_CLEARING)) { |
---|
102 | $total = db_result(db_query("SELECT COUNT(nid) FROM {feeds_node_item} WHERE id = '%s' AND feed_nid = %d", $source->id, $source->feed_nid)); |
---|
103 | $batch->setTotal(FEEDS_CLEARING, $total); |
---|
104 | } |
---|
105 | $result = db_query_range("SELECT nid FROM {feeds_node_item} WHERE id = '%s' AND feed_nid = %d", $source->id, $source->feed_nid, 0, variable_get('feeds_node_batch_size', FEEDS_NODE_BATCH_SIZE)); |
---|
106 | while ($node = db_fetch_object($result)) { |
---|
107 | _feeds_node_delete($node->nid); |
---|
108 | $batch->deleted++; |
---|
109 | } |
---|
110 | if (db_result(db_query_range("SELECT nid FROM {feeds_node_item} WHERE id = '%s' AND feed_nid = %d", $source->id, $source->feed_nid, 0, 1))) { |
---|
111 | $batch->setProgress(FEEDS_CLEARING, $batch->deleted); |
---|
112 | return; |
---|
113 | } |
---|
114 | |
---|
115 | // Set message. |
---|
116 | drupal_get_messages('status'); |
---|
117 | if ($batch->deleted) { |
---|
118 | drupal_set_message(format_plural($batch->deleted, 'Deleted @number node.', 'Deleted @number nodes.', array('@number' => $batch->deleted))); |
---|
119 | } |
---|
120 | else { |
---|
121 | drupal_set_message(t('There is no content to be deleted.')); |
---|
122 | } |
---|
123 | $batch->setProgress(FEEDS_CLEARING, FEEDS_BATCH_COMPLETE); |
---|
124 | } |
---|
125 | |
---|
126 | /** |
---|
127 | * Implement expire(). |
---|
128 | */ |
---|
129 | public function expire($time = NULL) { |
---|
130 | if ($time === NULL) { |
---|
131 | $time = $this->expiryTime(); |
---|
132 | } |
---|
133 | if ($time == FEEDS_EXPIRE_NEVER) { |
---|
134 | return; |
---|
135 | } |
---|
136 | $result = db_query_range("SELECT n.nid FROM {node} n JOIN {feeds_node_item} fni ON n.nid = fni.nid WHERE fni.id = '%s' AND n.created < %d", $this->id, FEEDS_REQUEST_TIME - $time, 0, variable_get('feeds_node_batch_size', FEEDS_NODE_BATCH_SIZE)); |
---|
137 | while ($node = db_fetch_object($result)) { |
---|
138 | _feeds_node_delete($node->nid); |
---|
139 | } |
---|
140 | if (db_result(db_query_range("SELECT n.nid FROM {node} n JOIN {feeds_node_item} fni ON n.nid = fni.nid WHERE fni.id = '%s' AND n.created < %d", $this->id, FEEDS_REQUEST_TIME - $time, 0, 1))) { |
---|
141 | return FEEDS_BATCH_ACTIVE; |
---|
142 | } |
---|
143 | return FEEDS_BATCH_COMPLETE; |
---|
144 | } |
---|
145 | |
---|
146 | /** |
---|
147 | * Return expiry time. |
---|
148 | */ |
---|
149 | public function expiryTime() { |
---|
150 | return $this->config['expire']; |
---|
151 | } |
---|
152 | |
---|
153 | /** |
---|
154 | * Override parent::configDefaults(). |
---|
155 | */ |
---|
156 | public function configDefaults() { |
---|
157 | $types = node_get_types('names'); |
---|
158 | $type = isset($types['story']) ? 'story' : key($types); |
---|
159 | return array( |
---|
160 | 'content_type' => $type, |
---|
161 | 'input_format' => FILTER_FORMAT_DEFAULT, |
---|
162 | 'update_existing' => FEEDS_SKIP_EXISTING, |
---|
163 | 'expire' => FEEDS_EXPIRE_NEVER, |
---|
164 | 'mappings' => array(), |
---|
165 | 'author' => 0, |
---|
166 | 'authorize' => 0, |
---|
167 | ); |
---|
168 | } |
---|
169 | |
---|
170 | /** |
---|
171 | * Override parent::configForm(). |
---|
172 | */ |
---|
173 | public function configForm(&$form_state) { |
---|
174 | $types = node_get_types('names'); |
---|
175 | $form = array(); |
---|
176 | $form['content_type'] = array( |
---|
177 | '#type' => 'select', |
---|
178 | '#title' => t('Content type'), |
---|
179 | '#description' => t('Select the content type for the nodes to be created. <strong>Note:</strong> Users with "import !feed_id feeds" permissions will be able to <strong>import</strong> nodes of the content type selected here regardless of the node level permissions. Further, users with "clear !feed_id permissions" will be able to <strong>delete</strong> imported nodes regardless of their node level permissions.', array('!feed_id' => $this->id)), |
---|
180 | '#options' => $types, |
---|
181 | '#default_value' => $this->config['content_type'], |
---|
182 | ); |
---|
183 | $format_options = array(FILTER_FORMAT_DEFAULT => t('Default format')); |
---|
184 | $formats = filter_formats(); |
---|
185 | foreach ($formats as $format) { |
---|
186 | $format_options[$format->format] = $format->name; |
---|
187 | } |
---|
188 | $form['input_format'] = array( |
---|
189 | '#type' => 'select', |
---|
190 | '#title' => t('Input format'), |
---|
191 | '#description' => t('Select the input format for the body field of the nodes to be created.'), |
---|
192 | '#options' => $format_options, |
---|
193 | '#default_value' => $this->config['input_format'], |
---|
194 | ); |
---|
195 | $author = user_load(array('uid' => $this->config['author'])); |
---|
196 | $form['author'] = array( |
---|
197 | '#type' => 'textfield', |
---|
198 | '#title' => t('Author'), |
---|
199 | '#description' => t('Select the author of the nodes to be created - leave empty to assign "anonymous".'), |
---|
200 | '#autocomplete_path' => 'user/autocomplete', |
---|
201 | '#default_value' => empty($author->name) ? 'anonymous' : check_plain($author->name), |
---|
202 | ); |
---|
203 | $form['authorize'] = array( |
---|
204 | '#type' => 'checkbox', |
---|
205 | '#title' => t('Authorize'), |
---|
206 | '#description' => t('Check that the author has permission to create the node.'), |
---|
207 | '#default_value' => $this->config['authorize'], |
---|
208 | ); |
---|
209 | $period = drupal_map_assoc(array(FEEDS_EXPIRE_NEVER, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 604800 * 4, 604800 * 12, 604800 * 24, 31536000), 'feeds_format_expire'); |
---|
210 | $form['expire'] = array( |
---|
211 | '#type' => 'select', |
---|
212 | '#title' => t('Expire nodes'), |
---|
213 | '#options' => $period, |
---|
214 | '#description' => t('Select after how much time nodes should be deleted. The node\'s published date will be used for determining the node\'s age, see Mapping settings.'), |
---|
215 | '#default_value' => $this->config['expire'], |
---|
216 | ); |
---|
217 | $form['update_existing'] = array( |
---|
218 | '#type' => 'radios', |
---|
219 | '#title' => t('Update existing nodes'), |
---|
220 | '#description' => t('Select how existing nodes should be updated. Existing nodes will be determined using mappings that are a "unique target".'), |
---|
221 | '#options' => array( |
---|
222 | FEEDS_SKIP_EXISTING => 'Do not update existing nodes', |
---|
223 | FEEDS_REPLACE_EXISTING => 'Replace existing nodes', |
---|
224 | FEEDS_UPDATE_EXISTING => 'Update existing nodes (slower than replacing them)', |
---|
225 | ), |
---|
226 | '#default_value' => $this->config['update_existing'], |
---|
227 | ); |
---|
228 | return $form; |
---|
229 | } |
---|
230 | |
---|
231 | /** |
---|
232 | * Override parent::configFormValidate(). |
---|
233 | */ |
---|
234 | public function configFormValidate(&$values) { |
---|
235 | if ($author = user_load(array('name' => $values['author']))) { |
---|
236 | $values['author'] = $author->uid; |
---|
237 | } |
---|
238 | else { |
---|
239 | $values['author'] = 0; |
---|
240 | } |
---|
241 | } |
---|
242 | |
---|
243 | /** |
---|
244 | * Reschedule if expiry time changes. |
---|
245 | */ |
---|
246 | public function configFormSubmit(&$values) { |
---|
247 | if ($this->config['expire'] != $values['expire']) { |
---|
248 | feeds_reschedule($this->id); |
---|
249 | } |
---|
250 | parent::configFormSubmit($values); |
---|
251 | } |
---|
252 | |
---|
253 | /** |
---|
254 | * Override setTargetElement to operate on a target item that is a node. |
---|
255 | */ |
---|
256 | public function setTargetElement(&$target_node, $target_element, $value) { |
---|
257 | switch ($target_element) { |
---|
258 | case 'url': |
---|
259 | case 'guid': |
---|
260 | $target_node->feeds_node_item->$target_element = $value; |
---|
261 | break; |
---|
262 | case 'body': |
---|
263 | $target_node->teaser = node_teaser($value); |
---|
264 | $target_node->body = $value; |
---|
265 | break; |
---|
266 | case 'title': |
---|
267 | case 'status': |
---|
268 | case 'created': |
---|
269 | case 'nid': |
---|
270 | case 'uid': |
---|
271 | $target_node->$target_element = $value; |
---|
272 | break; |
---|
273 | case 'user_name': |
---|
274 | if ($user = user_load(array('name' => $value))) { |
---|
275 | $target_node->uid = $user->uid; |
---|
276 | } |
---|
277 | break; |
---|
278 | case 'user_mail': |
---|
279 | if ($user = user_load(array('mail' => $value))) { |
---|
280 | $target_node->uid = $user->uid; |
---|
281 | } |
---|
282 | break; |
---|
283 | } |
---|
284 | } |
---|
285 | |
---|
286 | /** |
---|
287 | * Return available mapping targets. |
---|
288 | */ |
---|
289 | public function getMappingTargets() { |
---|
290 | $targets = array( |
---|
291 | 'title' => array( |
---|
292 | 'name' => t('Title'), |
---|
293 | 'description' => t('The title of the node.'), |
---|
294 | ), |
---|
295 | ); |
---|
296 | // Include body field only if available. |
---|
297 | $type = node_get_types('type', $this->config['content_type']); |
---|
298 | if ($type->has_body) { |
---|
299 | // Using 'teaser' instead of 'body' forces entire content above the break. |
---|
300 | $targets['body'] = array( |
---|
301 | 'name' => t('Body'), |
---|
302 | 'description' => t('The body of the node. The teaser will be the same as the entire body.'), |
---|
303 | ); |
---|
304 | } |
---|
305 | $targets += array( |
---|
306 | 'nid' => array( |
---|
307 | 'name' => t('Node ID'), |
---|
308 | 'description' => t('The nid of the node. NOTE: use this feature with care, node ids are usually assigned by Drupal.'), |
---|
309 | 'optional_unique' => TRUE, |
---|
310 | ), |
---|
311 | 'uid' => array( |
---|
312 | 'name' => t('User ID'), |
---|
313 | 'description' => t('The Drupal user ID of the node author.'), |
---|
314 | ), |
---|
315 | 'user_name' => array( |
---|
316 | 'name' => t('Username'), |
---|
317 | 'description' => t('The Drupal username of the node author.'), |
---|
318 | ), |
---|
319 | 'user_mail' => array( |
---|
320 | 'name' => t('User email'), |
---|
321 | 'description' => t('The email address of the node author.'), |
---|
322 | ), |
---|
323 | 'status' => array( |
---|
324 | 'name' => t('Published status'), |
---|
325 | 'description' => t('Whether a node is published or not. 1 stands for published, 0 for not published.'), |
---|
326 | ), |
---|
327 | 'created' => array( |
---|
328 | 'name' => t('Published date'), |
---|
329 | 'description' => t('The UNIX time when a node has been published.'), |
---|
330 | ), |
---|
331 | 'url' => array( |
---|
332 | 'name' => t('URL'), |
---|
333 | 'description' => t('The external URL of the node. E. g. the feed item URL in the case of a syndication feed. May be unique.'), |
---|
334 | 'optional_unique' => TRUE, |
---|
335 | ), |
---|
336 | 'guid' => array( |
---|
337 | 'name' => t('GUID'), |
---|
338 | 'description' => t('The external GUID of the node. E. g. the feed item GUID in the case of a syndication feed. May be unique.'), |
---|
339 | 'optional_unique' => TRUE, |
---|
340 | ), |
---|
341 | ); |
---|
342 | |
---|
343 | // Let other modules expose mapping targets. |
---|
344 | self::loadMappers(); |
---|
345 | drupal_alter('feeds_node_processor_targets', $targets, $this->config['content_type']); |
---|
346 | |
---|
347 | return $targets; |
---|
348 | } |
---|
349 | |
---|
350 | /** |
---|
351 | * Get nid of an existing feed item node if available. |
---|
352 | */ |
---|
353 | protected function existingItemId(FeedsImportBatch $batch, FeedsSource $source) { |
---|
354 | |
---|
355 | // Iterate through all unique targets and test whether they do already |
---|
356 | // exist in the database. |
---|
357 | foreach ($this->uniqueTargets($batch) as $target => $value) { |
---|
358 | switch ($target) { |
---|
359 | case 'nid': |
---|
360 | $nid = db_result(db_query("SELECT nid FROM {node} WHERE nid = %d", $value)); |
---|
361 | break; |
---|
362 | case 'url': |
---|
363 | $nid = db_result(db_query("SELECT nid FROM {feeds_node_item} WHERE feed_nid = %d AND id = '%s' AND url = '%s'", $source->feed_nid, $source->id, $value)); |
---|
364 | break; |
---|
365 | case 'guid': |
---|
366 | $nid = db_result(db_query("SELECT nid FROM {feeds_node_item} WHERE feed_nid = %d AND id = '%s' AND guid = '%s'", $source->feed_nid, $source->id, $value)); |
---|
367 | break; |
---|
368 | } |
---|
369 | if ($nid) { |
---|
370 | // Return with the first nid found. |
---|
371 | return $nid; |
---|
372 | } |
---|
373 | } |
---|
374 | return 0; |
---|
375 | } |
---|
376 | |
---|
377 | /** |
---|
378 | * Creates a new node object in memory and returns it. |
---|
379 | */ |
---|
380 | protected function buildNode($nid, $feed_nid) { |
---|
381 | $populate = FALSE; |
---|
382 | |
---|
383 | if (empty($nid)) { |
---|
384 | $node = new stdClass(); |
---|
385 | $node->created = FEEDS_REQUEST_TIME; |
---|
386 | $populate = TRUE; |
---|
387 | } |
---|
388 | else { |
---|
389 | if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) { |
---|
390 | $node = node_load($nid, NULL, TRUE); |
---|
391 | } |
---|
392 | else { |
---|
393 | $node = db_fetch_object(db_query("SELECT created, nid, vid, status FROM {node} WHERE nid = %d", $nid)); |
---|
394 | $populate = TRUE; |
---|
395 | } |
---|
396 | } |
---|
397 | if ($populate) { |
---|
398 | $node->type = $this->config['content_type']; |
---|
399 | $node->changed = FEEDS_REQUEST_TIME; |
---|
400 | $node->format = $this->config['input_format']; |
---|
401 | $node->feeds_node_item = new stdClass(); |
---|
402 | $node->feeds_node_item->id = $this->id; |
---|
403 | $node->feeds_node_item->imported = FEEDS_REQUEST_TIME; |
---|
404 | $node->feeds_node_item->feed_nid = $feed_nid; |
---|
405 | $node->feeds_node_item->url = ''; |
---|
406 | $node->feeds_node_item->guid = ''; |
---|
407 | } |
---|
408 | |
---|
409 | static $included; |
---|
410 | if (!$included) { |
---|
411 | module_load_include('inc', 'node', 'node.pages'); |
---|
412 | $included = TRUE; |
---|
413 | } |
---|
414 | node_object_prepare($node); |
---|
415 | |
---|
416 | // Populate properties that are set by node_object_prepare(). |
---|
417 | $node->log = 'Created/updated by FeedsNodeProcessor'; |
---|
418 | if ($populate) { |
---|
419 | $node->uid = $this->config['author']; |
---|
420 | } |
---|
421 | return $node; |
---|
422 | } |
---|
423 | |
---|
424 | /** |
---|
425 | * Create MD5 hash of item and mappings array. |
---|
426 | * |
---|
427 | * Include mappings as a change in mappings may have an affect on the item |
---|
428 | * produced. |
---|
429 | * |
---|
430 | * @return Always returns a hash, even with empty, NULL, FALSE: |
---|
431 | * Empty arrays return 40cd750bba9870f18aada2478b24840a |
---|
432 | * Empty/NULL/FALSE strings return d41d8cd98f00b204e9800998ecf8427e |
---|
433 | */ |
---|
434 | protected function hash($item) { |
---|
435 | static $serialized_mappings; |
---|
436 | if (!$serialized_mappings) { |
---|
437 | $serialized_mappings = serialize($this->config['mappings']); |
---|
438 | } |
---|
439 | return hash('md5', serialize($item) . $serialized_mappings); |
---|
440 | } |
---|
441 | |
---|
442 | /** |
---|
443 | * Retrieve MD5 hash of $nid from DB. |
---|
444 | * @return Empty string if no item is found, hash otherwise. |
---|
445 | */ |
---|
446 | protected function getHash($nid) { |
---|
447 | $hash = db_result(db_query("SELECT hash FROM {feeds_node_item} WHERE nid = %d", $nid)); |
---|
448 | if ($hash) { |
---|
449 | // Return with the hash. |
---|
450 | return $hash; |
---|
451 | } |
---|
452 | return ''; |
---|
453 | } |
---|
454 | } |
---|
455 | |
---|
456 | /** |
---|
457 | * Copy of node_delete() that circumvents node_access(). |
---|
458 | * |
---|
459 | * Used for batch deletion. |
---|
460 | */ |
---|
461 | function _feeds_node_delete($nid) { |
---|
462 | if ($node = node_load($nid, NULL, TRUE)) { |
---|
463 | db_query('DELETE FROM {node} WHERE nid = %d', $node->nid); |
---|
464 | db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid); |
---|
465 | db_query('DELETE FROM {node_access} WHERE nid = %d', $node->nid); |
---|
466 | |
---|
467 | // Call the node-specific callback (if any): |
---|
468 | node_invoke($node, 'delete'); |
---|
469 | node_invoke_nodeapi($node, 'delete'); |
---|
470 | |
---|
471 | // Clear the page and block caches. |
---|
472 | cache_clear_all(); |
---|
473 | |
---|
474 | // Remove this node from the search index if needed. |
---|
475 | if (function_exists('search_wipe')) { |
---|
476 | search_wipe($node->nid, 'node'); |
---|
477 | } |
---|
478 | watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title)); |
---|
479 | drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_types('name', $node), '%title' => $node->title))); |
---|
480 | } |
---|
481 | } |
---|