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( '', $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( '', $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