PluginProbe
FeedWordPress / 2017.1004
FeedWordPress v2017.1004
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.1004, at syndicatedpost.class.php

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