PluginProbe
FeedWordPress / 2014.0805
FeedWordPress v2014.0805
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
feedwordpress / syndicatedpost.class.php

syndicatedpost.class.php in FeedWordPress 2014.0805, at syndicatedpost.class.php

2,330 lines 76.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 require_once(dirname(__FILE__).'/feedtime.class.php');
3 require_once(dirname(__FILE__).'/syndicatedpostterm.class.php');
4
5 /**
6 * class SyndicatedPost: FeedWordPress uses to manage the conversion of
7 * incoming items from the feed parser into posts for the WordPress
8 * database. It contains several internal management methods primarily
9 * of interest to someone working on the FeedWordPress source, as well
10 * as some utility methods for extracting useful data from many
11 * different feed formats, which may be useful to FeedWordPress users
12 * who make use of feed data in PHP add-ons and filters.
13 *
14 * @version 2013.0525
15 */
16 class SyndicatedPost {
17 var $item = null; // MagpieRSS representation
18 var $entry = null; // SimplePie_Item representation
19
20 var $link = null;
21 var $feed = null;
22 var $feedmeta = null;
23
24 var $xmlns = array ();
25
26 var $post = array ();
27
28 var $named = array ();
29 var $preset_terms = array ();
30 var $feed_terms = array ();
31
32 var $_freshness = null;
33 var $_wp_id = null;
34 var $_wp_post = null;
35
36 /**
37 * SyndicatedPost constructor: Given a feed item and the source from
38 * which it was taken, prepare a post that can be inserted into the
39 * WordPress database on request, or updated in place if it has already
40 * been syndicated.
41 *
42 * @param array $item The item syndicated from the feed.
43 * @param SyndicatedLink $source The feed it was syndicated from.
44 */
45 function SyndicatedPost ($item, &$source) {
46 global $wpdb;
47
48 if ( empty($item) && empty($source) )
49 return;
50
51 if (is_array($item)
52 and isset($item['simplepie'])
53 and isset($item['magpie'])) :
54 $this->entry = $item['simplepie'];
55 $this->item = $item['magpie'];
56 $item = $item['magpie'];
57 elseif (is_a($item, 'SimplePie_Item')) :
58 $this->entry = $item;
59
60 // convert to Magpie for compat purposes
61 $mp = new MagpieFromSimplePie($source->simplepie, $this->entry);
62 $this->item = $mp->get_item();
63
64 // done with conversion object
65 $mp = NULL; unset($mp);
66 else :
67 $this->item = $item;
68 endif;
69
70 $this->link =& $source;
71 $this->feed = $source->magpie;
72 $this->feedmeta = $source->settings;
73
74 FeedWordPress::diagnostic('feed_items', 'Considering item ['.$this->guid().'] "'.$this->entry->get_title().'"');
75
76 # Dealing with namespaces can get so fucking fucked.
77 $this->xmlns['forward'] = $source->magpie->_XMLNS_FAMILIAR;
78 $this->xmlns['reverse'] = array();
79 foreach ($this->xmlns['forward'] as $url => $ns) :
80 if (!isset($this->xmlns['reverse'][$ns])) :
81 $this->xmlns['reverse'][$ns] = array();
82 endif;
83 $this->xmlns['reverse'][$ns][] = $url;
84 endforeach;
85
86 // Fucking SimplePie.
87 $this->xmlns['reverse']['rss'][] = '';
88
89 # These globals were originally an ugly kludge around a bug in
90 # apply_filters from WordPress 1.5. The bug was fixed in 1.5.1,
91 # and I sure hope at this point that nobody writing filters for
92 # FeedWordPress is still relying on them.
93 #
94 # Anyway, I hereby declare them DEPRECATED as of 8 February
95 # 2010. I'll probably remove the globals within 1-2 releases in
96 # the interests of code hygiene and memory usage. If you
97 # currently use them in your filters, I advise you switch off to
98 # accessing the public members SyndicatedPost::feed and
99 # SyndicatedPost::feedmeta.
100
101 global $fwp_channel, $fwp_feedmeta;
102 $fwp_channel = $this->feed; $fwp_feedmeta = $this->feedmeta;
103
104 // Trigger global syndicated_item filter.
105 $changed = apply_filters('syndicated_item', $this->item, $this);
106 $this->item = $changed;
107
108 // Allow for feed-specific syndicated_item filters.
109 $changed = apply_filters(
110 "syndicated_item_".$source->uri(),
111 $this->item,
112 $this
113 );
114 $this->item = $changed;
115
116 # Filters can halt further processing by returning NULL
117 if (is_null($this->item)) :
118 $this->post = NULL;
119 else :
120 # Note that nothing is run through esc_sql() here.
121 # That's deliberate. The escaping is done at the point
122 # of insertion, not here, to avoid double-escaping and
123 # to avoid screwing with syndicated_post filters
124
125 $this->post['post_title'] = apply_filters(
126 'syndicated_item_title',
127 $this->entry->get_title(), $this
128 );
129
130 $this->named['author'] = apply_filters(
131 'syndicated_item_author',
132 $this->author(), $this
133 );
134 // This just gives us an alphanumeric name for the author.
135 // We look up (or create) the numeric ID for the author
136 // in SyndicatedPost::add().
137
138 $this->post['post_content'] = apply_filters(
139 'syndicated_item_content',
140 $this->content(), $this
141 );
142
143 $excerpt = apply_filters('syndicated_item_excerpt', $this->excerpt(), $this);
144 if (!empty($excerpt)):
145 $this->post['post_excerpt'] = $excerpt;
146 endif;
147
148 // Dealing with timestamps in WordPress is so fucking fucked.
149 $offset = (int) get_option('gmt_offset') * 60 * 60;
150 $post_date_gmt = $this->published(array('default' => -1));
151 $post_modified_gmt = $this->updated(array('default' => -1));
152
153 $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $post_date_gmt);
154 $this->post['post_date'] = gmdate('Y-m-d H:i:s', $post_date_gmt + $offset);
155 $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $post_modified_gmt);
156 $this->post['post_modified'] = gmdate('Y-m-d H:i:s', $post_modified_gmt + $offset);
157
158 // Use feed-level preferences or the global default.
159 $this->post['post_status'] = $this->link->syndicated_status('post', 'publish');
160 $this->post['comment_status'] = $this->link->syndicated_status('comment', 'closed');
161 $this->post['ping_status'] = $this->link->syndicated_status('ping', 'closed');
162
163 // Unique ID (hopefully a unique tag: URI); failing that, the permalink
164 $this->post['guid'] = apply_filters('syndicated_item_guid', $this->guid(), $this);
165
166 // User-supplied custom settings to apply to each post.
167 // Do first so that FWP-generated custom settings will
168 // overwrite if necessary; thus preventing any munging.
169 $postMetaIn = $this->link->postmeta(array("parsed" => true));
170 $postMetaOut = array();
171
172 foreach ($postMetaIn as $key => $meta) :
173 $postMetaOut[$key] = $meta->do_substitutions($this);
174 endforeach;
175
176 foreach ($postMetaOut as $key => $values) :
177 $this->post['meta'][$key] = array();
178 foreach ($values as $value) :
179 $this->post['meta'][$key][] = apply_filters("syndicated_post_meta_{$key}", $value, $this);
180 endforeach;
181 endforeach;
182
183 // RSS 2.0 / Atom 1.0 enclosure support
184 $enclosures = $this->entry->get_enclosures();
185 if (is_array($enclosures)) : foreach ($enclosures as $enclosure) :
186 $this->post['meta']['enclosure'][] =
187 apply_filters('syndicated_item_enclosure_url', $enclosure->get_link(), $this)."\n".
188 apply_filters('syndicated_item_enclosure_length', $enclosure->get_length(), $this)."\n".
189 apply_filters('syndicated_item_enclosure_type', $enclosure->get_type(), $this);
190 endforeach; endif;
191
192 // In case you want to point back to the blog this was
193 // syndicated from.
194
195 $sourcemeta['syndication_source'] = apply_filters(
196 'syndicated_item_source_title',
197 $this->link->name(),
198 $this
199 );
200 $sourcemeta['syndication_source_uri'] = apply_filters(
201 'syndicated_item_source_link',
202 $this->link->homepage(),
203 $this
204 );
205 $sourcemeta['syndication_source_id'] = apply_filters(
206 'syndicated_item_source_id',
207 $this->link->guid(),
208 $this
209 );
210
211 // Make use of atom:source data, if present in an aggregated feed
212 $entry_source = $this->source();
213 if (!is_null($entry_source)) :
214 foreach ($entry_source as $what => $value) :
215 if (!is_null($value)) :
216 if ($what=='title') : $key = 'syndication_source';
217 elseif ($what=='feed') : $key = 'syndication_feed';
218 else : $key = "syndication_source_${what}";
219 endif;
220
221 $sourcemeta["${key}_original"] = apply_filters(
222 'syndicated_item_original_source_'.$what,
223 $value,
224 $this
225 );
226 endif;
227 endforeach;
228 endif;
229
230 foreach ($sourcemeta as $meta_key => $value) :
231 if (!is_null($value)) :
232 $this->post['meta'][$meta_key] = $value;
233 endif;
234 endforeach;
235
236 // Store information on human-readable and machine-readable comment URIs
237
238 // Human-readable comment URI
239 $commentLink = apply_filters('syndicated_item_comments', $this->comment_link(), $this);
240 if (!is_null($commentLink)) : $this->post['meta']['rss:comments'] = $commentLink; endif;
241
242 // Machine-readable content feed URI
243 $commentFeed = apply_filters('syndicated_item_commentrss', $this->comment_feed(), $this);
244 if (!is_null($commentFeed)) : $this->post['meta']['wfw:commentRSS'] = $commentFeed; endif;
245 // Yeah, yeah, now I know that it's supposed to be
246 // wfw:commentRss. Oh well. Path dependence, sucka.
247
248 // Store information to identify the feed that this came from
249 if (isset($this->feedmeta['link/uri'])) :
250 $this->post['meta']['syndication_feed'] = $this->feedmeta['link/uri'];
251 endif;
252 if (isset($this->feedmeta['link/id'])) :
253 $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
254 endif;
255
256 if (isset($this->item['source_link_self'])) :
257 $this->post['meta']['syndication_feed_original'] = $this->item['source_link_self'];
258 endif;
259
260 // In case you want to know the external permalink...
261 $this->post['meta']['syndication_permalink'] = apply_filters('syndicated_item_link', $this->permalink());
262
263 // Store a hash of the post content for checking whether something needs to be updated
264 $this->post['meta']['syndication_item_hash'] = $this->update_hash();
265
266 // Categories: start with default categories, if any.
267 $cats = array();
268 if ('no' != $this->link->setting('add/category', NULL, 'yes')) :
269 $fc = get_option("feedwordpress_syndication_cats");
270 if ($fc) :
271 $cats = array_merge($cats, explode("\n", $fc));
272 endif;
273 endif;
274
275 $fc = $this->link->setting('cats', NULL, array());
276 if (is_array($fc)) :
277 $cats = array_merge($cats, $fc);
278 endif;
279 $this->preset_terms['category'] = $cats;
280
281 // Now add categories from the post, if we have 'em
282 $cats = array();
283 $post_cats = $this->entry->get_categories();
284 if (is_array($post_cats)) : foreach ($post_cats as $cat) :
285 $cat_name = $cat->get_term();
286 if (!$cat_name) : $cat_name = $cat->get_label(); endif;
287
288 if ($this->link->setting('cat_split', NULL, NULL)) :
289 $pcre = "\007".$this->feedmeta['cat_split']."\007";
290 $cats = array_merge(
291 $cats,
292 preg_split(
293 $pcre,
294 $cat_name,
295 -1 /*=no limit*/,
296 PREG_SPLIT_NO_EMPTY
297 )
298 );
299 else :
300 $cats[] = $cat_name;
301 endif;
302 endforeach; endif;
303
304 $this->feed_terms['category'] = apply_filters('syndicated_item_categories', $cats, $this);
305
306 // Tags: start with default tags, if any
307 $tags = array();
308 if ('no' != $this->link->setting('add/post_tag', NULL, 'yes')) :
309 $ft = get_option("feedwordpress_syndication_tags", NULL);
310 $tags = (is_null($ft) ? array() : explode(FEEDWORDPRESS_CAT_SEPARATOR, $ft));
311 endif;
312
313 $ft = $this->link->setting('tags', NULL, array());
314 if (is_array($ft)) :
315 $tags = array_merge($tags, $ft);
316 endif;
317 $this->preset_terms['post_tag'] = $tags;
318
319 // Scan post for /a[@rel='tag'] and use as tags if present
320 $tags = $this->inline_tags();
321 $this->feed_terms['post_tag'] = apply_filters('syndicated_item_tags', $tags, $this);
322
323 $taxonomies = $this->link->taxonomies();
324 $feedTerms = $this->link->setting('terms', NULL, array());
325 $globalTerms = get_option('feedwordpress_syndication_terms', array());
326
327 $specials = array('category' => 'cats', 'post_tag' => 'tags');
328 foreach ($taxonomies as $tax) :
329 if (!isset($specials[$tax])) :
330 $terms = array();
331
332 // See if we should get the globals
333 if ('no' != $this->link->setting("add/$tax", NULL, 'yes')) :
334 if (isset($globalTerms[$tax])) :
335 $terms = $globalTerms[$tax];
336 endif;
337 endif;
338
339 // Now merge in the locals
340 if (isset($feedTerms[$tax])) :
341 $terms = array_merge($terms, $feedTerms[$tax]);
342 endif;
343
344 // That's all, folks.
345 $this->preset_terms[$tax] = $terms;
346 endif;
347 endforeach;
348
349 $this->post['post_type'] = apply_filters('syndicated_post_type', $this->link->setting('syndicated post type', 'syndicated_post_type', 'post'), $this);
350 endif;
351 } /* SyndicatedPost::SyndicatedPost() */
352
353 #####################################
354 #### EXTRACT DATA FROM FEED ITEM ####
355 #####################################
356
357 function substitution_function ($name) {
358 $ret = NULL;
359
360 switch ($name) :
361 // Allowed PHP string functions
362 case 'trim':
363 case 'ltrim':
364 case 'rtrim':
365 case 'strtoupper':
366 case 'strtolower':
367 case 'urlencode':
368 case 'urldecode':
369 $ret = $name;
370 endswitch;
371 return $ret;
372 }
373
374 /**
375 * SyndicatedPost::query uses an XPath-like syntax to query arbitrary
376 * elements within the syndicated item.
377 *
378 * @param string $path
379 * @returns array of string values representing contents of matching
380 * elements or attributes
381 */
382 function query ($path) {
383 $urlHash = array();
384
385 // Allow {url} notation for namespaces. URLs will contain : and /, so...
386 preg_match_all('/{([^}]+)}/', $path, $match, PREG_SET_ORDER);
387 foreach ($match as $ref) :
388 $urlHash[md5($ref[1])] = $ref[1];
389 endforeach;
390
391 foreach ($urlHash as $hash => $url) :
392 $path = str_replace('{'.$url.'}', '{#'.$hash.'}', $path);
393 endforeach;
394
395 $path = explode('/', $path);
396 foreach ($path as $index => $node) :
397 if (preg_match('/{#([^}]+)}/', $node, $ref)) :
398 if (isset($urlHash[$ref[1]])) :
399 $path[$index] = str_replace(
400 '{#'.$ref[1].'}',
401 '{'.$urlHash[$ref[1]].'}',
402 $node
403 );
404 endif;
405 endif;
406 endforeach;
407
408 // Start out with a get_item_tags query.
409 $node = '';
410 while (strlen($node)==0 and !is_null($node)) :
411 $node = array_shift($path);
412 endwhile;
413
414 switch ($node) :
415 case 'feed' :
416 case 'channel' :
417 $node = array_shift($path);
418 $data = $this->get_feed_root_element();
419 $data = array_merge($data, $this->get_feed_channel_elements());
420 break;
421 case 'item' :
422 $node = array_shift($path);
423 default :
424 $data = array($this->entry->data);
425 $method = NULL;
426 endswitch;
427
428 while (!is_null($node)) :
429 if (strlen($node) > 0) :
430 $matches = array();
431
432 list($axis, $element) = $this->xpath_name_and_axis($node);
433
434 foreach ($data as $datum) :
435 if (!is_string($datum) and isset($datum[$axis])) :
436 foreach ($datum[$axis] as $ns => $elements) :
437 if (isset($elements[$element])) :
438 // Potential match.
439 // Check namespace.
440 if (is_string($elements[$element])) : // Attribute
441 $addenda = array($elements[$element]);
442 $contexts = array($datum);
443 else : // Element
444 $addenda = $elements[$element];
445 $contexts = $elements[$element];
446 endif;
447
448 foreach ($addenda as $index => $addendum) :
449 $context = $contexts[$index];
450
451 $namespaces = $this->xpath_possible_namespaces($node, $context);
452 if (in_array($ns, $namespaces)) :
453 $matches[] = $addendum;
454 endif;
455 endforeach;
456 endif;
457 endforeach;
458 endif;
459 endforeach;
460
461 $data = $matches;
462 endif;
463 $node = array_shift($path);
464 endwhile;
465
466 $matches = array();
467 foreach ($data as $datum) :
468 if (is_string($datum)) :
469 $matches[] = $datum;
470 elseif (isset($datum['data'])) :
471 $matches[] = $datum['data'];
472 endif;
473 endforeach;
474 return $matches;
475 } /* SyndicatedPost::query() */
476
477 function get_feed_root_element () {
478 $matches = array();
479 foreach ($this->link->simplepie->data['child'] as $ns => $root) :
480 foreach ($root as $element => $data) :
481 $matches = array_merge($matches, $data);
482 endforeach;
483 endforeach;
484 return $matches;
485 } /* SyndicatedPost::get_feed_root_element() */
486
487 function get_feed_channel_elements () {
488 $rss = array(
489 SIMPLEPIE_NAMESPACE_RSS_090,
490 SIMPLEPIE_NAMESPACE_RSS_10,
491 'http://backend.userland.com/RSS2',
492 SIMPLEPIE_NAMESPACE_RSS_20,
493 );
494
495 $matches = array();
496 foreach ($rss as $ns) :
497 $data = $this->link->simplepie->get_feed_tags($ns, 'channel');
498 if (!is_null($data)) :
499 $matches = array_merge($matches, $data);
500 endif;
501 endforeach;
502 return $matches;
503 } /* SyndicatedPost::get_feed_channel_elements() */
504
505 function xpath_default_namespace () {
506 // Get the default namespace.
507 $type = $this->link->simplepie->get_type();
508 if ($type & SIMPLEPIE_TYPE_ATOM_10) :
509 $defaultNS = SIMPLEPIE_NAMESPACE_ATOM_10;
510 elseif ($type & SIMPLEPIE_TYPE_ATOM_03) :
511 $defaultNS = SIMPLEPIE_NAMESPACE_ATOM_03;
512 elseif ($type & SIMPLEPIE_TYPE_RSS_090) :
513 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_090;
514 elseif ($type & SIMPLEPIE_TYPE_RSS_10) :
515 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_10;
516 elseif ($type & SIMPLEPIE_TYPE_RSS_20) :
517 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20;
518 else :
519 $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20;
520 endif;
521 return $defaultNS;
522 } /* SyndicatedPost::xpath_default_namespace() */
523
524 function xpath_name_and_axis ($node) {
525 $ns = NULL; $element = NULL;
526
527 if (substr($node, 0, 1)=='@') :
528 $axis = 'attribs'; $node = substr($node, 1);
529 else :
530 $axis = 'child';
531 endif;
532
533 if (preg_match('/^{([^}]*)}(.*)$/', $node, $ref)) :
534 $element = $ref[2];
535 elseif (strpos($node, ':') !== FALSE) :
536 list($xmlns, $element) = explode(':', $node, 2);
537 else :
538 $element = $node;
539 endif;
540 return array($axis, $element);
541 } /* SyndicatedPost::xpath_local_name () */
542
543 function xpath_possible_namespaces ($node, $datum = array()) {
544 $ns = NULL; $element = NULL;
545
546 if (substr($node, 0, 1)=='@') :
547 $attr = '@'; $node = substr($node, 1);
548 else :
549 $attr = '';
550 endif;
551
552 if (preg_match('/^{([^}]*)}(.*)$/', $node, $ref)) :
553 $ns = array($ref[1]);
554 elseif (strpos($node, ':') !== FALSE) :
555 list($xmlns, $element) = explode(':', $node, 2);
556
557 if (isset($this->xmlns['reverse'][$xmlns])) :
558 $ns = $this->xmlns['reverse'][$xmlns];
559 else :
560 $ns = array($xmlns);
561 endif;
562
563 // Fucking SimplePie. For attributes in default xmlns.
564 $defaultNS = $this->xpath_default_namespace();
565 if (isset($this->xmlns['forward'][$defaultNS])
566 and ($xmlns==$this->xmlns['forward'][$defaultNS])) :
567 $ns[] = '';
568 endif;
569
570 if (isset($datum['xmlns'])) :
571 if (isset($datum['xmlns'][$xmlns])) :
572 $ns[] = $datum['xmlns'][$xmlns];
573 endif;
574 endif;
575 else :
576 // Often in SimplePie, the default namespace gets stored
577 // as an empty string rather than a URL.
578 $ns = array($this->xpath_default_namespace(), '');
579 endif;
580 return array_unique($ns);
581 } /* SyndicatedPost::xpath_possible_namespaces() */
582
583 function get_categories ($params = array()) {
584 return $this->entry->get_categories();
585 }
586
587 function title ($params = array()) {
588 return $this->entry->get_title();
589 } /* SyndicatedPost::title () */
590
591 function content ($params = array()) {
592 $params = wp_parse_args($params, array(
593 "full only" => false,
594 ));
595
596 $content = NULL;
597
598 // FIXME: This is one of the main places in the code still using
599 // the outmoded SimplePie - to - Magpie construction. We could
600 // replace using SimplePie_Item::get_tags() here. (Or if really
601 // ambitious we could attempt to just use
602 // SimplePie_Item::get_content() with content-only set to TRUE
603 // and some sanitization in effect. -CJ 1jul14
604
605 // atom:content, standard method of providing full HTML content
606 // in Atom feeds.
607 if (isset($this->item['atom_content'])) :
608 $content = $this->item['atom_content'];
609 elseif (isset($this->item['atom']['atom_content'])) :
610 $content = $this->item['atom']['atom_content'];
611
612 // Some exotics: back in the day, before widespread convergence
613 // on content:encoding, some RSS feeds took advantage of XML
614 // namespaces to use an inline xhtml:body or xhtml:div element
615 // for full-content HTML. (E.g. Sam Ruby's feed, IIRC.)
616 elseif (isset($this->item['xhtml']['body'])) :
617 $content = $this->item['xhtml']['body'];
618 elseif (isset($this->item['xhtml']['div'])) :
619 $content = $this->item['xhtml']['div'];
620
621 // content:encoded, most common method of providing full HTML in
622 // RSS 2.0 feeds.
623 elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
624 $content = $this->item['content']['encoded'];
625
626 // Fall back on elements that sometimes may contain full HTML
627 // but sometimes not.
628 elseif (!$params['full only']) :
629
630 // description element is sometimes used for full HTML
631 // sometimes for summary text in RSS. (By the letter of
632 // the standard, it should just be for summary text.)
633 if (isset($this->item['description'])) :
634 $content = $this->item['description'];
635 endif;
636
637 endif;
638 return $content;
639 } /* SyndicatedPost::content() */
640
641 function excerpt () {
642 # Identify and sanitize excerpt: atom:summary, or rss:description
643 $excerpt = $this->entry->get_description();
644
645 # Many RSS feeds use rss:description, inadvisably, to
646 # carry the entire post (typically with escaped HTML).
647 # If that's what happened, we don't want the full
648 # content for the excerpt.
649 $content = $this->content();
650
651 // Ignore whitespace, case, and tag cruft.
652 $theExcerpt = preg_replace('/\s+/', '', strtolower(strip_tags($excerpt)));
653 $theContent = preg_replace('/\s+/', '', strtolower(strip_tags($content)));
654
655 if ( empty($excerpt) or $theExcerpt == $theContent ) :
656 # If content is available, generate an excerpt.
657 if ( strlen(trim($content)) > 0 ) :
658 $excerpt = strip_tags($content);
659 if (strlen($excerpt) > 255) :
660 $excerpt = substr($excerpt,0,252).'...';
661 endif;
662 endif;
663 endif;
664 return $excerpt;
665 } /* SyndicatedPost::excerpt() */
666
667 function permalink () {
668 // Handles explicit <link> elements and also RSS 2.0 cases with
669 // <guid isPermaLink="true">, etc. Hooray!
670 $permalink = $this->entry->get_link();
671 return $permalink;
672 }
673
674 function created ($params = array()) {
675 $unfiltered = false; $default = NULL;
676 extract($params);
677
678 $date = '';
679 if (isset($this->item['dc']['created'])) :
680 $date = $this->item['dc']['created'];
681 elseif (isset($this->item['dcterms']['created'])) :
682 $date = $this->item['dcterms']['created'];
683 elseif (isset($this->item['created'])): // Atom 0.3
684 $date = $this->item['created'];
685 endif;
686
687 $time = new FeedTime($date);
688 $ts = $time->timestamp();
689 if (!$unfiltered) :
690 apply_filters('syndicated_item_created', $ts, $this);
691 endif;
692 return $ts;
693 } /* SyndicatedPost::created() */
694
695 function published ($params = array(), $default = NULL) {
696 $fallback = true; $unfiltered = false;
697 if (!is_array($params)) : // Old style
698 $fallback = $params;
699 else : // New style
700 extract($params);
701 endif;
702
703 $date = '';
704 $ts = null;
705
706 # RSS is a fucking mess. Figure out whether we have a date in
707 # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
708 # epoch format for reformatting. If we can't find anything,
709 # we'll use the last-updated time.
710 if (isset($this->item['dc']['date'])): // Dublin Core
711 $date = $this->item['dc']['date'];
712 elseif (isset($this->item['dcterms']['issued'])) : // Dublin Core extensions
713 $date = $this->item['dcterms']['issued'];
714 elseif (isset($this->item['published'])) : // Atom 1.0
715 $date = $this->item['published'];
716 elseif (isset($this->item['issued'])): // Atom 0.3
717 $date = $this->item['issued'];
718 elseif (isset($this->item['pubdate'])): // RSS 2.0
719 $date = $this->item['pubdate'];
720 endif;
721
722 if (strlen($date) > 0) :
723 $time = new FeedTime($date);
724 $ts = $time->timestamp();
725 elseif ($fallback) : // Fall back to <updated> / <modified> if present
726 $ts = $this->updated(/*fallback=*/ false, /*default=*/ $default);
727 endif;
728
729 # If everything failed, then default to the current time.
730 if (is_null($ts)) :
731 if (-1 == $default) :
732 $ts = time();
733 else :
734 $ts = $default;
735 endif;
736 endif;
737
738 if (!$unfiltered) :
739 $ts = apply_filters('syndicated_item_published', $ts, $this);
740 endif;
741 return $ts;
742 } /* SyndicatedPost::published() */
743
744 function updated ($params = array(), $default = -1) {
745 $fallback = true; $unfiltered = false;
746 if (!is_array($params)) : // Old style
747 $fallback = $params;
748 else : // New style
749 extract($params);
750 endif;
751
752 $date = '';
753 $ts = null;
754
755 # As far as I know, only dcterms and Atom have reliable ways to
756 # specify when something was *modified* last. If neither is
757 # available, then we'll try to get the time of publication.
758 if (isset($this->item['dc']['modified'])) : // Not really correct
759 $date = $this->item['dc']['modified'];
760 elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
761 $date = $this->item['dcterms']['modified'];
762 elseif (isset($this->item['modified'])): // Atom 0.3
763 $date = $this->item['modified'];
764 elseif (isset($this->item['updated'])): // Atom 1.0
765 $date = $this->item['updated'];
766 endif;
767
768 if (strlen($date) > 0) :
769 $time = new FeedTime($date);
770 $ts = $time->timestamp();
771 elseif ($fallback) : // Fall back to issued / dc:date
772 $ts = $this->published(/*fallback=*/ false, /*default=*/ $default);
773 endif;
774
775 # If everything failed, then default to the current time.
776 if (is_null($ts)) :
777 if (-1 == $default) :
778 $ts = time();
779 else :
780 $ts = $default;
781 endif;
782 endif;
783
784 if (!$unfiltered) :
785 apply_filters('syndicated_item_updated', $ts, $this);
786 endif;
787 return $ts;
788 } /* SyndicatedPost::updated() */
789
790 var $_hashes = array();
791 function stored_hashes ($id = NULL) {
792 if (is_null($id)) :
793 $id = $this->wp_id();
794 endif;
795
796 if (!isset($this->_hashes[$id])) :
797 $this->_hashes[$id] = get_post_custom_values(
798 'syndication_item_hash', $id
799 );
800 if (is_null($this->_hashes[$id])) :
801 $this->_hashes[$id] = array();
802 endif;
803 endif;
804 return $this->_hashes[$id];
805 }
806
807 function update_hash ($hashed = true) {
808 // Basis for tracking possible changes to item.
809 $hash = array(
810 "title" => $this->entry->get_title(),
811 "link" => $this->permalink(),
812 "content" => $this->content(),
813 "excerpt" => $this->excerpt(),
814 );
815
816 if ($hashed) :
817 $hash = md5(serialize($hash));
818 endif;
819
820 return $hash;
821 } /* SyndicatedPost::update_hash() */
822
823 static function normalize_guid_prefix () {
824 return trailingslashit(get_bloginfo('url')).'?guid=';
825 }
826
827 static function normalize_guid ($guid) {
828 $guid = trim($guid);
829 if (preg_match('/^[0-9a-z]{32}$/i', $guid)) : // MD5
830 $guid = SyndicatedPost::normalize_guid_prefix().strtolower($guid);
831 elseif ((strlen(esc_url($guid)) == 0) or (esc_url($guid) != $guid)) :
832 $guid = SyndicatedPost::normalize_guid_prefix().md5($guid);
833 endif;
834 $guid = trim($guid);
835 return $guid;
836 } /* SyndicatedPost::normalize_guid() */
837
838 function guid () {
839 $guid = null;
840 if (isset($this->item['id'])): // Atom 0.3 / 1.0
841 $guid = $this->item['id'];
842 elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
843 $guid = $this->item['atom']['id'];
844 elseif (isset($this->item['guid'])) : // RSS 2.0
845 $guid = $this->item['guid'];
846 elseif (isset($this->item['dc']['identifier'])) : // yeah, right
847 $guid = $this->item['dc']['identifier'];
848 endif;
849
850 // Un-set or too long to use as-is. Generate a tag: URI.
851 if (is_null($guid) or strlen($guid) > 250) :
852 // In case we need to check this again
853 $original_guid = $guid;
854
855 // The feed does not seem to have provided us with a
856 // usable unique identifier, so we'll have to cobble
857 // together a tag: URI that might work for us. The base
858 // of the URI will be the host name of the feed source ...
859 $bits = parse_url($this->link->uri());
860 $guid = 'tag:'.$bits['host'];
861
862 // Some ill-mannered feeds (for example, certain feeds
863 // coming from Google Calendar) have extraordinarily long
864 // guids -- so long that they exceed the 255 character
865 // width of the WordPress guid field. But if the string
866 // gets clipped by MySQL, uniqueness tests will fail
867 // forever after and the post will be endlessly
868 // reduplicated. So, instead, Guids Of A Certain Length
869 // are hashed down into a nice, manageable tag: URI.
870 if (!is_null($original_guid)) :
871 $guid .= ',2010-12-03:id.'.md5($original_guid);
872
873 // If we have a date of creation, then we can use that
874 // to uniquely identify the item. (On the other hand, if
875 // the feed producer was consicentious enough to
876 // generate dates of creation, she probably also was
877 // conscientious enough to generate unique identifiers.)
878 elseif (!is_null($this->created())) :
879 $guid .= '://post.'.date('YmdHis', $this->created());
880
881 // Otherwise, use both the URI of the item, *and* the
882 // item's title. We have to use both because titles are
883 // often not unique, and sometimes links aren't unique
884 // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
885 // some podcasts). But it's rare to have *both* the same
886 // title *and* the same link for two different items. So
887 // this is about the best we can do.
888 else :
889 $link = $this->permalink();
890 if (is_null($link)) : $link = $this->link->uri(); endif;
891 $guid .= '://'.md5($link.'/'.$this->item['title']);
892 endif;
893 endif;
894 return $guid;
895 } /* SyndicatedPost::guid() */
896
897 function author () {
898 $author = array ();
899
900 $aa = $this->entry->get_authors();
901 if (count($aa) > 0) :
902 $a = reset($aa);
903
904 $author = array(
905 'name' => $a->get_name(),
906 'email' => $a->get_email(),
907 'uri' => $a->get_link(),
908 );
909 endif;
910
911 if (FEEDWORDPRESS_COMPATIBILITY) :
912 // Search through the MagpieRSS elements: Atom, Dublin Core, RSS
913 if (isset($this->item['author_name'])):
914 $author['name'] = $this->item['author_name'];
915 elseif (isset($this->item['dc']['creator'])):
916 $author['name'] = $this->item['dc']['creator'];
917 elseif (isset($this->item['dc']['contributor'])):
918 $author['name'] = $this->item['dc']['contributor'];
919 elseif (isset($this->feed->channel['dc']['creator'])) :
920 $author['name'] = $this->feed->channel['dc']['creator'];
921 elseif (isset($this->feed->channel['dc']['contributor'])) :
922 $author['name'] = $this->feed->channel['dc']['contributor'];
923 elseif (isset($this->feed->channel['author_name'])) :
924 $author['name'] = $this->feed->channel['author_name'];
925 elseif ($this->feed->is_rss() and isset($this->item['author'])) :
926 // The author element in RSS is allegedly an
927 // e-mail address, but lots of people don't use
928 // it that way. So let's make of it what we can.
929 $author = parse_email_with_realname($this->item['author']);
930
931 if (!isset($author['name'])) :
932 if (isset($author['email'])) :
933 $author['name'] = $author['email'];
934 else :
935 $author['name'] = $this->feed->channel['title'];
936 endif;
937 endif;
938 endif;
939 endif;
940
941 if (!isset($author['name']) or is_null($author['name'])) :
942 // Nothing found. Try some crappy defaults.
943 if ($this->link->name()) :
944 $author['name'] = $this->link->name();
945 else :
946 $url = parse_url($this->link->uri());
947 $author['name'] = $url['host'];
948 endif;
949 endif;
950
951 if (FEEDWORDPRESS_COMPATIBILITY) :
952 if (isset($this->item['author_email'])):
953 $author['email'] = $this->item['author_email'];
954 elseif (isset($this->feed->channel['author_email'])) :
955 $author['email'] = $this->feed->channel['author_email'];
956 endif;
957
958 if (isset($this->item['author_uri'])):
959 $author['uri'] = $this->item['author_uri'];
960 elseif (isset($this->item['author_url'])):
961 $author['uri'] = $this->item['author_url'];
962 elseif (isset($this->feed->channel['author_uri'])) :
963 $author['uri'] = $this->item['author_uri'];
964 elseif (isset($this->feed->channel['author_url'])) :
965 $author['uri'] = $this->item['author_url'];
966 elseif (isset($this->feed->channel['link'])) :
967 $author['uri'] = $this->feed->channel['link'];
968 endif;
969 endif;
970
971 return $author;
972 } /* SyndicatedPost::author() */
973
974 /**
975 * SyndicatedPost::inline_tags: Return a list of all the tags embedded
976 * in post content using the a[@rel="tag"] microformat.
977 *
978 * @since 2010.0630
979 * @return array of string values containing the name of each tag
980 */
981 function inline_tags () {
982 $tags = array();
983 $content = $this->content();
984 $pattern = FeedWordPressHTML::tagWithAttributeRegex('a', 'rel', 'tag');
985 preg_match_all($pattern, $content, $refs, PREG_SET_ORDER);
986 if (count($refs) > 0) :
987 foreach ($refs as $ref) :
988 $tag = FeedWordPressHTML::tagWithAttributeMatch($ref);
989 $tags[] = $tag['content'];
990 endforeach;
991 endif;
992 return $tags;
993 }
994
995 /**
996 * SyndicatedPost::isTaggedAs: Test whether a feed item is
997 * tagged / categorized with a given string. Case and leading and
998 * trailing whitespace are ignored.
999 *
1000 * @param string $tag Tag to check for
1001 *
1002 * @return bool Whether or not at least one of the categories / tags on
1003 * $this->item is set to $tag (modulo case and leading and trailing
1004 * whitespace)
1005 */
1006 function isTaggedAs ($tag) {
1007 $desiredTag = strtolower(trim($tag)); // Normalize case and whitespace
1008
1009 // Check to see if this is tagged with $tag
1010 $currentCategory = 'category';
1011 $currentCategoryNumber = 1;
1012
1013 // If we have the new MagpieRSS, the number of category elements
1014 // on this item is stored under index "category#".
1015 if (isset($this->item['category#'])) :
1016 $numberOfCategories = (int) $this->item['category#'];
1017
1018 // We REALLY shouldn't have the old and busted MagpieRSS, but in
1019 // case we do, it doesn't support multiple categories, but there
1020 // might still be a single value under the "category" index.
1021 elseif (isset($this->item['category'])) :
1022 $numberOfCategories = 1;
1023
1024 // No standard category or tag elements on this feed item.
1025 else :
1026 $numberOfCategories = 0;
1027
1028 endif;
1029
1030 $isSoTagged = false; // Innocent until proven guilty
1031
1032 // Loop through category elements; if there are multiple
1033 // elements, they are indexed as category, category#2,
1034 // category#3, ... category#N
1035 while ($currentCategoryNumber <= $numberOfCategories) :
1036 if ($desiredTag == strtolower(trim($this->item[$currentCategory]))) :
1037 $isSoTagged = true; // Got it!
1038 break;
1039 endif;
1040
1041 $currentCategoryNumber += 1;
1042 $currentCategory = 'category#'.$currentCategoryNumber;
1043 endwhile;
1044
1045 return $isSoTagged;
1046 } /* SyndicatedPost::isTaggedAs() */
1047
1048 /**
1049 * SyndicatedPost::enclosures: returns an array with any enclosures
1050 * that may be attached to this syndicated item.
1051 *
1052 * @param string $type If you only want enclosures that match a certain
1053 * MIME type or group of MIME types, you can limit the enclosures
1054 * that will be returned to only those with a MIME type which
1055 * matches this regular expression.
1056 * @return array
1057 */
1058 function enclosures ($type = '/.*/') {
1059 $enclosures = array();
1060
1061 if (isset($this->item['enclosure#'])) :
1062 // Loop through enclosure, enclosure#2, enclosure#3, ....
1063 for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
1064 $eid = (($i > 1) ? "#{$id}" : "");
1065
1066 // Does it match the type we want?
1067 if (preg_match($type, $this->item["enclosure{$eid}@type"])) :
1068 $enclosures[] = array(
1069 "url" => $this->item["enclosure{$eid}@url"],
1070 "type" => $this->item["enclosure{$eid}@type"],
1071 "length" => $this->item["enclosure{$eid}@length"],
1072 );
1073 endif;
1074 endfor;
1075 endif;
1076 return $enclosures;
1077 } /* SyndicatedPost::enclosures() */
1078
1079 function source ($what = NULL) {
1080 $ret = NULL;
1081 $source = $this->entry->get_source();
1082 if ($source) :
1083 $ret = array();
1084 $ret['title'] = $source->get_title();
1085 $ret['uri'] = $source->get_link();
1086 $ret['feed'] = $source->get_link(0, 'self');
1087
1088 if ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'id')) :
1089 $ret['id'] = $id_tags[0]['data'];
1090 elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'id')) :
1091 $ret['id'] = $id_tags[0]['data'];
1092 elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'guid')) :
1093 $ret['id'] = $id_tags[0]['data'];
1094 elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'guid')) :
1095 $ret['id'] = $id_tags[0]['data'];
1096 elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'guid')) :
1097 $ret['id'] = $id_tags[0]['data'];
1098 endif;
1099 endif;
1100
1101 if (!is_null($what) and is_scalar($what)) :
1102 $ret = $ret[$what];
1103 endif;
1104 return $ret;
1105 }
1106
1107 function comment_link () {
1108 $url = null;
1109
1110 // RSS 2.0 has a standard <comments> element:
1111 // "<comments> is an optional sub-element of <item>. If present,
1112 // it is the url of the comments page for the item."
1113 // <http://cyber.law.harvard.edu/rss/rss.html#ltcommentsgtSubelementOfLtitemgt>
1114 if (isset($this->item['comments'])) :
1115 $url = $this->item['comments'];
1116 endif;
1117
1118 // The convention in Atom feeds is to use a standard <link>
1119 // element with @rel="replies" and @type="text/html".
1120 // Unfortunately, SimplePie_Item::get_links() allows us to filter
1121 // by the value of @rel, but not by the value of @type. *sigh*
1122
1123 // Try Atom 1.0 first
1124 $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link');
1125
1126 // Fall back and try Atom 0.3
1127 if (is_null($linkElements)) : $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link'); endif;
1128
1129 // Now loop through the elements, screening by @rel and @type
1130 if (is_array($linkElements)) : foreach ($linkElements as $link) :
1131 $rel = (isset($link['attribs']['']['rel']) ? $link['attribs']['']['rel'] : 'alternate');
1132 $type = (isset($link['attribs']['']['type']) ? $link['attribs']['']['type'] : NULL);
1133 $href = (isset($link['attribs']['']['href']) ? $link['attribs']['']['href'] : NULL);
1134
1135 if (strtolower($rel)=='replies' and $type=='text/html' and !is_null($href)) :
1136 $url = $href;
1137 endif;
1138 endforeach; endif;
1139
1140 return $url;
1141 }
1142
1143 function comment_feed () {
1144 $feed = null;
1145
1146 // Well Formed Web comment feeds extension for RSS 2.0
1147 // <http://www.sellsbrothers.com/spout/default.aspx?content=archive.htm#exposingRssComments>
1148 //
1149 // N.B.: Correct capitalization is wfw:commentRss, but
1150 // wfw:commentRSS is common in the wild (partly due to a typo in
1151 // the original spec). In any case, our item array is normalized
1152 // to all lowercase anyways.
1153 if (isset($this->item['wfw']['commentrss'])) :
1154 $feed = $this->item['wfw']['commentrss'];
1155 endif;
1156
1157 // In Atom 1.0, the convention is to use a standard link element
1158 // with @rel="replies". Sometimes this is also used to pass a
1159 // link to the human-readable comments page, so we also need to
1160 // check link/@type for a feed MIME type.
1161 //
1162 // Which is why I'm not using the SimplePie_Item::get_links()
1163 // method here, incidentally: it doesn't allow you to filter by
1164 // @type. *sigh*
1165 if (isset($this->item['link_replies'])) :
1166 // There may be multiple <link rel="replies"> elements; feeds have a feed MIME type
1167 $N = isset($this->item['link_replies#']) ? $this->item['link_replies#'] : 1;
1168 for ($i = 1; $i <= $N; $i++) :
1169 $currentElement = 'link_replies'.(($i > 1) ? '#'.$i : '');
1170 if (isset($this->item[$currentElement.'@type'])
1171 and preg_match("\007application/(atom|rss|rdf)\+xml\007i", $this->item[$currentElement.'@type'])) :
1172 $feed = $this->item[$currentElement];
1173 endif;
1174 endfor;
1175 endif;
1176 return $feed;
1177 } /* SyndicatedPost::comment_feed() */
1178
1179 ##################################
1180 #### BUILT-IN CONTENT FILTERS ####
1181 ##################################
1182
1183 var $uri_attrs = array (
1184 array('a', 'href'),
1185 array('applet', 'codebase'),
1186 array('area', 'href'),
1187 array('blockquote', 'cite'),
1188 array('body', 'background'),
1189 array('del', 'cite'),
1190 array('form', 'action'),
1191 array('frame', 'longdesc'),
1192 array('frame', 'src'),
1193 array('iframe', 'longdesc'),
1194 array('iframe', 'src'),
1195 array('head', 'profile'),
1196 array('img', 'longdesc'),
1197 array('img', 'src'),
1198 array('img', 'usemap'),
1199 array('input', 'src'),
1200 array('input', 'usemap'),
1201 array('ins', 'cite'),
1202 array('link', 'href'),
1203 array('object', 'classid'),
1204 array('object', 'codebase'),
1205 array('object', 'data'),
1206 array('object', 'usemap'),
1207 array('q', 'cite'),
1208 array('script', 'src')
1209 ); /* var SyndicatedPost::$uri_attrs */
1210
1211 var $_base = null;
1212
1213 function resolve_single_relative_uri ($refs) {
1214 $tag = FeedWordPressHTML::attributeMatch($refs);
1215 $url = SimplePie_Misc::absolutize_url($tag['value'], $this->_base);
1216 return $tag['prefix'] . $url . $tag['suffix'];
1217 } /* function SyndicatedPost::resolve_single_relative_uri() */
1218
1219 static function resolve_relative_uris ($content, $obj) {
1220 $set = $obj->link->setting('resolve relative', 'resolve_relative', 'yes');
1221 if ($set and $set != 'no') :
1222 // Fallback: if we don't have anything better, use the
1223 // item link from the feed
1224 $obj->_base = $obj->permalink(); // Reset the base for resolving relative URIs
1225
1226 // What we should do here, properly, is to use
1227 // SimplePie_Item::get_base() -- but that method is
1228 // currently broken. Or getting down and dirty in the
1229 // SimplePie representation of the content tags and
1230 // grabbing the xml_base member for the content element.
1231 // Maybe someday...
1232
1233 foreach ($obj->uri_attrs as $pair) :
1234 list($tag, $attr) = $pair;
1235 $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1236 $content = preg_replace_callback (
1237 $pattern,
1238 array($obj, 'resolve_single_relative_uri'),
1239 $content
1240 );
1241 endforeach;
1242 endif;
1243
1244 return $content;
1245 } /* function SyndicatedPost::resolve_relative_uris () */
1246
1247 var $strip_attrs = array (
1248 array('[a-z]+', 'target'),
1249 // array('[a-z]+', 'style'),
1250 // array('[a-z]+', 'on[a-z]+'),
1251 );
1252
1253 function strip_attribute_from_tag ($refs) {
1254 $tag = FeedWordPressHTML::attributeMatch($refs);
1255 return $tag['before_attribute'].$tag['after_attribute'];
1256 }
1257
1258 static function sanitize_content ($content, $obj) {
1259 # This kind of sucks. I intend to replace it with
1260 # lib_filter sometime soon.
1261 foreach ($obj->strip_attrs as $pair):
1262 list($tag,$attr) = $pair;
1263 $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1264
1265 $content = preg_replace_callback (
1266 $pattern,
1267 array($obj, 'strip_attribute_from_tag'),
1268 $content
1269 );
1270 endforeach;
1271 return $content;
1272 } /* SyndicatedPost::sanitize() */
1273
1274 #####################
1275 #### POST STATUS ####
1276 #####################
1277
1278 /**
1279 * SyndicatedPost::filtered: check whether or not this post has been
1280 * screened out by a registered filter.
1281 *
1282 * @return bool TRUE iff post has been filtered out by a previous filter
1283 */
1284 function filtered () {
1285 return is_null($this->post);
1286 } /* SyndicatedPost::filtered() */
1287
1288 /**
1289 * SyndicatedPost::freshness: check whether post is a new post to be
1290 * inserted, a previously syndicated post that needs to be updated to
1291 * match the latest revision, or a previously syndicated post that is
1292 * still up-to-date.
1293 *
1294 * @return int A status code representing the freshness of the post
1295 * -1 = post already syndicated; has a revision that needs to be stored, but not updated to
1296 * 0 = post already syndicated; no update needed
1297 * 1 = post already syndicated, but needs to be updated to latest
1298 * 2 = post has not yet been syndicated; needs to be created
1299 */
1300 function freshness ($format = 'number') {
1301 global $wpdb;
1302
1303 if ($this->filtered()) : // This should never happen.
1304 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1305 endif;
1306
1307 if (is_null($this->_freshness)) : // Not yet checked and cached.
1308 $guid = $this->post['guid'];
1309 $eguid = esc_sql($this->post['guid']);
1310
1311 $q = new WP_Query(array(
1312 'fields' => '_synfresh', // id, guid, post_modified_gmt
1313 'ignore_sticky_posts' => true,
1314 'guid' => $guid,
1315 ));
1316
1317 $old_post = NULL;
1318 if ($q->have_posts()) :
1319 while ($q->have_posts()) : $q->the_post();
1320 $old_post = $q->post;
1321 endwhile;
1322 endif;
1323
1324 if (is_null($old_post)) : // No post with this guid
1325 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$guid.'] "'.$this->entry->get_title().'" is a NEW POST.');
1326 $this->_wp_id = NULL;
1327 $this->_freshness = 2; // New content
1328 else :
1329 // Presume there is nothing new until we find
1330 // something new.
1331 $updated = false;
1332 $live = false;
1333
1334 // Pull the list of existing revisions to get
1335 // timestamps.
1336 $revisions = wp_get_post_revisions($old_post->ID);
1337 foreach ($revisions as $rev) :
1338 $revisions_ts[] = mysql2date('G', $rev->post_modified_gmt);
1339 endforeach;
1340
1341 $revisions_ts[] = mysql2date('G', $old_post->post_modified_gmt);
1342 $last_rev_ts = end($revisions_ts);
1343 $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
1344
1345 // If we have an explicit updated timestamp,
1346 // check that against existing stamps.
1347 if (!is_null($updated_ts)) :
1348 $updated = !in_array($updated_ts, $revisions_ts);
1349
1350 // If this a newer revision, make it go
1351 // live. If an older one, just record
1352 // the contents.
1353 $live = ($updated and ($updated_ts > $last_rev_ts));
1354 endif;
1355
1356 // This is a revision we haven't seen before, judging by the date.
1357
1358 $updatedReason = NULL;
1359 if ($updated) :
1360 $updatedReason = preg_replace(
1361 "/\s+/", " ",
1362 'has been marked with a new timestamp ('
1363 .date('Y-m-d H:i:s', $updated_ts)
1364 ." > "
1365 .date('Y-m-d H:i:s', $last_rev_ts)
1366 .')'
1367 );
1368
1369 // The date does not indicate a new revision, so
1370 // let's check the hash.
1371 else :
1372 // Or the hash...
1373 $hash = $this->update_hash();
1374 $seen = $this->stored_hashes($old_post->ID);
1375 if (count($seen) > 0) :
1376 $updated = !in_array($hash, $seen); // Not seen yet?
1377 else :
1378 $updated = true; // Can't find syndication meta-data
1379 endif;
1380
1381 if ($updated and FeedWordPress::diagnostic_on('feed_items:freshness:reasons')) :
1382 // In the absence of definitive
1383 // timestamp information, we
1384 // just have to assume that a
1385 // hash we haven't seen before
1386 // is a newer version.
1387 $live = true;
1388
1389 $updatedReason = ' has a not-yet-seen update hash: '
1390 .MyPHP::val($hash)
1391 .' not in {'
1392 .implode(", ", array_map(array('FeedWordPress', 'val'), $seen))
1393 .'}. Basis: '
1394 .MyPHP::val(array_keys($this->update_hash(false)));
1395 endif;
1396 endif;
1397
1398 $frozen = false;
1399 if ($updated) : // Ignore if the post is frozen
1400 $frozen = ('yes' == $this->link->setting('freeze updates', 'freeze_updates', NULL));
1401 if (!$frozen) :
1402 $frozen_values = get_post_custom_values('_syndication_freeze_updates', $old_post->ID);
1403 $frozen = (count($frozen_values) > 0 and 'yes' == $frozen_values[0]);
1404
1405 if ($frozen) :
1406 $updatedReason = ' IS BLOCKED FROM BEING UPDATED BY A UPDATE LOCK ON THIS POST, EVEN THOUGH IT '.$updatedReason;
1407 endif;
1408 else :
1409 $updatedReason = ' IS BLOCKED FROM BEING UPDATED BY A FEEDWORDPRESS UPDATE LOCK, EVEN THOUGH IT '.$updatedReason;
1410 endif;
1411 endif;
1412 $live = ($live and !$frozen);
1413
1414 if ($updated) :
1415 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$guid.'] "'.$this->entry->get_title().'" is an update of an existing post.');
1416 if (!is_null($updatedReason)) :
1417 $updatedReason = preg_replace('/\s+/', ' ', $updatedReason);
1418 FeedWordPress::diagnostic('feed_items:freshness:reasons', 'Item ['.$guid.'] "'.$this->entry->get_title().'" '.$updatedReason);
1419 endif;
1420
1421 $this->_freshness = apply_filters('syndicated_item_freshness', ($live ? 1 : -1), $updated, $frozen, $updated_ts, $last_rev_ts, $this);
1422
1423 $this->_wp_id = $old_post->ID;
1424 $this->_wp_post = $old_post;
1425
1426 // We want this to keep a running list of all the
1427 // processed update hashes.
1428 $this->post['meta']['syndication_item_hash'] = array_merge(
1429 $this->stored_hashes(),
1430 array($this->update_hash())
1431 );
1432 else :
1433 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$guid.'] "'.$this->entry->get_title().'" is a duplicate of an existing post.');
1434 $this->_freshness = 0; // Same old, same old
1435 $this->_wp_id = $old_post->ID;
1436 endif;
1437 endif;
1438 endif;
1439
1440 switch ($format) :
1441 case 'status' :
1442 switch ($this->_freshness) :
1443 case -1:
1444 $ret = 'stored';
1445 break;
1446 case 0:
1447 $ret = NULL;
1448 break;
1449 case 1:
1450 $ret = 'updated';
1451 break;
1452 case 2:
1453 default:
1454 $ret = 'new';
1455 break;
1456 endswitch;
1457 break;
1458 case 'number' :
1459 default :
1460 $ret = $this->_freshness;
1461 endswitch;
1462
1463
1464 return $ret;
1465 } /* SyndicatedPost::freshness () */
1466
1467 function has_fresh_content () {
1468 return ( ! $this->filtered() and $this->freshness() != 0 );
1469 } /* SyndicatedPost::has_fresh_content () */
1470
1471 function this_revision_needs_original_post ($freshness = NULL) {
1472 if (is_null($freshness)) :
1473 $freshness = $this->freshness();
1474 endif;
1475 return ( $freshness >= 2 );
1476 }
1477
1478 function this_revision_is_current ($freshness = NULL) {
1479 if (is_null($freshness)) :
1480 $freshness = $this->freshness();
1481 endif;
1482 return ( $freshness >= 1 );
1483 } /* SyndicatedPost::this_revision_is_current () */
1484
1485 function fresh_content_is_update () {
1486 return ($this->freshness() < 2);
1487 } /* SyndicatedPost::fresh_content_is_update () */
1488
1489 function fresh_storage_diagnostic () {
1490 $ret = NULL;
1491 switch ($this->freshness()) :
1492 case -1 :
1493 $ret = 'Storing alternate revision of existing post # '.$this->wp_id().', "'.$this->post['post_title'].'"';
1494 break;
1495 case 1 :
1496 $ret = 'Updating existing post # '.$this->wp_id().', "'.$this->post['post_title'].'"';
1497 break;
1498 case 2 :
1499 default :
1500 $ret = 'Inserting new post "'.$this->post['post_title'].'"';
1501 break;
1502 endswitch;
1503 return $ret;
1504 } /* SyndicatedPost::fresh_storage_diagnostic() */
1505
1506 function fresh_storage_hook () {
1507 $ret = NULL;
1508 switch ($this->freshness()) :
1509 case -1 :
1510 case 1 :
1511 $ret = 'update_syndicated_item';
1512 break;
1513 case 2 :
1514 default :
1515 $ret = 'post_syndicated_item';
1516 break;
1517 endswitch;
1518 return $ret;
1519 } /* SyndicatedPost::fresh_storage_hook () */
1520
1521 #################################################
1522 #### INTERNAL STORAGE AND MANAGEMENT METHODS ####
1523 #################################################
1524
1525 function wp_id () {
1526 if ($this->filtered()) : // This should never happen.
1527 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1528 endif;
1529
1530 if (is_null($this->_wp_id) and is_null($this->_freshness)) :
1531 $fresh = $this->freshness(); // sets WP DB id in the process
1532 endif;
1533 return $this->_wp_id;
1534 }
1535
1536 function store () {
1537 global $wpdb;
1538
1539 if ($this->filtered()) : // This should never happen.
1540 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1541 endif;
1542
1543 $freshness = $this->freshness();
1544 if ($this->has_fresh_content()) :
1545 # -- Look up, or create, numeric ID for author
1546 $this->post['post_author'] = $this->author_id (
1547 $this->link->setting('unfamiliar author', 'unfamiliar_author', 'create')
1548 );
1549
1550 if (is_null($this->post['post_author'])) :
1551 FeedWordPress::diagnostic('feed_items:rejected', 'Filtered out item ['.$this->guid().'] without syndication: no author available');
1552 $this->post = NULL;
1553 endif;
1554 endif;
1555
1556 // We have to check again in case post has been filtered during
1557 // the author_id lookup
1558 if ($this->has_fresh_content()) :
1559 $consider = array(
1560 'category' => array('abbr' => 'cats', 'domain' => array('category', 'post_tag')),
1561 'post_tag' => array('abbr' => 'tags', 'domain' => array('post_tag')),
1562 );
1563
1564 $termSet = array(); $valid = null;
1565 foreach ($consider as $what => $taxes) :
1566 if (!is_null($this->post)) : // Not filtered out yet
1567 # -- Look up, or create, numeric ID for categories
1568 $taxonomies = $this->link->setting("match/".$taxes['abbr'], 'match_'.$taxes['abbr'], $taxes['domain']);
1569
1570 // Eliminate dummy variables
1571 $taxonomies = array_filter($taxonomies, 'remove_dummy_zero');
1572
1573 $terms = $this->category_ids (
1574 $this->feed_terms[$what],
1575 $this->link->setting("unfamiliar {$what}", "unfamiliar_{$what}", 'create:'.$what),
1576 /*taxonomies=*/ $taxonomies,
1577 array(
1578 'singleton' => false, // I don't like surprises
1579 'filters' => true,
1580 )
1581 );
1582
1583 if (is_null($terms) or is_null($termSet)) :
1584 // filtered out -- no matches
1585 else :
1586 $valid = true;
1587
1588 // filter mode off, or at least one match
1589 foreach ($terms as $tax => $term_ids) :
1590 if (!isset($termSet[$tax])) :
1591 $termSet[$tax] = array();
1592 endif;
1593 $termSet[$tax] = array_merge($termSet[$tax], $term_ids);
1594 endforeach;
1595 endif;
1596 endif;
1597 endforeach;
1598
1599 if (is_null($valid)) : // Plonked
1600 $this->post = NULL;
1601 else : // We can proceed
1602 $this->post['tax_input'] = array();
1603 foreach ($termSet as $tax => $term_ids) :
1604 if (!isset($this->post['tax_input'][$tax])) :
1605 $this->post['tax_input'][$tax] = array();
1606 endif;
1607 $this->post['tax_input'][$tax] = array_merge(
1608 $this->post['tax_input'][$tax],
1609 $term_ids
1610 );
1611 endforeach;
1612
1613 // Now let's add on the feed and global presets
1614 foreach ($this->preset_terms as $tax => $term_ids) :
1615 if (!isset($this->post['tax_input'][$tax])) :
1616 $this->post['tax_input'][$tax] = array();
1617 endif;
1618
1619 $this->post['tax_input'][$tax] = array_merge (
1620 $this->post['tax_input'][$tax],
1621 $this->category_ids (
1622 /*terms=*/ $term_ids,
1623 /*unfamiliar=*/ 'create:'.$tax, // These are presets; for those added in a tagbox editor, the tag may not yet exist
1624 /*taxonomies=*/ array($tax),
1625 array(
1626 'singleton' => true,
1627 ))
1628 );
1629 endforeach;
1630 endif;
1631 endif;
1632
1633 // We have to check again in case the post has been filtered
1634 // during the category/tags/taxonomy terms lookup
1635 if ($this->has_fresh_content()) :
1636 // Filter some individual fields
1637
1638 // If there already is a post slug (from syndication or by manual
1639 // editing) don't cause WP to overwrite it by sending in a NULL
1640 // post_name. Props Chris Fritz 2012-11-28.
1641 $post_name = (is_null($this->_wp_post) ? NULL : $this->_wp_post->post_name);
1642
1643 // Allow filters to set post slug. Props niska.
1644 $post_name = apply_filters('syndicated_post_slug', $post_name, $this);
1645 if (!empty($post_name)) :
1646 $this->post['post_name'] = $post_name;
1647 endif;
1648
1649 $this->post = apply_filters('syndicated_post', $this->post, $this);
1650
1651 // Allow for feed-specific syndicated_post filters.
1652 $this->post = apply_filters(
1653 "syndicated_post_".$this->link->uri(),
1654 $this->post,
1655 $this
1656 );
1657 endif;
1658
1659 // Hook in early to make sure these get inserted if at all possible
1660 add_action(
1661 /*hook=*/ 'transition_post_status',
1662 /*callback=*/ array($this, 'add_rss_meta'),
1663 /*priority=*/ -10000, /* very early */
1664 /*arguments=*/ 3
1665 );
1666
1667 $ret = false;
1668 if ($this->has_fresh_content()) :
1669 $diag = $this->fresh_storage_diagnostic();
1670 if (!is_null($diag)) :
1671 FeedWordPress::diagnostic('syndicated_posts', $diag);
1672 endif;
1673
1674 $this->insert_post(/*update=*/ $this->fresh_content_is_update(), $this->freshness());
1675
1676 $hook = $this->fresh_storage_hook();
1677 if (!is_null($hook)) :
1678 do_action($hook, $this->wp_id(), $this);
1679 endif;
1680
1681 $ret = $this->freshness('status');
1682 endif;
1683
1684 // If this is a legit, non-filtered post, tag it as found on the
1685 // feed regardless of fresh or stale status
1686 if (!$this->filtered()) :
1687 $key = '_feedwordpress_retire_me_' . $this->link->id;
1688 delete_post_meta($this->wp_id(), $key);
1689
1690 $status = get_post_field('post_status', $this->wp_id());
1691 if ('fwpretired'==$status and $this->link->is_non_incremental()) :
1692 FeedWordPress::diagnostic('syndicated_posts', "Un-retiring previously retired post # ".$this->wp_id()." due to re-appearance on non-incremental feed.");
1693 set_post_field('post_status', $this->post['post_status'], $this->wp_id());
1694 wp_transition_post_status($this->post['post_status'], $status, $this->post);
1695 elseif ('fwpzapped'==$status) :
1696 // Set this new revision up to be
1697 // blanked on the next update.
1698 add_post_meta($this->wp_id(), '_feedwordpress_zapped_blank_me', 2, /*single=*/ true);
1699 endif;
1700 endif;
1701
1702 // Remove add_rss_meta hook
1703 remove_action(
1704 /*hook=*/ 'transition_post_status',
1705 /*callback=*/ array($this, 'add_rss_meta'),
1706 /*priority=*/ -10000, /* very early */
1707 /*arguments=*/ 3
1708 );
1709
1710 return $ret;
1711 } /* function SyndicatedPost::store () */
1712
1713 function insert_post ($update = false, $freshness = 2) {
1714 global $wpdb;
1715
1716 $dbpost = $this->normalize_post(/*new=*/ true);
1717
1718 if (!is_null($dbpost)) :
1719 $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1720
1721 // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1722 add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1723
1724 // Kludge to prevent kses filters from stripping the
1725 // content of posts when updating without a logged in
1726 // user who has `unfiltered_html` capability.
1727 $mungers = array('wp_filter_kses', 'wp_filter_post_kses');
1728 $removed = array();
1729 foreach ($mungers as $munger) :
1730 if (has_filter('content_save_pre', $munger)) :
1731 remove_filter('content_save_pre', $munger);
1732 $removed[] = $munger;
1733 endif;
1734 endforeach;
1735
1736 if ($update and function_exists('get_post_field')) :
1737 // Don't munge status fields that the user may
1738 // have reset manually
1739 $doNotMunge = array('post_status', 'comment_status', 'ping_status');
1740
1741 foreach ($doNotMunge as $field) :
1742 $dbpost[$field] = get_post_field($field, $this->wp_id());
1743 endforeach;
1744 endif;
1745
1746 // WP3's wp_insert_post scans current_user_can() for the
1747 // tax_input, with no apparent way to override. Ugh.
1748 add_action(
1749 /*hook=*/ 'transition_post_status',
1750 /*callback=*/ array($this, 'add_terms'),
1751 /*priority=*/ -10001, /* very early */
1752 /*arguments=*/ 3
1753 );
1754
1755 // WP3 appears to override whatever you give it for
1756 // post_modified. Ugh.
1757 add_action(
1758 /*hook=*/ 'transition_post_status',
1759 /*callback=*/ array($this, 'fix_post_modified_ts'),
1760 /*priority=*/ -10000, /* very early */
1761 /*arguments=*/ 3
1762 );
1763
1764 if ($update) :
1765 $this->post['ID'] = $this->wp_id();
1766 $dbpost['ID'] = $this->post['ID'];
1767 endif;
1768
1769 // O.K., is this a new post? If so, we need to create
1770 // the basic post record before we do anything else.
1771 if ($this->this_revision_needs_original_post()) :
1772 // *sigh*, for handling inconsistent slash expectations < 3.6
1773 $sdbpost = $this->db_sanitize_post($dbpost);
1774
1775 // Go ahead and insert the first post record to
1776 // anchor the revision history.
1777 $this->_wp_id = wp_insert_post($sdbpost, /*return wp_error=*/ true);
1778
1779 $dbpost['ID'] = $this->_wp_id;
1780 endif;
1781
1782 // Now that we've made sure the original exists, insert
1783 // this version here as a revision.
1784 $revision_id = _wp_put_post_revision($dbpost, /*autosave=*/ false);
1785
1786 if (!$this->this_revision_needs_original_post()) :
1787
1788 if ($this->this_revision_is_current()) :
1789
1790 wp_restore_post_revision($revision_id);
1791
1792 else :
1793
1794 // If we do not activate this revision, then the
1795 // add_rss_meta will not be called, which is
1796 // more or less as it should be, but that means
1797 // we have to actively record this revision's
1798 // update hash from here.
1799 $postId = $this->post['ID'];
1800 $key = 'syndication_item_hash';
1801 $hash = $this->update_hash();
1802 FeedWordPress::diagnostic('syndicated_posts:meta_data', "Adding post meta-datum to post [$postId]: [$key] = ".FeedWordPress::val($hash, /*no newlines=*/ true));
1803 add_post_meta( $postId, $key, $hash, /*unique=*/ false );
1804 endif;
1805 endif;
1806
1807 remove_action(
1808 /*hook=*/ 'transition_post_status',
1809 /*callback=*/ array($this, 'add_terms'),
1810 /*priority=*/ -10001, /* very early */
1811 /*arguments=*/ 3
1812 );
1813
1814 remove_action(
1815 /*hook=*/ 'transition_post_status',
1816 /*callback=*/ array($this, 'fix_post_modified_ts'),
1817 /*priority=*/ -10000, /* very early */
1818 /*arguments=*/ 3
1819 );
1820
1821 // Turn off ridiculous fucking kludges #1 and #2
1822 remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1823 foreach ($removed as $filter) :
1824 add_filter('content_save_pre', $filter);
1825 endforeach;
1826
1827 $this->validate_post_id($dbpost, $update, array(__CLASS__, __FUNCTION__));
1828 endif;
1829 } /* function SyndicatedPost::insert_post () */
1830
1831 function insert_new () {
1832 $this->insert_post(/*update=*/ false, 1);
1833 } /* SyndicatedPost::insert_new() */
1834
1835 function update_existing () {
1836 $this->insert_post(/*update=*/ true, 2);
1837 } /* SyndicatedPost::update_existing() */
1838
1839 /**
1840 * SyndicatedPost::normalize_post()
1841 *
1842 * @param bool $new If true, this post is to be inserted anew. If false, it is an update of an existing post.
1843 * @return array A normalized representation of the post ready to be inserted into the database or sent to the WordPress API functions
1844 */
1845 function normalize_post ($new = true) {
1846 global $wpdb;
1847
1848 $out = $this->post;
1849
1850 $fullPost = $out['post_title'].$out['post_content'];
1851 $fullPost .= (isset($out['post_excerpt']) ? $out['post_excerpt'] : '');
1852 if (strlen($fullPost) < 1) :
1853 // FIXME: Option for filtering out empty posts
1854 endif;
1855 if (strlen($out['post_title'])==0) :
1856 $offset = (int) get_option('gmt_offset') * 60 * 60;
1857 if (isset($this->post['meta']['syndication_source'])) :
1858 $source_title = $this->post['meta']['syndication_source'];
1859 else :
1860 $feed_url = parse_url($this->post['meta']['syndication_feed']);
1861 $source_title = $feed_url['host'];
1862 endif;
1863
1864 $out['post_title'] = $source_title
1865 .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
1866 // FIXME: Option for what to fill a blank title with...
1867 endif;
1868
1869 // Normalize the guid if necessary.
1870 $out['guid'] = SyndicatedPost::normalize_guid($out['guid']);
1871
1872 return $out;
1873 }
1874
1875 function db_sanitize_post ($out) {
1876 // < 3.6. Core API, including wp_insert_post(), will expect
1877 // properly slashed data. If wp_slash() exists, then this is
1878 // after the big change-over, and wp_insert_post() etc. will
1879 // expect *un*-slashed data.
1880 if (!function_exists('wp_slash')) :
1881
1882 foreach ($out as $key => $value) :
1883 if (is_string($value)) :
1884 $out[$key] = esc_sql($value);
1885 else :
1886 $out[$key] = $value;
1887 endif;
1888 endforeach;
1889
1890 endif;
1891
1892 return $out;
1893 }
1894
1895 /**
1896 * SyndicatedPost::validate_post_id()
1897 *
1898 * @param array $dbpost An array representing the post we attempted to insert or update
1899 * @param mixed $ns A string or array representing the namespace (class, method) whence this method was called.
1900 */
1901 function validate_post_id ($dbpost, $is_update, $ns) {
1902 if (is_array($ns)) : $ns = implode('::', $ns);
1903 else : $ns = (string) $ns; endif;
1904
1905 // This should never happen.
1906 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
1907 $verb = ($is_update ? 'update existing' : 'insert new');
1908 $guid = $this->guid();
1909 $url = $this->permalink();
1910 $feed = $this->link->uri(array('add_params' => true));
1911
1912 // wp_insert_post failed. Diagnostics, or barf up a critical bug
1913 // notice if we are in debug mode.
1914 $mesg = "Failed to $verb item [$guid]. WordPress API returned no valid post ID.\n"
1915 ."\t\tID = ".serialize($this->_wp_id)."\n"
1916 ."\t\tURL = ".MyPHP::val($url)
1917 ."\t\tFeed = ".MyPHP::val($feed);
1918
1919 FeedWordPress::diagnostic('updated_feeds:errors', "WordPress API error: $mesg");
1920 FeedWordPress::diagnostic('feed_items:rejected', $mesg);
1921
1922 $mesg = <<<EOM
1923 The WordPress API returned an invalid post ID
1924 when FeedWordPress tried to $verb item $guid
1925 [URL: $url]
1926 from the feed at $feed
1927
1928 $ns::_wp_id
1929 EOM;
1930 FeedWordPress::noncritical_bug(
1931 /*message=*/ $mesg,
1932 /*var =*/ array(
1933 "\$this->_wp_id" => $this->_wp_id,
1934 "\$dbpost" => $dbpost,
1935 ),
1936 /*line # =*/ __LINE__, /*filename=*/ __FILE__
1937 );
1938 endif;
1939 } /* SyndicatedPost::validate_post_id() */
1940
1941 /**
1942 * SyndicatedPost::fix_revision_meta() - Ensures that we get the meta
1943 * data (authorship, guid, etc.) that we want when storing revisions of
1944 * a syndicated post.
1945 *
1946 * In their infinite wisdom, the WordPress coders seem to have made it
1947 * completely impossible for a plugin that uses wp_insert_post() to set
1948 * certain meta-data (such as the author) when you store an old revision
1949 * of an updated post. Instead, it uses the WordPress defaults (= cur.
1950 * active user ID if the process is running with a user logged in, or
1951 * = #0 if there is no user logged in). This results in bogus authorship
1952 * data for revisions that are syndicated from off the feed, unless we
1953 * use a ridiculous kludge like this to end-run the munging of meta-data
1954 * by _wp_put_post_revision.
1955 *
1956 * @param int $revision_id The revision ID to fix up meta-data
1957 */
1958 function fix_revision_meta ($revision_id) {
1959 global $wpdb;
1960
1961 $post_author = (int) $this->post['post_author'];
1962
1963 $revision_id = (int) $revision_id;
1964
1965 // Let's fix the author.
1966 set_post_field('post_author', $this->post['post_author'], $revision_id);
1967
1968 // Let's fix the GUID to a dummy URL with the update hash.
1969 set_post_field('guid', 'http://feedwordpress.radgeek.com/?rev='.$this->update_hash(), $revision_id);
1970
1971 // Let's fire an event for add-ons and filters
1972 do_action('syndicated_post_fix_revision_meta', $revision_id, $this);
1973
1974 } /* SyndicatedPost::fix_revision_meta () */
1975
1976 /**
1977 * SyndicatedPost::add_terms() -- if FeedWordPress is processing an
1978 * automatic update, that generally means that wp_insert_post() is being
1979 * called under the user credentials of whoever is viewing the blog at
1980 * the time -- which usually means no user at all. But wp_insert_post()
1981 * checks current_user_can() before assigning any of the terms in a
1982 * post's tax_input structure -- which is unfortunate, since
1983 * current_user_can() always returns FALSE when there is no current user
1984 * logged in. Meaning that automatic updates get no terms assigned.
1985 *
1986 * So, wp_insert_post() is not going to do the term assignments for us.
1987 * If you want something done right....
1988 *
1989 * @param string $new_status Unused action parameter.
1990 * @param string $old_status Unused action parameter.
1991 * @param object $post The database record for the post just inserted.
1992 */
1993 function add_terms ($new_status, $old_status, $post) {
1994
1995 if ($new_status!='inherit') : // Bail if we are creating a revision.
1996 if ( is_array($this->post) and isset($this->post['tax_input']) and is_array($this->post['tax_input']) ) :
1997 foreach ($this->post['tax_input'] as $taxonomy => $terms) :
1998 if (is_array($terms)) :
1999 $terms = array_filter($terms); // strip out empties
2000 endif;
2001
2002 $res = wp_set_post_terms(
2003 /*post_id=*/ $post->ID,
2004 /*terms=*/ $terms,
2005 /*taxonomy=*/ $taxonomy
2006 );
2007
2008 FeedWordPress::diagnostic(
2009 'syndicated_posts:categories',
2010 'Category: post('.json_encode($post->ID).') '.$taxonomy
2011 .' := '
2012 .json_encode($terms)
2013 .' / result: '
2014 .json_encode($res)
2015 );
2016
2017 endforeach;
2018 endif;
2019 endif;
2020
2021 } /* SyndicatedPost::add_terms () */
2022
2023 /**
2024 * SyndicatedPost::fix_post_modified_ts() -- We would like to set
2025 * post_modified and post_modified_gmt to reflect the value of
2026 * <atom:updated> or equivalent elements on the feed. Unfortunately,
2027 * wp_insert_post() refuses to acknowledge explicitly-set post_modified
2028 * fields and overwrites them, either with the post_date (if new) or the
2029 * current timestamp (if updated).
2030 *
2031 * So, wp_insert_post() is not going to do the last-modified assignments
2032 * for us. If you want something done right....
2033 *
2034 * @param string $new_status Unused action parameter.
2035 * @param string $old_status Unused action parameter.
2036 * @param object $post The database record for the post just inserted.
2037 */
2038 function fix_post_modified_ts ($new_status, $old_status, $post) {
2039 global $wpdb;
2040 if ($new_status!='inherit') : // Bail if we are creating a revision.
2041 $wpdb->update( $wpdb->posts, /*data=*/ array(
2042 'post_modified' => $this->post['post_modified'],
2043 'post_modified_gmt' => $this->post['post_modified_gmt'],
2044 ), /*where=*/ array('ID' => $post->ID) );
2045 endif;
2046 } /* SyndicatedPost::fix_post_modified_ts () */
2047
2048 /**
2049 * SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
2050 * using the space for custom keys. The set of keys and values to add is
2051 * specified by the keys and values of $post['meta']. This is used to
2052 * store anything that the WordPress user might want to access from a
2053 * template concerning the post's original source that isn't provided
2054 * for by standard WP meta-data (i.e., any interesting data about the
2055 * syndicated post other than author, title, timestamp, categories, and
2056 * guid). It's also used to hook into WordPress's support for
2057 * enclosures.
2058 *
2059 * @param string $new_status Unused action parameter.
2060 * @param string $old_status Unused action parameter.
2061 * @param object $post The database record for the post just inserted.
2062 */
2063 function add_rss_meta ($new_status, $old_status, $post) {
2064 global $wpdb;
2065 if ($new_status!='inherit') : // Bail if we are creating a revision.
2066 FeedWordPress::diagnostic('syndicated_posts:meta_data', 'Adding post meta-data: {'.implode(", ", array_keys($this->post['meta'])).'}');
2067
2068 if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
2069 $postId = $post->ID;
2070
2071 // Aggregated posts should NOT send out pingbacks.
2072 // WordPress 2.1-2.2 claim you can tell them not to
2073 // using $post_pingback, but they don't listen, so we
2074 // make sure here.
2075 $result = $wpdb->query("
2076 DELETE FROM $wpdb->postmeta
2077 WHERE post_id='$postId' AND meta_key='_pingme'
2078 ");
2079
2080 foreach ( $this->post['meta'] as $key => $values ) :
2081 $eKey = esc_sql($key);
2082
2083 // If this is an update, clear out the old
2084 // values to avoid duplication.
2085 $result = $wpdb->query("
2086 DELETE FROM $wpdb->postmeta
2087 WHERE post_id='$postId' AND meta_key='$eKey'
2088 ");
2089
2090 // Allow for either a single value or an array
2091 if (!is_array($values)) $values = array($values);
2092 foreach ( $values as $value ) :
2093 FeedWordPress::diagnostic('syndicated_posts:meta_data', "Adding post meta-datum to post [$postId]: [$key] = ".MyPHP::val($value, /*no newlines=*/ true));
2094 add_post_meta($postId, $key, $value, /*unique=*/ false);
2095 endforeach;
2096 endforeach;
2097 endif;
2098 endif;
2099 } /* SyndicatedPost::add_rss_meta () */
2100
2101 /**
2102 * SyndicatedPost::author_id (): get the ID for an author name from
2103 * the feed. Create the author if necessary.
2104 *
2105 * @param string $unfamiliar_author
2106 *
2107 * @return NULL|int The numeric ID of the author to attribute the post to
2108 * NULL if the post should be filtered out.
2109 */
2110 function author_id ($unfamiliar_author = 'create') {
2111 global $wpdb;
2112
2113 $a = $this->named['author'];
2114
2115 $source = $this->source();
2116 $forbidden = apply_filters('feedwordpress_forbidden_author_names',
2117 array('admin', 'administrator', 'www', 'root'));
2118
2119 // Prepare the list of candidates to try for author name: name from
2120 // feed, original source title (if any), immediate source title live
2121 // from feed, subscription title, prettied version of feed homepage URL,
2122 // prettied version of feed URL, or, failing all, use "unknown author"
2123 // as last resort
2124
2125 $candidates = array();
2126 $candidates[] = $a['name'];
2127 if (!is_null($source)) : $candidates[] = $source['title']; endif;
2128 $candidates[] = $this->link->name(/*fromFeed=*/ true);
2129 $candidates[] = $this->link->name(/*fromFeed=*/ false);
2130 if (strlen($this->link->homepage()) > 0) : $candidates[] = feedwordpress_display_url($this->link->homepage()); endif;
2131 $candidates[] = feedwordpress_display_url($this->link->uri());
2132 $candidates[] = 'unknown author';
2133
2134 // Pick the first one that works from the list, screening against empty
2135 // or forbidden names.
2136
2137 $author = NULL;
2138 while (is_null($author) and ($candidate = each($candidates))) :
2139 if (!is_null($candidate['value'])
2140 and (strlen(trim($candidate['value'])) > 0)
2141 and !in_array(strtolower(trim($candidate['value'])), $forbidden)) :
2142 $author = $candidate['value'];
2143 endif;
2144 endwhile;
2145
2146 $email = (isset($a['email']) ? $a['email'] : NULL);
2147 $authorUrl = (isset($a['uri']) ? $a['uri'] : NULL);
2148
2149
2150 $hostUrl = $this->link->homepage();
2151 if (is_null($hostUrl) or (strlen($hostUrl) < 0)) :
2152 $hostUrl = $this->link->uri();
2153 endif;
2154
2155 $match_author_by_email = !('yes' == get_option("feedwordpress_do_not_match_author_by_email"));
2156 if ($match_author_by_email and !FeedWordPress::is_null_email($email)) :
2157 $test_email = $email;
2158 else :
2159 $test_email = NULL;
2160 endif;
2161
2162 // Never can be too careful...
2163 $login = sanitize_user($author, /*strict=*/ true);
2164
2165 // Possible for, e.g., foreign script author names
2166 if (strlen($login) < 1) :
2167 // No usable characters in author name for a login.
2168 // (Sometimes results from, e.g., foreign scripts.)
2169 //
2170 // We just need *something* in Western alphanumerics,
2171 // so let's try the domain name.
2172 //
2173 // Uniqueness will be guaranteed below if necessary.
2174
2175 $url = parse_url($hostUrl);
2176
2177 $login = sanitize_user($url['host'], /*strict=*/ true);
2178 if (strlen($login) < 1) :
2179 // This isn't working. Frak it.
2180 $login = 'syndicated';
2181 endif;
2182 endif;
2183
2184 $login = apply_filters('pre_user_login', $login);
2185
2186 $nice_author = sanitize_title($author);
2187 $nice_author = apply_filters('pre_user_nicename', $nice_author);
2188
2189 $reg_author = esc_sql(preg_quote($author));
2190 $author = esc_sql($author);
2191 $email = esc_sql($email);
2192 $test_email = esc_sql($test_email);
2193 $authorUrl = esc_sql($authorUrl);
2194
2195 // Check for an existing author rule....
2196 if (isset($this->link->settings['map authors']['name']['*'])) :
2197 $author_rule = $this->link->settings['map authors']['name']['*'];
2198 elseif (isset($this->link->settings['map authors']['name'][strtolower(trim($author))])) :
2199 $author_rule = $this->link->settings['map authors']['name'][strtolower(trim($author))];
2200 else :
2201 $author_rule = NULL;
2202 endif;
2203
2204 // User name is mapped to a particular author. If that author ID exists, use it.
2205 if (is_numeric($author_rule) and get_userdata((int) $author_rule)) :
2206 $id = (int) $author_rule;
2207
2208 // User name is filtered out
2209 elseif ('filter' == $author_rule) :
2210 $id = NULL;
2211
2212 else :
2213 // Check the database for an existing author record that might fit
2214
2215 // First try the user core data table.
2216 $id = $wpdb->get_var(
2217 "SELECT ID FROM $wpdb->users
2218 WHERE TRIM(LCASE(display_name)) = TRIM(LCASE('$author'))
2219 OR TRIM(LCASE(user_login)) = TRIM(LCASE('$author'))
2220 OR (
2221 LENGTH(TRIM(LCASE(user_email))) > 0
2222 AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
2223 )");
2224
2225 // If that fails, look for aliases in the user meta data table
2226 if (is_null($id)) :
2227 $id = $wpdb->get_var(
2228 "SELECT user_id FROM $wpdb->usermeta
2229 WHERE
2230 (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
2231 OR (
2232 meta_key = 'description'
2233 AND TRIM(LCASE(meta_value))
2234 RLIKE CONCAT(
2235 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
2236 TRIM(LCASE('$reg_author')),
2237 '( |\\t|\\r)*(\\n|\$)'
2238 )
2239 )
2240 ");
2241 endif;
2242
2243 // ... if you don't find one, then do what you need to do
2244 if (is_null($id)) :
2245 if ($unfamiliar_author === 'create') :
2246 $userdata = array();
2247
2248 // WordPress 3 is going to pitch a fit if we attempt to register
2249 // more than one user account with an empty e-mail address, so we
2250 // need *something* here. Ugh.
2251 if (strlen($email) == 0 or FeedWordPress::is_null_email($email)) :
2252 $url = parse_url($hostUrl);
2253 $email = $nice_author.'@'.$url['host'];
2254 endif;
2255
2256 #-- user table data
2257 $userdata['ID'] = NULL; // new user
2258 $userdata['user_login'] = $login;
2259 $userdata['user_nicename'] = $nice_author;
2260 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
2261 $userdata['user_email'] = $email;
2262 $userdata['user_url'] = $authorUrl;
2263 $userdata['nickname'] = $author;
2264 $parts = preg_split('/\s+/', trim($author), 2);
2265 if (isset($parts[0])) : $userdata['first_name'] = $parts[0]; endif;
2266 if (isset($parts[1])) : $userdata['last_name'] = $parts[1]; endif;
2267 $userdata['display_name'] = $author;
2268 $userdata['role'] = 'contributor';
2269
2270 do { // Keep trying until you get it right. Or until PHP crashes, I guess.
2271 $id = wp_insert_user($userdata);
2272 if (is_wp_error($id)) :
2273 $codes = $id->get_error_code();
2274 switch ($codes) :
2275 case 'empty_user_login' :
2276 case 'existing_user_login' :
2277 // Add a random disambiguator
2278 $userdata['user_login'] .= substr(md5(uniqid(microtime())), 0, 6);
2279 break;
2280 case 'existing_user_email' :
2281 // No disassemble!
2282 $parts = explode('@', $userdata['user_email'], 2);
2283
2284 // Add a random disambiguator as a gmail-style username extension
2285 $parts[0] .= '+'.substr(md5(uniqid(microtime())), 0, 6);
2286
2287 // Reassemble
2288 $userdata['user_email'] = $parts[0].'@'.$parts[1];
2289 break;
2290 endswitch;
2291 endif;
2292 } while (is_wp_error($id));
2293 elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
2294 $id = (int) $unfamiliar_author;
2295 elseif ($unfamiliar_author === 'default') :
2296 $id = 1;
2297 endif;
2298 endif;
2299 endif;
2300
2301 if ($id) :
2302 $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
2303
2304 // Multisite: Check whether the author has been recorded
2305 // on *this* blog before. If not, put her down as a
2306 // Contributor for *this* blog.
2307 $user = new WP_User((int) $id);
2308 if (empty($user->roles)) :
2309 $user->add_role('contributor');
2310 endif;
2311 endif;
2312 return $id;
2313 } /* function SyndicatedPost::author_id () */
2314
2315 /**
2316 * category_ids: look up (and create) category ids from a list of
2317 * categories
2318 *
2319 * @param array $cats
2320 * @param string $unfamiliar_category
2321 * @param array|null $taxonomies
2322 * @return array
2323 */
2324 function category_ids ($cats, $unfamiliar_category = 'create', $taxonomies = NULL, $params = array()) {
2325 return $this->link->category_ids($this, $cats, $unfamiliar_category, $taxonomies, $params);
2326 } /* SyndicatedPost::category_ids () */
2327
2328 } /* class SyndicatedPost */
2329
2330