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

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