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

2,121 lines 70.4 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 2010.0905
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 endif;
769 return $this->_hashes[$id];
770 }
771
772 function update_hash () {
773 return md5(serialize($this->item));
774 } /* SyndicatedPost::update_hash() */
775
776 function guid () {
777 $guid = null;
778 if (isset($this->item['id'])): // Atom 0.3 / 1.0
779 $guid = $this->item['id'];
780 elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
781 $guid = $this->item['atom']['id'];
782 elseif (isset($this->item['guid'])) : // RSS 2.0
783 $guid = $this->item['guid'];
784 elseif (isset($this->item['dc']['identifier'])) : // yeah, right
785 $guid = $this->item['dc']['identifier'];
786 endif;
787
788 // Un-set or too long to use as-is. Generate a tag: URI.
789 if (is_null($guid) or strlen($guid) > 250) :
790 // In case we need to check this again
791 $original_guid = $guid;
792
793 // The feed does not seem to have provided us with a
794 // usable unique identifier, so we'll have to cobble
795 // together a tag: URI that might work for us. The base
796 // of the URI will be the host name of the feed source ...
797 $bits = parse_url($this->link->uri());
798 $guid = 'tag:'.$bits['host'];
799
800 // Some ill-mannered feeds (for example, certain feeds
801 // coming from Google Calendar) have extraordinarily long
802 // guids -- so long that they exceed the 255 character
803 // width of the WordPress guid field. But if the string
804 // gets clipped by MySQL, uniqueness tests will fail
805 // forever after and the post will be endlessly
806 // reduplicated. So, instead, Guids Of A Certain Length
807 // are hashed down into a nice, manageable tag: URI.
808 if (!is_null($original_guid)) :
809 $guid .= ',2010-12-03:id.'.md5($original_guid);
810
811 // If we have a date of creation, then we can use that
812 // to uniquely identify the item. (On the other hand, if
813 // the feed producer was consicentious enough to
814 // generate dates of creation, she probably also was
815 // conscientious enough to generate unique identifiers.)
816 elseif (!is_null($this->created())) :
817 $guid .= '://post.'.date('YmdHis', $this->created());
818
819 // Otherwise, use both the URI of the item, *and* the
820 // item's title. We have to use both because titles are
821 // often not unique, and sometimes links aren't unique
822 // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
823 // some podcasts). But it's rare to have *both* the same
824 // title *and* the same link for two different items. So
825 // this is about the best we can do.
826 else :
827 $link = $this->permalink();
828 if (is_null($link)) : $link = $this->link->uri(); endif;
829 $guid .= '://'.md5($link.'/'.$this->item['title']);
830 endif;
831 endif;
832 return $guid;
833 } /* SyndicatedPost::guid() */
834
835 function author () {
836 $author = array ();
837
838 if (isset($this->item['author_name'])):
839 $author['name'] = $this->item['author_name'];
840 elseif (isset($this->item['dc']['creator'])):
841 $author['name'] = $this->item['dc']['creator'];
842 elseif (isset($this->item['dc']['contributor'])):
843 $author['name'] = $this->item['dc']['contributor'];
844 elseif (isset($this->feed->channel['dc']['creator'])) :
845 $author['name'] = $this->feed->channel['dc']['creator'];
846 elseif (isset($this->feed->channel['dc']['contributor'])) :
847 $author['name'] = $this->feed->channel['dc']['contributor'];
848 elseif (isset($this->feed->channel['author_name'])) :
849 $author['name'] = $this->feed->channel['author_name'];
850 elseif ($this->feed->is_rss() and isset($this->item['author'])) :
851 // The author element in RSS is allegedly an
852 // e-mail address, but lots of people don't use
853 // it that way. So let's make of it what we can.
854 $author = parse_email_with_realname($this->item['author']);
855
856 if (!isset($author['name'])) :
857 if (isset($author['email'])) :
858 $author['name'] = $author['email'];
859 else :
860 $author['name'] = $this->feed->channel['title'];
861 endif;
862 endif;
863 elseif ($this->link->name()) :
864 $author['name'] = $this->link->name();
865 else :
866 $url = parse_url($this->link->uri());
867 $author['name'] = $url['host'];
868 endif;
869
870 if (isset($this->item['author_email'])):
871 $author['email'] = $this->item['author_email'];
872 elseif (isset($this->feed->channel['author_email'])) :
873 $author['email'] = $this->feed->channel['author_email'];
874 endif;
875
876 if (isset($this->item['author_uri'])):
877 $author['uri'] = $this->item['author_uri'];
878 elseif (isset($this->item['author_url'])):
879 $author['uri'] = $this->item['author_url'];
880 elseif (isset($this->feed->channel['author_uri'])) :
881 $author['uri'] = $this->item['author_uri'];
882 elseif (isset($this->feed->channel['author_url'])) :
883 $author['uri'] = $this->item['author_url'];
884 elseif (isset($this->feed->channel['link'])) :
885 $author['uri'] = $this->feed->channel['link'];
886 endif;
887
888 return $author;
889 } /* SyndicatedPost::author() */
890
891 /**
892 * SyndicatedPost::inline_tags: Return a list of all the tags embedded
893 * in post content using the a[@rel="tag"] microformat.
894 *
895 * @since 2010.0630
896 * @return array of string values containing the name of each tag
897 */
898 function inline_tags () {
899 $tags = array();
900 $content = $this->content();
901 $pattern = FeedWordPressHTML::tagWithAttributeRegex('a', 'rel', 'tag');
902 preg_match_all($pattern, $content, $refs, PREG_SET_ORDER);
903 if (count($refs) > 0) :
904 foreach ($refs as $ref) :
905 $tag = FeedWordPressHTML::tagWithAttributeMatch($ref);
906 $tags[] = $tag['content'];
907 endforeach;
908 endif;
909 return $tags;
910 }
911
912 /**
913 * SyndicatedPost::isTaggedAs: Test whether a feed item is
914 * tagged / categorized with a given string. Case and leading and
915 * trailing whitespace are ignored.
916 *
917 * @param string $tag Tag to check for
918 *
919 * @return bool Whether or not at least one of the categories / tags on
920 * $this->item is set to $tag (modulo case and leading and trailing
921 * whitespace)
922 */
923 function isTaggedAs ($tag) {
924 $desiredTag = strtolower(trim($tag)); // Normalize case and whitespace
925
926 // Check to see if this is tagged with $tag
927 $currentCategory = 'category';
928 $currentCategoryNumber = 1;
929
930 // If we have the new MagpieRSS, the number of category elements
931 // on this item is stored under index "category#".
932 if (isset($this->item['category#'])) :
933 $numberOfCategories = (int) $this->item['category#'];
934
935 // We REALLY shouldn't have the old and busted MagpieRSS, but in
936 // case we do, it doesn't support multiple categories, but there
937 // might still be a single value under the "category" index.
938 elseif (isset($this->item['category'])) :
939 $numberOfCategories = 1;
940
941 // No standard category or tag elements on this feed item.
942 else :
943 $numberOfCategories = 0;
944
945 endif;
946
947 $isSoTagged = false; // Innocent until proven guilty
948
949 // Loop through category elements; if there are multiple
950 // elements, they are indexed as category, category#2,
951 // category#3, ... category#N
952 while ($currentCategoryNumber <= $numberOfCategories) :
953 if ($desiredTag == strtolower(trim($this->item[$currentCategory]))) :
954 $isSoTagged = true; // Got it!
955 break;
956 endif;
957
958 $currentCategoryNumber += 1;
959 $currentCategory = 'category#'.$currentCategoryNumber;
960 endwhile;
961
962 return $isSoTagged;
963 } /* SyndicatedPost::isTaggedAs() */
964
965 /**
966 * SyndicatedPost::enclosures: returns an array with any enclosures
967 * that may be attached to this syndicated item.
968 *
969 * @param string $type If you only want enclosures that match a certain
970 * MIME type or group of MIME types, you can limit the enclosures
971 * that will be returned to only those with a MIME type which
972 * matches this regular expression.
973 * @return array
974 */
975 function enclosures ($type = '/.*/') {
976 $enclosures = array();
977
978 if (isset($this->item['enclosure#'])) :
979 // Loop through enclosure, enclosure#2, enclosure#3, ....
980 for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
981 $eid = (($i > 1) ? "#{$id}" : "");
982
983 // Does it match the type we want?
984 if (preg_match($type, $this->item["enclosure{$eid}@type"])) :
985 $enclosures[] = array(
986 "url" => $this->item["enclosure{$eid}@url"],
987 "type" => $this->item["enclosure{$eid}@type"],
988 "length" => $this->item["enclosure{$eid}@length"],
989 );
990 endif;
991 endfor;
992 endif;
993 return $enclosures;
994 } /* SyndicatedPost::enclosures() */
995
996 function source ($what = NULL) {
997 $ret = NULL;
998 $source = $this->entry->get_source();
999 if ($source) :
1000 $ret = array();
1001 $ret['title'] = $source->get_title();
1002 $ret['uri'] = $source->get_link();
1003 $ret['feed'] = $source->get_link(0, 'self');
1004
1005 if ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'id')) :
1006 $ret['id'] = $id_tags[0]['data'];
1007 elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'id')) :
1008 $ret['id'] = $id_tags[0]['data'];
1009 elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'guid')) :
1010 $ret['id'] = $id_tags[0]['data'];
1011 elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'guid')) :
1012 $ret['id'] = $id_tags[0]['data'];
1013 elseif ($id_tags = $source->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'guid')) :
1014 $ret['id'] = $id_tags[0]['data'];
1015 endif;
1016 endif;
1017
1018 if (!is_null($what) and is_scalar($what)) :
1019 $ret = $ret[$what];
1020 endif;
1021 return $ret;
1022 }
1023
1024 function comment_link () {
1025 $url = null;
1026
1027 // RSS 2.0 has a standard <comments> element:
1028 // "<comments> is an optional sub-element of <item>. If present,
1029 // it is the url of the comments page for the item."
1030 // <http://cyber.law.harvard.edu/rss/rss.html#ltcommentsgtSubelementOfLtitemgt>
1031 if (isset($this->item['comments'])) :
1032 $url = $this->item['comments'];
1033 endif;
1034
1035 // The convention in Atom feeds is to use a standard <link>
1036 // element with @rel="replies" and @type="text/html".
1037 // Unfortunately, SimplePie_Item::get_links() allows us to filter
1038 // by the value of @rel, but not by the value of @type. *sigh*
1039
1040 // Try Atom 1.0 first
1041 $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link');
1042
1043 // Fall back and try Atom 0.3
1044 if (is_null($linkElements)) : $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link'); endif;
1045
1046 // Now loop through the elements, screening by @rel and @type
1047 if (is_array($linkElements)) : foreach ($linkElements as $link) :
1048 $rel = (isset($link['attribs']['']['rel']) ? $link['attribs']['']['rel'] : 'alternate');
1049 $type = (isset($link['attribs']['']['type']) ? $link['attribs']['']['type'] : NULL);
1050 $href = (isset($link['attribs']['']['href']) ? $link['attribs']['']['href'] : NULL);
1051
1052 if (strtolower($rel)=='replies' and $type=='text/html' and !is_null($href)) :
1053 $url = $href;
1054 endif;
1055 endforeach; endif;
1056
1057 return $url;
1058 }
1059
1060 function comment_feed () {
1061 $feed = null;
1062
1063 // Well Formed Web comment feeds extension for RSS 2.0
1064 // <http://www.sellsbrothers.com/spout/default.aspx?content=archive.htm#exposingRssComments>
1065 //
1066 // N.B.: Correct capitalization is wfw:commentRss, but
1067 // wfw:commentRSS is common in the wild (partly due to a typo in
1068 // the original spec). In any case, our item array is normalized
1069 // to all lowercase anyways.
1070 if (isset($this->item['wfw']['commentrss'])) :
1071 $feed = $this->item['wfw']['commentrss'];
1072 endif;
1073
1074 // In Atom 1.0, the convention is to use a standard link element
1075 // with @rel="replies". Sometimes this is also used to pass a
1076 // link to the human-readable comments page, so we also need to
1077 // check link/@type for a feed MIME type.
1078 //
1079 // Which is why I'm not using the SimplePie_Item::get_links()
1080 // method here, incidentally: it doesn't allow you to filter by
1081 // @type. *sigh*
1082 if (isset($this->item['link_replies'])) :
1083 // There may be multiple <link rel="replies"> elements; feeds have a feed MIME type
1084 $N = isset($this->item['link_replies#']) ? $this->item['link_replies#'] : 1;
1085 for ($i = 1; $i <= $N; $i++) :
1086 $currentElement = 'link_replies'.(($i > 1) ? '#'.$i : '');
1087 if (isset($this->item[$currentElement.'@type'])
1088 and preg_match("\007application/(atom|rss|rdf)\+xml\007i", $this->item[$currentElement.'@type'])) :
1089 $feed = $this->item[$currentElement];
1090 endif;
1091 endfor;
1092 endif;
1093 return $feed;
1094 } /* SyndicatedPost::comment_feed() */
1095
1096 ##################################
1097 #### BUILT-IN CONTENT FILTERS ####
1098 ##################################
1099
1100 var $uri_attrs = array (
1101 array('a', 'href'),
1102 array('applet', 'codebase'),
1103 array('area', 'href'),
1104 array('blockquote', 'cite'),
1105 array('body', 'background'),
1106 array('del', 'cite'),
1107 array('form', 'action'),
1108 array('frame', 'longdesc'),
1109 array('frame', 'src'),
1110 array('iframe', 'longdesc'),
1111 array('iframe', 'src'),
1112 array('head', 'profile'),
1113 array('img', 'longdesc'),
1114 array('img', 'src'),
1115 array('img', 'usemap'),
1116 array('input', 'src'),
1117 array('input', 'usemap'),
1118 array('ins', 'cite'),
1119 array('link', 'href'),
1120 array('object', 'classid'),
1121 array('object', 'codebase'),
1122 array('object', 'data'),
1123 array('object', 'usemap'),
1124 array('q', 'cite'),
1125 array('script', 'src')
1126 ); /* var SyndicatedPost::$uri_attrs */
1127
1128 var $_base = null;
1129
1130 function resolve_single_relative_uri ($refs) {
1131 $tag = FeedWordPressHTML::attributeMatch($refs);
1132 $url = SimplePie_Misc::absolutize_url($tag['value'], $this->_base);
1133 return $tag['prefix'] . $url . $tag['suffix'];
1134 } /* function SyndicatedPost::resolve_single_relative_uri() */
1135
1136 function resolve_relative_uris ($content, $obj) {
1137 $set = $obj->link->setting('resolve relative', 'resolve_relative', 'yes');
1138 if ($set and $set != 'no') :
1139 // Fallback: if we don't have anything better, use the
1140 // item link from the feed
1141 $obj->_base = $obj->permalink(); // Reset the base for resolving relative URIs
1142
1143 // What we should do here, properly, is to use
1144 // SimplePie_Item::get_base() -- but that method is
1145 // currently broken. Or getting down and dirty in the
1146 // SimplePie representation of the content tags and
1147 // grabbing the xml_base member for the content element.
1148 // Maybe someday...
1149
1150 foreach ($obj->uri_attrs as $pair) :
1151 list($tag, $attr) = $pair;
1152 $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1153 $content = preg_replace_callback (
1154 $pattern,
1155 array(&$obj, 'resolve_single_relative_uri'),
1156 $content
1157 );
1158 endforeach;
1159 endif;
1160
1161 return $content;
1162 } /* function SyndicatedPost::resolve_relative_uris () */
1163
1164 var $strip_attrs = array (
1165 array('[a-z]+', 'target'),
1166 // array('[a-z]+', 'style'),
1167 // array('[a-z]+', 'on[a-z]+'),
1168 );
1169
1170 function strip_attribute_from_tag ($refs) {
1171 $tag = FeedWordPressHTML::attributeMatch($refs);
1172 return $tag['before_attribute'].$tag['after_attribute'];
1173 }
1174
1175 function sanitize_content ($content, $obj) {
1176 # This kind of sucks. I intend to replace it with
1177 # lib_filter sometime soon.
1178 foreach ($obj->strip_attrs as $pair):
1179 list($tag,$attr) = $pair;
1180 $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1181
1182 $content = preg_replace_callback (
1183 $pattern,
1184 array(&$obj, 'strip_attribute_from_tag'),
1185 $content
1186 );
1187 endforeach;
1188 return $content;
1189 } /* SyndicatedPost::sanitize() */
1190
1191 #####################
1192 #### POST STATUS ####
1193 #####################
1194
1195 /**
1196 * SyndicatedPost::filtered: check whether or not this post has been
1197 * screened out by a registered filter.
1198 *
1199 * @return bool TRUE iff post has been filtered out by a previous filter
1200 */
1201 function filtered () {
1202 return is_null($this->post);
1203 } /* SyndicatedPost::filtered() */
1204
1205 /**
1206 * SyndicatedPost::freshness: check whether post is a new post to be
1207 * inserted, a previously syndicated post that needs to be updated to
1208 * match the latest revision, or a previously syndicated post that is
1209 * still up-to-date.
1210 *
1211 * @return int A status code representing the freshness of the post
1212 * 0 = post already syndicated; no update needed
1213 * 1 = post already syndicated, but needs to be updated to latest
1214 * 2 = post has not yet been syndicated; needs to be created
1215 */
1216 function freshness () {
1217 global $wpdb;
1218
1219 if ($this->filtered()) : // This should never happen.
1220 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1221 endif;
1222
1223 if (is_null($this->_freshness)) : // Not yet checked and cached.
1224 $guid = $wpdb->escape($this->guid());
1225
1226 $result = $wpdb->get_row("
1227 SELECT id, guid, post_modified_gmt
1228 FROM $wpdb->posts WHERE guid='$guid'
1229 ");
1230
1231 if (!$result) : // No post with this guid
1232 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$this->guid().'] "'.$this->entry->get_title().'" is a NEW POST.');
1233 $this->_wp_id = NULL;
1234 $this->_freshness = 2; // New content
1235 else :
1236 preg_match('/([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/', $result->post_modified_gmt, $backref);
1237
1238 $last_rev_ts = gmmktime($backref[4], $backref[5], $backref[6], $backref[2], $backref[3], $backref[1]);
1239 $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
1240
1241 // Check timestamps...
1242 $updated = (
1243 !is_null($updated_ts)
1244 and ($updated_ts > $last_rev_ts)
1245 );
1246
1247
1248 if (!$updated) :
1249 // Or the hash...
1250 $hash = $this->update_hash();
1251 $seen = $this->stored_hashes($result->id);
1252 if (count($seen) > 0) :
1253 $updated = !in_array($hash, $seen); // Not seen yet?
1254 else :
1255 $updated = true; // Can't find syndication meta-data
1256 endif;
1257 endif;
1258
1259 $frozen = false;
1260 if ($updated) : // Ignore if the post is frozen
1261 $frozen = ('yes' == $this->link->setting('freeze updates', 'freeze_updates', NULL));
1262 if (!$frozen) :
1263 $frozen_values = get_post_custom_values('_syndication_freeze_updates', $result->id);
1264 $frozen = (count($frozen_values) > 0 and 'yes' == $frozen_values[0]);
1265 endif;
1266 endif;
1267 $updated = ($updated and !$frozen);
1268
1269 if ($updated) :
1270 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$this->guid().'] "'.$this->entry->get_title().'" is an update of an existing post.');
1271 $this->_freshness = 1; // Updated content
1272 $this->_wp_id = $result->id;
1273
1274 // We want this to keep a running list of all the
1275 // processed update hashes.
1276 $this->post['meta']['syndication_item_hash'] = array_merge(
1277 $this->stored_hashes(),
1278 array($this->update_hash())
1279 );
1280
1281 else :
1282 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$this->guid().'] "'.$this->entry->get_title().'" is a duplicate of an existing post.');
1283 $this->_freshness = 0; // Same old, same old
1284 $this->_wp_id = $result->id;
1285 endif;
1286 endif;
1287 endif;
1288 return $this->_freshness;
1289 }
1290
1291 #################################################
1292 #### INTERNAL STORAGE AND MANAGEMENT METHODS ####
1293 #################################################
1294
1295 function wp_id () {
1296 if ($this->filtered()) : // This should never happen.
1297 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1298 endif;
1299
1300 if (is_null($this->_wp_id) and is_null($this->_freshness)) :
1301 $fresh = $this->freshness(); // sets WP DB id in the process
1302 endif;
1303 return $this->_wp_id;
1304 }
1305
1306 function store () {
1307 global $wpdb;
1308
1309 if ($this->filtered()) : // This should never happen.
1310 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1311 endif;
1312
1313 $freshness = $this->freshness();
1314 if ($freshness > 0) :
1315 # -- Look up, or create, numeric ID for author
1316 $this->post['post_author'] = $this->author_id (
1317 $this->link->setting('unfamiliar author', 'unfamiliar_author', 'create')
1318 );
1319
1320 if (is_null($this->post['post_author'])) :
1321 FeedWordPress::diagnostic('feed_items:rejected', 'Filtered out item ['.$this->guid().'] without syndication: no author available');
1322 $this->post = NULL;
1323 endif;
1324 endif;
1325
1326 if (!$this->filtered() and $freshness > 0) :
1327 $consider = array(
1328 'category' => array('abbr' => 'cats', 'domain' => array('category', 'post_tag')),
1329 'post_tag' => array('abbr' => 'tags', 'domain' => array('post_tag')),
1330 );
1331
1332 $termSet = array(); $valid = null;
1333 foreach ($consider as $what => $taxes) :
1334 if (!is_null($this->post)) : // Not filtered out yet
1335 # -- Look up, or create, numeric ID for categories
1336 $taxonomies = $this->link->setting("match/".$taxes['abbr'], 'match_'.$taxes['abbr'], $taxes['domain']);
1337
1338 // Eliminate dummy variables
1339 $taxonomies = array_filter($taxonomies, 'remove_dummy_zero');
1340
1341 $terms = $this->category_ids (
1342 $this->feed_terms[$what],
1343 $this->link->setting("unfamiliar {$what}", "unfamiliar_{$what}", 'create:'.$what),
1344 /*taxonomies=*/ $taxonomies,
1345 array(
1346 'singleton' => false, // I don't like surprises
1347 'filters' => true,
1348 )
1349 );
1350
1351 if (is_null($terms) or is_null($termSet)) :
1352 // filtered out -- no matches
1353 else :
1354 $valid = true;
1355
1356 // filter mode off, or at least one match
1357 foreach ($terms as $tax => $term_ids) :
1358 if (!isset($termSet[$tax])) :
1359 $termSet[$tax] = array();
1360 endif;
1361 $termSet[$tax] = array_merge($termSet[$tax], $term_ids);
1362 endforeach;
1363 endif;
1364 endif;
1365 endforeach;
1366
1367 if (is_null($valid)) : // Plonked
1368 $this->post = NULL;
1369 else : // We can proceed
1370 $this->post['tax_input'] = array();
1371 foreach ($termSet as $tax => $term_ids) :
1372 if (!isset($this->post['tax_input'][$tax])) :
1373 $this->post['tax_input'][$tax] = array();
1374 endif;
1375 $this->post['tax_input'][$tax] = array_merge(
1376 $this->post['tax_input'][$tax],
1377 $term_ids
1378 );
1379 endforeach;
1380
1381 // Now let's add on the feed and global presets
1382 foreach ($this->preset_terms as $tax => $term_ids) :
1383 if (!isset($this->post['tax_input'][$tax])) :
1384 $this->post['tax_input'][$tax] = array();
1385 endif;
1386
1387 $this->post['tax_input'][$tax] = array_merge (
1388 $this->post['tax_input'][$tax],
1389 $this->category_ids (
1390 /*terms=*/ $term_ids,
1391 /*unfamiliar=*/ 'create:'.$tax, // These are presets; for those added in a tagbox editor, the tag may not yet exist
1392 /*taxonomies=*/ array($tax),
1393 array(
1394 'singleton' => true,
1395 ))
1396 );
1397 endforeach;
1398 endif;
1399 endif;
1400
1401 if (!$this->filtered() and $freshness > 0) :
1402 // Filter some individual fields
1403
1404 // Allow filters to set post slug. Props niska.
1405 $post_name = apply_filters('syndicated_post_slug', NULL, $this);
1406 if (!empty($post_name)) :
1407 $this->post['post_name'] = $post_name;
1408 endif;
1409
1410 $this->post = apply_filters('syndicated_post', $this->post, $this);
1411
1412 // Allow for feed-specific syndicated_post filters.
1413 $this->post = apply_filters(
1414 "syndicated_post_".$this->link->uri(),
1415 $this->post,
1416 $this
1417 );
1418 endif;
1419
1420 // Hook in early to make sure these get inserted if at all possible
1421 add_action(
1422 /*hook=*/ 'transition_post_status',
1423 /*callback=*/ array(&$this, 'add_rss_meta'),
1424 /*priority=*/ -10000, /* very early */
1425 /*arguments=*/ 3
1426 );
1427
1428 $retval = array(1 => 'updated', 2 => 'new');
1429
1430 $ret = false;
1431 if (!$this->filtered() and isset($retval[$freshness])) :
1432 $diag = array(
1433 1 => 'Updating existing post # '.$this->wp_id().', "'.$this->post['post_title'].'"',
1434 2 => 'Inserting new post "'.$this->post['post_title'].'"',
1435 );
1436 FeedWordPress::diagnostic('syndicated_posts', $diag[$freshness]);
1437
1438 $this->insert_post(/*update=*/ ($freshness == 1));
1439
1440 $hook = array( 1 => 'update_syndicated_item', 2 => 'post_syndicated_item' );
1441 do_action($hook[$freshness], $this->wp_id(), $this);
1442
1443 $ret = $retval[$freshness];
1444 endif;
1445
1446 // Remove add_rss_meta hook
1447 remove_action(
1448 /*hook=*/ 'transition_post_status',
1449 /*callback=*/ array(&$this, 'add_rss_meta'),
1450 /*priority=*/ -10000, /* very early */
1451 /*arguments=*/ 3
1452 );
1453
1454 return $ret;
1455 } /* function SyndicatedPost::store () */
1456
1457 function insert_post ($update = false) {
1458 global $wpdb;
1459
1460 $dbpost = $this->normalize_post(/*new=*/ true);
1461 if (!is_null($dbpost)) :
1462 $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1463
1464 // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1465 add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1466
1467 // Kludge to prevent kses filters from stripping the
1468 // content of posts when updating without a logged in
1469 // user who has `unfiltered_html` capability.
1470 $mungers = array('wp_filter_kses', 'wp_filter_post_kses');
1471 $removed = array();
1472 foreach ($mungers as $munger) :
1473 if (has_filter('content_save_pre', $munger)) :
1474 remove_filter('content_save_pre', $munger);
1475 $removed[] = $munger;
1476 endif;
1477 endforeach;
1478
1479 if ($update and function_exists('get_post_field')) :
1480 // Don't munge status fields that the user may
1481 // have reset manually
1482 $doNotMunge = array('post_status', 'comment_status', 'ping_status');
1483
1484 foreach ($doNotMunge as $field) :
1485 $dbpost[$field] = get_post_field($field, $this->wp_id());
1486 endforeach;
1487 endif;
1488
1489 // WP3's wp_insert_post scans current_user_can() for the
1490 // tax_input, with no apparent way to override. Ugh.
1491 add_action(
1492 /*hook=*/ 'transition_post_status',
1493 /*callback=*/ array(&$this, 'add_terms'),
1494 /*priority=*/ -10001, /* very early */
1495 /*arguments=*/ 3
1496 );
1497
1498 // WP3 appears to override whatever you give it for
1499 // post_modified. Ugh.
1500 add_action(
1501 /*hook=*/ 'transition_post_status',
1502 /*callback=*/ array(&$this, 'fix_post_modified_ts'),
1503 /*priority=*/ -10000, /* very early */
1504 /*arguments=*/ 3
1505 );
1506
1507 if ($update) :
1508 $this->post['ID'] = $this->wp_id();
1509 $dbpost['ID'] = $this->post['ID'];
1510 endif;
1511 $this->_wp_id = wp_insert_post($dbpost, /*return wp_error=*/ true);
1512
1513 remove_action(
1514 /*hook=*/ 'transition_post_status',
1515 /*callback=*/ array(&$this, 'add_terms'),
1516 /*priority=*/ -10001, /* very early */
1517 /*arguments=*/ 3
1518 );
1519
1520 remove_action(
1521 /*hook=*/ 'transition_post_status',
1522 /*callback=*/ array(&$this, 'fix_post_modified_ts'),
1523 /*priority=*/ -10000, /* very early */
1524 /*arguments=*/ 3
1525 );
1526
1527 // Turn off ridiculous fucking kludges #1 and #2
1528 remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1529 foreach ($removed as $filter) :
1530 add_filter('content_save_pre', $removed);
1531 endforeach;
1532
1533 $this->validate_post_id($dbpost, $update, array(__CLASS__, __FUNCTION__));
1534 endif;
1535 } /* function SyndicatedPost::insert_post () */
1536
1537 function insert_new () {
1538 $this->insert_post(/*update=*/ false);
1539 } /* SyndicatedPost::insert_new() */
1540
1541 function update_existing () {
1542 $this->insert_post(/*update=*/ true);
1543 } /* SyndicatedPost::update_existing() */
1544
1545 /**
1546 * SyndicatedPost::normalize_post()
1547 *
1548 * @param bool $new If true, this post is to be inserted anew. If false, it is an update of an existing post.
1549 * @return array A normalized representation of the post ready to be inserted into the database or sent to the WordPress API functions
1550 */
1551 function normalize_post ($new = true) {
1552 global $wpdb;
1553
1554 $out = array();
1555
1556 // Why the fuck doesn't wp_insert_post already do this?
1557 foreach ($this->post as $key => $value) :
1558 if (is_string($value)) :
1559 $out[$key] = $wpdb->escape($value);
1560 else :
1561 $out[$key] = $value;
1562 endif;
1563 endforeach;
1564
1565 $fullPost = $out['post_title'].$out['post_content'];
1566 $fullPost .= (isset($out['post_excerpt']) ? $out['post_excerpt'] : '');
1567 if (strlen($fullPost) < 1) :
1568 // FIXME: Option for filtering out empty posts
1569 endif;
1570 if (strlen($out['post_title'])==0) :
1571 $offset = (int) get_option('gmt_offset') * 60 * 60;
1572 if (isset($this->post['meta']['syndication_source'])) :
1573 $source_title = $this->post['meta']['syndication_source'];
1574 else :
1575 $feed_url = parse_url($this->post['meta']['syndication_feed']);
1576 $source_title = $feed_url['host'];
1577 endif;
1578
1579 $out['post_title'] = $source_title
1580 .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
1581 // FIXME: Option for what to fill a blank title with...
1582 endif;
1583
1584 return $out;
1585 }
1586
1587 /**
1588 * SyndicatedPost::validate_post_id()
1589 *
1590 * @param array $dbpost An array representing the post we attempted to insert or update
1591 * @param mixed $ns A string or array representing the namespace (class, method) whence this method was called.
1592 */
1593 function validate_post_id ($dbpost, $is_update, $ns) {
1594 if (is_array($ns)) : $ns = implode('::', $ns);
1595 else : $ns = (string) $ns; endif;
1596
1597 // This should never happen.
1598 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
1599 $verb = ($is_update ? 'update existing' : 'insert new');
1600 $guid = $this->guid();
1601 $url = $this->permalink();
1602 $feed = $this->link->uri(array('add_params' => true));
1603
1604 // wp_insert_post failed. Diagnostics, or barf up a critical bug
1605 // notice if we are in debug mode.
1606 $mesg = "Failed to $verb item [$guid]. WordPress API returned no valid post ID.\n"
1607 ."\t\tID = ".serialize($this->_wp_id)."\n"
1608 ."\t\tURL = ".FeedWordPress::val($url)
1609 ."\t\tFeed = ".FeedWordPress::val($feed);
1610 FeedWordPress::diagnostic('updated_feeds:errors', "WordPress API error: $mesg");
1611 FeedWordPress::diagnostic('feed_items:rejected', $mesg);
1612
1613 $mesg = <<<EOM
1614 The WordPress API returned an invalid post ID
1615 when FeedWordPress tried to $verb item $guid
1616 [URL: $url]
1617 from the feed at $feed
1618
1619 $ns::_wp_id
1620 EOM;
1621 FeedWordPress::noncritical_bug(
1622 /*message=*/ $mesg,
1623 /*var =*/ array(
1624 "\$this->_wp_id" => $this->_wp_id,
1625 "\$dbpost" => $dbpost,
1626 ),
1627 /*line # =*/ __LINE__, /*filename=*/ __FILE__
1628 );
1629 endif;
1630 } /* SyndicatedPost::validate_post_id() */
1631
1632 /**
1633 * SyndicatedPost::fix_revision_meta() - Fixes the way WP 2.6+ fucks up
1634 * meta-data (authorship, etc.) when storing revisions of an updated
1635 * syndicated post.
1636 *
1637 * In their infinite wisdom, the WordPress coders have made it completely
1638 * impossible for a plugin that uses wp_insert_post() to set certain
1639 * meta-data (such as the author) when you store an old revision of an
1640 * updated post. Instead, it uses the WordPress defaults (= currently
1641 * active user ID if the process is running with a user logged in, or
1642 * = #0 if there is no user logged in). This results in bogus authorship
1643 * data for revisions that are syndicated from off the feed, unless we
1644 * use a ridiculous kludge like this to end-run the munging of meta-data
1645 * by _wp_put_post_revision.
1646 *
1647 * @param int $revision_id The revision ID to fix up meta-data
1648 */
1649 function fix_revision_meta ($revision_id) {
1650 global $wpdb;
1651
1652 $post_author = (int) $this->post['post_author'];
1653
1654 $revision_id = (int) $revision_id;
1655 $wpdb->query("
1656 UPDATE $wpdb->posts
1657 SET post_author={$this->post['post_author']}
1658 WHERE post_type = 'revision' AND ID='$revision_id'
1659 ");
1660 } /* SyndicatedPost::fix_revision_meta () */
1661
1662 /**
1663 * SyndicatedPost::add_terms() -- if FeedWordPress is processing an
1664 * automatic update, that generally means that wp_insert_post() is being
1665 * called under the user credentials of whoever is viewing the blog at
1666 * the time -- which usually means no user at all. But wp_insert_post()
1667 * checks current_user_can() before assigning any of the terms in a
1668 * post's tax_input structure -- which is unfortunate, since
1669 * current_user_can() always returns FALSE when there is no current user
1670 * logged in. Meaning that automatic updates get no terms assigned.
1671 *
1672 * So, wp_insert_post() is not going to do the term assignments for us.
1673 * If you want something done right....
1674 *
1675 * @param string $new_status Unused action parameter.
1676 * @param string $old_status Unused action parameter.
1677 * @param object $post The database record for the post just inserted.
1678 */
1679 function add_terms ($new_status, $old_status, $post) {
1680 if ($new_status!='inherit') : // Bail if we are creating a revision.
1681 if ( is_array($this->post) and isset($this->post['tax_input']) and is_array($this->post['tax_input']) ) :
1682 foreach ($this->post['tax_input'] as $taxonomy => $terms) :
1683 if (is_array($terms)) :
1684 $terms = array_filter($terms); // strip out empties
1685 endif;
1686 wp_set_post_terms($post->ID, $terms, $taxonomy);
1687 endforeach;
1688 endif;
1689 endif;
1690 } /* SyndicatedPost::add_terms () */
1691
1692 /**
1693 * SyndicatedPost::fix_post_modified_ts() -- We would like to set
1694 * post_modified and post_modified_gmt to reflect the value of
1695 * <atom:updated> or equivalent elements on the feed. Unfortunately,
1696 * wp_insert_post() refuses to acknowledge explicitly-set post_modified
1697 * fields and overwrites them, either with the post_date (if new) or the
1698 * current timestamp (if updated).
1699 *
1700 * So, wp_insert_post() is not going to do the last-modified assignments
1701 * for us. 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 fix_post_modified_ts ($new_status, $old_status, $post) {
1708 global $wpdb;
1709 if ($new_status!='inherit') : // Bail if we are creating a revision.
1710 $wpdb->update( $wpdb->posts, /*data=*/ array(
1711 'post_modified' => $this->post['post_modified'],
1712 'post_modified_gmt' => $this->post['post_modified_gmt'],
1713 ), /*where=*/ array('ID' => $post->ID) );
1714 endif;
1715 } /* SyndicatedPost::fix_post_modified_ts () */
1716
1717 /**
1718 * SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
1719 * using the space for custom keys. The set of keys and values to add is
1720 * specified by the keys and values of $post['meta']. This is used to
1721 * store anything that the WordPress user might want to access from a
1722 * template concerning the post's original source that isn't provided
1723 * for by standard WP meta-data (i.e., any interesting data about the
1724 * syndicated post other than author, title, timestamp, categories, and
1725 * guid). It's also used to hook into WordPress's support for
1726 * enclosures.
1727 *
1728 * @param string $new_status Unused action parameter.
1729 * @param string $old_status Unused action parameter.
1730 * @param object $post The database record for the post just inserted.
1731 */
1732 function add_rss_meta ($new_status, $old_status, $post) {
1733 global $wpdb;
1734 if ($new_status!='inherit') : // Bail if we are creating a revision.
1735 FeedWordPress::diagnostic('syndicated_posts:meta_data', 'Adding post meta-data: {'.implode(", ", array_keys($this->post['meta'])).'}');
1736
1737 if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
1738 $postId = $post->ID;
1739
1740 // Aggregated posts should NOT send out pingbacks.
1741 // WordPress 2.1-2.2 claim you can tell them not to
1742 // using $post_pingback, but they don't listen, so we
1743 // make sure here.
1744 $result = $wpdb->query("
1745 DELETE FROM $wpdb->postmeta
1746 WHERE post_id='$postId' AND meta_key='_pingme'
1747 ");
1748
1749 foreach ( $this->post['meta'] as $key => $values ) :
1750 $eKey = $wpdb->escape($key);
1751
1752 // If this is an update, clear out the old
1753 // values to avoid duplication.
1754 $result = $wpdb->query("
1755 DELETE FROM $wpdb->postmeta
1756 WHERE post_id='$postId' AND meta_key='$eKey'
1757 ");
1758
1759 // Allow for either a single value or an array
1760 if (!is_array($values)) $values = array($values);
1761 foreach ( $values as $value ) :
1762 FeedWordPress::diagnostic('syndicated_posts:meta_data', "Adding post meta-datum to post [$postId]: [$key] = ".FeedWordPress::val($value, /*no newlines=*/ true));
1763 add_post_meta($postId, $key, $value, /*unique=*/ false);
1764 endforeach;
1765 endforeach;
1766 endif;
1767 endif;
1768 } /* SyndicatedPost::add_rss_meta () */
1769
1770 /**
1771 * SyndicatedPost::author_id (): get the ID for an author name from
1772 * the feed. Create the author if necessary.
1773 *
1774 * @param string $unfamiliar_author
1775 *
1776 * @return NULL|int The numeric ID of the author to attribute the post to
1777 * NULL if the post should be filtered out.
1778 */
1779 function author_id ($unfamiliar_author = 'create') {
1780 global $wpdb;
1781
1782 $a = $this->named['author'];
1783
1784 $source = $this->source();
1785 $forbidden = apply_filters('feedwordpress_forbidden_author_names',
1786 array('admin', 'administrator', 'www', 'root'));
1787
1788 // Prepare the list of candidates to try for author name: name from
1789 // feed, original source title (if any), immediate source title live
1790 // from feed, subscription title, prettied version of feed homepage URL,
1791 // prettied version of feed URL, or, failing all, use "unknown author"
1792 // as last resort
1793
1794 $candidates = array();
1795 $candidates[] = $a['name'];
1796 if (!is_null($source)) : $candidates[] = $source['title']; endif;
1797 $candidates[] = $this->link->name(/*fromFeed=*/ true);
1798 $candidates[] = $this->link->name(/*fromFeed=*/ false);
1799 if (strlen($this->link->homepage()) > 0) : $candidates[] = feedwordpress_display_url($this->link->homepage()); endif;
1800 $candidates[] = feedwordpress_display_url($this->link->uri());
1801 $candidates[] = 'unknown author';
1802
1803 // Pick the first one that works from the list, screening against empty
1804 // or forbidden names.
1805
1806 $author = NULL;
1807 while (is_null($author) and ($candidate = each($candidates))) :
1808 if (!is_null($candidate['value'])
1809 and (strlen(trim($candidate['value'])) > 0)
1810 and !in_array(strtolower(trim($candidate['value'])), $forbidden)) :
1811 $author = $candidate['value'];
1812 endif;
1813 endwhile;
1814
1815 $email = (isset($a['email']) ? $a['email'] : NULL);
1816 $authorUrl = (isset($a['uri']) ? $a['uri'] : NULL);
1817
1818
1819 $hostUrl = $this->link->homepage();
1820 if (is_null($hostUrl) or (strlen($hostUrl) < 0)) :
1821 $hostUrl = $this->link->uri();
1822 endif;
1823
1824 $match_author_by_email = !('yes' == get_option("feedwordpress_do_not_match_author_by_email"));
1825 if ($match_author_by_email and !FeedWordPress::is_null_email($email)) :
1826 $test_email = $email;
1827 else :
1828 $test_email = NULL;
1829 endif;
1830
1831 // Never can be too careful...
1832 $login = sanitize_user($author, /*strict=*/ true);
1833
1834 // Possible for, e.g., foreign script author names
1835 if (strlen($login) < 1) :
1836 // No usable characters in author name for a login.
1837 // (Sometimes results from, e.g., foreign scripts.)
1838 //
1839 // We just need *something* in Western alphanumerics,
1840 // so let's try the domain name.
1841 //
1842 // Uniqueness will be guaranteed below if necessary.
1843
1844 $url = parse_url($hostUrl);
1845
1846 $login = sanitize_user($url['host'], /*strict=*/ true);
1847 if (strlen($login) < 1) :
1848 // This isn't working. Frak it.
1849 $login = 'syndicated';
1850 endif;
1851 endif;
1852
1853 $login = apply_filters('pre_user_login', $login);
1854
1855 $nice_author = sanitize_title($author);
1856 $nice_author = apply_filters('pre_user_nicename', $nice_author);
1857
1858 $reg_author = $wpdb->escape(preg_quote($author));
1859 $author = $wpdb->escape($author);
1860 $email = $wpdb->escape($email);
1861 $test_email = $wpdb->escape($test_email);
1862 $authorUrl = $wpdb->escape($authorUrl);
1863
1864 // Check for an existing author rule....
1865 if (isset($this->link->settings['map authors']['name']['*'])) :
1866 $author_rule = $this->link->settings['map authors']['name']['*'];
1867 elseif (isset($this->link->settings['map authors']['name'][strtolower(trim($author))])) :
1868 $author_rule = $this->link->settings['map authors']['name'][strtolower(trim($author))];
1869 else :
1870 $author_rule = NULL;
1871 endif;
1872
1873 // User name is mapped to a particular author. If that author ID exists, use it.
1874 if (is_numeric($author_rule) and get_userdata((int) $author_rule)) :
1875 $id = (int) $author_rule;
1876
1877 // User name is filtered out
1878 elseif ('filter' == $author_rule) :
1879 $id = NULL;
1880
1881 else :
1882 // Check the database for an existing author record that might fit
1883
1884 // First try the user core data table.
1885 $id = $wpdb->get_var(
1886 "SELECT ID FROM $wpdb->users
1887 WHERE TRIM(LCASE(display_name)) = TRIM(LCASE('$author'))
1888 OR TRIM(LCASE(user_login)) = TRIM(LCASE('$author'))
1889 OR (
1890 LENGTH(TRIM(LCASE(user_email))) > 0
1891 AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
1892 )");
1893
1894 // If that fails, look for aliases in the user meta data table
1895 if (is_null($id)) :
1896 $id = $wpdb->get_var(
1897 "SELECT user_id FROM $wpdb->usermeta
1898 WHERE
1899 (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
1900 OR (
1901 meta_key = 'description'
1902 AND TRIM(LCASE(meta_value))
1903 RLIKE CONCAT(
1904 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
1905 TRIM(LCASE('$reg_author')),
1906 '( |\\t|\\r)*(\\n|\$)'
1907 )
1908 )
1909 ");
1910 endif;
1911
1912 // ... if you don't find one, then do what you need to do
1913 if (is_null($id)) :
1914 if ($unfamiliar_author === 'create') :
1915 $userdata = array();
1916
1917 // WordPress 3 is going to pitch a fit if we attempt to register
1918 // more than one user account with an empty e-mail address, so we
1919 // need *something* here. Ugh.
1920 if (strlen($email) == 0 or FeedWordPress::is_null_email($email)) :
1921 $url = parse_url($hostUrl);
1922 $email = $nice_author.'@'.$url['host'];
1923 endif;
1924
1925 #-- user table data
1926 $userdata['ID'] = NULL; // new user
1927 $userdata['user_login'] = $login;
1928 $userdata['user_nicename'] = $nice_author;
1929 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
1930 $userdata['user_email'] = $email;
1931 $userdata['user_url'] = $authorUrl;
1932 $userdata['nickname'] = $author;
1933 $parts = preg_split('/\s+/', trim($author), 2);
1934 if (isset($parts[0])) : $userdata['first_name'] = $parts[0]; endif;
1935 if (isset($parts[1])) : $userdata['last_name'] = $parts[1]; endif;
1936 $userdata['display_name'] = $author;
1937 $userdata['role'] = 'contributor';
1938
1939 do { // Keep trying until you get it right. Or until PHP crashes, I guess.
1940 $id = wp_insert_user($userdata);
1941 if (is_wp_error($id)) :
1942 $codes = $id->get_error_code();
1943 switch ($codes) :
1944 case 'empty_user_login' :
1945 case 'existing_user_login' :
1946 // Add a random disambiguator
1947 $userdata['user_login'] .= substr(md5(uniqid(microtime())), 0, 6);
1948 break;
1949 case 'existing_user_email' :
1950 // No disassemble!
1951 $parts = explode('@', $userdata['user_email'], 2);
1952
1953 // Add a random disambiguator as a gmail-style username extension
1954 $parts[0] .= '+'.substr(md5(uniqid(microtime())), 0, 6);
1955
1956 // Reassemble
1957 $userdata['user_email'] = $parts[0].'@'.$parts[1];
1958 break;
1959 endswitch;
1960 endif;
1961 } while (is_wp_error($id));
1962 elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
1963 $id = (int) $unfamiliar_author;
1964 elseif ($unfamiliar_author === 'default') :
1965 $id = 1;
1966 endif;
1967 endif;
1968 endif;
1969
1970 if ($id) :
1971 $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
1972
1973 // Multisite: Check whether the author has been recorded
1974 // on *this* blog before. If not, put her down as a
1975 // Contributor for *this* blog.
1976 $user = new WP_User((int) $id);
1977 if (empty($user->roles)) :
1978 $user->add_role('contributor');
1979 endif;
1980 endif;
1981 return $id;
1982 } /* function SyndicatedPost::author_id () */
1983
1984 /**
1985 * category_ids: look up (and create) category ids from a list of categories
1986 *
1987 * @param array $cats
1988 * @param string $unfamiliar_category
1989 * @param array|null $taxonomies
1990 * @return array
1991 */
1992 function category_ids ($cats, $unfamiliar_category = 'create', $taxonomies = NULL, $params = array()) {
1993 $singleton = (isset($params['singleton']) ? $params['singleton'] : true);
1994 $allowFilters = (isset($params['filters']) ? $params['filters'] : false);
1995
1996 $catTax = 'category';
1997
1998 if (is_null($taxonomies)) :
1999 $taxonomies = array('category');
2000 endif;
2001
2002 // We need to normalize whitespace because (1) trailing
2003 // whitespace can cause PHP and MySQL not to see eye to eye on
2004 // VARCHAR comparisons for some versions of MySQL (cf.
2005 // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
2006 // because I doubt most people want to make a semantic
2007 // distinction between 'Computers' and 'Computers '
2008 $cats = array_map('trim', $cats);
2009
2010 $terms = array();
2011 foreach ($taxonomies as $tax) :
2012 $terms[$tax] = array();
2013 endforeach;
2014
2015 foreach ($cats as $cat_name) :
2016 if (preg_match('/^{([^#}]*)#([0-9]+)}$/', $cat_name, $backref)) :
2017 $cat_id = (int) $backref[2];
2018 $tax = $backref[1];
2019 if (strlen($tax) < 1) :
2020 $tax = $catTax;
2021 endif;
2022
2023 $term = term_exists($cat_id, $tax);
2024 if (!is_wp_error($term) and !!$term) :
2025 if (!isset($terms[$tax])) :
2026 $terms[$tax] = array();
2027 endif;
2028 $terms[$tax][] = $cat_id;
2029 endif;
2030 elseif (strlen($cat_name) > 0) :
2031 $familiar = false;
2032 foreach ($taxonomies as $tax) :
2033 if ($tax!='category' or strtolower($cat_name)!='uncategorized') :
2034 $term = term_exists($cat_name, $tax);
2035 if (!is_wp_error($term) and !!$term) :
2036 $familiar = true;
2037
2038 if (is_array($term)) :
2039 $term_id = (int) $term['term_id'];
2040 else :
2041 $term_id = (int) $term;
2042 endif;
2043
2044 if (!isset($terms[$tax])) :
2045 $terms[$tax] = array();
2046 endif;
2047 $terms[$tax][] = $term_id;
2048 break; // We're done here.
2049 endif;
2050 endif;
2051 endforeach;
2052
2053 if (!$familiar) :
2054 if ('tag'==$unfamiliar_category) :
2055 $unfamiliar_category = 'create:post_tag';
2056 endif;
2057
2058 if (preg_match('/^create(:(.*))?$/i', $unfamiliar_category, $ref)) :
2059 $tax = $catTax; // Default
2060 if (isset($ref[2]) and strlen($ref[2]) > 2) :
2061 $tax = $ref[2];
2062 endif;
2063 $term = wp_insert_term($cat_name, $tax);
2064 if (is_wp_error($term)) :
2065 FeedWordPress::noncritical_bug('term insertion problem', array('cat_name' => $cat_name, 'term' => $term, 'this' => $this), __LINE__, __FILE__);
2066 else :
2067 if (!isset($terms[$tax])) :
2068 $terms[$tax] = array();
2069 endif;
2070 $terms[$tax][] = (int) $term['term_id'];
2071 endif;
2072 endif;
2073 endif;
2074 endif;
2075 endforeach;
2076
2077 $filtersOn = $allowFilters;
2078 if ($allowFilters) :
2079 $filters = array_filter(
2080 $this->link->setting('match/filter', 'match_filter', array()),
2081 'remove_dummy_zero'
2082 );
2083 $filtersOn = ($filtersOn and is_array($filters) and (count($filters) > 0));
2084 endif;
2085
2086 // Check for filter conditions
2087 foreach ($terms as $tax => $term_ids) :
2088 if ($filtersOn
2089 and (count($term_ids)==0)
2090 and in_array($tax, $filters)) :
2091 $terms = NULL; // Drop the post
2092 break;
2093 else :
2094 $terms[$tax] = array_unique($term_ids);
2095 endif;
2096 endforeach;
2097
2098 if ($singleton and count($terms)==1) : // If we only searched one, just return the term IDs
2099 $terms = end($terms);
2100 endif;
2101 return $terms;
2102 } // function SyndicatedPost::category_ids ()
2103
2104 function use_api ($tag) {
2105 global $wp_db_version;
2106 switch ($tag) :
2107 case 'wp_insert_post':
2108 // Before 2.2, wp_insert_post does too much of the wrong stuff to use it
2109 // In 1.5 it was such a resource hog it would make PHP segfault on big updates
2110 $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_21);
2111 break;
2112 case 'post_status_pending':
2113 $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_23);
2114 break;
2115 endswitch;
2116 return $ret;
2117 } // function SyndicatedPost::use_api ()
2118
2119 } /* class SyndicatedPost */
2120
2121