Plugin Directory

Changeset 2871614


Ignore:
Timestamp:
02/27/2023 10:58:46 AM (3 years ago)
Author:
anthonyeden
Message:

Version 1.3.0

Location:
restful-syndication
Files:
2 edited
3 copied

Legend:

Unmodified
Added
Removed
  • restful-syndication/tags/1.3.0/index.php

    r2845739 r2871614  
    44Plugin URI: https://mediarealm.com.au/
    55Description: Import content from the Wordpress REST API on another Wordpress site
    6 Version: 1.2.1
     6Version: 1.3.0
    77Author: Media Realm
    88Author URI: https://www.mediarealm.com.au/
     
    7474            "options" => array()
    7575        ),
     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        ),
    7686        "remote_push_key" => array(
    7787            "title" => "Remote Content Push - Secure Key",
     
    8494       
    8595    );
     96
     97    public $errors_logged = array();
    8698
    8799    public function __construct() {
     
    90102        add_action('restful-syndication_cron', array($this, 'syndicate'));
    91103        add_filter('cron_schedules', array($this, 'cron_schedules'));
     104
     105        add_shortcode('restful_syndication_iframe', array($this, 'sc_iframe'));
    92106
    93107        add_action('rest_api_init', function () {
     
    106120        if(is_array($log) || is_object($log)) {
    107121           error_log($this->errors_prefix . print_r($log, true));
     122           $errors_logged[] = print_r($log, true);
    108123        } else {
    109124           error_log($this->errors_prefix . $log);
     125           $errors_logged[] = $log;
    110126        }
    111127     }
     
    177193        if($field['type'] == "text") {
    178194            // 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).'" />';
    180196        }  elseif($field['type'] == "textarea") {
    181197            // 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>';
    183199        } elseif($field['type'] == "password") {
    184200            // 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).'" />';
    186202        } elseif($field['type'] == "select") {
    187203            // Select / drop-down fields
    188204            echo '<select name="restful-syndication_settings['.$args['field_key'].']">';
    189205            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>';
    191207            }
    192208            echo '</select>';
    193209        } elseif($field['type'] == "checkbox") {
    194210            // 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" : "").' />';
    196212        } elseif($field['type'] == "readonly") {
    197213            // 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 />';
    199215        }
    200216    }
     
    212228        // Display a history of successful syndication runs
    213229        $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());
    214232        $last_attempt = get_option($this->settings_prefix . 'last_attempt', 0);
    215233        krsort($runs);
     234        krsort($runs_delete);
     235        krsort($runs_delete_media);
    216236
    217237        echo '<h2>Syndication History</h2>';
     
    230250            echo '<li>No posts have ever been ingested by this plugin</li>';
    231251        }
     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        }
    232268        echo '</ul>';
    233269
     
    236272        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>";
    237273        if(isset($_GET['ingestnow']) && $_GET['ingestnow'] == "true") {
     274
     275            // Verify nonce
     276            check_admin_referer('restful_syndication_ingest');
     277
    238278            echo "<p><strong>Attempting syndication now...</strong></p>";
    239279            $this->syndicate();
    240280            echo '<p><strong>Syndication complete!</strong></p>';
    241281        } 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>';
    243283        }
    244284       
     
    270310
    271311        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());
    273313            return;
    274314        }
     
    296336
    297337        $count = 0;
    298 
     338        $count_delete = 0;
     339        $count_delete_media = 0;
     340
     341        // Loop over every post and create a post entry
    299342        foreach($posts as $post) {
    300             // Loop over every post and create a post entry
    301343            $this->syndicate_one($post);
    302344            $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'));
    304355        }
    305356
     
    308359            $runs[time()] = $count;
    309360            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);
    310373        }
    311374
     
    332395        }
    333396
    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
    335421        $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
    338425        $images = $dom->getElementsByTagName('img');
    339426        $images_to_attach = array();
     
    375462            $url = $audio_source->item(0)->getAttribute('src');
    376463
     464            if(empty($url)) {
     465                continue;
     466            }
     467
    377468            // There is a bug in Wordpress causing audio URLs with URL Parameters to fail to load the player
    378469            // See https://core.trac.wordpress.org/ticket/30377
    379             // As a workaround, we strip URL parameters
     470            // As a partial workaround, we strip URL parameters
    380471            if(strpos($url, "?") !== false) {
    381472                $url = substr($url, 0, strpos($url, "?"));
     
    383474
    384475            // 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
    389481            $audio->parentNode->replaceChild($audio_shortcode, $audio);
    390482        }
     
    396488
    397489            // 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))
    399491                continue;
    400492
     
    409501
    410502                // 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
    415508                $youtube->parentNode->replaceChild($embed_shortcode, $youtube);
    416509            }
     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);
    417579        }
    418580
     
    609771
    610772        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);
    612774        }
    613775
     
    623785
    624786        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;
    625914    }
    626915
     
    7281017    }
    7291018
     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
    7301044    private function random_str($length, $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-=+`~') {
    7311045        // From https://stackoverflow.com/a/31284266/2888733
  • restful-syndication/tags/1.3.0/readme.txt

    r2845739 r2871614  
    2020== Installation ==
    2121
    22 1. Upload the plugin files to the `/wp-content/plugins/restful-syndication` directory, or install the plugin through the WordPress plugins screen directly.
     221. Install this plugin through the WordPress plugins screen.
    23232. Activate the plugin through the 'Plugins' screen in WordPress
    24 3. Use the Settings->RESTful Syndication screen to configure the plugin
     243. Use the Settings -> RESTful Syndication screen to configure the plugin
    2525 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.
    2727 c. Set the other options as desired
    2828 d. Save your settings
     
    4747= Something isn't working. What do I do? =
    4848
    49 Find the PHP Error log for your website/web-server. Any errors from this plugin should be prefixed with 'RESTful Syndication ERROR'.
     49Find the PHP Error Log for your website/web-server. Any errors from this plugin should be prefixed with 'RESTful Syndication ERROR'.
    5050
    5151= Do you provide support? =
     
    5858
    5959== 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
    6071
    6172= 1.2.1 =
  • restful-syndication/trunk/index.php

    r2845739 r2871614  
    44Plugin URI: https://mediarealm.com.au/
    55Description: Import content from the Wordpress REST API on another Wordpress site
    6 Version: 1.2.1
     6Version: 1.3.0
    77Author: Media Realm
    88Author URI: https://www.mediarealm.com.au/
     
    7474            "options" => array()
    7575        ),
     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        ),
    7686        "remote_push_key" => array(
    7787            "title" => "Remote Content Push - Secure Key",
     
    8494       
    8595    );
     96
     97    public $errors_logged = array();
    8698
    8799    public function __construct() {
     
    90102        add_action('restful-syndication_cron', array($this, 'syndicate'));
    91103        add_filter('cron_schedules', array($this, 'cron_schedules'));
     104
     105        add_shortcode('restful_syndication_iframe', array($this, 'sc_iframe'));
    92106
    93107        add_action('rest_api_init', function () {
     
    106120        if(is_array($log) || is_object($log)) {
    107121           error_log($this->errors_prefix . print_r($log, true));
     122           $errors_logged[] = print_r($log, true);
    108123        } else {
    109124           error_log($this->errors_prefix . $log);
     125           $errors_logged[] = $log;
    110126        }
    111127     }
     
    177193        if($field['type'] == "text") {
    178194            // 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).'" />';
    180196        }  elseif($field['type'] == "textarea") {
    181197            // 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>';
    183199        } elseif($field['type'] == "password") {
    184200            // 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).'" />';
    186202        } elseif($field['type'] == "select") {
    187203            // Select / drop-down fields
    188204            echo '<select name="restful-syndication_settings['.$args['field_key'].']">';
    189205            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>';
    191207            }
    192208            echo '</select>';
    193209        } elseif($field['type'] == "checkbox") {
    194210            // 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" : "").' />';
    196212        } elseif($field['type'] == "readonly") {
    197213            // 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 />';
    199215        }
    200216    }
     
    212228        // Display a history of successful syndication runs
    213229        $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());
    214232        $last_attempt = get_option($this->settings_prefix . 'last_attempt', 0);
    215233        krsort($runs);
     234        krsort($runs_delete);
     235        krsort($runs_delete_media);
    216236
    217237        echo '<h2>Syndication History</h2>';
     
    230250            echo '<li>No posts have ever been ingested by this plugin</li>';
    231251        }
     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        }
    232268        echo '</ul>';
    233269
     
    236272        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>";
    237273        if(isset($_GET['ingestnow']) && $_GET['ingestnow'] == "true") {
     274
     275            // Verify nonce
     276            check_admin_referer('restful_syndication_ingest');
     277
    238278            echo "<p><strong>Attempting syndication now...</strong></p>";
    239279            $this->syndicate();
    240280            echo '<p><strong>Syndication complete!</strong></p>';
    241281        } 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>';
    243283        }
    244284       
     
    270310
    271311        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());
    273313            return;
    274314        }
     
    296336
    297337        $count = 0;
    298 
     338        $count_delete = 0;
     339        $count_delete_media = 0;
     340
     341        // Loop over every post and create a post entry
    299342        foreach($posts as $post) {
    300             // Loop over every post and create a post entry
    301343            $this->syndicate_one($post);
    302344            $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'));
    304355        }
    305356
     
    308359            $runs[time()] = $count;
    309360            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);
    310373        }
    311374
     
    332395        }
    333396
    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
    335421        $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
    338425        $images = $dom->getElementsByTagName('img');
    339426        $images_to_attach = array();
     
    375462            $url = $audio_source->item(0)->getAttribute('src');
    376463
     464            if(empty($url)) {
     465                continue;
     466            }
     467
    377468            // There is a bug in Wordpress causing audio URLs with URL Parameters to fail to load the player
    378469            // See https://core.trac.wordpress.org/ticket/30377
    379             // As a workaround, we strip URL parameters
     470            // As a partial workaround, we strip URL parameters
    380471            if(strpos($url, "?") !== false) {
    381472                $url = substr($url, 0, strpos($url, "?"));
     
    383474
    384475            // 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
    389481            $audio->parentNode->replaceChild($audio_shortcode, $audio);
    390482        }
     
    396488
    397489            // 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))
    399491                continue;
    400492
     
    409501
    410502                // 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
    415508                $youtube->parentNode->replaceChild($embed_shortcode, $youtube);
    416509            }
     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);
    417579        }
    418580
     
    609771
    610772        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);
    612774        }
    613775
     
    623785
    624786        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;
    625914    }
    626915
     
    7281017    }
    7291018
     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
    7301044    private function random_str($length, $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-=+`~') {
    7311045        // From https://stackoverflow.com/a/31284266/2888733
  • restful-syndication/trunk/readme.txt

    r2845739 r2871614  
    2020== Installation ==
    2121
    22 1. Upload the plugin files to the `/wp-content/plugins/restful-syndication` directory, or install the plugin through the WordPress plugins screen directly.
     221. Install this plugin through the WordPress plugins screen.
    23232. Activate the plugin through the 'Plugins' screen in WordPress
    24 3. Use the Settings->RESTful Syndication screen to configure the plugin
     243. Use the Settings -> RESTful Syndication screen to configure the plugin
    2525 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.
    2727 c. Set the other options as desired
    2828 d. Save your settings
     
    4747= Something isn't working. What do I do? =
    4848
    49 Find the PHP Error log for your website/web-server. Any errors from this plugin should be prefixed with 'RESTful Syndication ERROR'.
     49Find the PHP Error Log for your website/web-server. Any errors from this plugin should be prefixed with 'RESTful Syndication ERROR'.
    5050
    5151= Do you provide support? =
     
    5858
    5959== 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
    6071
    6172= 1.2.1 =
Note: See TracChangeset for help on using the changeset viewer.