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
+
+
+## After
+
+
+## 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?'
])
+
+
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.', +])