Merge branch 'webmention-rocks' into 'nightly'

webmention.rocks

I have improved the webmention handling so that all but two of the webmention.rocks compliance tests pass now.  Also improved parsing of time/authors on incoming webmentions.

See merge request !128
This commit is contained in:
mmn 2016-06-17 16:26:21 -04:00
commit d4295cfb25
5 changed files with 93 additions and 36 deletions

View File

@ -248,10 +248,10 @@ class Notice extends Managed_DataObject
return common_local_url('shownotice', array('notice' => $this->id), null, null, false); return common_local_url('shownotice', array('notice' => $this->id), null, null, false);
} }
public function getTitle() public function getTitle($imply=true)
{ {
$title = null; $title = null;
if (Event::handle('GetNoticeTitle', array($this, &$title))) { if (Event::handle('GetNoticeTitle', array($this, &$title)) && $imply) {
// TRANS: Title of a notice posted without a title value. // TRANS: Title of a notice posted without a title value.
// TRANS: %1$s is a user name, %2$s is the notice creation date/time. // TRANS: %1$s is a user name, %2$s is the notice creation date/time.
$title = sprintf(_('%1$s\'s status on %2$s'), $title = sprintf(_('%1$s\'s status on %2$s'),

View File

@ -179,8 +179,9 @@ class NoticeListItem extends Widget
function showNoticeTitle() function showNoticeTitle()
{ {
if (Event::handle('StartShowNoticeTitle', array($this))) { if (Event::handle('StartShowNoticeTitle', array($this))) {
$nameClass = $this->notice->getTitle(false) ? 'p-name ' : '';
$this->element('a', array('href' => $this->notice->getUri(), $this->element('a', array('href' => $this->notice->getUri(),
'class' => 'p-name u-uid'), 'class' => $nameClass . 'u-uid'),
$this->notice->getTitle()); $this->notice->getTitle());
Event::handle('EndShowNoticeTitle', array($this)); Event::handle('EndShowNoticeTitle', array($this));
} }
@ -348,7 +349,8 @@ class NoticeListItem extends Widget
function showContent() function showContent()
{ {
// FIXME: URL, image, video, audio // FIXME: URL, image, video, audio
$this->out->elementStart('article', array('class' => 'e-content')); $nameClass = $this->notice->getTitle(false) ? '' : 'p-name ';
$this->out->elementStart('article', array('class' => $nameClass . 'e-content'));
if (Event::handle('StartShowNoticeContent', array($this->notice, $this->out, $this->out->getScoped()))) { if (Event::handle('StartShowNoticeContent', array($this->notice, $this->out, $this->out->getScoped()))) {
if ($this->maxchars > 0 && mb_strlen($this->notice->content) > $this->maxchars) { if ($this->maxchars > 0 && mb_strlen($this->notice->content) > $this->maxchars) {
$this->out->text(mb_substr($this->notice->content, 0, $this->maxchars) . '[…]'); $this->out->text(mb_substr($this->notice->content, 0, $this->maxchars) . '[…]');

View File

@ -101,14 +101,28 @@ class LinkbackPlugin extends Plugin
return true; return true;
} }
function unparse_url($parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "$scheme$user$pass$host$port$path$query$fragment";
}
function linkbackUrl($url) function linkbackUrl($url)
{ {
common_log(LOG_DEBUG,"Attempting linkback for " . $url); common_log(LOG_DEBUG,"Attempting linkback for " . $url);
$orig = $url; $orig = $url;
$url = htmlspecialchars_decode($orig); $url = htmlspecialchars_decode($orig);
$scheme = parse_url($url, PHP_URL_SCHEME); $base = parse_url($url);
if (!in_array($scheme, array('http', 'https'))) { if (!in_array($base['scheme'], array('http', 'https'))) {
return $orig; return $orig;
} }
@ -126,13 +140,17 @@ class LinkbackPlugin extends Plugin
return $orig; return $orig;
} }
// XXX: Should handle relative-URI resolution in these detections
$wm = $this->getWebmention($response); $wm = $this->getWebmention($response);
if(!empty($wm)) { if(!is_null($wm)) {
$wm = parse_url($wm);
if(!$wm) $wm = array();
if(!$wm['host']) $wm['host'] = $base['host'];
if(!$wm['scheme']) $wm['scheme'] = $base['scheme'];
if(!$wm['path']) $wm['path'] = $base['path'];
// It is the webmention receiver's job to resolve source // It is the webmention receiver's job to resolve source
// Ref: https://github.com/converspace/webmention/issues/43 // Ref: https://github.com/converspace/webmention/issues/43
$this->webmention($url, $wm); $this->webmention($url, $this->unparse_url($wm));
} else { } else {
$pb = $this->getPingback($response); $pb = $this->getPingback($response);
if (!empty($pb)) { if (!empty($pb)) {
@ -156,26 +174,26 @@ class LinkbackPlugin extends Plugin
$link = $response->getHeader('Link'); $link = $response->getHeader('Link');
if (!is_null($link)) { if (!is_null($link)) {
// XXX: the fetcher gives back a comma-separated string of all Link headers, I hope the parsing works reliably // XXX: the fetcher gives back a comma-separated string of all Link headers, I hope the parsing works reliably
if (preg_match('~<((?:https?://)?[^>]+)>; rel="webmention"~', $link, $match)) { if (preg_match('~<([^>]+)>; rel="?(?:[^" ]* )*(?:http://webmention.org/|webmention)(?: [^" ]*)*"?~', $link, $match)) {
return $match[1];
} elseif (preg_match('~<((?:https?://)?[^>]+)>; rel="http://webmention.org/?"~', $link, $match)) {
return $match[1]; return $match[1];
} }
} }
// FIXME: Do proper DOM traversal // FIXME: Do proper DOM traversal
if(preg_match('/<(?:link|a)[ ]+href="([^"]+)"[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]*\/?>/i', $response->getBody(), $match) // Currently fails https://webmention.rocks/test/13, https://webmention.rocks/test/17
|| preg_match('/<(?:link|a)[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]+href="([^"]+)"[ ]*\/?>/i', $response->getBody(), $match)) { if(preg_match('~<(?:link|a)[ ]+href="([^"]*)"[ ]+rel="(?:[^" ]* )*(?:http://webmention.org/|webmention)(?: [^" ]*)*"[ ]*/?>~i', $response->getBody(), $match)
return $match[1]; || preg_match('~<(?:link|a)[ ]+rel="(?:[^" ]* )*(?:http://webmention.org/|webmention)(?: [^" ]*)*"[ ]+href="([^"]*)"[ ]*/?>~i', $response->getBody(), $match)) {
} elseif (preg_match('/<(?:link|a)[ ]+href="([^"]+)"[ ]+rel="http:\/\/webmention\.org\/?"[ ]*\/?>/i', $response->getBody(), $match)
|| preg_match('/<(?:link|a)[ ]+rel="http:\/\/webmention\.org\/?"[ ]+href="([^"]+)"[ ]*\/?>/i', $response->getBody(), $match)) {
return $match[1]; return $match[1];
} }
return NULL;
} }
function webmention($url, $endpoint) { function webmention($url, $endpoint) {
$source = $this->notice->getUrl(); $source = $this->notice->getUrl();
common_log(LOG_DEBUG,"Attempting webmention to $endpoint for $url from $source");
$payload = array( $payload = array(
'source' => $source, 'source' => $source,
'target' => $url 'target' => $url
@ -191,7 +209,7 @@ class LinkbackPlugin extends Plugin
$payload $payload
); );
if(!in_array($response->getStatus(), array(200,202))) { if(!in_array($response->getStatus(), array(200,201,202))) {
common_log(LOG_WARNING, common_log(LOG_WARNING,
"Webmention request failed for '$url' ($endpoint)"); "Webmention request failed for '$url' ($endpoint)");
} }

View File

@ -40,30 +40,30 @@ class WebmentionAction extends Action
if(!$source) { if(!$source) {
echo _m('"source" is missing')."\n"; echo _m('"source" is missing')."\n";
throw new ServerException(_m('"source" is missing'), 400); throw new ClientException(_m('"source" is missing'), 400);
} }
if(!$target) { if(!$target) {
echo _m('"target" is missing')."\n"; echo _m('"target" is missing')."\n";
throw new ServerException(_m('"target" is missing'), 400); throw new ClientException(_m('"target" is missing'), 400);
} }
$response = linkback_get_source($source, $target); $response = linkback_get_source($source, $target);
if(!$response) { if(!$response) {
echo _m('Source does not link to target.')."\n"; echo _m('Source does not link to target.')."\n";
throw new ServerException(_m('Source does not link to target.'), 400); throw new ClientException(_m('Source does not link to target.'), 400);
} }
$notice = linkback_get_target($target); $notice = linkback_get_target($target);
if(!$notice) { if(!$notice) {
echo _m('Target not found')."\n"; echo _m('Target not found')."\n";
throw new ServerException(_m('Target not found'), 404); throw new ClientException(_m('Target not found'), 404);
} }
$url = linkback_save($source, $target, $response, $notice); $url = linkback_save($source, $target, $response, $notice);
if(!$url) { if(!$url) {
echo _m('An error occured while saving.')."\n"; echo _m('An error occured while saving.')."\n";
throw new ServerException(_m('An error occured while saving.'), 500); throw new ClientException(_m('An error occured while saving.'), 500);
} }
echo $url."\n"; echo $url."\n";

View File

@ -7,7 +7,7 @@ function linkback_lenient_target_match($body, $target) {
function linkback_get_source($source, $target) { function linkback_get_source($source, $target) {
// Check if we are pinging ourselves and ignore // Check if we are pinging ourselves and ignore
$localprefix = common_config('site', 'server') . '/' . common_config('site', 'path'); $localprefix = common_config('site', 'server') . '/' . common_config('site', 'path');
if(linkback_lenient_target_match($source, $localprefix)) { if(linkback_lenient_target_match($source, $localprefix) === 0) {
common_debug('Ignoring self ping from ' . $source . ' to ' . $target); common_debug('Ignoring self ping from ' . $source . ' to ' . $target);
return NULL; return NULL;
} }
@ -22,7 +22,7 @@ function linkback_get_source($source, $target) {
$body = htmlspecialchars_decode($response->getBody()); $body = htmlspecialchars_decode($response->getBody());
// We're slightly more lenient in our link detection than the spec requires // We're slightly more lenient in our link detection than the spec requires
if(!linkback_lenient_target_match($body, $target)) { if(linkback_lenient_target_match($body, $target) === FALSE) {
return NULL; return NULL;
} }
@ -56,7 +56,7 @@ function linkback_get_target($target) {
} }
if(!$user) { if(!$user) {
preg_match('/\/([^\/\?#]+)(?:#.*)?$/', $response->getEffectiveUrl(), $match); preg_match('/\/([^\/\?#]+)(?:#.*)?$/', $response->getEffectiveUrl(), $match);
if(linkback_lenient_target_match(common_profile_url($match[1]), $response->getEffectiveUrl())) { if(linkback_lenient_target_match(common_profile_url($match[1]), $response->getEffectiveUrl()) !== FALSE) {
$user = User::getKV('nickname', $match[1]); $user = User::getKV('nickname', $match[1]);
} }
} }
@ -70,7 +70,7 @@ function linkback_get_target($target) {
function linkback_is_contained_in($entry, $target) { function linkback_is_contained_in($entry, $target) {
foreach ((array)$entry['properties'] as $key => $values) { foreach ((array)$entry['properties'] as $key => $values) {
if(count(array_filter($values, function($x) use ($target) { return linkback_lenient_target_match($x, $target); })) > 0) { if(count(array_filter($values, function($x) use ($target) { return linkback_lenient_target_match($x, $target) !== FALSE; })) > 0) {
return $entry['properties']; return $entry['properties'];
} }
@ -79,7 +79,7 @@ function linkback_is_contained_in($entry, $target) {
if(isset($obj['type']) && array_intersect(array('h-cite', 'h-entry'), $obj['type']) && if(isset($obj['type']) && array_intersect(array('h-cite', 'h-entry'), $obj['type']) &&
isset($obj['properties']) && isset($obj['properties']['url']) && isset($obj['properties']) && isset($obj['properties']['url']) &&
count(array_filter($obj['properties']['url'], count(array_filter($obj['properties']['url'],
function($x) use ($target) { return linkback_lenient_target_match($x, $target); })) > 0 function($x) use ($target) { return linkback_lenient_target_match($x, $target) !== FALSE; })) > 0
) { ) {
return $entry['properties']; return $entry['properties'];
} }
@ -130,7 +130,7 @@ function linkback_entry_type($entry, $mf2, $target) {
if($mf2['rels'] && $mf2['rels']['in-reply-to']) { if($mf2['rels'] && $mf2['rels']['in-reply-to']) {
foreach($mf2['rels']['in-reply-to'] as $url) { foreach($mf2['rels']['in-reply-to'] as $url) {
if(linkback_lenient_target_match($url, $target)) { if(linkback_lenient_target_match($url, $target) !== FALSE) {
return 'reply'; return 'reply';
} }
} }
@ -144,7 +144,7 @@ function linkback_entry_type($entry, $mf2, $target) {
); );
foreach((array)$entry as $key => $values) { foreach((array)$entry as $key => $values) {
if(count(array_filter($values, function($x) use ($target) { return linkback_lenient_target_match($x, $target); })) > 0) { if(count(array_filter($values, function($x) use ($target) { return linkback_lenient_target_match($x, $target) != FALSE; })) > 0) {
if($classes[$key]) { return $classes[$key]; } if($classes[$key]) { return $classes[$key]; }
} }
@ -152,7 +152,7 @@ function linkback_entry_type($entry, $mf2, $target) {
if(isset($obj['type']) && array_intersect(array('h-cite', 'h-entry'), $obj['type']) && if(isset($obj['type']) && array_intersect(array('h-cite', 'h-entry'), $obj['type']) &&
isset($obj['properties']) && isset($obj['properties']['url']) && isset($obj['properties']) && isset($obj['properties']['url']) &&
count(array_filter($obj['properties']['url'], count(array_filter($obj['properties']['url'],
function($x) use ($target) { return linkback_lenient_target_match($x, $target); })) > 0 function($x) use ($target) { return linkback_lenient_target_match($x, $target) != FALSE; })) > 0
) { ) {
if($classes[$key]) { return $classes[$key]; } if($classes[$key]) { return $classes[$key]; }
} }
@ -243,8 +243,8 @@ function linkback_notice($source, $notice_or_user, $entry, $author, $mf2) {
if (isset($entry['published']) || isset($entry['updated'])) { if (isset($entry['published']) || isset($entry['updated'])) {
$options['created'] = isset($entry['published']) $options['created'] = isset($entry['published'])
? common_sql_date($entry['published'][0]) ? common_sql_date(strtotime($entry['published'][0]))
: common_sql_date($entry['updated'][0]); : common_sql_date(strtotime($entry['updated'][0]));
} }
if (isset($entry['photo']) && common_valid_http_url($entry['photo'])) { if (isset($entry['photo']) && common_valid_http_url($entry['photo'])) {
@ -280,9 +280,42 @@ function linkback_notice($source, $notice_or_user, $entry, $author, $mf2) {
return array($content, $options); return array($content, $options);
} }
function linkback_avatar($profile, $url) {
// Ripped from OStatus plugin for now
$temp_filename = tempnam(sys_get_temp_dir(), 'linback_avatar');
try {
$imgData = HTTPClient::quickGet($url);
// Make sure it's at least an image file. ImageFile can do the rest.
if (false === getimagesizefromstring($imgData)) {
return false;
}
file_put_contents($temp_filename, $imgData);
unset($imgData); // No need to carry this in memory.
$imagefile = new ImageFile(null, $temp_filename);
$filename = Avatar::filename($profile->id,
image_type_to_extension($imagefile->type),
null,
common_timestamp());
rename($temp_filename, Avatar::path($filename));
} catch (Exception $e) {
unlink($temp_filename);
throw $e;
}
// @todo FIXME: Hardcoded chmod is lame, but seems to be necessary to
// keep from accidentally saving images from command-line (queues)
// that can't be read from web server, which causes hard-to-notice
// problems later on:
//
// http://status.net/open-source/issues/2663
chmod(Avatar::path($filename), 0644);
$profile->setOriginal($filename);
}
function linkback_profile($entry, $mf2, $response, $target) { function linkback_profile($entry, $mf2, $response, $target) {
if(isset($entry['properties']['author']) && isset($entry['properties']['author'][0]['properties'])) { if(isset($entry['author']) && isset($entry['author'][0]['properties'])) {
$author = $entry['properties']['author'][0]['properties']; $author = $entry['author'][0]['properties'];
} else { } else {
$author = linkback_hcard($mf2, $response->getEffectiveUrl()); $author = linkback_hcard($mf2, $response->getEffectiveUrl());
} }
@ -315,6 +348,10 @@ function linkback_profile($entry, $mf2, $response, $target) {
$profile->nickname = isset($author['nickname']) ? $author['nickname'][0] : str_replace(' ', '', $author['name'][0]); $profile->nickname = isset($author['nickname']) ? $author['nickname'][0] : str_replace(' ', '', $author['name'][0]);
$profile->created = common_sql_now(); $profile->created = common_sql_now();
$profile->insert(); $profile->insert();
if($author['photo'] && $author['photo'][0]) {
linkback_avatar($profile, $author['photo'][0]);
}
} }
return array($profile, $author); return array($profile, $author);