diff --git a/readme.txt b/readme.txt index 678f942..2661683 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: https://paypal.me/petermolnar/3 Tags: webmention, pingback, indieweb Requires at least: 4.3 Tested up to: 4.4.1 -Stable tag: 0.2 +Stable tag: 0.3 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html Required minimum PHP version: 5.3 @@ -33,6 +33,12 @@ Version numbering logic: * every .B version indicates new features. * every ..C indicates bugfixes for A.B version. += 0.3 = +*2016-01-14* + +* split into 3 files: base, sender & receiver for readability + + = 0.2 = *2016-01-13* diff --git a/receiver.php b/receiver.php new file mode 100644 index 0000000..fab334e --- /dev/null +++ b/receiver.php @@ -0,0 +1,718 @@ + 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 ); + } + + public function __construct() { + parent::__construct(); + + // 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' ) ); + + add_action( 'parse_query', array( &$this, 'receive' ) ); + + add_action( 'wp_head', array( &$this, 'html_header' ), 99 ); + add_action( 'send_headers', array( &$this, 'http_header' ) ); + + // register the action for processing received + add_action( static::cron, array( &$this, 'process' ) ); + + // additional comment types + add_action( 'admin_comment_types_dropdown', array( &$this, 'comment_types_dropdown' ) ); + + } + + public function init () { + + // add webmention endpoint to query vars + add_filter( 'query_vars', array( &$this, 'add_query_var' ) ); + + // because this needs one more filter + add_filter( 'get_avatar_comment_types', array( &$this, 'add_comment_types' ) ); + + // additional avatar filter + add_filter( 'get_avatar' , array( &$this, 'get_avatar' ), 1, 5 ); + + if ( ! wp_get_schedule( static::cron ) ) + wp_schedule_event( time(), static::cron, static::cron ); + + } + + + /** + * plugin deactivation hook + * + * makes sure there are no scheduled cron hooks left + * + */ + public static function plugin_deactivate () { + + wp_unschedule_event( time(), static::cron ); + wp_clear_scheduled_hook( static::cron ); + + } + + /** + * Extend the "filter by comment type" of in the comments section + * of the admin interface with all of our methods + * + * @param array $types the different comment types + * + * @return array the filtered comment types + */ + public function comment_types_dropdown( $types ) { + $options = static::get_options(); + + foreach ( $options['comment_types'] as $type => $fancy ) + if ( ! isset( $types[ $type ] ) ) + $types[ $type ] = ucfirst( $type ); + + return $types; + } + + /** + * extend the abailable comment types in WordPress with the ones recognized + * by the plugin, including reacji + * + * @param array $types current types + * + * @return array extended types + */ + public function add_comment_types ( $types ) { + $options = static::get_options(); + + foreach ( $options['comment_types'] as $type => $fancy ) + if ( ! in_array( $type, $types ) ) + array_push( $types, $type ); + + return $types; + } + + /** + * add webmention to accepted query vars + * + * @param array $vars current query vars + * + * @return array extended vars + */ + public function add_query_var( $vars ) { + array_push( $vars, static::endpoint() ); + return $vars; + } + + + /** + * extends HTML header with webmention endpoint + * + * @output string two lines of HTML + * + */ + public function html_header () { + $endpoint = site_url( '?'. static::endpoint() .'=endpoint' ); + + // backwards compatibility with v0.1 + echo '' . "\n"; + echo '' . "\n"; + } + + /** + * extends HTTP response header with webmention endpoints + * + * @output two lines of Link: in HTTP header + * + */ + public function http_header() { + $endpoint = site_url( '?'. static::endpoint() .'=endpoint' ); + + // backwards compatibility with v0.1 + header( 'Link: <' . $endpoint . '>; rel="http://webmention.org/"', false ); + header( 'Link: <' . $endpoint . '>; rel="webmention"', false ); + } + + /** + * parse & queue incoming webmention endpoint requests + * + * @param mixed $wp WordPress Query + * + */ + public function receive ( $wp ) { + + // check if it is a webmention request or not + if ( ! array_key_exists( static::endpoint(), $wp->query_vars ) ) + return false; + + // plain text header + header( 'Content-Type: text/plain; charset=' . get_option( 'blog_charset' ) ); + + // check if source url is transmitted + if ( ! isset( $_POST['source'] ) ) { + status_header( 400 ); + echo '"source" is missing'; + exit; + } + + // check if target url is transmitted + if ( ! isset( $_POST['target'] ) ) { + status_header( 400 ); + echo '"target" is missing'; + exit; + } + + $target = filter_var( $_POST['target'], FILTER_SANITIZE_URL ); + $source = filter_var( $_POST['source'], FILTER_SANITIZE_URL ); + + if ( false === filter_var( $target, FILTER_VALIDATE_URL ) ) { + status_header( 400 ); + echo '"target" is an invalid URL'; + exit; + } + + if ( false === filter_var( $source, FILTER_VALIDATE_URL ) ) { + status_header( 400 ); + echo '"source" is an invalid URL'; + exit; + } + + $post_id = static::validate_local( $target ); + + if (! $post_id || 0 == $post_id ) { + status_header( 404 ); + echo '"target" not found.'; + exit; + } + + // check if pings are allowed + if ( ! pings_open( $post_id ) ) { + status_header( 403 ); + echo 'Pings are disabled for this post'; + exit; + } + + // queue here, the remote check will be async + //$r = static::queue_receive( $source, $target, $post_id ); + $r = static::queue_add( 'in', $source, $target, 'post', $post_id ); + + if ( true == $r ) { + status_header( 202 ); + echo 'Webmention accepted in the queue.'; + } + else { + status_header( 500 ); + echo 'Something went wrong; please try again later!'; + } + + exit; + } + + /** + * worker method for doing received webmentions + * triggered by cron + * + */ + public function process () { + + $incoming = static::queue_get ( 'in', static::per_batch() ); + + if ( empty( $incoming ) ) + return true; + + foreach ( (array)$incoming as $received ) { + + // this really should not happen, but if it does, get rid of this entry immediately + if (! isset( $received->target ) || + empty( $received->target ) || + ! isset( $received->source ) || + empty( $received->source ) + ) { + static::debug( " target or souce empty, aborting" ); + static::queue_del ( $received->id ); + continue; + } + + static::debug( "processing webmention: target -> {$received->target}, source -> {$received->source}" ); + + if ( empty( $received->object_id ) || 0 == $received->object_id ) + $post_id = url_to_postid ( $received->target ); + else + $post_id = $received->object_id; + + $post = get_post ( $post_id ); + + if ( ! static::is_post( $post ) ) { + static::debug( " no post found for this mention, try again later, who knows?" ); + //static::queue_del ( $received->id ); + continue; + } + + // too many retries, drop this mention and walk away + if ( $received->tries >= static::retry() ) { + static::debug( " this mention was tried earlier and failed too many times, drop it" ); + static::queue_del ( $received->id ); + continue; + } + + // increment retries + static::queue_inc ( $received->id ); + + // validate target + $remote = static::try_receive_remote( $post_id, $received->source, $received->target ); + + if ( false === $remote || empty( $remote ) ) { + static::debug( " parsing this mention failed, retrying next time" ); + continue; + } + + // we have remote data ! + $c = static::try_parse_remote ( $post_id, $received->source, $received->target, $remote ); + $ins = static::insert_comment ( $post_id, $received->source, $received->target, $remote, $c ); + + if ( true === $ins ) { + static::debug( " duplicate (or something similar): this queue element has to be ignored; deleting queue entry" ); + static::queue_del ( $received->id ); + } + elseif ( is_numeric( $ins ) ) { + static::debug( " all went well, we have a comment id: {$ins}, deleting queue entry" ); + static::queue_done ( $received->id ); + } + else { + static::debug( "This is unexpected. Try again." ); + } + } + } + + /** + * try to get contents of webmention originator + * + * @param int $post_id ID of post + * @param string $source Originator URL + * @param string $target Target URL + * + * @return bool|array false on error; plain array or Mf2 parsed (and + * flattened ) array of remote content on success + */ + protected static function try_receive_remote ( &$post_id, &$source, &$target ) { + + $content = false; + $q = static::_wp_remote_get( $source ); + + if ( false === $q ) + return false; + + $targets = array ( + $target, + wp_get_shortlink( $post_id ), + get_permalink( $post_id ) + ); + + $found = false; + + foreach ( $targets as $k => $t ) { + $t = preg_replace( '/https?:\/\/(?:www.)?/', '', $t ); + $t = preg_replace( '/#.*/', '', $t ); + $t = untrailingslashit( $t ); + //$targets[ $k ] = $t; + + if ( ! stristr( $q['body'], $t ) ) + $found = true; + + } + + // check if source really links to target + // this could be a temporary error, so we'll retry later this one as well + if ( false == $found ) { + static::debug( " missing link to {$t} in remote body" ); + return false; + } + + $ctype = isset( $q['headers']['content-type'] ) ? $q['headers']['content-type'] : 'text/html'; + + if ( "text/plain" == $ctype ) { + static::debug( " interesting, plain text webmention. I'm not prepared for this yet" ); + return false; + } + elseif ( $ctype == "application/json" ) { + static::debug( " content is JSON" ); + $content = json_decode( $q['body'], true ); + } + else { + static::debug( " content is (probably) html, trying to parse it with MF2" ); + try { + $content = Mf2\parse( $q['body'], $source ); + } + catch ( Exception $e ) { + static::debug( " parsing MF2 failed: " . $e->getMessage() ); + return false; + } + + $content = static::flatten_mf2_array( $content ); + } + + return $content; + } + + /** + * try to convert remote content to comment + * + * @param int $post_id ID of post + * @param string $source Originator URL + * @param string $target Target URL + * @param array $content (Mf2) array of remote content + * + * @return bool|int false on error; insterted comment ID on success + */ + protected static function try_parse_remote ( &$post_id, &$source, &$target, &$content ) { + + $item = false; + $p_authors = array(); + + if ( isset( $content['items']['properties'] ) && isset( $content['items']['type'] ) ) { + $item = $content['items']; + } + elseif ( is_array($content['items'] ) && ! empty( $content['items']['type'] ) ) { + foreach ( $content['items'] as $i ) { + if ( 'h-entry' == $i['type'] ) { + $items[] = $i; + } + elseif ( 'h-card' == $i['type'] ) { + $p_authors[] = $i; + } + elseif ( 'u-comment' == $i['type'] ) { + $comments[] = $i; + } + } + } + + if ( ! empty ( $items ) ) + $item = array_pop( $items ); + elseif ( empty( $items ) && ! empty( $comments ) ) + $item = array_pop( $comments ); + + if (! $item || empty( $item )) { + static::debug(' no parseable h-entry found, saving as standard mention comment'); + $c = array ( + 'comment_author' => $source, + 'comment_author_url' => $source, + 'comment_author_email' => '', + 'comment_post_ID' => $post_id, + 'comment_type' => 'webmention', + 'comment_date' => date("Y-m-d H:i:s"), + 'comment_date_gmt' => date("Y-m-d H:i:s"), + 'comment_agent' => __CLASS__, + 'comment_approved' => 0, + 'comment_content' => sprintf( __( 'This entry was webmentioned on %s.' ), $source, $source ), + ); + return $c; + } + + // process author + $author_name = $author_url = $avatar = $a = false; + + if ( isset( $item['properties']['author'] ) ) { + $a = $item['properties']['author']; + } + elseif ( ! empty( $p_authors ) ) { + $a = array_pop( $p_authors ); + } + + if ( $a && isset( $a['properties'] ) ) { + $a = $a['properties']; + + if ( isset($a['name']) && ! empty( $a['name'] ) ) + $author_name = $a['name']; + + $try_photos = array ('photo', 'avatar'); + $p = false; + foreach ( $try_photos as $photo ) { + if (isset( $a[ $photo ]) && ! empty( $a[ $photo ] ) ) { + $p = $a[ $photo ]; + if ( !empty( $p ) ) { + $avatar = $p; + break; + } + } + } + } + + // process type + $type = 'webmention'; + + foreach ( static::mftypes() as $k => $mapped ) { + if ( is_array( $item['properties'] ) && isset( $item['properties'][ $mapped ]) ) + $type = $k; + } + + //process content + $c = ''; + if ( isset( $item['properties']['content'] ) && isset( $item['properties']['content']['html'] ) ) + $c = $item['properties']['content']['html']; + if ( isset( $item['properties']['content'] ) && isset( $item['properties']['content']['value'] ) ) + $c = wp_filter_kses( $item['properties']['content']['value'] ); + + // REACJI + $emoji = EmojiRecognizer::isSingleEmoji( $c ); + + if ( $emoji ) { + static::debug( "wheeeee, reacji!" ); + $type = 'reacji'; + //static::register_reacji( $type ); + } + + // process date + if ( isset( $item['properties']['modified'] ) ) + $date = strtotime( $item['properties']['modified'] ); + elseif ( isset( $item['properties']['published'] ) ) + $date = strtotime( $item['properties']['published'] ); + else + $date = time(); + + $name = empty( $author_name ) ? $source : $author_name; + + $c = array ( + 'comment_author' => $name, + 'comment_author_url' => $source, + 'comment_author_email' => '', + 'comment_post_ID' => $post_id, + 'comment_type' => $type, + 'comment_date' => date( "Y-m-d H:i:s", $date ), + 'comment_date_gmt' => date( "Y-m-d H:i:s", $date ), + 'comment_agent' => __CLASS__, + 'comment_approved' => 0, + 'comment_content' => $c, + 'comment_avatar' => $avatar, + ); + + return $c; + } + + /** + * Comment inserter + * + * @param string &$post_id post ID + * @param string &$source Originator URL + * @param string &$target Target URL + * @param mixed &$raw Raw format of the comment, like JSON response from the provider + * @param array &$comment array formatted to match a WP Comment requirement + * + */ + protected static function insert_comment ( &$post_id, &$source, &$target, &$raw, &$comment ) { + + $comment_id = false; + + $avatar = false; + if( isset( $comment['comment_avatar'] ) ) { + $avatar = $comment['comment_avatar']; + unset( $comment['comment_avatar'] ); + } + + // safety first + $comment['comment_author_email'] = filter_var ( $comment['comment_author_email'], FILTER_SANITIZE_EMAIL ); + $comment['comment_author_url'] = filter_var ( $comment['comment_author_url'], FILTER_SANITIZE_URL ); + $comment['comment_author'] = filter_var ( $comment['comment_author'], FILTER_SANITIZE_STRING); + + //test if we already have this imported + $testargs = array( + 'author_url' => $comment['comment_author_url'], + 'post_id' => $post_id, + ); + + // so if the type is comment and you add type = 'comment', WP will not return the comments + // such logical! + if ( 'comment' != $comment['comment_type'] ) + $testargs['type'] = $comment['comment_type']; + + // in case it's a fav or a like, the date field is not always present + // but there should be only one of those, so the lack of a date field indicates + // that we should not look for a date when checking the existence of the + // comment + if ( isset( $comment['comment_date'] ) && ! empty( $comment['comment_date'] ) ) { + // in case you're aware of a nicer way of doing this, please tell me + // or commit a change... + + $tmp = explode ( " ", $comment['comment_date'] ); + $d = explode( "-", $tmp[0] ); + $t = explode ( ':', $tmp[1] ); + + $testargs['date_query'] = array( + 'year' => $d[0], + 'monthnum' => $d[1], + 'day' => $d[2], + 'hour' => $t[0], + 'minute' => $t[1], + 'second' => $t[2], + ); + + //test if we already have this imported + static::debug( "checking comment existence (with date) for post #{$post_id}" ); + } + else { + // we do need a date + $comment['comment_date'] = date( "Y-m-d H:i:s" ); + $comment['comment_date_gmt'] = date( "Y-m-d H:i:s" ); + + static::debug( "checking comment existence (no date) for post #{$post_id}" ); + } + + $existing = get_comments( $testargs ); + + // no matching comment yet, insert it + if ( ! empty( $existing ) ) { + static::debug ( "comment already exists" ); + return true; + } + + // disable flood control, just in case + remove_filter( 'check_comment_flood', 'check_comment_flood_db', 10, 3 ); + $comment = apply_filters( 'preprocess_comment', $comment ); + + if ( $comment_id = wp_new_comment( $comment ) ) { + + // add avatar for later use if present + if ( ! empty( $avatar ) ) + update_comment_meta( $comment_id, 'avatar', $avatar ); + + // full raw response for the vote, just in case + update_comment_meta( $comment_id, 'webmention_source_mf2', $raw ); + + // original request + update_comment_meta( $comment_id, 'webmention_original', array( 'target' => $target, 'source' => $source) ); + + // info + $r = "new comment inserted for {$post_id} as #{$comment_id}"; + + // notify author + // wp_notify_postauthor( $comment_id ); + } + else { + $r = "something went wrong when trying to insert comment for post #{$post_id}"; + } + + // re-add flood control + add_filter( 'check_comment_flood', 'check_comment_flood_db', 10, 3 ); + + static::debug( $r ); + + return $comment_id; + } + + /** + * of there is a comment meta 'avatar' field, use that as avatar for the commenter + * + * @param string $avatar the current avatar image string + * @param mixed $id_or_email this could be anything that triggered the avatar all + * @param string $size size for the image to display + * @param string $default optional fallback + * @param string $alt alt text for the avatar image + */ + public function get_avatar( $avatar, $id_or_email, $size, $default = '', $alt = '' ) { + if ( ! is_object( $id_or_email ) || ! isset( $id_or_email->comment_type ) ) + return $avatar; + + // check if comment has an avatar + $c_avatar = get_comment_meta( $id_or_email->comment_ID, 'avatar', true ); + + if ( ! $c_avatar ) + return $avatar; + + if ( false === $alt ) + $safe_alt = ''; + else + $safe_alt = esc_attr( $alt ); + + return sprintf( '%s', $safe_alt, $c_avatar, $size, $size ); + } + +} \ No newline at end of file diff --git a/sender.php b/sender.php new file mode 100644 index 0000000..9f5b55f --- /dev/null +++ b/sender.php @@ -0,0 +1,346 @@ + $e ) + $pung[ $k ] = strtolower( $e ); + + $pung = array_unique($pung); + + return $pung; + } + + /** + * triggered on post transition, applied when new status is publish, therefore + * applied on edit of published posts as well + * add a post meta to the post to be processed by the send processor + * + * @param string $new_status New post status + * @param string $old_status Previous post status + * @param object $post WP Post object + */ + public function queue( $new_status, $old_status, $post ) { + + if ( ! static::is_post( $post ) ) { + static::debug( "Whoops, this is not a post." ); + return false; + } + + if ( 'publish' != $new_status ) { + static::debug( "Not adding {$post->ID} to mention queue yet; not published" ); + return false; + } + + static::debug("Trying to get urls for #{$post->ID}"); + + // try to avoid redirects, so no shortlink is sent for now as source + $source = get_permalink( $post->ID ); + + // process the content as if it was the_content() + $content = static::get_the_content( $post ); + + // get all urls in content + $urls = static::extract_urls( $content ); + + // for special ocasions when someone wants to add to this list + $urls = apply_filters( 'webmention_links', $urls, $post->ID ); + + // lowercase url is good for your mental health + foreach ( $urls as $k => $url ) + $urls[ $k ] = strtolower( $url ); + + // remove all already pinged urls + $pung = get_pung( $post->ID ); + $urls = array_diff ( $urls, $pung ); + + foreach ( $urls as $target ) { + $r = static::queue_add ( 'out', $source, $target, $post->post_type, $post->ID ); + + if ( !$r ) + static::debug( " tried adding post #{$post->ID}, url: {$target} to mention queue, but it didn't go well" ); + } + + } + + /** + * worker method for doing received webmentions + * triggered by cron + * + */ + public function process () { + + $outgoing = static::queue_get ( 'out', static::per_batch() ); + + if ( empty( $outgoing ) ) + return true; + + foreach ( (array)$outgoing as $send ) { + + // this really should not happen, but if it does, get rid of this entry immediately + if (! isset( $send->target ) || + empty( $send->target ) || + ! isset( $send->source ) || + empty( $send->source ) + ) { + static::debug( " target or souce empty, aborting" ); + static::queue_del ( $send->id ); + continue; + } + + static::debug( "processing webmention: target -> {$send->target}, source -> {$send->source}" ); + + // too many retries, drop this mention and walk away + if ( $send->tries >= static::retry() ) { + static::debug( " this mention was tried earlier and failed too many times, drop it" ); + static::queue_del ( $send->id ); + continue; + } + + // increment retries + static::queue_inc ( $send->id ); + + // try sending + $s = static::send( $send->source, $send->target ); + + if ( is_wp_error ( $s ) ) { + static::debug( " sending failed: " . $s->get_error_message() ); + } + else { + static::debug( " sending succeeded!" ); + + $post_types = get_post_types( '', 'names' ); + if ( in_array( $send->object_type, $post_types ) && 0 != $send->object_id ) + add_ping( $send->object_id, $send->target ); + + static::queue_done ( $send->id, $s ); + } + } + } + + /** + * send a single webmention + * based on [webmention](https://github.com/pfefferle/wordpress-webmention) + * + */ + public static function send ( $source, $target, $post_id = false ) { + $options = static::get_options(); + + // stop selfpings on the same URL + if ( isset( $options['disable_selfpings_same_url'] ) && + $options['disable_selfpings_same_url'] == '1' && + $source === $target + ) + return false; + + // stop selfpings on the same domain + if ( isset( $options['disable_selfpings_same_domain'] ) && + $options['disable_selfpings_same_domain'] == '1' && + parse_url( $source, PHP_URL_HOST ) == parse_url( $target, PHP_URL_HOST ) + ) + return false; + + // discover the webmention endpoint + $webmention_server_url = static::discover_endpoint( $target ); + + // if I can't find an endpoint, perhaps you can! + $webmention_server_url = apply_filters( 'webmention_server_url', $webmention_server_url, $target ); + + if ( $webmention_server_url ) { + $args = array( + 'body' => 'source=' . urlencode( $source ) . '&target=' . urlencode( $target ), + 'timeout' => static::remote_timeout(), + ); + + static::debug( "Sending webmention to: " .$webmention_server_url . " as: " . $args['body'] ); + $response = wp_remote_post( $webmention_server_url, $args ); + + // use the response to do something usefull + // do_action( 'webmention_post_send', $response, $source, $target, $post_ID ); + + return $response; + } + + return false; + } + + /** + * Finds a WebMention server URI based on the given URL + * + * code from [webmention](https://github.com/pfefferle/wordpress-webmention) + * + * Checks the HTML for the rel="http://webmention.org/" link and http://webmention.org/ headers. It does + * a check for the http://webmention.org/ headers first and returns that, if available. The + * check for the rel="http://webmention.org/" has more overhead than just the header. + * + * @param string $url URL to ping + * + * @return bool|string False on failure, string containing URI on success + */ + protected static function discover_endpoint( $url ) { + + // Not an URL. This should never happen. + if ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) + return false; + + // do not search for a WebMention server on our own uploads + $uploads_dir = wp_upload_dir(); + if ( 0 === strpos( $url, $uploads_dir['baseurl'] ) ) + return false; + + $response = wp_remote_head( $url, array( 'timeout' => static::remote_timeout(), 'httpversion' => '1.0' ) ); + + if ( is_wp_error( $response ) ) { + static::debug( "Something went wrong: " . $response->get_error_message() ); + return false; + } + + // check link header + if ( $links = wp_remote_retrieve_header( $response, 'link' ) ) { + if ( is_array( $links ) ) { + foreach ( $links as $link ) { + if ( preg_match( '/<(.[^>]+)>;\s+rel\s?=\s?[\"\']?(http:\/\/)?webmention(.org)?\/?[\"\']?/i', $link, $result ) ) { + return self::make_url_absolute( $url, $result[1] ); + } + } + } else { + if ( preg_match( '/<(.[^>]+)>;\s+rel\s?=\s?[\"\']?(http:\/\/)?webmention(.org)?\/?[\"\']?/i', $links, $result ) ) { + return self::make_url_absolute( $url, $result[1] ); + } + } + } + + // not an (x)html, sgml, or xml page, no use going further + if ( preg_match( '#(image|audio|video|model)/#is', wp_remote_retrieve_header( $response, 'content-type' ) ) ) { + return false; + } + + // now do a GET since we're going to look in the html headers (and we're sure its not a binary file) + $response = wp_remote_get( $url, array( 'timeout' => static::remote_timeout(), 'httpversion' => '1.0' ) ); + + if ( is_wp_error( $response ) ) { + return false; + } + + $contents = wp_remote_retrieve_body( $response ); + + // boost performance and use alreade the header + $header = substr( $contents, 0, stripos( $contents, '' ) ); + + // unicode to HTML entities + $contents = mb_convert_encoding( $contents, 'HTML-ENTITIES', mb_detect_encoding( $contents ) ); + + libxml_use_internal_errors( true ); + + $doc = new DOMDocument(); + $doc->loadHTML( $contents ); + + $xpath = new DOMXPath( $doc ); + + // check elements + // checks only head-links + foreach ( $xpath->query( '//head/link[contains(concat(" ", @rel, " "), " webmention ") or contains(@rel, "webmention.org")]/@href' ) as $result ) { + return self::make_url_absolute( $url, $result->value ); + } + + // check elements + // checks only body>a-links + foreach ( $xpath->query( '//body//a[contains(concat(" ", @rel, " "), " webmention ") or contains(@rel, "webmention.org")]/@href' ) as $result ) { + return self::make_url_absolute( $url, $result->value ); + } + + return false; + } + +} \ No newline at end of file diff --git a/wp-webmention-again.php b/wp-webmention-again.php index 2155d6f..763a4c2 100644 --- a/wp-webmention-again.php +++ b/wp-webmention-again.php @@ -3,7 +3,7 @@ Plugin Name: wp-webmention-again Plugin URI: https://github.com/petermolnar/wp-webmention-again Description: -Version: 0.2 +Version: 0.3 Author: Peter Molnar Author URI: http://petermolnar.eu/ License: GPLv3 @@ -12,14 +12,6 @@ Required minimum PHP version: 5.3 if ( ! class_exists( 'WP_Webmention_Again' ) ): -// global send_webmention function -if ( ! function_exists( 'send_webmention' ) ) { - function send_webmention( $source, $target ) { - return WP_Webmention_Again::queue_add ( 'out', $source, $target ); - } -} - - // something else might have loaded this already if ( ! class_exists( 'Mf2\Parser' ) ) { require ( __DIR__ . '/vendor/autoload.php' ); @@ -33,31 +25,11 @@ if ( ! class_exists( 'EmojiRecognizer' ) ) { class WP_Webmention_Again { - // post meta key for queued incoming mentions - const meta_received = '_webmention_received'; - // cron handle for processing incoming - const cron_received = 'webmention_received'; - // post meta key for posts marked as outgoing and in need of processing - const meta_send = '_webmention_send'; - // cron handle for processing outgoing - const cron_send = 'webmention_send'; // WP cache expiration seconds const expire = 10; - // + // queue & history table name const tablename = 'webmentions'; - /** - * 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 known_reacji () { - return apply_filters( 'wp-webmention-again_known_reacji', 'reacji' ); - } - /** * regular cron interval for processing incoming * @@ -70,95 +42,6 @@ class WP_Webmention_Again { return apply_filters( 'wp-webmention-again_remote_timeout', 100 ); } - /** - * 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 interval_received () { - return apply_filters( 'wp-webmention-again_interval_received', 90 ); - } - - /** - * regular cron interval for processing outgoing - * - * use 'wp-webmention-again_interval_send' to filter this integer - * - * @return int cron interval in seconds - * - */ - protected static function interval_send () { - return apply_filters( 'wp-webmention-again_interval_send', 90 ); - } - - /** - * max number of retries ( both for outgoing and incoming ) - * - * use 'wp-webmention-again_retry' to filter this integer - * - * @return int cron interval in seconds - * - */ - protected static function retry () { - return apply_filters( 'wp-webmention-again_retry', 5 ); - } - - /** - * endpoint for receiving webmentions - * - * use 'wp-webmention-again_endpoint' to filter this string - * - * @return string name of endpoint - * - */ - protected static function endpoint() { - return apply_filters( 'wp-webmention-again_endpoint', 'webmention' ); - } - - /** - * maximum amount of posts per batch to be processed - * - * use 'wp-webmention-again_per_batch' to filter this int - * - * @return int posts per batch - * - */ - protected static function per_batch () { - return apply_filters( 'wp-webmention-again_per_batch', 10 ); - } - - /** - * 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 */ @@ -166,57 +49,15 @@ class WP_Webmention_Again { add_action( 'init', array( &$this, 'init' ) ); - add_action( 'parse_query', array( &$this, 'receive' ) ); - - add_action( 'wp_head', array( &$this, 'html_header' ), 99 ); - add_action( 'send_headers', array( &$this, 'http_header' ) ); - // this is mostly for debugging reasons - register_activation_hook( __FILE__ , array( 'WP_Webmention_Again', 'plugin_activate' ) ); + register_activation_hook( __FILE__ , array( __CLASS__ , 'plugin_activate' ) ); // clear schedules if there's any on deactivation - register_deactivation_hook( __FILE__ , array( 'WP_Webmention_Again', 'plugin_deactivate' ) ); + register_deactivation_hook( __FILE__ , array( __CLASS__ , 'plugin_deactivate' ) ); // extend current cron schedules with our entry add_filter( 'cron_schedules', array(&$this, 'add_cron_schedule' ) ); - // register the action for processing received - add_action( static::cron_received, array( &$this, 'process_received' ) ); - - // register the action for processing received - add_action( static::cron_send, array( &$this, 'process_send' ) ); - - // additional comment types - add_action( 'admin_comment_types_dropdown', array( &$this, 'comment_types_dropdown' ) ); - - // register new posts - add_action( 'transition_post_status', array( &$this, 'queue_send' ), 12, 5 ); - } - - /** - * runs at WordPress init hook - * - */ - public function init() { - - // add webmention endpoint to query vars - add_filter( 'query_vars', array( &$this, 'add_query_var' ) ); - - // because this needs one more filter - add_filter( 'get_avatar_comment_types', array( &$this, 'add_comment_types' ) ); - - // additional avatar filter - add_filter( 'get_avatar' , array( &$this, 'get_avatar' ), 1, 5 ); - - // get_pung is not restrictive enough - add_filter ( 'get_pung', array( &$this, 'get_pung' ) ); - - if ( ! wp_get_schedule( static::cron_received ) ) - wp_schedule_event( time(), static::cron_received, static::cron_received ); - - if ( ! wp_get_schedule( static::cron_send ) ) - wp_schedule_event( time(), static::cron_send, static::cron_send ); - } /** @@ -254,6 +95,8 @@ class WP_Webmention_Again { `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`) @@ -276,11 +119,6 @@ class WP_Webmention_Again { * */ public static function plugin_deactivate () { - wp_unschedule_event( time(), static::cron_received ); - wp_clear_scheduled_hook( static::cron_received ); - - wp_unschedule_event( time(), static::cron_send ); - wp_clear_scheduled_hook( static::cron_send ); global $wpdb; $dbname = $wpdb->prefix . static::tablename; @@ -306,59 +144,14 @@ class WP_Webmention_Again { */ public function add_cron_schedule ( $schedules ) { - $schedules[ static::cron_received ] = array ( - 'interval' => static::interval_received(), - 'display' => sprintf(__( 'every %d seconds' ), static::interval_received() ) - ); - - $schedules[ static::cron_send ] = array ( - 'interval' => static::interval_send(), - 'display' => sprintf(__( 'every %d seconds' ), static::interval_send() ) + $schedules[ static::cron ] = array ( + 'interval' => static::interval(), + 'display' => sprintf(__( 'every %d seconds' ), static::interval() ) ); return $schedules; } - /** - * extends HTML header with webmention endpoint - * - * @output string two lines of HTML - * - */ - public function html_header () { - $endpoint = site_url( '?'. static::endpoint() .'=endpoint' ); - - // backwards compatibility with v0.1 - echo '' . "\n"; - echo '' . "\n"; - } - - /** - * extends HTTP response header with webmention endpoints - * - * @output two lines of Link: in HTTP header - * - */ - public function http_header() { - $endpoint = site_url( '?'. static::endpoint() .'=endpoint' ); - - // backwards compatibility with v0.1 - header( 'Link: <' . $endpoint . '>; rel="http://webmention.org/"', false ); - header( 'Link: <' . $endpoint . '>; rel="webmention"', false ); - } - - /** - * add webmention to accepted query vars - * - * @param array $vars current query vars - * - * @return array extended vars - */ - public function add_query_var( $vars ) { - array_push( $vars, static::endpoint() ); - return $vars; - } - /** * get plugin options * @@ -381,62 +174,6 @@ class WP_Webmention_Again { return $options; } - /** - * extends the current registered comment types in options to have - * single char emoji as comment type - * - * @param char $reacji single emoticon character to add as comment type - * - * - public static function register_reacji ( $reacji ) { - $known_reacji = get_option( static::known_reacji() ); - - if (!is_array($known_reacji)) - $known_reacji = array(); - - if ( ! in_array( $reacji, $known_reacji ) ) { - array_push( $known_reacji, $reacji ); - update_option( static::known_reacji() , $options ); - } - - } - - /** - * Extend the "filter by comment type" of in the comments section - * of the admin interface with all of our methods - * - * @param array $types the different comment types - * - * @return array the filtered comment types - */ - public function comment_types_dropdown( $types ) { - $options = static::get_options(); - - foreach ( $options['comment_types'] as $type => $fancy ) - if ( ! isset( $types[ $type ] ) ) - $types[ $type ] = ucfirst( $type ); - - return $types; - } - - /** - * extend the abailable comment types in WordPress with the ones recognized - * by the plugin, including reacji - * - * @param array $types current types - * - * @return array extended types - */ - public function add_comment_types ( $types ) { - $options = static::get_options(); - - foreach ( $options['comment_types'] as $type => $fancy ) - if ( ! in_array( $type, $types ) ) - array_push( $types, $type ); - - return $types; - } - /** * insert a webmention to the queue * @@ -464,8 +201,8 @@ class WP_Webmention_Again { return true; $q = $wpdb->prepare( "INSERT INTO `{$dbname}` - (`id`,`date`,`direction`, `tries`,`source`, `target`, `object_type`, `object_id`) VALUES - ( '%s', NOW(), '%s', 0, '%s', '%s', '%s', %d );", + (`id`,`date`,`direction`, `tries`,`source`, `target`, `object_type`, `object_id`, `status`, `note` ) VALUES + ( '%s', NOW(), '%s', 0, '%s', '%s', '%s', %d, 0, '' );", $id, $direction, $source, $target, $object, $object_id ); try { @@ -534,6 +271,40 @@ class WP_Webmention_Again { 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'; ", $id, $node ); + + try { + $req = $wpdb->query( $q ); + } + catch (Exception $e) { + static::debug('Something went wrong: ' . $e->getMessage()); + } + + return $req; + } + /** * get a batch of elements according to direction * @@ -553,7 +324,7 @@ class WP_Webmention_Again { global $wpdb; $dbname = $wpdb->prefix . static::tablename; - $q = $wpdb->prepare( "SELECT * FROM `{$dbname}` WHERE `direction` = '%s' LIMIT %d;", $direction, $limit ); + $q = $wpdb->prepare( "SELECT * FROM `{$dbname}` WHERE `direction` = '%s' and `status` = 0 LIMIT %d;", $direction, $limit ); try { $req = $wpdb->get_results( $q ); @@ -604,156 +375,6 @@ class WP_Webmention_Again { return false; } - /** - * parse & queue incoming webmention endpoint requests - * - * @param mixed $wp WordPress Query - * - */ - public function receive ( $wp ) { - - // check if it is a webmention request or not - if ( ! array_key_exists( static::endpoint(), $wp->query_vars ) ) - return false; - - // plain text header - header( 'Content-Type: text/plain; charset=' . get_option( 'blog_charset' ) ); - - // check if source url is transmitted - if ( ! isset( $_POST['source'] ) ) { - status_header( 400 ); - echo '"source" is missing'; - exit; - } - - // check if target url is transmitted - if ( ! isset( $_POST['target'] ) ) { - status_header( 400 ); - echo '"target" is missing'; - exit; - } - - $target = filter_var( $_POST['target'], FILTER_SANITIZE_URL ); - $source = filter_var( $_POST['source'], FILTER_SANITIZE_URL ); - - if ( false === filter_var( $target, FILTER_VALIDATE_URL ) ) { - status_header( 400 ); - echo '"target" is an invalid URL'; - exit; - } - - if ( false === filter_var( $source, FILTER_VALIDATE_URL ) ) { - status_header( 400 ); - echo '"source" is an invalid URL'; - exit; - } - - $post_id = static::validate_local( $target ); - - if (! $post_id || 0 == $post_id ) { - status_header( 404 ); - echo '"target" not found.'; - exit; - } - - // check if pings are allowed - if ( ! pings_open( $post_id ) ) { - status_header( 403 ); - echo 'Pings are disabled for this post'; - exit; - } - - // queue here, the remote check will be async - //$r = static::queue_receive( $source, $target, $post_id ); - $r = static::queue_add( 'in', $source, $target, 'post', $post_id ); - - if ( true == $r ) { - status_header( 202 ); - echo 'Webmention accepted in the queue.'; - } - else { - status_header( 500 ); - echo 'Something went wrong; please try again later!'; - } - - exit; - } - - /** - * worker method for doing received webmentions - * triggered by cron - * - */ - public function process_received () { - - $incoming = static::queue_get ( 'in', static::per_batch() ); - - if ( empty( $incoming ) ) - return true; - - foreach ( (array)$incoming as $received ) { - - // this really should not happen, but if it does, get rid of this entry immediately - if (! isset( $received->target ) || - empty( $received->target ) || - ! isset( $received->source ) || - empty( $received->source ) - ) { - static::debug( " target or souce empty, aborting" ); - static::queue_del ( $received->id ); - continue; - } - - static::debug( "processing webmention: target -> {$received->target}, source -> {$received->source}" ); - - if ( empty( $received->object_id ) || 0 == $received->object_id ) - $post_id = url_to_postid ( $received->target ); - else - $post_id = $received->object_id; - $post = get_post ( $post_id ); - - if ( ! static::is_post( $post ) ) { - static::debug( " no post found for this mention, try again later, who knows?" ); - //static::queue_del ( $received->id ); - continue; - } - - // too many retries, drop this mention and walk away - if ( $received->tries >= static::retry() ) { - static::debug( " this mention was tried earlier and failed too many times, drop it" ); - static::queue_del ( $received->id ); - continue; - } - - // increment retries - static::queue_inc ( $received->id ); - - // validate target - $remote = static::try_receive_remote( $post_id, $received->source, $received->target ); - - if ( false === $remote || empty( $remote ) ) { - static::debug( " parsing this mention failed, retrying next time" ); - continue; - } - - // we have remote data ! - $c = static::try_parse_remote ( $post_id, $received->source, $received->target, $remote ); - $ins = static::insert_comment ( $post_id, $received->source, $received->target, $remote, $c ); - - if ( true === $ins ) { - static::debug( " duplicate (or something similar): this queue element has to be ignored; deleting queue entry" ); - static::queue_del ( $received->id ); - } - elseif ( is_numeric( $ins ) ) { - static::debug( " all went well, we have a comment id: {$ins}, deleting queue entry" ); - static::queue_del ( $received->id ); - } - else { - static::debug( "This is unexpected. Try again." ); - } - } - } - /** * extended wp_remote_get with debugging * @@ -792,649 +413,6 @@ class WP_Webmention_Again { return $q; } - /** - * try to get contents of webmention originator - * - * @param int $post_id ID of post - * @param string $source Originator URL - * @param string $target Target URL - * - * @return bool|array false on error; plain array or Mf2 parsed (and - * flattened ) array of remote content on success - */ - protected static function try_receive_remote ( &$post_id, &$source, &$target ) { - - $content = false; - $q = static::_wp_remote_get( $source ); - - if ( false === $q ) - return false; - - $targets = array ( - $target, - wp_get_shortlink( $post_id ), - get_permalink( $post_id ) - ); - - $found = false; - - foreach ( $targets as $k => $t ) { - $t = preg_replace( '/https?:\/\/(?:www.)?/', '', $t ); - $t = preg_replace( '/#.*/', '', $t ); - $t = untrailingslashit( $t ); - //$targets[ $k ] = $t; - - if ( ! stristr( $q['body'], $t ) ) - $found = true; - - } - - // check if source really links to target - // this could be a temporary error, so we'll retry later this one as well - if ( false == $found ) { - static::debug( " missing link to {$t} in remote body" ); - return false; - } - - $ctype = isset( $q['headers']['content-type'] ) ? $q['headers']['content-type'] : 'text/html'; - - if ( "text/plain" == $ctype ) { - static::debug( " interesting, plain text webmention. I'm not prepared for this yet" ); - return false; - } - elseif ( $ctype == "application/json" ) { - static::debug( " content is JSON" ); - $content = json_decode( $q['body'], true ); - } - else { - static::debug( " content is (probably) html, trying to parse it with MF2" ); - try { - $content = Mf2\parse( $q['body'], $source ); - } - catch ( Exception $e ) { - static::debug( " parsing MF2 failed: " . $e->getMessage() ); - return false; - } - - $content = static::flatten_mf2_array( $content ); - } - - return $content; - } - - /** - * try to convert remote content to comment - * - * @param int $post_id ID of post - * @param string $source Originator URL - * @param string $target Target URL - * @param array $content (Mf2) array of remote content - * - * @return bool|int false on error; insterted comment ID on success - */ - protected static function try_parse_remote ( &$post_id, &$source, &$target, &$content ) { - - $item = false; - $p_authors = array(); - - if ( isset( $content['items']['properties'] ) && isset( $content['items']['type'] ) ) { - $item = $content['items']; - } - elseif ( is_array($content['items'] ) && ! empty( $content['items']['type'] ) ) { - foreach ( $content['items'] as $i ) { - if ( 'h-entry' == $i['type'] ) { - $items[] = $i; - } - elseif ( 'h-card' == $i['type'] ) { - $p_authors[] = $i; - } - elseif ( 'u-comment' == $i['type'] ) { - $comments[] = $i; - } - } - } - - if ( ! empty ( $items ) ) - $item = array_pop( $items ); - elseif ( empty( $items ) && ! empty( $comments ) ) - $item = array_pop( $comments ); - - if (! $item || empty( $item )) { - static::debug(' no parseable h-entry found, saving as standard mention comment'); - $c = array ( - 'comment_author' => $source, - 'comment_author_url' => $source, - 'comment_author_email' => '', - 'comment_post_ID' => $post_id, - 'comment_type' => 'webmention', - 'comment_date' => date("Y-m-d H:i:s"), - 'comment_date_gmt' => date("Y-m-d H:i:s"), - 'comment_agent' => __CLASS__, - 'comment_approved' => 0, - 'comment_content' => sprintf( __( 'This entry was webmentioned on %s.' ), $source, $source ), - ); - return $c; - } - - // process author - $author_name = $author_url = $avatar = $a = false; - - if ( isset( $item['properties']['author'] ) ) { - $a = $item['properties']['author']; - } - elseif ( ! empty( $p_authors ) ) { - $a = array_pop( $p_authors ); - } - - if ( $a && isset( $a['properties'] ) ) { - $a = $a['properties']; - - if ( isset($a['name']) && ! empty( $a['name'] ) ) - $author_name = $a['name']; - - $try_photos = array ('photo', 'avatar'); - $p = false; - foreach ( $try_photos as $photo ) { - if (isset( $a[ $photo ]) && ! empty( $a[ $photo ] ) ) { - $p = $a[ $photo ]; - if ( !empty( $p ) ) { - $avatar = $p; - break; - } - } - } - } - - // process type - $type = 'webmention'; - - foreach ( static::mftypes() as $k => $mapped ) { - if ( is_array( $item['properties'] ) && isset( $item['properties'][ $mapped ]) ) - $type = $k; - } - - //process content - $c = ''; - if ( isset( $item['properties']['content'] ) && isset( $item['properties']['content']['html'] ) ) - $c = $item['properties']['content']['html']; - if ( isset( $item['properties']['content'] ) && isset( $item['properties']['content']['value'] ) ) - $c = wp_filter_kses( $item['properties']['content']['value'] ); - - // REACJI - $emoji = EmojiRecognizer::isSingleEmoji( $c ); - - if ( $emoji ) { - static::debug( "wheeeee, reacji!" ); - $type = 'reacji'; - //static::register_reacji( $type ); - } - - // process date - if ( isset( $item['properties']['modified'] ) ) - $date = strtotime( $item['properties']['modified'] ); - elseif ( isset( $item['properties']['published'] ) ) - $date = strtotime( $item['properties']['published'] ); - else - $date = time(); - - $name = empty( $author_name ) ? $source : $author_name; - - $c = array ( - 'comment_author' => $name, - 'comment_author_url' => $source, - 'comment_author_email' => '', - 'comment_post_ID' => $post_id, - 'comment_type' => $type, - 'comment_date' => date( "Y-m-d H:i:s", $date ), - 'comment_date_gmt' => date( "Y-m-d H:i:s", $date ), - 'comment_agent' => __CLASS__, - 'comment_approved' => 0, - 'comment_content' => $c, - 'comment_avatar' => $avatar, - ); - - return $c; - } - - /** - * Comment inserter - * - * @param string &$post_id post ID - * @param string &$source Originator URL - * @param string &$target Target URL - * @param mixed &$raw Raw format of the comment, like JSON response from the provider - * @param array &$comment array formatted to match a WP Comment requirement - * - */ - protected static function insert_comment ( &$post_id, &$source, &$target, &$raw, &$comment ) { - - $comment_id = false; - - $avatar = false; - if( isset( $comment['comment_avatar'] ) ) { - $avatar = $comment['comment_avatar']; - unset( $comment['comment_avatar'] ); - } - - // safety first - $comment['comment_author_email'] = filter_var ( $comment['comment_author_email'], FILTER_SANITIZE_EMAIL ); - $comment['comment_author_url'] = filter_var ( $comment['comment_author_url'], FILTER_SANITIZE_URL ); - $comment['comment_author'] = filter_var ( $comment['comment_author'], FILTER_SANITIZE_STRING); - - //test if we already have this imported - $testargs = array( - 'author_url' => $comment['comment_author_url'], - 'post_id' => $post_id, - ); - - // so if the type is comment and you add type = 'comment', WP will not return the comments - // such logical! - if ( 'comment' != $comment['comment_type'] ) - $testargs['type'] = $comment['comment_type']; - - // in case it's a fav or a like, the date field is not always present - // but there should be only one of those, so the lack of a date field indicates - // that we should not look for a date when checking the existence of the - // comment - if ( isset( $comment['comment_date'] ) && ! empty( $comment['comment_date'] ) ) { - // in case you're aware of a nicer way of doing this, please tell me - // or commit a change... - - $tmp = explode ( " ", $comment['comment_date'] ); - $d = explode( "-", $tmp[0] ); - $t = explode ( ':', $tmp[1] ); - - $testargs['date_query'] = array( - 'year' => $d[0], - 'monthnum' => $d[1], - 'day' => $d[2], - 'hour' => $t[0], - 'minute' => $t[1], - 'second' => $t[2], - ); - - //test if we already have this imported - static::debug( "checking comment existence (with date) for post #{$post_id}" ); - } - else { - // we do need a date - $comment['comment_date'] = date( "Y-m-d H:i:s" ); - $comment['comment_date_gmt'] = date( "Y-m-d H:i:s" ); - - static::debug( "checking comment existence (no date) for post #{$post_id}" ); - } - - $existing = get_comments( $testargs ); - - // no matching comment yet, insert it - if ( ! empty( $existing ) ) { - static::debug ( "comment already exists" ); - return true; - } - - // disable flood control, just in case - remove_filter( 'check_comment_flood', 'check_comment_flood_db', 10, 3 ); - $comment = apply_filters( 'preprocess_comment', $comment ); - - if ( $comment_id = wp_new_comment( $comment ) ) { - - // add avatar for later use if present - if ( ! empty( $avatar ) ) - update_comment_meta( $comment_id, 'avatar', $avatar ); - - // full raw response for the vote, just in case - update_comment_meta( $comment_id, 'webmention_source_mf2', $raw ); - - // original request - update_comment_meta( $comment_id, 'webmention_original', array( 'target' => $target, 'source' => $source) ); - - // info - $r = "new comment inserted for {$post_id} as #{$comment_id}"; - - // notify author - // wp_notify_postauthor( $comment_id ); - } - else { - $r = "something went wrong when trying to insert comment for post #{$post_id}"; - } - - // re-add flood control - add_filter( 'check_comment_flood', 'check_comment_flood_db', 10, 3 ); - - static::debug( $r ); - - return $comment_id; - } - - - /** - * triggered on post transition, applied when new status is publish, therefore - * applied on edit of published posts as well - * add a post meta to the post to be processed by the send processor - * - * @param string $new_status New post status - * @param string $old_status Previous post status - * @param object $post WP Post object - * - public function queue_send( $new_status, $old_status, $post ) { - if ( ! static::is_post( $post ) ) { - static::debug( "Whoops, this is not a post." ); - return false; - } - - if ( 'publish' != $new_status ) { - static::debug( "Not adding {$post->ID} to mention queue yet; not published" ); - return false; - } - - $r = add_post_meta( $post->ID, static::meta_send, 1, true ); - - if ( ! $r ) { - static::debug( "Tried adding post #{$post->ID} to mention queue, but it didn't go well" ); - } - //else { - //// fire up a single cron event if the scheduled is too far in the future - //$next = wp_next_scheduled( static::cron_send ) - time (); - //if ( $next > static::interval_send_min() ) - //wp_schedule_single_event( time() , static::cron_send ); - //} - - return $r; - } - */ - - /** - * triggered on post transition, applied when new status is publish, therefore - * applied on edit of published posts as well - * add a post meta to the post to be processed by the send processor - * - * @param string $new_status New post status - * @param string $old_status Previous post status - * @param object $post WP Post object - */ - public function queue_send( $new_status, $old_status, $post ) { - - if ( ! static::is_post( $post ) ) { - static::debug( "Whoops, this is not a post." ); - return false; - } - - if ( 'publish' != $new_status ) { - static::debug( "Not adding {$post->ID} to mention queue yet; not published" ); - return false; - } - - static::debug("Trying to get urls for #{$post->ID}"); - - // try to avoid redirects, so no shortlink is sent for now as source - $source = get_permalink( $post->ID ); - - // process the content as if it was the_content() - $content = static::get_the_content( $post ); - - // get all urls in content - $urls = static::extract_urls( $content ); - - // for special ocasions when someone wants to add to this list - $urls = apply_filters( 'webmention_links', $urls, $post->ID ); - - // lowercase url is good for your mental health - foreach ( $urls as $k => $url ) - $urls[ $k ] = strtolower( $url ); - - // remove all already pinged urls - $pung = get_pung( $post->ID ); - $urls = array_diff ( $urls, $pung ); - - foreach ( $urls as $target ) { - $r = static::queue_add ( 'out', $source, $target, $post->post_type, $post->ID ); - - if ( !$r ) - static::debug( " tried adding post #{$post->ID}, url: {$target} to mention queue, but it didn't go well" ); - } - - } - - /** - * worker method for doing received webmentions - * triggered by cron - * - */ - public function process_send () { - - $outgoing = static::queue_get ( 'out', static::per_batch() ); - - if ( empty( $outgoing ) ) - return true; - - foreach ( (array)$outgoing as $send ) { - - // this really should not happen, but if it does, get rid of this entry immediately - if (! isset( $send->target ) || - empty( $send->target ) || - ! isset( $send->source ) || - empty( $send->source ) - ) { - static::debug( " target or souce empty, aborting" ); - static::queue_del ( $send->id ); - continue; - } - - static::debug( "processing webmention: target -> {$send->target}, source -> {$send->source}" ); - - // too many retries, drop this mention and walk away - if ( $send->tries >= static::retry() ) { - static::debug( " this mention was tried earlier and failed too many times, drop it" ); - static::queue_del ( $send->id ); - continue; - } - - // increment retries - static::queue_inc ( $send->id ); - - // try sending - $s = static::send( $send->source, $send->target ); - - if ( !$s ) { - static::debug( " sending failed; retrying later ({$tries} time)" ); - } - else { - static::debug( " sending succeeded!" ); - - $post_types = get_post_types( '', 'names' ); - if ( in_array( $send->object_type, $post_types ) && 0 != $send->object_id ) - add_ping( $send->object_id, $send->target ); - - static::queue_del ( $send->id ); - } - } - } - - - /** - * make pung stricter - * - * @param array $pung array of pinged urls - * - * @return array a better array of pinged urls - * - */ - public function get_pung ( $pung ) { - - foreach ($pung as $k => $e ) - $pung[ $k ] = strtolower( $e ); - - $pung = array_unique($pung); - - return $pung; - } - - /** - * get all posts that have meta entry for sending - * - * @return array of WP Post objects - */ - protected static function get_send () { - global $wpdb; - - $r = array(); - - $dbname = "{$wpdb->prefix}postmeta"; - $key = static::meta_send; - $limit = static::per_batch(); - $db_command = "SELECT DISTINCT `post_id` FROM `{$dbname}` WHERE `meta_key` = '{$key}' LIMIT {$limit}"; - - try { - $q = $wpdb->get_results( $db_command ); - } - catch ( Exception $e ) { - static::debug( "Something went wrong: " . $e->getMessage() ); - } - - if ( ! empty( $q ) && is_array( $q ) ) { - foreach ( $q as $post ) { - array_push( $r, $post->post_id ); - } - } - - return $r; - } - - /** - * send a single webmention - * based on [webmention](https://github.com/pfefferle/wordpress-webmention) - * - */ - public static function send ( $source, $target, $post_id = false ) { - $options = static::get_options(); - - // stop selfpings on the same URL - if ( isset( $options['disable_selfpings_same_url'] ) && - $options['disable_selfpings_same_url'] == '1' && - $source === $target - ) - return false; - - // stop selfpings on the same domain - if ( isset( $options['disable_selfpings_same_domain'] ) && - $options['disable_selfpings_same_domain'] == '1' && - parse_url( $source, PHP_URL_HOST ) == parse_url( $target, PHP_URL_HOST ) - ) - return false; - - // discover the webmention endpoint - $webmention_server_url = static::discover_endpoint( $target ); - - // if I can't find an endpoint, perhaps you can! - $webmention_server_url = apply_filters( 'webmention_server_url', $webmention_server_url, $target ); - - if ( $webmention_server_url ) { - $args = array( - 'body' => 'source=' . urlencode( $source ) . '&target=' . urlencode( $target ), - 'timeout' => static::remote_timeout(), - ); - - static::debug( "Sending webmention to: " .$webmention_server_url . " as: " . $args['body'] ); - $response = wp_remote_post( $webmention_server_url, $args ); - - // use the response to do something usefull - // do_action( 'webmention_post_send', $response, $source, $target, $post_ID ); - - return $response; - } - - return false; - } - - /** - * Finds a WebMention server URI based on the given URL - * - * code from [webmention](https://github.com/pfefferle/wordpress-webmention) - * - * Checks the HTML for the rel="http://webmention.org/" link and http://webmention.org/ headers. It does - * a check for the http://webmention.org/ headers first and returns that, if available. The - * check for the rel="http://webmention.org/" has more overhead than just the header. - * - * @param string $url URL to ping - * - * @return bool|string False on failure, string containing URI on success - */ - protected static function discover_endpoint( $url ) { - - // Not an URL. This should never happen. - if ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) - return false; - - // do not search for a WebMention server on our own uploads - $uploads_dir = wp_upload_dir(); - if ( 0 === strpos( $url, $uploads_dir['baseurl'] ) ) - return false; - - $response = wp_remote_head( $url, array( 'timeout' => static::remote_timeout(), 'httpversion' => '1.0' ) ); - - if ( is_wp_error( $response ) ) { - static::debug( "Something went wrong: " . $response->get_error_message() ); - return false; - } - - // check link header - if ( $links = wp_remote_retrieve_header( $response, 'link' ) ) { - if ( is_array( $links ) ) { - foreach ( $links as $link ) { - if ( preg_match( '/<(.[^>]+)>;\s+rel\s?=\s?[\"\']?(http:\/\/)?webmention(.org)?\/?[\"\']?/i', $link, $result ) ) { - return self::make_url_absolute( $url, $result[1] ); - } - } - } else { - if ( preg_match( '/<(.[^>]+)>;\s+rel\s?=\s?[\"\']?(http:\/\/)?webmention(.org)?\/?[\"\']?/i', $links, $result ) ) { - return self::make_url_absolute( $url, $result[1] ); - } - } - } - - // not an (x)html, sgml, or xml page, no use going further - if ( preg_match( '#(image|audio|video|model)/#is', wp_remote_retrieve_header( $response, 'content-type' ) ) ) { - return false; - } - - // now do a GET since we're going to look in the html headers (and we're sure its not a binary file) - $response = wp_remote_get( $url, array( 'timeout' => static::remote_timeout(), 'httpversion' => '1.0' ) ); - - if ( is_wp_error( $response ) ) { - return false; - } - - $contents = wp_remote_retrieve_body( $response ); - - // boost performance and use alreade the header - $header = substr( $contents, 0, stripos( $contents, '' ) ); - - // unicode to HTML entities - $contents = mb_convert_encoding( $contents, 'HTML-ENTITIES', mb_detect_encoding( $contents ) ); - - libxml_use_internal_errors( true ); - - $doc = new DOMDocument(); - $doc->loadHTML( $contents ); - - $xpath = new DOMXPath( $doc ); - - // check elements - // checks only head-links - foreach ( $xpath->query( '//head/link[contains(concat(" ", @rel, " "), " webmention ") or contains(@rel, "webmention.org")]/@href' ) as $result ) { - return self::make_url_absolute( $url, $result->value ); - } - - // check elements - // checks only body>a-links - foreach ( $xpath->query( '//body//a[contains(concat(" ", @rel, " "), " webmention ") or contains(@rel, "webmention.org")]/@href' ) as $result ) { - return self::make_url_absolute( $url, $result->value ); - } - - return false; - } - - /** * validated a URL that is supposed to be on our site * @@ -1555,34 +533,6 @@ class WP_Webmention_Again { return $scheme . '://' . $abs; } - - /** - * of there is a comment meta 'avatar' field, use that as avatar for the commenter - * - * @param string $avatar the current avatar image string - * @param mixed $id_or_email this could be anything that triggered the avatar all - * @param string $size size for the image to display - * @param string $default optional fallback - * @param string $alt alt text for the avatar image - */ - public function get_avatar( $avatar, $id_or_email, $size, $default = '', $alt = '' ) { - if ( ! is_object( $id_or_email ) || ! isset( $id_or_email->comment_type ) ) - return $avatar; - - // check if comment has an avatar - $c_avatar = get_comment_meta( $id_or_email->comment_ID, 'avatar', true ); - - if ( ! $c_avatar ) - return $avatar; - - if ( false === $alt ) - $safe_alt = ''; - else - $safe_alt = esc_attr( $alt ); - - return sprintf( '%s', $safe_alt, $c_avatar, $size, $size ); - } - /** * get content like the_content * @@ -1631,6 +581,20 @@ class WP_Webmention_Again { } } -$WP_Webmention_Again = new WP_Webmention_Again(); + +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 ) { + return WP_Webmention_Again_Sender::queue ( 'out', $source, $target ); + } +} + endif; \ No newline at end of file