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);
}
public function getTitle()
public function getTitle($imply=true)
{
$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: %1$s is a user name, %2$s is the notice creation date/time.
$title = sprintf(_('%1$s\'s status on %2$s'),

View File

@ -179,8 +179,9 @@ class NoticeListItem extends Widget
function showNoticeTitle()
{
if (Event::handle('StartShowNoticeTitle', array($this))) {
$nameClass = $this->notice->getTitle(false) ? 'p-name ' : '';
$this->element('a', array('href' => $this->notice->getUri(),
'class' => 'p-name u-uid'),
'class' => $nameClass . 'u-uid'),
$this->notice->getTitle());
Event::handle('EndShowNoticeTitle', array($this));
}
@ -348,7 +349,8 @@ class NoticeListItem extends Widget
function showContent()
{
// 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 ($this->maxchars > 0 && mb_strlen($this->notice->content) > $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;
}
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)
{
common_log(LOG_DEBUG,"Attempting linkback for " . $url);
$orig = $url;
$url = htmlspecialchars_decode($orig);
$scheme = parse_url($url, PHP_URL_SCHEME);
if (!in_array($scheme, array('http', 'https'))) {
$base = parse_url($url);
if (!in_array($base['scheme'], array('http', 'https'))) {
return $orig;
}
@ -126,13 +140,17 @@ class LinkbackPlugin extends Plugin
return $orig;
}
// XXX: Should handle relative-URI resolution in these detections
$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
// Ref: https://github.com/converspace/webmention/issues/43
$this->webmention($url, $wm);
$this->webmention($url, $this->unparse_url($wm));
} else {
$pb = $this->getPingback($response);
if (!empty($pb)) {
@ -156,26 +174,26 @@ class LinkbackPlugin extends Plugin
$link = $response->getHeader('Link');
if (!is_null($link)) {
// 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)) {
return $match[1];
} elseif (preg_match('~<((?:https?://)?[^>]+)>; rel="http://webmention.org/?"~', $link, $match)) {
if (preg_match('~<([^>]+)>; rel="?(?:[^" ]* )*(?:http://webmention.org/|webmention)(?: [^" ]*)*"?~', $link, $match)) {
return $match[1];
}
}
// FIXME: Do proper DOM traversal
if(preg_match('/<(?:link|a)[ ]+href="([^"]+)"[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]*\/?>/i', $response->getBody(), $match)
|| preg_match('/<(?:link|a)[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]+href="([^"]+)"[ ]*\/?>/i', $response->getBody(), $match)) {
return $match[1];
} 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)) {
// Currently fails https://webmention.rocks/test/13, https://webmention.rocks/test/17
if(preg_match('~<(?:link|a)[ ]+href="([^"]*)"[ ]+rel="(?:[^" ]* )*(?:http://webmention.org/|webmention)(?: [^" ]*)*"[ ]*/?>~i', $response->getBody(), $match)
|| preg_match('~<(?:link|a)[ ]+rel="(?:[^" ]* )*(?:http://webmention.org/|webmention)(?: [^" ]*)*"[ ]+href="([^"]*)"[ ]*/?>~i', $response->getBody(), $match)) {
return $match[1];
}
return NULL;
}
function webmention($url, $endpoint) {
$source = $this->notice->getUrl();
common_log(LOG_DEBUG,"Attempting webmention to $endpoint for $url from $source");
$payload = array(
'source' => $source,
'target' => $url
@ -191,7 +209,7 @@ class LinkbackPlugin extends Plugin
$payload
);
if(!in_array($response->getStatus(), array(200,202))) {
if(!in_array($response->getStatus(), array(200,201,202))) {
common_log(LOG_WARNING,
"Webmention request failed for '$url' ($endpoint)");
}

View File

@ -40,30 +40,30 @@ class WebmentionAction extends Action
if(!$source) {
echo _m('"source" is missing')."\n";
throw new ServerException(_m('"source" is missing'), 400);
throw new ClientException(_m('"source" is missing'), 400);
}
if(!$target) {
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);
if(!$response) {
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);
if(!$notice) {
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);
if(!$url) {
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";

View File

@ -7,7 +7,7 @@ function linkback_lenient_target_match($body, $target) {
function linkback_get_source($source, $target) {
// Check if we are pinging ourselves and ignore
$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);
return NULL;
}
@ -22,7 +22,7 @@ function linkback_get_source($source, $target) {
$body = htmlspecialchars_decode($response->getBody());
// 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;
}
@ -56,7 +56,7 @@ function linkback_get_target($target) {
}
if(!$user) {
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]);
}
}
@ -70,7 +70,7 @@ function linkback_get_target($target) {
function linkback_is_contained_in($entry, $target) {
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'];
}
@ -79,7 +79,7 @@ function linkback_is_contained_in($entry, $target) {
if(isset($obj['type']) && array_intersect(array('h-cite', 'h-entry'), $obj['type']) &&
isset($obj['properties']) && isset($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'];
}
@ -130,7 +130,7 @@ function linkback_entry_type($entry, $mf2, $target) {
if($mf2['rels'] && $mf2['rels']['in-reply-to']) {
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';
}
}
@ -144,7 +144,7 @@ function linkback_entry_type($entry, $mf2, $target) {
);
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]; }
}
@ -152,7 +152,7 @@ function linkback_entry_type($entry, $mf2, $target) {
if(isset($obj['type']) && array_intersect(array('h-cite', 'h-entry'), $obj['type']) &&
isset($obj['properties']) && isset($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]; }
}
@ -243,8 +243,8 @@ function linkback_notice($source, $notice_or_user, $entry, $author, $mf2) {
if (isset($entry['published']) || isset($entry['updated'])) {
$options['created'] = isset($entry['published'])
? common_sql_date($entry['published'][0])
: common_sql_date($entry['updated'][0]);
? common_sql_date(strtotime($entry['published'][0]))
: common_sql_date(strtotime($entry['updated'][0]));
}
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);
}
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) {
if(isset($entry['properties']['author']) && isset($entry['properties']['author'][0]['properties'])) {
$author = $entry['properties']['author'][0]['properties'];
if(isset($entry['author']) && isset($entry['author'][0]['properties'])) {
$author = $entry['author'][0]['properties'];
} else {
$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->created = common_sql_now();
$profile->insert();
if($author['photo'] && $author['photo'][0]) {
linkback_avatar($profile, $author['photo'][0]);
}
}
return array($profile, $author);