PluginProbe
FeedWordPress / 2024.0511
FeedWordPress v2024.0511
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 2024.0511, at syndicatedpost.class.php

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