PluginProbe
FeedWordPress / trunk
FeedWordPress vtrunk
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 trunk, at syndicatedpost.class.php

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