PluginProbe
FeedWordPress / 2013.0504
FeedWordPress v2013.0504
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 2013.0504, at syndicatedpost.class.php

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