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