From 75cb5cf454c8f698546dacebf081b5ec0ba3b283 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Thu, 21 Jul 2016 11:12:01 +0100 Subject: [PATCH 1/7] Updated .lock --- composer.lock | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/composer.lock b/composer.lock index da7115b0..cacb990a 100644 --- a/composer.lock +++ b/composer.lock @@ -59,20 +59,20 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.18.28", + "version": "3.18.31", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c75d3ba185d5db6998124fa1a99a63e5d529b247" + "reference": "dad0b7db5fa8f3c7a3805efb2a1e86a50f11fe8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c75d3ba185d5db6998124fa1a99a63e5d529b247", - "reference": "c75d3ba185d5db6998124fa1a99a63e5d529b247", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/dad0b7db5fa8f3c7a3805efb2a1e86a50f11fe8b", + "reference": "dad0b7db5fa8f3c7a3805efb2a1e86a50f11fe8b", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "~5.3|~6.0.1|~6.1", + "guzzlehttp/guzzle": "^5.3.1|^6.2.1", "guzzlehttp/promises": "~1.0", "guzzlehttp/psr7": "~1.3.1", "mtdowling/jmespath.php": "~2.2", @@ -135,7 +135,7 @@ "s3", "sdk" ], - "time": "2016-07-13 20:34:06" + "time": "2016-07-19 17:25:45" }, { "name": "barnabywalters/mf-cleaner", @@ -385,16 +385,16 @@ }, { "name": "ezyang/htmlpurifier", - "version": "v4.7.0", + "version": "v4.8.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "ae1828d955112356f7677c465f94f7deb7d27a40" + "reference": "d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/ae1828d955112356f7677c465f94f7deb7d27a40", - "reference": "ae1828d955112356f7677c465f94f7deb7d27a40", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2", + "reference": "d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2", "shasum": "" }, "require": { @@ -425,7 +425,7 @@ "keywords": [ "html" ], - "time": "2015-08-05 01:03:42" + "time": "2016-07-16 12:58:58" }, { "name": "geo-io/interface", @@ -1134,16 +1134,16 @@ }, { "name": "laravel/framework", - "version": "v5.2.39", + "version": "5.2.41", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "c2a77050269b4e03bd9a735a9f24e573a7598b8a" + "reference": "29ba2e310cfeb42ab6545bcd81ff4c2ec1f6b5c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/c2a77050269b4e03bd9a735a9f24e573a7598b8a", - "reference": "c2a77050269b4e03bd9a735a9f24e573a7598b8a", + "url": "https://api.github.com/repos/laravel/framework/zipball/29ba2e310cfeb42ab6545bcd81ff4c2ec1f6b5c2", + "reference": "29ba2e310cfeb42ab6545bcd81ff4c2ec1f6b5c2", "shasum": "" }, "require": { @@ -1200,7 +1200,8 @@ "illuminate/support": "self.version", "illuminate/translation": "self.version", "illuminate/validation": "self.version", - "illuminate/view": "self.version" + "illuminate/view": "self.version", + "tightenco/collect": "self.version" }, "require-dev": { "aws/aws-sdk-php": "~3.0", @@ -1259,7 +1260,7 @@ "framework", "laravel" ], - "time": "2016-06-17 19:25:12" + "time": "2016-07-20 13:13:06" }, { "name": "lcobucci/jwt", @@ -4454,16 +4455,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.4.6", + "version": "5.4.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "2f1fc94b77ea6418bd6a06c64a1dac0645fbce59" + "reference": "6c8a756c17a1a92a066c99860eb57922e8b723da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2f1fc94b77ea6418bd6a06c64a1dac0645fbce59", - "reference": "2f1fc94b77ea6418bd6a06c64a1dac0645fbce59", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6c8a756c17a1a92a066c99860eb57922e8b723da", + "reference": "6c8a756c17a1a92a066c99860eb57922e8b723da", "shasum": "" }, "require": { @@ -4528,7 +4529,7 @@ "testing", "xunit" ], - "time": "2016-06-16 06:01:15" + "time": "2016-07-21 06:55:27" }, { "name": "phpunit/phpunit-mock-objects", From 558548380003ec4135a9cfe4a9b468c3203db699 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Fri, 29 Jul 2016 09:55:27 +0100 Subject: [PATCH 2/7] Start work on better webmention support --- .../Controllers/WebMentionsController.php | 5 ++- app/Jobs/ProcessWebMention.php | 33 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/app/Http/Controllers/WebMentionsController.php b/app/Http/Controllers/WebMentionsController.php index fc254058..ad0a59fe 100644 --- a/app/Http/Controllers/WebMentionsController.php +++ b/app/Http/Controllers/WebMentionsController.php @@ -28,7 +28,7 @@ class WebMentionsController extends Controller } //next check the $target is valid - $path = parse_url($request->input('target'))['path']; + $path = parse_url($request->input('target'), PHP_URL_PATH); $pathParts = explode('/', $path); switch ($pathParts[1]) { @@ -36,9 +36,8 @@ class WebMentionsController extends Controller //we have a note $noteId = $pathParts[2]; $numbers = new Numbers(); - $realId = $numbers->b60tonum($noteId); try { - $note = Note::findOrFail($realId); + $note = Note::findOrFail($numbers->b60tonum($noteId)); $this->dispatch(new ProcessWebMention($note, $request->input('source'))); } catch (ModelNotFoundException $e) { return new Response('This note doesn’t exist.', 400); diff --git a/app/Jobs/ProcessWebMention.php b/app/Jobs/ProcessWebMention.php index b2593427..d9d73eb4 100644 --- a/app/Jobs/ProcessWebMention.php +++ b/app/Jobs/ProcessWebMention.php @@ -2,8 +2,8 @@ namespace App\Jobs; +use Mf2; use App\Note; -use Mf2\parse; use HTMLPurifier; use App\WebMention; use GuzzleHttp\Client; @@ -11,6 +11,7 @@ use HTMLPurifier_Config; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Jonnybarnes\WebmentionsParser\Parser; +use GuzzleHttp\Exception\RequestException; use Illuminate\Contracts\Queue\ShouldQueue; class ProcessWebMention extends Job implements ShouldQueue @@ -44,7 +45,11 @@ class ProcessWebMention extends Job implements ShouldQueue $sourceURL = parse_url($this->source); $baseURL = $sourceURL['scheme'] . '://' . $sourceURL['host']; $remoteContent = $this->getRemoteContent($this->source); - $microformats = $this->parseHTML($remoteContent, $baseURL); + if ($remoteContent === null) { + return false; + } + $mf2Parser = new Mf2\Parser($remoteContent, $baseURL, true); + $microformats = $mf2Parser->parse(); $count = WebMention::where('source', '=', $this->source)->count(); if ($count > 0) { //we already have a webmention from this source @@ -140,14 +145,18 @@ class ProcessWebMention extends Job implements ShouldQueue /** * Retreive the remote content from a URL, and caches the result. * - * @param string The URL to retreive content from - * @return string The HTML from the URL + * @param string The URL to retreive content from + * @return string|null The HTML from the URL (or null if error) */ private function getRemoteContent($url) { $client = new Client(); - $response = $client->get($url); + try { + $response = $client->request('GET', $url); + } catch(RequestException $e) { + return; + } $html = (string) $response->getBody(); $path = storage_path() . '/HTML/' . $this->createFilenameFromURL($url); $this->fileForceContents($path, $html); @@ -189,20 +198,6 @@ class ProcessWebMention extends Job implements ShouldQueue file_put_contents("$dir/$name", $contents); } - /** - * A wrapper function for php-mf2’s parse method. - * - * @param string The HTML to parse - * @param string The base URL to resolve relative URLs in the HTML against - * @return array The porcessed microformats - */ - private function parseHTML($html, $baseurl) - { - $microformats = \Mf2\parse((string) $html, $baseurl); - - return $microformats; - } - /** * Save a profile image to the local cache. * From c81eddd12cc7885a267ac60df00f4bc1f9fa16ca Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Fri, 29 Jul 2016 10:00:05 +0100 Subject: [PATCH 3/7] Update .lock --- composer.lock | 71 ++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/composer.lock b/composer.lock index cacb990a..36372915 100644 --- a/composer.lock +++ b/composer.lock @@ -59,16 +59,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.18.31", + "version": "3.18.35", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "dad0b7db5fa8f3c7a3805efb2a1e86a50f11fe8b" + "reference": "8133c4ce336c8cac97218546f896f491bb6ffd7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/dad0b7db5fa8f3c7a3805efb2a1e86a50f11fe8b", - "reference": "dad0b7db5fa8f3c7a3805efb2a1e86a50f11fe8b", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8133c4ce336c8cac97218546f896f491bb6ffd7e", + "reference": "8133c4ce336c8cac97218546f896f491bb6ffd7e", "shasum": "" }, "require": { @@ -135,7 +135,7 @@ "s3", "sdk" ], - "time": "2016-07-19 17:25:45" + "time": "2016-07-29 01:07:59" }, { "name": "barnabywalters/mf-cleaner", @@ -429,27 +429,22 @@ }, { "name": "geo-io/interface", - "version": "v1.0.0", + "version": "v1.0.1", "source": { "type": "git", "url": "https://github.com/geo-io/interface.git", - "reference": "cdbb55801e3f8d5485227c2031cc7a3c16ccd06a" + "reference": "cf46fe7b013de20ab8b601238c7d91b480810644" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/geo-io/interface/zipball/cdbb55801e3f8d5485227c2031cc7a3c16ccd06a", - "reference": "cdbb55801e3f8d5485227c2031cc7a3c16ccd06a", + "url": "https://api.github.com/repos/geo-io/interface/zipball/cf46fe7b013de20ab8b601238c7d91b480810644", + "reference": "cf46fe7b013de20ab8b601238c7d91b480810644", "shasum": "" }, "require": { "php": ">=5.3.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "psr-4": { "GeoIO\\": "src/" @@ -471,7 +466,7 @@ "geometry", "io" ], - "time": "2015-04-17 18:52:52" + "time": "2016-07-28 07:17:02" }, { "name": "geo-io/wkb-parser", @@ -1264,16 +1259,16 @@ }, { "name": "lcobucci/jwt", - "version": "3.1.1", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "afea8e682e911a21574fd8519321b32522fa25b5" + "reference": "9a8db2cd42346f96993928ff6a6c22563f555ab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/afea8e682e911a21574fd8519321b32522fa25b5", - "reference": "afea8e682e911a21574fd8519321b32522fa25b5", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/9a8db2cd42346f96993928ff6a6c22563f555ab1", + "reference": "9a8db2cd42346f96993928ff6a6c22563f555ab1", "shasum": "" }, "require": { @@ -1281,7 +1276,7 @@ "php": ">=5.5" }, "require-dev": { - "mdanter/ecc": "~0.3", + "mdanter/ecc": "~0.3.1", "mikey179/vfsstream": "~1.5", "phpmd/phpmd": "~2.2", "phpunit/php-invoker": "~1.1", @@ -1318,7 +1313,7 @@ "JWS", "jwt" ], - "time": "2016-03-24 22:46:13" + "time": "2016-07-27 11:09:37" }, { "name": "league/commonmark", @@ -3682,16 +3677,16 @@ }, { "name": "filp/whoops", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "d13505b240a6f580bc75ba591da30299d6cb0eec" + "reference": "8828aaa2178e0a19325522e2a45282ff0a14649b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/d13505b240a6f580bc75ba591da30299d6cb0eec", - "reference": "d13505b240a6f580bc75ba591da30299d6cb0eec", + "url": "https://api.github.com/repos/filp/whoops/zipball/8828aaa2178e0a19325522e2a45282ff0a14649b", + "reference": "8828aaa2178e0a19325522e2a45282ff0a14649b", "shasum": "" }, "require": { @@ -3738,7 +3733,7 @@ "whoops", "zf2" ], - "time": "2016-04-07 06:16:25" + "time": "2016-05-06 18:25:35" }, { "name": "fzaninotto/faker", @@ -4211,16 +4206,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "900370c81280cc0d942ffbc5912d80464eaee7e9" + "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/900370c81280cc0d942ffbc5912d80464eaee7e9", - "reference": "900370c81280cc0d942ffbc5912d80464eaee7e9", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5f3f7e736d6319d5f1fc402aff8b026da26709a3", + "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3", "shasum": "" }, "require": { @@ -4229,7 +4224,7 @@ "phpunit/php-text-template": "~1.2", "phpunit/php-token-stream": "^1.4.2", "sebastian/code-unit-reverse-lookup": "~1.0", - "sebastian/environment": "^1.3.2", + "sebastian/environment": "^1.3.2 || ^2.0", "sebastian/version": "~1.0|~2.0" }, "require-dev": { @@ -4270,7 +4265,7 @@ "testing", "xunit" ], - "time": "2016-06-03 05:03:56" + "time": "2016-07-26 14:39:29" }, { "name": "phpunit/php-file-iterator", @@ -4455,16 +4450,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.4.7", + "version": "5.4.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6c8a756c17a1a92a066c99860eb57922e8b723da" + "reference": "3132365e1430c091f208e120b8845d39c25f20e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6c8a756c17a1a92a066c99860eb57922e8b723da", - "reference": "6c8a756c17a1a92a066c99860eb57922e8b723da", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3132365e1430c091f208e120b8845d39c25f20e6", + "reference": "3132365e1430c091f208e120b8845d39c25f20e6", "shasum": "" }, "require": { @@ -4476,7 +4471,7 @@ "myclabs/deep-copy": "~1.3", "php": "^5.6 || ^7.0", "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "^4.0", + "phpunit/php-code-coverage": "^4.0.1", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "^1.0.6", @@ -4529,7 +4524,7 @@ "testing", "xunit" ], - "time": "2016-07-21 06:55:27" + "time": "2016-07-26 14:48:00" }, { "name": "phpunit/phpunit-mock-objects", From bb165479429bc6c01f7fd85cacce23734cdefb74 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Fri, 29 Jul 2016 10:48:05 +0100 Subject: [PATCH 4/7] Add a missing space (PSR-2) --- app/Jobs/ProcessWebMention.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/ProcessWebMention.php b/app/Jobs/ProcessWebMention.php index d9d73eb4..324fee6a 100644 --- a/app/Jobs/ProcessWebMention.php +++ b/app/Jobs/ProcessWebMention.php @@ -154,7 +154,7 @@ class ProcessWebMention extends Job implements ShouldQueue try { $response = $client->request('GET', $url); - } catch(RequestException $e) { + } catch (RequestException $e) { return; } $html = (string) $response->getBody(); From 84c7969a4eb71aad37cbff80f8b47f22f366c0ef Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Fri, 29 Jul 2016 14:22:49 +0100 Subject: [PATCH 5/7] Add an mf2 column to webemtnions, type `jsonb` --- changelog.md | 4 +++ ..._jsonb_mf2_column_to_webmentions_table.php | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 database/migrations/2016_07_29_113150_add_jsonb_mf2_column_to_webmentions_table.php diff --git a/changelog.md b/changelog.md index f6b62544..4b8f3b2f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## Version {next} + - Adding jsonb column to store webmentions’ mf2. + * As of L5.2 this needs a custom command to drop NOT NULL from content, L5.3 should allow a fix for this + ## Version 0.0.8.5 (2016-07-18) - Set the size of the textarea in a form better - Update to latest Guzzle to fix CVE-2016-5385 diff --git a/database/migrations/2016_07_29_113150_add_jsonb_mf2_column_to_webmentions_table.php b/database/migrations/2016_07_29_113150_add_jsonb_mf2_column_to_webmentions_table.php new file mode 100644 index 00000000..04ad6f0a --- /dev/null +++ b/database/migrations/2016_07_29_113150_add_jsonb_mf2_column_to_webmentions_table.php @@ -0,0 +1,33 @@ +jsonb('mf2')->nullable(); + $table->index(['mf2']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('webmentions', function (Blueprint $table) { + $table->dropIndex(['mf2']); + $table->dropColumn('mf2'); + }); + } +} From a9f089098c8e57d2685eb8af32f456d0999bafc7 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Wed, 3 Aug 2016 16:08:30 +0100 Subject: [PATCH 6/7] Work on webmwtion code refactoring --- app/Http/Controllers/NotesController.php | 98 +++++++---- app/Jobs/ProcessWebMention.php | 212 ++++++++--------------- app/Jobs/SaveProfileImage.php | 67 +++++++ composer.lock | 116 ++++++------- resources/views/singlenote.blade.php | 4 +- tests/NotesTest.php | 25 +-- tests/ProcessWebMentionTest.php | 29 ++++ tests/WebMentionsTest.php | 92 ++++++++++ 8 files changed, 395 insertions(+), 248 deletions(-) create mode 100644 app/Jobs/SaveProfileImage.php create mode 100644 tests/ProcessWebMentionTest.php create mode 100644 tests/WebMentionsTest.php diff --git a/app/Http/Controllers/NotesController.php b/app/Http/Controllers/NotesController.php index 1d6125fe..590ff654 100644 --- a/app/Http/Controllers/NotesController.php +++ b/app/Http/Controllers/NotesController.php @@ -7,6 +7,8 @@ use Twitter; use App\Tag; use App\Note; use Jonnybarnes\IndieWeb\Numbers; +use Illuminate\Filesystem\Filesystem; +use Jonnybarnes\WebmentionsParser\Authorship; // Need to sort out Twitter and webmentions! @@ -23,8 +25,8 @@ class NotesController extends Controller foreach ($notes as $note) { $replies = 0; foreach ($note->webmentions as $webmention) { - if ($webmention->type == 'reply') { - $replies = $replies + 1; + if ($webmention->type == 'in-reply-to') { + $replies++; } } $note->replies = $replies; @@ -67,31 +69,51 @@ class NotesController extends Controller public function singleNote($urlId) { $numbers = new Numbers(); + $authorship = new Authorship(); $realId = $numbers->b60tonum($urlId); $note = Note::find($realId); $replies = []; $reposts = []; $likes = []; foreach ($note->webmentions as $webmention) { + /* + reply->url | + reply->photo | Author + reply->name | + reply->source + reply->date + reply->reply + + repost->url | + repost->photo | Author + repost->name | + repost->date + repost->source + + like->url | + like->photo | Author + like->name | + */ + $microformats = json_decode($webmention->mf2); + $authorHCard = $authorship->findAuthor($microformats); + $content['url'] = $authorHCard['properties']['url'][0]; + $content['photo'] = $this->createPhotoLink($authorHCard['properties']['photo'][0]); + $content['name'] = $authorHCard['properties']['name'][0]; switch ($webmention->type) { - case 'reply': - $content = unserialize($webmention->content); - $content['source'] = $this->bridgyReply($webmention->source); - $content['photo'] = $this->createPhotoLink($content['photo']); + case 'in-reply-to': + $content['source'] = $webmention->source; $content['date'] = $carbon->parse($content['date'])->toDayDateTimeString(); + $content['reply'] = $microformats['items'][0]['properties']['content'][0]['html_purified']; $replies[] = $content; break; - case 'repost': - $content = unserialize($webmention->content); - $content['photo'] = $this->createPhotoLink($content['photo']); + case 'repost-of': $content['date'] = $carbon->parse($content['date'])->toDayDateTimeString(); + $content['source'] = $webmention->source; $reposts[] = $content; break; - case 'like': - $content = unserialize($webmention->content); - $content['photo'] = $this->createPhotoLink($content['photo']); + case 'like-of': $likes[] = $content; break; } @@ -164,41 +186,43 @@ class NotesController extends Controller return view('taggednotes', ['notes' => $notes, 'tag' => $tag]); } - /** - * Swap a brid.gy URL shim-ing a twitter reply to a real twitter link. - * - * @param string - * @return string - */ - public function bridgyReply($source) - { - $url = $source; - if (mb_substr($source, 0, 28, 'UTF-8') == 'https://brid-gy.appspot.com/') { - $parts = explode('/', $source); - $tweetId = array_pop($parts); - if ($tweetId) { - $url = 'https://twitter.com/_/status/' . $tweetId; - } - } - - return $url; - } - /** * Create the photo link. * + * We shall leave twitter.com and twimg.com links as they are. Then we shall + * check for local copies, if that fails leave the link as is. + * * @param string * @return string */ public function createPhotoLink($url) { - $host = parse_url($url)['host']; - if ($host != 'twitter.com' && $host != 'pbs.twimg.com') { - return '/assets/profile-images/' . $host . '/image'; - } - if (mb_substr($url, 0, 20) == 'http://pbs.twimg.com') { + $host = parse_url($url, PHP_URL_HOST); + if ($host == 'pbs.twimg.com') { + //make sure we use HTTPS, we know twitter supports it return str_replace('http://', 'https://', $url); } + if ($host == 'twitter.com') { + if (Cache::has($url)) { + return Cache::get($url); + } + $username = parse_url($url, PHP_URL_PATH); + try { + $info = Twitter::getUsers(['screen_name' => $username]); + $profile_image = $info->profile_image_url_https; + Cache::put($url, $profile_image, 10080); //1 week + } catch (Exception $e) { + return $url; //not sure here + } + + return $profile_image; + } + $filesystem = new Filesystem(); + if ($filesystem->exists(public_path() . '/assets/profile-images/' . $host . '/image')) { + return '/assets/profile-images/' . $host . '/image'; + } + + return $url; } /** diff --git a/app/Jobs/ProcessWebMention.php b/app/Jobs/ProcessWebMention.php index 324fee6a..380ffb94 100644 --- a/app/Jobs/ProcessWebMention.php +++ b/app/Jobs/ProcessWebMention.php @@ -13,13 +13,16 @@ use Illuminate\Queue\InteractsWithQueue; use Jonnybarnes\WebmentionsParser\Parser; use GuzzleHttp\Exception\RequestException; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\DispatchesJobs; +use App\Exceptions\RemoteContentNotFoundException; class ProcessWebMention extends Job implements ShouldQueue { - use InteractsWithQueue, SerializesModels; + use InteractsWithQueue, SerializesModels, DispatchesJobs; protected $note; protected $source; + protected $guzzle; /** * Create a new job instance. @@ -28,10 +31,11 @@ class ProcessWebMention extends Job implements ShouldQueue * @param string $source * @return void */ - public function __construct(Note $note, $source) + public function __construct(Note $note, $source, Client $guzzle = null) { $this->note = $note; $this->source = $source; + $this->guzzle = $guzzle ?? new Client(); } /** @@ -46,100 +50,60 @@ class ProcessWebMention extends Job implements ShouldQueue $baseURL = $sourceURL['scheme'] . '://' . $sourceURL['host']; $remoteContent = $this->getRemoteContent($this->source); if ($remoteContent === null) { - return false; + throw new RemoteContentNotFoundException; } - $mf2Parser = new Mf2\Parser($remoteContent, $baseURL, true); - $microformats = $mf2Parser->parse(); - $count = WebMention::where('source', '=', $this->source)->count(); - if ($count > 0) { - //we already have a webmention from this source - $webmentions = WebMention::where('source', '=', $this->source)->get(); - foreach ($webmentions as $webmention) { - //now check it still 'mentions' this target - //we switch for each type of mention (reply/like/repost) - switch ($webmention->type) { - case 'reply': - if ($parser->checkInReplyTo($microformats, $note->longurl) == false) { - //it doesn't so delete - $webmention->delete(); + $microformats = Mf2\parse($remoteContent, $baseURL); + $webmentions = WebMention::where('source', $this->source)->get(); + foreach ($webmentions as $webmention) { + //check webmention still references target + //we try each type of mention (reply/like/repost) + if ($webmention->type == 'in-reply-to') { + if ($parser->checkInReplyTo($microformats, $this->note->longurl) == false) { + //it doesn't so delete + $webmention->delete(); - return true; - } - //webmenion is still a reply, so update content - $content = $parser->replyContent($microformats); - $this->saveImage($content); - $content['reply'] = $this->filterHTML($content['reply']); - $content = serialize($content); - $webmention->content = $content; - $webmention->save(); + return; + } + //webmenion is still a reply, so update content + $microformats = $this->filterHTML($microformats); + $this->dispatch(new SaveProfileImage($microformats)); + $webmention->mf2 = json_encode($microformats); + $webmention->save(); - return true; - break; - case 'like': - if ($parser->checkLikeOf($microformats, $note->longurl) == false) { - //it doesn't so delete - $webmention->delete(); + return; + } + if ($webmention->type == 'like-of') { + if ($parser->checkLikeOf($microformats, $note->longurl) == false) { + //it doesn't so delete + $webmention->delete(); - return true; - } //note we don't need to do anything if it still is a like - break; - case 'repost': - if ($parser->checkRepostOf($microformats, $note->longurl) == false) { - //it doesn't so delete - $webmention->delete(); + return; + } //note we don't need to do anything if it still is a like + } + if ($webmention->type == 'repost-of') { + if ($parser->checkRepostOf($microformats, $note->longurl) == false) { + //it doesn't so delete + $webmention->delete(); + + return; + } //again, we don't need to do anything if it still is a repost + } + }//foreach - return true; - } //again, we don't need to do anything if it still is a repost - break; - }//switch - }//foreach - }//if //no wemention in db so create new one $webmention = new WebMention(); - //check it is in fact a reply - if ($parser->checkInReplyTo($microformats, $note->longurl)) { - $content = $parser->replyContent($microformats); - $this->saveImage($content); - $content['reply'] = $this->filterHTML($content['reply']); - $content = serialize($content); - $webmention->source = $this->source; - $webmention->target = $note->longurl; - $webmention->commentable_id = $this->note->id; - $webmention->commentable_type = 'App\Note'; - $webmention->type = 'reply'; - $webmention->content = $content; - $webmention->save(); + $type = $parser->getMentionType($microformats); //throw error here? + $this->dispatch(new SaveProfileImage($microformats)); + $microformats = $this->filterHTML($microformats); + $webmention->source = $this->source; + $webmention->target = $this->note->longurl; + $webmention->commentable_id = $this->note->id; + $webmention->commentable_type = 'App\Note'; + $webmention->type = $type; + $webmention->mf2 = json_encode($microformats); + $webmention->save(); - return true; - } elseif ($parser->checkLikeOf($microformats, $note->longurl)) { - //it is a like - $content = $parser->likeContent($microformats); - $this->saveImage($content); - $content = serialize($content); - $webmention->source = $this->source; - $webmention->target = $note->longurl; - $webmention->commentable_id = $this->note->id; - $webmention->commentable_type = 'App\Note'; - $webmention->type = 'like'; - $webmention->content = $content; - $webmention->save(); - - return true; - } elseif ($parser->checkRepostOf($microformats, $note->longurl)) { - //it is a repost - $content = $parser->repostContent($microformats); - $this->saveImage($content); - $content = serialize($content); - $webmention->source = $this->source; - $webmention->target = $note->longurl; - $webmention->commentable_id = $this->note->id; - $webmention->commentable_type = 'App\Note'; - $webmention->type = 'repost'; - $webmention->content = $content; - $webmention->save(); - - return true; - } + return; } /** @@ -150,16 +114,20 @@ class ProcessWebMention extends Job implements ShouldQueue */ private function getRemoteContent($url) { - $client = new Client(); - try { - $response = $client->request('GET', $url); + $response = $this->guzzle->request('GET', $url); } catch (RequestException $e) { return; } $html = (string) $response->getBody(); $path = storage_path() . '/HTML/' . $this->createFilenameFromURL($url); - $this->fileForceContents($path, $html); + $parts = explode('/', $path); + $name = array_pop($parts); + $dir = implode('/', $parts); + if (! is_dir($dir)) { + mkdir($dir, 0755, true); + } + file_put_contents("$dir/$name", $html); return $html; } @@ -182,65 +150,29 @@ class ProcessWebMention extends Job implements ShouldQueue } /** - * Save a file, and create any necessary folders. + * Filter the HTML in a reply webmention. * - * @param string The directory to save to - * @param binary The file to save + * @param array The unfiltered microformats + * @return array The filtered microformats */ - private function fileForceContents($dir, $contents) + private function filterHTML($microformats) { - $parts = explode('/', $dir); - $name = array_pop($parts); - $dir = implode('/', $parts); - if (! is_dir($dir)) { - mkdir($dir, 0755, true); + if (isset($microformats['items'][0]['properties']['content'][0]['html'])) { + $microformats['items'][0]['properties']['content'][0]['html_purified'] = $this->useHTMLPurifier( + $microformats['items'][0]['properties']['content'][0]['html'] + ); } - file_put_contents("$dir/$name", $contents); + + return $microformats; } /** - * Save a profile image to the local cache. - * - * @param array source content - * @return bool wether image was saved or not (we don’t save twitter profiles) - */ - public function saveImage(array $content) - { - $photo = $content['photo']; - $home = $content['url']; - //dont save pbs.twimg.com links - if (parse_url($photo)['host'] != 'pbs.twimg.com' - && parse_url($photo)['host'] != 'twitter.com') { - $client = new Client(); - try { - $response = $client->get($photo); - $image = $response->getBody(true); - $path = public_path() . '/assets/profile-images/' . parse_url($home)['host'] . '/image'; - $this->fileForceContents($path, $image); - } catch (Exception $e) { - // we are openning and reading the default image so that - // fileForceContent work - $default = public_path() . '/assets/profile-images/default-image'; - $handle = fopen($default, 'rb'); - $image = fread($handle, filesize($default)); - fclose($handle); - $path = public_path() . '/assets/profile-images/' . parse_url($home)['host'] . '/image'; - $this->fileForceContents($path, $image); - } - - return true; - } - - return false; - } - - /** - * Purify HTML received from a webmention. + * Set up and use HTMLPurifer on some HTML. * * @param string The HTML to be processed * @return string The processed HTML */ - public function filterHTML($html) + private function useHTMLPurifier($html) { $config = HTMLPurifier_Config::createDefault(); $config->set('Cache.SerializerPath', storage_path() . '/HTMLPurifier'); diff --git a/app/Jobs/SaveProfileImage.php b/app/Jobs/SaveProfileImage.php new file mode 100644 index 00000000..5600fe21 --- /dev/null +++ b/app/Jobs/SaveProfileImage.php @@ -0,0 +1,67 @@ +microformats = $microformats; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle(Authorship $authorship) + { + try { + $author = $authorship->findAuthor($microformats); + } catch (AuthorshipParserException $e) { + return; + } + $photo = $author['properties'][0]['photo'][0]; + $home = $author['properties'][0]['url'][0]; + //dont save pbs.twimg.com links + if (parse_url($photo, PHP_URL_HOST) != 'pbs.twimg.com' + && parse_url($photo, PHP_URL_HOST) != 'twitter.com') { + $client = new Client(); + try { + $response = $client->get($photo); + $image = $response->getBody(true); + } catch (RequestException $e) { + // we are openning and reading the default image so that + $default = public_path() . '/assets/profile-images/default-image'; + $handle = fopen($default, 'rb'); + $image = fread($handle, filesize($default)); + fclose($handle); + } + $path = public_path() . '/assets/profile-images/' . parse_url($home, PHP_URL_HOST) . '/image'; + $parts = explode('/', $path); + $name = array_pop($parts); + $dir = implode('/', $parts); + if (! is_dir($dir)) { + mkdir($dir, 0755, true); + } + file_put_contents("$dir/$name", $image); + } + } +} diff --git a/composer.lock b/composer.lock index 36372915..92da6d7a 100644 --- a/composer.lock +++ b/composer.lock @@ -1672,16 +1672,16 @@ }, { "name": "monolog/monolog", - "version": "1.20.0", + "version": "1.21.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037" + "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/55841909e2bcde01b5318c35f2b74f8ecc86e037", - "reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f42fbdfd53e306bda545845e4dbfd3e72edb4952", + "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952", "shasum": "" }, "require": { @@ -1746,7 +1746,7 @@ "logging", "psr-3" ], - "time": "2016-07-02 14:02:10" + "time": "2016-07-29 03:23:52" }, { "name": "mtdowling/cron-expression", @@ -2652,16 +2652,16 @@ }, { "name": "symfony/console", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a7abb7153f6d1da47f87ec50274844e246b09d9f" + "reference": "926061e74229e935d3c5b4e9ba87237316c6693f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a7abb7153f6d1da47f87ec50274844e246b09d9f", - "reference": "a7abb7153f6d1da47f87ec50274844e246b09d9f", + "url": "https://api.github.com/repos/symfony/console/zipball/926061e74229e935d3c5b4e9ba87237316c6693f", + "reference": "926061e74229e935d3c5b4e9ba87237316c6693f", "shasum": "" }, "require": { @@ -2708,20 +2708,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-06-29 07:02:21" + "time": "2016-07-30 07:22:48" }, { "name": "symfony/debug", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "c54bc3539c3b87e86799533801e8ae0e971d78c2" + "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/c54bc3539c3b87e86799533801e8ae0e971d78c2", - "reference": "c54bc3539c3b87e86799533801e8ae0e971d78c2", + "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a", + "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a", "shasum": "" }, "require": { @@ -2765,20 +2765,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:40:00" + "time": "2016-07-30 07:22:48" }, { "name": "symfony/event-dispatcher", - "version": "v3.1.2", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "7f9839ede2070f53e7e2f0849b9bd14748c434c5" + "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7f9839ede2070f53e7e2f0849b9bd14748c434c5", - "reference": "7f9839ede2070f53e7e2f0849b9bd14748c434c5", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c0c00c80b3a69132c4e55c3e7db32b4a387615e5", + "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5", "shasum": "" }, "require": { @@ -2825,11 +2825,11 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:41:56" + "time": "2016-07-19 10:45:57" }, { "name": "symfony/finder", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -2878,16 +2878,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "1341139f906d295baa4f4abd55293d07e25a065a" + "reference": "49ba00f8ede742169cb6b70abe33243f4d673f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1341139f906d295baa4f4abd55293d07e25a065a", - "reference": "1341139f906d295baa4f4abd55293d07e25a065a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/49ba00f8ede742169cb6b70abe33243f4d673f82", + "reference": "49ba00f8ede742169cb6b70abe33243f4d673f82", "shasum": "" }, "require": { @@ -2927,20 +2927,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-06-29 07:02:21" + "time": "2016-07-17 13:54:30" }, { "name": "symfony/http-kernel", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "177b63b2d50b63fa6d82ea41359ed9928cc7a1fb" + "reference": "d97ba4425e36e79c794e7d14ff36f00f081b37b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/177b63b2d50b63fa6d82ea41359ed9928cc7a1fb", - "reference": "177b63b2d50b63fa6d82ea41359ed9928cc7a1fb", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/d97ba4425e36e79c794e7d14ff36f00f081b37b3", + "reference": "d97ba4425e36e79c794e7d14ff36f00f081b37b3", "shasum": "" }, "require": { @@ -3009,7 +3009,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2016-06-30 16:30:17" + "time": "2016-07-30 09:10:37" }, { "name": "symfony/polyfill-mbstring", @@ -3180,16 +3180,16 @@ }, { "name": "symfony/process", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d7cde1f9d94d87060204f863779389b61c382eeb" + "reference": "768debc5996f599c4372b322d9061dba2a4bf505" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d7cde1f9d94d87060204f863779389b61c382eeb", - "reference": "d7cde1f9d94d87060204f863779389b61c382eeb", + "url": "https://api.github.com/repos/symfony/process/zipball/768debc5996f599c4372b322d9061dba2a4bf505", + "reference": "768debc5996f599c4372b322d9061dba2a4bf505", "shasum": "" }, "require": { @@ -3225,11 +3225,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:40:00" + "time": "2016-07-28 11:13:34" }, { "name": "symfony/routing", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -3304,16 +3304,16 @@ }, { "name": "symfony/translation", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "6bf844e1ee3c820c012386c10427a5c67bbefec8" + "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/6bf844e1ee3c820c012386c10427a5c67bbefec8", - "reference": "6bf844e1ee3c820c012386c10427a5c67bbefec8", + "url": "https://api.github.com/repos/symfony/translation/zipball/eee6c664853fd0576f21ae25725cfffeafe83f26", + "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26", "shasum": "" }, "require": { @@ -3364,20 +3364,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:40:00" + "time": "2016-07-30 07:22:48" }, { "name": "symfony/var-dumper", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "2f046e9a9d571f22cc8b26783564876713b06579" + "reference": "1f7e071aafc6676fcb6e3f0497f87c2397247377" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2f046e9a9d571f22cc8b26783564876713b06579", - "reference": "2f046e9a9d571f22cc8b26783564876713b06579", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/1f7e071aafc6676fcb6e3f0497f87c2397247377", + "reference": "1f7e071aafc6676fcb6e3f0497f87c2397247377", "shasum": "" }, "require": { @@ -3427,7 +3427,7 @@ "debug", "dump" ], - "time": "2016-06-29 05:40:00" + "time": "2016-07-26 08:03:56" }, { "name": "themattharris/tmhoauth", @@ -5100,7 +5100,7 @@ }, { "name": "symfony/css-selector", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -5153,16 +5153,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "62769e3409006b937bb333b29da8df9a8b262975" + "reference": "dff8fecf1f56990d88058e3a1885c2a5f1b8e970" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/62769e3409006b937bb333b29da8df9a8b262975", - "reference": "62769e3409006b937bb333b29da8df9a8b262975", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/dff8fecf1f56990d88058e3a1885c2a5f1b8e970", + "reference": "dff8fecf1f56990d88058e3a1885c2a5f1b8e970", "shasum": "" }, "require": { @@ -5205,20 +5205,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:40:00" + "time": "2016-07-30 07:22:48" }, { "name": "symfony/yaml", - "version": "v3.1.2", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "2884c26ce4c1d61aebf423a8b912950fe7c764de" + "reference": "1819adf2066880c7967df7180f4f662b6f0567ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/2884c26ce4c1d61aebf423a8b912950fe7c764de", - "reference": "2884c26ce4c1d61aebf423a8b912950fe7c764de", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1819adf2066880c7967df7180f4f662b6f0567ac", + "reference": "1819adf2066880c7967df7180f4f662b6f0567ac", "shasum": "" }, "require": { @@ -5254,7 +5254,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:41:56" + "time": "2016-07-17 14:02:08" }, { "name": "webmozart/assert", diff --git a/resources/views/singlenote.blade.php b/resources/views/singlenote.blade.php index d564616e..9c5f6525 100644 --- a/resources/views/singlenote.blade.php +++ b/resources/views/singlenote.blade.php @@ -20,13 +20,13 @@ @if(count($likes) > 0)

