PluginProbe
FeedWordPress / 2020.0118
FeedWordPress v2020.0118
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 2020.0118, at syndicatedpost.class.php

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