PluginProbe
FeedWordPress / 2022.0204
FeedWordPress v2022.0204
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 2022.0204, at syndicatedpost.class.php

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