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