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