PluginProbe
FeedWordPress / 2017.1020
FeedWordPress v2017.1020
trunk 0.8 0.9 0.91 0.95 0.96 0.97 0.98 0.981 0.99 0.991 0.992 0.993 2008.1030 2008.1101 2008.1105 2008.1214 2009.0612 2009.0613 2009.0618 2009.0707 2009.1111 2009.1112 2010.0127 2010.0528 All 65 releases
feedwordpress / syndicatedpost.class.php

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

2,340 lines 78.4 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 FeedWordPress::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 $old_post = $q->post;
1248 endwhile;
1249 endif;
1250
1251 if (is_null($old_post)) : // No post with this guid
1252 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$guid.'] "'.$this->entry->get_title().'" is a NEW POST.');
1253 $this->_wp_id = NULL;
1254 $this->_freshness = 2; // New content
1255 else :
1256 // Presume there is nothing new until we find
1257 // something new.
1258 $updated = false;
1259 $live = false;
1260
1261 // Pull the list of existing revisions to get
1262 // timestamps.
1263 $revisions = wp_get_post_revisions($old_post->ID);
1264 foreach ($revisions as $rev) :
1265 $revisions_ts[] = mysql2date('G', $rev->post_modified_gmt);
1266 endforeach;
1267
1268 $revisions_ts[] = mysql2date('G', $old_post->post_modified_gmt);
1269 $last_rev_ts = end($revisions_ts);
1270 $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
1271
1272 // If we have an explicit updated timestamp,
1273 // check that against existing stamps.
1274 if (!is_null($updated_ts)) :
1275 $updated = !in_array($updated_ts, $revisions_ts);
1276
1277 // If this a newer revision, make it go
1278 // live. If an older one, just record
1279 // the contents.
1280 $live = ($updated and ($updated_ts > $last_rev_ts));
1281 endif;
1282
1283 // This is a revision we haven't seen before, judging by the date.
1284
1285 $updatedReason = NULL;
1286 if ($updated) :
1287 $updatedReason = preg_replace(
1288 "/\s+/", " ",
1289 'has been marked with a new timestamp ('
1290 .date('Y-m-d H:i:s', $updated_ts)
1291 ." > "
1292 .date('Y-m-d H:i:s', $last_rev_ts)
1293 .')'
1294 );
1295
1296 // The date does not indicate a new revision, so
1297 // let's check the hash.
1298 else :
1299 // Or the hash...
1300 $hash = $this->update_hash();
1301 $seen = $this->stored_hashes($old_post->ID);
1302 if (count($seen) > 0) :
1303 $updated = !in_array($hash, $seen); // Not seen yet?
1304 else :
1305 $updated = true; // Can't find syndication meta-data
1306 endif;
1307
1308 if ($updated and FeedWordPress::diagnostic_on('feed_items:freshness:reasons')) :
1309 // In the absence of definitive
1310 // timestamp information, we
1311 // just have to assume that a
1312 // hash we haven't seen before
1313 // is a newer version.
1314 $live = true;
1315
1316 $updatedReason = ' has a not-yet-seen update hash: '
1317 .MyPHP::val($hash)
1318 .' not in {'
1319 .implode(", ", array_map(array('FeedWordPress', 'val'), $seen))
1320 .'}. Basis: '
1321 .MyPHP::val(array_keys($this->update_hash(false)));
1322 endif;
1323 endif;
1324
1325 $frozen = false;
1326 if ($updated) : // Ignore if the post is frozen
1327 $frozen = ('yes' == $this->link->setting('freeze updates', 'freeze_updates', NULL));
1328 if (!$frozen) :
1329 $frozen_values = get_post_custom_values('_syndication_freeze_updates', $old_post->ID);
1330 $frozen = (count($frozen_values) > 0 and 'yes' == $frozen_values[0]);
1331
1332 if ($frozen) :
1333 $updatedReason = ' IS BLOCKED FROM BEING UPDATED BY A UPDATE LOCK ON THIS POST, EVEN THOUGH IT '.$updatedReason;
1334 endif;
1335 else :
1336 $updatedReason = ' IS BLOCKED FROM BEING UPDATED BY A FEEDWORDPRESS UPDATE LOCK, EVEN THOUGH IT '.$updatedReason;
1337 endif;
1338 endif;
1339 $live = ($live and !$frozen);
1340
1341 if ($updated) :
1342 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$guid.'] "'.$this->entry->get_title().'" is an update of an existing post.');
1343 if (!is_null($updatedReason)) :
1344 $updatedReason = preg_replace('/\s+/', ' ', $updatedReason);
1345 FeedWordPress::diagnostic('feed_items:freshness:reasons', 'Item ['.$guid.'] "'.$this->entry->get_title().'" '.$updatedReason);
1346 endif;
1347
1348 $this->_freshness = apply_filters('syndicated_item_freshness', ($live ? 1 : -1), $updated, $frozen, $updated_ts, $last_rev_ts, $this);
1349
1350 $this->_wp_id = $old_post->ID;
1351 $this->_wp_post = $old_post;
1352
1353 // We want this to keep a running list of all the
1354 // processed update hashes.
1355 $this->post['meta']['syndication_item_hash'] = array_merge(
1356 $this->stored_hashes(),
1357 array($this->update_hash())
1358 );
1359 else :
1360 FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$guid.'] "'.$this->entry->get_title().'" is a duplicate of an existing post.');
1361 $this->_freshness = 0; // Same old, same old
1362 $this->_wp_id = $old_post->ID;
1363 endif;
1364 endif;
1365 endif;
1366
1367 switch ($format) :
1368 case 'status' :
1369 switch ($this->_freshness) :
1370 case -1:
1371 $ret = 'stored';
1372 break;
1373 case 0:
1374 $ret = NULL;
1375 break;
1376 case 1:
1377 $ret = 'updated';
1378 break;
1379 case 2:
1380 default:
1381 $ret = 'new';
1382 break;
1383 endswitch;
1384 break;
1385 case 'number' :
1386 default :
1387 $ret = $this->_freshness;
1388 endswitch;
1389
1390
1391 return $ret;
1392 } /* SyndicatedPost::freshness () */
1393
1394 function has_fresh_content () {
1395 return ( ! $this->filtered() and $this->freshness() != 0 );
1396 } /* SyndicatedPost::has_fresh_content () */
1397
1398 function this_revision_needs_original_post ($freshness = NULL) {
1399 if (is_null($freshness)) :
1400 $freshness = $this->freshness();
1401 endif;
1402 return ( $freshness >= 2 );
1403 }
1404
1405 function this_revision_is_current ($freshness = NULL) {
1406 if (is_null($freshness)) :
1407 $freshness = $this->freshness();
1408 endif;
1409 return ( $freshness >= 1 );
1410 } /* SyndicatedPost::this_revision_is_current () */
1411
1412 function fresh_content_is_update () {
1413 return ($this->freshness() < 2);
1414 } /* SyndicatedPost::fresh_content_is_update () */
1415
1416 function fresh_storage_diagnostic () {
1417 $ret = NULL;
1418 switch ($this->freshness()) :
1419 case -1 :
1420 $ret = 'Storing alternate revision of existing post # '.$this->wp_id().', "'.$this->post['post_title'].'"';
1421 break;
1422 case 1 :
1423 $ret = 'Updating existing post # '.$this->wp_id().', "'.$this->post['post_title'].'"';
1424 break;
1425 case 2 :
1426 default :
1427 $ret = 'Inserting new post "'.$this->post['post_title'].'"';
1428 break;
1429 endswitch;
1430 return $ret;
1431 } /* SyndicatedPost::fresh_storage_diagnostic() */
1432
1433 function fresh_storage_hook () {
1434 $ret = NULL;
1435 switch ($this->freshness()) :
1436 case -1 :
1437 case 1 :
1438 $ret = 'update_syndicated_item';
1439 break;
1440 case 2 :
1441 default :
1442 $ret = 'post_syndicated_item';
1443 break;
1444 endswitch;
1445 return $ret;
1446 } /* SyndicatedPost::fresh_storage_hook () */
1447
1448 #################################################
1449 #### INTERNAL STORAGE AND MANAGEMENT METHODS ####
1450 #################################################
1451
1452 function wp_id () {
1453 if ($this->filtered()) : // This should never happen.
1454 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1455 endif;
1456
1457 if (is_null($this->_wp_id) and is_null($this->_freshness)) :
1458 $fresh = $this->freshness(); // sets WP DB id in the process
1459 endif;
1460 return $this->_wp_id;
1461 }
1462
1463 public function store () {
1464 global $wpdb;
1465
1466 if ($this->filtered()) : // This should never happen.
1467 FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
1468 endif;
1469
1470 $freshness = $this->freshness();
1471 if ($this->has_fresh_content()) :
1472 # -- Look up, or create, numeric ID for author
1473 $this->post['post_author'] = $this->author_id (
1474 $this->link->setting('unfamiliar author', 'unfamiliar_author', 'create')
1475 );
1476
1477 if (is_null($this->post['post_author'])) :
1478 FeedWordPress::diagnostic('feed_items:rejected', 'Filtered out item ['.$this->guid().'] without syndication: no author available');
1479 $this->post = NULL;
1480 endif;
1481 endif;
1482
1483 // We have to check again in case post has been filtered during
1484 // the author_id lookup
1485 if ($this->has_fresh_content()) :
1486 $mapping = apply_filters('syndicated_post_terms_mapping', array(
1487 'category' => array('abbr' => 'cats', 'unfamiliar' => 'category', 'domain' => array('category', 'post_tag')),
1488 'post_tag' => array('abbr' => 'tags', 'unfamiliar' => 'post_tag', 'domain' => array('post_tag')),
1489 ), $this);
1490
1491 $termSet = array(); $valid = null;
1492 foreach ($this->feed_terms as $what => $anTerms) :
1493 // Default to using the inclusive procedures (for cats) rather than exclusive (for inline tags)
1494 $taxes = (isset($mapping[$what]) ? $mapping[$what] : $mapping['category']);
1495 $unfamiliar = $taxes['unfamiliar'];
1496
1497 if (!is_null($this->post)) : // Not filtered out yet
1498 # -- Look up, or create, numeric ID for categories
1499 $taxonomies = $this->link->setting("match/".$taxes['abbr'], 'match_'.$taxes['abbr'], $taxes['domain']);
1500
1501 // Eliminate dummy variables
1502 $taxonomies = array_filter($taxonomies, 'remove_dummy_zero');
1503
1504 // Allow FWP add-on filters to control the taxonomies we use to search for a term
1505 $taxonomies = apply_filters("syndicated_post_terms_match", $taxonomies, $what, $this);
1506 $taxonomies = apply_filters("syndicated_post_terms_match_${what}", $taxonomies, $this);
1507
1508 // Allow FWP add-on filters to control with greater precision what happens on unmatched
1509 $unmatched = apply_filters("syndicated_post_terms_unfamiliar",
1510 $this->link->setting(
1511 "unfamiliar {$unfamiliar}",
1512 "unfamiliar_{$unfamiliar}",
1513 'create:'.$unfamiliar
1514 ),
1515 $what,
1516 $this
1517 );
1518
1519 $terms = $this->category_ids (
1520 $anTerms,
1521 $unmatched,
1522 /*taxonomies=*/ $taxonomies,
1523 array(
1524 'singleton' => false, // I don't like surprises
1525 'filters' => true,
1526 )
1527 );
1528
1529 if (is_null($terms) or is_null($termSet)) :
1530 // filtered out -- no matches
1531 else :
1532 $valid = true;
1533
1534 // filter mode off, or at least one match
1535 foreach ($terms as $tax => $term_ids) :
1536 if (!isset($termSet[$tax])) :
1537 $termSet[$tax] = array();
1538 endif;
1539 $termSet[$tax] = array_merge($termSet[$tax], $term_ids);
1540 endforeach;
1541 endif;
1542 endif;
1543 endforeach;
1544
1545 if (is_null($valid)) : // Plonked
1546 $this->post = NULL;
1547 else : // We can proceed
1548 $this->post['tax_input'] = array();
1549 foreach ($termSet as $tax => $term_ids) :
1550 if (!isset($this->post['tax_input'][$tax])) :
1551 $this->post['tax_input'][$tax] = array();
1552 endif;
1553 $this->post['tax_input'][$tax] = array_merge(
1554 $this->post['tax_input'][$tax],
1555 $term_ids
1556 );
1557 endforeach;
1558
1559 // Now let's add on the feed and global presets
1560 foreach ($this->preset_terms as $tax => $term_ids) :
1561 if (!isset($this->post['tax_input'][$tax])) :
1562 $this->post['tax_input'][$tax] = array();
1563 endif;
1564
1565 $this->post['tax_input'][$tax] = array_merge (
1566 $this->post['tax_input'][$tax],
1567 $this->category_ids (
1568 /*terms=*/ $term_ids,
1569 /*unfamiliar=*/ 'create:'.$tax, // These are presets; for those added in a tagbox editor, the tag may not yet exist
1570 /*taxonomies=*/ array($tax),
1571 array(
1572 'singleton' => true,
1573 ))
1574 );
1575 endforeach;
1576 endif;
1577 endif;
1578
1579 // We have to check again in case the post has been filtered
1580 // during the category/tags/taxonomy terms lookup
1581 if ($this->has_fresh_content()) :
1582 // Filter some individual fields
1583
1584 // If there already is a post slug (from syndication or by manual
1585 // editing) don't cause WP to overwrite it by sending in a NULL
1586 // post_name. Props Chris Fritz 2012-11-28.
1587 $post_name = (is_null($this->_wp_post) ? NULL : $this->_wp_post->post_name);
1588
1589 // Allow filters to set post slug. Props niska.
1590 $post_name = apply_filters('syndicated_post_slug', $post_name, $this);
1591 if (!empty($post_name)) :
1592 $this->post['post_name'] = $post_name;
1593 endif;
1594
1595 $this->post = apply_filters('syndicated_post', $this->post, $this);
1596
1597 // Allow for feed-specific syndicated_post filters.
1598 $this->post = apply_filters(
1599 "syndicated_post_".$this->link->uri(),
1600 $this->post,
1601 $this
1602 );
1603 endif;
1604
1605 // Hook in early to make sure these get inserted if at all possible
1606 add_action(
1607 /*hook=*/ 'transition_post_status',
1608 /*callback=*/ array($this, 'add_rss_meta'),
1609 /*priority=*/ -10000, /* very early */
1610 /*arguments=*/ 3
1611 );
1612
1613 $ret = false;
1614 if ($this->has_fresh_content()) :
1615 $diag = $this->fresh_storage_diagnostic();
1616 if (!is_null($diag)) :
1617 FeedWordPress::diagnostic('syndicated_posts', $diag);
1618 endif;
1619
1620 $this->insert_post(/*update=*/ $this->fresh_content_is_update(), $this->freshness());
1621
1622 $hook = $this->fresh_storage_hook();
1623 if (!is_null($hook)) :
1624 do_action($hook, $this->wp_id(), $this);
1625 endif;
1626
1627 $ret = $this->freshness('status');
1628 endif;
1629
1630 // If this is a legit, non-filtered post, tag it as found on the
1631 // feed regardless of fresh or stale status
1632 if (!$this->filtered()) :
1633 $key = '_feedwordpress_retire_me_' . $this->link->id;
1634 delete_post_meta($this->wp_id(), $key);
1635
1636 $status = get_post_field('post_status', $this->wp_id());
1637 if ('fwpretired'==$status and $this->link->is_non_incremental()) :
1638 FeedWordPress::diagnostic('syndicated_posts', "Un-retiring previously retired post # ".$this->wp_id()." due to re-appearance on non-incremental feed.");
1639 set_post_field('post_status', $this->post['post_status'], $this->wp_id());
1640 wp_transition_post_status($this->post['post_status'], $status, $this->post);
1641 elseif ('fwpzapped'==$status) :
1642 // Set this new revision up to be
1643 // blanked on the next update.
1644 add_post_meta($this->wp_id(), '_feedwordpress_zapped_blank_me', 2, /*single=*/ true);
1645 endif;
1646 endif;
1647
1648 // Remove add_rss_meta hook
1649 remove_action(
1650 /*hook=*/ 'transition_post_status',
1651 /*callback=*/ array($this, 'add_rss_meta'),
1652 /*priority=*/ -10000, /* very early */
1653 /*arguments=*/ 3
1654 );
1655
1656 return $ret;
1657 } /* function SyndicatedPost::store () */
1658
1659 function insert_post ($update = false, $freshness = 2) {
1660 global $wpdb;
1661
1662 $dbpost = $this->normalize_post(/*new=*/ true);
1663
1664 $ret = null;
1665
1666 if (!is_null($dbpost)) :
1667 $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
1668
1669 // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
1670 add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1671
1672 // Kludge to prevent kses filters from stripping the
1673 // content of posts when updating without a logged in
1674 // user who has `unfiltered_html` capability.
1675 $mungers = array('wp_filter_kses', 'wp_filter_post_kses');
1676 $removed = array();
1677 foreach ($mungers as $munger) :
1678 if (has_filter('content_save_pre', $munger)) :
1679 remove_filter('content_save_pre', $munger);
1680 $removed[] = $munger;
1681 endif;
1682 endforeach;
1683
1684 if ($update and function_exists('get_post_field')) :
1685 // Don't munge status fields that the user may
1686 // have reset manually
1687 $doNotMunge = array('post_status', 'comment_status', 'ping_status');
1688
1689 foreach ($doNotMunge as $field) :
1690 $dbpost[$field] = get_post_field($field, $this->wp_id());
1691 endforeach;
1692 endif;
1693
1694 // WP3's wp_insert_post scans current_user_can() for the
1695 // tax_input, with no apparent way to override. Ugh.
1696 add_action(
1697 /*hook=*/ 'transition_post_status',
1698 /*callback=*/ array($this, 'add_terms'),
1699 /*priority=*/ -10001, /* very early */
1700 /*arguments=*/ 3
1701 );
1702
1703 // WP3 appears to override whatever you give it for
1704 // post_modified. Ugh.
1705 add_action(
1706 /*hook=*/ 'transition_post_status',
1707 /*callback=*/ array($this, 'fix_post_modified_ts'),
1708 /*priority=*/ -10000, /* very early */
1709 /*arguments=*/ 3
1710 );
1711
1712 if ($update) :
1713 $this->post['ID'] = $this->wp_id();
1714 $dbpost['ID'] = $this->post['ID'];
1715 endif;
1716
1717 // O.K., is this a new post? If so, we need to create
1718 // the basic post record before we do anything else.
1719 if ($this->this_revision_needs_original_post()) :
1720 // *sigh*, for handling inconsistent slash expectations < 3.6
1721 $sdbpost = $this->db_sanitize_post($dbpost);
1722
1723 // Go ahead and insert the first post record to
1724 // anchor the revision history.
1725
1726 $this->_wp_id = wp_insert_post($sdbpost, /*return wp_error=*/ true);
1727
1728 $dbpost['ID'] = $this->_wp_id;
1729 endif;
1730
1731 // Sanity check: if the attempt to insert post
1732 // returned an error, then feeding that error
1733 // object in to _wp_put_post_revision() would
1734 // cause a fatal error. Better to break out.
1735 if (!is_wp_error($this->_wp_id)) :
1736 // Now that we've made sure the original exists, insert
1737 // this version here as a revision.
1738 $revision_id = _wp_put_post_revision($dbpost, /*autosave=*/ false);
1739
1740 if (!$this->this_revision_needs_original_post()) :
1741
1742 if ($this->this_revision_is_current()) :
1743
1744 wp_restore_post_revision($revision_id);
1745
1746 else :
1747
1748 // If we do not activate this revision, then the
1749 // add_rss_meta will not be called, which is
1750 // more or less as it should be, but that means
1751 // we have to actively record this revision's
1752 // update hash from here.
1753 $postId = $this->post['ID'];
1754 $key = 'syndication_item_hash';
1755 $hash = $this->update_hash();
1756 FeedWordPress::diagnostic('syndicated_posts:meta_data', "Adding post meta-datum to post [$postId]: [$key] = ".FeedWordPress::val($hash, /*no newlines=*/ true));
1757 add_post_meta( $postId, $key, $hash, /*unique=*/ false );
1758 endif;
1759 endif;
1760 endif;
1761
1762 remove_action(
1763 /*hook=*/ 'transition_post_status',
1764 /*callback=*/ array($this, 'add_terms'),
1765 /*priority=*/ -10001, /* very early */
1766 /*arguments=*/ 3
1767 );
1768
1769 remove_action(
1770 /*hook=*/ 'transition_post_status',
1771 /*callback=*/ array($this, 'fix_post_modified_ts'),
1772 /*priority=*/ -10000, /* very early */
1773 /*arguments=*/ 3
1774 );
1775
1776 // Turn off ridiculous fucking kludges #1 and #2
1777 remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
1778 foreach ($removed as $filter) :
1779 add_filter('content_save_pre', $filter);
1780 endforeach;
1781
1782 $this->validate_post_id($dbpost, $update, array(__CLASS__, __FUNCTION__));
1783
1784 $ret = $this->_wp_id;
1785 endif;
1786 return $ret;
1787 } /* function SyndicatedPost::insert_post () */
1788
1789 function insert_new () {
1790 $this->insert_post(/*update=*/ false, 1);
1791 } /* SyndicatedPost::insert_new() */
1792
1793 function update_existing () {
1794 $this->insert_post(/*update=*/ true, 2);
1795 } /* SyndicatedPost::update_existing() */
1796
1797 /**
1798 * SyndicatedPost::normalize_post()
1799 *
1800 * @param bool $new If true, this post is to be inserted anew. If false, it is an update of an existing post.
1801 * @return array A normalized representation of the post ready to be inserted into the database or sent to the WordPress API functions
1802 */
1803 function normalize_post ($new = true) {
1804 global $wpdb;
1805
1806 $out = $this->post;
1807
1808 $fullPost = $out['post_title'].$out['post_content'];
1809 $fullPost .= (isset($out['post_excerpt']) ? $out['post_excerpt'] : '');
1810 if (strlen($fullPost) < 1) :
1811 // FIXME: Option for filtering out empty posts
1812 endif;
1813 if (strlen($out['post_title'])==0) :
1814 $offset = (int) get_option('gmt_offset') * 60 * 60;
1815 if (isset($this->post['meta']['syndication_source'])) :
1816 $source_title = $this->post['meta']['syndication_source'];
1817 else :
1818 $feed_url = parse_url($this->post['meta']['syndication_feed']);
1819 $source_title = $feed_url['host'];
1820 endif;
1821
1822 $out['post_title'] = $source_title
1823 .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
1824 // FIXME: Option for what to fill a blank title with...
1825 endif;
1826
1827 // Normalize the guid if necessary.
1828 $out['guid'] = SyndicatedPost::normalize_guid($out['guid']);
1829
1830 return $out;
1831 }
1832
1833 public function db_sanitize_post_check_encoding ($out) {
1834 // Check encoding recursively: every string field needs to be checked
1835 // for character encoding issues. This is a bit problematic because we
1836 // *should* be using DB_CHARSET, but DB_CHARSET sometimes has values
1837 // that work for MySQL but not for PHP mb_check_encoding. So instead
1838 // we must rely on WordPress setting blog_charset and hope that the user
1839 // has got their database encoding set up to roughly match
1840 $charset = get_option('blog_charset', 'utf8');
1841
1842 foreach ($out as $key => $value) :
1843 if (is_string($value)) :
1844
1845 if (!function_exists('mb_check_encoding') or mb_check_encoding($value, $charset)) :
1846 $out[$key] = $value;
1847 else :
1848 $fromCharset = mb_detect_encoding($value, mb_detect_order(), /*strict=*/ true);
1849 $out[$key] = mb_convert_encoding($value, $charset, $fromCharset);
1850 endif;
1851
1852 elseif (is_array($value)) :
1853 $out[$key] = $this->db_sanitize_post_check_encoding($value);
1854
1855 else :
1856 $out[$key] = $value;
1857 endif;
1858
1859 endforeach;
1860
1861 return $out;
1862 } /* SyndicatedPost::db_sanitize_post_check_encoding () */
1863
1864 function db_sanitize_post ($out) {
1865 global $wp_db_version;
1866
1867 $out = $this->db_sanitize_post_check_encoding($out);
1868
1869 // < 3.6. Core API, including `wp_insert_post()`, expects
1870 // properly slashed data. If `wp_slash()` exists, then
1871 // this is after the big change-over in how data slashing
1872 // was handled.
1873 if (!function_exists('wp_slash')) :
1874
1875 foreach ($out as $key => $value) :
1876 if (is_string($value)) :
1877 $out[$key] = esc_sql($value);
1878 else :
1879 $out[$key] = $value;
1880 endif;
1881 endforeach;
1882
1883 // For revisions [@23416,@23554), core API expects
1884 // unslashed data. Cf. <https://core.trac.wordpress.org/browser/trunk/wp-includes/post.php?rev=23416>
1885 // NOOP for those revisions.
1886
1887 // In revisions @23554 to present, `wp_insert_post()`
1888 // expects slashed data once again.
1889 // Cf. <https://core.trac.wordpress.org/changeset/23554/trunk/wp-includes/post.php?contextall=1>
1890 // But at least now we can use the wp_slash API function to do that.
1891 // Hooray.
1892
1893 elseif ($wp_db_version >= 23524) :
1894
1895 $out = wp_slash($out);
1896
1897 endif;
1898
1899 return $out;
1900 }
1901
1902 /**
1903 * SyndicatedPost::validate_post_id()
1904 *
1905 * @param array $dbpost An array representing the post we attempted to insert or update
1906 * @param mixed $ns A string or array representing the namespace (class, method) whence this method was called.
1907 */
1908 function validate_post_id ($dbpost, $is_update, $ns) {
1909 if (is_array($ns)) : $ns = implode('::', $ns);
1910 else : $ns = (string) $ns; endif;
1911
1912 // This should never happen.
1913 if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
1914 $verb = ($is_update ? 'update existing' : 'insert new');
1915 $guid = $this->guid();
1916 $url = $this->permalink();
1917 $feed = $this->link->uri(array('add_params' => true));
1918
1919 // wp_insert_post failed. Diagnostics, or barf up a critical bug
1920 // notice if we are in debug mode.
1921 $mesg = "Failed to $verb item [$guid]. WordPress API returned no valid post ID.\n"
1922 ."\t\tID = ".serialize($this->_wp_id)."\n"
1923 ."\t\tURL = ".MyPHP::val($url)
1924 ."\t\tFeed = ".MyPHP::val($feed);
1925
1926 FeedWordPress::diagnostic('updated_feeds:errors', "WordPress API error: $mesg");
1927 FeedWordPress::diagnostic('feed_items:rejected', $mesg);
1928
1929 $mesg = <<<EOM
1930 The WordPress API returned an invalid post ID
1931 when FeedWordPress tried to $verb item $guid
1932 [URL: $url]
1933 from the feed at $feed
1934
1935 $ns::_wp_id
1936 EOM;
1937 FeedWordPress::noncritical_bug(
1938 /*message=*/ $mesg,
1939 /*var =*/ array(
1940 "\$this->_wp_id" => $this->_wp_id,
1941 "\$dbpost" => $dbpost,
1942 ),
1943 /*line # =*/ __LINE__, /*filename=*/ __FILE__
1944 );
1945 endif;
1946 } /* SyndicatedPost::validate_post_id() */
1947
1948 /**
1949 * SyndicatedPost::fix_revision_meta() - Ensures that we get the meta
1950 * data (authorship, guid, etc.) that we want when storing revisions of
1951 * a syndicated post.
1952 *
1953 * In their infinite wisdom, the WordPress coders seem to have made it
1954 * completely impossible for a plugin that uses wp_insert_post() to set
1955 * certain meta-data (such as the author) when you store an old revision
1956 * of an updated post. Instead, it uses the WordPress defaults (= cur.
1957 * active user ID if the process is running with a user logged in, or
1958 * = #0 if there is no user logged in). This results in bogus authorship
1959 * data for revisions that are syndicated from off the feed, unless we
1960 * use a ridiculous kludge like this to end-run the munging of meta-data
1961 * by _wp_put_post_revision.
1962 *
1963 * @param int $revision_id The revision ID to fix up meta-data
1964 */
1965 function fix_revision_meta ($revision_id) {
1966 global $wpdb;
1967
1968 $post_author = (int) $this->post['post_author'];
1969
1970 $revision_id = (int) $revision_id;
1971
1972 // Let's fix the author.
1973 set_post_field('post_author', $this->post['post_author'], $revision_id);
1974
1975 // Let's fix the GUID to a dummy URL with the update hash.
1976 set_post_field('guid', 'http://feedwordpress.radgeek.com/?rev='.$this->update_hash(), $revision_id);
1977
1978 // Let's fire an event for add-ons and filters
1979 do_action('syndicated_post_fix_revision_meta', $revision_id, $this);
1980
1981 } /* SyndicatedPost::fix_revision_meta () */
1982
1983 /**
1984 * SyndicatedPost::add_terms() -- if FeedWordPress is processing an
1985 * automatic update, that generally means that wp_insert_post() is being
1986 * called under the user credentials of whoever is viewing the blog at
1987 * the time -- which usually means no user at all. But wp_insert_post()
1988 * checks current_user_can() before assigning any of the terms in a
1989 * post's tax_input structure -- which is unfortunate, since
1990 * current_user_can() always returns FALSE when there is no current user
1991 * logged in. Meaning that automatic updates get no terms assigned.
1992 *
1993 * So, wp_insert_post() is not going to do the term assignments for us.
1994 * If you want something done right....
1995 *
1996 * @param string $new_status Unused action parameter.
1997 * @param string $old_status Unused action parameter.
1998 * @param object $post The database record for the post just inserted.
1999 */
2000 function add_terms ($new_status, $old_status, $post) {
2001
2002 if ($new_status!='inherit') : // Bail if we are creating a revision.
2003 if ( is_array($this->post) and isset($this->post['tax_input']) and is_array($this->post['tax_input']) ) :
2004 foreach ($this->post['tax_input'] as $taxonomy => $terms) :
2005 if (is_array($terms)) :
2006 $terms = array_filter($terms); // strip out empties
2007 endif;
2008
2009 $res = wp_set_post_terms(
2010 /*post_id=*/ $post->ID,
2011 /*terms=*/ $terms,
2012 /*taxonomy=*/ $taxonomy
2013 );
2014
2015 FeedWordPress::diagnostic(
2016 'syndicated_posts:categories',
2017 'Category: post('.json_encode($post->ID).') '.$taxonomy
2018 .' := '
2019 .json_encode($terms)
2020 .' / result: '
2021 .json_encode($res)
2022 );
2023
2024 endforeach;
2025 endif;
2026 endif;
2027
2028 } /* SyndicatedPost::add_terms () */
2029
2030 /**
2031 * SyndicatedPost::fix_post_modified_ts() -- We would like to set
2032 * post_modified and post_modified_gmt to reflect the value of
2033 * <atom:updated> or equivalent elements on the feed. Unfortunately,
2034 * wp_insert_post() refuses to acknowledge explicitly-set post_modified
2035 * fields and overwrites them, either with the post_date (if new) or the
2036 * current timestamp (if updated).
2037 *
2038 * So, wp_insert_post() is not going to do the last-modified assignments
2039 * for us. If you want something done right....
2040 *
2041 * @param string $new_status Unused action parameter.
2042 * @param string $old_status Unused action parameter.
2043 * @param object $post The database record for the post just inserted.
2044 */
2045 function fix_post_modified_ts ($new_status, $old_status, $post) {
2046 global $wpdb;
2047 if ($new_status!='inherit') : // Bail if we are creating a revision.
2048 $wpdb->update( $wpdb->posts, /*data=*/ array(
2049 'post_modified' => $this->post['post_modified'],
2050 'post_modified_gmt' => $this->post['post_modified_gmt'],
2051 ), /*where=*/ array('ID' => $post->ID) );
2052 endif;
2053 } /* SyndicatedPost::fix_post_modified_ts () */
2054
2055 /**
2056 * SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
2057 * using the space for custom keys. The set of keys and values to add is
2058 * specified by the keys and values of $post['meta']. This is used to
2059 * store anything that the WordPress user might want to access from a
2060 * template concerning the post's original source that isn't provided
2061 * for by standard WP meta-data (i.e., any interesting data about the
2062 * syndicated post other than author, title, timestamp, categories, and
2063 * guid). It's also used to hook into WordPress's support for
2064 * enclosures.
2065 *
2066 * @param string $new_status Unused action parameter.
2067 * @param string $old_status Unused action parameter.
2068 * @param object $post The database record for the post just inserted.
2069 */
2070 function add_rss_meta ($new_status, $old_status, $post) {
2071 global $wpdb;
2072 if ($new_status!='inherit') : // Bail if we are creating a revision.
2073 FeedWordPress::diagnostic('syndicated_posts:meta_data', 'Adding post meta-data: {'.implode(", ", array_keys($this->post['meta'])).'}');
2074
2075 if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
2076 $postId = $post->ID;
2077
2078 // Aggregated posts should NOT send out pingbacks.
2079 // WordPress 2.1-2.2 claim you can tell them not to
2080 // using $post_pingback, but they don't listen, so we
2081 // make sure here.
2082 $result = $wpdb->query("
2083 DELETE FROM $wpdb->postmeta
2084 WHERE post_id='$postId' AND meta_key='_pingme'
2085 ");
2086
2087 foreach ( $this->post['meta'] as $key => $values ) :
2088 $eKey = esc_sql($key);
2089
2090 // If this is an update, clear out the old
2091 // values to avoid duplication.
2092 $result = $wpdb->query("
2093 DELETE FROM $wpdb->postmeta
2094 WHERE post_id='$postId' AND meta_key='$eKey'
2095 ");
2096
2097 // Allow for either a single value or an array
2098 if (!is_array($values)) $values = array($values);
2099 foreach ( $values as $value ) :
2100 FeedWordPress::diagnostic('syndicated_posts:meta_data', "Adding post meta-datum to post [$postId]: [$key] = ".MyPHP::val($value, /*no newlines=*/ true));
2101 add_post_meta($postId, $key, $value, /*unique=*/ false);
2102 endforeach;
2103 endforeach;
2104 endif;
2105 endif;
2106 } /* SyndicatedPost::add_rss_meta () */
2107
2108 /**
2109 * SyndicatedPost::author_id (): get the ID for an author name from
2110 * the feed. Create the author if necessary.
2111 *
2112 * @param string $unfamiliar_author
2113 *
2114 * @return NULL|int The numeric ID of the author to attribute the post to
2115 * NULL if the post should be filtered out.
2116 */
2117 function author_id ($unfamiliar_author = 'create') {
2118 global $wpdb;
2119
2120 $a = $this->named['author'];
2121
2122 $source = $this->source();
2123 $forbidden = apply_filters('feedwordpress_forbidden_author_names',
2124 array('admin', 'administrator', 'www', 'root'));
2125
2126 // Prepare the list of candidates to try for author name: name from
2127 // feed, original source title (if any), immediate source title live
2128 // from feed, subscription title, prettied version of feed homepage URL,
2129 // prettied version of feed URL, or, failing all, use "unknown author"
2130 // as last resort
2131
2132 $candidates = array();
2133 $candidates[] = $a['name'];
2134 if (!is_null($source)) : $candidates[] = $source['title']; endif;
2135 $candidates[] = $this->link->name(/*fromFeed=*/ true);
2136 $candidates[] = $this->link->name(/*fromFeed=*/ false);
2137 if (strlen($this->link->homepage()) > 0) : $candidates[] = feedwordpress_display_url($this->link->homepage()); endif;
2138 $candidates[] = feedwordpress_display_url($this->link->uri());
2139 $candidates[] = 'unknown author';
2140
2141 // Pick the first one that works from the list, screening against empty
2142 // or forbidden names.
2143
2144 $author = NULL;
2145 while (is_null($author) and ($candidate = each($candidates))) :
2146 if (!is_null($candidate['value'])
2147 and (strlen(trim($candidate['value'])) > 0)
2148 and !in_array(strtolower(trim($candidate['value'])), $forbidden)) :
2149 $author = $candidate['value'];
2150 endif;
2151 endwhile;
2152
2153 $email = (isset($a['email']) ? $a['email'] : NULL);
2154 $authorUrl = (isset($a['uri']) ? $a['uri'] : NULL);
2155
2156
2157 $hostUrl = $this->link->homepage();
2158 if (is_null($hostUrl) or (strlen($hostUrl) < 0)) :
2159 $hostUrl = $this->link->uri();
2160 endif;
2161
2162 $match_author_by_email = !('yes' == get_option("feedwordpress_do_not_match_author_by_email"));
2163 if ($match_author_by_email and !FeedWordPress::is_null_email($email)) :
2164 $test_email = $email;
2165 else :
2166 $test_email = NULL;
2167 endif;
2168
2169 // Never can be too careful...
2170 $login = sanitize_user($author, /*strict=*/ true);
2171
2172 // Possible for, e.g., foreign script author names
2173 if (strlen($login) < 1) :
2174 // No usable characters in author name for a login.
2175 // (Sometimes results from, e.g., foreign scripts.)
2176 //
2177 // We just need *something* in Western alphanumerics,
2178 // so let's try the domain name.
2179 //
2180 // Uniqueness will be guaranteed below if necessary.
2181
2182 $url = parse_url($hostUrl);
2183
2184 $login = sanitize_user($url['host'], /*strict=*/ true);
2185 if (strlen($login) < 1) :
2186 // This isn't working. Frak it.
2187 $login = 'syndicated';
2188 endif;
2189 endif;
2190
2191 $login = apply_filters('pre_user_login', $login);
2192
2193 $nice_author = sanitize_title($author);
2194 $nice_author = apply_filters('pre_user_nicename', $nice_author);
2195
2196 $reg_author = esc_sql(preg_quote($author));
2197 $author = esc_sql($author);
2198 $email = esc_sql($email);
2199 $test_email = esc_sql($test_email);
2200 $authorUrl = esc_sql($authorUrl);
2201
2202 // Check for an existing author rule....
2203 if (isset($this->link->settings['map authors']['name']['*'])) :
2204 $author_rule = $this->link->settings['map authors']['name']['*'];
2205 elseif (isset($this->link->settings['map authors']['name'][strtolower(trim($author))])) :
2206 $author_rule = $this->link->settings['map authors']['name'][strtolower(trim($author))];
2207 else :
2208 $author_rule = NULL;
2209 endif;
2210
2211 // User name is mapped to a particular author. If that author ID exists, use it.
2212 if (is_numeric($author_rule) and get_userdata((int) $author_rule)) :
2213 $id = (int) $author_rule;
2214
2215 // User name is filtered out
2216 elseif ('filter' == $author_rule) :
2217 $id = NULL;
2218
2219 else :
2220 // Check the database for an existing author record that might fit
2221
2222 // First try the user core data table.
2223 $id = $wpdb->get_var(
2224 "SELECT ID FROM $wpdb->users
2225 WHERE TRIM(LCASE(display_name)) = TRIM(LCASE('$author'))
2226 OR TRIM(LCASE(user_login)) = TRIM(LCASE('$author'))
2227 OR (
2228 LENGTH(TRIM(LCASE(user_email))) > 0
2229 AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
2230 )");
2231
2232 // If that fails, look for aliases in the user meta data table
2233 if (is_null($id)) :
2234 $id = $wpdb->get_var(
2235 "SELECT user_id FROM $wpdb->usermeta
2236 WHERE
2237 (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
2238 OR (
2239 meta_key = 'description'
2240 AND TRIM(LCASE(meta_value))
2241 RLIKE CONCAT(
2242 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
2243 TRIM(LCASE('$reg_author')),
2244 '( |\\t|\\r)*(\\n|\$)'
2245 )
2246 )
2247 ");
2248 endif;
2249
2250 // ... if you don't find one, then do what you need to do
2251 if (is_null($id)) :
2252 if ($unfamiliar_author === 'create') :
2253 $userdata = array();
2254
2255 // WordPress 3 is going to pitch a fit if we attempt to register
2256 // more than one user account with an empty e-mail address, so we
2257 // need *something* here. Ugh.
2258 if (strlen($email) == 0 or FeedWordPress::is_null_email($email)) :
2259 $url = parse_url($hostUrl);
2260 $email = $nice_author.'@'.$url['host'];
2261 endif;
2262
2263 #-- user table data
2264 $userdata['ID'] = NULL; // new user
2265 $userdata['user_login'] = $login;
2266 $userdata['user_nicename'] = $nice_author;
2267 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
2268 $userdata['user_email'] = $email;
2269 $userdata['user_url'] = $authorUrl;
2270 $userdata['nickname'] = $author;
2271 $parts = preg_split('/\s+/', trim($author), 2);
2272 if (isset($parts[0])) : $userdata['first_name'] = $parts[0]; endif;
2273 if (isset($parts[1])) : $userdata['last_name'] = $parts[1]; endif;
2274 $userdata['display_name'] = $author;
2275 $userdata['role'] = 'contributor';
2276
2277 do { // Keep trying until you get it right. Or until PHP crashes, I guess.
2278 $id = wp_insert_user($userdata);
2279 if (is_wp_error($id)) :
2280 $codes = $id->get_error_code();
2281 switch ($codes) :
2282 case 'empty_user_login' :
2283 case 'existing_user_login' :
2284 // Add a random disambiguator
2285 $userdata['user_login'] .= substr(md5(uniqid(microtime())), 0, 6);
2286 break;
2287 case 'user_nicename_too_long' :
2288 // Add a limited 50 caracters user_nicename based on user_login
2289 $userdata['user_nicename'] = mb_substr( $userdata['user_login'], 0, 50 );
2290 break;
2291 case 'existing_user_email' :
2292 // No disassemble!
2293 $parts = explode('@', $userdata['user_email'], 2);
2294
2295 // Add a random disambiguator as a gmail-style username extension
2296 $parts[0] .= '+'.substr(md5(uniqid(microtime())), 0, 6);
2297
2298 // Reassemble
2299 $userdata['user_email'] = $parts[0].'@'.$parts[1];
2300 break;
2301 endswitch;
2302 endif;
2303 } while (is_wp_error($id));
2304 elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
2305 $id = (int) $unfamiliar_author;
2306 elseif ($unfamiliar_author === 'default') :
2307 $id = 1;
2308 endif;
2309 endif;
2310 endif;
2311
2312 if ($id) :
2313 $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
2314
2315 // Multisite: Check whether the author has been recorded
2316 // on *this* blog before. If not, put her down as a
2317 // Contributor for *this* blog.
2318 $user = new WP_User((int) $id);
2319 if (empty($user->roles)) :
2320 $user->add_role('contributor');
2321 endif;
2322 endif;
2323 return $id;
2324 } /* function SyndicatedPost::author_id () */
2325
2326 /**
2327 * category_ids: look up (and create) category ids from a list of
2328 * categories
2329 *
2330 * @param array $cats
2331 * @param string $unfamiliar_category
2332 * @param array|null $taxonomies
2333 * @return array
2334 */
2335 function category_ids ($cats, $unfamiliar_category = 'create', $taxonomies = NULL, $params = array()) {
2336 return $this->link->category_ids($this, $cats, $unfamiliar_category, $taxonomies, $params);
2337 } /* SyndicatedPost::category_ids () */
2338
2339 } /* class SyndicatedPost */
2340