PluginProbe
FeedWordPress / 2011.0602
FeedWordPress v2011.0602
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 2011.0602, at syndicatedpost.class.php

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