PluginProbe
FeedWordPress / 2015.0514
FeedWordPress v2015.0514
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 2015.0514, at syndicatedpost.class.php

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