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