Changeset 2871614
- Timestamp:
- 02/27/2023 10:58:46 AM (3 years ago)
- Location:
- restful-syndication
- Files:
-
- 2 edited
- 3 copied
-
tags/1.3.0 (copied) (copied from restful-syndication/trunk)
-
tags/1.3.0/index.php (copied) (copied from restful-syndication/trunk/index.php) (20 diffs)
-
tags/1.3.0/readme.txt (copied) (copied from restful-syndication/trunk/readme.txt) (3 diffs)
-
trunk/index.php (modified) (20 diffs)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
restful-syndication/tags/1.3.0/index.php
r2845739 r2871614 4 4 Plugin URI: https://mediarealm.com.au/ 5 5 Description: Import content from the Wordpress REST API on another Wordpress site 6 Version: 1. 2.16 Version: 1.3.0 7 7 Author: Media Realm 8 8 Author URI: https://www.mediarealm.com.au/ … … 74 74 "options" => array() 75 75 ), 76 "purge_media_days" => array( 77 "title" => "Auto Purge Media After X Days", 78 "type" => "text", 79 "default" => "", 80 ), 81 "purge_posts_days" => array( 82 "title" => "Auto Purge Posts After X Days", 83 "type" => "text", 84 "default" => "", 85 ), 76 86 "remote_push_key" => array( 77 87 "title" => "Remote Content Push - Secure Key", … … 84 94 85 95 ); 96 97 public $errors_logged = array(); 86 98 87 99 public function __construct() { … … 90 102 add_action('restful-syndication_cron', array($this, 'syndicate')); 91 103 add_filter('cron_schedules', array($this, 'cron_schedules')); 104 105 add_shortcode('restful_syndication_iframe', array($this, 'sc_iframe')); 92 106 93 107 add_action('rest_api_init', function () { … … 106 120 if(is_array($log) || is_object($log)) { 107 121 error_log($this->errors_prefix . print_r($log, true)); 122 $errors_logged[] = print_r($log, true); 108 123 } else { 109 124 error_log($this->errors_prefix . $log); 125 $errors_logged[] = $log; 110 126 } 111 127 } … … 177 193 if($field['type'] == "text") { 178 194 // Text fields 179 echo '<input type="text" name="restful-syndication_settings['. $args['field_key'].']" value="'.htmlspecialchars($value, ENT_QUOTES).'" />';195 echo '<input type="text" name="restful-syndication_settings['.esc_attr($args['field_key']).']" value="'.esc_attr($value).'" />'; 180 196 } elseif($field['type'] == "textarea") { 181 197 // Textarea fields 182 echo '<textarea name="restful-syndication_settings['. $args['field_key'].']">'.htmlspecialchars($value, ENT_QUOTES).'</textarea>';198 echo '<textarea name="restful-syndication_settings['.esc_attr($args['field_key']).']">'.esc_html($value).'</textarea>'; 183 199 } elseif($field['type'] == "password") { 184 200 // Password fields 185 echo '<input type="password" name="restful-syndication_settings['. $args['field_key'].']" value="'.htmlspecialchars($value, ENT_QUOTES).'" />';201 echo '<input type="password" name="restful-syndication_settings['.esc_attr($args['field_key']).']" value="'.esc_attr($value).'" />'; 186 202 } elseif($field['type'] == "select") { 187 203 // Select / drop-down fields 188 204 echo '<select name="restful-syndication_settings['.$args['field_key'].']">'; 189 205 foreach($field['options'] as $selectValue => $name) { 190 echo '<option value="'. $selectValue.'" '.($value == $selectValue ? "selected" : "").'>'.$name.'</option>';206 echo '<option value="'.esc_attr($selectValue).'" '.($value == $selectValue ? "selected" : "").'>'.esc_html($name).'</option>'; 191 207 } 192 208 echo '</select>'; 193 209 } elseif($field['type'] == "checkbox") { 194 210 // Checkbox fields 195 echo '<input type="checkbox" name="restful-syndication_settings['. $args['field_key'].']" value="true" '.("true" == $value ? "checked" : "").' />';211 echo '<input type="checkbox" name="restful-syndication_settings['.esc_attr($args['field_key']).']" value="true" '.("true" == $value ? "checked" : "").' />'; 196 212 } elseif($field['type'] == "readonly") { 197 213 // Readonly field 198 echo '<input type="text" name="restful-syndication_settings['. $args['field_key'].']" value="'.htmlspecialchars($value, ENT_QUOTES).'" readonly />';214 echo '<input type="text" name="restful-syndication_settings['.esc_attr($args['field_key']).']" value="'.esc_attr($value).'" readonly />'; 199 215 } 200 216 } … … 212 228 // Display a history of successful syndication runs 213 229 $runs = get_option($this->settings_prefix . 'history', array()); 230 $runs_delete = get_option($this->settings_prefix . 'history_delete', array()); 231 $runs_delete_media = get_option($this->settings_prefix . 'history_delete_media', array()); 214 232 $last_attempt = get_option($this->settings_prefix . 'last_attempt', 0); 215 233 krsort($runs); 234 krsort($runs_delete); 235 krsort($runs_delete_media); 216 236 217 237 echo '<h2>Syndication History</h2>'; … … 230 250 echo '<li>No posts have ever been ingested by this plugin</li>'; 231 251 } 252 $runCount = 0; 253 foreach($runs_delete as $time => $count) { 254 echo '<li>'.date("Y-m-d H:i:s", $time).': '.$count.' '.($count == 1 ? "post" : "posts").' deleted</li>'; 255 $runCount++; 256 257 if($runCount > 10) 258 break; 259 } 260 $runCount = 0; 261 foreach($runs_delete_media as $time => $count) { 262 echo '<li>'.date("Y-m-d H:i:s", $time).': '.$count.' media files deleted</li>'; 263 $runCount++; 264 265 if($runCount > 10) 266 break; 267 } 232 268 echo '</ul>'; 233 269 … … 236 272 echo "<p>This plugin uses WP-Cron to automatically ingest posts every 15 minutes. If you're impatient, you can do it now using the button below.</p>"; 237 273 if(isset($_GET['ingestnow']) && $_GET['ingestnow'] == "true") { 274 275 // Verify nonce 276 check_admin_referer('restful_syndication_ingest'); 277 238 278 echo "<p><strong>Attempting syndication now...</strong></p>"; 239 279 $this->syndicate(); 240 280 echo '<p><strong>Syndication complete!</strong></p>'; 241 281 } else { 242 echo '<p class="submit"><a href=" ?page='.$_GET['page'].'&ingestnow=true" class="button button-primary">Ingest Posts Now</a></p>';282 echo '<p class="submit"><a href="'.wp_nonce_url('?page='.$_GET['page'].'&ingestnow=true', 'restful_syndication_ingest').'" class="button button-primary">Ingest Posts Now</a></p>'; 243 283 } 244 284 … … 270 310 271 311 if(is_wp_error($response)) { 272 $this->log("HTTP request failed when calling WP REST API. ");312 $this->log("HTTP request failed when calling WP REST API. " . $response->get_error_message()); 273 313 return; 274 314 } … … 296 336 297 337 $count = 0; 298 338 $count_delete = 0; 339 $count_delete_media = 0; 340 341 // Loop over every post and create a post entry 299 342 foreach($posts as $post) { 300 // Loop over every post and create a post entry301 343 $this->syndicate_one($post); 302 344 $count++; 303 345 } 346 347 // Delete media older than a certain date 348 if(is_numeric($options['purge_media_days']) && $options['purge_media_days'] > 7) { 349 $count_delete_media += count($this->clean_media($options['purge_media_days'], 'post')); 350 } 351 352 // Delete posts older than a certain date 353 if(is_numeric($options['purge_posts_days']) && $options['purge_posts_days'] > 7) { 354 $count_delete += count($this->clean_posts($options['purge_posts_days'], 'post')); 304 355 } 305 356 … … 308 359 $runs[time()] = $count; 309 360 update_option($this->settings_prefix . 'history', $runs); 361 } 362 363 if($count_delete > 0) { 364 $runs = get_option($this->settings_prefix . 'history_delete', array()); 365 $runs[time()] = $count_delete; 366 update_option($this->settings_prefix . 'history_delete', $runs); 367 } 368 369 if($count_delete_media > 0) { 370 $runs = get_option($this->settings_prefix . 'history_delete_media', array()); 371 $runs[time()] = $count_delete_media; 372 update_option($this->settings_prefix . 'history_delete_media', $runs); 310 373 } 311 374 … … 332 395 } 333 396 334 // Download any embedded images found in the HTML 397 // Check for empty fields and fail early 398 if(!isset($post['guid']['rendered']) || empty($post['guid']['rendered'])) { 399 $this->log("Post GUID is empty - skipping."); 400 return; 401 } 402 403 $post['title']['rendered'] = trim($post['title']['rendered']); 404 405 if(empty($post['title']['rendered'])) { 406 $this->log("Post title is empty - skipping. " . $post['guid']['rendered']); 407 return; 408 } 409 410 if(!isset($post['content']['rendered']) || empty($post['content']['rendered'])) { 411 $this->log("Post body is empty - skipping. " . $post['guid']['rendered']); 412 return; 413 } 414 415 // Strip some problematic conditional tags from the HTML 416 $html = $post['content']['rendered']; 417 $html = str_replace("<!--[if lt IE 9]>", "", $html); 418 $html = str_replace("<![endif]-->", "", $html); 419 420 // Parse the HTML 335 421 $dom = new domDocument; 336 $dom->loadHTML('<?xml encoding="utf-8" ?>' . $post['content']['rendered'], LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); 337 422 $dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); 423 424 // Find and download any embedded images found in the HTML 338 425 $images = $dom->getElementsByTagName('img'); 339 426 $images_to_attach = array(); … … 375 462 $url = $audio_source->item(0)->getAttribute('src'); 376 463 464 if(empty($url)) { 465 continue; 466 } 467 377 468 // There is a bug in Wordpress causing audio URLs with URL Parameters to fail to load the player 378 469 // See https://core.trac.wordpress.org/ticket/30377 379 // As a workaround, we strip URL parameters470 // As a partial workaround, we strip URL parameters 380 471 if(strpos($url, "?") !== false) { 381 472 $url = substr($url, 0, strpos($url, "?")); … … 383 474 384 475 // Create a new paragraph, and insert the audio shortcode 385 $audio_shortcode = $dom->createElement('p'); 386 $audio_shortcode->nodeValue = '[audio src="'.$url.'"]'; 387 388 // Replace the original <audio> tag with this new <p>[audio]</p> arrangement 476 $audio_shortcode = $dom->createElement('div'); 477 $audio_shortcode->setAttribute('class', 'audio-filter'); 478 $audio_shortcode->nodeValue = '[audio src="'.esc_url($url).'"]'; 479 480 // Replace the original <audio> tag with this new <div class="audio-filter">[audio]</div> arrangement 389 481 $audio->parentNode->replaceChild($audio_shortcode, $audio); 390 482 } … … 396 488 397 489 // Skip non-youtube divs 398 if(!$youtube->hasAttribute('class') || strpos($youtube->getAttribute('class'), 'embed_youtube') === false)490 if(!$youtube->hasAttribute('class') || (strpos($youtube->getAttribute('class'), 'embed_youtube') === false && strpos($youtube->getAttribute('class'), 'video-filter') === false)) 399 491 continue; 400 492 … … 409 501 410 502 // Create a new paragraph, and insert the audio shortcode 411 $embed_shortcode = $dom->createElement('p'); 412 $embed_shortcode->nodeValue = '[embed]'.$url_new.'[/embed]'; 413 414 // Replace the original <div class="embed_youtube"> tag with this new <p>[embed]url[/embed]</p> arrangement 503 $embed_shortcode = $dom->createElement('div'); 504 $embed_shortcode->setAttribute('class', 'video-filter'); 505 $embed_shortcode->nodeValue = '[embed]'.esc_url($url_new).'[/embed]'; 506 507 // Replace the original <div class="embed_youtube"> tag with this new <div>[embed]url[/embed]</div> arrangement 415 508 $youtube->parentNode->replaceChild($embed_shortcode, $youtube); 416 509 } 510 } 511 512 // Find Instagram embeds, and turn them into [restful_syndication_iframe] shortcodes 513 $instagrams = $dom->getElementsByTagName('blockquote'); 514 515 foreach($instagrams as $instagram) { 516 517 // Skip non-youtube blockquotes 518 if(!$instagram->hasAttribute('data-instgrm-permalink')) 519 continue; 520 521 // Get the original Instagram URL 522 $url = $instagram->getAttribute('data-instgrm-permalink'); 523 524 // Skip empty URLs 525 if(empty($url)) { 526 continue; 527 } 528 529 // Add /embed to URL 530 $url_parsed = parse_url($url); 531 if($url_parsed === false) { 532 continue; 533 } 534 $url = $url_parsed['scheme'] . "://" . $url_parsed['host'] . str_replace("//", "/", $url_parsed['path'] . "/embed") . "?" . $url_parsed['query']; 535 536 // Get width and height 537 $width = '100%'; 538 $height = '650'; 539 540 // Create a new paragraph, and insert the iframe shortcode 541 $embed_shortcode = $dom->createElement('p'); 542 $embed_shortcode->nodeValue = '[restful_syndication_iframe src="'.esc_url($url).'" width="'.esc_attr($width).'" height="'.esc_attr($height).'"]'; 543 544 // Replace the original <iframe> tag with this new <p>[restful_syndication_iframe src="url"]</p> arrangement 545 $instagram->parentNode->replaceChild($embed_shortcode, $instagram); 546 } 547 548 // Find iFrames, and turn them into [restful_syndication_iframe] shortcodes 549 $iframes = $dom->getElementsByTagName('iframe'); 550 551 foreach($iframes as $iframeKey => $iframe) { 552 553 // Skip iframes without src field 554 if(!$iframe->hasAttribute('src')) { 555 continue; 556 } 557 558 // Get the original iFrame src URL 559 $url = $iframe->getAttribute('src'); 560 561 // Skip empty URLs 562 if(empty($url)) { 563 continue; 564 } 565 566 // Get width and height 567 $width = '100%'; 568 $height = '300'; 569 if($iframe->hasAttribute('height')) { 570 $height = $iframe->getAttribute('height'); 571 } 572 573 // Create a new paragraph, and insert the iframe shortcode 574 $embed_shortcode = $dom->createElement('p'); 575 $embed_shortcode->nodeValue = '[restful_syndication_iframe src="'.esc_url($url).'" width="'.esc_attr($width).'" height="'.esc_attr($height).'"]'; 576 577 // Replace the original <iframe> tag with this new <p>[restful_syndication_iframe src="url"]</p> arrangement 578 $iframe->parentNode->replaceChild($embed_shortcode, $iframe); 417 579 } 418 580 … … 609 771 610 772 if(empty($payload) || $payload == null) { 611 return array("error_msg" => 'Failed to fetch post data from API' );773 return array("error_msg" => 'Failed to fetch post data from API', "errors" => $errors_logged); 612 774 } 613 775 … … 623 785 624 786 return array("post_id" => $post_id); 787 } 788 789 private function clean_media($days, $post_type) { 790 // Deletes media after a certain age 791 792 if(!is_numeric($days)) { 793 return; 794 } 795 796 $days = absint($days); 797 798 if($days < 7) { 799 return; 800 } 801 802 // Find posts 803 $post_ids = $this->posts_older_than($days, $post_type); 804 805 $return = array(); 806 807 // Loop over posts and find featured images attached to these posts 808 foreach($post_ids as $post_id) { 809 $image_id = get_post_thumbnail_id($post_id); 810 811 if($image_id === false || $image_id === 0) { 812 continue; 813 } 814 815 // Send the attachment to the trash 816 $delete_image = wp_delete_attachment($image_id, false); 817 818 if($delete_image !== false && $delete_image !== null) { 819 $return[] = $image_id; 820 } 821 } 822 823 return $return; 824 } 825 826 private function clean_posts($days, $post_type) { 827 // Deletes posts after a certain age 828 829 if(!is_numeric($days)) { 830 return; 831 } 832 833 $days = absint($days); 834 835 if($days < 7) { 836 return; 837 } 838 839 // Find posts 840 $post_ids = $this->posts_older_than($days, $post_type); 841 842 $return = array(); 843 844 // Loop over posts 845 foreach($post_ids as $post_id) { 846 // Delete featured image first 847 $image_id = get_post_thumbnail_id($post_id); 848 849 if($image_id !== false) { 850 // Send the attachment to the trash 851 $delete_image = wp_delete_attachment($image_id, false); 852 853 if($delete_image !== false) { 854 $return[] = $image_id; 855 } 856 } 857 858 // Now delete the post itself 859 $delete_post = wp_trash_post($post_id); 860 if($delete_post !== false) { 861 $return[] = $post_id; 862 } 863 } 864 865 return $return; 866 } 867 868 private function posts_older_than($days, $post_type) { 869 // Returns a list of Posts older than a certain age 870 871 if(!is_numeric($days)) { 872 return array(); 873 } 874 875 $days = absint($days); 876 877 $options = get_option($this->settings_prefix . 'settings'); 878 $domain = parse_url($options['site_url'], PHP_URL_HOST); 879 $domain_1 = 'https://' . $domain; 880 $domain_2 = 'http://' . $domain; 881 882 if($domain_1 == 'https://' || $domain_2 == 'http://') { 883 return array(); 884 } 885 886 $posts = new WP_Query(array( 887 'post_type' => $post_type, 888 'orderby' => 'post_date_gmt', 889 'order' => 'ASC', 890 'meta_query' => array( 891 'relation' => 'OR', 892 'meta_value_1' => array( 893 'key' => '_'.$this->settings_prefix.'source_guid', 894 'value' => $domain_1, 895 'compare' => 'LIKE', 896 ), 897 'meta_value_2' => array( 898 'key' => '_'.$this->settings_prefix.'source_guid', 899 'value' => $domain_2, 900 'compare' => 'LIKE', 901 ) 902 ), 903 'date_query' => array( 904 array( 905 'column' => 'post_date_gmt', 906 'before' => $days . ' days ago', 907 ), 908 ), 909 'fields' => 'ids', 910 'posts_per_page' => -1, 911 )); 912 913 return $posts->posts; 625 914 } 626 915 … … 728 1017 } 729 1018 1019 public function sc_iframe($atts) { 1020 // This iFrame can only be used on posts imported by this plugin 1021 // It is a security mechanism instead of just allowing iFrame's for all users accross the site 1022 // We assume the source site is at least semi-trusted (trusted enough to embed an iframe at least) 1023 1024 $a = shortcode_atts(array( 1025 "src" => "", 1026 "width" => "100%", 1027 "height" => "200", 1028 ), $atts); 1029 1030 global $post; 1031 if(!isset($post)) { 1032 return ''; 1033 } 1034 1035 $source_guid = get_post_meta($post->ID, '_'.$this->settings_prefix.'source_guid', true); 1036 1037 if(empty($source_guid)) { 1038 return ''; 1039 } 1040 1041 return '<iframe src="'.esc_url($a['src']).'" width="'.esc_attr($a['width']).'" height="'.esc_attr($a['height']).'" border="0"></iframe>'; 1042 } 1043 730 1044 private function random_str($length, $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-=+`~') { 731 1045 // From https://stackoverflow.com/a/31284266/2888733 -
restful-syndication/tags/1.3.0/readme.txt
r2845739 r2871614 20 20 == Installation == 21 21 22 1. Upload the plugin files to the `/wp-content/plugins/restful-syndication` directory, or install the plugin through the WordPress plugins screen directly.22 1. Install this plugin through the WordPress plugins screen. 23 23 2. Activate the plugin through the 'Plugins' screen in WordPress 24 3. Use the Settings ->RESTful Syndication screen to configure the plugin24 3. Use the Settings -> RESTful Syndication screen to configure the plugin 25 25 a. Set the 'Master Site URL' to the base URL of the site to pull the content from (e.g. https://example.com/) 26 b. Username and Password are not required by default (but may be required by the person running the Master Site) 26 b. Username and Password are not required by default (but may be required by the person running the Master Site). You can use the WordPress Applicaton Password feature for authentication. 27 27 c. Set the other options as desired 28 28 d. Save your settings … … 47 47 = Something isn't working. What do I do? = 48 48 49 Find the PHP Error log for your website/web-server. Any errors from this plugin should be prefixed with 'RESTful Syndication ERROR'.49 Find the PHP Error Log for your website/web-server. Any errors from this plugin should be prefixed with 'RESTful Syndication ERROR'. 50 50 51 51 = Do you provide support? = … … 58 58 59 59 == Changelog == 60 61 = 1.3.0 = 62 63 * Add new options to purge media & posts older than a certain number of days 64 * Additional compatibility for YouTube and Audio embeds 65 * Allow iFrames to be syndicated 66 * Translate Instagram embeds into iFrames 67 * Bugfix for Audio embeds 68 * Catch errors causing empty posts to be syndicated 69 * Security hardening on the admin screen 70 * Additional logging details 60 71 61 72 = 1.2.1 = -
restful-syndication/trunk/index.php
r2845739 r2871614 4 4 Plugin URI: https://mediarealm.com.au/ 5 5 Description: Import content from the Wordpress REST API on another Wordpress site 6 Version: 1. 2.16 Version: 1.3.0 7 7 Author: Media Realm 8 8 Author URI: https://www.mediarealm.com.au/ … … 74 74 "options" => array() 75 75 ), 76 "purge_media_days" => array( 77 "title" => "Auto Purge Media After X Days", 78 "type" => "text", 79 "default" => "", 80 ), 81 "purge_posts_days" => array( 82 "title" => "Auto Purge Posts After X Days", 83 "type" => "text", 84 "default" => "", 85 ), 76 86 "remote_push_key" => array( 77 87 "title" => "Remote Content Push - Secure Key", … … 84 94 85 95 ); 96 97 public $errors_logged = array(); 86 98 87 99 public function __construct() { … … 90 102 add_action('restful-syndication_cron', array($this, 'syndicate')); 91 103 add_filter('cron_schedules', array($this, 'cron_schedules')); 104 105 add_shortcode('restful_syndication_iframe', array($this, 'sc_iframe')); 92 106 93 107 add_action('rest_api_init', function () { … … 106 120 if(is_array($log) || is_object($log)) { 107 121 error_log($this->errors_prefix . print_r($log, true)); 122 $errors_logged[] = print_r($log, true); 108 123 } else { 109 124 error_log($this->errors_prefix . $log); 125 $errors_logged[] = $log; 110 126 } 111 127 } … … 177 193 if($field['type'] == "text") { 178 194 // Text fields 179 echo '<input type="text" name="restful-syndication_settings['. $args['field_key'].']" value="'.htmlspecialchars($value, ENT_QUOTES).'" />';195 echo '<input type="text" name="restful-syndication_settings['.esc_attr($args['field_key']).']" value="'.esc_attr($value).'" />'; 180 196 } elseif($field['type'] == "textarea") { 181 197 // Textarea fields 182 echo '<textarea name="restful-syndication_settings['. $args['field_key'].']">'.htmlspecialchars($value, ENT_QUOTES).'</textarea>';198 echo '<textarea name="restful-syndication_settings['.esc_attr($args['field_key']).']">'.esc_html($value).'</textarea>'; 183 199 } elseif($field['type'] == "password") { 184 200 // Password fields 185 echo '<input type="password" name="restful-syndication_settings['. $args['field_key'].']" value="'.htmlspecialchars($value, ENT_QUOTES).'" />';201 echo '<input type="password" name="restful-syndication_settings['.esc_attr($args['field_key']).']" value="'.esc_attr($value).'" />'; 186 202 } elseif($field['type'] == "select") { 187 203 // Select / drop-down fields 188 204 echo '<select name="restful-syndication_settings['.$args['field_key'].']">'; 189 205 foreach($field['options'] as $selectValue => $name) { 190 echo '<option value="'. $selectValue.'" '.($value == $selectValue ? "selected" : "").'>'.$name.'</option>';206 echo '<option value="'.esc_attr($selectValue).'" '.($value == $selectValue ? "selected" : "").'>'.esc_html($name).'</option>'; 191 207 } 192 208 echo '</select>'; 193 209 } elseif($field['type'] == "checkbox") { 194 210 // Checkbox fields 195 echo '<input type="checkbox" name="restful-syndication_settings['. $args['field_key'].']" value="true" '.("true" == $value ? "checked" : "").' />';211 echo '<input type="checkbox" name="restful-syndication_settings['.esc_attr($args['field_key']).']" value="true" '.("true" == $value ? "checked" : "").' />'; 196 212 } elseif($field['type'] == "readonly") { 197 213 // Readonly field 198 echo '<input type="text" name="restful-syndication_settings['. $args['field_key'].']" value="'.htmlspecialchars($value, ENT_QUOTES).'" readonly />';214 echo '<input type="text" name="restful-syndication_settings['.esc_attr($args['field_key']).']" value="'.esc_attr($value).'" readonly />'; 199 215 } 200 216 } … … 212 228 // Display a history of successful syndication runs 213 229 $runs = get_option($this->settings_prefix . 'history', array()); 230 $runs_delete = get_option($this->settings_prefix . 'history_delete', array()); 231 $runs_delete_media = get_option($this->settings_prefix . 'history_delete_media', array()); 214 232 $last_attempt = get_option($this->settings_prefix . 'last_attempt', 0); 215 233 krsort($runs); 234 krsort($runs_delete); 235 krsort($runs_delete_media); 216 236 217 237 echo '<h2>Syndication History</h2>'; … … 230 250 echo '<li>No posts have ever been ingested by this plugin</li>'; 231 251 } 252 $runCount = 0; 253 foreach($runs_delete as $time => $count) { 254 echo '<li>'.date("Y-m-d H:i:s", $time).': '.$count.' '.($count == 1 ? "post" : "posts").' deleted</li>'; 255 $runCount++; 256 257 if($runCount > 10) 258 break; 259 } 260 $runCount = 0; 261 foreach($runs_delete_media as $time => $count) { 262 echo '<li>'.date("Y-m-d H:i:s", $time).': '.$count.' media files deleted</li>'; 263 $runCount++; 264 265 if($runCount > 10) 266 break; 267 } 232 268 echo '</ul>'; 233 269 … … 236 272 echo "<p>This plugin uses WP-Cron to automatically ingest posts every 15 minutes. If you're impatient, you can do it now using the button below.</p>"; 237 273 if(isset($_GET['ingestnow']) && $_GET['ingestnow'] == "true") { 274 275 // Verify nonce 276 check_admin_referer('restful_syndication_ingest'); 277 238 278 echo "<p><strong>Attempting syndication now...</strong></p>"; 239 279 $this->syndicate(); 240 280 echo '<p><strong>Syndication complete!</strong></p>'; 241 281 } else { 242 echo '<p class="submit"><a href=" ?page='.$_GET['page'].'&ingestnow=true" class="button button-primary">Ingest Posts Now</a></p>';282 echo '<p class="submit"><a href="'.wp_nonce_url('?page='.$_GET['page'].'&ingestnow=true', 'restful_syndication_ingest').'" class="button button-primary">Ingest Posts Now</a></p>'; 243 283 } 244 284 … … 270 310 271 311 if(is_wp_error($response)) { 272 $this->log("HTTP request failed when calling WP REST API. ");312 $this->log("HTTP request failed when calling WP REST API. " . $response->get_error_message()); 273 313 return; 274 314 } … … 296 336 297 337 $count = 0; 298 338 $count_delete = 0; 339 $count_delete_media = 0; 340 341 // Loop over every post and create a post entry 299 342 foreach($posts as $post) { 300 // Loop over every post and create a post entry301 343 $this->syndicate_one($post); 302 344 $count++; 303 345 } 346 347 // Delete media older than a certain date 348 if(is_numeric($options['purge_media_days']) && $options['purge_media_days'] > 7) { 349 $count_delete_media += count($this->clean_media($options['purge_media_days'], 'post')); 350 } 351 352 // Delete posts older than a certain date 353 if(is_numeric($options['purge_posts_days']) && $options['purge_posts_days'] > 7) { 354 $count_delete += count($this->clean_posts($options['purge_posts_days'], 'post')); 304 355 } 305 356 … … 308 359 $runs[time()] = $count; 309 360 update_option($this->settings_prefix . 'history', $runs); 361 } 362 363 if($count_delete > 0) { 364 $runs = get_option($this->settings_prefix . 'history_delete', array()); 365 $runs[time()] = $count_delete; 366 update_option($this->settings_prefix . 'history_delete', $runs); 367 } 368 369 if($count_delete_media > 0) { 370 $runs = get_option($this->settings_prefix . 'history_delete_media', array()); 371 $runs[time()] = $count_delete_media; 372 update_option($this->settings_prefix . 'history_delete_media', $runs); 310 373 } 311 374 … … 332 395 } 333 396 334 // Download any embedded images found in the HTML 397 // Check for empty fields and fail early 398 if(!isset($post['guid']['rendered']) || empty($post['guid']['rendered'])) { 399 $this->log("Post GUID is empty - skipping."); 400 return; 401 } 402 403 $post['title']['rendered'] = trim($post['title']['rendered']); 404 405 if(empty($post['title']['rendered'])) { 406 $this->log("Post title is empty - skipping. " . $post['guid']['rendered']); 407 return; 408 } 409 410 if(!isset($post['content']['rendered']) || empty($post['content']['rendered'])) { 411 $this->log("Post body is empty - skipping. " . $post['guid']['rendered']); 412 return; 413 } 414 415 // Strip some problematic conditional tags from the HTML 416 $html = $post['content']['rendered']; 417 $html = str_replace("<!--[if lt IE 9]>", "", $html); 418 $html = str_replace("<![endif]-->", "", $html); 419 420 // Parse the HTML 335 421 $dom = new domDocument; 336 $dom->loadHTML('<?xml encoding="utf-8" ?>' . $post['content']['rendered'], LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); 337 422 $dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); 423 424 // Find and download any embedded images found in the HTML 338 425 $images = $dom->getElementsByTagName('img'); 339 426 $images_to_attach = array(); … … 375 462 $url = $audio_source->item(0)->getAttribute('src'); 376 463 464 if(empty($url)) { 465 continue; 466 } 467 377 468 // There is a bug in Wordpress causing audio URLs with URL Parameters to fail to load the player 378 469 // See https://core.trac.wordpress.org/ticket/30377 379 // As a workaround, we strip URL parameters470 // As a partial workaround, we strip URL parameters 380 471 if(strpos($url, "?") !== false) { 381 472 $url = substr($url, 0, strpos($url, "?")); … … 383 474 384 475 // Create a new paragraph, and insert the audio shortcode 385 $audio_shortcode = $dom->createElement('p'); 386 $audio_shortcode->nodeValue = '[audio src="'.$url.'"]'; 387 388 // Replace the original <audio> tag with this new <p>[audio]</p> arrangement 476 $audio_shortcode = $dom->createElement('div'); 477 $audio_shortcode->setAttribute('class', 'audio-filter'); 478 $audio_shortcode->nodeValue = '[audio src="'.esc_url($url).'"]'; 479 480 // Replace the original <audio> tag with this new <div class="audio-filter">[audio]</div> arrangement 389 481 $audio->parentNode->replaceChild($audio_shortcode, $audio); 390 482 } … … 396 488 397 489 // Skip non-youtube divs 398 if(!$youtube->hasAttribute('class') || strpos($youtube->getAttribute('class'), 'embed_youtube') === false)490 if(!$youtube->hasAttribute('class') || (strpos($youtube->getAttribute('class'), 'embed_youtube') === false && strpos($youtube->getAttribute('class'), 'video-filter') === false)) 399 491 continue; 400 492 … … 409 501 410 502 // Create a new paragraph, and insert the audio shortcode 411 $embed_shortcode = $dom->createElement('p'); 412 $embed_shortcode->nodeValue = '[embed]'.$url_new.'[/embed]'; 413 414 // Replace the original <div class="embed_youtube"> tag with this new <p>[embed]url[/embed]</p> arrangement 503 $embed_shortcode = $dom->createElement('div'); 504 $embed_shortcode->setAttribute('class', 'video-filter'); 505 $embed_shortcode->nodeValue = '[embed]'.esc_url($url_new).'[/embed]'; 506 507 // Replace the original <div class="embed_youtube"> tag with this new <div>[embed]url[/embed]</div> arrangement 415 508 $youtube->parentNode->replaceChild($embed_shortcode, $youtube); 416 509 } 510 } 511 512 // Find Instagram embeds, and turn them into [restful_syndication_iframe] shortcodes 513 $instagrams = $dom->getElementsByTagName('blockquote'); 514 515 foreach($instagrams as $instagram) { 516 517 // Skip non-youtube blockquotes 518 if(!$instagram->hasAttribute('data-instgrm-permalink')) 519 continue; 520 521 // Get the original Instagram URL 522 $url = $instagram->getAttribute('data-instgrm-permalink'); 523 524 // Skip empty URLs 525 if(empty($url)) { 526 continue; 527 } 528 529 // Add /embed to URL 530 $url_parsed = parse_url($url); 531 if($url_parsed === false) { 532 continue; 533 } 534 $url = $url_parsed['scheme'] . "://" . $url_parsed['host'] . str_replace("//", "/", $url_parsed['path'] . "/embed") . "?" . $url_parsed['query']; 535 536 // Get width and height 537 $width = '100%'; 538 $height = '650'; 539 540 // Create a new paragraph, and insert the iframe shortcode 541 $embed_shortcode = $dom->createElement('p'); 542 $embed_shortcode->nodeValue = '[restful_syndication_iframe src="'.esc_url($url).'" width="'.esc_attr($width).'" height="'.esc_attr($height).'"]'; 543 544 // Replace the original <iframe> tag with this new <p>[restful_syndication_iframe src="url"]</p> arrangement 545 $instagram->parentNode->replaceChild($embed_shortcode, $instagram); 546 } 547 548 // Find iFrames, and turn them into [restful_syndication_iframe] shortcodes 549 $iframes = $dom->getElementsByTagName('iframe'); 550 551 foreach($iframes as $iframeKey => $iframe) { 552 553 // Skip iframes without src field 554 if(!$iframe->hasAttribute('src')) { 555 continue; 556 } 557 558 // Get the original iFrame src URL 559 $url = $iframe->getAttribute('src'); 560 561 // Skip empty URLs 562 if(empty($url)) { 563 continue; 564 } 565 566 // Get width and height 567 $width = '100%'; 568 $height = '300'; 569 if($iframe->hasAttribute('height')) { 570 $height = $iframe->getAttribute('height'); 571 } 572 573 // Create a new paragraph, and insert the iframe shortcode 574 $embed_shortcode = $dom->createElement('p'); 575 $embed_shortcode->nodeValue = '[restful_syndication_iframe src="'.esc_url($url).'" width="'.esc_attr($width).'" height="'.esc_attr($height).'"]'; 576 577 // Replace the original <iframe> tag with this new <p>[restful_syndication_iframe src="url"]</p> arrangement 578 $iframe->parentNode->replaceChild($embed_shortcode, $iframe); 417 579 } 418 580 … … 609 771 610 772 if(empty($payload) || $payload == null) { 611 return array("error_msg" => 'Failed to fetch post data from API' );773 return array("error_msg" => 'Failed to fetch post data from API', "errors" => $errors_logged); 612 774 } 613 775 … … 623 785 624 786 return array("post_id" => $post_id); 787 } 788 789 private function clean_media($days, $post_type) { 790 // Deletes media after a certain age 791 792 if(!is_numeric($days)) { 793 return; 794 } 795 796 $days = absint($days); 797 798 if($days < 7) { 799 return; 800 } 801 802 // Find posts 803 $post_ids = $this->posts_older_than($days, $post_type); 804 805 $return = array(); 806 807 // Loop over posts and find featured images attached to these posts 808 foreach($post_ids as $post_id) { 809 $image_id = get_post_thumbnail_id($post_id); 810 811 if($image_id === false || $image_id === 0) { 812 continue; 813 } 814 815 // Send the attachment to the trash 816 $delete_image = wp_delete_attachment($image_id, false); 817 818 if($delete_image !== false && $delete_image !== null) { 819 $return[] = $image_id; 820 } 821 } 822 823 return $return; 824 } 825 826 private function clean_posts($days, $post_type) { 827 // Deletes posts after a certain age 828 829 if(!is_numeric($days)) { 830 return; 831 } 832 833 $days = absint($days); 834 835 if($days < 7) { 836 return; 837 } 838 839 // Find posts 840 $post_ids = $this->posts_older_than($days, $post_type); 841 842 $return = array(); 843 844 // Loop over posts 845 foreach($post_ids as $post_id) { 846 // Delete featured image first 847 $image_id = get_post_thumbnail_id($post_id); 848 849 if($image_id !== false) { 850 // Send the attachment to the trash 851 $delete_image = wp_delete_attachment($image_id, false); 852 853 if($delete_image !== false) { 854 $return[] = $image_id; 855 } 856 } 857 858 // Now delete the post itself 859 $delete_post = wp_trash_post($post_id); 860 if($delete_post !== false) { 861 $return[] = $post_id; 862 } 863 } 864 865 return $return; 866 } 867 868 private function posts_older_than($days, $post_type) { 869 // Returns a list of Posts older than a certain age 870 871 if(!is_numeric($days)) { 872 return array(); 873 } 874 875 $days = absint($days); 876 877 $options = get_option($this->settings_prefix . 'settings'); 878 $domain = parse_url($options['site_url'], PHP_URL_HOST); 879 $domain_1 = 'https://' . $domain; 880 $domain_2 = 'http://' . $domain; 881 882 if($domain_1 == 'https://' || $domain_2 == 'http://') { 883 return array(); 884 } 885 886 $posts = new WP_Query(array( 887 'post_type' => $post_type, 888 'orderby' => 'post_date_gmt', 889 'order' => 'ASC', 890 'meta_query' => array( 891 'relation' => 'OR', 892 'meta_value_1' => array( 893 'key' => '_'.$this->settings_prefix.'source_guid', 894 'value' => $domain_1, 895 'compare' => 'LIKE', 896 ), 897 'meta_value_2' => array( 898 'key' => '_'.$this->settings_prefix.'source_guid', 899 'value' => $domain_2, 900 'compare' => 'LIKE', 901 ) 902 ), 903 'date_query' => array( 904 array( 905 'column' => 'post_date_gmt', 906 'before' => $days . ' days ago', 907 ), 908 ), 909 'fields' => 'ids', 910 'posts_per_page' => -1, 911 )); 912 913 return $posts->posts; 625 914 } 626 915 … … 728 1017 } 729 1018 1019 public function sc_iframe($atts) { 1020 // This iFrame can only be used on posts imported by this plugin 1021 // It is a security mechanism instead of just allowing iFrame's for all users accross the site 1022 // We assume the source site is at least semi-trusted (trusted enough to embed an iframe at least) 1023 1024 $a = shortcode_atts(array( 1025 "src" => "", 1026 "width" => "100%", 1027 "height" => "200", 1028 ), $atts); 1029 1030 global $post; 1031 if(!isset($post)) { 1032 return ''; 1033 } 1034 1035 $source_guid = get_post_meta($post->ID, '_'.$this->settings_prefix.'source_guid', true); 1036 1037 if(empty($source_guid)) { 1038 return ''; 1039 } 1040 1041 return '<iframe src="'.esc_url($a['src']).'" width="'.esc_attr($a['width']).'" height="'.esc_attr($a['height']).'" border="0"></iframe>'; 1042 } 1043 730 1044 private function random_str($length, $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-=+`~') { 731 1045 // From https://stackoverflow.com/a/31284266/2888733 -
restful-syndication/trunk/readme.txt
r2845739 r2871614 20 20 == Installation == 21 21 22 1. Upload the plugin files to the `/wp-content/plugins/restful-syndication` directory, or install the plugin through the WordPress plugins screen directly.22 1. Install this plugin through the WordPress plugins screen. 23 23 2. Activate the plugin through the 'Plugins' screen in WordPress 24 3. Use the Settings ->RESTful Syndication screen to configure the plugin24 3. Use the Settings -> RESTful Syndication screen to configure the plugin 25 25 a. Set the 'Master Site URL' to the base URL of the site to pull the content from (e.g. https://example.com/) 26 b. Username and Password are not required by default (but may be required by the person running the Master Site) 26 b. Username and Password are not required by default (but may be required by the person running the Master Site). You can use the WordPress Applicaton Password feature for authentication. 27 27 c. Set the other options as desired 28 28 d. Save your settings … … 47 47 = Something isn't working. What do I do? = 48 48 49 Find the PHP Error log for your website/web-server. Any errors from this plugin should be prefixed with 'RESTful Syndication ERROR'.49 Find the PHP Error Log for your website/web-server. Any errors from this plugin should be prefixed with 'RESTful Syndication ERROR'. 50 50 51 51 = Do you provide support? = … … 58 58 59 59 == Changelog == 60 61 = 1.3.0 = 62 63 * Add new options to purge media & posts older than a certain number of days 64 * Additional compatibility for YouTube and Audio embeds 65 * Allow iFrames to be syndicated 66 * Translate Instagram embeds into iFrames 67 * Bugfix for Audio embeds 68 * Catch errors causing empty posts to be syndicated 69 * Security hardening on the admin screen 70 * Additional logging details 60 71 61 72 = 1.2.1 =
Note: See TracChangeset
for help on using the changeset viewer.