Likes

@endif @foreach($likes as $like) - + @endforeach @if(count($reposts) > 0)

Reposts

@endif @foreach($reposts as $repost)

{{ $repost['name'] }} - reposted this at {{ $repost['date'] }}.

+ reposted this at {{ $repost['date'] }}.

@endforeach @stop diff --git a/tests/NotesTest.php b/tests/NotesTest.php index 233704e3..c1ac954d 100644 --- a/tests/NotesTest.php +++ b/tests/NotesTest.php @@ -2,6 +2,7 @@ namespace App\Tests; +use Cache; use TestCase; use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Foundation\Testing\DatabaseMigrations; @@ -131,15 +132,15 @@ class NotesTest extends TestCase } /** - * Test the bridgy url shim method. + * Test a correct profile link is formed from a generic URL. * * @return void */ - public function testBridgy() + public function testCreatePhotoLinkWithNonCachedImage() { - $url = 'https://brid-gy.appspot.com/comment/twitter/jonnybarnes/497778866816299008/497781260937203712'; - $expected = 'https://twitter.com/_/status/497781260937203712'; - $this->assertEquals($expected, $this->notesController->bridgyReply($url)); + $homepage = 'https://example.org/profile.png'; + $expected = 'https://example.org/profile.png'; + $this->assertEquals($expected, $this->notesController->createPhotoLink($homepage)); } /** @@ -147,10 +148,10 @@ class NotesTest extends TestCase * * @return void */ - public function testCreatePhotoLinkWithGenericURL() + public function testCreatePhotoLinkWithCachedImage() { - $homepage = 'https://example.org'; - $expected = '/assets/profile-images/example.org/image'; + $homepage = 'https://aaronparecki.com/profile.png'; + $expected = '/assets/profile-images/aaronparecki.com/image'; $this->assertEquals($expected, $this->notesController->createPhotoLink($homepage)); } @@ -159,7 +160,7 @@ class NotesTest extends TestCase * * @return void */ - public function testCreatePhotoLinkWithTwitterProfileImageURL() + public function testCreatePhotoLinkWithTwimgProfileImageURL() { $twitterProfileImage = 'http://pbs.twimg.com/1234'; $expected = 'https://pbs.twimg.com/1234'; @@ -171,9 +172,11 @@ class NotesTest extends TestCase * * @return void */ - public function testCreatePhotoLinkWithTwitterURL() + public function testCreatePhotoLinkWithCachedTwitterURL() { $twitterURL = 'https://twitter.com/example'; - $this->assertNull($this->notesController->createPhotoLink($twitterURL)); + $expected = 'https://pbs.twimg.com/static_profile_link.jpg'; + Cache::put($twitterURL, $expected, 1); + $this->assertEquals($expected, $this->notesController->createPhotoLink($twitterURL)); } } diff --git a/tests/ProcessWebMentionTest.php b/tests/ProcessWebMentionTest.php new file mode 100644 index 00000000..77e04ba0 --- /dev/null +++ b/tests/ProcessWebMentionTest.php @@ -0,0 +1,29 @@ +appurl = config('app.url'); + } + + /** + * A basic test. + * + * @return void + */ + public function testExample() + { + + } +} diff --git a/tests/WebMentionsTest.php b/tests/WebMentionsTest.php new file mode 100644 index 00000000..a0bf502c --- /dev/null +++ b/tests/WebMentionsTest.php @@ -0,0 +1,92 @@ +appurl = config('app.url'); + } + + /** + * Test webmentions without source and target are rejected. + * + * @return void + */ + public function testWebmentionsWithoutSourceAndTargetAreRejected() + { + $this->call('POST', $this->appurl . '/webmention', ['source' => 'https://example.org/post/123']); + $this->assertResponseStatus(400) + ->see('You need both the target and source parameters'); + } + + /** + * Test invalid target gets a 400 response. + * + * @return void + */ + public function testInvalidTargetReturns400Response() + { + $this->call('POST', $this->appurl . '/webmention', [ + 'source' => 'https://example.org/post/123', + 'target' => $this->appurl . '/invalid/target' + ]); + $this->assertResponseStatus(400) + ->see('Invalid request'); + } + + /** + * Test blog target gets a 501 response. + * + * @return void + */ + public function testBlogpostTargetReturns501Response() + { + $this->call('POST', $this->appurl . '/webmention', [ + 'source' => 'https://example.org/post/123', + 'target' => $this->appurl . '/blog/target' + ]); + $this->assertResponseStatus(501) + ->see('I don’t accept webmentions for blog posts yet.'); + } + + /** + * Test that a non-existant note gives a 400 response. + * + * @return void + */ + public function testNonexistantNoteReturns400Response() + { + $this->call('POST', $this->appurl . '/webmention', [ + 'source' => 'https://example.org/post/123', + 'target' => $this->appurl . '/notes/ZZZZZ' + ]); + $this->assertResponseStatus(400) + ->see('This note doesn’t exist.'); + } + + /** + * Test a legit webmention triggers the ProcessWebMention job. + * + * @return void + */ + public function testLegitimateWebmnetionTriggersProcessWebMentionJob() + { + $this->expectsJobs(\App\Jobs\ProcessWebMention::class); + $this->call('POST', $this->appurl . '/webmention', [ + 'source' => 'https://example.org/post/123', + 'target' => $this->appurl . '/notes/B' + ]); + $this->assertResponseStatus(202) + ->see('Webmention received, it will be processed shortly'); + } +} From 94ec2ec25404eb632b9afab7a6d2c66cd694cbe0 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Mon, 5 Sep 2016 23:39:54 +0100 Subject: [PATCH 7/7] Should now be able to send webmentions to webmention.rocks --- app/Jobs/SendWebMentions.php | 40 +++++++++++++++++++++++++----------- changelog.md | 2 ++ composer.lock | 15 +++++++------- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/app/Jobs/SendWebMentions.php b/app/Jobs/SendWebMentions.php index 4cfbaafc..4caf6c7d 100644 --- a/app/Jobs/SendWebMentions.php +++ b/app/Jobs/SendWebMentions.php @@ -20,9 +20,10 @@ class SendWebMentions extends Job implements ShouldQueue * @param Note $note * @return void */ - public function __construct(Note $note) + public function __construct(Note $note, Client $guzzle = null) { $this->note = $note; + $this->guzzle = $guzzle ?? new Client(); } /** @@ -31,16 +32,16 @@ class SendWebMentions extends Job implements ShouldQueue * @param \GuzzleHttp\Client $guzzle * @return void */ - public function handle(Client $guzzle) + public function handle() { //grab the URLs $urlsInReplyTo = explode(' ', $this->note->in_reply_to); $urlsNote = $this->getLinks($this->note->note); $urls = array_filter(array_merge($urlsInReplyTo, $urlsNote)); //filter out none URLs foreach ($urls as $url) { - $endpoint = $this->discoverWebmentionEndpoint($url, $guzzle); + $endpoint = $this->discoverWebmentionEndpoint($url, $this->guzzle); if ($endpoint) { - $guzzle->post($endpoint, [ + $this->guzzle->post($endpoint, [ 'form_params' => [ 'source' => $this->note->longurl, 'target' => $url, @@ -73,8 +74,8 @@ class SendWebMentions extends Job implements ShouldQueue //check HTTP Headers for webmention endpoint $links = \GuzzleHttp\Psr7\parse_header($response->getHeader('Link')); foreach ($links as $link) { - if ($link['rel'] == 'webmention') { - return trim($link[0], '<>'); + if (mb_stristr($link['rel'], 'webmention')) { + return $this->resolveUri($link[0], $url); } } @@ -89,11 +90,7 @@ class SendWebMentions extends Job implements ShouldQueue $endpoint = $rels[0]['http://webmention.org/'][0]; } if ($endpoint) { - if (filter_var($endpoint, FILTER_VALIDATE_URL)) { - return $endpoint; - } - //it must be a relative url, so resolve with php-mf2 - return $mf2->resolveUrl($endpoint); + return $this->resolveUri($endpoint, $url); } return false; @@ -105,7 +102,7 @@ class SendWebMentions extends Job implements ShouldQueue * @param string $html * @return array $urls */ - private function getLinks($html) + public function getLinks($html) { $urls = []; $dom = new \DOMDocument(); @@ -117,4 +114,23 @@ class SendWebMentions extends Job implements ShouldQueue return $urls; } + + /** + * Resolve a URI if necessary. + * + * @param string $url + * @param string $base + * @return string + */ + public function resolveUri(string $url, string $base): string + { + $endpoint = \GuzzleHttp\Psr7\uri_for($url); + if ($endpoint->getScheme() !== null) { + return (string) $endpoint; + } + return (string) \GuzzleHttp\Psr7\Uri::resolve( + \GuzzleHttp\Psr7\uri_for($base), + $endpoint + ); + } } diff --git a/changelog.md b/changelog.md index 4b8f3b2f..788e181e 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,8 @@ ## Version {next} - Adding jsonb column to store webmentions’ mf2. * As of L5.2 this needs a custom command to drop NOT NULL from content, L5.3 should allow a fix for this + - Refactor receiving webmention code + - Refactor sending webmention code to pass webmention.rocks ## Version 0.0.8.5 (2016-07-18) - Set the size of the textarea in a form better diff --git a/composer.lock b/composer.lock index 92da6d7a..f7578a1f 100644 --- a/composer.lock +++ b/composer.lock @@ -182,12 +182,12 @@ "version": "0.16", "source": { "type": "git", - "url": "https://github.com/Bosnadev/Database.git", + "url": "https://github.com/bosnadev/database.git", "reference": "c2748d118415d30ce69b792448689285d01ffdb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bosnadev/Database/zipball/c2748d118415d30ce69b792448689285d01ffdb9", + "url": "https://api.github.com/repos/bosnadev/database/zipball/c2748d118415d30ce69b792448689285d01ffdb9", "reference": "c2748d118415d30ce69b792448689285d01ffdb9", "shasum": "" }, @@ -2154,16 +2154,16 @@ }, { "name": "psr/http-message", - "version": "1.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", - "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", "shasum": "" }, "require": { @@ -2191,6 +2191,7 @@ } ], "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", "keywords": [ "http", "http-message", @@ -2199,7 +2200,7 @@ "request", "response" ], - "time": "2015-05-04 20:22:00" + "time": "2016-08-06 14:39:51" }, { "name": "psr/log",