diff --git a/app/Checkers/VisualDiff.php b/app/Checkers/VisualDiff.php new file mode 100644 index 0000000..4953e9e --- /dev/null +++ b/app/Checkers/VisualDiff.php @@ -0,0 +1,128 @@ +website = $website; + $this->url = $url; + } + + public function run() + { + $this->fetch(); + $this->compare(); + $this->notify(); + } + + private function fetch() + { + $filename = (string) Uuid::uuid4() . '.png'; + +// $this->scan = new Model([ +// 'url' => $this->url, +//// 'screenshot' => $filename, +// 'screenshot' => '80df5ce1-0edf-46e3-a222-090069cb6a48.png', +// ]); + + $this->scan = $this->website->visualDiffs() + ->latest() + ->where('url', $this->url) + ->first(); + + try { +// Browsershot::url($this->url) +// ->windowSize(1440, 1024) +// ->fullPage() +// ->waitUntilNetworkIdle() +// ->setDelay(1000) +// ->save( +// Storage::disk('screenshots')->path($filename) +// ); + +// $this->website->visualDiffs()->save($this->scan); + } catch (Exception $exception) { + if (app()->environment('dev')) { + throw $exception; + } + } + } + + private function compare() + { + $lastScan = $this->website->visualDiffs() + ->latest() + ->where('id', '!=', $this->scan->id) + ->where('url', $this->url) + ->first(); + + if (!$lastScan) { + return; + } + + if ($lastScan->image->getHeight() > $this->scan->image->getHeight()) { + $this->scan->image->resizeCanvas(null, $lastScan->image->getHeight(), 'top-left')->save(); + } else { + $lastScan->image->resizeCanvas(null, $this->scan->image->getHeight(), 'top-left')->save(); + } + + $differ = \BeyondCode\VisualDiff\VisualDiff::diff( + $lastScan->full_screenshot_path, + $this->scan->full_screenshot_path + ); + + $this->scan->diff_path = 'diff-' . $this->scan->id . '-' . $lastScan->id . '.png'; + $this->scan->compared_with = $lastScan->id; + + try { + $diff = $differ->save( + Storage::disk('screenshots')->path($this->scan->diff_path) + ); + + $this->scan->diff_found = data_get($diff, 'pixels', 0) > 10; + } catch (Exception $exception) { + if (app()->environment('dev')) { + throw $exception; + } + } + + $this->scan->save(); + } + + private function notify() + { + if (!$this->scan) { + return null; + } + + if (empty($this->scan->diff_found)) { + return null; + } + + $this->website->user->notify( + new VisualDifferenceFound($this->website, $this->scan) + ); + } +} diff --git a/app/Console/Commands/VisualDiffCommand.php b/app/Console/Commands/VisualDiffCommand.php new file mode 100644 index 0000000..64bcaff --- /dev/null +++ b/app/Console/Commands/VisualDiffCommand.php @@ -0,0 +1,52 @@ +option('force', false); + $websiteId = $this->argument('website'); + $website = Website::findOrFail($websiteId); + + if (!$website->visual_diff_enabled && !$forced) { + return $this->error('Visual diffs disabled for ' . $website->url . ', use --force to force a check.'); + } + + $website->visual_urls_to_scan->each(function ($url) use ($website) { + VisualDiffCheck::dispatchNow($website, $url); + }); + } +} diff --git a/app/HasVisualDiffs.php b/app/HasVisualDiffs.php new file mode 100644 index 0000000..742149d --- /dev/null +++ b/app/HasVisualDiffs.php @@ -0,0 +1,26 @@ +hasMany(VisualDiff::class); + } + + public function getLastVisualDiffsAttribute() + { + return $this->visualDiffs()->orderBy('created_at', 'desc')->take(2)->get(); + } + + public function getVisualUrlsToScanAttribute() + { + return collect(explode("\n", $this->visual_diff_urls))->map(function ($url) { + return trim($url); + })->filter(function ($url) { + return filter_var($url, FILTER_VALIDATE_URL); + })->values(); + } + +} diff --git a/app/Jobs/VisualDiffCheck.php b/app/Jobs/VisualDiffCheck.php new file mode 100644 index 0000000..cc42269 --- /dev/null +++ b/app/Jobs/VisualDiffCheck.php @@ -0,0 +1,55 @@ +website = $website; + $this->url = $url; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $checker = new VisualDiff($this->website, $this->url); + $checker->run(); + } + + public function tags() + { + return [ + static::class, + 'Website:' . $this->website->id, + ]; + } +} diff --git a/app/Notifications/VisualDifferenceFound.php b/app/Notifications/VisualDifferenceFound.php new file mode 100644 index 0000000..95d2802 --- /dev/null +++ b/app/Notifications/VisualDifferenceFound.php @@ -0,0 +1,77 @@ +website = $website; + $this->scan = $scan; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database', 'mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->subject('🌄 Visual Difference on: ' . $this->website->url) + ->markdown('mail.visual-diff', [ + 'website' => $this->website, + 'scan' => $this->scan, + ]); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + 'website' => $this->website, + 'scan' => $this->scan, + ]; + } +} diff --git a/app/VisualDiff.php b/app/VisualDiff.php new file mode 100644 index 0000000..9cea5a3 --- /dev/null +++ b/app/VisualDiff.php @@ -0,0 +1,76 @@ + 'boolean', + ]; + + protected $onceListeners = []; + + public function __get($key) + { + if (Str::startsWith($key, '___once_listener__')) { + + return $this->onceListeners[$key]; + } + + return parent::__get($key); + } + + public function __set($key, $value) + { + if (Str::startsWith($key, '___once_listener__')) { + + $this->onceListeners[$key] = $value; + + return; + } + + parent::__set($key, $value); + } + + public function comparedWith() + { + return $this->belongsTo(VisualDiff::class, 'compared_with'); + } + + public function getFullScreenshotPathAttribute() + { + return Storage::disk('screenshots')->path($this->screenshot); + } + + public function getScreenshotUrlAttribute() + { + return Storage::disk('screenshots')->url($this->screenshot); + } + + public function getDiffUrlAttribute() + { + return Storage::disk('screenshots')->url($this->diff_path); + } + + public function getImageAttribute() + { + return once(function () { + return Image::make( + $this->full_screenshot_path + ); + }); + } +} diff --git a/app/Website.php b/app/Website.php index 93a86ad..8df3bfb 100644 --- a/app/Website.php +++ b/app/Website.php @@ -21,6 +21,7 @@ class Website extends Model use HasOpenGraph; use HasCertificates; use HasCrawledPages; + use HasVisualDiffs; protected $fillable = [ 'url', @@ -33,6 +34,8 @@ class Website extends Model 'cron_enabled', 'crawler_enabled', 'cron_key', + 'visual_diff_urls', + 'visual_diff_enabled', ]; protected static function boot() diff --git a/composer.json b/composer.json index a0e011b..7f939a1 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,10 @@ "morrislaptop/laravel-queue-clear": "^1.2", "owenmelbz/domain-enforcement": "^0.0.7", "predis/predis": "^1.1", + "qortex/laravel-7-visual-diff": "^2.0", "sebastian/diff": "^3.0", "spatie/crawler": "^4.6", + "spatie/once": "^2.2", "spatie/ssl-certificate": "^1.15", "visualappeal/php-ssllabs-api": "^1.0", "whoisdoma/dnsparser": "dev-master", diff --git a/composer.lock b/composer.lock index e416b5c..4005db3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "09c056a8ad73a9f64a51990a47725390", + "content-hash": "ffc3de4ee3b334c6540f3e9557e47a36", "packages": [ { "name": "asm89/stack-cors", @@ -2794,6 +2794,71 @@ ], "time": "2020-05-03T19:32:03+00:00" }, + { + "name": "qortex/laravel-7-visual-diff", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/QortexDevs/laravel-visual-diff.git", + "reference": "866cd9180aa7fc66e70adfe170498ac289d4b440" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/QortexDevs/laravel-visual-diff/zipball/866cd9180aa7fc66e70adfe170498ac289d4b440", + "reference": "866cd9180aa7fc66e70adfe170498ac289d4b440", + "shasum": "" + }, + "require": { + "illuminate/support": "^7", + "php": "^7.1", + "spatie/browsershot": "^3.37", + "symfony/process": "^5.1" + }, + "require-dev": { + "orchestra/testbench-dusk": "3.6.x@dev", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "BeyondCode\\VisualDiff\\VisualDiffServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "BeyondCode\\VisualDiff\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcel Pociot", + "email": "marcel@beyondco.de", + "homepage": "https://beyondcode.de", + "role": "Developer" + }, + { + "name": "Nick Mitin", + "email": "nick.mitin@qortex.ru", + "homepage": "https://qortex.ru", + "role": "Developer" + } + ], + "description": "Marcel Pociot's based beyondcode/laravel-visual-diff adapted for Laravel 7", + "homepage": "https://github.com/qortex/laravel-visual-diff", + "keywords": [ + "beyondcode", + "laravel-visual-diff", + "qortex", + "visual regression" + ], + "time": "2020-08-08T04:25:46+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -3294,6 +3359,59 @@ ], "time": "2017-09-18T09:51:20+00:00" }, + { + "name": "spatie/once", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/once.git", + "reference": "6d4a379546684043c225b9a96d8ced7b8a84b889" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/once/zipball/6d4a379546684043c225b9a96d8ced7b8a84b889", + "reference": "6d4a379546684043c225b9a96d8ced7b8a84b889", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "larapack/dd": "^1.1", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Once\\": "src" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A magic memoization function", + "homepage": "https://github.com/spatie/once", + "keywords": [ + "cache", + "callable", + "memozation", + "once", + "spatie" + ], + "time": "2020-02-18T15:10:38+00:00" + }, { "name": "spatie/robots-txt", "version": "1.0.7", @@ -6983,6 +7101,7 @@ "keywords": [ "tokenizer" ], + "abandoned": true, "time": "2019-09-17T06:23:10+00:00" }, { diff --git a/config/filesystems.php b/config/filesystems.php index 77fa5de..9a0f4a7 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -55,6 +55,13 @@ 'visibility' => 'public', ], + 'screenshots' => [ + 'driver' => 'local', + 'root' => storage_path('app/public/screenshots'), + 'url' => env('APP_URL').'/storage/screenshots', + 'visibility' => 'public', + ], + 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), diff --git a/database/migrations/2020_10_06_130938_adding_visual_diff_options.php b/database/migrations/2020_10_06_130938_adding_visual_diff_options.php new file mode 100644 index 0000000..3512db7 --- /dev/null +++ b/database/migrations/2020_10_06_130938_adding_visual_diff_options.php @@ -0,0 +1,36 @@ +boolean('visual_diff_enabled')->default(0)->after('crawler_enabled'); + $table->boolean('in_queue_visual_diff')->default(0)->after('in_queue_uptime'); + $table->longText('visual_diff_urls')->nullable()->after('crawler_enabled'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('websites', function (Blueprint $table) { + $table->dropColumn('visual_diff_enabled'); + $table->dropColumn('visual_diff_urls'); +// $table->dropColumn('in_queue_visual_diff'); + }); + } +} diff --git a/database/migrations/2020_10_06_132924_create_visual_diffs_table.php b/database/migrations/2020_10_06_132924_create_visual_diffs_table.php new file mode 100644 index 0000000..02bd28f --- /dev/null +++ b/database/migrations/2020_10_06_132924_create_visual_diffs_table.php @@ -0,0 +1,37 @@ +id(); + $table->unsignedBigInteger('website_id'); + $table->string('url'); + $table->string('screenshot'); + $table->boolean('diff_found')->default(0); + $table->string('diff_path')->nullable(); + $table->unsignedBigInteger('compared_with')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('visual_diffs'); + } +} diff --git a/package-lock.json b/package-lock.json index f76c3c0..f90e0d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1834,8 +1834,7 @@ "@types/node": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.2.tgz", - "integrity": "sha512-iQgg5AfQVQ766QGtK90g3EctbIe5Xwf1xMafnQB3WUr5hkrT5CUMbzMGtxSsICNWSgExILgQ+8kCfX2p0OKWGg==", - "dev": true + "integrity": "sha512-iQgg5AfQVQ766QGtK90g3EctbIe5Xwf1xMafnQB3WUr5hkrT5CUMbzMGtxSsICNWSgExILgQ+8kCfX2p0OKWGg==" }, "@types/parse-json": { "version": "4.0.0", @@ -1870,6 +1869,15 @@ "@types/react": "*" } }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@vue/component-compiler-utils": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.1.2.tgz", @@ -2296,6 +2304,11 @@ "object-assign": "4.x" } }, + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" + }, "agentkeepalive": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.2.0.tgz", @@ -2993,8 +3006,7 @@ "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "batch": { "version": "0.6.1", @@ -3024,6 +3036,27 @@ "file-uri-to-path": "1.0.0" } }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + } + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -3259,6 +3292,11 @@ } } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -3488,8 +3526,7 @@ "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "chrome-trace-event": { "version": "1.0.2", @@ -4568,6 +4605,11 @@ "minimist": "^1.1.1" } }, + "devtools-protocol": { + "version": "0.0.799653", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.799653.tgz", + "integrity": "sha512-t1CcaZbvm8pOlikqrsIM9GOa7Ipp07+4h/q9u0JXBWjPCjHdBl9KkddX87Vv9vBHoBGtwV79sYQNGnQM6iS5gg==" + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4858,7 +4900,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -5357,6 +5398,40 @@ } } }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "requires": { + "ms": "2.1.2" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", @@ -5419,6 +5494,14 @@ } } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -5771,6 +5854,11 @@ } } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -6366,6 +6454,30 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "requires": { + "agent-base": "5", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "humps": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", @@ -6417,8 +6529,7 @@ "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "iferr": { "version": "0.1.5", @@ -7875,6 +7986,11 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "moment": { "version": "2.25.3", "resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz", @@ -8621,6 +8737,11 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -8654,6 +8775,14 @@ "pinkie": "^2.0.0" } }, + "pixelmatch": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz", + "integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==", + "requires": { + "pngjs": "^4.0.1" + } + }, "pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -8729,6 +8858,11 @@ "insert-css": "^2.0.0" } }, + "pngjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz", + "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==" + }, "portfinder": { "version": "1.0.26", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", @@ -9570,6 +9704,11 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -9740,6 +9879,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -9778,7 +9922,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -9813,6 +9956,103 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "puppeteer": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.3.1.tgz", + "integrity": "sha512-YTM1RaBeYrj6n7IlRXRYLqJHF+GM7tasbvrNFx6w1S16G76NrPq7oYFKLDO+BQsXNtS8kW2GxWCXjIMPvfDyaQ==", + "requires": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.799653", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^4.0.0", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.0.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "requires": { + "ms": "2.1.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" + } + } + }, "purgecss": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-2.2.1.tgz", @@ -10575,7 +10815,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -10966,8 +11205,7 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex": { "version": "1.1.0", @@ -11898,7 +12136,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "requires": { "safe-buffer": "~5.2.0" } @@ -12109,6 +12346,29 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, + "tar-fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", + "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", + "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "terser": { "version": "3.17.0", "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", @@ -12190,8 +12450,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.5", @@ -12403,6 +12662,26 @@ } } }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + }, + "dependencies": { + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + } + } + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -12632,8 +12911,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.1", @@ -13428,6 +13706,15 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/package.json b/package.json index d681fa7..374a635 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "antd": "^3.26.17", "aos": "^2.3.4", "chart.js": "^2.9.3", + "pixelmatch": "^5.2.1", + "puppeteer": "^5.3.1", "react": "16.8.6", "react-chartjs-2": "^2.9.0", "react-diff-viewer": "^2.0.6" diff --git a/resources/views/mail/visual-diff.blade.php b/resources/views/mail/visual-diff.blade.php new file mode 100644 index 0000000..8393550 --- /dev/null +++ b/resources/views/mail/visual-diff.blade.php @@ -0,0 +1,17 @@ +@component('mail::message') +# A difference has been found on: + +{{ $scan->url }} at {{ $scan->created_at->format('d/m/Y H:i:s') }} + +## Before +Screenshot from the previous scan. + +## After +Screenshot from the last scan. + +## Differences +Heat map of differences. + +Thanks,
+{{ config('app.name') }} +@endcomponent diff --git a/resources/views/websites-form.blade.php b/resources/views/websites-form.blade.php index f85950e..79b596a 100644 --- a/resources/views/websites-form.blade.php +++ b/resources/views/websites-form.blade.php @@ -55,6 +55,14 @@ 'label' => 'Enable Crawler?' ]) + +
+ @include('maelstrom::inputs.switch', [ + 'name' => 'visual_diff_enabled', + 'label' => 'Enable Visual Diff?', + 'hide_off' => ['visual_diff_urls'], + ]) +
@@ -100,3 +108,10 @@

Once you've "Saved" this website, we'll provide you with your ping endpoints.

@endif + +@include('maelstrom::inputs.text', [ + 'html_type' => 'textarea', + 'name' => 'visual_diff_urls', + 'label' => 'URLs to visually monitor.', + 'help' => '1 URL per line to check, Please include domain and protocol.', +])