PluginProbe
FeedWordPress / 2011.0706
FeedWordPress v2011.0706
trunk 0.8 0.9 0.91 0.95 0.96 0.97 0.98 0.981 0.99 0.991 0.992 0.993 2008.1030 2008.1101 2008.1105 2008.1214 2009.0612 2009.0613 2009.0618 2009.0707 2009.1111 2009.1112 2010.0127 2010.0528 All 65 releases
← All changes | syndicatedpost.class.php +996 -541 2010.05282011.0706 View file →
@@ -9,9 +9,9 @@
9 9 * as some utility methods for extracting useful data from many
10 10 * different feed formats, which may be useful to FeedWordPress users
11 11 * who make use of feed data in PHP add-ons and filters.
12 12 *
13 - * @version 2010.0528
13 + * @version 2011.0601
14 14 */
15 15 class SyndicatedPost {
16 16 var $item = null; // MagpieRSS representation
17 17 var $entry = null; // SimplePie_Item representation
@@ -23,8 +23,12 @@
23 23 var $xmlns = array ();
24 24
25 25 var $post = array ();
26 26
27 + var $named = array ();
28 + var $preset_terms = array ();
29 + var $feed_terms = array ();
30 +
27 31 var $_freshness = null;
28 32 var $_wp_id = null;
29 33
30 34 /**
@@ -35,9 +39,9 @@
35 39 *
36 40 * @param array $item The item syndicated from the feed.
37 41 * @param SyndicatedLink $source The feed it was syndicated from.
38 42 */
39 - function SyndicatedPost ($item, $source) {
43 + function SyndicatedPost ($item, &$source) {
40 44 global $wpdb;
41 45
42 46 if (is_array($item)
43 47 and isset($item['simplepie'])
@@ -44,16 +48,27 @@
44 48 and isset($item['magpie'])) :
45 49 $this->entry = $item['simplepie'];
46 50 $this->item = $item['magpie'];
47 51 $item = $item['magpie'];
52 + elseif (is_a($item, 'SimplePie_Item')) :
53 + $this->entry = $item;
54 +
55 + // convert to Magpie for compat purposes
56 + $mp = new MagpieFromSimplePie($source->simplepie, $this->entry);
57 + $this->item = $mp->get_item();
58 +
59 + // done with conversion object
60 + $mp = NULL; unset($mp);
48 61 else :
49 62 $this->item = $item;
50 63 endif;
51 64
52 - $this->link = $source;
65 + $this->link =& $source;
53 66 $this->feed = $source->magpie;
54 67 $this->feedmeta = $source->settings;
55 68
69 + FeedWordPress::diagnostic('feed_items', 'Considering item ['.$this->guid().'] "'.$this->entry->get_title().'"');
70 +
56 71 # Dealing with namespaces can get so fucking fucked.
57 72 $this->xmlns['forward'] = $source->magpie->_XMLNS_FAMILIAR;
58 73 $this->xmlns['reverse'] = array();
59 74 foreach ($this->xmlns['forward'] as $url => $ns) :
@@ -81,17 +96,19 @@
81 96 global $fwp_channel, $fwp_feedmeta;
82 97 $fwp_channel = $this->feed; $fwp_feedmeta = $this->feedmeta;
83 98
84 99 // Trigger global syndicated_item filter.
85 - $this->item = apply_filters('syndicated_item', $this->item, $this);
100 + $changed = apply_filters('syndicated_item', $this->item, $this);
101 + $this->item = $changed;
86 102
87 103 // Allow for feed-specific syndicated_item filters.
88 - $this->item = apply_filters(
104 + $changed = apply_filters(
89 105 "syndicated_item_".$source->uri(),
90 106 $this->item,
91 107 $this
92 108 );
93 -
109 + $this->item = $changed;
110 +
94 111 # Filters can halt further processing by returning NULL
95 112 if (is_null($this->item)) :
96 113 $this->post = NULL;
97 114 else :
@@ -104,9 +121,9 @@
104 121 'syndicated_item_title',
105 122 $this->entry->get_title(), $this
106 123 );
107 124
108 - $this->post['named']['author'] = apply_filters(
125 + $this->named['author'] = apply_filters(
109 126 'syndicated_item_author',
110 127 $this->author(), $this
111 128 );
112 129 // This just gives us an alphanumeric name for the author.
@@ -118,23 +135,22 @@
118 135 $this->content(), $this
119 136 );
120 137
121 138 $excerpt = apply_filters('syndicated_item_excerpt', $this->excerpt(), $this);
122 - if (!is_null($excerpt)):
139 + if (!empty($excerpt)):
123 140 $this->post['post_excerpt'] = $excerpt;
124 141 endif;
125 -
126 - $this->post['epoch']['issued'] = apply_filters('syndicated_item_published', $this->published(), $this);
127 - $this->post['epoch']['created'] = apply_filters('syndicated_item_created', $this->created(), $this);
128 - $this->post['epoch']['modified'] = apply_filters('syndicated_item_updated', $this->updated(), $this);
129 142
130 143 // Dealing with timestamps in WordPress is so fucking fucked.
131 144 $offset = (int) get_option('gmt_offset') * 60 * 60;
132 - $this->post['post_date'] = gmdate('Y-m-d H:i:s', $this->published(/*fallback=*/ true, /*default=*/ -1) + $offset);
133 - $this->post['post_modified'] = gmdate('Y-m-d H:i:s', $this->updated(/*fallback=*/ true, /*default=*/ -1) + $offset);
134 - $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $this->published(/*fallback=*/ true, /*default=*/ -1));
135 - $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $this->updated(/*fallback=*/ true, /*default=*/ -1));
145 + $post_date_gmt = $this->published(array('default' => -1));
146 + $post_modified_gmt = $this->updated(array('default' => -1));
136 147
148 + $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $post_date_gmt);
149 + $this->post['post_date'] = gmdate('Y-m-d H:i:s', $post_date_gmt + $offset);
150 + $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $post_modified_gmt);
151 + $this->post['post_modified'] = gmdate('Y-m-d H:i:s', $post_modified_gmt + $offset);
152 +
137 153 // Use feed-level preferences or the global default.
138 154 $this->post['post_status'] = $this->link->syndicated_status('post', 'publish');
139 155 $this->post['comment_status'] = $this->link->syndicated_status('comment', 'closed');
140 156 $this->post['ping_status'] = $this->link->syndicated_status('ping', 'closed');
@@ -199,29 +215,51 @@
199 215 apply_filters('syndicated_item_enclosure_length', $enclosure->get_length(), $this)."\n".
200 216 apply_filters('syndicated_item_enclosure_type', $enclosure->get_type(), $this);
201 217 endforeach; endif;
202 218
203 - // In case you want to point back to the blog this was syndicated from
204 - if (isset($this->feed->channel['title'])) :
205 - $this->post['meta']['syndication_source'] = apply_filters('syndicated_item_source_title', $this->feed->channel['title'], $this);
206 - endif;
207 -
208 - if (isset($this->feed->channel['link'])) :
209 - $this->post['meta']['syndication_source_uri'] = apply_filters('syndicated_item_source_link', $this->feed->channel['link'], $this);
210 - endif;
219 + // In case you want to point back to the blog this was
220 + // syndicated from.
211 221
222 + $sourcemeta['syndication_source'] = apply_filters(
223 + 'syndicated_item_source_title',
224 + $this->link->name(),
225 + $this
226 + );
227 + $sourcemeta['syndication_source_uri'] = apply_filters(
228 + 'syndicated_item_source_link',
229 + $this->link->homepage(),
230 + $this
231 + );
232 + $sourcemeta['syndication_source_id'] = apply_filters(
233 + 'syndicated_item_source_id',
234 + $this->link->guid(),
235 + $this
236 + );
237 +
212 238 // Make use of atom:source data, if present in an aggregated feed
213 - if (isset($this->item['source_title'])) :
214 - $this->post['meta']['syndication_source_original'] = $this->item['source_title'];
239 + $entry_source = $this->source();
240 + if (!is_null($entry_source)) :
241 + foreach ($entry_source as $what => $value) :
242 + if (!is_null($value)) :
243 + if ($what=='title') : $key = 'syndication_source';
244 + elseif ($what=='feed') : $key = 'syndication_feed';
245 + else : $key = "syndication_source_${what}";
246 + endif;
247 +
248 + $sourcemeta["${key}_original"] = apply_filters(
249 + 'syndicated_item_original_source_'.$what,
250 + $value,
251 + $this
252 + );
253 + endif;
254 + endforeach;
215 255 endif;
216 -
217 - if (isset($this->item['source_link'])) :
218 - $this->post['meta']['syndication_source_uri_original'] = $this->item['source_link'];
219 - endif;
220 256
221 - if (isset($this->item['source_id'])) :
222 - $this->post['meta']['syndication_source_id_original'] = $this->item['source_id'];
223 - endif;
257 + foreach ($sourcemeta as $meta_key => $value) :
258 + if (!is_null($value)) :
259 + $this->post['meta'][$meta_key] = $value;
260 + endif;
261 + endforeach;
224 262
225 263 // Store information on human-readable and machine-readable comment URIs
226 264
227 265 // Human-readable comment URI
@@ -251,53 +289,92 @@
251 289
252 290 // Store a hash of the post content for checking whether something needs to be updated
253 291 $this->post['meta']['syndication_item_hash'] = $this->update_hash();
254 292
255 - // Feed-by-feed options for author and category creation
256 - $this->post['named']['unfamiliar']['author'] = (isset($this->feedmeta['unfamiliar author']) ? $this->feedmeta['unfamiliar author'] : null);
257 - $this->post['named']['unfamiliar']['category'] = (isset($this->feedmeta['unfamiliar category']) ? $this->feedmeta['unfamiliar category'] : null);
258 -
259 - // Categories: start with default categories, if any
260 - $fc = get_option("feedwordpress_syndication_cats");
261 - if ($fc) :
262 - $this->post['named']['preset/category'] = explode("\n", $fc);
263 - else :
264 - $this->post['named']['preset/category'] = array();
293 + // Categories: start with default categories, if any.
294 + $cats = array();
295 + if ('no' != $this->link->setting('add/category', NULL, 'yes')) :
296 + $fc = get_option("feedwordpress_syndication_cats");
297 + if ($fc) :
298 + $cats = array_merge($cats, explode("\n", $fc));
299 + endif;
265 300 endif;
266 301
267 - if (isset($this->feedmeta['cats']) and is_array($this->feedmeta['cats'])) :
268 - $this->post['named']['preset/category'] = array_merge($this->post['named']['preset/category'], $this->feedmeta['cats']);
302 + $fc = $this->link->setting('cats', NULL, array());
303 + if (is_array($fc)) :
304 + $cats = array_merge($cats, $fc);
269 305 endif;
270 -
306 + $this->preset_terms['category'] = $cats;
307 +
271 308 // Now add categories from the post, if we have 'em
272 - $this->post['named']['category'] = array();
273 - if ( isset($this->item['category#']) ) :
274 - for ($i = 1; $i <= $this->item['category#']; $i++) :
275 - $cat_idx = (($i > 1) ? "#{$i}" : "");
276 - $cat = $this->item["category{$cat_idx}"];
309 + $cats = array();
310 + $post_cats = $this->entry->get_categories();
311 + if (is_array($post_cats)) : foreach ($post_cats as $cat) :
312 + $cat_name = $cat->get_term();
313 + if (!$cat_name) : $cat_name = $cat->get_label(); endif;
314 +
315 + if ($this->link->setting('cat_split', NULL, NULL)) :
316 + $pcre = "\007".$this->feedmeta['cat_split']."\007";
317 + $cats = array_merge(
318 + $cats,
319 + preg_split(
320 + $pcre,
321 + $cat_name,
322 + -1 /*=no limit*/,
323 + PREG_SPLIT_NO_EMPTY
324 + )
325 + );
326 + else :
327 + $cats[] = $cat_name;
328 + endif;
329 + endforeach; endif;
277 330
278 - if ( isset($this->feedmeta['cat_split']) and strlen($this->feedmeta['cat_split']) > 0) :
279 - $pcre = "\007".$this->feedmeta['cat_split']."\007";
280 - $this->post['named']['category'] = array_merge($this->post['named']['category'], preg_split($pcre, $cat, -1 /*=no limit*/, PREG_SPLIT_NO_EMPTY));
281 - else :
282 - $this->post['named']['category'][] = $cat;
283 - endif;
284 - endfor;
285 - endif;
286 - $this->post['named']['category'] = apply_filters('syndicated_item_categories', $this->post['named']['category'], $this);
331 + $this->feed_terms['category'] = apply_filters('syndicated_item_categories', $cats, $this);
287 332
288 333 // Tags: start with default tags, if any
289 - $ft = get_option("feedwordpress_syndication_tags");
290 - if ($ft) :
291 - $this->post['tags_input'] = explode(FEEDWORDPRESS_CAT_SEPARATOR, $ft);
292 - else :
293 - $this->post['tags_input'] = array();
334 + $tags = array();
335 + if ('no' != $this->link->setting('add/post_tag', NULL, 'yes')) :
336 + $ft = get_option("feedwordpress_syndication_tags", NULL);
337 + $tags = (is_null($ft) ? array() : explode(FEEDWORDPRESS_CAT_SEPARATOR, $ft));
294 338 endif;
295 339
296 - if (isset($this->feedmeta['tags']) and is_array($this->feedmeta['tags'])) :
297 - $this->post['tags_input'] = array_merge($this->post['tags_input'], $this->feedmeta['tags']);
340 + $ft = $this->link->setting('tags', NULL, array());
341 + if (is_array($ft)) :
342 + $tags = array_merge($tags, $ft);
298 343 endif;
299 - $this->post['tags_input'] = apply_filters('syndicated_item_tags', $this->post['tags_input'], $this);
344 + $this->preset_terms['post_tag'] = $tags;
345 +
346 + // Scan post for /a[@rel='tag'] and use as tags if present
347 + $tags = $this->inline_tags();
348 + $this->feed_terms['post_tag'] = apply_filters('syndicated_item_tags', $tags, $this);
349 +
350 + $taxonomies = $this->link->taxonomies();
351 + $feedTerms = $this->link->setting('terms', NULL, array());
352 + $globalTerms = get_option('feedwordpress_syndication_terms', array());
353 +
354 + $specials = array('category' => 'cats', 'post_tag' => 'tags');
355 + foreach ($taxonomies as $tax) :
356 + if (!isset($specials[$tax])) :
357 + $terms = array();
358 +
359 + // See if we should get the globals
360 + if ('no' != $this->link->setting("add/$tax", NULL, 'yes')) :
361 + if (isset($globalTerms[$tax])) :
362 + $terms = $globalTerms[$tax];
363 + endif;
364 + endif;
365 +
366 + // Now merge in the locals
367 + if (isset($feedTerms[$tax])) :
368 + $terms = array_merge($terms, $feedTerms[$tax]);
369 + endif;
370 +
371 + // That's all, folks.
372 + $this->preset_terms[$tax] = $terms;
373 + endif;
374 + endforeach;
375 +
376 + $this->post['post_type'] = apply_filters('syndicated_post_type', $this->link->setting('syndicated post type', 'syndicated_post_type', 'post'), $this);
300 377 endif;
301 378 } /* SyndicatedPost::SyndicatedPost() */
302 379
303 380 #####################################
@@ -346,62 +423,50 @@
346 423
347 424 switch ($node) :
348 425 case 'feed' :
349 426 case 'channel' :
350 - $method = "get_${node}_tags";
351 427 $node = array_shift($path);
428 + $data = $this->get_feed_root_element();
429 + $data = array_merge($data, $this->get_feed_channel_elements());
352 430 break;
353 431 case 'item' :
354 432 $node = array_shift($path);
355 433 default :
434 + $data = array($this->entry->data);
356 435 $method = NULL;
357 436 endswitch;
358 437
359 - $data = array();
360 - if (!is_null($node)) :
361 - list($namespaces, $element) = $this->xpath_extended_name($node);
362 -
363 - $matches = array();
364 - foreach ($namespaces as $ns) :
365 - if (!is_null($method)) :
366 - $el = $this->link->simplepie->{$method}($ns, $element);
367 - else :
368 - $el = $this->entry->get_item_tags($ns, $element);
369 - endif;
370 -
371 - if (!is_null($el)) :
372 - $matches = array_merge($matches, $el);
373 - endif;
374 - endforeach;
375 - $data = $matches;
376 -
377 - $node = array_shift($path);
378 - endif;
379 -
380 438 while (!is_null($node)) :
381 439 if (strlen($node) > 0) :
382 440 $matches = array();
383 -
384 - list($ns, $element) = $this->xpath_extended_name($node);
385 -
386 - if (preg_match('/^@(.*)$/', $element, $ref)) :
387 - $element = $ref[1];
388 - $axis = 'attribs';
389 - else :
390 - $axis = 'child';
391 - endif;
392 441
442 + list($axis, $element) = $this->xpath_name_and_axis($node);
443 +
393 444 foreach ($data as $datum) :
394 - foreach ($namespaces as $ns) :
395 - if (!is_string($datum)
396 - and isset($datum[$axis][$ns][$element])) :
397 - if (is_string($datum[$axis][$ns][$element])) :
398 - $matches[] = $datum[$axis][$ns][$element];
399 - else :
400 - $matches = array_merge($matches, $datum[$axis][$ns][$element]);
445 + if (!is_string($datum) and isset($datum[$axis])) :
446 + foreach ($datum[$axis] as $ns => $elements) :
447 + if (isset($elements[$element])) :
448 + // Potential match.
449 + // Check namespace.
450 + if (is_string($elements[$element])) : // Attribute
451 + $addenda = array($elements[$element]);
452 + $contexts = array($datum);
453 + else : // Element
454 + $addenda = $elements[$element];
455 + $contexts = $elements[$element];
456 + endif;
457 +
458 + foreach ($addenda as $index => $addendum) :
459 + $context = $contexts[$index];
460 +
461 + $namespaces = $this->xpath_possible_namespaces($node, $context);
462 + if (in_array($ns, $namespaces)) :
463 + $matches[] = $addendum;
464 + endif;
465 + endforeach;
401 466 endif;
402 - endif;
403 - endforeach;
467 + endforeach;
468 + endif;
404 469 endforeach;
405 470
406 471 $data = $matches;
407 472 endif;
@@ -418,8 +483,36 @@
418 483 endforeach;
419 484 return $matches;
420 485 } /* SyndicatedPost::query() */
421 486
487 + function get_feed_root_element () {
488 + $matches = array();
489 + foreach ($this->link->simplepie->data['child'] as $ns => $root) :
490 + foreach ($root as $element => $data) :
491 + $matches = array_merge($matches, $data);
492 + endforeach;
493 + endforeach;
494 + return $matches;
495 + } /* SyndicatedPost::get_feed_root_element() */
496 +
497 + function get_feed_channel_elements () {
498 + $rss = array(
499 + SIMPLEPIE_NAMESPACE_RSS_090,
500 + SIMPLEPIE_NAMESPACE_RSS_10,
501 + 'http://backend.userland.com/RSS2',
502 + SIMPLEPIE_NAMESPACE_RSS_20,
503 + );
504 +
505 + $matches = array();
506 + foreach ($rss as $ns) :
507 + $data = $this->link->simplepie->get_feed_tags($ns, 'channel');
508 + if (!is_null($data)) :
509 + $matches = array_merge($matches, $data);
510 + endif;
511 + endforeach;
512 + return $matches;
513 + } /* SyndicatedPost::get_feed_channel_elements() */
514 +
422 515 function xpath_default_namespace () {
423 516 // Get the default namespace.
424 517 $type = $this->link->simplepie->get_type();
425 518 if ($type & SIMPLEPIE_TYPE_ATOM_10) :
@@ -436,22 +529,42 @@
436 529 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20;
437 530 endif;
438 531 return $defaultNS;
439 532 } /* SyndicatedPost::xpath_default_namespace() */
440 -
441 - function xpath_extended_name ($node) {
533 +
534 + function xpath_name_and_axis ($node) {
442 535 $ns = NULL; $element = NULL;
443 -
536 +
444 537 if (substr($node, 0, 1)=='@') :
538 + $axis = 'attribs'; $node = substr($node, 1);
539 + else :
540 + $axis = 'child';
541 + endif;
542 +
543 + if (preg_match('/^{([^}]*)}(.*)$/', $node, $ref)) :
544 + $element = $ref[2];
545 + elseif (strpos($node, ':') !== FALSE) :
546 + list($xmlns, $element) = explode(':', $node, 2);
547 + else :
548 + $element = $node;
549 + endif;
550 + return array($axis, $element);
551 + } /* SyndicatedPost::xpath_local_name () */
552 +
553 + function xpath_possible_namespaces ($node, $datum = array()) {
554 + $ns = NULL; $element = NULL;
555 +
556 + if (substr($node, 0, 1)=='@') :
445 557 $attr = '@'; $node = substr($node, 1);
446 558 else :
447 559 $attr = '';
448 560 endif;
449 -
561 +
450 562 if (preg_match('/^{([^}]*)}(.*)$/', $node, $ref)) :
451 - $ns = array($ref[1]); $element = $ref[2];
563 + $ns = array($ref[1]);
452 564 elseif (strpos($node, ':') !== FALSE) :
453 565 list($xmlns, $element) = explode(':', $node, 2);
566 +
454 567 if (isset($this->xmlns['reverse'][$xmlns])) :
455 568 $ns = $this->xmlns['reverse'][$xmlns];
456 569 else :
457 570 $ns = array($xmlns);
@@ -457,19 +570,26 @@
457 570 $ns = array($xmlns);
458 571 endif;
459 572
460 573 // Fucking SimplePie. For attributes in default xmlns.
461 - if ($xmlns==$this->xmlns['forward'][$defaultNS[0]]) :
574 + $defaultNS = $this->xpath_default_namespace();
575 + if (isset($this->xmlns['forward'][$defaultNS])
576 + and ($xmlns==$this->xmlns['forward'][$defaultNS])) :
462 577 $ns[] = '';
463 578 endif;
579 +
580 + if (isset($datum['xmlns'])) :
581 + if (isset($datum['xmlns'][$xmlns])) :
582 + $ns[] = $datum['xmlns'][$xmlns];
583 + endif;
584 + endif;
464 585 else :
465 586 // Often in SimplePie, the default namespace gets stored
466 587 // as an empty string rather than a URL.
467 588 $ns = array($this->xpath_default_namespace(), '');
468 - $element = $node;
469 589 endif;
470 - return array(array_unique($ns), $attr.$element);
471 - } /* SyndicatedPost::xpath_extended_name () */
590 + return array_unique($ns);
591 + } /* SyndicatedPost::xpath_possible_namespaces() */
472 592
473 593 function content () {
474 594 $content = NULL;
475 595 if (isset($this->item['atom_content'])) :
@@ -479,9 +599,9 @@
479 599 elseif (isset($this->item['xhtml']['div'])) :
480 600 $content = $this->item['xhtml']['div'];
481 601 elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
482 602 $content = $this->item['content']['encoded'];
483 - else:
603 + elseif (isset($this->item['description'])) :
484 604 $content = $this->item['description'];
485 605 endif;
486 606 return $content;
487 607 } /* SyndicatedPost::content() */
@@ -494,9 +614,14 @@
494 614 # carry the entire post (typically with escaped HTML).
495 615 # If that's what happened, we don't want the full
496 616 # content for the excerpt.
497 617 $content = $this->content();
498 - if ( is_null($excerpt) or $excerpt == $content ) :
618 +
619 + // Ignore whitespace, case, and tag cruft.
620 + $theExcerpt = preg_replace('/\s+/', '', strtolower(strip_tags($excerpt)));
621 + $theContent = preg_replace('/\s+/', '', strtolower(strip_tags($content)));
622 +
623 + if ( empty($excerpt) or $theExcerpt == $theContent ) :
499 624 # If content is available, generate an excerpt.
500 625 if ( strlen(trim($content)) > 0 ) :
501 626 $excerpt = strip_tags($content);
502 627 if (strlen($excerpt) > 255) :
@@ -513,9 +638,12 @@
513 638 $permalink = $this->entry->get_link();
514 639 return $permalink;
515 640 }
516 641
517 - function created () {
642 + function created ($params = array()) {
643 + $unfiltered = false; $default = NULL;
644 + extract($params);
645 +
518 646 $date = '';
519 647 if (isset($this->item['dc']['created'])) :
520 648 $date = $this->item['dc']['created'];
521 649 elseif (isset($this->item['dcterms']['created'])) :
@@ -523,14 +651,26 @@
523 651 elseif (isset($this->item['created'])): // Atom 0.3
524 652 $date = $this->item['created'];
525 653 endif;
526 654
527 - $epoch = new FeedTime($date);
528 - return $epoch->timestamp();
655 + $time = new FeedTime($date);
656 + $ts = $time->timestamp();
657 + if (!$unfiltered) :
658 + apply_filters('syndicated_item_created', $ts, $this);
659 + endif;
660 + return $ts;
529 661 } /* SyndicatedPost::created() */
530 662
531 - function published ($fallback = true, $default = NULL) {
663 + function published ($params = array(), $default = NULL) {
664 + $fallback = true; $unfiltered = false;
665 + if (!is_array($params)) : // Old style
666 + $fallback = $params;
667 + else : // New style
668 + extract($params);
669 + endif;
670 +
532 671 $date = '';
672 + $ts = null;
533 673
534 674 # RSS is a fucking mess. Figure out whether we have a date in
535 675 # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
536 676 # epoch format for reformatting. If we can't find anything,
@@ -548,27 +688,38 @@
548 688 endif;
549 689
550 690 if (strlen($date) > 0) :
551 691 $time = new FeedTime($date);
552 - $epoch = $time->timestamp();
692 + $ts = $time->timestamp();
553 693 elseif ($fallback) : // Fall back to <updated> / <modified> if present
554 - $epoch = $this->updated(/*fallback=*/ false, /*default=*/ $default);
694 + $ts = $this->updated(/*fallback=*/ false, /*default=*/ $default);
555 695 endif;
556 696
557 697 # If everything failed, then default to the current time.
558 - if (is_null($epoch)) :
698 + if (is_null($ts)) :
559 699 if (-1 == $default) :
560 - $epoch = time();
700 + $ts = time();
561 701 else :
562 - $epoch = $default;
702 + $ts = $default;
563 703 endif;
564 704 endif;
565 705
566 - return $epoch;
706 + if (!$unfiltered) :
707 + $ts = apply_filters('syndicated_item_published', $ts, $this);
708 + endif;
709 + return $ts;
567 710 } /* SyndicatedPost::published() */
568 711
569 - function updated ($fallback = true, $default = -1) {
712 + function updated ($params = array(), $default = -1) {
713 + $fallback = true; $unfiltered = false;
714 + if (!is_array($params)) : // Old style
715 + $fallback = $params;
716 + else : // New style
717 + extract($params);
718 + endif;
719 +
570 720 $date = '';
721 + $ts = null;
571 722
572 723 # As far as I know, only dcterms and Atom have reliable ways to
573 724 # specify when something was *modified* last. If neither is
574 725 # available, then we'll try to get the time of publication.
@@ -577,59 +728,111 @@
577 728 elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
578 729 $date = $this->item['dcterms']['modified'];
579 730 elseif (isset($this->item['modified'])): // Atom 0.3
580 731 $date = $this->item['modified'];
581 - elseif (isset($this->item['updated'])): // Atom 1.0
732 + elseif (isset($this->item['updated'])): // Atom 1.0
582 733 $date = $this->item['updated'];
583 734 endif;
584 735
585 736 if (strlen($date) > 0) :
586 737 $time = new FeedTime($date);
587 - $epoch = $time->timestamp();
738 + $ts = $time->timestamp();
588 739 elseif ($fallback) : // Fall back to issued / dc:date
589 - $epoch = $this->published(/*fallback=*/ false, /*default=*/ $default);
740 + $ts = $this->published(/*fallback=*/ false, /*default=*/ $default);
590 741 endif;
591 742
592 743 # If everything failed, then default to the current time.
593 - if (is_null($epoch)) :
744 + if (is_null($ts)) :
594 745 if (-1 == $default) :
595 - $epoch = time();
746 + $ts = time();
596 747 else :
597 - $epoch = $default;
748 + $ts = $default;
598 749 endif;
599 750 endif;
600 751
601 - return $epoch;
752 + if (!$unfiltered) :
753 + apply_filters('syndicated_item_updated', $ts, $this);
754 + endif;
755 + return $ts;
602 756 } /* SyndicatedPost::updated() */
603 757
758 + var $_hashes = array();
759 + function stored_hashes ($id = NULL) {
760 + if (is_null($id)) :
761 + $id = $this->wp_id();
762 + endif;
763 +
764 + if (!isset($this->_hashes[$id])) :
765 + $this->_hashes[$id] = get_post_custom_values(
766 + 'syndication_item_hash', $id
767 + );
768 + if (is_null($this->_hashes[$id])) :
769 + $this->_hashes[$id] = array();
770 + endif;
771 + endif;
772 + return $this->_hashes[$id];
773 + }
774 +
604 775 function update_hash () {
605 776 return md5(serialize($this->item));
606 777 } /* SyndicatedPost::update_hash() */
607 778
779 + /*static*/ function normalize_guid_prefix () {
780 + return trailingslashit(get_bloginfo('url')).'?guid=';
781 + }
782 +
783 + /*static*/ function normalize_guid ($guid) {
784 + $guid = trim($guid);
785 + if (preg_match('/^[0-9a-z]{32}$/i', $guid)) : // MD5
786 + $guid = SyndicatedPost::normalize_guid_prefix().strtolower($guid);
787 + elseif ((strlen(esc_url($guid)) == 0) or (esc_url($guid) != $guid)) :
788 + $guid = SyndicatedPost::normalize_guid_prefix().md5($guid);
789 + endif;
790 + $guid = trim($guid);
791 + return $guid;
792 + } /* SyndicatedPost::normalize_guid() */
793 +
608 794 function guid () {
609 795 $guid = null;
610 - if (isset($this->item['id'])): // Atom 0.3 / 1.0
796 + if (isset($this->item['id'])): // Atom 0.3 / 1.0
611 797 $guid = $this->item['id'];
612 - elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
798 + elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
613 799 $guid = $this->item['atom']['id'];
614 - elseif (isset($this->item['guid'])) : // RSS 2.0
800 + elseif (isset($this->item['guid'])) : // RSS 2.0
615 801 $guid = $this->item['guid'];
616 - elseif (isset($this->item['dc']['identifier'])) :// yeah, right
802 + elseif (isset($this->item['dc']['identifier'])) : // yeah, right
617 803 $guid = $this->item['dc']['identifier'];
618 - else :
804 + endif;
805 +
806 + // Un-set or too long to use as-is. Generate a tag: URI.
807 + if (is_null($guid) or strlen($guid) > 250) :
808 + // In case we need to check this again
809 + $original_guid = $guid;
810 +
619 811 // The feed does not seem to have provided us with a
620 - // unique identifier, so we'll have to cobble together
621 - // a tag: URI that might work for us. The base of the
622 - // URI will be the host name of the feed source ...
623 - $bits = parse_url($this->feedmeta['link/uri']);
812 + // usable unique identifier, so we'll have to cobble
813 + // together a tag: URI that might work for us. The base
814 + // of the URI will be the host name of the feed source ...
815 + $bits = parse_url($this->link->uri());
624 816 $guid = 'tag:'.$bits['host'];
625 817
818 + // Some ill-mannered feeds (for example, certain feeds
819 + // coming from Google Calendar) have extraordinarily long
820 + // guids -- so long that they exceed the 255 character
821 + // width of the WordPress guid field. But if the string
822 + // gets clipped by MySQL, uniqueness tests will fail
823 + // forever after and the post will be endlessly
824 + // reduplicated. So, instead, Guids Of A Certain Length
825 + // are hashed down into a nice, manageable tag: URI.
826 + if (!is_null($original_guid)) :
827 + $guid .= ',2010-12-03:id.'.md5($original_guid);
828 +
626 829 // If we have a date of creation, then we can use that
627 830 // to uniquely identify the item. (On the other hand, if
628 831 // the feed producer was consicentious enough to
629 832 // generate dates of creation, she probably also was
630 833 // conscientious enough to generate unique identifiers.)
631 - if (!is_null($this->created())) :
834 + elseif (!is_null($this->created())) :
632 835 $guid .= '://post.'.date('YmdHis', $this->created());
633 836
634 837 // Otherwise, use both the URI of the item, *and* the
635 838 // item's title. We have to use both because titles are
@@ -638,9 +841,11 @@
638 841 // some podcasts). But it's rare to have *both* the same
639 842 // title *and* the same link for two different items. So
640 843 // this is about the best we can do.
641 844 else :
642 - $guid .= '://'.md5($this->item['link'].'/'.$this->item['title']);
845 + $link = $this->permalink();
846 + if (is_null($link)) : $link = $this->link->uri(); endif;
847 + $guid .= '://'.md5($link.'/'.$this->item['title']);
643 848 endif;
644 849 endif;
645 850 return $guid;
646 851 } /* SyndicatedPost::guid() */
@@ -672,10 +877,13 @@
672 877 else :
673 878 $author['name'] = $this->feed->channel['title'];
674 879 endif;
675 880 endif;
881 + elseif ($this->link->name()) :
882 + $author['name'] = $this->link->name();
676 883 else :
677 - $author['name'] = $this->feed->channel['title'];
884 + $url = parse_url($this->link->uri());
885 + $author['name'] = $url['host'];
678 886 endif;
679 887
680 888 if (isset($this->item['author_email'])):
681 889 $author['email'] = $this->item['author_email'];
@@ -682,13 +890,17 @@
682 890 elseif (isset($this->feed->channel['author_email'])) :
683 891 $author['email'] = $this->feed->channel['author_email'];
684 892 endif;
685 893
686 - if (isset($this->item['author_url'])):
894 + if (isset($this->item['author_uri'])):
895 + $author['uri'] = $this->item['author_uri'];
896 + elseif (isset($this->item['author_url'])):
687 897 $author['uri'] = $this->item['author_url'];
898 + elseif (isset($this->feed->channel['author_uri'])) :
899 + $author['uri'] = $this->item['author_uri'];
688 900 elseif (isset($this->feed->channel['author_url'])) :
689 901 $author['uri'] = $this->item['author_url'];
690 - else:
902 + elseif (isset($this->feed->channel['link'])) :
691 903 $author['uri'] = $this->feed->channel['link'];
692 904 endif;
693 905
694 906 return $author;
@@ -694,8 +906,29 @@
694 906 return $author;
695 907 } /* SyndicatedPost::author() */
696 908
697 909 /**
910 + * SyndicatedPost::inline_tags: Return a list of all the tags embedded
911 + * in post content using the a[@rel="tag"] microformat.
912 + *
913 + * @since 2010.0630
914 + * @return array of string values containing the name of each tag
915 + */
916 + function inline_tags () {
917 + $tags = array();
918 + $content = $this->content();
919 + $pattern = FeedWordPressHTML::tagWithAttributeRegex('a', 'rel', 'tag');
920 + preg_match_all($pattern, $content, $refs, PREG_SET_ORDER);
921 + if (count($refs) > 0) :
922 + foreach ($refs as $ref) :
923 + $tag = FeedWordPressHTML::tagWithAttributeMatch($ref);
924 + $tags[] = $tag['content'];
925 + endforeach;
926 + endif;
927 + return $tags;
928 + }
929 +
930 + /**
698 931 * SyndicatedPost::isTaggedAs: Test whether a feed item is
699 932 * tagged / categorized with a given string. Case and leading and
700 933 * trailing whitespace are ignored.
701 934 *
@@ -777,8 +1010,36 @@
777 1010 endif;
778 1011 return $enclosures;
779 1012 } /* SyndicatedPost::enclosures() */
780 1013
1014 + function source ($what = NULL) {
1015 + $ret = NULL;
1016 + $source = $this->entry->get_source();
1017 + if ($source) :
1018 + $ret = array();
1019 + $ret['title'] = $source->get_title();
1020 + $ret['uri'] = $source->get_link();
1021 + $ret['feed'] = $source->get_link(0, 'self');
1022 +
1023 + if ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'id')) :
1024 + $ret['id'] = $id_tags[0]['data'];
1025 + elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'id')) :
1026 + $ret['id'] = $id_tags[0]['data'];
1027 + elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'guid')) :
1028 + $ret['id'] = $id_tags[0]['data'];
1029 + elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'guid')) :
1030 + $ret['id'] = $id_tags[0]['data'];
1031 + elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'guid')) :
1032 + $ret['id'] = $id_tags[0]['data'];
1033 + endif;
1034 + endif;
1035 +
1036 + if (!is_null($what) and is_scalar($what)) :
1037 + $ret = $ret[$what];
1038 + endif;
1039 + return $ret;
1040 + }
1041 +
781 1042 function comment_link () {
782 1043 $url = null;
783 1044
784 1045 // RSS 2.0 has a standard <comments> element:
@@ -885,9 +1146,9 @@
885 1146 var $_base = null;
886 1147
887 1148 function resolve_single_relative_uri ($refs) {
888 1149 $tag = FeedWordPressHTML::attributeMatch($refs);
889 - $url = Relative_URI::resolve($tag['value'], $this->_base);
1150 + $url = SimplePie_Misc::absolutize_url($tag['value'], $this->_base);
890 1151 return $tag['prefix'] . $url . $tag['suffix'];
891 1152 } /* function SyndicatedPost::resolve_single_relative_uri() */
892 1153
893 1154 function resolve_relative_uris ($content, $obj) {
@@ -894,9 +1155,9 @@
894 1155 $set = $obj->link->setting('resolve relative', 'resolve_relative', 'yes');
895 1156 if ($set and $set != 'no') :
896 1157 // Fallback: if we don't have anything better, use the
897 1158 // item link from the feed
898 - $obj->_base = $obj->item['link']; // Reset the base for resolving relative URIs
1159 + $obj->_base = $obj->permalink(); // Reset the base for resolving relative URIs
899 1160
900 1161 // What we should do here, properly, is to use
901 1162 // SimplePie_Item::get_base() -- but that method is
902 1163 // currently broken. Or getting down and dirty in the
@@ -973,39 +1234,37 @@
973 1234 function freshness () {
974 1235 global $wpdb;
975 1236
976 1237 if ($this->filtered()) : // This should never happen.
977 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1238 + FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
978 1239 endif;
979 1240
980 - if (is_null($this->_freshness)) :
1241 + if (is_null($this->_freshness)) : // Not yet checked and cached.
981 1242 $guid = $wpdb->escape($this->guid());
982 1243
983 - $result = $wpdb->get_row("
984 - SELECT id, guid, post_modified_gmt
985 - FROM $wpdb->posts WHERE guid='$guid'
986 - ");
987 -
988 - if (!$result) :
1244 + $q = new WP_Query(array(
1245 + 'fields' => '_synfresh', // id, guid, post_modified_gmt
1246 + 'ignore_sticky_posts' => true,
1247 + 'guid' => $this->guid(),
1248 + ));
1249 +
1250 + $old_post = NULL;
1251 + if ($q->have_posts()) :
1252 + while ($q->have_posts()) : $q->the_post();
1253 + $old_post = $q->post;
1254 + endwhile;
1255 + endif;
1256 +
1257 + if (is_null($old_post)) : // No post with this guid
1258 + FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$this->guid().'] "'.$this->entry->get_title().'" is a NEW POST.');
1259 + $this->_wp_id = NULL;
989 1260 $this->_freshness = 2; // New content
990 - else:
991 - $stored_update_hashes = get_post_custom_values('syndication_item_hash', $result->id);
992 - if (count($stored_update_hashes) > 0) :
993 - $stored_update_hash = $stored_update_hashes[0];
994 - $update_hash_changed = ($stored_update_hash != $this->update_hash());
995 - else :
996 - $update_hash_changed = false;
997 - endif;
1261 + else :
1262 + preg_match('/([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/', $old_post->post_modified_gmt, $backref);
998 1263
999 - preg_match('/([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/', $result->post_modified_gmt, $backref);
1000 -
1001 1264 $last_rev_ts = gmmktime($backref[4], $backref[5], $backref[6], $backref[2], $backref[3], $backref[1]);
1002 1265 $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
1003 1266
1004 - $frozen_values = get_post_custom_values('_syndication_freeze_updates', $result->id);
1005 - $frozen_post = (count($frozen_values) > 0 and 'yes' == $frozen_values[0]);
1006 - $frozen_feed = ('yes' == $this->link->setting('freeze updates', 'freeze_updates', NULL));
1007 -
1008 1267 // Check timestamps...
1009 1268 $updated = (
1010 1269 !is_null($updated_ts)
1011 1270 and ($updated_ts > $last_rev_ts)
@@ -1010,25 +1269,45 @@
1010 1269 !is_null($updated_ts)
1011 1270 and ($updated_ts > $last_rev_ts)
1012 1271 );
1013 1272
1014 -
1015 - // Or the hash...
1016 - $updated = ($updated or $update_hash_changed);
1017 1273
1018 - // But only if the post is not frozen.
1019 - $updated = (
1020 - $updated
1021 - and !$frozen_post
1022 - and !$frozen_feed
1023 - );
1274 + if (!$updated) :
1275 + // Or the hash...
1276 + $hash = $this->update_hash();
1277 + $seen = $this->stored_hashes($old_post->ID);
1278 + if (count($seen) > 0) :
1279 + $updated = !in_array($hash, $seen); // Not seen yet?
1280 + else :
1281 + $updated = true; // Can't find syndication meta-data
1282 + endif;
1283 + endif;
1024 1284
1285 + $frozen = false;
1286 + if ($updated) : // Ignore if the post is frozen
1287 + $frozen = ('yes' == $this->link->setting('freeze updates', 'freeze_updates', NULL));
1288 + if (!$frozen) :
1289 + $frozen_values = get_post_custom_values('_syndication_freeze_updates', $old_post->ID);
1290 + $frozen = (count($frozen_values) > 0 and 'yes' == $frozen_values[0]);
1291 + endif;
1292 + endif;
1293 + $updated = ($updated and !$frozen);
1294 +
1025 1295 if ($updated) :
1296 + FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$this->guid().'] "'.$this->entry->get_title().'" is an update of an existing post.');
1026 1297 $this->_freshness = 1; // Updated content
1027 - $this->_wp_id = $result->id;
1298 + $this->_wp_id = $old_post->ID;
1299 +
1300 + // We want this to keep a running list of all the
1301 + // processed update hashes.
1302 + $this->post['meta']['syndication_item_hash'] = array_merge(
1303 + $this->stored_hashes(),
1304 + array($this->update_hash())
1305 + );
1028 1306 else :
1307 + FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$this->guid().'] "'.$this->entry->get_title().'" is a duplicate of an existing post.');
1029 1308 $this->_freshness = 0; // Same old, same old
1030 - $this->_wp_id = $result->id;
1309 + $this->_wp_id = $old_post->ID;
1031 1310 endif;
1032 1311 endif;
1033 1312 endif;
1034 1313 return $this->_freshness;
@@ -1039,9 +1318,9 @@
1039 1318 #################################################
1040 1319
1041 1320 function wp_id () {
1042 1321 if ($this->filtered()) : // This should never happen.
1043 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1322 + FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1044 1323 endif;
1045 1324
1046 1325 if (is_null($this->_wp_id) and is_null($this->_freshness)) :
1047 1326 $fresh = $this->freshness(); // sets WP DB id in the process
@@ -1052,9 +1331,9 @@
1052 1331 function store () {
1053 1332 global $wpdb;
1054 1333
1055 1334 if ($this->filtered()) : // This should never happen.
1056 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1335 + FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1057 1336 endif;
1058 1337
1059 1338 $freshness = $this->freshness();
1060 1339 if ($freshness > 0) :
@@ -1059,50 +1338,103 @@
1059 1338 $freshness = $this->freshness();
1060 1339 if ($freshness > 0) :
1061 1340 # -- Look up, or create, numeric ID for author
1062 1341 $this->post['post_author'] = $this->author_id (
1063 - FeedWordPress::on_unfamiliar('author', $this->post['named']['unfamiliar']['author'])
1342 + $this->link->setting('unfamiliar author', 'unfamiliar_author', 'create')
1064 1343 );
1065 1344
1066 1345 if (is_null($this->post['post_author'])) :
1346 + FeedWordPress::diagnostic('feed_items:rejected', 'Filtered out item ['.$this->guid().'] without syndication: no author available');
1067 1347 $this->post = NULL;
1068 1348 endif;
1069 1349 endif;
1070 1350
1071 1351 if (!$this->filtered() and $freshness > 0) :
1072 - # -- Look up, or create, numeric ID for categories
1073 - list($pcats, $ptags) = $this->category_ids (
1074 - $this->post['named']['category'],
1075 - FeedWordPress::on_unfamiliar('category', $this->post['named']['unfamiliar']['category']),
1076 - /*tags_too=*/ true
1352 + $consider = array(
1353 + 'category' => array('abbr' => 'cats', 'domain' => array('category', 'post_tag')),
1354 + 'post_tag' => array('abbr' => 'tags', 'domain' => array('post_tag')),
1077 1355 );
1078 1356
1079 - $this->post['post_category'] = $pcats;
1080 - $this->post['tags_input'] = array_merge($this->post['tags_input'], $ptags);
1357 + $termSet = array(); $valid = null;
1358 + foreach ($consider as $what => $taxes) :
1359 + if (!is_null($this->post)) : // Not filtered out yet
1360 + # -- Look up, or create, numeric ID for categories
1361 + $taxonomies = $this->link->setting("match/".$taxes['abbr'], 'match_'.$taxes['abbr'], $taxes['domain']);
1362 +
1363 + // Eliminate dummy variables
1364 + $taxonomies = array_filter($taxonomies, 'remove_dummy_zero');
1365 +
1366 + $terms = $this->category_ids (
1367 + $this->feed_terms[$what],
1368 + $this->link->setting("unfamiliar {$what}", "unfamiliar_{$what}", 'create:'.$what),
1369 + /*taxonomies=*/ $taxonomies,
1370 + array(
1371 + 'singleton' => false, // I don't like surprises
1372 + 'filters' => true,
1373 + )
1374 + );
1375 +
1376 + if (is_null($terms) or is_null($termSet)) :
1377 + // filtered out -- no matches
1378 + else :
1379 + $valid = true;
1081 1380
1082 - if (is_null($this->post['post_category'])) :
1083 - // filter mode on, no matching categories; drop the post
1381 + // filter mode off, or at least one match
1382 + foreach ($terms as $tax => $term_ids) :
1383 + if (!isset($termSet[$tax])) :
1384 + $termSet[$tax] = array();
1385 + endif;
1386 + $termSet[$tax] = array_merge($termSet[$tax], $term_ids);
1387 + endforeach;
1388 + endif;
1389 + endif;
1390 + endforeach;
1391 +
1392 + if (is_null($valid)) : // Plonked
1084 1393 $this->post = NULL;
1085 - else :
1086 - // filter mode off or at least one match; now add on the feed and global presets
1087 - $this->post['post_category'] = array_merge (
1088 - $this->post['post_category'],
1089 - $this->category_ids (
1090 - $this->post['named']['preset/category'],
1091 - 'default'
1092 - )
1093 - );
1394 + else : // We can proceed
1395 + $this->post['tax_input'] = array();
1396 + foreach ($termSet as $tax => $term_ids) :
1397 + if (!isset($this->post['tax_input'][$tax])) :
1398 + $this->post['tax_input'][$tax] = array();
1399 + endif;
1400 + $this->post['tax_input'][$tax] = array_merge(
1401 + $this->post['tax_input'][$tax],
1402 + $term_ids
1403 + );
1404 + endforeach;
1094 1405
1095 - if (count($this->post['post_category']) < 1) :
1096 - $this->post['post_category'][] = 1; // Default to category 1 ("Uncategorized" / "General") if nothing else
1097 - endif;
1406 + // Now let's add on the feed and global presets
1407 + foreach ($this->preset_terms as $tax => $term_ids) :
1408 + if (!isset($this->post['tax_input'][$tax])) :
1409 + $this->post['tax_input'][$tax] = array();
1410 + endif;
1411 +
1412 + $this->post['tax_input'][$tax] = array_merge (
1413 + $this->post['tax_input'][$tax],
1414 + $this->category_ids (
1415 + /*terms=*/ $term_ids,
1416 + /*unfamiliar=*/ 'create:'.$tax, // These are presets; for those added in a tagbox editor, the tag may not yet exist
1417 + /*taxonomies=*/ array($tax),
1418 + array(
1419 + 'singleton' => true,
1420 + ))
1421 + );
1422 + endforeach;
1098 1423 endif;
1099 1424 endif;
1100 1425
1101 1426 if (!$this->filtered() and $freshness > 0) :
1102 - unset($this->post['named']);
1427 + // Filter some individual fields
1428 +
1429 + // Allow filters to set post slug. Props niska.
1430 + $post_name = apply_filters('syndicated_post_slug', NULL, $this);
1431 + if (!empty($post_name)) :
1432 + $this->post['post_name'] = $post_name;
1433 + endif;
1434 +
1103 1435 $this->post = apply_filters('syndicated_post', $this->post, $this);
1104 -
1436 +
1105 1437 // Allow for feed-specific syndicated_post filters.
1106 1438 $this->post = apply_filters(
1107 1439 "syndicated_post_".$this->link->uri(),
1108 1440 $this->post,
@@ -1112,195 +1444,129 @@
1112 1444
1113 1445 // Hook in early to make sure these get inserted if at all possible
1114 1446 add_action(
1115 1447 /*hook=*/ 'transition_post_status',
1116 - /*callback=*/ array($this, 'add_rss_meta'),
1448 + /*callback=*/ array(&$this, 'add_rss_meta'),
1117 1449 /*priority=*/ -10000, /* very early */
1118 1450 /*arguments=*/ 3
1119 1451 );
1120 1452
1121 - if (!$this->filtered() and $freshness == 2) :
1122 - // The item has not yet been added. So let's add it.
1123 - $this->insert_new();
1124 - do_action('post_syndicated_item', $this->wp_id(), $this);
1453 + $retval = array(1 => 'updated', 2 => 'new');
1454 +
1455 + $ret = false;
1456 + if (!$this->filtered() and isset($retval[$freshness])) :
1457 + $diag = array(
1458 + 1 => 'Updating existing post # '.$this->wp_id().', "'.$this->post['post_title'].'"',
1459 + 2 => 'Inserting new post "'.$this->post['post_title'].'"',
1460 + );
1461 + FeedWordPress::diagnostic('syndicated_posts', $diag[$freshness]);
1125 1462
1126 - $ret = 'new';
1127 - elseif (!$this->filtered() and $freshness == 1) :
1128 - $this->post['ID'] = $this->wp_id();
1129 - $this->update_existing();
1130 - do_action('update_syndicated_item', $this->wp_id(), $this);
1463 + $this->insert_post(/*update=*/ ($freshness == 1));
1464 +
1465 + $hook = array( 1 => 'update_syndicated_item', 2 => 'post_syndicated_item' );
1466 + do_action($hook[$freshness], $this->wp_id(), $this);
1131 1467
1132 - $ret = 'updated';
1133 - else :
1134 - $ret = false;
1468 + $ret = $retval[$freshness];
1135 1469 endif;
1136 1470
1137 1471 // Remove add_rss_meta hook
1138 1472 remove_action(
1139 1473 /*hook=*/ 'transition_post_status',
1140 - /*callback=*/ array($this, 'add_rss_meta')
1474 + /*callback=*/ array(&$this, 'add_rss_meta'),
1475 + /*priority=*/ -10000, /* very early */
1476 + /*arguments=*/ 3
1141 1477 );
1142 1478
1143 1479 return $ret;
1144 1480 } /* function SyndicatedPost::store () */
1145 1481
1146 - function insert_new () {
1147 - global $wpdb, $wp_db_version;
1482 + function insert_post ($update = false) {
1483 + global $wpdb;
1148 1484
1149 1485 $dbpost = $this->normalize_post(/*new=*/ true);
1150 1486 if (!is_null($dbpost)) :
1151 - if ($this->use_api('wp_insert_post')) :
1152 - $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1487 + $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1153 1488
1154 - // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1155 - add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1489 + // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1490 + add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1156 1491
1157 - // Kludge to prevent kses filters from stripping the
1158 - // content of posts when updating without a logged in
1159 - // user who has `unfiltered_html` capability.
1160 - add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1492 + // Kludge to prevent kses filters from stripping the
1493 + // content of posts when updating without a logged in
1494 + // user who has `unfiltered_html` capability.
1495 + $mungers = array('wp_filter_kses', 'wp_filter_post_kses');
1496 + $removed = array();
1497 + foreach ($mungers as $munger) :
1498 + if (has_filter('content_save_pre', $munger)) :
1499 + remove_filter('content_save_pre', $munger);
1500 + $removed[] = $munger;
1501 + endif;
1502 + endforeach;
1503 +
1504 + if ($update and function_exists('get_post_field')) :
1505 + // Don't munge status fields that the user may
1506 + // have reset manually
1507 + $doNotMunge = array('post_status', 'comment_status', 'ping_status');
1161 1508
1162 - $this->_wp_id = wp_insert_post($dbpost);
1163 -
1164 - // Turn off ridiculous fucking kludges #1 and #2
1165 - remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1166 - remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1167 -
1168 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
1169 -
1170 - // Unfortunately, as of WordPress 2.3, wp_insert_post()
1171 - // *still* offers no way to use a guid of your choice,
1172 - // and munges your post modified timestamp, too.
1173 - $result = $wpdb->query("
1174 - UPDATE $wpdb->posts
1175 - SET
1176 - guid='{$dbpost['guid']}',
1177 - post_modified='{$dbpost['post_modified']}',
1178 - post_modified_gmt='{$dbpost['post_modified_gmt']}'
1179 - WHERE ID='{$this->_wp_id}'
1180 - ");
1181 - else :
1182 - # The right way to do this is the above. But, alas,
1183 - # in earlier versions of WordPress, wp_insert_post has
1184 - # too much behavior (mainly related to pings) that can't
1185 - # be overridden. In WordPress 1.5, it's enough of a
1186 - # resource hog to make PHP segfault after inserting
1187 - # 50-100 posts. This can get pretty annoying, especially
1188 - # if you are trying to update your feeds for the first
1189 - # time.
1190 -
1191 - $result = $wpdb->query("
1192 - INSERT INTO $wpdb->posts
1193 - SET
1194 - guid = '{$dbpost['guid']}',
1195 - post_author = '{$dbpost['post_author']}',
1196 - post_date = '{$dbpost['post_date']}',
1197 - post_date_gmt = '{$dbpost['post_date_gmt']}',
1198 - post_content = '{$dbpost['post_content']}',"
1199 - .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
1200 - post_title = '{$dbpost['post_title']}',
1201 - post_name = '{$dbpost['post_name']}',
1202 - post_modified = '{$dbpost['post_modified']}',
1203 - post_modified_gmt = '{$dbpost['post_modified_gmt']}',
1204 - comment_status = '{$dbpost['comment_status']}',
1205 - ping_status = '{$dbpost['ping_status']}',
1206 - post_status = '{$dbpost['post_status']}'
1207 - ");
1208 - $this->_wp_id = $wpdb->insert_id;
1209 -
1210 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
1211 -
1212 - // WordPress 1.5.x - 2.0.x
1213 - wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
1214 -
1215 - // Since we are not going through official channels, we need to
1216 - // manually tell WordPress that we've published a new post.
1217 - // We need to make sure to do this in order for FeedWordPress
1218 - // to play well with the staticize-reloaded plugin (something
1219 - // that a large aggregator website is going to *want* to be
1220 - // able to use).
1221 - do_action('publish_post', $this->_wp_id);
1509 + foreach ($doNotMunge as $field) :
1510 + $dbpost[$field] = get_post_field($field, $this->wp_id());
1511 + endforeach;
1222 1512 endif;
1223 - endif;
1224 - } /* SyndicatedPost::insert_new() */
1513 +
1514 + // WP3's wp_insert_post scans current_user_can() for the
1515 + // tax_input, with no apparent way to override. Ugh.
1516 + add_action(
1517 + /*hook=*/ 'transition_post_status',
1518 + /*callback=*/ array(&$this, 'add_terms'),
1519 + /*priority=*/ -10001, /* very early */
1520 + /*arguments=*/ 3
1521 + );
1522 +
1523 + // WP3 appears to override whatever you give it for
1524 + // post_modified. Ugh.
1525 + add_action(
1526 + /*hook=*/ 'transition_post_status',
1527 + /*callback=*/ array(&$this, 'fix_post_modified_ts'),
1528 + /*priority=*/ -10000, /* very early */
1529 + /*arguments=*/ 3
1530 + );
1225 1531
1226 - function update_existing () {
1227 - global $wpdb;
1532 + if ($update) :
1533 + $this->post['ID'] = $this->wp_id();
1534 + $dbpost['ID'] = $this->post['ID'];
1535 + endif;
1536 +
1537 + $this->_wp_id = wp_insert_post($dbpost, /*return wp_error=*/ true);
1228 1538
1229 - // Why the fuck doesn't wp_insert_post already do this?
1230 - $dbpost = $this->normalize_post(/*new=*/ false);
1231 - if (!is_null($dbpost)) :
1232 - if ($this->use_api('wp_insert_post')) :
1233 - $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1234 -
1235 - // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1236 - add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1237 -
1238 - // Kludge to prevent kses filters from stripping the
1239 - // content of posts when updating without a logged in
1240 - // user who has `unfiltered_html` capability.
1241 - add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1539 + remove_action(
1540 + /*hook=*/ 'transition_post_status',
1541 + /*callback=*/ array(&$this, 'add_terms'),
1542 + /*priority=*/ -10001, /* very early */
1543 + /*arguments=*/ 3
1544 + );
1242 1545
1243 - // Don't munge status fields that the user may have reset manually
1244 - if (function_exists('get_post_field')) :
1245 - $doNotMunge = array('post_status', 'comment_status', 'ping_status');
1246 - foreach ($doNotMunge as $field) :
1247 - $dbpost[$field] = get_post_field($field, $this->wp_id());
1248 - endforeach;
1249 - endif;
1546 + remove_action(
1547 + /*hook=*/ 'transition_post_status',
1548 + /*callback=*/ array(&$this, 'fix_post_modified_ts'),
1549 + /*priority=*/ -10000, /* very early */
1550 + /*arguments=*/ 3
1551 + );
1250 1552
1251 - $this->_wp_id = wp_insert_post($dbpost);
1553 + // Turn off ridiculous fucking kludges #1 and #2
1554 + remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1555 + foreach ($removed as $filter) :
1556 + add_filter('content_save_pre', $removed);
1557 + endforeach;
1558 +
1559 + $this->validate_post_id($dbpost, $update, array(__CLASS__, __FUNCTION__));
1560 + endif;
1561 + } /* function SyndicatedPost::insert_post () */
1252 1562
1253 - // Turn off ridiculous fucking kludges #1 and #2
1254 - remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1255 - remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1256 -
1257 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
1258 -
1259 - // Unfortunately, as of WordPress 2.3, wp_insert_post()
1260 - // munges your post modified timestamp.
1261 - $result = $wpdb->query("
1262 - UPDATE $wpdb->posts
1263 - SET
1264 - post_modified='{$dbpost['post_modified']}',
1265 - post_modified_gmt='{$dbpost['post_modified_gmt']}'
1266 - WHERE ID='{$this->_wp_id}'
1267 - ");
1268 - else :
1269 -
1270 - $result = $wpdb->query("
1271 - UPDATE $wpdb->posts
1272 - SET
1273 - post_author = '{$dbpost['post_author']}',
1274 - post_content = '{$dbpost['post_content']}',"
1275 - .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
1276 - post_title = '{$dbpost['post_title']}',
1277 - post_name = '{$dbpost['post_name']}',
1278 - post_modified = '{$dbpost['post_modified']}',
1279 - post_modified_gmt = '{$dbpost['post_modified_gmt']}'
1280 - WHERE guid='{$dbpost['guid']}'
1281 - ");
1282 -
1283 - // WordPress 2.1.x and up
1284 - if (function_exists('wp_set_post_categories')) :
1285 - wp_set_post_categories($this->wp_id(), $this->post['post_category']);
1286 - // WordPress 1.5.x - 2.0.x
1287 - elseif (function_exists('wp_set_post_cats')) :
1288 - wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
1289 - // This should never happen.
1290 - else :
1291 - FeedWordPress::critical_bug(__CLASS__.'::'.__FUNCTION.'(): no post categorizing function', array("dbpost" => $dbpost, "this" => $this), __LINE__);
1292 - endif;
1293 -
1294 - // Since we are not going through official channels, we need to
1295 - // manually tell WordPress that we've published a new post.
1296 - // We need to make sure to do this in order for FeedWordPress
1297 - // to play well with the staticize-reloaded plugin (something
1298 - // that a large aggregator website is going to *want* to be
1299 - // able to use).
1300 - do_action('edit_post', $this->post['ID']);
1301 - endif;
1302 - endif;
1563 + function insert_new () {
1564 + $this->insert_post(/*update=*/ false);
1565 + } /* SyndicatedPost::insert_new() */
1566 +
1567 + function update_existing () {
1568 + $this->insert_post(/*update=*/ true);
1303 1569 } /* SyndicatedPost::update_existing() */
1304 1570
1305 1571 /**
1306 1572 * SyndicatedPost::normalize_post()
@@ -1310,12 +1576,34 @@
1310 1576 */
1311 1577 function normalize_post ($new = true) {
1312 1578 global $wpdb;
1313 1579
1314 - $out = array();
1580 + $out = $this->post;
1315 1581
1582 + $fullPost = $out['post_title'].$out['post_content'];
1583 + $fullPost .= (isset($out['post_excerpt']) ? $out['post_excerpt'] : '');
1584 + if (strlen($fullPost) < 1) :
1585 + // FIXME: Option for filtering out empty posts
1586 + endif;
1587 + if (strlen($out['post_title'])==0) :
1588 + $offset = (int) get_option('gmt_offset') * 60 * 60;
1589 + if (isset($this->post['meta']['syndication_source'])) :
1590 + $source_title = $this->post['meta']['syndication_source'];
1591 + else :
1592 + $feed_url = parse_url($this->post['meta']['syndication_feed']);
1593 + $source_title = $feed_url['host'];
1594 + endif;
1595 +
1596 + $out['post_title'] = $source_title
1597 + .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
1598 + // FIXME: Option for what to fill a blank title with...
1599 + endif;
1600 +
1601 + // Normalize the guid if necessary.
1602 + $out['guid'] = SyndicatedPost::normalize_guid($out['guid']);
1603 +
1316 1604 // Why the fuck doesn't wp_insert_post already do this?
1317 - foreach ($this->post as $key => $value) :
1605 + foreach ($out as $key => $value) :
1318 1606 if (is_string($value)) :
1319 1607 $out[$key] = $wpdb->escape($value);
1320 1608 else :
1321 1609 $out[$key] = $value;
@@ -1321,19 +1609,8 @@
1321 1609 $out[$key] = $value;
1322 1610 endif;
1323 1611 endforeach;
1324 1612
1325 - if (strlen($out['post_title'].$out['post_content'].$out['post_excerpt']) == 0) :
1326 - // FIXME: Option for filtering out empty posts
1327 - endif;
1328 - if (strlen($out['post_title'])==0) :
1329 - $offset = (int) get_option('gmt_offset') * 60 * 60;
1330 - $out['post_title'] =
1331 - $this->post['meta']['syndication_source']
1332 - .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
1333 - // FIXME: Option for what to fill a blank title with...
1334 - endif;
1335 -
1336 1613 return $out;
1337 1614 }
1338 1615
1339 1616 /**
@@ -1341,22 +1618,43 @@
1341 1618 *
1342 1619 * @param array $dbpost An array representing the post we attempted to insert or update
1343 1620 * @param mixed $ns A string or array representing the namespace (class, method) whence this method was called.
1344 1621 */
1345 - function validate_post_id ($dbpost, $ns) {
1622 + function validate_post_id ($dbpost, $is_update, $ns) {
1346 1623 if (is_array($ns)) : $ns = implode('::', $ns);
1347 1624 else : $ns = (string) $ns; endif;
1348 -
1625 +
1349 1626 // This should never happen.
1350 1627 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
1351 - FeedWordPress::critical_bug(
1352 - /*name=*/ $ns.'::_wp_id',
1628 + $verb = ($is_update ? 'update existing' : 'insert new');
1629 + $guid = $this->guid();
1630 + $url = $this->permalink();
1631 + $feed = $this->link->uri(array('add_params' => true));
1632 +
1633 + // wp_insert_post failed. Diagnostics, or barf up a critical bug
1634 + // notice if we are in debug mode.
1635 + $mesg = "Failed to $verb item [$guid]. WordPress API returned no valid post ID.\n"
1636 + ."\t\tID = ".serialize($this->_wp_id)."\n"
1637 + ."\t\tURL = ".FeedWordPress::val($url)
1638 + ."\t\tFeed = ".FeedWordPress::val($feed);
1639 + FeedWordPress::diagnostic('updated_feeds:errors', "WordPress API error: $mesg");
1640 + FeedWordPress::diagnostic('feed_items:rejected', $mesg);
1641 +
1642 + $mesg = <<<EOM
1643 +The WordPress API returned an invalid post ID
1644 + when FeedWordPress tried to $verb item $guid
1645 + [URL: $url]
1646 + from the feed at $feed
1647 +
1648 +$ns::_wp_id
1649 +EOM;
1650 + FeedWordPress::noncritical_bug(
1651 + /*message=*/ $mesg,
1353 1652 /*var =*/ array(
1354 1653 "\$this->_wp_id" => $this->_wp_id,
1355 1654 "\$dbpost" => $dbpost,
1356 - "\$this" => $this
1357 1655 ),
1358 - /*line # =*/ __LINE__
1656 + /*line # =*/ __LINE__, /*filename=*/ __FILE__
1359 1657 );
1360 1658 endif;
1361 1659 } /* SyndicatedPost::validate_post_id() */
1362 1660
@@ -1388,87 +1686,171 @@
1388 1686 SET post_author={$this->post['post_author']}
1389 1687 WHERE post_type = 'revision' AND ID='$revision_id'
1390 1688 ");
1391 1689 } /* SyndicatedPost::fix_revision_meta () */
1392 -
1690 +
1393 1691 /**
1394 - * SyndicatedPost::avoid_kses_munge() -- If FeedWordPress is processing
1395 - * an automatic update, that generally means that wp_insert_post() is
1396 - * being called under the user credentials of whoever is viewing the
1397 - * blog at the time -- usually meaning no user at all. But if WordPress
1398 - * gets a wp_insert_post() when current_user_can('unfiltered_html') is
1399 - * false, it will run the content of the post through a kses function
1400 - * that strips out lots of HTML tags -- notably <object> and some others.
1401 - * This causes problems for syndicating (for example) feeds that contain
1402 - * YouTube videos. It also produces an unexpected asymmetry between
1403 - * automatically-initiated updates and updates initiated manually from
1404 - * the WordPress Dashboard (which are usually initiated under the
1405 - * credentials of a logged-in admin, and so don't get run through the
1406 - * kses function). So, to avoid the whole mess, what we do here is
1407 - * just forcibly disable the kses munging for a single syndicated post,
1408 - * by restoring the contents of the `post_content` field.
1692 + * SyndicatedPost::add_terms() -- if FeedWordPress is processing an
1693 + * automatic update, that generally means that wp_insert_post() is being
1694 + * called under the user credentials of whoever is viewing the blog at
1695 + * the time -- which usually means no user at all. But wp_insert_post()
1696 + * checks current_user_can() before assigning any of the terms in a
1697 + * post's tax_input structure -- which is unfortunate, since
1698 + * current_user_can() always returns FALSE when there is no current user
1699 + * logged in. Meaning that automatic updates get no terms assigned.
1409 1700 *
1410 - * @param string $content The content of the post, after other filters have gotten to it
1411 - * @return string The original content of the post, before other filters had a chance to munge it.
1701 + * So, wp_insert_post() is not going to do the term assignments for us.
1702 + * If you want something done right....
1703 + *
1704 + * @param string $new_status Unused action parameter.
1705 + * @param string $old_status Unused action parameter.
1706 + * @param object $post The database record for the post just inserted.
1412 1707 */
1413 - function avoid_kses_munge ($content) {
1414 - global $wpdb;
1415 - return $wpdb->escape($this->post['post_content']);
1416 - }
1417 -
1418 - // SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
1419 - // using the space for custom keys. The set of keys and values to add is
1420 - // specified by the keys and values of $post['meta']. This is used to
1421 - // store anything that the WordPress user might want to access from a
1422 - // template concerning the post's original source that isn't provided
1423 - // for by standard WP meta-data (i.e., any interesting data about the
1424 - // syndicated post other than author, title, timestamp, categories, and
1425 - // guid). It's also used to hook into WordPress's support for
1426 - // enclosures.
1708 + function add_terms ($new_status, $old_status, $post) {
1709 + if ($new_status!='inherit') : // Bail if we are creating a revision.
1710 + if ( is_array($this->post) and isset($this->post['tax_input']) and is_array($this->post['tax_input']) ) :
1711 + foreach ($this->post['tax_input'] as $taxonomy => $terms) :
1712 + if (is_array($terms)) :
1713 + $terms = array_filter($terms); // strip out empties
1714 + endif;
1715 + wp_set_post_terms($post->ID, $terms, $taxonomy);
1716 + endforeach;
1717 + endif;
1718 + endif;
1719 + } /* SyndicatedPost::add_terms () */
1720 +
1721 + /**
1722 + * SyndicatedPost::fix_post_modified_ts() -- We would like to set
1723 + * post_modified and post_modified_gmt to reflect the value of
1724 + * <atom:updated> or equivalent elements on the feed. Unfortunately,
1725 + * wp_insert_post() refuses to acknowledge explicitly-set post_modified
1726 + * fields and overwrites them, either with the post_date (if new) or the
1727 + * current timestamp (if updated).
1728 + *
1729 + * So, wp_insert_post() is not going to do the last-modified assignments
1730 + * for us. If you want something done right....
1731 + *
1732 + * @param string $new_status Unused action parameter.
1733 + * @param string $old_status Unused action parameter.
1734 + * @param object $post The database record for the post just inserted.
1735 + */
1736 + function fix_post_modified_ts ($new_status, $old_status, $post) {
1737 + global $wpdb;
1738 + if ($new_status!='inherit') : // Bail if we are creating a revision.
1739 + $wpdb->update( $wpdb->posts, /*data=*/ array(
1740 + 'post_modified' => $this->post['post_modified'],
1741 + 'post_modified_gmt' => $this->post['post_modified_gmt'],
1742 + ), /*where=*/ array('ID' => $post->ID) );
1743 + endif;
1744 + } /* SyndicatedPost::fix_post_modified_ts () */
1745 +
1746 + /**
1747 + * SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
1748 + * using the space for custom keys. The set of keys and values to add is
1749 + * specified by the keys and values of $post['meta']. This is used to
1750 + * store anything that the WordPress user might want to access from a
1751 + * template concerning the post's original source that isn't provided
1752 + * for by standard WP meta-data (i.e., any interesting data about the
1753 + * syndicated post other than author, title, timestamp, categories, and
1754 + * guid). It's also used to hook into WordPress's support for
1755 + * enclosures.
1756 + *
1757 + * @param string $new_status Unused action parameter.
1758 + * @param string $old_status Unused action parameter.
1759 + * @param object $post The database record for the post just inserted.
1760 + */
1427 1761 function add_rss_meta ($new_status, $old_status, $post) {
1428 1762 global $wpdb;
1429 - if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
1430 - $postId = $this->wp_id();
1431 -
1432 - // Aggregated posts should NOT send out pingbacks.
1433 - // WordPress 2.1-2.2 claim you can tell them not to
1434 - // using $post_pingback, but they don't listen, so we
1435 - // make sure here.
1436 - $result = $wpdb->query("
1437 - DELETE FROM $wpdb->postmeta
1438 - WHERE post_id='$postId' AND meta_key='_pingme'
1439 - ");
1763 + if ($new_status!='inherit') : // Bail if we are creating a revision.
1764 + FeedWordPress::diagnostic('syndicated_posts:meta_data', 'Adding post meta-data: {'.implode(", ", array_keys($this->post['meta'])).'}');
1440 1765
1441 - foreach ( $this->post['meta'] as $key => $values ) :
1442 -
1443 - $eKey = $wpdb->escape($key);
1444 -
1445 - // If this is an update, clear out the old
1446 - // values to avoid duplication.
1766 + if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
1767 + $postId = $post->ID;
1768 +
1769 + // Aggregated posts should NOT send out pingbacks.
1770 + // WordPress 2.1-2.2 claim you can tell them not to
1771 + // using $post_pingback, but they don't listen, so we
1772 + // make sure here.
1447 1773 $result = $wpdb->query("
1448 1774 DELETE FROM $wpdb->postmeta
1449 - WHERE post_id='$postId' AND meta_key='$eKey'
1775 + WHERE post_id='$postId' AND meta_key='_pingme'
1450 1776 ");
1451 -
1452 - // Allow for either a single value or an array
1453 - if (!is_array($values)) $values = array($values);
1454 - foreach ( $values as $value ) :
1455 - add_post_meta($postId, $key, $value, /*unique=*/ false);
1777 +
1778 + foreach ( $this->post['meta'] as $key => $values ) :
1779 + $eKey = $wpdb->escape($key);
1780 +
1781 + // If this is an update, clear out the old
1782 + // values to avoid duplication.
1783 + $result = $wpdb->query("
1784 + DELETE FROM $wpdb->postmeta
1785 + WHERE post_id='$postId' AND meta_key='$eKey'
1786 + ");
1787 +
1788 + // Allow for either a single value or an array
1789 + if (!is_array($values)) $values = array($values);
1790 + foreach ( $values as $value ) :
1791 + FeedWordPress::diagnostic('syndicated_posts:meta_data', "Adding post meta-datum to post [$postId]: [$key] = ".FeedWordPress::val($value, /*no newlines=*/ true));
1792 + add_post_meta($postId, $key, $value, /*unique=*/ false);
1793 + endforeach;
1456 1794 endforeach;
1457 - endforeach;
1795 + endif;
1458 1796 endif;
1459 1797 } /* SyndicatedPost::add_rss_meta () */
1460 1798
1461 - // SyndicatedPost::author_id (): get the ID for an author name from
1462 - // the feed. Create the author if necessary.
1799 + /**
1800 + * SyndicatedPost::author_id (): get the ID for an author name from
1801 + * the feed. Create the author if necessary.
1802 + *
1803 + * @param string $unfamiliar_author
1804 + *
1805 + * @return NULL|int The numeric ID of the author to attribute the post to
1806 + * NULL if the post should be filtered out.
1807 + */
1463 1808 function author_id ($unfamiliar_author = 'create') {
1464 1809 global $wpdb;
1465 1810
1466 - $a = $this->author();
1467 - $author = $a['name'];
1811 + $a = $this->named['author'];
1812 +
1813 + $source = $this->source();
1814 + $forbidden = apply_filters('feedwordpress_forbidden_author_names',
1815 + array('admin', 'administrator', 'www', 'root'));
1816 +
1817 + // Prepare the list of candidates to try for author name: name from
1818 + // feed, original source title (if any), immediate source title live
1819 + // from feed, subscription title, prettied version of feed homepage URL,
1820 + // prettied version of feed URL, or, failing all, use "unknown author"
1821 + // as last resort
1822 +
1823 + $candidates = array();
1824 + $candidates[] = $a['name'];
1825 + if (!is_null($source)) : $candidates[] = $source['title']; endif;
1826 + $candidates[] = $this->link->name(/*fromFeed=*/ true);
1827 + $candidates[] = $this->link->name(/*fromFeed=*/ false);
1828 + if (strlen($this->link->homepage()) > 0) : $candidates[] = feedwordpress_display_url($this->link->homepage()); endif;
1829 + $candidates[] = feedwordpress_display_url($this->link->uri());
1830 + $candidates[] = 'unknown author';
1831 +
1832 + // Pick the first one that works from the list, screening against empty
1833 + // or forbidden names.
1834 +
1835 + $author = NULL;
1836 + while (is_null($author) and ($candidate = each($candidates))) :
1837 + if (!is_null($candidate['value'])
1838 + and (strlen(trim($candidate['value'])) > 0)
1839 + and !in_array(strtolower(trim($candidate['value'])), $forbidden)) :
1840 + $author = $candidate['value'];
1841 + endif;
1842 + endwhile;
1843 +
1468 1844 $email = (isset($a['email']) ? $a['email'] : NULL);
1469 - $url = (isset($a['uri']) ? $a['uri'] : NULL);
1845 + $authorUrl = (isset($a['uri']) ? $a['uri'] : NULL);
1470 1846
1847 +
1848 + $hostUrl = $this->link->homepage();
1849 + if (is_null($hostUrl) or (strlen($hostUrl) < 0)) :
1850 + $hostUrl = $this->link->uri();
1851 + endif;
1852 +
1471 1853 $match_author_by_email = !('yes' == get_option("feedwordpress_do_not_match_author_by_email"));
1472 1854 if ($match_author_by_email and !FeedWordPress::is_null_email($email)) :
1473 1855 $test_email = $email;
1474 1856 else :
@@ -1476,8 +1858,28 @@
1476 1858 endif;
1477 1859
1478 1860 // Never can be too careful...
1479 1861 $login = sanitize_user($author, /*strict=*/ true);
1862 +
1863 + // Possible for, e.g., foreign script author names
1864 + if (strlen($login) < 1) :
1865 + // No usable characters in author name for a login.
1866 + // (Sometimes results from, e.g., foreign scripts.)
1867 + //
1868 + // We just need *something* in Western alphanumerics,
1869 + // so let's try the domain name.
1870 + //
1871 + // Uniqueness will be guaranteed below if necessary.
1872 +
1873 + $url = parse_url($hostUrl);
1874 +
1875 + $login = sanitize_user($url['host'], /*strict=*/ true);
1876 + if (strlen($login) < 1) :
1877 + // This isn't working. Frak it.
1878 + $login = 'syndicated';
1879 + endif;
1880 + endif;
1881 +
1480 1882 $login = apply_filters('pre_user_login', $login);
1481 1883
1482 1884 $nice_author = sanitize_title($author);
1483 1885 $nice_author = apply_filters('pre_user_nicename', $nice_author);
@@ -1485,12 +1887,14 @@
1485 1887 $reg_author = $wpdb->escape(preg_quote($author));
1486 1888 $author = $wpdb->escape($author);
1487 1889 $email = $wpdb->escape($email);
1488 1890 $test_email = $wpdb->escape($test_email);
1489 - $url = $wpdb->escape($url);
1891 + $authorUrl = $wpdb->escape($authorUrl);
1490 1892
1491 1893 // Check for an existing author rule....
1492 - if (isset($this->link->settings['map authors']['name'][strtolower(trim($author))])) :
1894 + if (isset($this->link->settings['map authors']['name']['*'])) :
1895 + $author_rule = $this->link->settings['map authors']['name']['*'];
1896 + elseif (isset($this->link->settings['map authors']['name'][strtolower(trim($author))])) :
1493 1897 $author_rule = $this->link->settings['map authors']['name'][strtolower(trim($author))];
1494 1898 else :
1495 1899 $author_rule = NULL;
1496 1900 endif;
@@ -1508,16 +1912,14 @@
1508 1912
1509 1913 // First try the user core data table.
1510 1914 $id = $wpdb->get_var(
1511 1915 "SELECT ID FROM $wpdb->users
1512 - WHERE
1513 - TRIM(LCASE(user_login)) = TRIM(LCASE('$login'))
1514 - OR (
1515 - LENGTH(TRIM(LCASE(user_email))) > 0
1516 - AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
1517 - )
1518 - OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author'))
1519 - ");
1916 + WHERE TRIM(LCASE(display_name)) = TRIM(LCASE('$author'))
1917 + OR TRIM(LCASE(user_login)) = TRIM(LCASE('$author'))
1918 + OR (
1919 + LENGTH(TRIM(LCASE(user_email))) > 0
1920 + AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
1921 + )");
1520 1922
1521 1923 // If that fails, look for aliases in the user meta data table
1522 1924 if (is_null($id)) :
1523 1925 $id = $wpdb->get_var(
@@ -1544,9 +1946,9 @@
1544 1946 // WordPress 3 is going to pitch a fit if we attempt to register
1545 1947 // more than one user account with an empty e-mail address, so we
1546 1948 // need *something* here. Ugh.
1547 1949 if (strlen($email) == 0 or FeedWordPress::is_null_email($email)) :
1548 - $url = parse_url($this->feed->channel['link']);
1950 + $url = parse_url($hostUrl);
1549 1951 $email = $nice_author.'@'.$url['host'];
1550 1952 endif;
1551 1953
1552 1954 #-- user table data
@@ -1554,12 +1956,39 @@
1554 1956 $userdata['user_login'] = $login;
1555 1957 $userdata['user_nicename'] = $nice_author;
1556 1958 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
1557 1959 $userdata['user_email'] = $email;
1558 - $userdata['user_url'] = $url;
1960 + $userdata['user_url'] = $authorUrl;
1961 + $userdata['nickname'] = $author;
1962 + $parts = preg_split('/\s+/', trim($author), 2);
1963 + if (isset($parts[0])) : $userdata['first_name'] = $parts[0]; endif;
1964 + if (isset($parts[1])) : $userdata['last_name'] = $parts[1]; endif;
1559 1965 $userdata['display_name'] = $author;
1560 -
1561 - $id = wp_insert_user($userdata);
1966 + $userdata['role'] = 'contributor';
1967 +
1968 + do { // Keep trying until you get it right. Or until PHP crashes, I guess.
1969 + $id = wp_insert_user($userdata);
1970 + if (is_wp_error($id)) :
1971 + $codes = $id->get_error_code();
1972 + switch ($codes) :
1973 + case 'empty_user_login' :
1974 + case 'existing_user_login' :
1975 + // Add a random disambiguator
1976 + $userdata['user_login'] .= substr(md5(uniqid(microtime())), 0, 6);
1977 + break;
1978 + case 'existing_user_email' :
1979 + // No disassemble!
1980 + $parts = explode('@', $userdata['user_email'], 2);
1981 +
1982 + // Add a random disambiguator as a gmail-style username extension
1983 + $parts[0] .= '+'.substr(md5(uniqid(microtime())), 0, 6);
1984 +
1985 + // Reassemble
1986 + $userdata['user_email'] = $parts[0].'@'.$parts[1];
1987 + break;
1988 + endswitch;
1989 + endif;
1990 + } while (is_wp_error($id));
1562 1991 elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
1563 1992 $id = (int) $unfamiliar_author;
1564 1993 elseif ($unfamiliar_author === 'default') :
1565 1994 $id = 1;
@@ -1568,16 +1997,38 @@
1568 1997 endif;
1569 1998
1570 1999 if ($id) :
1571 2000 $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
2001 +
2002 + // Multisite: Check whether the author has been recorded
2003 + // on *this* blog before. If not, put her down as a
2004 + // Contributor for *this* blog.
2005 + $user = new WP_User((int) $id);
2006 + if (empty($user->roles)) :
2007 + $user->add_role('contributor');
2008 + endif;
1572 2009 endif;
1573 2010 return $id;
1574 - } // function SyndicatedPost::author_id ()
2011 + } /* function SyndicatedPost::author_id () */
1575 2012
1576 - // look up (and create) category ids from a list of categories
1577 - function category_ids ($cats, $unfamiliar_category = 'create', $tags_too = false) {
1578 - global $wpdb;
2013 + /**
2014 + * category_ids: look up (and create) category ids from a list of categories
2015 + *
2016 + * @param array $cats
2017 + * @param string $unfamiliar_category
2018 + * @param array|null $taxonomies
2019 + * @return array
2020 + */
2021 + function category_ids ($cats, $unfamiliar_category = 'create', $taxonomies = NULL, $params = array()) {
2022 + $singleton = (isset($params['singleton']) ? $params['singleton'] : true);
2023 + $allowFilters = (isset($params['filters']) ? $params['filters'] : false);
2024 +
2025 + $catTax = 'category';
1579 2026
2027 + if (is_null($taxonomies)) :
2028 + $taxonomies = array('category');
2029 + endif;
2030 +
1580 2031 // We need to normalize whitespace because (1) trailing
1581 2032 // whitespace can cause PHP and MySQL not to see eye to eye on
1582 2033 // VARCHAR comparisons for some versions of MySQL (cf.
1583 2034 // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
@@ -1584,96 +2035,100 @@
1584 2035 // because I doubt most people want to make a semantic
1585 2036 // distinction between 'Computers' and 'Computers '
1586 2037 $cats = array_map('trim', $cats);
1587 2038
1588 - $tags = array();
2039 + $terms = array();
2040 + foreach ($taxonomies as $tax) :
2041 + $terms[$tax] = array();
2042 + endforeach;
2043 +
2044 + foreach ($cats as $cat_name) :
2045 + if (preg_match('/^{([^#}]*)#([0-9]+)}$/', $cat_name, $backref)) :
2046 + $cat_id = (int) $backref[2];
2047 + $tax = $backref[1];
2048 + if (strlen($tax) < 1) :
2049 + $tax = $catTax;
2050 + endif;
1589 2051
1590 - $cat_ids = array ();
1591 - foreach ($cats as $cat_name) :
1592 - if (preg_match('/^{#([0-9]+)}$/', $cat_name, $backref)) :
1593 - $cat_id = (int) $backref[1];
1594 - if (function_exists('is_term') and is_term($cat_id, 'category')) :
1595 - $cat_ids[] = $cat_id;
1596 - elseif (get_category($cat_id)) :
1597 - $cat_ids[] = $cat_id;
2052 + $term = term_exists($cat_id, $tax);
2053 + if (!is_wp_error($term) and !!$term) :
2054 + if (!isset($terms[$tax])) :
2055 + $terms[$tax] = array();
2056 + endif;
2057 + $terms[$tax][] = $cat_id;
1598 2058 endif;
1599 2059 elseif (strlen($cat_name) > 0) :
1600 - $esc = $wpdb->escape($cat_name);
1601 - $resc = $wpdb->escape(preg_quote($cat_name));
1602 -
1603 - // WordPress 2.3+
1604 - if (function_exists('is_term')) :
1605 - $cat_id = is_term($cat_name, 'category');
1606 - if ($cat_id) :
1607 - $cat_ids[] = $cat_id['term_id'];
1608 - // There must be a better way to do this...
1609 - elseif ($results = $wpdb->get_results(
1610 - "SELECT term_id
1611 - FROM $wpdb->term_taxonomy
1612 - WHERE
1613 - LOWER(description) RLIKE
1614 - CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)')"
1615 - )) :
1616 - foreach ($results AS $term) :
1617 - $cat_ids[] = (int) $term->term_id;
1618 - endforeach;
1619 - elseif ('tag'==$unfamiliar_category) :
1620 - $tags[] = $cat_name;
1621 - elseif ('create'===$unfamiliar_category) :
1622 - $term = wp_insert_term($cat_name, 'category');
1623 - if (is_wp_error($term)) :
1624 - FeedWordPress::noncritical_bug('term insertion problem', array('cat_name' => $cat_name, 'term' => $term, 'this' => $this), __LINE__);
1625 - else :
1626 - $cat_ids[] = $term['term_id'];
2060 + $familiar = false;
2061 + foreach ($taxonomies as $tax) :
2062 + if ($tax!='category' or strtolower($cat_name)!='uncategorized') :
2063 + $term = term_exists($cat_name, $tax);
2064 + if (!is_wp_error($term) and !!$term) :
2065 + $familiar = true;
2066 +
2067 + if (is_array($term)) :
2068 + $term_id = (int) $term['term_id'];
2069 + else :
2070 + $term_id = (int) $term;
2071 + endif;
2072 +
2073 + if (!isset($terms[$tax])) :
2074 + $terms[$tax] = array();
2075 + endif;
2076 + $terms[$tax][] = $term_id;
2077 + break; // We're done here.
1627 2078 endif;
1628 2079 endif;
2080 + endforeach;
1629 2081
1630 - // WordPress 1.5.x - 2.2.x
1631 - else :
1632 - $results = $wpdb->get_results(
1633 - "SELECT cat_ID
1634 - FROM $wpdb->categories
1635 - WHERE
1636 - (LOWER(cat_name) = LOWER('$esc'))
1637 - OR (LOWER(category_description)
1638 - RLIKE CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)'))
1639 - ");
1640 - if ($results) :
1641 - foreach ($results as $term) :
1642 - $cat_ids[] = (int) $term->cat_ID;
1643 - endforeach;
1644 - elseif ('create'===$unfamiliar_category) :
1645 - if (function_exists('wp_insert_category')) :
1646 - $cat_id = wp_insert_category(array('cat_name' => $esc));
1647 - // And into the database we go.
2082 + if (!$familiar) :
2083 + if ('tag'==$unfamiliar_category) :
2084 + $unfamiliar_category = 'create:post_tag';
2085 + endif;
2086 +
2087 + if (preg_match('/^create(:(.*))?$/i', $unfamiliar_category, $ref)) :
2088 + $tax = $catTax; // Default
2089 + if (isset($ref[2]) and strlen($ref[2]) > 2) :
2090 + $tax = $ref[2];
2091 + endif;
2092 + $term = wp_insert_term($cat_name, $tax);
2093 + if (is_wp_error($term)) :
2094 + FeedWordPress::noncritical_bug('term insertion problem', array('cat_name' => $cat_name, 'term' => $term, 'this' => $this), __LINE__, __FILE__);
1648 2095 else :
1649 - $nice_kitty = sanitize_title($cat_name);
1650 - $wpdb->query(sprintf("
1651 - INSERT INTO $wpdb->categories
1652 - SET
1653 - cat_name='%s',
1654 - category_nicename='%s'
1655 - ", $esc, $nice_kitty
1656 - ));
1657 - $cat_id = $wpdb->insert_id;
2096 + if (!isset($terms[$tax])) :
2097 + $terms[$tax] = array();
2098 + endif;
2099 + $terms[$tax][] = (int) $term['term_id'];
1658 2100 endif;
1659 - $cat_ids[] = $cat_id;
1660 2101 endif;
1661 2102 endif;
1662 2103 endif;
1663 2104 endforeach;
1664 2105
1665 - if ((count($cat_ids) == 0) and ($unfamiliar_category === 'filter')) :
1666 - $cat_ids = NULL; // Drop the post
1667 - else :
1668 - $cat_ids = array_unique($cat_ids);
2106 + $filtersOn = $allowFilters;
2107 + if ($allowFilters) :
2108 + $filters = array_filter(
2109 + $this->link->setting('match/filter', 'match_filter', array()),
2110 + 'remove_dummy_zero'
2111 + );
2112 + $filtersOn = ($filtersOn and is_array($filters) and (count($filters) > 0));
1669 2113 endif;
1670 2114
1671 - if ($tags_too) : $ret = array($cat_ids, $tags);
1672 - else : $ret = $cat_ids;
2115 + // Check for filter conditions
2116 + foreach ($terms as $tax => $term_ids) :
2117 + if ($filtersOn
2118 + and (count($term_ids)==0)
2119 + and in_array($tax, $filters)) :
2120 + $terms = NULL; // Drop the post
2121 + break;
2122 + else :
2123 + $terms[$tax] = array_unique($term_ids);
2124 + endif;
2125 + endforeach;
2126 +
2127 + if ($singleton and count($terms)==1) : // If we only searched one, just return the term IDs
2128 + $terms = end($terms);
1673 2129 endif;
1674 -
1675 - return $ret;
2130 + return $terms;
1676 2131 } // function SyndicatedPost::category_ids ()
1677 2132
1678 2133 function use_api ($tag) {
1679 2134 global $wp_db_version;