PluginProbe
FeedWordPress / 2017.0913
FeedWordPress v2017.0913
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
feedwordpress / syndicatedpost.class.php

syndicatedpost.class.php in FeedWordPress 2017.0913, at syndicatedpost.class.php

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