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