Author URI: http://petermolnar.eu/ License: GPLv3 */ if ( ! class_exists( 'WP_Webmention_Again' ) ): // something else might have loaded this already if ( ! class_exists( 'Mf2\Parser' ) ) { require ( __DIR__ . '/vendor/autoload.php' ); } // if something has loaded Mf2 already, the autoload won't kick in, // and we need this if ( ! class_exists( 'EmojiRecognizer' ) ) { require ( __DIR__ . '/vendor/dissolve/single-emoji-recognizer/src/emoji.php' ); } class WP_Webmention_Again { // WP cache expiration seconds const expire = 10; // queue & history table name const tablename = 'webmentions'; const logfile = __DIR__ . '/webmentions.log'; /** * regular cron interval for processing incoming * * use 'wp-webmention-again_interval_received' to filter this integer * * @return int cron interval in seconds * */ protected static function remote_timeout () { return apply_filters( 'wp-webmention-again_remote_timeout', 100 ); } /** * mapping for comment types -> mf2 names * * use 'wp-webmention-again_mftypes' to filter this array * * @return array array of comment_type => mf2_name entries * */ protected static function mftypes () { $map = array ( // http://indiewebcamp.com/reply 'reply' => 'in-reply-to', // http://indiewebcamp.com/repost 'repost' => 'repost-of', // http://indiewebcamp.com/like 'like' => 'like-of', // http://indiewebcamp.com/favorite 'favorite' => 'favorite-of', // http://indiewebcamp.com/bookmark 'bookmark' => 'bookmark-of', // http://indiewebcamp.com/rsvp 'rsvp' => 'rsvp', // http://indiewebcamp.com/tag 'tag' => 'tag-of', ); return apply_filters( 'wp-webmention-again_mftypes', $map ); } /** * runs on plugin load */ public function __construct() { add_action( 'init', array( &$this, 'init' ) ); // this is mostly for debugging reasons register_activation_hook( __FILE__ , array( __CLASS__ , 'plugin_activate' ) ); // clear schedules if there's any on deactivation register_deactivation_hook( __FILE__ , array( __CLASS__ , 'plugin_deactivate' ) ); // extend current cron schedules with our entry add_filter( 'cron_schedules', array(&$this, 'add_cron_schedule' ) ); add_action( 'trigger_archive_org', array( &$this, 'archive' ) ); static::lookfordeleted(); } public static function lookfordeleted() { $url = substr( trim( $_SERVER['REQUEST_URI'], '/' ), 0, 191 ); if ( empty($url) ) return false; $query = new WP_Query( array ( 'name' => $url . '__trashed', 'post_status' => 'trash', 'post_type' => 'any' )); if ( ! empty( $query->posts ) && $query->is_singular ) { static::debug("Found deleted post for {$url}"); status_header(410); nocache_headers(); die('This post was removed.'); } } /** * plugin activation hook * * dies if PHP version is too low * */ public static function plugin_activate() { if ( version_compare( phpversion(), 5.3, '<' ) ) { die( 'The minimum PHP version required for this plugin is 5.3' ); } global $wpdb; $dbname = $wpdb->prefix . static::tablename; //Use the character set and collation that's configured for WP tables $charset_collate = ''; if ( !empty($wpdb->charset) ){ $charset = str_replace('-', '', $wpdb->charset); $charset_collate = "DEFAULT CHARACTER SET {$charset}"; } if ( !empty($wpdb->collate) ){ $charset_collate .= " COLLATE {$wpdb->collate}"; } $db_command = "CREATE TABLE IF NOT EXISTS `{$dbname}` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `date` datetime NOT NULL, `direction` varchar(12) NOT NULL DEFAULT 'in', `tries` int(4) NOT NULL DEFAULT '0', `source` text NOT NULL, `target` text NOT NULL, `object_type` varchar(255) NOT NULL DEFAULT 'post', `object_id` bigint(20) NOT NULL, `status` tinyint(4) NOT NULL DEFAULT '0', `note` text NOT NULL, PRIMARY KEY (`id`), KEY `time` (`date`), KEY `key` (`direction`) ) {$charset_collate};"; static::debug("Initiating DB {$dbname}", 4); try { $wpdb->query( $db_command ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } } /** * plugin deactivation hook * * makes sure there are no scheduled cron hooks left * */ public static function plugin_deactivate () { global $wpdb; $dbname = $wpdb->prefix . static::tablename; $db_command = "DROP TABLE IF EXISTS `{$dbname}`;"; static::debug("Deleting DB {$dbname}", 4); try { $wpdb->query( $db_command ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } } /** * add our own schedule * * @param array $schedules - current schedules list * * @return array $schedules - extended schedules list */ public function add_cron_schedule ( $schedules ) { $schedules[ static::cron ] = array ( 'interval' => static::interval(), 'display' => sprintf(__( 'every %d seconds' ), static::interval() ) ); return $schedules; } /** * get plugin options * * use 'wp-webmention-again_comment_types' to filter registered comment types * before being applied * * @return array plugin options * */ public static function get_options () { $options = get_option( __CLASS__ ); if ( isset( $options['comment_types'] ) && is_array( $options['comment_types'] ) ) $options['comment_types'] = array_merge($options['comment_types'], static::mftypes()); else $options['comment_types'] = static::mftypes(); $options['comment_types'] = apply_filters( 'wp-webmention-again_comment_types', $options['comment_types'] ); return $options; } /** * insert a webmention to the queue * * @param string $direction - 'in' or 'out' * @param string $source - source URL * @param string $target - target URL * @param string $object - object type: post, comment, etc. * @param int $object_id - ID of object * * @return false|string - false on failure, inserted ID on success * */ public static function queue_add ( $direction, $source, $target, $object = '', $object_id = 0, $epoch = false ) { global $wpdb; $dbname = $wpdb->prefix . static::tablename; $direction = strtolower($direction); $valid_directions = array ( 'in', 'out' ); if ( ! in_array ( $direction, $valid_directions ) ) return false; //$id = sha1($source . $target . ); //$id = uniqid(); if ( static::queue_exists ( $direction, $source, $target, $epoch ) ) return true; $q = $wpdb->prepare( "INSERT INTO `{$dbname}` (`date`,`direction`, `tries`,`source`, `target`, `object_type`, `object_id`, `status`, `note` ) VALUES ( NOW(), '%s', 0, '%s', '%s', '%s', %d, 0, '' );", $direction, $source, $target, $object, $object_id ); try { $req = $wpdb->query( $q ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } static::log ( $direction, $source, $target ); return $wpdb->insert_id; } /** * increment tries counter for a queue element * * @param string $id - ID of queue element * * @return bool - query success/failure * */ public static function queue_inc ( $id ) { if ( empty( $id ) ) return false; global $wpdb; $dbname = $wpdb->prefix . static::tablename; $q = $wpdb->prepare( "UPDATE `{$dbname}` SET `tries` = `tries` + 1 WHERE `id` = '%s'; ", $id ); try { $req = $wpdb->query( $q ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } return $req; } /** * delete an entry from the webmentions queue * * @param string $id - ID of webmention queue element * * @return bool - query success/failure * */ public static function queue_del ( $id ) { if ( empty( $id ) ) return false; global $wpdb; $dbname = $wpdb->prefix . static::tablename; $q = $wpdb->prepare( "DELETE FROM `{$dbname}` WHERE `id` = '%s' LIMIT 1;", $id ); try { $req = $wpdb->query( $q ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } return $req; } /** * delete an entry from the webmentions queue * * @param string $id - ID of webmention queue element * * @return bool - query success/failure * */ public static function queue_done ( $id, $note = '' ) { if ( empty( $id ) ) return false; if ( ! empty ( $note ) && ! is_string ( $note ) ) $note = json_encode ( $note ); global $wpdb; $dbname = $wpdb->prefix . static::tablename; if ( empty( $note ) ) $q = $wpdb->prepare( "UPDATE `{$dbname}` SET `status` = 1 WHERE `id` = '%s'; ", $id ); else $q = $wpdb->prepare( "UPDATE `{$dbname}` SET `status` = 1, `note`='%s' WHERE `id` = '%s'; ", $note, $id ); try { $req = $wpdb->query( $q ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } //do_action ( 'wp_webmention_again_done', $id ); return $req; } /** * get a batch of elements according to direction * * @param string $direction - 'in' or 'out' * @param int $limit - max number of items to get * * @return array of queue objects * */ public static function queue_get ( $direction, $limit = 1 ) { $direction = strtolower($direction); $valid_directions = array ( 'in', 'out' ); if ( ! in_array ( $direction, $valid_directions ) ) return false; global $wpdb; $dbname = $wpdb->prefix . static::tablename; $q = $wpdb->prepare( "SELECT * FROM `{$dbname}` WHERE `direction` = '%s' and `status` = 0 and `tries` < ". static::retry() ." LIMIT %d;", $direction, $limit ); try { $req = $wpdb->get_results( $q ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } if ( ! empty ( $req ) ) return $req; return false; } /** * checks existence of a queue element * * @param string $direction - 'in' or 'out' * @param string $source - source URL * @param string $target - target URL * @param int $epoch - epoch to compare date with; used for updates * * @return bool true on existing element, false on not found */ public static function queue_exists ( $direction, $source, $target, $epoch = false ) { global $wpdb; $dbname = $wpdb->prefix . static::tablename; $direction = strtolower($direction); $valid_directions = array ( 'in', 'out' ); if ( ! in_array ( $direction, $valid_directions ) ) return false; $q = $wpdb->prepare( "SELECT date, status FROM `{$dbname}` WHERE `direction` = '%s' and `source` = '%s' and `target`='%s' ORDER BY date DESC LIMIT 1;", $direction, $source, $target ); try { $req = $wpdb->get_row( $q ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } if ( ! empty ( $req ) ) { if ( false != $epoch && 0 != $req->status ) { $dbtime = strtotime( $req->date ); if ( $epoch > $dbtime ) { // the post was recently updated return false; } } return true; } return false; } /** * get a single webmention based on id * * @param int $id webmention id * * @return array of webmention * * public static function webmention_get ( $id ) { global $wpdb; $dbname = $wpdb->prefix . static::tablename; $q = $wpdb->prepare( "SELECT * FROM `{$dbname}` WHERE `id` = '%d'", $id ); try { $req = $wpdb->get_results( $q ); } catch (Exception $e) { static::debug('Something went wrong: ' . $e->getMessage(), 4); } if ( ! empty ( $req ) ) return $req; return false; } */ /** * extended wp_remote_get with debugging * * @param string $source URL to pull * * @return array wp_remote_get array */ protected static function _wp_remote_get ( &$source ) { $content = false; static::debug( " fetching {$source}", 6); $url = htmlspecialchars_decode( $source ); $q = wp_remote_get( $source ); if ( is_wp_error( $q ) ) { static::debug( " something went wrong: " . $q->get_error_message() ); return false; } if ( !is_array( $q ) ) { static::debug( " $q is not an array. It should be one.", 6); return false; } if ( ! isset( $q['headers'] ) || ! is_array( $q['headers'] ) ) { static::debug( " missing response headers.", 6); return false; } if ( ! isset( $q['body'] ) || empty( $q['body'] ) ) { static::debug( " missing body", 6); return false; } static::debug('Headers: ' . json_encode($q['headers']), 7); return $q; } /** * validated a URL that is supposed to be on our site * * @param string $url URL to check against * * @return bool|int false on not found, int post_id on found */ protected static function validate_local ( $url ) { // normalize url scheme, url_to_postid will take care of it anyway $url = preg_replace( '/^https?:\/\//i', 'http://', strtolower($url) ); $postid = url_to_postid( $url ); static::debug( "Found postid for {$url}: {$postid}", 6); return apply_filters ('wp_webmention_again_validate_local', $postid , $url ); } /** * recursive walker for an mf2 array: if it find an element which has a value * of a single element array, flatten the array to the element * * @param array $mf2 Mf2 array to flatten * * @return array flattened Mf2 array * */ protected static function flatten_mf2_array ( $mf2 ) { if (is_array($mf2) && count($mf2) == 1) { $mf2 = array_pop($mf2); } if (is_array($mf2)) { foreach($mf2 as $key => $value) { $mf2[$key] = static::flatten_mf2_array($value); } } return $mf2; } /** * find all urls in a text * * @param string $text - text to parse * * @return array array of URLS found in the test */ protected static function extract_urls( &$text ) { $matches = array(); preg_match_all("/\b(?:http|https)\:\/\/?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.[a-zA-Z0-9\.\/\?\:@\-_=#]*/i", $text, $matches); $matches = $matches[0]; $matches = array_unique($matches); return $matches; } /** * validates a post object if it really is a post * * @param $post object Wordpress Post Object to check * * @return bool true if it's a post, false if not */ protected static function is_post ( &$post ) { if ( !empty($post) && is_object($post) && isset($post->ID) && !empty($post->ID) ) return true; return false; } /** * Converts relative to absolute urls * * code from [webmention](https://github.com/pfefferle/wordpress-webmention) * which is based on the code of 99webtools.com * * @link http://99webtools.com/relative-path-into-absolute-url.php * * @param string $base the base url * @param string $rel the relative url * * @return string the absolute url */ protected static function make_url_absolute( $base, $rel ) { if ( 0 === strpos( $rel, '//' ) ) { return parse_url( $base, PHP_URL_SCHEME ) . ':' . $rel; } // return if already absolute URL if ( parse_url( $rel, PHP_URL_SCHEME ) != '' ) { return $rel; } // queries and anchors if ( '#' == $rel[0] || '?' == $rel[0] ) { return $base . $rel; } // parse base URL and convert to local variables: // $scheme, $host, $path extract( parse_url( $base ) ); // remove non-directory element from path $path = preg_replace( '#/[^/]*$#', '', $path ); // destroy path if relative url points to root if ( '/' == $rel[0] ) { $path = ''; } // dirty absolute URL $abs = "$host"; // check port if ( isset( $port ) && ! empty( $port ) ) { $abs .= ":$port"; } // add path + rel $abs .= "$path/$rel"; // replace '//' or '/./' or '/foo/../' with '/' $re = array( '#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#' ); for ( $n = 1; $n > 0; $abs = preg_replace( $re, '/', $abs, -1, $n ) ) { } // absolute URL is ready! return $scheme . '://' . $abs; } /** * get content like the_content * * @param object $post - WP Post object * * @return string the_content */ protected static function get_the_content( &$post ){ if ( ! static::is_post( $post ) ) return false; if ( $cached = wp_cache_get ( $post->ID, __CLASS__ . __FUNCTION__ ) ) return $cached; $r = apply_filters( 'the_content', $post->post_content ); wp_cache_set ( $post->ID, $r, __CLASS__ . __FUNCTION__, static::expire ); return $r; } /** * * debug messages; will only work if WP_DEBUG is on * or if the level is LOG_ERR, but that will kill the process * * @param string $message * @param int $level * * @output log to syslog | wp_die on high level * @return false on not taking action, true on log sent */ public static function debug( $message, $level = LOG_NOTICE ) { if ( empty( $message ) ) return false; if ( @is_array( $message ) || @is_object ( $message ) ) $message = json_encode($message); $levels = array ( LOG_EMERG => 0, // system is unusable LOG_ALERT => 1, // Alert action must be taken immediately LOG_CRIT => 2, // Critical critical conditions LOG_ERR => 3, // Error error conditions LOG_WARNING => 4, // Warning warning conditions LOG_NOTICE => 5, // Notice normal but significant condition LOG_INFO => 6, // Informational informational messages LOG_DEBUG => 7, // Debug debug-level messages ); // number for number based comparison // should work with the defines only, this is just a make-it-sure step $level_ = $levels [ $level ]; // in case WordPress debug log has a minimum level if ( defined ( 'WP_DEBUG_LEVEL' ) ) { $wp_level = $levels [ WP_DEBUG_LEVEL ]; if ( $level_ > $wp_level ) { return false; } } // ERR, CRIT, ALERT and EMERG if ( 3 >= $level_ ) { wp_die( '
' . $message . '
' ); exit; } $trace = debug_backtrace(); $caller = $trace[1]; $parent = $caller['function']; if (isset($caller['class'])) $parent = $caller['class'] . '::' . $parent; return error_log( "{$parent}: {$message}" ); } /** */ public static function log( $direction, $source, $target, $object, $object_id, $epoch ) { $date = date("c"); $logmsg = "{$date} {$direction} {$source} {$target}\n"; file_put_contents ( static::logfile , $logmsg, FILE_APPEND|LOCK_EX ); } /** * */ public static function archive( $url ) { $args = array( 'timeout' => 60, 'redirection' => 5, 'httpversion' => '1.1', 'user-agent' => 'Lynx/2.8.9dev.1 libwww-FM/2.14 SSL-MM/1.4.1', ); return wp_remote_get( 'https://web.archive.org/save/' . $url ); } } require ( __DIR__ . '/sender.php' ); $WP_Webmention_Again_Sender = new WP_Webmention_Again_Sender(); require ( __DIR__ . '/receiver.php' ); $WP_Webmention_Again_Receiver = new WP_Webmention_Again_Receiver(); // global send_webmention function if ( ! function_exists( 'send_webmention' ) ) { function send_webmention( $source, $target, $object = '', $object_id = 0 ) { return WP_Webmention_Again_Sender::queue_add ( 'out', $source, $target, $object, $object_id ); } } endif;