PluginProbe
FeedWordPress / 2010.0602
FeedWordPress v2010.0602
trunk 0.8 0.9 0.91 0.95 0.96 0.97 0.98 0.981 0.99 0.991 0.992 0.993 2008.1030 2008.1101 2008.1105 2008.1214 2009.0612 2009.0613 2009.0618 2009.0707 2009.1111 2009.1112 2010.0127 2010.0528 All 65 releases
← All changes | syndicatedpost.class.php +924 -489 2009.11112010.0602 View file →
@@ -1,39 +1,98 @@
1 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.0531
14 + */
2 15 class SyndicatedPost {
3 - var $item = null;
4 -
16 + var $item = null; // MagpieRSS representation
17 + var $entry = null; // SimplePie_Item representation
18 +
5 19 var $link = null;
6 20 var $feed = null;
7 21 var $feedmeta = null;
8 22
23 + var $xmlns = array ();
24 +
9 25 var $post = array ();
10 26
11 27 var $_freshness = null;
12 28 var $_wp_id = null;
13 29
14 - function SyndicatedPost ($item, $link) {
30 + /**
31 + * SyndicatedPost constructor: Given a feed item and the source from
32 + * which it was taken, prepare a post that can be inserted into the
33 + * WordPress database on request, or updated in place if it has already
34 + * been syndicated.
35 + *
36 + * @param array $item The item syndicated from the feed.
37 + * @param SyndicatedLink $source The feed it was syndicated from.
38 + */
39 + function SyndicatedPost ($item, $source) {
15 40 global $wpdb;
16 41
17 - $this->link = $link;
18 - $feedmeta = $link->settings;
19 - $feed = $link->magpie;
42 + if (is_array($item)
43 + and isset($item['simplepie'])
44 + and isset($item['magpie'])) :
45 + $this->entry = $item['simplepie'];
46 + $this->item = $item['magpie'];
47 + $item = $item['magpie'];
48 + else :
49 + $this->item = $item;
50 + endif;
20 51
21 - # This is ugly as all hell. I'd like to use apply_filters()'s
22 - # alleged support for a variable argument count, but this seems
23 - # to have been broken in WordPress 1.5. It'll be fixed somehow
24 - # in WP 1.5.1, but I'm aiming at WP 1.5 compatibility across
25 - # the board here.
52 + FeedWordPress::diagnostic('feed_items', 'Considering item ['.$this->guid().'] "'.$this->entry->get_title().'"');
53 +
54 + $this->link = $source;
55 + $this->feed = $source->magpie;
56 + $this->feedmeta = $source->settings;
57 +
58 + # Dealing with namespaces can get so fucking fucked.
59 + $this->xmlns['forward'] = $source->magpie->_XMLNS_FAMILIAR;
60 + $this->xmlns['reverse'] = array();
61 + foreach ($this->xmlns['forward'] as $url => $ns) :
62 + if (!isset($this->xmlns['reverse'][$ns])) :
63 + $this->xmlns['reverse'][$ns] = array();
64 + endif;
65 + $this->xmlns['reverse'][$ns][] = $url;
66 + endforeach;
67 +
68 + // Fucking SimplePie.
69 + $this->xmlns['reverse']['rss'][] = '';
70 +
71 + # These globals were originally an ugly kludge around a bug in
72 + # apply_filters from WordPress 1.5. The bug was fixed in 1.5.1,
73 + # and I sure hope at this point that nobody writing filters for
74 + # FeedWordPress is still relying on them.
26 75 #
27 - # Cf.: <http://mosquito.wordpress.org/view.php?id=901>
76 + # Anyway, I hereby declare them DEPRECATED as of 8 February
77 + # 2010. I'll probably remove the globals within 1-2 releases in
78 + # the interests of code hygiene and memory usage. If you
79 + # currently use them in your filters, I advise you switch off to
80 + # accessing the public members SyndicatedPost::feed and
81 + # SyndicatedPost::feedmeta.
82 +
28 83 global $fwp_channel, $fwp_feedmeta;
29 - $fwp_channel = $feed; $fwp_feedmeta = $feedmeta;
84 + $fwp_channel = $this->feed; $fwp_feedmeta = $this->feedmeta;
30 85
31 - $this->feed = $feed;
32 - $this->feedmeta = $feedmeta;
33 -
34 - $this->item = $item;
86 + // Trigger global syndicated_item filter.
35 87 $this->item = apply_filters('syndicated_item', $this->item, $this);
88 +
89 + // Allow for feed-specific syndicated_item filters.
90 + $this->item = apply_filters(
91 + "syndicated_item_".$source->uri(),
92 + $this->item,
93 + $this
94 + );
36 95
37 96 # Filters can halt further processing by returning NULL
38 97 if (is_null($this->item)) :
39 98 $this->post = NULL;
@@ -42,51 +101,31 @@
42 101 # That's deliberate. The escaping is done at the point
43 102 # of insertion, not here, to avoid double-escaping and
44 103 # to avoid screwing with syndicated_post filters
45 104
46 - $this->post['post_title'] = apply_filters('syndicated_item_title', $this->item['title'], $this);
105 + $this->post['post_title'] = apply_filters(
106 + 'syndicated_item_title',
107 + $this->entry->get_title(), $this
108 + );
47 109
48 - // This just gives us an alphanumeric representation of
49 - // the author. We will look up (or create) the numeric
50 - // ID for the author in SyndicatedPost::add()
51 - $this->post['named']['author'] = apply_filters('syndicated_item_author', $this->author(), $this);
110 + $this->post['named']['author'] = apply_filters(
111 + 'syndicated_item_author',
112 + $this->author(), $this
113 + );
114 + // This just gives us an alphanumeric name for the author.
115 + // We look up (or create) the numeric ID for the author
116 + // in SyndicatedPost::add().
52 117
53 - # Identify content and sanitize it.
54 - # ---------------------------------
55 - if (isset($this->item['atom_content'])) :
56 - $content = $this->item['atom_content'];
57 - elseif (isset($this->item['xhtml']['body'])) :
58 - $content = $this->item['xhtml']['body'];
59 - elseif (isset($this->item['xhtml']['div'])) :
60 - $content = $this->item['xhtml']['div'];
61 - elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
62 - $content = $this->item['content']['encoded'];
63 - else:
64 - $content = $this->item['description'];
65 - endif;
66 - $this->post['post_content'] = apply_filters('syndicated_item_content', $content, $this);
67 -
68 - # Identify and sanitize excerpt
69 - $excerpt = NULL;
70 - if ( isset($this->item['description']) and $this->item['description'] ) :
71 - $excerpt = $this->item['description'];
72 - elseif ( isset($content) and $content ) :
73 - $excerpt = strip_tags($content);
74 - if (strlen($excerpt) > 255) :
75 - $excerpt = substr($excerpt,0,252).'...';
76 - endif;
77 - endif;
78 - $excerpt = apply_filters('syndicated_item_excerpt', $excerpt, $this);
79 -
80 - if (!is_null($excerpt)):
118 + $this->post['post_content'] = apply_filters(
119 + 'syndicated_item_content',
120 + $this->content(), $this
121 + );
122 +
123 + $excerpt = apply_filters('syndicated_item_excerpt', $this->excerpt(), $this);
124 + if (!empty($excerpt)):
81 125 $this->post['post_excerpt'] = $excerpt;
82 126 endif;
83 127
84 - // This is unnecessary if we use wp_insert_post
85 - if (!$this->use_api('wp_insert_post')) :
86 - $this->post['post_name'] = sanitize_title($this->post['post_title']);
87 - endif;
88 -
89 128 $this->post['epoch']['issued'] = apply_filters('syndicated_item_published', $this->published(), $this);
90 129 $this->post['epoch']['created'] = apply_filters('syndicated_item_created', $this->created(), $this);
91 130 $this->post['epoch']['modified'] = apply_filters('syndicated_item_updated', $this->updated(), $this);
92 131
@@ -91,12 +130,12 @@
91 130 $this->post['epoch']['modified'] = apply_filters('syndicated_item_updated', $this->updated(), $this);
92 131
93 132 // Dealing with timestamps in WordPress is so fucking fucked.
94 133 $offset = (int) get_option('gmt_offset') * 60 * 60;
95 - $this->post['post_date'] = gmdate('Y-m-d H:i:s', $this->published() + $offset);
96 - $this->post['post_modified'] = gmdate('Y-m-d H:i:s', $this->updated() + $offset);
97 - $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $this->published());
98 - $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $this->updated());
134 + $this->post['post_date'] = gmdate('Y-m-d H:i:s', apply_filters('syndicated_item_published', $this->published(/*fallback=*/ true, /*default=*/ -1), $this) + $offset);
135 + $this->post['post_modified'] = gmdate('Y-m-d H:i:s', apply_filters('syndicated_item_updated', $this->updated(/*fallback=*/ true, /*default=*/ -1), $this) + $offset);
136 + $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', apply_filters('syndicated_item_published', $this->published(/*fallback=*/ true, /*default=*/ -1), $this));
137 + $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', apply_filters('syndicated_item_updated', $this->updated(/*fallback=*/ true, /*default=*/ -1), $this));
99 138
100 139 // Use feed-level preferences or the global default.
101 140 $this->post['post_status'] = $this->link->syndicated_status('post', 'publish');
102 141 $this->post['comment_status'] = $this->link->syndicated_status('comment', 'closed');
@@ -120,20 +159,49 @@
120 159 endif;
121 160 if (!is_array($custom_settings)) :
122 161 $custom_settings = array();
123 162 endif;
124 - $this->post['meta'] = array_merge($default_custom_settings, $custom_settings);
163 +
164 + $postMetaIn = array_merge($default_custom_settings, $custom_settings);
165 + $postMetaOut = array();
125 166
167 + // Big ugly fuckin loop to do any element substitutions
168 + // that we may need.
169 + foreach ($postMetaIn as $key => $values) :
170 + if (is_string($values)) : $values = array($values); endif;
171 +
172 + $postMetaOut[$key] = array();
173 + foreach ($values as $value) :
174 + if (preg_match('/\$\( ([^)]+) \)/x', $value, $ref)) :
175 + $elements = $this->query($ref[1]);
176 + foreach ($elements as $element) :
177 + $postMetaOut[$key][] = str_replace(
178 + $ref[0],
179 + $element,
180 + $value
181 + );
182 + endforeach;
183 + else :
184 + $postMetaOut[$key][] = $value;
185 + endif;
186 + endforeach;
187 + endforeach;
188 +
189 + foreach ($postMetaOut as $key => $values) :
190 + $this->post['meta'][$key] = array();
191 + foreach ($values as $value) :
192 + $this->post['meta'][$key][] = apply_filters("syndicated_post_meta_{$key}", $value, $this);
193 + endforeach;
194 + endforeach;
195 +
126 196 // RSS 2.0 / Atom 1.0 enclosure support
127 - if ( isset($this->item['enclosure#']) ) :
128 - for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
129 - $eid = (($i > 1) ? "#{$id}" : "");
130 - $this->post['meta']['enclosure'][] =
131 - apply_filters('syndicated_item_enclosure_url', $this->item["enclosure{$eid}@url"], $this)."\n".
132 - apply_filters('syndicated_item_enclosure_length', $this->item["enclosure{$eid}@length"], $this)."\n".
133 - apply_filters('syndicated_item_enclosure_type', $this->item["enclosure{$eid}@type"], $this);
134 - endfor;
135 - endif;
197 + $enclosures = $this->entry->get_enclosures();
198 + if (is_array($enclosures)) : foreach ($enclosures as $enclosure) :
199 + $this->post['meta']['enclosure'][] =
200 + apply_filters('syndicated_item_enclosure_url', $enclosure->get_link(), $this)."\n".
201 + apply_filters('syndicated_item_enclosure_length', $enclosure->get_length(), $this)."\n".
202 + apply_filters('syndicated_item_enclosure_type', $enclosure->get_type(), $this);
203 + endforeach; endif;
136 204
137 205 // In case you want to point back to the blog this was syndicated from
138 206 if (isset($this->feed->channel['title'])) :
139 207 $this->post['meta']['syndication_source'] = apply_filters('syndicated_item_source_title', $this->feed->channel['title'], $this);
@@ -156,33 +224,26 @@
156 224 $this->post['meta']['syndication_source_id_original'] = $this->item['source_id'];
157 225 endif;
158 226
159 227 // Store information on human-readable and machine-readable comment URIs
160 - if (isset($this->item['comments'])) :
161 - $this->post['meta']['rss:comments'] = apply_filters('syndicated_item_comments', $this->item['comments']);
162 - endif;
163 228
164 - // RSS 2.0 comment feeds extension
165 - if (isset($this->item['wfw']['commentrss'])) :
166 - $this->post['meta']['wfw:commentRSS'] = apply_filters('syndicated_item_commentrss', $this->item['wfw']['commentrss']);
167 - endif;
229 + // Human-readable comment URI
230 + $commentLink = apply_filters('syndicated_item_comments', $this->comment_link(), $this);
231 + if (!is_null($commentLink)) : $this->post['meta']['rss:comments'] = $commentLink; endif;
168 232
169 - // Atom 1.0 comment feeds link-rel
170 - if (isset($this->item['link_replies'])) :
171 - // There may be multiple <link rel="replies"> elements; feeds have a feed MIME type
172 - $N = isset($this->item['link_replies#']) ? $this->item['link_replies#'] : 1;
173 - for ($i = 1; $i <= $N; $i++) :
174 - $currentElement = 'link_replies'.(($i > 1) ? '#'.$i : '');
175 - if (isset($this->item[$currentElement.'@type'])
176 - and preg_match("\007application/(atom|rss|rdf)\+xml\007i", $this->item[$currentElement.'@type'])) :
177 - $this->post['meta']['wfw:commentRSS'] = apply_filters('syndicated_item_commentrss', $this->item[$currentElement]);
178 - endif;
179 - endfor;
180 - endif;
233 + // Machine-readable content feed URI
234 + $commentFeed = apply_filters('syndicated_item_commentrss', $this->comment_feed(), $this);
235 + if (!is_null($commentFeed)) : $this->post['meta']['wfw:commentRSS'] = $commentFeed; endif;
236 + // Yeah, yeah, now I know that it's supposed to be
237 + // wfw:commentRss. Oh well. Path dependence, sucka.
181 238
182 239 // Store information to identify the feed that this came from
183 - $this->post['meta']['syndication_feed'] = $this->feedmeta['link/uri'];
184 - $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
240 + if (isset($this->feedmeta['link/uri'])) :
241 + $this->post['meta']['syndication_feed'] = $this->feedmeta['link/uri'];
242 + endif;
243 + if (isset($this->feedmeta['link/id'])) :
244 + $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
245 + endif;
185 246
186 247 if (isset($this->item['source_link_self'])) :
187 248 $this->post['meta']['syndication_feed_original'] = $this->item['source_link_self'];
188 249 endif;
@@ -187,20 +248,10 @@
187 248 $this->post['meta']['syndication_feed_original'] = $this->item['source_link_self'];
188 249 endif;
189 250
190 251 // In case you want to know the external permalink...
191 - if (isset($this->item['link'])) :
192 - $permalink = $this->item['link'];
252 + $this->post['meta']['syndication_permalink'] = apply_filters('syndicated_item_link', $this->permalink());
193 253
194 - // No <link> element. See if this feed has <guid isPermalink="true"> ....
195 - elseif (isset($this->item['guid'])) :
196 - if (isset($this->item['guid@ispermalink']) and strtolower(trim($this->item['guid@ispermalink'])) != 'false') :
197 - $permalink = $this->item['guid'];
198 - endif;
199 - endif;
200 -
201 - $this->post['meta']['syndication_permalink'] = apply_filters('syndicated_item_link', $permalink);
202 -
203 254 // Store a hash of the post content for checking whether something needs to be updated
204 255 $this->post['meta']['syndication_item_hash'] = $this->update_hash();
205 256
206 257 // Feed-by-feed options for author and category creation
@@ -248,14 +299,685 @@
248 299 $this->post['tags_input'] = array_merge($this->post['tags_input'], $this->feedmeta['tags']);
249 300 endif;
250 301 $this->post['tags_input'] = apply_filters('syndicated_item_tags', $this->post['tags_input'], $this);
251 302 endif;
252 - } // SyndicatedPost::SyndicatedPost()
303 + } /* SyndicatedPost::SyndicatedPost() */
253 304
305 + #####################################
306 + #### EXTRACT DATA FROM FEED ITEM ####
307 + #####################################
308 +
309 + /**
310 + * SyndicatedPost::query uses an XPath-like syntax to query arbitrary
311 + * elements within the syndicated item.
312 + *
313 + * @param string $path
314 + * @returns array of string values representing contents of matching
315 + * elements or attributes
316 + */
317 + function query ($path) {
318 + $urlHash = array();
319 +
320 + // Allow {url} notation for namespaces. URLs will contain : and /, so...
321 + preg_match_all('/{([^}]+)}/', $path, $match, PREG_SET_ORDER);
322 + foreach ($match as $ref) :
323 + $urlHash[md5($ref[1])] = $ref[1];
324 + endforeach;
325 +
326 + foreach ($urlHash as $hash => $url) :
327 + $path = str_replace('{'.$url.'}', '{#'.$hash.'}', $path);
328 + endforeach;
329 +
330 + $path = explode('/', $path);
331 + foreach ($path as $index => $node) :
332 + if (preg_match('/{#([^}]+)}/', $node, $ref)) :
333 + if (isset($urlHash[$ref[1]])) :
334 + $path[$index] = str_replace(
335 + '{#'.$ref[1].'}',
336 + '{'.$urlHash[$ref[1]].'}',
337 + $node
338 + );
339 + endif;
340 + endif;
341 + endforeach;
342 +
343 + // Start out with a get_item_tags query.
344 + $node = '';
345 + while (strlen($node)==0 and !is_null($node)) :
346 + $node = array_shift($path);
347 + endwhile;
348 +
349 + switch ($node) :
350 + case 'feed' :
351 + case 'channel' :
352 + $method = "get_${node}_tags";
353 + $node = array_shift($path);
354 + break;
355 + case 'item' :
356 + $node = array_shift($path);
357 + default :
358 + $method = NULL;
359 + endswitch;
360 +
361 + $data = array();
362 + if (!is_null($node)) :
363 + list($namespaces, $element) = $this->xpath_extended_name($node);
364 +
365 + $matches = array();
366 + foreach ($namespaces as $ns) :
367 + if (!is_null($method)) :
368 + $el = $this->link->simplepie->{$method}($ns, $element);
369 + else :
370 + $el = $this->entry->get_item_tags($ns, $element);
371 + endif;
372 +
373 + if (!is_null($el)) :
374 + $matches = array_merge($matches, $el);
375 + endif;
376 + endforeach;
377 + $data = $matches;
378 +
379 + $node = array_shift($path);
380 + endif;
381 +
382 + while (!is_null($node)) :
383 + if (strlen($node) > 0) :
384 + $matches = array();
385 +
386 + list($ns, $element) = $this->xpath_extended_name($node);
387 +
388 + if (preg_match('/^@(.*)$/', $element, $ref)) :
389 + $element = $ref[1];
390 + $axis = 'attribs';
391 + else :
392 + $axis = 'child';
393 + endif;
394 +
395 + foreach ($data as $datum) :
396 + foreach ($namespaces as $ns) :
397 + if (!is_string($datum)
398 + and isset($datum[$axis][$ns][$element])) :
399 + if (is_string($datum[$axis][$ns][$element])) :
400 + $matches[] = $datum[$axis][$ns][$element];
401 + else :
402 + $matches = array_merge($matches, $datum[$axis][$ns][$element]);
403 + endif;
404 + endif;
405 + endforeach;
406 + endforeach;
407 +
408 + $data = $matches;
409 + endif;
410 + $node = array_shift($path);
411 + endwhile;
412 +
413 + $matches = array();
414 + foreach ($data as $datum) :
415 + if (is_string($datum)) :
416 + $matches[] = $datum;
417 + elseif (isset($datum['data'])) :
418 + $matches[] = $datum['data'];
419 + endif;
420 + endforeach;
421 + return $matches;
422 + } /* SyndicatedPost::query() */
423 +
424 + function xpath_default_namespace () {
425 + // Get the default namespace.
426 + $type = $this->link->simplepie->get_type();
427 + if ($type & SIMPLEPIE_TYPE_ATOM_10) :
428 + $defaultNS = SIMPLEPIE_NAMESPACE_ATOM_10;
429 + elseif ($type & SIMPLEPIE_TYPE_ATOM_03) :
430 + $defaultNS = SIMPLEPIE_NAMESPACE_ATOM_03;
431 + elseif ($type & SIMPLEPIE_TYPE_RSS_090) :
432 + $defaultNS = SIMPLEPIE_NAMESPACE_RSS_090;
433 + elseif ($type & SIMPLEPIE_TYPE_RSS_10) :
434 + $defaultNS = SIMPLEPIE_NAMESPACE_RSS_10;
435 + elseif ($type & SIMPLEPIE_TYPE_RSS_20) :
436 + $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20;
437 + else :
438 + $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20;
439 + endif;
440 + return $defaultNS;
441 + } /* SyndicatedPost::xpath_default_namespace() */
442 +
443 + function xpath_extended_name ($node) {
444 + $ns = NULL; $element = NULL;
445 +
446 + if (substr($node, 0, 1)=='@') :
447 + $attr = '@'; $node = substr($node, 1);
448 + else :
449 + $attr = '';
450 + endif;
451 +
452 + if (preg_match('/^{([^}]*)}(.*)$/', $node, $ref)) :
453 + $ns = array($ref[1]); $element = $ref[2];
454 + elseif (strpos($node, ':') !== FALSE) :
455 + list($xmlns, $element) = explode(':', $node, 2);
456 + if (isset($this->xmlns['reverse'][$xmlns])) :
457 + $ns = $this->xmlns['reverse'][$xmlns];
458 + else :
459 + $ns = array($xmlns);
460 + endif;
461 +
462 + // Fucking SimplePie. For attributes in default xmlns.
463 + if ($xmlns==$this->xmlns['forward'][$defaultNS[0]]) :
464 + $ns[] = '';
465 + endif;
466 + else :
467 + // Often in SimplePie, the default namespace gets stored
468 + // as an empty string rather than a URL.
469 + $ns = array($this->xpath_default_namespace(), '');
470 + $element = $node;
471 + endif;
472 + return array(array_unique($ns), $attr.$element);
473 + } /* SyndicatedPost::xpath_extended_name () */
474 +
475 + function content () {
476 + $content = NULL;
477 + if (isset($this->item['atom_content'])) :
478 + $content = $this->item['atom_content'];
479 + elseif (isset($this->item['xhtml']['body'])) :
480 + $content = $this->item['xhtml']['body'];
481 + elseif (isset($this->item['xhtml']['div'])) :
482 + $content = $this->item['xhtml']['div'];
483 + elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
484 + $content = $this->item['content']['encoded'];
485 + else:
486 + $content = $this->item['description'];
487 + endif;
488 + return $content;
489 + } /* SyndicatedPost::content() */
490 +
491 + function excerpt () {
492 + # Identify and sanitize excerpt: atom:summary, or rss:description
493 + $excerpt = $this->entry->get_description();
494 +
495 + # Many RSS feeds use rss:description, inadvisably, to
496 + # carry the entire post (typically with escaped HTML).
497 + # If that's what happened, we don't want the full
498 + # content for the excerpt.
499 + $content = $this->content();
500 +
501 + // Ignore whitespace, case, and tag cruft.
502 + $theExcerpt = preg_replace('/\s+/', '', strtolower(strip_tags($excerpt)));
503 + $theContent = preg_replace('/\s+/', '', strtolower(strip_Tags($content)));
504 +
505 + if ( empty($excerpt) or $theExcerpt == $theContent ) :
506 + # If content is available, generate an excerpt.
507 + if ( strlen(trim($content)) > 0 ) :
508 + $excerpt = strip_tags($content);
509 + if (strlen($excerpt) > 255) :
510 + $excerpt = substr($excerpt,0,252).'...';
511 + endif;
512 + endif;
513 + endif;
514 + return $excerpt;
515 + } /* SyndicatedPost::excerpt() */
516 +
517 + function permalink () {
518 + // Handles explicit <link> elements and also RSS 2.0 cases with
519 + // <guid isPermaLink="true">, etc. Hooray!
520 + $permalink = $this->entry->get_link();
521 + return $permalink;
522 + }
523 +
524 + function created () {
525 + $date = '';
526 + if (isset($this->item['dc']['created'])) :
527 + $date = $this->item['dc']['created'];
528 + elseif (isset($this->item['dcterms']['created'])) :
529 + $date = $this->item['dcterms']['created'];
530 + elseif (isset($this->item['created'])): // Atom 0.3
531 + $date = $this->item['created'];
532 + endif;
533 +
534 + $epoch = new FeedTime($date);
535 + return $epoch->timestamp();
536 + } /* SyndicatedPost::created() */
537 +
538 + function published ($fallback = true, $default = NULL) {
539 + $date = '';
540 +
541 + # RSS is a fucking mess. Figure out whether we have a date in
542 + # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
543 + # epoch format for reformatting. If we can't find anything,
544 + # we'll use the last-updated time.
545 + if (isset($this->item['dc']['date'])): // Dublin Core
546 + $date = $this->item['dc']['date'];
547 + elseif (isset($this->item['dcterms']['issued'])) : // Dublin Core extensions
548 + $date = $this->item['dcterms']['issued'];
549 + elseif (isset($this->item['published'])) : // Atom 1.0
550 + $date = $this->item['published'];
551 + elseif (isset($this->item['issued'])): // Atom 0.3
552 + $date = $this->item['issued'];
553 + elseif (isset($this->item['pubdate'])): // RSS 2.0
554 + $date = $this->item['pubdate'];
555 + endif;
556 +
557 + if (strlen($date) > 0) :
558 + $time = new FeedTime($date);
559 + $epoch = $time->timestamp();
560 + elseif ($fallback) : // Fall back to <updated> / <modified> if present
561 + $epoch = $this->updated(/*fallback=*/ false, /*default=*/ $default);
562 + endif;
563 +
564 + # If everything failed, then default to the current time.
565 + if (is_null($epoch)) :
566 + if (-1 == $default) :
567 + $epoch = time();
568 + else :
569 + $epoch = $default;
570 + endif;
571 + endif;
572 +
573 + return $epoch;
574 + } /* SyndicatedPost::published() */
575 +
576 + function updated ($fallback = true, $default = -1) {
577 + $date = '';
578 +
579 + # As far as I know, only dcterms and Atom have reliable ways to
580 + # specify when something was *modified* last. If neither is
581 + # available, then we'll try to get the time of publication.
582 + if (isset($this->item['dc']['modified'])) : // Not really correct
583 + $date = $this->item['dc']['modified'];
584 + elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
585 + $date = $this->item['dcterms']['modified'];
586 + elseif (isset($this->item['modified'])): // Atom 0.3
587 + $date = $this->item['modified'];
588 + elseif (isset($this->item['updated'])): // Atom 1.0
589 + $date = $this->item['updated'];
590 + endif;
591 +
592 + if (strlen($date) > 0) :
593 + $time = new FeedTime($date);
594 + $epoch = $time->timestamp();
595 + elseif ($fallback) : // Fall back to issued / dc:date
596 + $epoch = $this->published(/*fallback=*/ false, /*default=*/ $default);
597 + endif;
598 +
599 + # If everything failed, then default to the current time.
600 + if (is_null($epoch)) :
601 + if (-1 == $default) :
602 + $epoch = time();
603 + else :
604 + $epoch = $default;
605 + endif;
606 + endif;
607 +
608 + return $epoch;
609 + } /* SyndicatedPost::updated() */
610 +
611 + function update_hash () {
612 + return md5(serialize($this->item));
613 + } /* SyndicatedPost::update_hash() */
614 +
615 + function guid () {
616 + $guid = null;
617 + if (isset($this->item['id'])): // Atom 0.3 / 1.0
618 + $guid = $this->item['id'];
619 + elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
620 + $guid = $this->item['atom']['id'];
621 + elseif (isset($this->item['guid'])) : // RSS 2.0
622 + $guid = $this->item['guid'];
623 + elseif (isset($this->item['dc']['identifier'])) :// yeah, right
624 + $guid = $this->item['dc']['identifier'];
625 + else :
626 + // The feed does not seem to have provided us with a
627 + // unique identifier, so we'll have to cobble together
628 + // a tag: URI that might work for us. The base of the
629 + // URI will be the host name of the feed source ...
630 + $bits = parse_url($this->feedmeta['link/uri']);
631 + $guid = 'tag:'.$bits['host'];
632 +
633 + // If we have a date of creation, then we can use that
634 + // to uniquely identify the item. (On the other hand, if
635 + // the feed producer was consicentious enough to
636 + // generate dates of creation, she probably also was
637 + // conscientious enough to generate unique identifiers.)
638 + if (!is_null($this->created())) :
639 + $guid .= '://post.'.date('YmdHis', $this->created());
640 +
641 + // Otherwise, use both the URI of the item, *and* the
642 + // item's title. We have to use both because titles are
643 + // often not unique, and sometimes links aren't unique
644 + // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
645 + // some podcasts). But it's rare to have *both* the same
646 + // title *and* the same link for two different items. So
647 + // this is about the best we can do.
648 + else :
649 + $guid .= '://'.md5($this->item['link'].'/'.$this->item['title']);
650 + endif;
651 + endif;
652 + return $guid;
653 + } /* SyndicatedPost::guid() */
654 +
655 + function author () {
656 + $author = array ();
657 +
658 + if (isset($this->item['author_name'])):
659 + $author['name'] = $this->item['author_name'];
660 + elseif (isset($this->item['dc']['creator'])):
661 + $author['name'] = $this->item['dc']['creator'];
662 + elseif (isset($this->item['dc']['contributor'])):
663 + $author['name'] = $this->item['dc']['contributor'];
664 + elseif (isset($this->feed->channel['dc']['creator'])) :
665 + $author['name'] = $this->feed->channel['dc']['creator'];
666 + elseif (isset($this->feed->channel['dc']['contributor'])) :
667 + $author['name'] = $this->feed->channel['dc']['contributor'];
668 + elseif (isset($this->feed->channel['author_name'])) :
669 + $author['name'] = $this->feed->channel['author_name'];
670 + elseif ($this->feed->is_rss() and isset($this->item['author'])) :
671 + // The author element in RSS is allegedly an
672 + // e-mail address, but lots of people don't use
673 + // it that way. So let's make of it what we can.
674 + $author = parse_email_with_realname($this->item['author']);
675 +
676 + if (!isset($author['name'])) :
677 + if (isset($author['email'])) :
678 + $author['name'] = $author['email'];
679 + else :
680 + $author['name'] = $this->feed->channel['title'];
681 + endif;
682 + endif;
683 + else :
684 + $author['name'] = $this->feed->channel['title'];
685 + endif;
686 +
687 + if (isset($this->item['author_email'])):
688 + $author['email'] = $this->item['author_email'];
689 + elseif (isset($this->feed->channel['author_email'])) :
690 + $author['email'] = $this->feed->channel['author_email'];
691 + endif;
692 +
693 + if (isset($this->item['author_url'])):
694 + $author['uri'] = $this->item['author_url'];
695 + elseif (isset($this->feed->channel['author_url'])) :
696 + $author['uri'] = $this->item['author_url'];
697 + elseif (isset($this->feed->channel['link'])) :
698 + $author['uri'] = $this->feed->channel['link'];
699 + endif;
700 +
701 + return $author;
702 + } /* SyndicatedPost::author() */
703 +
704 + /**
705 + * SyndicatedPost::isTaggedAs: Test whether a feed item is
706 + * tagged / categorized with a given string. Case and leading and
707 + * trailing whitespace are ignored.
708 + *
709 + * @param string $tag Tag to check for
710 + *
711 + * @return bool Whether or not at least one of the categories / tags on
712 + * $this->item is set to $tag (modulo case and leading and trailing
713 + * whitespace)
714 + */
715 + function isTaggedAs ($tag) {
716 + $desiredTag = strtolower(trim($tag)); // Normalize case and whitespace
717 +
718 + // Check to see if this is tagged with $tag
719 + $currentCategory = 'category';
720 + $currentCategoryNumber = 1;
721 +
722 + // If we have the new MagpieRSS, the number of category elements
723 + // on this item is stored under index "category#".
724 + if (isset($this->item['category#'])) :
725 + $numberOfCategories = (int) $this->item['category#'];
726 +
727 + // We REALLY shouldn't have the old and busted MagpieRSS, but in
728 + // case we do, it doesn't support multiple categories, but there
729 + // might still be a single value under the "category" index.
730 + elseif (isset($this->item['category'])) :
731 + $numberOfCategories = 1;
732 +
733 + // No standard category or tag elements on this feed item.
734 + else :
735 + $numberOfCategories = 0;
736 +
737 + endif;
738 +
739 + $isSoTagged = false; // Innocent until proven guilty
740 +
741 + // Loop through category elements; if there are multiple
742 + // elements, they are indexed as category, category#2,
743 + // category#3, ... category#N
744 + while ($currentCategoryNumber <= $numberOfCategories) :
745 + if ($desiredTag == strtolower(trim($this->item[$currentCategory]))) :
746 + $isSoTagged = true; // Got it!
747 + break;
748 + endif;
749 +
750 + $currentCategoryNumber += 1;
751 + $currentCategory = 'category#'.$currentCategoryNumber;
752 + endwhile;
753 +
754 + return $isSoTagged;
755 + } /* SyndicatedPost::isTaggedAs() */
756 +
757 + /**
758 + * SyndicatedPost::enclosures: returns an array with any enclosures
759 + * that may be attached to this syndicated item.
760 + *
761 + * @param string $type If you only want enclosures that match a certain
762 + * MIME type or group of MIME types, you can limit the enclosures
763 + * that will be returned to only those with a MIME type which
764 + * matches this regular expression.
765 + * @return array
766 + */
767 + function enclosures ($type = '/.*/') {
768 + $enclosures = array();
769 +
770 + if (isset($this->item['enclosure#'])) :
771 + // Loop through enclosure, enclosure#2, enclosure#3, ....
772 + for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
773 + $eid = (($i > 1) ? "#{$id}" : "");
774 +
775 + // Does it match the type we want?
776 + if (preg_match($type, $this->item["enclosure{$eid}@type"])) :
777 + $enclosures[] = array(
778 + "url" => $this->item["enclosure{$eid}@url"],
779 + "type" => $this->item["enclosure{$eid}@type"],
780 + "length" => $this->item["enclosure{$eid}@length"],
781 + );
782 + endif;
783 + endfor;
784 + endif;
785 + return $enclosures;
786 + } /* SyndicatedPost::enclosures() */
787 +
788 + function comment_link () {
789 + $url = null;
790 +
791 + // RSS 2.0 has a standard <comments> element:
792 + // "<comments> is an optional sub-element of <item>. If present,
793 + // it is the url of the comments page for the item."
794 + // <http://cyber.law.harvard.edu/rss/rss.html#ltcommentsgtSubelementOfLtitemgt>
795 + if (isset($this->item['comments'])) :
796 + $url = $this->item['comments'];
797 + endif;
798 +
799 + // The convention in Atom feeds is to use a standard <link>
800 + // element with @rel="replies" and @type="text/html".
801 + // Unfortunately, SimplePie_Item::get_links() allows us to filter
802 + // by the value of @rel, but not by the value of @type. *sigh*
803 +
804 + // Try Atom 1.0 first
805 + $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link');
806 +
807 + // Fall back and try Atom 0.3
808 + if (is_null($linkElements)) : $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link'); endif;
809 +
810 + // Now loop through the elements, screening by @rel and @type
811 + if (is_array($linkElements)) : foreach ($linkElements as $link) :
812 + $rel = (isset($link['attribs']['']['rel']) ? $link['attribs']['']['rel'] : 'alternate');
813 + $type = (isset($link['attribs']['']['type']) ? $link['attribs']['']['type'] : NULL);
814 + $href = (isset($link['attribs']['']['href']) ? $link['attribs']['']['href'] : NULL);
815 +
816 + if (strtolower($rel)=='replies' and $type=='text/html' and !is_null($href)) :
817 + $url = $href;
818 + endif;
819 + endforeach; endif;
820 +
821 + return $url;
822 + }
823 +
824 + function comment_feed () {
825 + $feed = null;
826 +
827 + // Well Formed Web comment feeds extension for RSS 2.0
828 + // <http://www.sellsbrothers.com/spout/default.aspx?content=archive.htm#exposingRssComments>
829 + //
830 + // N.B.: Correct capitalization is wfw:commentRss, but
831 + // wfw:commentRSS is common in the wild (partly due to a typo in
832 + // the original spec). In any case, our item array is normalized
833 + // to all lowercase anyways.
834 + if (isset($this->item['wfw']['commentrss'])) :
835 + $feed = $this->item['wfw']['commentrss'];
836 + endif;
837 +
838 + // In Atom 1.0, the convention is to use a standard link element
839 + // with @rel="replies". Sometimes this is also used to pass a
840 + // link to the human-readable comments page, so we also need to
841 + // check link/@type for a feed MIME type.
842 + //
843 + // Which is why I'm not using the SimplePie_Item::get_links()
844 + // method here, incidentally: it doesn't allow you to filter by
845 + // @type. *sigh*
846 + if (isset($this->item['link_replies'])) :
847 + // There may be multiple <link rel="replies"> elements; feeds have a feed MIME type
848 + $N = isset($this->item['link_replies#']) ? $this->item['link_replies#'] : 1;
849 + for ($i = 1; $i <= $N; $i++) :
850 + $currentElement = 'link_replies'.(($i > 1) ? '#'.$i : '');
851 + if (isset($this->item[$currentElement.'@type'])
852 + and preg_match("\007application/(atom|rss|rdf)\+xml\007i", $this->item[$currentElement.'@type'])) :
853 + $feed = $this->item[$currentElement];
854 + endif;
855 + endfor;
856 + endif;
857 + return $feed;
858 + } /* SyndicatedPost::comment_feed() */
859 +
860 + ##################################
861 + #### BUILT-IN CONTENT FILTERS ####
862 + ##################################
863 +
864 + var $uri_attrs = array (
865 + array('a', 'href'),
866 + array('applet', 'codebase'),
867 + array('area', 'href'),
868 + array('blockquote', 'cite'),
869 + array('body', 'background'),
870 + array('del', 'cite'),
871 + array('form', 'action'),
872 + array('frame', 'longdesc'),
873 + array('frame', 'src'),
874 + array('iframe', 'longdesc'),
875 + array('iframe', 'src'),
876 + array('head', 'profile'),
877 + array('img', 'longdesc'),
878 + array('img', 'src'),
879 + array('img', 'usemap'),
880 + array('input', 'src'),
881 + array('input', 'usemap'),
882 + array('ins', 'cite'),
883 + array('link', 'href'),
884 + array('object', 'classid'),
885 + array('object', 'codebase'),
886 + array('object', 'data'),
887 + array('object', 'usemap'),
888 + array('q', 'cite'),
889 + array('script', 'src')
890 + ); /* var SyndicatedPost::$uri_attrs */
891 +
892 + var $_base = null;
893 +
894 + function resolve_single_relative_uri ($refs) {
895 + $tag = FeedWordPressHTML::attributeMatch($refs);
896 + $url = Relative_URI::resolve($tag['value'], $this->_base);
897 + return $tag['prefix'] . $url . $tag['suffix'];
898 + } /* function SyndicatedPost::resolve_single_relative_uri() */
899 +
900 + function resolve_relative_uris ($content, $obj) {
901 + $set = $obj->link->setting('resolve relative', 'resolve_relative', 'yes');
902 + if ($set and $set != 'no') :
903 + // Fallback: if we don't have anything better, use the
904 + // item link from the feed
905 + $obj->_base = $obj->item['link']; // Reset the base for resolving relative URIs
906 +
907 + // What we should do here, properly, is to use
908 + // SimplePie_Item::get_base() -- but that method is
909 + // currently broken. Or getting down and dirty in the
910 + // SimplePie representation of the content tags and
911 + // grabbing the xml_base member for the content element.
912 + // Maybe someday...
913 +
914 + foreach ($obj->uri_attrs as $pair) :
915 + list($tag, $attr) = $pair;
916 + $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
917 + $content = preg_replace_callback (
918 + $pattern,
919 + array(&$obj, 'resolve_single_relative_uri'),
920 + $content
921 + );
922 + endforeach;
923 + endif;
924 +
925 + return $content;
926 + } /* function SyndicatedPost::resolve_relative_uris () */
927 +
928 + var $strip_attrs = array (
929 + array('[a-z]+', 'target'),
930 +// array('[a-z]+', 'style'),
931 +// array('[a-z]+', 'on[a-z]+'),
932 + );
933 +
934 + function strip_attribute_from_tag ($refs) {
935 + $tag = FeedWordPressHTML::attributeMatch($refs);
936 + return $tag['before_attribute'].$tag['after_attribute'];
937 + }
938 +
939 + function sanitize_content ($content, $obj) {
940 + # This kind of sucks. I intend to replace it with
941 + # lib_filter sometime soon.
942 + foreach ($obj->strip_attrs as $pair):
943 + list($tag,$attr) = $pair;
944 + $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
945 +
946 + $content = preg_replace_callback (
947 + $pattern,
948 + array(&$obj, 'strip_attribute_from_tag'),
949 + $content
950 + );
951 + endforeach;
952 + return $content;
953 + } /* SyndicatedPost::sanitize() */
954 +
955 + #####################
956 + #### POST STATUS ####
957 + #####################
958 +
959 + /**
960 + * SyndicatedPost::filtered: check whether or not this post has been
961 + * screened out by a registered filter.
962 + *
963 + * @return bool TRUE iff post has been filtered out by a previous filter
964 + */
254 965 function filtered () {
255 966 return is_null($this->post);
256 - }
967 + } /* SyndicatedPost::filtered() */
257 968
969 + /**
970 + * SyndicatedPost::freshness: check whether post is a new post to be
971 + * inserted, a previously syndicated post that needs to be updated to
972 + * match the latest revision, or a previously syndicated post that is
973 + * still up-to-date.
974 + *
975 + * @return int A status code representing the freshness of the post
976 + * 0 = post already syndicated; no update needed
977 + * 1 = post already syndicated, but needs to be updated to latest
978 + * 2 = post has not yet been syndicated; needs to be created
979 + */
258 980 function freshness () {
259 981 global $wpdb;
260 982
261 983 if ($this->filtered()) : // This should never happen.
@@ -277,9 +999,9 @@
277 999 if (count($stored_update_hashes) > 0) :
278 1000 $stored_update_hash = $stored_update_hashes[0];
279 1001 $update_hash_changed = ($stored_update_hash != $this->update_hash());
280 1002 else :
281 - $update_hash_changed = false;
1003 + $update_hash_changed = true; // Can't find syndication meta-data
282 1004 endif;
283 1005
284 1006 preg_match('/([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/', $result->post_modified_gmt, $backref);
285 1007
@@ -284,13 +1006,30 @@
284 1006 preg_match('/([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/', $result->post_modified_gmt, $backref);
285 1007
286 1008 $last_rev_ts = gmmktime($backref[4], $backref[5], $backref[6], $backref[2], $backref[3], $backref[1]);
287 1009 $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
288 - $updated = ((
1010 +
1011 + $frozen_values = get_post_custom_values('_syndication_freeze_updates', $result->id);
1012 + $frozen_post = (count($frozen_values) > 0 and 'yes' == $frozen_values[0]);
1013 + $frozen_feed = ('yes' == $this->link->setting('freeze updates', 'freeze_updates', NULL));
1014 +
1015 + // Check timestamps...
1016 + $updated = (
289 1017 !is_null($updated_ts)
290 1018 and ($updated_ts > $last_rev_ts)
291 - ) or $update_hash_changed);
292 -
1019 + );
1020 +
1021 +
1022 + // Or the hash...
1023 + $updated = ($updated or $update_hash_changed);
1024 +
1025 + // But only if the post is not frozen.
1026 + $updated = (
1027 + $updated
1028 + and !$frozen_post
1029 + and !$frozen_feed
1030 + );
1031 +
293 1032 if ($updated) :
294 1033 $this->_freshness = 1; // Updated content
295 1034 $this->_wp_id = $result->id;
296 1035 else :
@@ -301,8 +1040,12 @@
301 1040 endif;
302 1041 return $this->_freshness;
303 1042 }
304 1043
1044 + #################################################
1045 + #### INTERNAL STORAGE AND MANAGEMENT METHODS ####
1046 + #################################################
1047 +
305 1048 function wp_id () {
306 1049 if ($this->filtered()) : // This should never happen.
307 1050 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
308 1051 endif;
@@ -364,30 +1107,55 @@
364 1107
365 1108 if (!$this->filtered() and $freshness > 0) :
366 1109 unset($this->post['named']);
367 1110 $this->post = apply_filters('syndicated_post', $this->post, $this);
1111 +
1112 + // Allow for feed-specific syndicated_post filters.
1113 + $this->post = apply_filters(
1114 + "syndicated_post_".$this->link->uri(),
1115 + $this->post,
1116 + $this
1117 + );
368 1118 endif;
369 1119
1120 + // Hook in early to make sure these get inserted if at all possible
1121 + add_action(
1122 + /*hook=*/ 'transition_post_status',
1123 + /*callback=*/ array(&$this, 'add_rss_meta'),
1124 + /*priority=*/ -10000, /* very early */
1125 + /*arguments=*/ 3
1126 + );
1127 +
370 1128 if (!$this->filtered() and $freshness == 2) :
371 1129 // The item has not yet been added. So let's add it.
1130 + FeedWordPress::diagnostic('syndicated_posts', 'Inserting new post "'.$this->post['post_title'].'"');
1131 +
372 1132 $this->insert_new();
373 - $this->add_rss_meta();
374 - do_action('post_syndicated_item', $this->wp_id());
1133 + do_action('post_syndicated_item', $this->wp_id(), $this);
375 1134
376 1135 $ret = 'new';
377 1136 elseif (!$this->filtered() and $freshness == 1) :
1137 + FeedWordPress::diagnostic('syndicated_posts', 'Updating existing post # '.$this->wp_id().', "'.$this->post['post_title'].'"');
1138 +
378 1139 $this->post['ID'] = $this->wp_id();
379 1140 $this->update_existing();
380 - $this->add_rss_meta();
381 - do_action('update_syndicated_item', $this->wp_id());
1141 + do_action('update_syndicated_item', $this->wp_id(), $this);
382 1142
383 1143 $ret = 'updated';
384 1144 else :
385 1145 $ret = false;
386 1146 endif;
387 -
1147 +
1148 + // Remove add_rss_meta hook
1149 + remove_action(
1150 + /*hook=*/ 'transition_post_status',
1151 + /*callback=*/ array(&$this, 'add_rss_meta'),
1152 + /*priority=*/ -10000, /* very early */
1153 + /*arguments=*/ 3
1154 + );
1155 +
388 1156 return $ret;
389 - } // function SyndicatedPost::store ()
1157 + } /* function SyndicatedPost::store () */
390 1158
391 1159 function insert_new () {
392 1160 global $wpdb, $wp_db_version;
393 1161
@@ -668,12 +1436,14 @@
668 1436 // for by standard WP meta-data (i.e., any interesting data about the
669 1437 // syndicated post other than author, title, timestamp, categories, and
670 1438 // guid). It's also used to hook into WordPress's support for
671 1439 // enclosures.
672 - function add_rss_meta () {
1440 + function add_rss_meta ($new_status, $old_status, $post) {
1441 + FeedWordPress::diagnostic('syndicated_posts:meta_data', 'Adding post meta-data: {'.implode(", ", array_keys($this->post['meta'])).'}');
1442 +
673 1443 global $wpdb;
674 1444 if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
675 - $postId = $this->wp_id();
1445 + $postId = $post->ID;
676 1446
677 1447 // Aggregated posts should NOT send out pingbacks.
678 1448 // WordPress 2.1-2.2 claim you can tell them not to
679 1449 // using $post_pingback, but they don't listen, so we
@@ -683,35 +1453,22 @@
683 1453 WHERE post_id='$postId' AND meta_key='_pingme'
684 1454 ");
685 1455
686 1456 foreach ( $this->post['meta'] as $key => $values ) :
1457 + $eKey = $wpdb->escape($key);
687 1458
688 - $key = $wpdb->escape($key);
689 -
690 1459 // If this is an update, clear out the old
691 1460 // values to avoid duplication.
692 1461 $result = $wpdb->query("
693 1462 DELETE FROM $wpdb->postmeta
694 - WHERE post_id='$postId' AND meta_key='$key'
1463 + WHERE post_id='$postId' AND meta_key='$eKey'
695 1464 ");
696 1465
697 1466 // Allow for either a single value or an array
698 1467 if (!is_array($values)) $values = array($values);
699 1468 foreach ( $values as $value ) :
700 - $value = $wpdb->escape($value);
701 - $result = $wpdb->query("
702 - INSERT INTO $wpdb->postmeta
703 - SET
704 - post_id='$postId',
705 - meta_key='$key',
706 - meta_value='$value'
707 - ");
708 - if (!$result) :
709 - $err = mysql_error();
710 - if (FEEDWORDPRESS_DEBUG) :
711 - echo "[DEBUG:".date('Y-m-d H:i:S')."][feedwordpress]: post metadata insertion FAILED for field '$key' := '$value': [$err]";
712 - endif;
713 - endif;
1469 + FeedWordPress::diagnostic('syndicated_posts:meta_data', "Adding post meta-datum to post [$postId]: [$key] = ".FeedWordPress::val($value, /*no newlines=*/ true));
1470 + add_post_meta($postId, $key, $value, /*unique=*/ false);
714 1471 endforeach;
715 1472 endforeach;
716 1473 endif;
717 1474 } /* SyndicatedPost::add_rss_meta () */
@@ -722,10 +1479,10 @@
722 1479 global $wpdb;
723 1480
724 1481 $a = $this->author();
725 1482 $author = $a['name'];
726 - $email = $a['email'];
727 - $url = $a['uri'];
1483 + $email = (isset($a['email']) ? $a['email'] : NULL);
1484 + $url = (isset($a['uri']) ? $a['uri'] : NULL);
728 1485
729 1486 $match_author_by_email = !('yes' == get_option("feedwordpress_do_not_match_author_by_email"));
730 1487 if ($match_author_by_email and !FeedWordPress::is_null_email($email)) :
731 1488 $test_email = $email;
@@ -763,65 +1520,36 @@
763 1520
764 1521 else :
765 1522 // Check the database for an existing author record that might fit
766 1523
767 - #-- WordPress 2.0+
768 - if (fwp_test_wp_version(FWP_SCHEMA_HAS_USERMETA)) :
769 -
770 - // First try the user core data table.
1524 + // First try the user core data table.
1525 + $id = $wpdb->get_var(
1526 + "SELECT ID FROM $wpdb->users
1527 + WHERE
1528 + TRIM(LCASE(user_login)) = TRIM(LCASE('$login'))
1529 + OR (
1530 + LENGTH(TRIM(LCASE(user_email))) > 0
1531 + AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
1532 + )
1533 + OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author'))
1534 + ");
1535 +
1536 + // If that fails, look for aliases in the user meta data table
1537 + if (is_null($id)) :
771 1538 $id = $wpdb->get_var(
772 - "SELECT ID FROM $wpdb->users
1539 + "SELECT user_id FROM $wpdb->usermeta
773 1540 WHERE
774 - TRIM(LCASE(user_login)) = TRIM(LCASE('$login'))
1541 + (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
775 1542 OR (
776 - LENGTH(TRIM(LCASE(user_email))) > 0
777 - AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
778 - )
779 - OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author'))
780 - ");
781 -
782 - // If that fails, look for aliases in the user meta data table
783 - if (is_null($id)) :
784 - $id = $wpdb->get_var(
785 - "SELECT user_id FROM $wpdb->usermeta
786 - WHERE
787 - (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
788 - OR (
789 - meta_key = 'description'
790 - AND TRIM(LCASE(meta_value))
791 - RLIKE CONCAT(
792 - '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
793 - TRIM(LCASE('$reg_author')),
794 - '( |\\t|\\r)*(\\n|\$)'
795 - )
796 - )
797 - ");
798 - endif;
799 -
800 - #-- WordPress 1.5.x
801 - else :
802 - $id = $wpdb->get_var(
803 - "SELECT ID from $wpdb->users
804 - WHERE
805 - TRIM(LCASE(user_login)) = TRIM(LCASE('$login')) OR
806 - (
807 - LENGTH(TRIM(LCASE(user_email))) > 0
808 - AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
809 - ) OR
810 - TRIM(LCASE(user_firstname)) = TRIM(LCASE('$author')) OR
811 - TRIM(LCASE(user_nickname)) = TRIM(LCASE('$author')) OR
812 - TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author')) OR
813 - TRIM(LCASE(user_description)) = TRIM(LCASE('$author')) OR
814 - (
815 - LOWER(user_description)
1543 + meta_key = 'description'
1544 + AND TRIM(LCASE(meta_value))
816 1545 RLIKE CONCAT(
817 1546 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
818 - LCASE('$reg_author'),
1547 + TRIM(LCASE('$reg_author')),
819 1548 '( |\\t|\\r)*(\\n|\$)'
820 1549 )
821 1550 )
822 1551 ");
823 -
824 1552 endif;
825 1553
826 1554 // ... if you don't find one, then do what you need to do
827 1555 if (is_null($id)) :
@@ -827,8 +1555,20 @@
827 1555 if (is_null($id)) :
828 1556 if ($unfamiliar_author === 'create') :
829 1557 $userdata = array();
830 1558
1559 + // WordPress 3 is going to pitch a fit if we attempt to register
1560 + // more than one user account with an empty e-mail address, so we
1561 + // need *something* here. Ugh.
1562 + if (strlen($email) == 0 or FeedWordPress::is_null_email($email)) :
1563 + $hostUrl = $this->link->homepage();
1564 + if (is_null($hostUrl) or (strlen($hostUrl) < 0)) :
1565 + $hostUrl = $this->link->uri();
1566 + endif;
1567 + $url = parse_url($hostUrl);
1568 + $email = $nice_author.'@'.$url['host'];
1569 + endif;
1570 +
831 1571 #-- user table data
832 1572 $userdata['ID'] = NULL; // new user
833 1573 $userdata['user_login'] = $login;
834 1574 $userdata['user_nicename'] = $nice_author;
@@ -835,9 +1575,9 @@
835 1575 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
836 1576 $userdata['user_email'] = $email;
837 1577 $userdata['user_url'] = $url;
838 1578 $userdata['display_name'] = $author;
839 -
1579 +
840 1580 $id = wp_insert_user($userdata);
841 1581 elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
842 1582 $id = (int) $unfamiliar_author;
843 1583 elseif ($unfamiliar_author === 'default') :
@@ -968,311 +1708,6 @@
968 1708 endswitch;
969 1709 return $ret;
970 1710 } // function SyndicatedPost::use_api ()
971 1711
972 - #### EXTRACT DATA FROM FEED ITEM ####
973 -
974 - function created () {
975 - $epoch = null;
976 - if (isset($this->item['dc']['created'])) :
977 - $epoch = @parse_w3cdtf($this->item['dc']['created']);
978 - elseif (isset($this->item['dcterms']['created'])) :
979 - $epoch = @parse_w3cdtf($this->item['dcterms']['created']);
980 - elseif (isset($this->item['created'])): // Atom 0.3
981 - $epoch = @parse_w3cdtf($this->item['created']);
982 - endif;
983 - return $epoch;
984 - }
985 - function published ($fallback = true) {
986 - $epoch = null;
987 -
988 - # RSS is a fucking mess. Figure out whether we have a date in
989 - # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
990 - # epoch format for reformatting. If we can't find anything,
991 - # we'll use the last-updated time.
992 - if (isset($this->item['dc']['date'])): // Dublin Core
993 - $epoch = @parse_w3cdtf($this->item['dc']['date']);
994 - elseif (isset($this->item['dcterms']['issued'])) : // Dublin Core extensions
995 - $epoch = @parse_w3cdtf($this->item['dcterms']['issued']);
996 - elseif (isset($this->item['published'])) : // Atom 1.0
997 - $epoch = @parse_w3cdtf($this->item['published']);
998 - elseif (isset($this->item['issued'])): // Atom 0.3
999 - $epoch = @parse_w3cdtf($this->item['issued']);
1000 - elseif (isset($this->item['pubdate'])): // RSS 2.0
1001 - $epoch = strtotime($this->item['pubdate']);
1002 - elseif ($fallback) : // Fall back to <updated> / <modified> if present
1003 - $epoch = $this->updated(/*fallback=*/ false);
1004 - endif;
1005 -
1006 - # If everything failed, then default to the current time.
1007 - if (is_null($epoch)) :
1008 - if (-1 == $default) :
1009 - $epoch = time();
1010 - else :
1011 - $epoch = $default;
1012 - endif;
1013 - endif;
1014 -
1015 - return $epoch;
1016 - }
1017 - function updated ($fallback = true, $default = -1) {
1018 - $epoch = null;
1019 -
1020 - # As far as I know, only dcterms and Atom have reliable ways to
1021 - # specify when something was *modified* last. If neither is
1022 - # available, then we'll try to get the time of publication.
1023 - if (isset($this->item['dc']['modified'])) : // Not really correct
1024 - $epoch = @parse_w3cdtf($this->item['dc']['modified']);
1025 - elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
1026 - $epoch = @parse_w3cdtf($this->item['dcterms']['modified']);
1027 - elseif (isset($this->item['modified'])): // Atom 0.3
1028 - $epoch = @parse_w3cdtf($this->item['modified']);
1029 - elseif (isset($this->item['updated'])): // Atom 1.0
1030 - $epoch = @parse_w3cdtf($this->item['updated']);
1031 - elseif ($fallback) : // Fall back to issued / dc:date
1032 - $epoch = $this->published(/*fallback=*/ false, /*default=*/ $default);
1033 - endif;
1034 -
1035 - # If everything failed, then default to the current time.
1036 - if (is_null($epoch)) :
1037 - if (-1 == $default) :
1038 - $epoch = time();
1039 - else :
1040 - $epoch = $default;
1041 - endif;
1042 - endif;
1043 -
1044 - return $epoch;
1045 - }
1046 -
1047 - function update_hash () {
1048 - return md5(serialize($this->item));
1049 - }
1050 -
1051 - function guid () {
1052 - $guid = null;
1053 - if (isset($this->item['id'])): // Atom 0.3 / 1.0
1054 - $guid = $this->item['id'];
1055 - elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
1056 - $guid = $this->item['atom']['id'];
1057 - elseif (isset($this->item['guid'])) : // RSS 2.0
1058 - $guid = $this->item['guid'];
1059 - elseif (isset($this->item['dc']['identifier'])) :// yeah, right
1060 - $guid = $this->item['dc']['identifier'];
1061 - else :
1062 - // The feed does not seem to have provided us with a
1063 - // unique identifier, so we'll have to cobble together
1064 - // a tag: URI that might work for us. The base of the
1065 - // URI will be the host name of the feed source ...
1066 - $bits = parse_url($this->feedmeta['link/uri']);
1067 - $guid = 'tag:'.$bits['host'];
1068 -
1069 - // If we have a date of creation, then we can use that
1070 - // to uniquely identify the item. (On the other hand, if
1071 - // the feed producer was consicentious enough to
1072 - // generate dates of creation, she probably also was
1073 - // conscientious enough to generate unique identifiers.)
1074 - if (!is_null($this->created())) :
1075 - $guid .= '://post.'.date('YmdHis', $this->created());
1076 -
1077 - // Otherwise, use both the URI of the item, *and* the
1078 - // item's title. We have to use both because titles are
1079 - // often not unique, and sometimes links aren't unique
1080 - // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
1081 - // some podcasts). But it's rare to have *both* the same
1082 - // title *and* the same link for two different items. So
1083 - // this is about the best we can do.
1084 - else :
1085 - $guid .= '://'.md5($this->item['link'].'/'.$this->item['title']);
1086 - endif;
1087 - endif;
1088 - return $guid;
1089 - }
1090 -
1091 - function author () {
1092 - $author = array ();
1093 -
1094 - if (isset($this->item['author_name'])):
1095 - $author['name'] = $this->item['author_name'];
1096 - elseif (isset($this->item['dc']['creator'])):
1097 - $author['name'] = $this->item['dc']['creator'];
1098 - elseif (isset($this->item['dc']['contributor'])):
1099 - $author['name'] = $this->item['dc']['contributor'];
1100 - elseif (isset($this->feed->channel['dc']['creator'])) :
1101 - $author['name'] = $this->feed->channel['dc']['creator'];
1102 - elseif (isset($this->feed->channel['dc']['contributor'])) :
1103 - $author['name'] = $this->feed->channel['dc']['contributor'];
1104 - elseif (isset($this->feed->channel['author_name'])) :
1105 - $author['name'] = $this->feed->channel['author_name'];
1106 - elseif ($this->feed->is_rss() and isset($this->item['author'])) :
1107 - // The author element in RSS is allegedly an
1108 - // e-mail address, but lots of people don't use
1109 - // it that way. So let's make of it what we can.
1110 - $author = parse_email_with_realname($this->item['author']);
1111 -
1112 - if (!isset($author['name'])) :
1113 - if (isset($author['email'])) :
1114 - $author['name'] = $author['email'];
1115 - else :
1116 - $author['name'] = $this->feed->channel['title'];
1117 - endif;
1118 - endif;
1119 - else :
1120 - $author['name'] = $this->feed->channel['title'];
1121 - endif;
1122 -
1123 - if (isset($this->item['author_email'])):
1124 - $author['email'] = $this->item['author_email'];
1125 - elseif (isset($this->feed->channel['author_email'])) :
1126 - $author['email'] = $this->feed->channel['author_email'];
1127 - endif;
1128 -
1129 - if (isset($this->item['author_url'])):
1130 - $author['uri'] = $this->item['author_url'];
1131 - elseif (isset($this->feed->channel['author_url'])) :
1132 - $author['uri'] = $this->item['author_url'];
1133 - else:
1134 - $author['uri'] = $this->feed->channel['link'];
1135 - endif;
1136 -
1137 - return $author;
1138 - } // SyndicatedPost::author()
1139 -
1140 - /**
1141 - * SyndicatedPost::isTaggedAs: Test whether a feed item is
1142 - * tagged / categorized with a given string. Case and leading and
1143 - * trailing whitespace are ignored.
1144 - *
1145 - * @param string $tag Tag to check for
1146 - *
1147 - * @return bool Whether or not at least one of the categories / tags on
1148 - * $this->item is set to $tag (modulo case and leading and trailing
1149 - * whitespace)
1150 - */
1151 - function isTaggedAs ($tag) {
1152 - $desiredTag = strtolower(trim($tag)); // Normalize case and whitespace
1153 -
1154 - // Check to see if this is tagged with $tag
1155 - $currentCategory = 'category';
1156 - $currentCategoryNumber = 1;
1157 -
1158 - // If we have the new MagpieRSS, the number of category elements
1159 - // on this item is stored under index "category#".
1160 - if (isset($this->item['category#'])) :
1161 - $numberOfCategories = (int) $this->item['category#'];
1162 -
1163 - // We REALLY shouldn't have the old and busted MagpieRSS, but in
1164 - // case we do, it doesn't support multiple categories, but there
1165 - // might still be a single value under the "category" index.
1166 - elseif (isset($this->item['category'])) :
1167 - $numberOfCategories = 1;
1168 -
1169 - // No standard category or tag elements on this feed item.
1170 - else :
1171 - $numberOfCategories = 0;
1172 -
1173 - endif;
1174 -
1175 - $isSoTagged = false; // Innocent until proven guilty
1176 -
1177 - // Loop through category elements; if there are multiple
1178 - // elements, they are indexed as category, category#2,
1179 - // category#3, ... category#N
1180 - while ($currentCategoryNumber <= $numberOfCategories) :
1181 - if ($desiredTag == strtolower(trim($this->item[$currentCategory]))) :
1182 - $isSoTagged = true; // Got it!
1183 - break;
1184 - endif;
1185 -
1186 - $currentCategoryNumber += 1;
1187 - $currentCategory = 'category#'.$currentCategoryNumber;
1188 - endwhile;
1189 -
1190 - return $isSoTagged;
1191 - } /* SyndicatedPost::isTaggedAs() */
1192 -
1193 - var $uri_attrs = array (
1194 - array('a', 'href'),
1195 - array('applet', 'codebase'),
1196 - array('area', 'href'),
1197 - array('blockquote', 'cite'),
1198 - array('body', 'background'),
1199 - array('del', 'cite'),
1200 - array('form', 'action'),
1201 - array('frame', 'longdesc'),
1202 - array('frame', 'src'),
1203 - array('iframe', 'longdesc'),
1204 - array('iframe', 'src'),
1205 - array('head', 'profile'),
1206 - array('img', 'longdesc'),
1207 - array('img', 'src'),
1208 - array('img', 'usemap'),
1209 - array('input', 'src'),
1210 - array('input', 'usemap'),
1211 - array('ins', 'cite'),
1212 - array('link', 'href'),
1213 - array('object', 'classid'),
1214 - array('object', 'codebase'),
1215 - array('object', 'data'),
1216 - array('object', 'usemap'),
1217 - array('q', 'cite'),
1218 - array('script', 'src')
1219 - ); /* var SyndicatedPost::$uri_attrs */
1220 -
1221 - var $_base = null;
1222 -
1223 - function resolve_single_relative_uri ($refs) {
1224 - $tag = FeedWordPressHTML::attributeMatch($refs);
1225 - $url = Relative_URI::resolve($tag['value'], $this->_base);
1226 - return $tag['prefix'] . $url . $tag['suffix'];
1227 - } /* function SyndicatedPost::resolve_single_relative_uri() */
1228 -
1229 - function resolve_relative_uris ($content, $obj) {
1230 - # The MagpieRSS upgrade has some `xml:base` support baked in.
1231 - # However, sometimes people do silly things, like putting
1232 - # relative URIs out on a production RSS 2.0 feed or other feeds
1233 - # with no good support for `xml:base`. So we'll do our best to
1234 - # try to catch any remaining relative URIs and resolve them as
1235 - # best we can.
1236 - $obj->_base = $obj->item['link']; // Reset the base for resolving relative URIs
1237 -
1238 - foreach ($obj->uri_attrs as $pair) :
1239 - list($tag, $attr) = $pair;
1240 - $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1241 - $content = preg_replace_callback (
1242 - $pattern,
1243 - array(&$obj, 'resolve_single_relative_uri'),
1244 - $content
1245 - );
1246 - endforeach;
1247 -
1248 - return $content;
1249 - } /* function SyndicatedPost::resolve_relative_uris () */
1250 -
1251 - var $strip_attrs = array (
1252 - array('[a-z]+', 'target'),
1253 -// array('[a-z]+', 'style'),
1254 -// array('[a-z]+', 'on[a-z]+'),
1255 - );
1256 -
1257 - function strip_attribute_from_tag ($refs) {
1258 - $tag = FeedWordPressHTML::attributeMatch($refs);
1259 - return $tag['before_attribute'].$tag['after_attribute'];
1260 - }
1261 -
1262 - function sanitize_content ($content, $obj) {
1263 - # This kind of sucks. I intend to replace it with
1264 - # lib_filter sometime soon.
1265 - foreach ($obj->strip_attrs as $pair):
1266 - list($tag,$attr) = $pair;
1267 - $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1268 -
1269 - $content = preg_replace_callback (
1270 - $pattern,
1271 - array(&$obj, 'strip_attribute_from_tag'),
1272 - $content
1273 - );
1274 - endforeach;
1275 - return $content;
1276 - }
1277 -} // class SyndicatedPost
1712 +} /* class SyndicatedPost */
1278 1713