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 +2140 -982 2009.11122022.0203 View file →
@@ -1,103 +1,150 @@
1 1 <?php
2 +require_once(dirname(__FILE__).'/feedtime.class.php');
3 +require_once(dirname(__FILE__).'/syndicatedpostterm.class.php');
4 +require_once(dirname(__FILE__).'/syndicatedpostxpathquery.class.php');
5 +
6 +/**
7 + * class SyndicatedPost: FeedWordPress uses to manage the conversion of
8 + * incoming items from the feed parser into posts for the WordPress
9 + * database. It contains several internal management methods primarily
10 + * of interest to someone working on the FeedWordPress source, as well
11 + * as some utility methods for extracting useful data from many
12 + * different feed formats, which may be useful to FeedWordPress users
13 + * who make use of feed data in PHP add-ons and filters.
14 + *
15 + * @version 2017.1018
16 + */
2 17 class SyndicatedPost {
3 - var $item = null;
4 -
18 + var $item = null; // MagpieRSS representation
19 + var $entry = null; // SimplePie_Item representation
20 +
5 21 var $link = null;
6 22 var $feed = null;
7 23 var $feedmeta = null;
8 -
24 +
25 + var $xmlns = array ();
26 +
9 27 var $post = array ();
10 28
29 + var $named = array ();
30 + var $preset_terms = array ();
31 + var $feed_terms = array ();
32 +
11 33 var $_freshness = null;
12 34 var $_wp_id = null;
35 + var $_wp_post = null;
13 36
14 - function SyndicatedPost ($item, $link) {
37 + /**
38 + * SyndicatedPost constructor: Given a feed item and the source from
39 + * which it was taken, prepare a post that can be inserted into the
40 + * WordPress database on request, or updated in place if it has already
41 + * been syndicated.
42 + *
43 + * @param array $item The item syndicated from the feed.
44 + * @param SyndicatedLink $source The feed it was syndicated from.
45 + */
46 + public function __construct ($item, $source) {
15 47 global $wpdb;
16 48
17 - $this->link = $link;
18 - $feedmeta = $link->settings;
19 - $feed = $link->magpie;
49 + if ( empty($item) and empty($source) )
50 + return;
20 51
21 - # This is ugly as all hell. I'd like to use apply_filters()'s
22 - # alleged support for a variable argument count, but this seems
23 - # to have been broken in WordPress 1.5. It'll be fixed somehow
24 - # in WP 1.5.1, but I'm aiming at WP 1.5 compatibility across
25 - # the board here.
26 - #
27 - # Cf.: <http://mosquito.wordpress.org/view.php?id=901>
28 - global $fwp_channel, $fwp_feedmeta;
29 - $fwp_channel = $feed; $fwp_feedmeta = $feedmeta;
52 + if (is_array($item)
53 + and isset($item['simplepie'])
54 + and isset($item['magpie'])) :
55 + $this->entry = $item['simplepie'];
56 + $this->item = $item['magpie'];
57 + $item = $item['magpie'];
58 + elseif (is_a($item, 'SimplePie_Item')) :
59 + $this->entry = $item;
30 60
31 - $this->feed = $feed;
32 - $this->feedmeta = $feedmeta;
61 + // convert to Magpie for compat purposes
62 + $mp = new MagpieFromSimplePie($source->simplepie, $this->entry);
63 + $this->item = $mp->get_item();
33 64
34 - $this->item = $item;
35 - $this->item = apply_filters('syndicated_item', $this->item, $this);
65 + // done with conversion object
66 + $mp = NULL; unset($mp);
67 + else :
68 + $this->item = $item;
69 + endif;
36 70
71 + $this->link = $source;
72 + $this->feed = $source->magpie;
73 + $this->feedmeta = $source->settings;
74 +
75 + FeedWordPress::diagnostic('feed_items', 'Considering item ['.$this->guid().'] "'.$this->entry->get_title().'"');
76 +
77 + # Dealing with namespaces can get so fucking fucked.
78 + $this->xmlns['forward'] = $source->magpie->_XMLNS_FAMILIAR;
79 + $this->xmlns['reverse'] = array();
80 + foreach ($this->xmlns['forward'] as $url => $ns) :
81 + if (!isset($this->xmlns['reverse'][$ns])) :
82 + $this->xmlns['reverse'][$ns] = array();
83 + endif;
84 + $this->xmlns['reverse'][$ns][] = $url;
85 + endforeach;
86 +
87 + // Fucking SimplePie.
88 + $this->xmlns['reverse']['rss'][] = '';
89 +
90 + // Trigger global syndicated_item filter.
91 + $changed = apply_filters('syndicated_item', $this->item, $this);
92 + $this->item = $changed;
93 +
94 + // Allow for feed-specific syndicated_item filters.
95 + $changed = apply_filters(
96 + "syndicated_item_".$source->uri(),
97 + $this->item,
98 + $this
99 + );
100 + $this->item = $changed;
101 +
37 102 # Filters can halt further processing by returning NULL
38 103 if (is_null($this->item)) :
39 104 $this->post = NULL;
40 105 else :
41 - # Note that nothing is run through $wpdb->escape() here.
106 +
107 + # Note that nothing is run through esc_sql() here.
42 108 # That's deliberate. The escaping is done at the point
43 109 # of insertion, not here, to avoid double-escaping and
44 110 # to avoid screwing with syndicated_post filters
45 111
46 - $this->post['post_title'] = apply_filters('syndicated_item_title', $this->item['title'], $this);
112 + $this->post['post_title'] = apply_filters(
113 + 'syndicated_item_title',
114 + $this->entry->get_title(), $this
115 + );
47 116
48 - // This just gives us an alphanumeric representation of
49 - // the author. We will look up (or create) the numeric
50 - // ID for the author in SyndicatedPost::add()
51 - $this->post['named']['author'] = apply_filters('syndicated_item_author', $this->author(), $this);
52 117
53 - # Identify content and sanitize it.
54 - # ---------------------------------
55 - if (isset($this->item['atom_content'])) :
56 - $content = $this->item['atom_content'];
57 - elseif (isset($this->item['xhtml']['body'])) :
58 - $content = $this->item['xhtml']['body'];
59 - elseif (isset($this->item['xhtml']['div'])) :
60 - $content = $this->item['xhtml']['div'];
61 - elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
62 - $content = $this->item['content']['encoded'];
63 - else:
64 - $content = $this->item['description'];
65 - endif;
66 - $this->post['post_content'] = apply_filters('syndicated_item_content', $content, $this);
118 + $this->named['author'] = apply_filters(
119 + 'syndicated_item_author',
120 + $this->author(), $this
121 + );
122 + // This just gives us an alphanumeric name for the author.
123 + // We look up (or create) the numeric ID for the author
124 + // in SyndicatedPost::add().
67 125
68 - # Identify and sanitize excerpt
69 - $excerpt = NULL;
70 - if ( isset($this->item['description']) and $this->item['description'] ) :
71 - $excerpt = $this->item['description'];
72 - elseif ( isset($content) and $content ) :
73 - $excerpt = strip_tags($content);
74 - if (strlen($excerpt) > 255) :
75 - $excerpt = substr($excerpt,0,252).'...';
76 - endif;
77 - endif;
78 - $excerpt = apply_filters('syndicated_item_excerpt', $excerpt, $this);
126 + $this->post['post_content'] = apply_filters(
127 + 'syndicated_item_content',
128 + $this->content(), $this
129 + );
130 +
131 + $excerpt = apply_filters('syndicated_item_excerpt', $this->excerpt(), $this);
79 132
80 - if (!is_null($excerpt)):
133 + if (!empty($excerpt)):
81 134 $this->post['post_excerpt'] = $excerpt;
82 135 endif;
83 -
84 - // This is unnecessary if we use wp_insert_post
85 - if (!$this->use_api('wp_insert_post')) :
86 - $this->post['post_name'] = sanitize_title($this->post['post_title']);
87 - endif;
88 136
89 - $this->post['epoch']['issued'] = apply_filters('syndicated_item_published', $this->published(), $this);
90 - $this->post['epoch']['created'] = apply_filters('syndicated_item_created', $this->created(), $this);
91 - $this->post['epoch']['modified'] = apply_filters('syndicated_item_updated', $this->updated(), $this);
92 -
93 137 // Dealing with timestamps in WordPress is so fucking fucked.
94 138 $offset = (int) get_option('gmt_offset') * 60 * 60;
95 - $this->post['post_date'] = gmdate('Y-m-d H:i:s', $this->published() + $offset);
96 - $this->post['post_modified'] = gmdate('Y-m-d H:i:s', $this->updated() + $offset);
97 - $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $this->published());
98 - $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $this->updated());
139 + $post_date_gmt = $this->published(array('default' => -1));
140 + $post_modified_gmt = $this->updated(array('default' => -1));
99 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 +
100 147 // Use feed-level preferences or the global default.
101 148 $this->post['post_status'] = $this->link->syndicated_status('post', 'publish');
102 149 $this->post['comment_status'] = $this->link->syndicated_status('comment', 'closed');
103 150 $this->post['ping_status'] = $this->link->syndicated_status('ping', 'closed');
@@ -104,210 +151,1315 @@
104 151
105 152 // Unique ID (hopefully a unique tag: URI); failing that, the permalink
106 153 $this->post['guid'] = apply_filters('syndicated_item_guid', $this->guid(), $this);
107 154
108 - // User-supplied custom settings to apply to each post. Do first so that FWP-generated custom settings will overwrite if necessary; thus preventing any munging
109 - $default_custom_settings = get_option('feedwordpress_custom_settings');
110 - if ($default_custom_settings and !is_array($default_custom_settings)) :
111 - $default_custom_settings = unserialize($default_custom_settings);
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));
159 + $postMetaOut = array();
160 +
161 + foreach ($postMetaIn as $key => $meta) :
162 + $postMetaOut[$key] = $meta->do_substitutions($this);
163 + endforeach;
164 +
165 + foreach ($postMetaOut as $key => $values) :
166 + if (is_null($values)) { // have chosen to replace value with empty string
167 + $values = [''];
168 + }
169 + $this->post['meta'][$key] = array();
170 + foreach ($values as $value) :
171 + $this->post['meta'][$key][] = apply_filters("syndicated_post_meta_{$key}", $value, $this);
172 + endforeach;
173 + endforeach;
174 +
175 + // RSS 2.0 / Atom 1.0 enclosure support
176 + $enclosures = $this->entry->get_enclosures();
177 + if (is_array($enclosures)) : foreach ($enclosures as $enclosure) :
178 + $this->post['meta']['enclosure'][] =
179 + apply_filters('syndicated_item_enclosure_url', $enclosure->get_link(), $this)."\n".
180 + apply_filters('syndicated_item_enclosure_length', $enclosure->get_length(), $this)."\n".
181 + apply_filters('syndicated_item_enclosure_type', $enclosure->get_type(), $this);
182 + endforeach; endif;
183 +
184 + // In case you want to point back to the blog this was
185 + // syndicated from.
186 +
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 +
203 + // Make use of atom:source data, if present in an aggregated feed
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;
112 220 endif;
113 - if (!is_array($default_custom_settings)) :
114 - $default_custom_settings = array();
221 +
222 + foreach ($sourcemeta as $meta_key => $value) :
223 + if (!is_null($value)) :
224 + $this->post['meta'][$meta_key] = $value;
225 + endif;
226 + endforeach;
227 +
228 + // Store information on human-readable and machine-readable comment URIs
229 +
230 + // Human-readable comment URI
231 + $commentLink = apply_filters('syndicated_item_comments', $this->comment_link(), $this);
232 + if (!is_null($commentLink)) : $this->post['meta']['rss:comments'] = $commentLink; endif;
233 +
234 + // Machine-readable content feed URI
235 + $commentFeed = apply_filters('syndicated_item_commentrss', $this->comment_feed(), $this);
236 + if (!is_null($commentFeed)) : $this->post['meta']['wfw:commentRSS'] = $commentFeed; endif;
237 + // Yeah, yeah, now I know that it's supposed to be
238 + // wfw:commentRss. Oh well. Path dependence, sucka.
239 +
240 + // Store information to identify the feed that this came from
241 + if (isset($this->feedmeta['link/uri'])) :
242 + $this->post['meta']['syndication_feed'] = $this->feedmeta['link/uri'];
115 243 endif;
116 -
117 - $custom_settings = (isset($this->link->settings['postmeta']) ? $this->link->settings['postmeta'] : null);
118 - if ($custom_settings and !is_array($custom_settings)) :
119 - $custom_settings = unserialize($custom_settings);
244 + if (isset($this->feedmeta['link/id'])) :
245 + $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
120 246 endif;
121 - if (!is_array($custom_settings)) :
122 - $custom_settings = array();
247 +
248 + if (isset($this->item['source_link_self'])) :
249 + $this->post['meta']['syndication_feed_original'] = $this->item['source_link_self'];
123 250 endif;
124 - $this->post['meta'] = array_merge($default_custom_settings, $custom_settings);
125 251
126 - // RSS 2.0 / Atom 1.0 enclosure support
127 - if ( isset($this->item['enclosure#']) ) :
128 - for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
129 - $eid = (($i > 1) ? "#{$id}" : "");
130 - $this->post['meta']['enclosure'][] =
131 - apply_filters('syndicated_item_enclosure_url', $this->item["enclosure{$eid}@url"], $this)."\n".
132 - apply_filters('syndicated_item_enclosure_length', $this->item["enclosure{$eid}@length"], $this)."\n".
133 - apply_filters('syndicated_item_enclosure_type', $this->item["enclosure{$eid}@type"], $this);
134 - endfor;
252 + // In case you want to know the external permalink...
253 + $this->post['meta']['syndication_permalink'] = apply_filters('syndicated_item_link', $this->permalink());
254 +
255 + // Store a hash of the post content for checking whether something needs to be updated
256 + $this->post['meta']['syndication_item_hash'] = $this->update_hash();
257 +
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);
262 +
263 + $this->post['post_type'] = apply_filters('syndicated_post_type', $this->link->setting('syndicated post type', 'syndicated_post_type', 'post'), $this);
264 + endif;
265 + } /* SyndicatedPost::__construct() */
266 +
267 + #####################################
268 + #### EXTRACT DATA FROM FEED ITEM ####
269 + #####################################
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 +
288 + /**
289 + * SyndicatedPost::query uses an XPath-like syntax to query arbitrary
290 + * elements within the syndicated item.
291 + *
292 + * @param string $path
293 + * @returns array of string values representing contents of matching
294 + * elements or attributes
295 + */
296 + public function query ($path) {
297 + $xq = new SyndicatedPostXPathQuery(array("path" => $path));
298 +
299 + $feedChannel = array_merge(
300 + $this->get_feed_root_element(),
301 + $this->get_feed_channel_elements()
302 + );
303 +
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 + ));
316 +
317 + return $matches;
318 + } /* SyndicatedPost::query() */
319 +
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);
325 + endforeach;
326 + endforeach;
327 + return $matches;
328 + } /* SyndicatedPost::get_feed_root_element() */
329 +
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 + );
337 +
338 + $matches = array();
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);
135 343 endif;
344 + endforeach;
345 + return $matches;
346 + } /* SyndicatedPost::get_feed_channel_elements() */
136 347
137 - // In case you want to point back to the blog this was syndicated from
138 - if (isset($this->feed->channel['title'])) :
139 - $this->post['meta']['syndication_source'] = apply_filters('syndicated_item_source_title', $this->feed->channel['title'], $this);
140 - endif;
348 + public function get_categories ($params = array()) {
349 + return $this->entry->get_categories();
350 + }
351 +
352 + public function title ($params = array()) {
353 + return $this->entry->get_title();
354 + } /* SyndicatedPost::title () */
355 +
356 + public function content ($params = array()) {
141 357
142 - if (isset($this->feed->channel['link'])) :
143 - $this->post['meta']['syndication_source_uri'] = apply_filters('syndicated_item_source_link', $this->feed->channel['link'], $this);
358 + $params = wp_parse_args($params, array(
359 + "full only" => false,
360 + ));
361 +
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.
373 + if (isset($this->item['atom_content'])) :
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.)
382 + elseif (isset($this->item['xhtml']['body'])) :
383 + $content = $this->item['xhtml']['body'];
384 + elseif (isset($this->item['xhtml']['div'])) :
385 + $content = $this->item['xhtml']['div'];
386 +
387 + // content:encoded, most common method of providing full HTML in
388 + // RSS 2.0 feeds.
389 + elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
390 + $content = $this->item['content']['encoded'];
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'];
144 401 endif;
145 402
146 - // Make use of atom:source data, if present in an aggregated feed
147 - if (isset($this->item['source_title'])) :
148 - $this->post['meta']['syndication_source_original'] = $this->item['source_title'];
403 + endif;
404 +
405 + return $content;
406 + } /* SyndicatedPost::content() */
407 +
408 + public function excerpt () {
409 + # Identify and sanitize excerpt: atom:summary, or rss:description
410 + $excerpt = $this->entry->get_description();
411 +
412 + # Many RSS feeds use rss:description, inadvisably, to
413 + # carry the entire post (typically with escaped HTML).
414 + # If that's what happened, we don't want the full
415 + # content for the excerpt.
416 + $content = $this->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 ) :
422 + # If content is available, generate an excerpt.
423 + if ( strlen(trim($content)) > 0 ) :
424 + $excerpt = strip_tags($content);
425 + if (strlen($excerpt) > 255) :
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).'...';
432 + endif;
149 433 endif;
434 + endif;
150 435
151 - if (isset($this->item['source_link'])) :
152 - $this->post['meta']['syndication_source_uri_original'] = $this->item['source_link'];
436 + return $excerpt;
437 + } /* SyndicatedPost::excerpt() */
438 +
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 () {
446 + // Handles explicit <link> elements and also RSS 2.0 cases with
447 + // <guid isPermaLink="true">, etc. Hooray!
448 + $permalink = $this->entry->get_link();
449 + return $permalink;
450 + } /* SyndicatedPost::permalink () */
451 +
452 + public function created ($params = array()) {
453 + $unfiltered = false; $default = NULL;
454 + extract($params);
455 +
456 + $date = '';
457 + if (isset($this->item['dc']['created'])) :
458 + $date = $this->item['dc']['created'];
459 + elseif (isset($this->item['dcterms']['created'])) :
460 + $date = $this->item['dcterms']['created'];
461 + elseif (isset($this->item['created'])): // Atom 0.3
462 + $date = $this->item['created'];
463 + endif;
464 +
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;
471 + } /* SyndicatedPost::created() */
472 +
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 +
481 + $date = '';
482 + $ts = null;
483 +
484 + # RSS is a fucking mess. Figure out whether we have a date in
485 + # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
486 + # epoch format for reformatting. If we can't find anything,
487 + # we'll use the last-updated time.
488 + if (isset($this->item['dc']['date'])): // Dublin Core
489 + $date = $this->item['dc']['date'];
490 + elseif (isset($this->item['dcterms']['issued'])) : // Dublin Core extensions
491 + $date = $this->item['dcterms']['issued'];
492 + elseif (isset($this->item['published'])) : // Atom 1.0
493 + $date = $this->item['published'];
494 + elseif (isset($this->item['issued'])): // Atom 0.3
495 + $date = $this->item['issued'];
496 + elseif (isset($this->item['pubdate'])): // RSS 2.0
497 + $date = $this->item['pubdate'];
498 + endif;
499 +
500 + if (strlen($date) > 0) :
501 + $time = new FeedTime($date);
502 + $ts = $time->timestamp();
503 + elseif ($fallback) : // Fall back to <updated> / <modified> if present
504 + $ts = $this->updated(/*fallback=*/ false, /*default=*/ $default);
505 + endif;
506 +
507 + # If everything failed, then default to the current time.
508 + if (is_null($ts)) :
509 + if (-1 == $default) :
510 + $ts = time();
511 + else :
512 + $ts = $default;
153 513 endif;
154 -
155 - if (isset($this->item['source_id'])) :
156 - $this->post['meta']['syndication_source_id_original'] = $this->item['source_id'];
514 + endif;
515 +
516 + if (!$unfiltered) :
517 + $ts = apply_filters('syndicated_item_published', $ts, $this);
518 + endif;
519 + return $ts;
520 + } /* SyndicatedPost::published() */
521 +
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 +
530 + $date = '';
531 + $ts = null;
532 +
533 + # As far as I know, only dcterms and Atom have reliable ways to
534 + # specify when something was *modified* last. If neither is
535 + # available, then we'll try to get the time of publication.
536 + if (isset($this->item['dc']['modified'])) : // Not really correct
537 + $date = $this->item['dc']['modified'];
538 + elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
539 + $date = $this->item['dcterms']['modified'];
540 + elseif (isset($this->item['modified'])): // Atom 0.3
541 + $date = $this->item['modified'];
542 + elseif (isset($this->item['updated'])): // Atom 1.0
543 + $date = $this->item['updated'];
544 + endif;
545 +
546 + if (strlen($date) > 0) :
547 + $time = new FeedTime($date);
548 + $ts = $time->timestamp();
549 + elseif ($fallback) : // Fall back to issued / dc:date
550 + $ts = $this->published(/*fallback=*/ false, /*default=*/ $default);
551 + endif;
552 +
553 + # If everything failed, then default to the current time.
554 + if (is_null($ts)) :
555 + if (-1 == $default) :
556 + $ts = time();
557 + else :
558 + $ts = $default;
157 559 endif;
560 + endif;
158 561
159 - // Store information on human-readable and machine-readable comment URIs
160 - if (isset($this->item['comments'])) :
161 - $this->post['meta']['rss:comments'] = apply_filters('syndicated_item_comments', $this->item['comments']);
562 + if (!$unfiltered) :
563 + $ts = apply_filters('syndicated_item_updated', $ts, $this);
564 + endif;
565 + return $ts;
566 + } /* SyndicatedPost::updated() */
567 +
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();
162 580 endif;
163 -
164 - // RSS 2.0 comment feeds extension
165 - if (isset($this->item['wfw']['commentrss'])) :
166 - $this->post['meta']['wfw:commentRSS'] = apply_filters('syndicated_item_commentrss', $this->item['wfw']['commentrss']);
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;
599 + } /* SyndicatedPost::update_hash() */
600 +
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 () {
649 + $guid = null;
650 + if (isset($this->item['id'])): // Atom 0.3 / 1.0
651 + $guid = $this->item['id'];
652 + elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
653 + $guid = $this->item['atom']['id'];
654 + elseif (isset($this->item['guid'])) : // RSS 2.0
655 + $guid = $this->item['guid'];
656 + elseif (isset($this->item['dc']['identifier'])) : // yeah, right
657 + $guid = $this->item['dc']['identifier'];
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 +
665 + // The feed does not seem to have provided us with a
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());
670 + $guid = 'tag:'.$bits['host'];
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 +
683 + // If we have a date of creation, then we can use that
684 + // to uniquely identify the item. (On the other hand, if
685 + // the feed producer was consicentious enough to
686 + // generate dates of creation, she probably also was
687 + // conscientious enough to generate unique identifiers.)
688 + elseif (!is_null($this->created())) :
689 + $guid .= '://post.'.date('YmdHis', $this->created());
690 +
691 + // Otherwise, use both the URI of the item, *and* the
692 + // item's title. We have to use both because titles are
693 + // often not unique, and sometimes links aren't unique
694 + // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
695 + // some podcasts). But it's rare to have *both* the same
696 + // title *and* the same link for two different items. So
697 + // this is about the best we can do.
698 + else :
699 + $link = $this->permalink();
700 + if (is_null($link)) : $link = $this->link->uri(); endif;
701 + $guid .= '://'.md5($link.'/'.$this->title());
167 702 endif;
703 + endif;
704 + return $guid;
705 + } /* SyndicatedPost::guid() */
168 706
169 - // Atom 1.0 comment feeds link-rel
170 - if (isset($this->item['link_replies'])) :
171 - // There may be multiple <link rel="replies"> elements; feeds have a feed MIME type
172 - $N = isset($this->item['link_replies#']) ? $this->item['link_replies#'] : 1;
173 - for ($i = 1; $i <= $N; $i++) :
174 - $currentElement = 'link_replies'.(($i > 1) ? '#'.$i : '');
175 - if (isset($this->item[$currentElement.'@type'])
176 - and preg_match("\007application/(atom|rss|rdf)\+xml\007i", $this->item[$currentElement.'@type'])) :
177 - $this->post['meta']['wfw:commentRSS'] = apply_filters('syndicated_item_commentrss', $this->item[$currentElement]);
178 - endif;
179 - endfor;
180 - endif;
707 + public function author () {
708 + $author = array ();
181 709
182 - // Store information to identify the feed that this came from
183 - $this->post['meta']['syndication_feed'] = $this->feedmeta['link/uri'];
184 - $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
710 + $aa = $this->entry->get_authors();
711 + if (is_countable($aa) and count($aa) > 0) :
712 + $a = reset($aa);
185 713
186 - if (isset($this->item['source_link_self'])) :
187 - $this->post['meta']['syndication_feed_original'] = $this->item['source_link_self'];
188 - endif;
714 + $author = array(
715 + 'name' => $a->get_name(),
716 + 'email' => $a->get_email(),
717 + 'uri' => $a->get_link(),
718 + );
719 + endif;
189 720
190 - // In case you want to know the external permalink...
191 - if (isset($this->item['link'])) :
192 - $permalink = $this->item['link'];
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']);
193 740
194 - // No <link> element. See if this feed has <guid isPermalink="true"> ....
195 - elseif (isset($this->item['guid'])) :
196 - if (isset($this->item['guid@ispermalink']) and strtolower(trim($this->item['guid@ispermalink'])) != 'false') :
197 - $permalink = $this->item['guid'];
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;
198 747 endif;
199 748 endif;
749 + endif;
200 750
201 - $this->post['meta']['syndication_permalink'] = apply_filters('syndicated_item_link', $permalink);
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;
759 + endif;
202 760
203 - // Store a hash of the post content for checking whether something needs to be updated
204 - $this->post['meta']['syndication_item_hash'] = $this->update_hash();
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;
205 767
206 - // Feed-by-feed options for author and category creation
207 - $this->post['named']['unfamiliar']['author'] = (isset($this->feedmeta['unfamiliar author']) ? $this->feedmeta['unfamiliar author'] : null);
208 - $this->post['named']['unfamiliar']['category'] = (isset($this->feedmeta['unfamiliar category']) ? $this->feedmeta['unfamiliar category'] : null);
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;
779 + endif;
209 780
210 - // Categories: start with default categories, if any
781 + return $author;
782 + } /* SyndicatedPost::author() */
783 +
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')) :
211 799 $fc = get_option("feedwordpress_syndication_cats");
212 800 if ($fc) :
213 - $this->post['named']['preset/category'] = explode("\n", $fc);
214 - else :
215 - $this->post['named']['preset/category'] = array();
801 + $cats = array_merge($cats, explode("\n", $fc));
216 802 endif;
803 + endif;
217 804
218 - if (isset($this->feedmeta['cats']) and is_array($this->feedmeta['cats'])) :
219 - $this->post['named']['preset/category'] = array_merge($this->post['named']['preset/category'], $this->feedmeta['cats']);
220 - endif;
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;
221 810
222 - // Now add categories from the post, if we have 'em
223 - $this->post['named']['category'] = array();
224 - if ( isset($this->item['category#']) ) :
225 - for ($i = 1; $i <= $this->item['category#']; $i++) :
226 - $cat_idx = (($i > 1) ? "#{$i}" : "");
227 - $cat = $this->item["category{$cat_idx}"];
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;
228 817
229 - if ( isset($this->feedmeta['cat_split']) and strlen($this->feedmeta['cat_split']) > 0) :
230 - $pcre = "\007".$this->feedmeta['cat_split']."\007";
231 - $this->post['named']['category'] = array_merge($this->post['named']['category'], preg_split($pcre, $cat, -1 /*=no limit*/, PREG_SPLIT_NO_EMPTY));
232 - else :
233 - $this->post['named']['category'][] = $cat;
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];
234 839 endif;
235 - endfor;
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;
236 849 endif;
237 - $this->post['named']['category'] = apply_filters('syndicated_item_categories', $this->post['named']['category'], $this);
238 -
239 - // Tags: start with default tags, if any
240 - $ft = get_option("feedwordpress_syndication_tags");
241 - if ($ft) :
242 - $this->post['tags_input'] = explode(FEEDWORDPRESS_CAT_SEPARATOR, $ft);
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 + );
243 883 else :
244 - $this->post['tags_input'] = array();
884 + $cats[] = $cat_name;
245 885 endif;
246 -
247 - if (isset($this->feedmeta['tags']) and is_array($this->feedmeta['tags'])) :
248 - $this->post['tags_input'] = array_merge($this->post['tags_input'], $this->feedmeta['tags']);
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 + /**
919 + * SyndicatedPost::isTaggedAs: Test whether a feed item is
920 + * tagged / categorized with a given string. Case and leading and
921 + * trailing whitespace are ignored.
922 + *
923 + * @param string $tag Tag to check for
924 + *
925 + * @return bool Whether or not at least one of the categories / tags on
926 + * $this->item is set to $tag (modulo case and leading and trailing
927 + * whitespace)
928 + */
929 + function isTaggedAs ($tag) {
930 + $desiredTag = strtolower(trim($tag)); // Normalize case and whitespace
931 +
932 + // Check to see if this is tagged with $tag
933 + $currentCategory = 'category';
934 + $currentCategoryNumber = 1;
935 +
936 + // If we have the new MagpieRSS, the number of category elements
937 + // on this item is stored under index "category#".
938 + if (isset($this->item['category#'])) :
939 + $numberOfCategories = (int) $this->item['category#'];
940 +
941 + // We REALLY shouldn't have the old and busted MagpieRSS, but in
942 + // case we do, it doesn't support multiple categories, but there
943 + // might still be a single value under the "category" index.
944 + elseif (isset($this->item['category'])) :
945 + $numberOfCategories = 1;
946 +
947 + // No standard category or tag elements on this feed item.
948 + else :
949 + $numberOfCategories = 0;
950 +
951 + endif;
952 +
953 + $isSoTagged = false; // Innocent until proven guilty
954 +
955 + // Loop through category elements; if there are multiple
956 + // elements, they are indexed as category, category#2,
957 + // category#3, ... category#N
958 + while ($currentCategoryNumber <= $numberOfCategories) :
959 + if ($desiredTag == strtolower(trim($this->item[$currentCategory]))) :
960 + $isSoTagged = true; // Got it!
961 + break;
249 962 endif;
250 - $this->post['tags_input'] = apply_filters('syndicated_item_tags', $this->post['tags_input'], $this);
963 +
964 + $currentCategoryNumber += 1;
965 + $currentCategory = 'category#'.$currentCategoryNumber;
966 + endwhile;
967 +
968 + return $isSoTagged;
969 + } /* SyndicatedPost::isTaggedAs() */
970 +
971 + /**
972 + * SyndicatedPost::enclosures: returns an array with any enclosures
973 + * that may be attached to this syndicated item.
974 + *
975 + * @param string $type If you only want enclosures that match a certain
976 + * MIME type or group of MIME types, you can limit the enclosures
977 + * that will be returned to only those with a MIME type which
978 + * matches this regular expression.
979 + * @return array
980 + */
981 + function enclosures ($type = '/.*/') {
982 + $enclosures = array();
983 +
984 + if (isset($this->item['enclosure#'])) :
985 + // Loop through enclosure, enclosure#2, enclosure#3, ....
986 + for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
987 + $eid = (($i > 1) ? "#{$id}" : "");
988 +
989 + // Does it match the type we want?
990 + if (preg_match($type, $this->item["enclosure{$eid}@type"])) :
991 + $enclosures[] = array(
992 + "url" => $this->item["enclosure{$eid}@url"],
993 + "type" => $this->item["enclosure{$eid}@type"],
994 + "length" => $this->item["enclosure{$eid}@length"],
995 + );
996 + endif;
997 + endfor;
251 998 endif;
252 - } // SyndicatedPost::SyndicatedPost()
999 + return $enclosures;
1000 + } /* SyndicatedPost::enclosures() */
253 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 +
1030 + function comment_link () {
1031 + $url = null;
1032 +
1033 + // RSS 2.0 has a standard <comments> element:
1034 + // "<comments> is an optional sub-element of <item>. If present,
1035 + // it is the url of the comments page for the item."
1036 + // <http://cyber.law.harvard.edu/rss/rss.html#ltcommentsgtSubelementOfLtitemgt>
1037 + if (isset($this->item['comments'])) :
1038 + $url = $this->item['comments'];
1039 + endif;
1040 +
1041 + // The convention in Atom feeds is to use a standard <link>
1042 + // element with @rel="replies" and @type="text/html".
1043 + // Unfortunately, SimplePie_Item::get_links() allows us to filter
1044 + // by the value of @rel, but not by the value of @type. *sigh*
1045 +
1046 + // Try Atom 1.0 first
1047 + $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link');
1048 +
1049 + // Fall back and try Atom 0.3
1050 + if (is_null($linkElements)) : $linkElements = $this->entry->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link'); endif;
1051 +
1052 + // Now loop through the elements, screening by @rel and @type
1053 + if (is_array($linkElements)) : foreach ($linkElements as $link) :
1054 + $rel = (isset($link['attribs']['']['rel']) ? $link['attribs']['']['rel'] : 'alternate');
1055 + $type = (isset($link['attribs']['']['type']) ? $link['attribs']['']['type'] : NULL);
1056 + $href = (isset($link['attribs']['']['href']) ? $link['attribs']['']['href'] : NULL);
1057 +
1058 + if (strtolower($rel)=='replies' and $type=='text/html' and !is_null($href)) :
1059 + $url = $href;
1060 + endif;
1061 + endforeach; endif;
1062 +
1063 + return $url;
1064 + }
1065 +
1066 + function comment_feed () {
1067 + $feed = null;
1068 +
1069 + // Well Formed Web comment feeds extension for RSS 2.0
1070 + // <http://www.sellsbrothers.com/spout/default.aspx?content=archive.htm#exposingRssComments>
1071 + //
1072 + // N.B.: Correct capitalization is wfw:commentRss, but
1073 + // wfw:commentRSS is common in the wild (partly due to a typo in
1074 + // the original spec). In any case, our item array is normalized
1075 + // to all lowercase anyways.
1076 + if (isset($this->item['wfw']['commentrss'])) :
1077 + $feed = $this->item['wfw']['commentrss'];
1078 + endif;
1079 +
1080 + // In Atom 1.0, the convention is to use a standard link element
1081 + // with @rel="replies". Sometimes this is also used to pass a
1082 + // link to the human-readable comments page, so we also need to
1083 + // check link/@type for a feed MIME type.
1084 + //
1085 + // Which is why I'm not using the SimplePie_Item::get_links()
1086 + // method here, incidentally: it doesn't allow you to filter by
1087 + // @type. *sigh*
1088 + if (isset($this->item['link_replies'])) :
1089 + // There may be multiple <link rel="replies"> elements; feeds have a feed MIME type
1090 + $N = isset($this->item['link_replies#']) ? $this->item['link_replies#'] : 1;
1091 + for ($i = 1; $i <= $N; $i++) :
1092 + $currentElement = 'link_replies'.(($i > 1) ? '#'.$i : '');
1093 + if (isset($this->item[$currentElement.'@type'])
1094 + and preg_match("\007application/(atom|rss|rdf)\+xml\007i", $this->item[$currentElement.'@type'])) :
1095 + $feed = $this->item[$currentElement];
1096 + endif;
1097 + endfor;
1098 + endif;
1099 + return $feed;
1100 + } /* SyndicatedPost::comment_feed() */
1101 +
1102 + ##################################
1103 + #### BUILT-IN CONTENT FILTERS ####
1104 + ##################################
1105 +
1106 + var $uri_attrs = array (
1107 + array('a', 'href'),
1108 + array('applet', 'codebase'),
1109 + array('area', 'href'),
1110 + array('blockquote', 'cite'),
1111 + array('body', 'background'),
1112 + array('del', 'cite'),
1113 + array('form', 'action'),
1114 + array('frame', 'longdesc'),
1115 + array('frame', 'src'),
1116 + array('iframe', 'longdesc'),
1117 + array('iframe', 'src'),
1118 + array('head', 'profile'),
1119 + array('img', 'longdesc'),
1120 + array('img', 'src'),
1121 + array('img', 'usemap'),
1122 + array('input', 'src'),
1123 + array('input', 'usemap'),
1124 + array('ins', 'cite'),
1125 + array('link', 'href'),
1126 + array('object', 'classid'),
1127 + array('object', 'codebase'),
1128 + array('object', 'data'),
1129 + array('object', 'usemap'),
1130 + array('q', 'cite'),
1131 + array('script', 'src')
1132 + ); /* var SyndicatedPost::$uri_attrs */
1133 +
1134 + var $_base = null;
1135 +
1136 + function resolve_single_relative_uri ($refs) {
1137 + $tag = FeedWordPressHTML::attributeMatch($refs);
1138 + $url = SimplePie_Misc::absolutize_url($tag['value'], $this->_base);
1139 +
1140 + return $tag['prefix'] . $url . $tag['suffix'];
1141 + } /* function SyndicatedPost::resolve_single_relative_uri() */
1142 +
1143 + static function resolve_relative_uris ($content, $obj) {
1144 + $set = $obj->link->setting('resolve relative', 'resolve_relative', 'yes');
1145 + if ($set and $set != 'no') :
1146 + // Fallback: if we don't have anything better, use the
1147 + // item link from the feed
1148 + $obj->_base = $obj->permalink(); // Reset the base for resolving relative URIs
1149 +
1150 + // What we should do here, properly, is to use
1151 + // SimplePie_Item::get_base() -- but that method is
1152 + // currently broken. Or getting down and dirty in the
1153 + // SimplePie representation of the content tags and
1154 + // grabbing the xml_base member for the content element.
1155 + // Maybe someday...
1156 +
1157 + foreach ($obj->uri_attrs as $pair) :
1158 + list($tag, $attr) = $pair;
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>
1165 + $content = preg_replace_callback (
1166 + $pattern,
1167 + array($obj, 'resolve_single_relative_uri'),
1168 + $content
1169 + );
1170 +
1171 + endforeach;
1172 + endif;
1173 +
1174 + return $content;
1175 + } /* function SyndicatedPost::resolve_relative_uris () */
1176 +
1177 + var $strip_attrs = array (
1178 + array('[a-z]+', 'target'),
1179 +// array('[a-z]+', 'style'),
1180 +// array('[a-z]+', 'on[a-z]+'),
1181 + );
1182 +
1183 + function strip_attribute_from_tag ($refs) {
1184 + $tag = FeedWordPressHTML::attributeMatch($refs);
1185 + return $tag['before_attribute'].$tag['after_attribute'];
1186 + }
1187 +
1188 + static function sanitize_content ($content, $obj) {
1189 + # This kind of sucks. I intend to replace it with
1190 + # lib_filter sometime soon.
1191 + foreach ($obj->strip_attrs as $pair):
1192 + list($tag,$attr) = $pair;
1193 + $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1194 +
1195 + $content = preg_replace_callback (
1196 + $pattern,
1197 + array($obj, 'strip_attribute_from_tag'),
1198 + $content
1199 + );
1200 + endforeach;
1201 + return $content;
1202 + } /* SyndicatedPost::sanitize() */
1203 +
1204 + #####################
1205 + #### POST STATUS ####
1206 + #####################
1207 +
1208 + /**
1209 + * SyndicatedPost::filtered: check whether or not this post has been
1210 + * screened out by a registered filter.
1211 + *
1212 + * @return bool TRUE iff post has been filtered out by a previous filter
1213 + */
254 1214 function filtered () {
255 1215 return is_null($this->post);
256 - }
1216 + } /* SyndicatedPost::filtered() */
257 1217
258 - function freshness () {
1218 + /**
1219 + * SyndicatedPost::freshness: check whether post is a new post to be
1220 + * inserted, a previously syndicated post that needs to be updated to
1221 + * match the latest revision, or a previously syndicated post that is
1222 + * still up-to-date.
1223 + *
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
1226 + * 0 = post already syndicated; no update needed
1227 + * 1 = post already syndicated, but needs to be updated to latest
1228 + * 2 = post has not yet been syndicated; needs to be created
1229 + */
1230 + function freshness ($format = 'number') {
259 1231 global $wpdb;
260 1232
261 1233 if ($this->filtered()) : // This should never happen.
262 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1234 + FeedWordPressDiagnostic::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
263 1235 endif;
264 -
265 - if (is_null($this->_freshness)) :
266 - $guid = $wpdb->escape($this->guid());
267 1236
268 - $result = $wpdb->get_row("
269 - SELECT id, guid, post_modified_gmt
270 - FROM $wpdb->posts WHERE guid='$guid'
271 - ");
1237 + if (is_null($this->_freshness)) : // Not yet checked and cached.
1238 + $guid = $this->post['guid'];
1239 + $eguid = esc_sql($this->post['guid']);
272 1240
273 - 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;
274 1259 $this->_freshness = 2; // New content
275 - else:
276 - $stored_update_hashes = get_post_custom_values('syndication_item_hash', $result->id);
277 - if (count($stored_update_hashes) > 0) :
278 - $stored_update_hash = $stored_update_hashes[0];
279 - $update_hash_changed = ($stored_update_hash != $this->update_hash());
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.
280 1303 else :
281 - $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;
282 1328 endif;
283 1329
284 - 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));
285 1336
286 - $last_rev_ts = gmmktime($backref[4], $backref[5], $backref[6], $backref[2], $backref[3], $backref[1]);
287 - $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
288 - $updated = ((
289 - !is_null($updated_ts)
290 - and ($updated_ts > $last_rev_ts)
291 - ) or $update_hash_changed);
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);
292 1345
293 1346 if ($updated) :
294 - $this->_freshness = 1; // Updated content
295 - $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 + );
296 1364 else :
1365 + FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$guid.'] "'.$this->entry->get_title().'" is a duplicate of an existing post.');
297 1366 $this->_freshness = 0; // Same old, same old
298 - $this->_wp_id = $result->id;
1367 + $this->_wp_id = $old_post->ID;
299 1368 endif;
300 1369 endif;
301 1370 endif;
302 - 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 );
303 1408 }
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 +
1453 + #################################################
1454 + #### INTERNAL STORAGE AND MANAGEMENT METHODS ####
1455 + #################################################
304 1456
305 1457 function wp_id () {
306 1458 if ($this->filtered()) : // This should never happen.
307 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1459 + FeedWordPressDiagnostic::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
308 1460 endif;
309 -
1461 +
310 1462 if (is_null($this->_wp_id) and is_null($this->_freshness)) :
311 1463 $fresh = $this->freshness(); // sets WP DB id in the process
312 1464 endif;
313 1465 return $this->_wp_id;
@@ -312,240 +1464,395 @@
312 1464 endif;
313 1465 return $this->_wp_id;
314 1466 }
315 1467
316 - 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 () {
317 1612 global $wpdb;
318 1613
319 1614 if ($this->filtered()) : // This should never happen.
320 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1615 + FeedWordPressDiagnostic::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
321 1616 endif;
322 -
1617 +
323 1618 $freshness = $this->freshness();
324 - if ($freshness > 0) :
325 - # -- Look up, or create, numeric ID for author
326 - $this->post['post_author'] = $this->author_id (
327 - FeedWordPress::on_unfamiliar('author', $this->post['named']['unfamiliar']['author'])
328 - );
1619 + if ($this->has_fresh_content()) :
1620 + $this->secure_author_id();
1621 + endif;
329 1622
330 - if (is_null($this->post['post_author'])) :
331 - $this->post = NULL;
332 - endif;
1623 + if ($this->has_fresh_content()) : // Was this filtered during author_id lookup?
1624 + $this->secure_term_ids();
333 1625 endif;
334 -
335 - if (!$this->filtered() and $freshness > 0) :
336 - # -- Look up, or create, numeric ID for categories
337 - list($pcats, $ptags) = $this->category_ids (
338 - $this->post['named']['category'],
339 - FeedWordPress::on_unfamiliar('category', $this->post['named']['unfamiliar']['category']),
340 - /*tags_too=*/ true
341 - );
342 1626
343 - $this->post['post_category'] = $pcats;
344 - $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
345 1631
346 - if (is_null($this->post['post_category'])) :
347 - // filter mode on, no matching categories; drop the post
348 - $this->post = NULL;
349 - else :
350 - // filter mode off or at least one match; now add on the feed and global presets
351 - $this->post['post_category'] = array_merge (
352 - $this->post['post_category'],
353 - $this->category_ids (
354 - $this->post['named']['preset/category'],
355 - 'default'
356 - )
357 - );
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);
358 1636
359 - if (count($this->post['post_category']) < 1) :
360 - $this->post['post_category'][] = 1; // Default to category 1 ("Uncategorized" / "General") if nothing else
361 - endif;
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;
362 1641 endif;
1642 +
1643 + $this->post = apply_filters('syndicated_post', $this->post, $this);
1644 +
1645 + // Allow for feed-specific syndicated_post filters.
1646 + $this->post = apply_filters(
1647 + "syndicated_post_".$this->link->uri(),
1648 + $this->post,
1649 + $this
1650 + );
363 1651 endif;
1652 +
1653 + // Hook in early to make sure these get inserted if at all possible
1654 + add_action(
1655 + /*hook=*/ 'transition_post_status',
1656 + /*callback=*/ array($this, 'add_rss_meta'),
1657 + /*priority=*/ -10000, /* very early */
1658 + /*arguments=*/ 3
1659 + );
364 1660
365 - if (!$this->filtered() and $freshness > 0) :
366 - unset($this->post['named']);
367 - $this->post = apply_filters('syndicated_post', $this->post, $this);
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());
1669 +
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');
368 1676 endif;
369 -
370 - if (!$this->filtered() and $freshness == 2) :
371 - // The item has not yet been added. So let's add it.
372 - $this->insert_new();
373 - $this->add_rss_meta();
374 - do_action('post_syndicated_item', $this->wp_id());
375 1677
376 - $ret = 'new';
377 - elseif (!$this->filtered() and $freshness == 1) :
378 - $this->post['ID'] = $this->wp_id();
379 - $this->update_existing();
380 - $this->add_rss_meta();
381 - do_action('update_syndicated_item', $this->wp_id());
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);
382 1683
383 - $ret = 'updated';
384 - else :
385 - $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;
386 1694 endif;
387 -
1695 +
1696 + // Remove add_rss_meta hook
1697 + remove_action(
1698 + /*hook=*/ 'transition_post_status',
1699 + /*callback=*/ array($this, 'add_rss_meta'),
1700 + /*priority=*/ -10000, /* very early */
1701 + /*arguments=*/ 3
1702 + );
1703 +
388 1704 return $ret;
389 - } // function SyndicatedPost::store ()
390 -
391 - function insert_new () {
392 - global $wpdb, $wp_db_version;
1705 + } /* function SyndicatedPost::store () */
393 1706
1707 + function insert_post ($update = false, $freshness = 2) {
1708 + global $wpdb;
1709 +
394 1710 $dbpost = $this->normalize_post(/*new=*/ true);
1711 +
1712 + $ret = null;
1713 +
395 1714 if (!is_null($dbpost)) :
396 - if ($this->use_api('wp_insert_post')) :
397 - $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
398 -
399 - // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
400 - add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
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);
401 1770
402 - // Kludge to prevent kses filters from stripping the
403 - // content of posts when updating without a logged in
404 - // user who has `unfiltered_html` capability.
405 - add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
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);
406 1775
407 - $this->_wp_id = wp_insert_post($dbpost);
408 -
409 - // Turn off ridiculous fucking kludges #1 and #2
410 - remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
411 - remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
412 -
413 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
414 -
415 - // Unfortunately, as of WordPress 2.3, wp_insert_post()
416 - // *still* offers no way to use a guid of your choice,
417 - // and munges your post modified timestamp, too.
418 - $result = $wpdb->query("
419 - UPDATE $wpdb->posts
420 - SET
421 - guid='{$dbpost['guid']}',
422 - post_modified='{$dbpost['post_modified']}',
423 - post_modified_gmt='{$dbpost['post_modified_gmt']}'
424 - WHERE ID='{$this->_wp_id}'
425 - ");
426 - else :
427 - # The right way to do this is the above. But, alas,
428 - # in earlier versions of WordPress, wp_insert_post has
429 - # too much behavior (mainly related to pings) that can't
430 - # be overridden. In WordPress 1.5, it's enough of a
431 - # resource hog to make PHP segfault after inserting
432 - # 50-100 posts. This can get pretty annoying, especially
433 - # if you are trying to update your feeds for the first
434 - # time.
435 -
436 - $result = $wpdb->query("
437 - INSERT INTO $wpdb->posts
438 - SET
439 - guid = '{$dbpost['guid']}',
440 - post_author = '{$dbpost['post_author']}',
441 - post_date = '{$dbpost['post_date']}',
442 - post_date_gmt = '{$dbpost['post_date_gmt']}',
443 - post_content = '{$dbpost['post_content']}',"
444 - .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
445 - post_title = '{$dbpost['post_title']}',
446 - post_name = '{$dbpost['post_name']}',
447 - post_modified = '{$dbpost['post_modified']}',
448 - post_modified_gmt = '{$dbpost['post_modified_gmt']}',
449 - comment_status = '{$dbpost['comment_status']}',
450 - ping_status = '{$dbpost['ping_status']}',
451 - post_status = '{$dbpost['post_status']}'
452 - ");
453 - $this->_wp_id = $wpdb->insert_id;
454 -
455 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
456 -
457 - // WordPress 1.5.x - 2.0.x
458 - wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
459 -
460 - // Since we are not going through official channels, we need to
461 - // manually tell WordPress that we've published a new post.
462 - // We need to make sure to do this in order for FeedWordPress
463 - // to play well with the staticize-reloaded plugin (something
464 - // that a large aggregator website is going to *want* to be
465 - // able to use).
466 - do_action('publish_post', $this->_wp_id);
1776 + $dbpost['ID'] = $this->_wp_id;
467 1777 endif;
468 - endif;
469 - } /* 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);
470 1787
471 - function update_existing () {
472 - global $wpdb;
1788 + if (!$this->this_revision_needs_original_post()) :
1789 +
1790 + if ($this->this_revision_is_current()) :
473 1791
474 - // Why the fuck doesn't wp_insert_post already do this?
475 - $dbpost = $this->normalize_post(/*new=*/ false);
476 - if (!is_null($dbpost)) :
477 - if ($this->use_api('wp_insert_post')) :
478 - $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
479 -
480 - // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
481 - add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
482 -
483 - // Kludge to prevent kses filters from stripping the
484 - // content of posts when updating without a logged in
485 - // user who has `unfiltered_html` capability.
486 - add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
1792 + wp_restore_post_revision($revision_id);
487 1793
488 - // Don't munge status fields that the user may have reset manually
489 - if (function_exists('get_post_field')) :
490 - $doNotMunge = array('post_status', 'comment_status', 'ping_status');
491 - foreach ($doNotMunge as $field) :
492 - $dbpost[$field] = get_post_field($field, $this->wp_id());
493 - endforeach;
494 - endif;
1794 + else :
495 1795
496 - $this->_wp_id = wp_insert_post($dbpost);
497 -
498 - // Turn off ridiculous fucking kludges #1 and #2
499 - remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
500 - remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
501 -
502 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
503 -
504 - // Unfortunately, as of WordPress 2.3, wp_insert_post()
505 - // munges your post modified timestamp.
506 - $result = $wpdb->query("
507 - UPDATE $wpdb->posts
508 - SET
509 - post_modified='{$dbpost['post_modified']}',
510 - post_modified_gmt='{$dbpost['post_modified_gmt']}'
511 - WHERE ID='{$this->_wp_id}'
512 - ");
513 - else :
514 -
515 - $result = $wpdb->query("
516 - UPDATE $wpdb->posts
517 - SET
518 - post_author = '{$dbpost['post_author']}',
519 - post_content = '{$dbpost['post_content']}',"
520 - .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
521 - post_title = '{$dbpost['post_title']}',
522 - post_name = '{$dbpost['post_name']}',
523 - post_modified = '{$dbpost['post_modified']}',
524 - post_modified_gmt = '{$dbpost['post_modified_gmt']}'
525 - WHERE guid='{$dbpost['guid']}'
526 - ");
527 -
528 - // WordPress 2.1.x and up
529 - if (function_exists('wp_set_post_categories')) :
530 - wp_set_post_categories($this->wp_id(), $this->post['post_category']);
531 - // WordPress 1.5.x - 2.0.x
532 - elseif (function_exists('wp_set_post_cats')) :
533 - wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
534 - // This should never happen.
535 - else :
536 - FeedWordPress::critical_bug(__CLASS__.'::'.__FUNCTION.'(): no post categorizing function', array("dbpost" => $dbpost, "this" => $this), __LINE__);
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;
537 1807 endif;
538 -
539 - // Since we are not going through official channels, we need to
540 - // manually tell WordPress that we've published a new post.
541 - // We need to make sure to do this in order for FeedWordPress
542 - // to play well with the staticize-reloaded plugin (something
543 - // that a large aggregator website is going to *want* to be
544 - // able to use).
545 - do_action('edit_post', $this->post['ID']);
546 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;
547 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);
548 1855 } /* SyndicatedPost::update_existing() */
549 1856
550 1857 /**
551 1858 * SyndicatedPost::normalize_post()
@@ -555,33 +1862,104 @@
555 1862 */
556 1863 function normalize_post ($new = true) {
557 1864 global $wpdb;
558 1865
559 - $out = array();
1866 + $out = $this->post;
560 1867
561 - // Why the fuck doesn't wp_insert_post already do this?
562 - foreach ($this->post as $key => $value) :
563 - if (is_string($value)) :
564 - $out[$key] = $wpdb->escape($value);
565 - else :
566 - $out[$key] = $value;
567 - endif;
568 - endforeach;
569 -
570 - 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) :
571 1871 // FIXME: Option for filtering out empty posts
572 1872 endif;
573 1873 if (strlen($out['post_title'])==0) :
574 1874 $offset = (int) get_option('gmt_offset') * 60 * 60;
575 - $out['post_title'] =
576 - $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
577 1883 .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
578 1884 // FIXME: Option for what to fill a blank title with...
579 1885 endif;
580 1886
1887 + // Normalize the guid if necessary.
1888 + $out['guid'] = SyndicatedPost::normalize_guid($out['guid']);
1889 +
581 1890 return $out;
582 1891 }
583 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 +
584 1962 /**
585 1963 * SyndicatedPost::validate_post_id()
586 1964 *
587 1965 * @param array $dbpost An array representing the post we attempted to insert or update
@@ -586,35 +1964,57 @@
586 1964 *
587 1965 * @param array $dbpost An array representing the post we attempted to insert or update
588 1966 * @param mixed $ns A string or array representing the namespace (class, method) whence this method was called.
589 1967 */
590 - function validate_post_id ($dbpost, $ns) {
1968 + function validate_post_id ($dbpost, $is_update, $ns) {
591 1969 if (is_array($ns)) : $ns = implode('::', $ns);
592 1970 else : $ns = (string) $ns; endif;
593 -
1971 +
594 1972 // This should never happen.
595 1973 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
596 - FeedWordPress::critical_bug(
597 - /*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,
598 1999 /*var =*/ array(
599 2000 "\$this->_wp_id" => $this->_wp_id,
600 2001 "\$dbpost" => $dbpost,
601 - "\$this" => $this
602 2002 ),
603 - /*line # =*/ __LINE__
2003 + /*line # =*/ __LINE__, /*filename=*/ __FILE__
604 2004 );
605 2005 endif;
606 2006 } /* SyndicatedPost::validate_post_id() */
607 -
2007 +
608 2008 /**
609 - * SyndicatedPost::fix_revision_meta() - Fixes the way WP 2.6+ fucks up
610 - * meta-data (authorship, etc.) when storing revisions of an updated
611 - * 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.
612 2012 *
613 - * In their infinite wisdom, the WordPress coders have made it completely
614 - * impossible for a plugin that uses wp_insert_post() to set certain
615 - * meta-data (such as the author) when you store an old revision of an
616 - * 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.
617 2017 * active user ID if the process is running with a user logged in, or
618 2018 * = #0 if there is no user logged in). This results in bogus authorship
619 2019 * data for revisions that are syndicated from off the feed, unless we
620 2020 * use a ridiculous kludge like this to end-run the munging of meta-data
@@ -623,110 +2023,204 @@
623 2023 * @param int $revision_id The revision ID to fix up meta-data
624 2024 */
625 2025 function fix_revision_meta ($revision_id) {
626 2026 global $wpdb;
2027 +
2028 + $post_author = (int) $this->post['post_author'];
2029 +
2030 + $revision_id = (int) $revision_id;
627 2031
628 - $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);
629 2034
630 - $revision_id = (int) $revision_id;
631 - $wpdb->query("
632 - UPDATE $wpdb->posts
633 - SET post_author={$this->post['post_author']}
634 - WHERE post_type = 'revision' AND ID='$revision_id'
635 - ");
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 +
636 2041 } /* SyndicatedPost::fix_revision_meta () */
637 2042
638 2043 /**
639 - * SyndicatedPost::avoid_kses_munge() -- If FeedWordPress is processing
640 - * an automatic update, that generally means that wp_insert_post() is
641 - * being called under the user credentials of whoever is viewing the
642 - * blog at the time -- usually meaning no user at all. But if WordPress
643 - * gets a wp_insert_post() when current_user_can('unfiltered_html') is
644 - * false, it will run the content of the post through a kses function
645 - * that strips out lots of HTML tags -- notably <object> and some others.
646 - * This causes problems for syndicating (for example) feeds that contain
647 - * YouTube videos. It also produces an unexpected asymmetry between
648 - * automatically-initiated updates and updates initiated manually from
649 - * the WordPress Dashboard (which are usually initiated under the
650 - * credentials of a logged-in admin, and so don't get run through the
651 - * kses function). So, to avoid the whole mess, what we do here is
652 - * just forcibly disable the kses munging for a single syndicated post,
653 - * by restoring the contents of the `post_content` field.
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.
654 2052 *
655 - * @param string $content The content of the post, after other filters have gotten to it
656 - * @return string The original content of the post, before other filters had a chance to munge it.
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.
657 2059 */
658 - function avoid_kses_munge ($content) {
659 - global $wpdb;
660 - return $wpdb->escape($this->post['post_content']);
661 - }
662 -
663 - // SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
664 - // using the space for custom keys. The set of keys and values to add is
665 - // specified by the keys and values of $post['meta']. This is used to
666 - // store anything that the WordPress user might want to access from a
667 - // template concerning the post's original source that isn't provided
668 - // for by standard WP meta-data (i.e., any interesting data about the
669 - // syndicated post other than author, title, timestamp, categories, and
670 - // guid). It's also used to hook into WordPress's support for
671 - // enclosures.
672 - function add_rss_meta () {
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) {
673 2106 global $wpdb;
674 - if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
675 - $postId = $this->wp_id();
676 -
677 - // Aggregated posts should NOT send out pingbacks.
678 - // WordPress 2.1-2.2 claim you can tell them not to
679 - // using $post_pingback, but they don't listen, so we
680 - // make sure here.
681 - $result = $wpdb->query("
682 - DELETE FROM $wpdb->postmeta
683 - WHERE post_id='$postId' AND meta_key='_pingme'
684 - ");
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 () */
685 2114
686 - foreach ( $this->post['meta'] as $key => $values ) :
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 + */
2130 + function add_rss_meta ($new_status, $old_status, $post) {
2131 + global $wpdb;
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'])).'}');
687 2134
688 - $key = $wpdb->escape($key);
2135 + if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
2136 + $postId = $post->ID;
689 2137
690 - // If this is an update, clear out the old
691 - // 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.
692 2142 $result = $wpdb->query("
693 2143 DELETE FROM $wpdb->postmeta
694 - WHERE post_id='$postId' AND meta_key='$key'
2144 + WHERE post_id='$postId' AND meta_key='_pingme'
695 2145 ");
696 2146
697 - // Allow for either a single value or an array
698 - if (!is_array($values)) $values = array($values);
699 - foreach ( $values as $value ) :
700 - $value = $wpdb->escape($value);
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.
701 2152 $result = $wpdb->query("
702 - INSERT INTO $wpdb->postmeta
703 - SET
704 - post_id='$postId',
705 - meta_key='$key',
706 - meta_value='$value'
2153 + DELETE FROM $wpdb->postmeta
2154 + WHERE post_id='$postId' AND meta_key='$eKey'
707 2155 ");
708 - if (!$result) :
709 - $err = mysql_error();
710 - if (FEEDWORDPRESS_DEBUG) :
711 - echo "[DEBUG:".date('Y-m-d H:i:S')."][feedwordpress]: post metadata insertion FAILED for field '$key' := '$value': [$err]";
712 - endif;
713 - endif;
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;
714 2163 endforeach;
715 - endforeach;
2164 + endif;
716 2165 endif;
717 2166 } /* SyndicatedPost::add_rss_meta () */
718 2167
719 - // SyndicatedPost::author_id (): get the ID for an author name from
720 - // 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 + */
721 2177 function author_id ($unfamiliar_author = 'create') {
722 2178 global $wpdb;
723 2179
724 - $a = $this->author();
725 - $author = $a['name'];
726 - $email = $a['email'];
727 - $url = $a['uri'];
2180 + $a = $this->named['author'];
728 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 +
2214 + $email = (isset($a['email']) ? $a['email'] : NULL);
2215 + $authorUrl = (isset($a['uri']) ? $a['uri'] : NULL);
2216 +
2217 +
2218 + $hostUrl = $this->link->homepage();
2219 + if (is_null($hostUrl) or (strlen($hostUrl) < 0)) :
2220 + $hostUrl = $this->link->uri();
2221 + endif;
2222 +
729 2223 $match_author_by_email = !('yes' == get_option("feedwordpress_do_not_match_author_by_email"));
730 2224 if ($match_author_by_email and !FeedWordPress::is_null_email($email)) :
731 2225 $test_email = $email;
732 2226 else :
@@ -734,21 +2228,43 @@
734 2228 endif;
735 2229
736 2230 // Never can be too careful...
737 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 +
738 2252 $login = apply_filters('pre_user_login', $login);
739 2253
740 2254 $nice_author = sanitize_title($author);
741 2255 $nice_author = apply_filters('pre_user_nicename', $nice_author);
742 2256
743 - $reg_author = $wpdb->escape(preg_quote($author));
744 - $author = $wpdb->escape($author);
745 - $email = $wpdb->escape($email);
746 - $test_email = $wpdb->escape($test_email);
747 - $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);
748 2262
749 2263 // Check for an existing author rule....
750 - 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))])) :
751 2267 $author_rule = $this->link->settings['map authors']['name'][strtolower(trim($author))];
752 2268 else :
753 2269 $author_rule = NULL;
754 2270 endif;
@@ -759,69 +2275,38 @@
759 2275
760 2276 // User name is filtered out
761 2277 elseif ('filter' == $author_rule) :
762 2278 $id = NULL;
763 -
2279 +
764 2280 else :
765 2281 // Check the database for an existing author record that might fit
766 2282
767 - #-- WordPress 2.0+
768 - if (fwp_test_wp_version(FWP_SCHEMA_HAS_USERMETA)) :
2283 + // First try the user core data table.
2284 + $id = $wpdb->get_var(
2285 + "SELECT ID FROM $wpdb->users
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 + )");
769 2292
770 - // First try the user core data table.
2293 + // If that fails, look for aliases in the user meta data table
2294 + if (is_null($id)) :
771 2295 $id = $wpdb->get_var(
772 - "SELECT ID FROM $wpdb->users
2296 + "SELECT user_id FROM $wpdb->usermeta
773 2297 WHERE
774 - TRIM(LCASE(user_login)) = TRIM(LCASE('$login'))
2298 + (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
775 2299 OR (
776 - LENGTH(TRIM(LCASE(user_email))) > 0
777 - AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
778 - )
779 - OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author'))
780 - ");
781 -
782 - // If that fails, look for aliases in the user meta data table
783 - if (is_null($id)) :
784 - $id = $wpdb->get_var(
785 - "SELECT user_id FROM $wpdb->usermeta
786 - WHERE
787 - (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
788 - OR (
789 - meta_key = 'description'
790 - AND TRIM(LCASE(meta_value))
791 - RLIKE CONCAT(
792 - '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
793 - TRIM(LCASE('$reg_author')),
794 - '( |\\t|\\r)*(\\n|\$)'
795 - )
796 - )
797 - ");
798 - endif;
799 -
800 - #-- WordPress 1.5.x
801 - else :
802 - $id = $wpdb->get_var(
803 - "SELECT ID from $wpdb->users
804 - WHERE
805 - TRIM(LCASE(user_login)) = TRIM(LCASE('$login')) OR
806 - (
807 - LENGTH(TRIM(LCASE(user_email))) > 0
808 - AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
809 - ) OR
810 - TRIM(LCASE(user_firstname)) = TRIM(LCASE('$author')) OR
811 - TRIM(LCASE(user_nickname)) = TRIM(LCASE('$author')) OR
812 - TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author')) OR
813 - TRIM(LCASE(user_description)) = TRIM(LCASE('$author')) OR
814 - (
815 - LOWER(user_description)
2300 + meta_key = 'description'
2301 + AND TRIM(LCASE(meta_value))
816 2302 RLIKE CONCAT(
817 2303 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
818 - LCASE('$reg_author'),
2304 + TRIM(LCASE('$reg_author')),
819 2305 '( |\\t|\\r)*(\\n|\$)'
820 2306 )
821 2307 )
822 2308 ");
823 -
824 2309 endif;
825 2310
826 2311 // ... if you don't find one, then do what you need to do
827 2312 if (is_null($id)) :
@@ -827,8 +2312,16 @@
827 2312 if (is_null($id)) :
828 2313 if ($unfamiliar_author === 'create') :
829 2314 $userdata = array();
830 2315
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.)
2319 + if (strlen($email) == 0 or FeedWordPress::is_null_email($email)) :
2320 + $url = parse_url($hostUrl);
2321 + $email = $nice_author.'@'.$url['host'];
2322 + endif;
2323 +
831 2324 #-- user table data
832 2325 $userdata['ID'] = NULL; // new user
833 2326 $userdata['user_login'] = $login;
834 2327 $userdata['user_nicename'] = $nice_author;
@@ -833,12 +2326,79 @@
833 2326 $userdata['user_login'] = $login;
834 2327 $userdata['user_nicename'] = $nice_author;
835 2328 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
836 2329 $userdata['user_email'] = $email;
837 - $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 +
838 2337 $userdata['display_name'] = $author;
839 -
840 - $id = wp_insert_user($userdata);
2338 + $userdata['role'] = 'contributor';
2339 +
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 +
841 2401 elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
842 2402 $id = (int) $unfamiliar_author;
843 2403 elseif ($unfamiliar_author === 'default') :
844 2404 $id = 1;
@@ -847,432 +2407,30 @@
847 2407 endif;
848 2408
849 2409 if ($id) :
850 2410 $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
851 - endif;
852 - return $id;
853 - } // function SyndicatedPost::author_id ()
854 2411
855 - // look up (and create) category ids from a list of categories
856 - function category_ids ($cats, $unfamiliar_category = 'create', $tags_too = false) {
857 - global $wpdb;
858 -
859 - // We need to normalize whitespace because (1) trailing
860 - // whitespace can cause PHP and MySQL not to see eye to eye on
861 - // VARCHAR comparisons for some versions of MySQL (cf.
862 - // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
863 - // because I doubt most people want to make a semantic
864 - // distinction between 'Computers' and 'Computers '
865 - $cats = array_map('trim', $cats);
866 -
867 - $tags = array();
868 -
869 - $cat_ids = array ();
870 - foreach ($cats as $cat_name) :
871 - if (preg_match('/^{#([0-9]+)}$/', $cat_name, $backref)) :
872 - $cat_id = (int) $backref[1];
873 - if (function_exists('is_term') and is_term($cat_id, 'category')) :
874 - $cat_ids[] = $cat_id;
875 - elseif (get_category($cat_id)) :
876 - $cat_ids[] = $cat_id;
877 - endif;
878 - elseif (strlen($cat_name) > 0) :
879 - $esc = $wpdb->escape($cat_name);
880 - $resc = $wpdb->escape(preg_quote($cat_name));
881 -
882 - // WordPress 2.3+
883 - if (function_exists('is_term')) :
884 - $cat_id = is_term($cat_name, 'category');
885 - if ($cat_id) :
886 - $cat_ids[] = $cat_id['term_id'];
887 - // There must be a better way to do this...
888 - elseif ($results = $wpdb->get_results(
889 - "SELECT term_id
890 - FROM $wpdb->term_taxonomy
891 - WHERE
892 - LOWER(description) RLIKE
893 - CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)')"
894 - )) :
895 - foreach ($results AS $term) :
896 - $cat_ids[] = (int) $term->term_id;
897 - endforeach;
898 - elseif ('tag'==$unfamiliar_category) :
899 - $tags[] = $cat_name;
900 - elseif ('create'===$unfamiliar_category) :
901 - $term = wp_insert_term($cat_name, 'category');
902 - if (is_wp_error($term)) :
903 - FeedWordPress::noncritical_bug('term insertion problem', array('cat_name' => $cat_name, 'term' => $term, 'this' => $this), __LINE__);
904 - else :
905 - $cat_ids[] = $term['term_id'];
906 - endif;
907 - endif;
908 -
909 - // WordPress 1.5.x - 2.2.x
910 - else :
911 - $results = $wpdb->get_results(
912 - "SELECT cat_ID
913 - FROM $wpdb->categories
914 - WHERE
915 - (LOWER(cat_name) = LOWER('$esc'))
916 - OR (LOWER(category_description)
917 - RLIKE CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)'))
918 - ");
919 - if ($results) :
920 - foreach ($results as $term) :
921 - $cat_ids[] = (int) $term->cat_ID;
922 - endforeach;
923 - elseif ('create'===$unfamiliar_category) :
924 - if (function_exists('wp_insert_category')) :
925 - $cat_id = wp_insert_category(array('cat_name' => $esc));
926 - // And into the database we go.
927 - else :
928 - $nice_kitty = sanitize_title($cat_name);
929 - $wpdb->query(sprintf("
930 - INSERT INTO $wpdb->categories
931 - SET
932 - cat_name='%s',
933 - category_nicename='%s'
934 - ", $esc, $nice_kitty
935 - ));
936 - $cat_id = $wpdb->insert_id;
937 - endif;
938 - $cat_ids[] = $cat_id;
939 - endif;
940 - 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');
941 2418 endif;
942 - endforeach;
943 -
944 - if ((count($cat_ids) == 0) and ($unfamiliar_category === 'filter')) :
945 - $cat_ids = NULL; // Drop the post
946 - else :
947 - $cat_ids = array_unique($cat_ids);
948 2419 endif;
949 -
950 - if ($tags_too) : $ret = array($cat_ids, $tags);
951 - else : $ret = $cat_ids;
952 - endif;
2420 + return $id;
2421 + } /* function SyndicatedPost::author_id () */
953 2422
954 - return $ret;
955 - } // function SyndicatedPost::category_ids ()
956 -
957 - function use_api ($tag) {
958 - global $wp_db_version;
959 - switch ($tag) :
960 - case 'wp_insert_post':
961 - // Before 2.2, wp_insert_post does too much of the wrong stuff to use it
962 - // In 1.5 it was such a resource hog it would make PHP segfault on big updates
963 - $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_21);
964 - break;
965 - case 'post_status_pending':
966 - $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_23);
967 - break;
968 - endswitch;
969 - return $ret;
970 - } // function SyndicatedPost::use_api ()
971 -
972 - #### EXTRACT DATA FROM FEED ITEM ####
973 -
974 - function created () {
975 - $epoch = null;
976 - if (isset($this->item['dc']['created'])) :
977 - $epoch = @parse_w3cdtf($this->item['dc']['created']);
978 - elseif (isset($this->item['dcterms']['created'])) :
979 - $epoch = @parse_w3cdtf($this->item['dcterms']['created']);
980 - elseif (isset($this->item['created'])): // Atom 0.3
981 - $epoch = @parse_w3cdtf($this->item['created']);
982 - endif;
983 - return $epoch;
984 - }
985 - function published ($fallback = true) {
986 - $epoch = null;
987 -
988 - # RSS is a fucking mess. Figure out whether we have a date in
989 - # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
990 - # epoch format for reformatting. If we can't find anything,
991 - # we'll use the last-updated time.
992 - if (isset($this->item['dc']['date'])): // Dublin Core
993 - $epoch = @parse_w3cdtf($this->item['dc']['date']);
994 - elseif (isset($this->item['dcterms']['issued'])) : // Dublin Core extensions
995 - $epoch = @parse_w3cdtf($this->item['dcterms']['issued']);
996 - elseif (isset($this->item['published'])) : // Atom 1.0
997 - $epoch = @parse_w3cdtf($this->item['published']);
998 - elseif (isset($this->item['issued'])): // Atom 0.3
999 - $epoch = @parse_w3cdtf($this->item['issued']);
1000 - elseif (isset($this->item['pubdate'])): // RSS 2.0
1001 - $epoch = strtotime($this->item['pubdate']);
1002 - elseif ($fallback) : // Fall back to <updated> / <modified> if present
1003 - $epoch = $this->updated(/*fallback=*/ false);
1004 - endif;
1005 -
1006 - # If everything failed, then default to the current time.
1007 - if (is_null($epoch)) :
1008 - if (-1 == $default) :
1009 - $epoch = time();
1010 - else :
1011 - $epoch = $default;
1012 - endif;
1013 - endif;
1014 -
1015 - return $epoch;
1016 - }
1017 - function updated ($fallback = true, $default = -1) {
1018 - $epoch = null;
1019 -
1020 - # As far as I know, only dcterms and Atom have reliable ways to
1021 - # specify when something was *modified* last. If neither is
1022 - # available, then we'll try to get the time of publication.
1023 - if (isset($this->item['dc']['modified'])) : // Not really correct
1024 - $epoch = @parse_w3cdtf($this->item['dc']['modified']);
1025 - elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
1026 - $epoch = @parse_w3cdtf($this->item['dcterms']['modified']);
1027 - elseif (isset($this->item['modified'])): // Atom 0.3
1028 - $epoch = @parse_w3cdtf($this->item['modified']);
1029 - elseif (isset($this->item['updated'])): // Atom 1.0
1030 - $epoch = @parse_w3cdtf($this->item['updated']);
1031 - elseif ($fallback) : // Fall back to issued / dc:date
1032 - $epoch = $this->published(/*fallback=*/ false, /*default=*/ $default);
1033 - endif;
1034 -
1035 - # If everything failed, then default to the current time.
1036 - if (is_null($epoch)) :
1037 - if (-1 == $default) :
1038 - $epoch = time();
1039 - else :
1040 - $epoch = $default;
1041 - endif;
1042 - endif;
1043 -
1044 - return $epoch;
1045 - }
1046 -
1047 - function update_hash () {
1048 - return md5(serialize($this->item));
1049 - }
1050 -
1051 - function guid () {
1052 - $guid = null;
1053 - if (isset($this->item['id'])): // Atom 0.3 / 1.0
1054 - $guid = $this->item['id'];
1055 - elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
1056 - $guid = $this->item['atom']['id'];
1057 - elseif (isset($this->item['guid'])) : // RSS 2.0
1058 - $guid = $this->item['guid'];
1059 - elseif (isset($this->item['dc']['identifier'])) :// yeah, right
1060 - $guid = $this->item['dc']['identifier'];
1061 - else :
1062 - // The feed does not seem to have provided us with a
1063 - // unique identifier, so we'll have to cobble together
1064 - // a tag: URI that might work for us. The base of the
1065 - // URI will be the host name of the feed source ...
1066 - $bits = parse_url($this->feedmeta['link/uri']);
1067 - $guid = 'tag:'.$bits['host'];
1068 -
1069 - // If we have a date of creation, then we can use that
1070 - // to uniquely identify the item. (On the other hand, if
1071 - // the feed producer was consicentious enough to
1072 - // generate dates of creation, she probably also was
1073 - // conscientious enough to generate unique identifiers.)
1074 - if (!is_null($this->created())) :
1075 - $guid .= '://post.'.date('YmdHis', $this->created());
1076 -
1077 - // Otherwise, use both the URI of the item, *and* the
1078 - // item's title. We have to use both because titles are
1079 - // often not unique, and sometimes links aren't unique
1080 - // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
1081 - // some podcasts). But it's rare to have *both* the same
1082 - // title *and* the same link for two different items. So
1083 - // this is about the best we can do.
1084 - else :
1085 - $guid .= '://'.md5($this->item['link'].'/'.$this->item['title']);
1086 - endif;
1087 - endif;
1088 - return $guid;
1089 - }
1090 -
1091 - function author () {
1092 - $author = array ();
1093 -
1094 - if (isset($this->item['author_name'])):
1095 - $author['name'] = $this->item['author_name'];
1096 - elseif (isset($this->item['dc']['creator'])):
1097 - $author['name'] = $this->item['dc']['creator'];
1098 - elseif (isset($this->item['dc']['contributor'])):
1099 - $author['name'] = $this->item['dc']['contributor'];
1100 - elseif (isset($this->feed->channel['dc']['creator'])) :
1101 - $author['name'] = $this->feed->channel['dc']['creator'];
1102 - elseif (isset($this->feed->channel['dc']['contributor'])) :
1103 - $author['name'] = $this->feed->channel['dc']['contributor'];
1104 - elseif (isset($this->feed->channel['author_name'])) :
1105 - $author['name'] = $this->feed->channel['author_name'];
1106 - elseif ($this->feed->is_rss() and isset($this->item['author'])) :
1107 - // The author element in RSS is allegedly an
1108 - // e-mail address, but lots of people don't use
1109 - // it that way. So let's make of it what we can.
1110 - $author = parse_email_with_realname($this->item['author']);
1111 -
1112 - if (!isset($author['name'])) :
1113 - if (isset($author['email'])) :
1114 - $author['name'] = $author['email'];
1115 - else :
1116 - $author['name'] = $this->feed->channel['title'];
1117 - endif;
1118 - endif;
1119 - else :
1120 - $author['name'] = $this->feed->channel['title'];
1121 - endif;
1122 -
1123 - if (isset($this->item['author_email'])):
1124 - $author['email'] = $this->item['author_email'];
1125 - elseif (isset($this->feed->channel['author_email'])) :
1126 - $author['email'] = $this->feed->channel['author_email'];
1127 - endif;
1128 -
1129 - if (isset($this->item['author_url'])):
1130 - $author['uri'] = $this->item['author_url'];
1131 - elseif (isset($this->feed->channel['author_url'])) :
1132 - $author['uri'] = $this->item['author_url'];
1133 - else:
1134 - $author['uri'] = $this->feed->channel['link'];
1135 - endif;
1136 -
1137 - return $author;
1138 - } // SyndicatedPost::author()
1139 -
1140 2423 /**
1141 - * SyndicatedPost::isTaggedAs: Test whether a feed item is
1142 - * tagged / categorized with a given string. Case and leading and
1143 - * trailing whitespace are ignored.
2424 + * category_ids: look up (and create) category ids from a list of
2425 + * categories
1144 2426 *
1145 - * @param string $tag Tag to check for
1146 - *
1147 - * @return bool Whether or not at least one of the categories / tags on
1148 - * $this->item is set to $tag (modulo case and leading and trailing
1149 - * whitespace)
2427 + * @param array $cats
2428 + * @param string $unfamiliar_category
2429 + * @param array|null $taxonomies
2430 + * @return array
1150 2431 */
1151 - function isTaggedAs ($tag) {
1152 - $desiredTag = strtolower(trim($tag)); // Normalize case and whitespace
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 () */
1153 2435
1154 - // Check to see if this is tagged with $tag
1155 - $currentCategory = 'category';
1156 - $currentCategoryNumber = 1;
1157 -
1158 - // If we have the new MagpieRSS, the number of category elements
1159 - // on this item is stored under index "category#".
1160 - if (isset($this->item['category#'])) :
1161 - $numberOfCategories = (int) $this->item['category#'];
1162 -
1163 - // We REALLY shouldn't have the old and busted MagpieRSS, but in
1164 - // case we do, it doesn't support multiple categories, but there
1165 - // might still be a single value under the "category" index.
1166 - elseif (isset($this->item['category'])) :
1167 - $numberOfCategories = 1;
1168 -
1169 - // No standard category or tag elements on this feed item.
1170 - else :
1171 - $numberOfCategories = 0;
1172 -
1173 - endif;
1174 -
1175 - $isSoTagged = false; // Innocent until proven guilty
1176 -
1177 - // Loop through category elements; if there are multiple
1178 - // elements, they are indexed as category, category#2,
1179 - // category#3, ... category#N
1180 - while ($currentCategoryNumber <= $numberOfCategories) :
1181 - if ($desiredTag == strtolower(trim($this->item[$currentCategory]))) :
1182 - $isSoTagged = true; // Got it!
1183 - break;
1184 - endif;
1185 -
1186 - $currentCategoryNumber += 1;
1187 - $currentCategory = 'category#'.$currentCategoryNumber;
1188 - endwhile;
1189 -
1190 - return $isSoTagged;
1191 - } /* SyndicatedPost::isTaggedAs() */
1192 -
1193 - var $uri_attrs = array (
1194 - array('a', 'href'),
1195 - array('applet', 'codebase'),
1196 - array('area', 'href'),
1197 - array('blockquote', 'cite'),
1198 - array('body', 'background'),
1199 - array('del', 'cite'),
1200 - array('form', 'action'),
1201 - array('frame', 'longdesc'),
1202 - array('frame', 'src'),
1203 - array('iframe', 'longdesc'),
1204 - array('iframe', 'src'),
1205 - array('head', 'profile'),
1206 - array('img', 'longdesc'),
1207 - array('img', 'src'),
1208 - array('img', 'usemap'),
1209 - array('input', 'src'),
1210 - array('input', 'usemap'),
1211 - array('ins', 'cite'),
1212 - array('link', 'href'),
1213 - array('object', 'classid'),
1214 - array('object', 'codebase'),
1215 - array('object', 'data'),
1216 - array('object', 'usemap'),
1217 - array('q', 'cite'),
1218 - array('script', 'src')
1219 - ); /* var SyndicatedPost::$uri_attrs */
1220 -
1221 - var $_base = null;
1222 -
1223 - function resolve_single_relative_uri ($refs) {
1224 - $tag = FeedWordPressHTML::attributeMatch($refs);
1225 - $url = Relative_URI::resolve($tag['value'], $this->_base);
1226 - return $tag['prefix'] . $url . $tag['suffix'];
1227 - } /* function SyndicatedPost::resolve_single_relative_uri() */
1228 -
1229 - function resolve_relative_uris ($content, $obj) {
1230 - # The MagpieRSS upgrade has some `xml:base` support baked in.
1231 - # However, sometimes people do silly things, like putting
1232 - # relative URIs out on a production RSS 2.0 feed or other feeds
1233 - # with no good support for `xml:base`. So we'll do our best to
1234 - # try to catch any remaining relative URIs and resolve them as
1235 - # best we can.
1236 - $obj->_base = $obj->item['link']; // Reset the base for resolving relative URIs
1237 -
1238 - foreach ($obj->uri_attrs as $pair) :
1239 - list($tag, $attr) = $pair;
1240 - $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1241 - $content = preg_replace_callback (
1242 - $pattern,
1243 - array(&$obj, 'resolve_single_relative_uri'),
1244 - $content
1245 - );
1246 - endforeach;
1247 -
1248 - return $content;
1249 - } /* function SyndicatedPost::resolve_relative_uris () */
1250 -
1251 - var $strip_attrs = array (
1252 - array('[a-z]+', 'target'),
1253 -// array('[a-z]+', 'style'),
1254 -// array('[a-z]+', 'on[a-z]+'),
1255 - );
1256 -
1257 - function strip_attribute_from_tag ($refs) {
1258 - $tag = FeedWordPressHTML::attributeMatch($refs);
1259 - return $tag['before_attribute'].$tag['after_attribute'];
1260 - }
1261 -
1262 - function sanitize_content ($content, $obj) {
1263 - # This kind of sucks. I intend to replace it with
1264 - # lib_filter sometime soon.
1265 - foreach ($obj->strip_attrs as $pair):
1266 - list($tag,$attr) = $pair;
1267 - $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1268 -
1269 - $content = preg_replace_callback (
1270 - $pattern,
1271 - array(&$obj, 'strip_attribute_from_tag'),
1272 - $content
1273 - );
1274 - endforeach;
1275 - return $content;
1276 - }
1277 -} // class SyndicatedPost
1278 -
2436 +} /* class SyndicatedPost */