PluginProbe
FeedWordPress / 2016.1211
FeedWordPress v2016.1211
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 2016.1211, at syndicatedpost.class.php

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