PluginProbe
FeedWordPress / 2025.1211
FeedWordPress v2025.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
← All changes | syndicatedpost.class.php +2171 -925 2009.07072025.1211 View file →
@@ -1,288 +1,1470 @@
1 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 + */
2 17 class SyndicatedPost {
18 + /** @var MagpieRSS|null MagpieRSS representation. */
3 19 var $item = null;
4 -
20 + /** @var SimplePie_Item|null SimplePie_Item representation. */
21 + var $entry = null;
22 +
5 23 var $link = null;
6 24 var $feed = null;
7 25 var $feedmeta = null;
8 -
9 - var $post = array ();
10 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 +
11 35 var $_freshness = null;
12 36 var $_wp_id = null;
37 + var $_wp_post = null;
13 38
14 - function SyndicatedPost ($item, $link) {
15 - global $wpdb;
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;
16 51
17 - $this->link = $link;
18 - $feedmeta = $link->settings;
19 - $feed = $link->magpie;
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;
20 60
21 - # This is ugly as all hell. I'd like to use apply_filters()'s
22 - # alleged support for a variable argument count, but this seems
23 - # to have been broken in WordPress 1.5. It'll be fixed somehow
24 - # in WP 1.5.1, but I'm aiming at WP 1.5 compatibility across
25 - # the board here.
26 - #
27 - # Cf.: <http://mosquito.wordpress.org/view.php?id=901>
28 - global $fwp_channel, $fwp_feedmeta;
29 - $fwp_channel = $feed; $fwp_feedmeta = $feedmeta;
61 + // convert to Magpie for compat purposes
62 + $mpie = new MagpieFromSimplePie( $source->simplepie, $this->entry );
63 + $this->item = $mpie->get_item();
30 64
31 - $this->feed = $feed;
32 - $this->feedmeta = $feedmeta;
65 + // done with conversion object
66 + $mpie = NULL; unset( $mpie );
67 + else :
68 + $this->item = $item;
69 + endif;
33 70
34 - $this->item = $item;
35 - $this->item = apply_filters('syndicated_item', $this->item, $this);
71 + $this->link = $source;
72 + $this->feed = $source->magpie;
73 + $this->feedmeta = $source->settings;
36 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 +
37 102 # Filters can halt further processing by returning NULL
38 - if (is_null($this->item)) :
103 + if ( is_null( $this->item ) ) :
39 104 $this->post = NULL;
40 105 else :
41 - # Note that nothing is run through $wpdb->escape() here.
106 +
107 + # Note that nothing is run through esc_sql() here.
42 108 # That's deliberate. The escaping is done at the point
43 109 # of insertion, not here, to avoid double-escaping and
44 110 # to avoid screwing with syndicated_post filters
45 111
46 - $this->post['post_title'] = apply_filters('syndicated_item_title', $this->item['title'], $this);
112 + $this->post['post_title'] = apply_filters(
113 + 'syndicated_item_title',
114 + $this->entry->get_title(),
115 + $this
116 + );
47 117
48 - // This just gives us an alphanumeric representation of
49 - // the author. We will look up (or create) the numeric
50 - // ID for the author in SyndicatedPost::add()
51 - $this->post['named']['author'] = apply_filters('syndicated_item_author', $this->author(), $this);
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().
52 126
53 - # Identify content and sanitize it.
54 - # ---------------------------------
55 - if (isset($this->item['atom_content'])) :
56 - $content = $this->item['atom_content'];
57 - elseif (isset($this->item['xhtml']['body'])) :
58 - $content = $this->item['xhtml']['body'];
59 - elseif (isset($this->item['xhtml']['div'])) :
60 - $content = $this->item['xhtml']['div'];
61 - elseif (isset($this->item['content']['encoded']) and $this->item['content']['encoded']):
62 - $content = $this->item['content']['encoded'];
63 - else:
64 - $content = $this->item['description'];
65 - endif;
66 - $this->post['post_content'] = apply_filters('syndicated_item_content', $content, $this);
127 + $this->post['post_content'] = apply_filters(
128 + 'syndicated_item_content',
129 + $this->content(),
130 + $this
131 + );
67 132
68 - # Identify and sanitize excerpt
69 - $excerpt = NULL;
70 - if ( isset($this->item['description']) and $this->item['description'] ) :
71 - $excerpt = $this->item['description'];
72 - elseif ( isset($content) and $content ) :
73 - $excerpt = strip_tags($content);
74 - if (strlen($excerpt) > 255) :
75 - $excerpt = substr($excerpt,0,252).'...';
76 - endif;
77 - endif;
78 - $excerpt = apply_filters('syndicated_item_excerpt', $excerpt, $this);
133 + $excerpt = apply_filters( 'syndicated_item_excerpt', $this->excerpt(), $this );
79 134
80 - if (!is_null($excerpt)):
135 + if ( !empty( $excerpt ) ):
81 136 $this->post['post_excerpt'] = $excerpt;
82 137 endif;
83 -
84 - // This is unnecessary if we use wp_insert_post
85 - if (!$this->use_api('wp_insert_post')) :
86 - $this->post['post_name'] = sanitize_title($this->post['post_title']);
87 - endif;
88 138
89 - $this->post['epoch']['issued'] = apply_filters('syndicated_item_published', $this->published(), $this);
90 - $this->post['epoch']['created'] = apply_filters('syndicated_item_created', $this->created(), $this);
91 - $this->post['epoch']['modified'] = apply_filters('syndicated_item_updated', $this->updated(), $this);
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 ) );
92 143
93 - // Dealing with timestamps in WordPress is so fucking fucked.
94 - $offset = (int) get_option('gmt_offset') * 60 * 60;
95 - $this->post['post_date'] = gmdate('Y-m-d H:i:s', $this->published() + $offset);
96 - $this->post['post_modified'] = gmdate('Y-m-d H:i:s', $this->updated() + $offset);
97 - $this->post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $this->published());
98 - $this->post['post_modified_gmt'] = gmdate('Y-m-d H:i:s', $this->updated());
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 );
99 148
100 149 // Use feed-level preferences or the global default.
101 - $this->post['post_status'] = $this->link->syndicated_status('post', 'publish');
102 - $this->post['comment_status'] = $this->link->syndicated_status('comment', 'closed');
103 - $this->post['ping_status'] = $this->link->syndicated_status('ping', 'closed');
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' );
104 153
105 154 // Unique ID (hopefully a unique tag: URI); failing that, the permalink
106 - $this->post['guid'] = apply_filters('syndicated_item_guid', $this->guid(), $this);
155 + $this->post['guid'] = apply_filters( 'syndicated_item_guid', $this->guid(), $this );
107 156
108 - // User-supplied custom settings to apply to each post. Do first so that FWP-generated custom settings will overwrite if necessary; thus preventing any munging
109 - $default_custom_settings = get_option('feedwordpress_custom_settings');
110 - if ($default_custom_settings) :
111 - $default_custom_settings = unserialize($default_custom_settings);
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;
112 222 endif;
113 - if (!is_array($default_custom_settings)) :
114 - $default_custom_settings = array();
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'];
115 245 endif;
116 -
117 - $custom_settings = (isset($this->link->settings['postmeta']) ? $this->link->settings['postmeta'] : null);
118 - if ($custom_settings) :
119 - $custom_settings = unserialize($custom_settings);
246 + if ( isset( $this->feedmeta['link/id'] ) ) :
247 + $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
120 248 endif;
121 - if (!is_array($custom_settings)) :
122 - $custom_settings = array();
249 +
250 + if ( isset( $this->item['source_link_self'] ) ) :
251 + $this->post['meta']['syndication_feed_original'] = $this->item['source_link_self'];
123 252 endif;
124 - $this->post['meta'] = array_merge($default_custom_settings, $custom_settings);
125 253
126 - // RSS 2.0 / Atom 1.0 enclosure support
127 - if ( isset($this->item['enclosure#']) ) :
128 - for ($i = 1; $i <= $this->item['enclosure#']; $i++) :
129 - $eid = (($i > 1) ? "#{$id}" : "");
130 - $this->post['meta']['enclosure'][] =
131 - apply_filters('syndicated_item_enclosure_url', $this->item["enclosure{$eid}@url"], $this)."\n".
132 - apply_filters('syndicated_item_enclosure_length', $this->item["enclosure{$eid}@length"], $this)."\n".
133 - apply_filters('syndicated_item_enclosure_type', $this->item["enclosure{$eid}@type"], $this);
134 - endfor;
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);
135 349 endif;
350 + endforeach;
351 + return $matches;
352 + } /* SyndicatedPost::get_feed_channel_elements() */
136 353
137 - // In case you want to point back to the blog this was syndicated from
138 - if (isset($this->feed->channel['title'])) :
139 - $this->post['meta']['syndication_source'] = apply_filters('syndicated_item_source_title', $this->feed->channel['title'], $this);
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'];
140 407 endif;
141 408
142 - if (isset($this->feed->channel['link'])) :
143 - $this->post['meta']['syndication_source_uri'] = apply_filters('syndicated_item_source_link', $this->feed->channel['link'], $this);
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;
144 439 endif;
145 -
146 - // Make use of atom:source data, if present in an aggregated feed
147 - if (isset($this->item['source_title'])) :
148 - $this->post['meta']['syndication_source_original'] = $this->item['source_title'];
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;
149 519 endif;
520 + endif;
150 521
151 - if (isset($this->item['source_link'])) :
152 - $this->post['meta']['syndication_source_uri_original'] = $this->item['source_link'];
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;
153 565 endif;
154 -
155 - if (isset($this->item['source_id'])) :
156 - $this->post['meta']['syndication_source_id_original'] = $this->item['source_id'];
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();
157 586 endif;
587 + endif;
588 + return $this->_hashes[$id];
589 + }
158 590
159 - // Store information on human-readable and machine-readable comment URIs
160 - if (isset($this->item['comments'])) :
161 - $this->post['meta']['rss:comments'] = apply_filters('syndicated_item_comments', $this->item['comments']);
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());
162 708 endif;
163 - if (isset($this->item['wfw']['commentrss'])) :
164 - $this->post['meta']['wfw:commentRSS'] = apply_filters('syndicated_item_commentrss', $this->item['wfw']['commentrss']);
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;
165 754 endif;
755 + endif;
166 756
167 - // Store information to identify the feed that this came from
168 - $this->post['meta']['syndication_feed'] = $this->feedmeta['link/uri'];
169 - $this->post['meta']['syndication_feed_id'] = $this->feedmeta['link/id'];
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;
170 766
171 - if (isset($this->item['source_link_self'])) :
172 - $this->post['meta']['syndication_feed_original'] = $this->item['source_link_self'];
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'];
173 772 endif;
174 773
175 - // In case you want to know the external permalink...
176 - $this->post['meta']['syndication_permalink'] = apply_filters('syndicated_item_link', $this->item['link']);
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;
177 786
178 - // Store a hash of the post content for checking whether something needs to be updated
179 - $this->post['meta']['syndication_item_hash'] = $this->update_hash();
787 + return $author;
788 + } /* SyndicatedPost::author() */
180 789
181 - // Feed-by-feed options for author and category creation
182 - $this->post['named']['unfamiliar']['author'] = (isset($this->feedmeta['unfamiliar author']) ? $this->feedmeta['unfamiliar author'] : null);
183 - $this->post['named']['unfamiliar']['category'] = (isset($this->feedmeta['unfamiliar category']) ? $this->feedmeta['unfamiliar category'] : null);
184 -
185 - // Categories: start with default categories, if any
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')) :
186 805 $fc = get_option("feedwordpress_syndication_cats");
187 806 if ($fc) :
188 - $this->post['named']['preset/category'] = explode("\n", $fc);
189 - else :
190 - $this->post['named']['preset/category'] = array();
807 + $cats = array_merge($cats, explode("\n", $fc));
191 808 endif;
809 + endif;
192 810
193 - if (isset($this->feedmeta['cats']) and is_array($this->feedmeta['cats'])) :
194 - $this->post['named']['preset/category'] = array_merge($this->post['named']['preset/category'], $this->feedmeta['cats']);
195 - endif;
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;
196 816
197 - // Now add categories from the post, if we have 'em
198 - $this->post['named']['category'] = array();
199 - if ( isset($this->item['category#']) ) :
200 - for ($i = 1; $i <= $this->item['category#']; $i++) :
201 - $cat_idx = (($i > 1) ? "#{$i}" : "");
202 - $cat = $this->item["category{$cat_idx}"];
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;
203 823
204 - if ( isset($this->feedmeta['cat_split']) and strlen($this->feedmeta['cat_split']) > 0) :
205 - $pcre = "\007".$this->feedmeta['cat_split']."\007";
206 - $this->post['named']['category'] = array_merge($this->post['named']['category'], preg_split($pcre, $cat, -1 /*=no limit*/, PREG_SPLIT_NO_EMPTY));
207 - else :
208 - $this->post['named']['category'][] = $cat;
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];
209 845 endif;
210 - endfor;
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;
211 855 endif;
212 - $this->post['named']['category'] = apply_filters('syndicated_item_categories', $this->post['named']['category'], $this);
213 -
214 - // Tags: start with default tags, if any
215 - $ft = get_option("feedwordpress_syndication_tags");
216 - if ($ft) :
217 - $this->post['tags_input'] = explode(FEEDWORDPRESS_CAT_SEPARATOR, $ft);
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 + );
218 889 else :
219 - $this->post['tags_input'] = array();
890 + $cats[] = $cat_name;
220 891 endif;
221 -
222 - if (isset($this->feedmeta['tags']) and is_array($this->feedmeta['tags'])) :
223 - $this->post['tags_input'] = array_merge($this->post['tags_input'], $this->feedmeta['tags']);
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;
224 968 endif;
225 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;
226 1004 endif;
227 - } // SyndicatedPost::SyndicatedPost()
1005 + return $enclosures;
1006 + } /* SyndicatedPost::enclosures() */
228 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 + */
229 1220 function filtered () {
230 1221 return is_null($this->post);
231 - }
1222 + } /* SyndicatedPost::filtered() */
232 1223
233 - function freshness () {
234 - global $wpdb;
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') {
235 1237
236 1238 if ($this->filtered()) : // This should never happen.
237 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1239 + FeedWordPressDiagnostic::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
238 1240 endif;
239 -
240 - if (is_null($this->_freshness)) :
241 - $guid = $wpdb->escape($this->guid());
242 1241
243 - $result = $wpdb->get_row("
244 - SELECT id, guid, post_modified_gmt
245 - FROM $wpdb->posts WHERE guid='$guid'
246 - ");
1242 + if (is_null($this->_freshness)) : // Not yet checked and cached.
1243 + $guid = $this->post['guid'];
1244 + $eguid = esc_sql($this->post['guid']);
247 1245
248 - if (!$result) :
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;
249 1264 $this->_freshness = 2; // New content
250 - else:
251 - $stored_update_hashes = get_post_custom_values('syndication_item_hash', $result->id);
252 - if (count($stored_update_hashes) > 0) :
253 - $stored_update_hash = $stored_update_hashes[0];
254 - $update_hash_changed = ($stored_update_hash != $this->update_hash());
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.
255 1308 else :
256 - $update_hash_changed = false;
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;
257 1333 endif;
258 1334
259 - preg_match('/([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/', $result->post_modified_gmt, $backref);
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));
260 1341
261 - $last_rev_ts = gmmktime($backref[4], $backref[5], $backref[6], $backref[2], $backref[3], $backref[1]);
262 - $updated_ts = $this->updated(/*fallback=*/ true, /*default=*/ NULL);
263 - $updated = ((
264 - !is_null($updated_ts)
265 - and ($updated_ts > $last_rev_ts)
266 - ) or $update_hash_changed);
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);
267 1350
268 1351 if ($updated) :
269 - $this->_freshness = 1; // Updated content
270 - $this->_wp_id = $result->id;
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 + );
271 1369 else :
1370 + FeedWordPress::diagnostic('feed_items:freshness', 'Item ['.$guid.'] "'.$this->entry->get_title().'" is a duplicate of an existing post.');
272 1371 $this->_freshness = 0; // Same old, same old
273 - $this->_wp_id = $result->id;
1372 + $this->_wp_id = $old_post->ID;
274 1373 endif;
275 1374 endif;
276 1375 endif;
277 - return $this->_freshness;
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 );
278 1413 }
279 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 +
280 1462 function wp_id () {
281 1463 if ($this->filtered()) : // This should never happen.
282 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
1464 + FeedWordPressDiagnostic::critical_bug('SyndicatedPost', $this, __LINE__, __FILE__);
283 1465 endif;
284 -
1466 +
285 1467 if (is_null($this->_wp_id) and is_null($this->_freshness)) :
286 1468 $fresh = $this->freshness(); // sets WP DB id in the process
287 1469 endif;
288 1470 return $this->_wp_id;
@@ -287,272 +1469,492 @@
287 1469 endif;
288 1470 return $this->_wp_id;
289 1471 }
290 1472
291 - function store () {
292 - global $wpdb;
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 + );
293 1493
294 - if ($this->filtered()) : // This should never happen.
295 - FeedWordPress::critical_bug('SyndicatedPost', $this, __LINE__);
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;
296 1497 endif;
297 -
298 - $freshness = $this->freshness();
299 - if ($freshness > 0) :
300 - # -- Look up, or create, numeric ID for author
301 - $this->post['post_author'] = $this->author_id (
302 - FeedWordPress::on_unfamiliar('author', $this->post['named']['unfamiliar']['author'])
303 - );
1498 + } /* SyndicatedPost::secure_author_id() */
304 1499
305 - if (is_null($this->post['post_author'])) :
306 - $this->post = NULL;
307 - endif;
308 - endif;
309 -
310 - if (!$this->filtered() and $freshness > 0) :
311 - # -- Look up, or create, numeric ID for categories
312 - list($pcats, $ptags) = $this->category_ids (
313 - $this->post['named']['category'],
314 - FeedWordPress::on_unfamiliar('category', $this->post['named']['unfamiliar']['category']),
315 - /*tags_too=*/ true
316 - );
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);
317 1522
318 - $this->post['post_category'] = $pcats;
319 - $this->post['tags_input'] = array_merge($this->post['tags_input'], $ptags);
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'];
320 1528
321 - if (is_null($this->post['post_category'])) :
322 - // filter mode on, no matching categories; drop the post
323 - $this->post = NULL;
324 - else :
325 - // filter mode off or at least one match; now add on the feed and global presets
326 - $this->post['post_category'] = array_merge (
327 - $this->post['post_category'],
328 - $this->category_ids (
329 - $this->post['named']['preset/category'],
330 - 'default'
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,
331 1558 )
332 1559 );
333 1560
334 - if (count($this->post['post_category']) < 1) :
335 - $this->post['post_category'][] = 1; // Default to category 1 ("Uncategorized" / "General") if nothing else
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;
336 1573 endif;
337 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;
338 1608 endif;
339 -
340 - if (!$this->filtered() and $freshness > 0) :
341 - unset($this->post['named']);
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 +
342 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 + );
343 1654 endif;
344 -
345 - if (!$this->filtered() and $freshness == 2) :
346 - // The item has not yet been added. So let's add it.
347 - $this->insert_new();
348 - $this->add_rss_meta();
349 - do_action('post_syndicated_item', $this->wp_id());
350 1655
351 - $ret = 'new';
352 - elseif (!$this->filtered() and $freshness == 1) :
353 - $this->post['ID'] = $this->wp_id();
354 - $this->update_existing();
355 - $this->add_rss_meta();
356 - do_action('update_syndicated_item', $this->wp_id());
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 + );
357 1663
358 - $ret = 'updated';
359 - else :
360 - $ret = false;
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');
361 1679 endif;
362 -
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 +
363 1707 return $ret;
364 - } // function SyndicatedPost::store ()
365 -
366 - function insert_new () {
367 - global $wpdb, $wp_db_version;
1708 + } /* function SyndicatedPost::store () */
368 1709
1710 + function insert_post ($update = false, $freshness = 2) {
369 1711 $dbpost = $this->normalize_post(/*new=*/ true);
370 - if (!is_null($dbpost)) :
371 - if ($this->use_api('wp_insert_post')) :
372 - $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
373 -
374 - // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
375 - add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
376 -
377 - // Kludge to prevent kses filters from stripping the
378 - // content of posts when updating without a logged in
379 - // user who has `unfiltered_html` capability.
380 - add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
381 -
382 - $this->_wp_id = wp_insert_post($dbpost);
383 -
384 - // Turn off ridiculous fucking kludges #1 and #2
385 - remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
386 - remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
387 -
388 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
389 -
390 - // Unfortunately, as of WordPress 2.3, wp_insert_post()
391 - // *still* offers no way to use a guid of your choice,
392 - // and munges your post modified timestamp, too.
393 - $result = $wpdb->query("
394 - UPDATE $wpdb->posts
395 - SET
396 - guid='{$dbpost['guid']}',
397 - post_modified='{$dbpost['post_modified']}',
398 - post_modified_gmt='{$dbpost['post_modified_gmt']}'
399 - WHERE ID='{$this->_wp_id}'
400 - ");
401 - else :
402 - # The right way to do this is the above. But, alas,
403 - # in earlier versions of WordPress, wp_insert_post has
404 - # too much behavior (mainly related to pings) that can't
405 - # be overridden. In WordPress 1.5, it's enough of a
406 - # resource hog to make PHP segfault after inserting
407 - # 50-100 posts. This can get pretty annoying, especially
408 - # if you are trying to update your feeds for the first
409 - # time.
410 -
411 - $result = $wpdb->query("
412 - INSERT INTO $wpdb->posts
413 - SET
414 - guid = '{$dbpost['guid']}',
415 - post_author = '{$dbpost['post_author']}',
416 - post_date = '{$dbpost['post_date']}',
417 - post_date_gmt = '{$dbpost['post_date_gmt']}',
418 - post_content = '{$dbpost['post_content']}',"
419 - .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
420 - post_title = '{$dbpost['post_title']}',
421 - post_name = '{$dbpost['post_name']}',
422 - post_modified = '{$dbpost['post_modified']}',
423 - post_modified_gmt = '{$dbpost['post_modified_gmt']}',
424 - comment_status = '{$dbpost['comment_status']}',
425 - ping_status = '{$dbpost['ping_status']}',
426 - post_status = '{$dbpost['post_status']}'
427 - ");
428 - $this->_wp_id = $wpdb->insert_id;
429 -
430 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
431 -
432 - // WordPress 1.5.x - 2.0.x
433 - wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
434 -
435 - // Since we are not going through official channels, we need to
436 - // manually tell WordPress that we've published a new post.
437 - // We need to make sure to do this in order for FeedWordPress
438 - // to play well with the staticize-reloaded plugin (something
439 - // that a large aggregator website is going to *want* to be
440 - // able to use).
441 - do_action('publish_post', $this->_wp_id);
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;
442 1741 endif;
443 - endif;
444 - } /* SyndicatedPost::insert_new() */
445 1742
446 - function update_existing () {
447 - global $wpdb;
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 + );
448 1751
449 - // Why the fuck doesn't wp_insert_post already do this?
450 - $dbpost = $this->normalize_post(/*new=*/ false);
451 - if (!is_null($dbpost)) :
452 - if ($this->use_api('wp_insert_post')) :
453 - $dbpost['post_pingback'] = false; // Tell WP 2.1 and 2.2 not to process for pingbacks
454 -
455 - // This is a ridiculous fucking kludge necessitated by WordPress 2.6 munging authorship meta-data
456 - add_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
457 -
458 - // Kludge to prevent kses filters from stripping the
459 - // content of posts when updating without a logged in
460 - // user who has `unfiltered_html` capability.
461 - add_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
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 + );
462 1760
463 - // Don't munge status fields that the user may have reset manually
464 - if (function_exists('get_post_field')) :
465 - $doNotMunge = array('post_status', 'comment_status', 'ping_status');
466 - foreach ($doNotMunge as $field) :
467 - $dbpost[$field] = get_post_field($field, $this->wp_id());
468 - endforeach;
469 - endif;
1761 + if ($update) :
1762 + $this->post['ID'] = $this->wp_id();
1763 + $dbpost['ID'] = $this->post['ID'];
1764 + endif;
470 1765
471 - $this->_wp_id = wp_insert_post($dbpost);
472 -
473 - // Turn off ridiculous fucking kludges #1 and #2
474 - remove_action('_wp_put_post_revision', array($this, 'fix_revision_meta'));
475 - remove_filter('content_save_pre', array($this, 'avoid_kses_munge'), 11);
476 -
477 - $this->validate_post_id($dbpost, array(__CLASS__, __FUNCTION__));
478 -
479 - // Unfortunately, as of WordPress 2.3, wp_insert_post()
480 - // munges your post modified timestamp.
481 - $result = $wpdb->query("
482 - UPDATE $wpdb->posts
483 - SET
484 - post_modified='{$dbpost['post_modified']}',
485 - post_modified_gmt='{$dbpost['post_modified_gmt']}'
486 - WHERE ID='{$this->_wp_id}'
487 - ");
488 - else :
489 -
490 - $result = $wpdb->query("
491 - UPDATE $wpdb->posts
492 - SET
493 - post_author = '{$dbpost['post_author']}',
494 - post_content = '{$dbpost['post_content']}',"
495 - .(isset($dbpost['post_excerpt']) ? "post_excerpt = '{$dbpost['post_excerpt']}'," : "")."
496 - post_title = '{$dbpost['post_title']}',
497 - post_name = '{$dbpost['post_name']}',
498 - post_modified = '{$dbpost['post_modified']}',
499 - post_modified_gmt = '{$dbpost['post_modified_gmt']}'
500 - WHERE guid='{$dbpost['guid']}'
501 - ");
502 -
503 - // WordPress 2.1.x and up
504 - if (function_exists('wp_set_post_categories')) :
505 - wp_set_post_categories($this->wp_id(), $this->post['post_category']);
506 - // WordPress 1.5.x - 2.0.x
507 - elseif (function_exists('wp_set_post_cats')) :
508 - wp_set_post_cats('1', $this->wp_id(), $this->post['post_category']);
509 - // This should never happen.
510 - else :
511 - FeedWordPress::critical_bug(__CLASS__.'::'.__FUNCTION.'(): no post categorizing function', array("dbpost" => $dbpost, "this" => $this), __LINE__);
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;
512 1808 endif;
513 -
514 - // Since we are not going through official channels, we need to
515 - // manually tell WordPress that we've published a new post.
516 - // We need to make sure to do this in order for FeedWordPress
517 - // to play well with the staticize-reloaded plugin (something
518 - // that a large aggregator website is going to *want* to be
519 - // able to use).
520 - do_action('edit_post', $this->post['ID']);
521 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;
522 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);
523 1856 } /* SyndicatedPost::update_existing() */
524 1857
525 1858 /**
526 1859 * SyndicatedPost::normalize_post()
527 1860 *
528 - * @param bool $new If true, this post is to be inserted anew. If false, it is an update of an existing post.
1861 + * @param bool $new If true, this post is to be inserted anew. If false, it is an update of an existing post. (Unused)
529 1862 * @return array A normalized representation of the post ready to be inserted into the database or sent to the WordPress API functions
530 1863 */
531 - function normalize_post ($new = true) {
532 - global $wpdb;
1864 + function normalize_post( $new = true ) {
1865 + $out = $this->post;
533 1866
534 - $out = array();
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;
535 1880
536 - // Why the fuck doesn't wp_insert_post already do this?
537 - foreach ($this->post as $key => $value) :
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) :
538 1902 if (is_string($value)) :
539 - $out[$key] = $wpdb->escape($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 +
540 1914 else :
541 1915 $out[$key] = $value;
542 1916 endif;
1917 +
543 1918 endforeach;
544 1919
545 - if (strlen($out['post_title'].$out['post_content'].$out['post_excerpt']) == 0) :
546 - // FIXME: Option for filtering out empty posts
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 +
547 1956 endif;
548 - if (strlen($out['post_title'])==0) :
549 - $offset = (int) get_option('gmt_offset') * 60 * 60;
550 - $out['post_title'] =
551 - $this->post['meta']['syndication_source']
552 - .' '.gmdate('Y-m-d H:i:s', $this->published() + $offset);
553 - // FIXME: Option for what to fill a blank title with...
554 - endif;
555 1957
556 1958 return $out;
557 1959 }
558 1960
@@ -561,35 +1963,57 @@
561 1963 *
562 1964 * @param array $dbpost An array representing the post we attempted to insert or update
563 1965 * @param mixed $ns A string or array representing the namespace (class, method) whence this method was called.
564 1966 */
565 - function validate_post_id ($dbpost, $ns) {
1967 + function validate_post_id ($dbpost, $is_update, $ns) {
566 1968 if (is_array($ns)) : $ns = implode('::', $ns);
567 1969 else : $ns = (string) $ns; endif;
568 -
1970 +
569 1971 // This should never happen.
570 - if (!is_numeric($this->_wp_id) or ($this->_wp_id == 0)) :
571 - FeedWordPress::critical_bug(
572 - /*name=*/ $ns.'::_wp_id',
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,
573 1998 /*var =*/ array(
574 1999 "\$this->_wp_id" => $this->_wp_id,
575 2000 "\$dbpost" => $dbpost,
576 - "\$this" => $this
577 2001 ),
578 - /*line # =*/ __LINE__
2002 + /*line # =*/ __LINE__, /*filename=*/ __FILE__
579 2003 );
580 2004 endif;
581 2005 } /* SyndicatedPost::validate_post_id() */
582 -
2006 +
583 2007 /**
584 - * SyndicatedPost::fix_revision_meta() - Fixes the way WP 2.6+ fucks up
585 - * meta-data (authorship, etc.) when storing revisions of an updated
586 - * syndicated post.
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.
587 2011 *
588 - * In their infinite wisdom, the WordPress coders have made it completely
589 - * impossible for a plugin that uses wp_insert_post() to set certain
590 - * meta-data (such as the author) when you store an old revision of an
591 - * updated post. Instead, it uses the WordPress defaults (= currently
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.
592 2016 * active user ID if the process is running with a user logged in, or
593 2017 * = #0 if there is no user logged in). This results in bogus authorship
594 2018 * data for revisions that are syndicated from off the feed, unless we
595 2019 * use a ridiculous kludge like this to end-run the munging of meta-data
@@ -596,106 +2020,209 @@
596 2020 * by _wp_put_post_revision.
597 2021 *
598 2022 * @param int $revision_id The revision ID to fix up meta-data
599 2023 */
600 - function fix_revision_meta ($revision_id) {
601 - global $wpdb;
602 -
603 - $post_author = (int) $this->post['post_author'];
604 -
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 +
605 2027 $revision_id = (int) $revision_id;
606 - $wpdb->query("
607 - UPDATE $wpdb->posts
608 - SET post_author={$this->post['post_author']}
609 - WHERE post_type = 'revision' AND ID='$revision_id'
610 - ");
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 +
611 2038 } /* SyndicatedPost::fix_revision_meta () */
612 2039
613 2040 /**
614 - * SyndicatedPost::avoid_kses_munge() -- If FeedWordPress is processing
615 - * an automatic update, that generally means that wp_insert_post() is
616 - * being called under the user credentials of whoever is viewing the
617 - * blog at the time -- usually meaning no user at all. But if WordPress
618 - * gets a wp_insert_post() when current_user_can('unfiltered_html') is
619 - * false, it will run the content of the post through a kses function
620 - * that strips out lots of HTML tags -- notably <object> and some others.
621 - * This causes problems for syndicating (for example) feeds that contain
622 - * YouTube videos. It also produces an unexpected asymmetry between
623 - * automatically-initiated updates and updates initiated manually from
624 - * the WordPress Dashboard (which are usually initiated under the
625 - * credentials of a logged-in admin, and so don't get run through the
626 - * kses function). So, to avoid the whole mess, what we do here is
627 - * just forcibly disable the kses munging for a single syndicated post,
628 - * by restoring the contents of the `post_content` field.
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.
629 2049 *
630 - * @param string $content The content of the post, after other filters have gotten to it
631 - * @return string The original content of the post, before other filters had a chance to munge it.
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.
632 2056 */
633 - function avoid_kses_munge ($content) {
634 - global $wpdb;
635 - return $wpdb->escape($this->post['post_content']);
636 - }
637 -
638 - // SyndicatedPost::add_rss_meta: adds interesting meta-data to each entry
639 - // using the space for custom keys. The set of keys and values to add is
640 - // specified by the keys and values of $post['meta']. This is used to
641 - // store anything that the WordPress user might want to access from a
642 - // template concerning the post's original source that isn't provided
643 - // for by standard WP meta-data (i.e., any interesting data about the
644 - // syndicated post other than author, title, timestamp, categories, and
645 - // guid). It's also used to hook into WordPress's support for
646 - // enclosures.
647 - function add_rss_meta () {
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) {
648 2103 global $wpdb;
649 - if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
650 - $postId = $this->wp_id();
651 -
652 - // Aggregated posts should NOT send out pingbacks.
653 - // WordPress 2.1-2.2 claim you can tell them not to
654 - // using $post_pingback, but they don't listen, so we
655 - // make sure here.
656 - $result = $wpdb->query("
657 - DELETE FROM $wpdb->postmeta
658 - WHERE post_id='$postId' AND meta_key='_pingme'
659 - ");
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 () */
660 2111
661 - foreach ( $this->post['meta'] as $key => $values ) :
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'])).'}');
662 2131
663 - $key = $wpdb->escape($key);
2132 + if ( is_array($this->post) and isset($this->post['meta']) and is_array($this->post['meta']) ) :
2133 + $postId = $post->ID;
664 2134
665 - // If this is an update, clear out the old
666 - // values to avoid duplication.
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.
667 2139 $result = $wpdb->query("
668 2140 DELETE FROM $wpdb->postmeta
669 - WHERE post_id='$postId' AND meta_key='$key'
2141 + WHERE post_id='$postId' AND meta_key='_pingme'
670 2142 ");
671 2143
672 - // Allow for either a single value or an array
673 - if (!is_array($values)) $values = array($values);
674 - foreach ( $values as $value ) :
675 - $value = $wpdb->escape($value);
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.
676 2149 $result = $wpdb->query("
677 - INSERT INTO $wpdb->postmeta
678 - SET
679 - post_id='$postId',
680 - meta_key='$key',
681 - meta_value='$value'
2150 + DELETE FROM $wpdb->postmeta
2151 + WHERE post_id='$postId' AND meta_key='$eKey'
682 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;
683 2160 endforeach;
684 - endforeach;
2161 +
2162 + if ( $result === false)
2163 + {
2164 + error_log( "delete query failed: " . $wpdb->last_error );
2165 + }
2166 + endif;
685 2167 endif;
686 2168 } /* SyndicatedPost::add_rss_meta () */
687 2169
688 - // SyndicatedPost::author_id (): get the ID for an author name from
689 - // the feed. Create the author if necessary.
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 + */
690 2179 function author_id ($unfamiliar_author = 'create') {
691 2180 global $wpdb;
692 2181
693 - $a = $this->author();
694 - $author = $a['name'];
695 - $email = $a['email'];
696 - $url = $a['uri'];
2182 + $a = $this->named['author'];
697 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 +
698 2225 $match_author_by_email = !('yes' == get_option("feedwordpress_do_not_match_author_by_email"));
699 2226 if ($match_author_by_email and !FeedWordPress::is_null_email($email)) :
700 2227 $test_email = $email;
701 2228 else :
@@ -703,21 +2230,43 @@
703 2230 endif;
704 2231
705 2232 // Never can be too careful...
706 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 +
707 2254 $login = apply_filters('pre_user_login', $login);
708 2255
709 2256 $nice_author = sanitize_title($author);
710 2257 $nice_author = apply_filters('pre_user_nicename', $nice_author);
711 2258
712 - $reg_author = $wpdb->escape(preg_quote($author));
713 - $author = $wpdb->escape($author);
714 - $email = $wpdb->escape($email);
715 - $test_email = $wpdb->escape($test_email);
716 - $url = $wpdb->escape($url);
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);
717 2264
718 2265 // Check for an existing author rule....
719 - if (isset($this->link->settings['map authors']['name'][strtolower(trim($author))])) :
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))])) :
720 2269 $author_rule = $this->link->settings['map authors']['name'][strtolower(trim($author))];
721 2270 else :
722 2271 $author_rule = NULL;
723 2272 endif;
@@ -722,75 +2271,46 @@
722 2271 $author_rule = NULL;
723 2272 endif;
724 2273
725 2274 // User name is mapped to a particular author. If that author ID exists, use it.
726 - if (is_numeric($author_rule) and get_userdata((int) $author_rule)) :
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)) :
727 2278 $id = (int) $author_rule;
728 2279
729 2280 // User name is filtered out
730 2281 elseif ('filter' == $author_rule) :
731 2282 $id = NULL;
732 -
2283 +
733 2284 else :
734 2285 // Check the database for an existing author record that might fit
735 2286
736 - #-- WordPress 2.0+
737 - if (fwp_test_wp_version(FWP_SCHEMA_HAS_USERMETA)) :
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 + )");
738 2296
739 - // First try the user core data table.
2297 + // If that fails, look for aliases in the user meta data table
2298 + if (is_null($id)) :
740 2299 $id = $wpdb->get_var(
741 - "SELECT ID FROM $wpdb->users
2300 + "SELECT user_id FROM $wpdb->usermeta
742 2301 WHERE
743 - TRIM(LCASE(user_login)) = TRIM(LCASE('$login'))
2302 + (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
744 2303 OR (
745 - LENGTH(TRIM(LCASE(user_email))) > 0
746 - AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
747 - )
748 - OR TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author'))
749 - ");
750 -
751 - // If that fails, look for aliases in the user meta data table
752 - if (is_null($id)) :
753 - $id = $wpdb->get_var(
754 - "SELECT user_id FROM $wpdb->usermeta
755 - WHERE
756 - (meta_key = 'description' AND TRIM(LCASE(meta_value)) = TRIM(LCASE('$author')))
757 - OR (
758 - meta_key = 'description'
759 - AND TRIM(LCASE(meta_value))
760 - RLIKE CONCAT(
761 - '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
762 - TRIM(LCASE('$reg_author')),
763 - '( |\\t|\\r)*(\\n|\$)'
764 - )
765 - )
766 - ");
767 - endif;
768 -
769 - #-- WordPress 1.5.x
770 - else :
771 - $id = $wpdb->get_var(
772 - "SELECT ID from $wpdb->users
773 - WHERE
774 - TRIM(LCASE(user_login)) = TRIM(LCASE('$login')) OR
775 - (
776 - LENGTH(TRIM(LCASE(user_email))) > 0
777 - AND TRIM(LCASE(user_email)) = TRIM(LCASE('$test_email'))
778 - ) OR
779 - TRIM(LCASE(user_firstname)) = TRIM(LCASE('$author')) OR
780 - TRIM(LCASE(user_nickname)) = TRIM(LCASE('$author')) OR
781 - TRIM(LCASE(user_nicename)) = TRIM(LCASE('$nice_author')) OR
782 - TRIM(LCASE(user_description)) = TRIM(LCASE('$author')) OR
783 - (
784 - LOWER(user_description)
2304 + meta_key = 'description'
2305 + AND TRIM(LCASE(meta_value))
785 2306 RLIKE CONCAT(
786 2307 '(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*',
787 - LCASE('$reg_author'),
2308 + TRIM(LCASE('$reg_author')),
788 2309 '( |\\t|\\r)*(\\n|\$)'
789 2310 )
790 2311 )
791 2312 ");
792 -
793 2313 endif;
794 2314
795 2315 // ... if you don't find one, then do what you need to do
796 2316 if (is_null($id)) :
@@ -796,8 +2316,16 @@
796 2316 if (is_null($id)) :
797 2317 if ($unfamiliar_author === 'create') :
798 2318 $userdata = array();
799 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 +
800 2328 #-- user table data
801 2329 $userdata['ID'] = NULL; // new user
802 2330 $userdata['user_login'] = $login;
803 2331 $userdata['user_nicename'] = $nice_author;
@@ -802,393 +2330,111 @@
802 2330 $userdata['user_login'] = $login;
803 2331 $userdata['user_nicename'] = $nice_author;
804 2332 $userdata['user_pass'] = substr(md5(uniqid(microtime())), 0, 6); // just something random to lock it up
805 2333 $userdata['user_email'] = $email;
806 - $userdata['user_url'] = $url;
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 +
807 2341 $userdata['display_name'] = $author;
808 -
809 - $id = wp_insert_user($userdata);
810 - elseif (is_numeric($unfamiliar_author) and get_userdata((int) $unfamiliar_author)) :
811 - $id = (int) $unfamiliar_author;
812 - elseif ($unfamiliar_author === 'default') :
813 - $id = 1;
814 - endif;
815 - endif;
816 - endif;
2342 + $userdata['role'] = 'contributor';
817 2343
818 - if ($id) :
819 - $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
820 - endif;
821 - return $id;
822 - } // function SyndicatedPost::author_id ()
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);
823 2370
824 - // look up (and create) category ids from a list of categories
825 - function category_ids ($cats, $unfamiliar_category = 'create', $tags_too = false) {
826 - global $wpdb;
2371 + // Add a random disambiguator as a gmail-style username extension
2372 + $parts[0] .= '+'.substr(md5(uniqid(microtime())), 0, 6);
827 2373
828 - // We need to normalize whitespace because (1) trailing
829 - // whitespace can cause PHP and MySQL not to see eye to eye on
830 - // VARCHAR comparisons for some versions of MySQL (cf.
831 - // <http://dev.mysql.com/doc/mysql/en/char.html>), and (2)
832 - // because I doubt most people want to make a semantic
833 - // distinction between 'Computers' and 'Computers '
834 - $cats = array_map('trim', $cats);
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);
835 2381
836 - $tags = array();
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));
837 2393
838 - $cat_ids = array ();
839 - foreach ($cats as $cat_name) :
840 - if (preg_match('/^{#([0-9]+)}$/', $cat_name, $backref)) :
841 - $cat_id = (int) $backref[1];
842 - if (function_exists('is_term') and is_term($cat_id, 'category')) :
843 - $cat_ids[] = $cat_id;
844 - elseif (get_category($cat_id)) :
845 - $cat_ids[] = $cat_id;
846 - endif;
847 - elseif (strlen($cat_name) > 0) :
848 - $esc = $wpdb->escape($cat_name);
849 - $resc = $wpdb->escape(preg_quote($cat_name));
850 -
851 - // WordPress 2.3+
852 - if (function_exists('is_term')) :
853 - $cat_id = is_term($cat_name, 'category');
854 - if ($cat_id) :
855 - $cat_ids[] = $cat_id['term_id'];
856 - // There must be a better way to do this...
857 - elseif ($results = $wpdb->get_results(
858 - "SELECT term_id
859 - FROM $wpdb->term_taxonomy
860 - WHERE
861 - LOWER(description) RLIKE
862 - CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)')"
863 - )) :
864 - foreach ($results AS $term) :
865 - $cat_ids[] = (int) $term->term_id;
866 - endforeach;
867 - elseif ('tag'==$unfamiliar_category) :
868 - $tags[] = $cat_name;
869 - elseif ('create'===$unfamiliar_category) :
870 - $term = wp_insert_term($cat_name, 'category');
871 - if (is_wp_error($term)) :
872 - FeedWordPress::noncritical_bug('term insertion problem', array('cat_name' => $cat_name, 'term' => $term, 'this' => $this), __LINE__);
873 - else :
874 - $cat_ids[] = $term['term_id'];
875 - endif;
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;
876 2403 endif;
877 -
878 - // WordPress 1.5.x - 2.2.x
879 - else :
880 - $results = $wpdb->get_results(
881 - "SELECT cat_ID
882 - FROM $wpdb->categories
883 - WHERE
884 - (LOWER(cat_name) = LOWER('$esc'))
885 - OR (LOWER(category_description)
886 - RLIKE CONCAT('(^|\\n)a\\.?k\\.?a\\.?( |\\t)*:?( |\\t)*', LOWER('{$resc}'), '( |\\t|\\r)*(\\n|\$)'))
887 - ");
888 - if ($results) :
889 - foreach ($results as $term) :
890 - $cat_ids[] = (int) $term->cat_ID;
891 - endforeach;
892 - elseif ('create'===$unfamiliar_category) :
893 - if (function_exists('wp_insert_category')) :
894 - $cat_id = wp_insert_category(array('cat_name' => $cat_name));
895 - // And into the database we go.
896 - else :
897 - $nice_kitty = sanitize_title($cat_name);
898 - $wpdb->query(sprintf("
899 - INSERT INTO $wpdb->categories
900 - SET
901 - cat_name='%s',
902 - category_nicename='%s'
903 - ", $wpdb->escape($cat_name), $nice_kitty
904 - ));
905 - $cat_id = $wpdb->insert_id;
906 - endif;
907 - $cat_ids[] = $cat_id;
908 - 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;
909 2409 endif;
910 2410 endif;
911 - endforeach;
912 -
913 - if ((count($cat_ids) == 0) and ($unfamiliar_category === 'filter')) :
914 - $cat_ids = NULL; // Drop the post
915 - else :
916 - $cat_ids = array_unique($cat_ids);
917 2411 endif;
918 -
919 - if ($tags_too) : $ret = array($cat_ids, $tags);
920 - else : $ret = $cat_ids;
921 - endif;
922 2412
923 - return $ret;
924 - } // function SyndicatedPost::category_ids ()
2413 + if ($id) :
2414 + $this->link->settings['map authors']['name'][strtolower(trim($author))] = $id;
925 2415
926 - function use_api ($tag) {
927 - global $wp_db_version;
928 - switch ($tag) :
929 - case 'wp_insert_post':
930 - // Before 2.2, wp_insert_post does too much of the wrong stuff to use it
931 - // In 1.5 it was such a resource hog it would make PHP segfault on big updates
932 - $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_21);
933 - break;
934 - case 'post_status_pending':
935 - $ret = (isset($wp_db_version) and $wp_db_version > FWP_SCHEMA_23);
936 - break;
937 - endswitch;
938 - return $ret;
939 - } // function SyndicatedPost::use_api ()
940 -
941 - #### EXTRACT DATA FROM FEED ITEM ####
942 -
943 - function created () {
944 - $epoch = null;
945 - if (isset($this->item['dc']['created'])) :
946 - $epoch = @parse_w3cdtf($this->item['dc']['created']);
947 - elseif (isset($this->item['dcterms']['created'])) :
948 - $epoch = @parse_w3cdtf($this->item['dcterms']['created']);
949 - elseif (isset($this->item['created'])): // Atom 0.3
950 - $epoch = @parse_w3cdtf($this->item['created']);
951 - endif;
952 - return $epoch;
953 - }
954 - function published ($fallback = true) {
955 - $epoch = null;
956 -
957 - # RSS is a fucking mess. Figure out whether we have a date in
958 - # <dc:date>, <issued>, <pubDate>, etc., and get it into Unix
959 - # epoch format for reformatting. If we can't find anything,
960 - # we'll use the last-updated time.
961 - if (isset($this->item['dc']['date'])): // Dublin Core
962 - $epoch = @parse_w3cdtf($this->item['dc']['date']);
963 - elseif (isset($this->item['dcterms']['issued'])) : // Dublin Core extensions
964 - $epoch = @parse_w3cdtf($this->item['dcterms']['issued']);
965 - elseif (isset($this->item['published'])) : // Atom 1.0
966 - $epoch = @parse_w3cdtf($this->item['published']);
967 - elseif (isset($this->item['issued'])): // Atom 0.3
968 - $epoch = @parse_w3cdtf($this->item['issued']);
969 - elseif (isset($this->item['pubdate'])): // RSS 2.0
970 - $epoch = strtotime($this->item['pubdate']);
971 - elseif ($fallback) : // Fall back to <updated> / <modified> if present
972 - $epoch = $this->updated(/*fallback=*/ false);
973 - endif;
974 -
975 - # If everything failed, then default to the current time.
976 - if (is_null($epoch)) :
977 - if (-1 == $default) :
978 - $epoch = time();
979 - else :
980 - $epoch = $default;
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');
981 2422 endif;
982 2423 endif;
983 -
984 - return $epoch;
985 - }
986 - function updated ($fallback = true, $default = -1) {
987 - $epoch = null;
2424 + return $id;
2425 + } /* function SyndicatedPost::author_id () */
988 2426
989 - # As far as I know, only dcterms and Atom have reliable ways to
990 - # specify when something was *modified* last. If neither is
991 - # available, then we'll try to get the time of publication.
992 - if (isset($this->item['dc']['modified'])) : // Not really correct
993 - $epoch = @parse_w3cdtf($this->item['dc']['modified']);
994 - elseif (isset($this->item['dcterms']['modified'])) : // Dublin Core extensions
995 - $epoch = @parse_w3cdtf($this->item['dcterms']['modified']);
996 - elseif (isset($this->item['modified'])): // Atom 0.3
997 - $epoch = @parse_w3cdtf($this->item['modified']);
998 - elseif (isset($this->item['updated'])): // Atom 1.0
999 - $epoch = @parse_w3cdtf($this->item['updated']);
1000 - elseif ($fallback) : // Fall back to issued / dc:date
1001 - $epoch = $this->published(/*fallback=*/ false, /*default=*/ $default);
1002 - endif;
1003 -
1004 - # If everything failed, then default to the current time.
1005 - if (is_null($epoch)) :
1006 - if (-1 == $default) :
1007 - $epoch = time();
1008 - else :
1009 - $epoch = $default;
1010 - endif;
1011 - endif;
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 () */
1012 2439
1013 - return $epoch;
1014 - }
1015 -
1016 - function update_hash () {
1017 - return md5(serialize($this->item));
1018 - }
1019 -
1020 - function guid () {
1021 - $guid = null;
1022 - if (isset($this->item['id'])): // Atom 0.3 / 1.0
1023 - $guid = $this->item['id'];
1024 - elseif (isset($this->item['atom']['id'])) : // Namespaced Atom
1025 - $guid = $this->item['atom']['id'];
1026 - elseif (isset($this->item['guid'])) : // RSS 2.0
1027 - $guid = $this->item['guid'];
1028 - elseif (isset($this->item['dc']['identifier'])) :// yeah, right
1029 - $guid = $this->item['dc']['identifier'];
1030 - else :
1031 - // The feed does not seem to have provided us with a
1032 - // unique identifier, so we'll have to cobble together
1033 - // a tag: URI that might work for us. The base of the
1034 - // URI will be the host name of the feed source ...
1035 - $bits = parse_url($this->feedmeta['link/uri']);
1036 - $guid = 'tag:'.$bits['host'];
1037 -
1038 - // If we have a date of creation, then we can use that
1039 - // to uniquely identify the item. (On the other hand, if
1040 - // the feed producer was consicentious enough to
1041 - // generate dates of creation, she probably also was
1042 - // conscientious enough to generate unique identifiers.)
1043 - if (!is_null($this->created())) :
1044 - $guid .= '://post.'.date('YmdHis', $this->created());
1045 -
1046 - // Otherwise, use both the URI of the item, *and* the
1047 - // item's title. We have to use both because titles are
1048 - // often not unique, and sometimes links aren't unique
1049 - // either (e.g. Bitch (S)HITLIST, Mozilla Dot Org news,
1050 - // some podcasts). But it's rare to have *both* the same
1051 - // title *and* the same link for two different items. So
1052 - // this is about the best we can do.
1053 - else :
1054 - $guid .= '://'.md5($this->item['link'].'/'.$this->item['title']);
1055 - endif;
1056 - endif;
1057 - return $guid;
1058 - }
1059 -
1060 - function author () {
1061 - $author = array ();
1062 -
1063 - if (isset($this->item['author_name'])):
1064 - $author['name'] = $this->item['author_name'];
1065 - elseif (isset($this->item['dc']['creator'])):
1066 - $author['name'] = $this->item['dc']['creator'];
1067 - elseif (isset($this->item['dc']['contributor'])):
1068 - $author['name'] = $this->item['dc']['contributor'];
1069 - elseif (isset($this->feed->channel['dc']['creator'])) :
1070 - $author['name'] = $this->feed->channel['dc']['creator'];
1071 - elseif (isset($this->feed->channel['dc']['contributor'])) :
1072 - $author['name'] = $this->feed->channel['dc']['contributor'];
1073 - elseif (isset($this->feed->channel['author_name'])) :
1074 - $author['name'] = $this->feed->channel['author_name'];
1075 - elseif ($this->feed->is_rss() and isset($this->item['author'])) :
1076 - // The author element in RSS is allegedly an
1077 - // e-mail address, but lots of people don't use
1078 - // it that way. So let's make of it what we can.
1079 - $author = parse_email_with_realname($this->item['author']);
1080 -
1081 - if (!isset($author['name'])) :
1082 - if (isset($author['email'])) :
1083 - $author['name'] = $author['email'];
1084 - else :
1085 - $author['name'] = $this->feed->channel['title'];
1086 - endif;
1087 - endif;
1088 - else :
1089 - $author['name'] = $this->feed->channel['title'];
1090 - endif;
1091 -
1092 - if (isset($this->item['author_email'])):
1093 - $author['email'] = $this->item['author_email'];
1094 - elseif (isset($this->feed->channel['author_email'])) :
1095 - $author['email'] = $this->feed->channel['author_email'];
1096 - endif;
1097 -
1098 - if (isset($this->item['author_url'])):
1099 - $author['uri'] = $this->item['author_url'];
1100 - elseif (isset($this->feed->channel['author_url'])) :
1101 - $author['uri'] = $this->item['author_url'];
1102 - else:
1103 - $author['uri'] = $this->feed->channel['link'];
1104 - endif;
1105 -
1106 - return $author;
1107 - } // SyndicatedPost::author()
1108 -
1109 - var $uri_attrs = array (
1110 - array('a', 'href'),
1111 - array('applet', 'codebase'),
1112 - array('area', 'href'),
1113 - array('blockquote', 'cite'),
1114 - array('body', 'background'),
1115 - array('del', 'cite'),
1116 - array('form', 'action'),
1117 - array('frame', 'longdesc'),
1118 - array('frame', 'src'),
1119 - array('iframe', 'longdesc'),
1120 - array('iframe', 'src'),
1121 - array('head', 'profile'),
1122 - array('img', 'longdesc'),
1123 - array('img', 'src'),
1124 - array('img', 'usemap'),
1125 - array('input', 'src'),
1126 - array('input', 'usemap'),
1127 - array('ins', 'cite'),
1128 - array('link', 'href'),
1129 - array('object', 'classid'),
1130 - array('object', 'codebase'),
1131 - array('object', 'data'),
1132 - array('object', 'usemap'),
1133 - array('q', 'cite'),
1134 - array('script', 'src')
1135 - ); /* var SyndicatedPost::$uri_attrs */
1136 -
1137 - var $_base = null;
1138 -
1139 - function resolve_single_relative_uri ($refs) {
1140 - $tag = FeedWordPressHTML::attributeMatch($refs);
1141 - $url = Relative_URI::resolve($tag['value'], $this->_base);
1142 - return $tag['prefix'] . $url . $tag['suffix'];
1143 - } /* function SyndicatedPost::resolve_single_relative_uri() */
1144 -
1145 - function resolve_relative_uris ($content, $obj) {
1146 - # The MagpieRSS upgrade has some `xml:base` support baked in.
1147 - # However, sometimes people do silly things, like putting
1148 - # relative URIs out on a production RSS 2.0 feed or other feeds
1149 - # with no good support for `xml:base`. So we'll do our best to
1150 - # try to catch any remaining relative URIs and resolve them as
1151 - # best we can.
1152 - $obj->_base = $obj->item['link']; // Reset the base for resolving relative URIs
1153 -
1154 - foreach ($obj->uri_attrs as $pair) :
1155 - list($tag, $attr) = $pair;
1156 - $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1157 - $content = preg_replace_callback (
1158 - $pattern,
1159 - array(&$obj, 'resolve_single_relative_uri'),
1160 - $content
1161 - );
1162 - endforeach;
1163 -
1164 - return $content;
1165 - } /* function SyndicatedPost::resolve_relative_uris () */
1166 -
1167 - var $strip_attrs = array (
1168 - array('[a-z]+', 'target'),
1169 -// array('[a-z]+', 'style'),
1170 -// array('[a-z]+', 'on[a-z]+'),
1171 - );
1172 -
1173 - function strip_attribute_from_tag ($refs) {
1174 - $tag = FeedWordPressHTML::attributeMatch($refs);
1175 - return $tag['before_attribute'].$tag['after_attribute'];
1176 - }
1177 -
1178 - function sanitize_content ($content, $obj) {
1179 - # This kind of sucks. I intend to replace it with
1180 - # lib_filter sometime soon.
1181 - foreach ($obj->strip_attrs as $pair):
1182 - list($tag,$attr) = $pair;
1183 - $pattern = FeedWordPressHTML::attributeRegex($tag, $attr);
1184 -
1185 - $content = preg_replace_callback (
1186 - $pattern,
1187 - array(&$obj, 'strip_attribute_from_tag'),
1188 - $content
1189 - );
1190 - endforeach;
1191 - return $content;
1192 - }
1193 -} // class SyndicatedPost
1194 -
2440 +} /* class SyndicatedPost */