diff --git a/composer.json b/composer.json index cd6c539..86306c4 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "keywords": ["Laravel", "ipinfolaravel"], "require": { "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "ipinfo/ipinfo": "^3.2.0" + "ipinfo/ipinfo": "^3.3.0" }, "require-dev": { "phpunit/phpunit": "^12.0", diff --git a/config/ipinfocorelaravel.php b/config/ipinfocorelaravel.php new file mode 100644 index 0000000..87b2d3e --- /dev/null +++ b/config/ipinfocorelaravel.php @@ -0,0 +1,5 @@ +configure(); + + if ($this->filter && call_user_func($this->filter, $request)) { + $details = null; + } else { + try { + $details = $this->ipinfo->getDetails( + $this->ip_selector->getIP($request), + ); + } catch (\Exception $e) { + $details = null; + + // users can't catch this exception with their own wrapper + // middleware unfortunately, so we catch it for them. but for + // backwards-compatibility, we throw the exception again unless + // they've told us not to. + if ($this->no_except != true) { + throw $e; + } + } + } + + $request->attributes->set("ipinfo", $details); + + return $next($request); + } + + /** + * Determine settings based on user-defined configs or use defaults. + */ + public function configure() + { + $this->access_token = config("services.ipinfo.access_token", null); + $this->filter = config("services.ipinfo.filter", [ + $this, + "defaultFilter", + ]); + $this->no_except = config("services.ipinfo.no_except", false); + $this->ip_selector = config( + "services.ipinfo.ip_selector", + new DefaultIPSelector(), + ); + + if ( + $custom_countries = config("services.ipinfo.countries_file", null) + ) { + $this->settings["countries_file"] = $custom_countries; + } + + if ($custom_cache = config("services.ipinfo.cache", null)) { + $this->settings["cache"] = $custom_cache; + } else { + $maxsize = config( + "services.ipinfo.cache_maxsize", + self::CACHE_MAXSIZE, + ); + $ttl = config("services.ipinfo.cache_ttl", self::CACHE_TTL); + $this->settings["cache"] = new DefaultCache($maxsize, $ttl); + } + + $this->ipinfo = new IPinfoCoreClient( + $this->access_token, + $this->settings, + ); + } + + /** + * Should IP lookup be skipped. + * @param Request $request Request object. + * @return bool Whether or not to filter out. + */ + public function defaultFilter($request) + { + $user_agent = $request->header("user-agent"); + if ($user_agent) { + $lower_user_agent = strtolower($user_agent); + + $is_spider = strpos($lower_user_agent, "spider") !== false; + $is_bot = strpos($lower_user_agent, "bot") !== false; + + return $is_spider || $is_bot; + } + + return false; + } +} diff --git a/src/core/ipinfocorelaravelServiceProvider.php b/src/core/ipinfocorelaravelServiceProvider.php new file mode 100644 index 0000000..c37b850 --- /dev/null +++ b/src/core/ipinfocorelaravelServiceProvider.php @@ -0,0 +1,64 @@ +app->runningInConsole()) { + $this->bootForConsole(); + } + } + + /** + * Register any package services. + * @return void + */ + public function register() + { + $this->mergeConfigFrom( + __DIR__ . "/../../config/ipinfocorelaravel.php", + "ipinfocorelaravel", + ); + + // Register the service the package provides. + $this->app->singleton( + "ipinfocorelaravel", + fn($app) => new ipinfocorelaravel(), + ); + } + + /** + * Get the services provided by the provider. + * @return array + */ + public function provides() + { + return ["ipinfocorelaravel"]; + } + + /** + * Console-specific booting. + * @return void + */ + protected function bootForConsole() + { + // Publishing the configuration file. + $this->publishes( + [ + __DIR__ . "/../../config/ipinfocorelaravel.php" => config_path( + "ipinfocorelaravel.php", + ), + ], + "ipinfocorelaravel.config", + ); + } +} diff --git a/tests/IpinfocorelaravelTest.php b/tests/IpinfocorelaravelTest.php new file mode 100644 index 0000000..8635f62 --- /dev/null +++ b/tests/IpinfocorelaravelTest.php @@ -0,0 +1,145 @@ +getMockBuilder(ipinfocorelaravel::class) + ->onlyMethods(["configure"]) + ->getMock(); + $mw->method("configure")->willReturn(null); + $mw->ipinfo = $client; + $mw->ip_selector = $selector; + $mw->filter = $filter; + $mw->no_except = $noExcept; + return $mw; + } + + public function test_handle_merges_details_on_success() + { + $details = (object) ["country" => "US", "ip" => "8.8.8.8"]; + $client = $this->createMock(IPinfoCoreClient::class); + $client + ->expects($this->once()) + ->method("getDetails") + ->with("8.8.8.8") + ->willReturn($details); + + $selector = $this->createMock(IPHandlerInterface::class); + $selector->method("getIP")->willReturn("8.8.8.8"); + + $mw = $this->makeMiddleware($client, $selector); + + $request = Request::create("/foo", "GET"); + $next = function ($req) use (&$out) { + $out = $req->get("ipinfo"); + return new Response("OK", 200); + }; + + $resp = $mw->handle($request, $next); + + $this->assertSame($details, $out); + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals("OK", $resp->getContent()); + } + + public function test_handle_skips_lookup_when_filter_true() + { + $client = $this->createMock(IPinfoCoreClient::class); + $client->expects($this->never())->method("getDetails"); + + $selector = $this->createMock(IPHandlerInterface::class); + $selector->expects($this->never())->method("getIP"); + + $filter = fn($req) => true; + $mw = $this->makeMiddleware($client, $selector, $filter); + + $request = Request::create("/", "GET"); + $request->headers->set("user-agent", "GoogleBot"); + + $next = function ($req) use (&$out) { + $out = $req->get("ipinfo"); + return new Response(); + }; + + $mw->handle($request, $next); + $this->assertNull($out); + } + + public function test_handle_throws_if_client_throws_and_no_except_false() + { + $this->expectException(\Exception::class); + + $client = $this->createMock(IPinfoCoreClient::class); + $client + ->method("getDetails") + ->willThrowException(new \Exception("boom")); + + $selector = $this->createMock(IPHandlerInterface::class); + $selector->method("getIP")->willReturn("1.1.1.1"); + + $mw = $this->makeMiddleware($client, $selector, null, false); + $mw->handle(Request::create("/", "GET"), fn($r) => new Response()); + } + + public function test_handle_swallows_if_client_throws_and_no_except_true() + { + $client = $this->createMock(IPinfoCoreClient::class); + $client + ->method("getDetails") + ->willThrowException(new \Exception("boom")); + + $selector = $this->createMock(IPHandlerInterface::class); + $selector->method("getIP")->willReturn("1.1.1.1"); + + $mw = $this->makeMiddleware($client, $selector, null, true); + + $next = function ($req) use (&$out) { + $out = $req->get("ipinfo"); + return new Response(); + }; + + $mw->handle(Request::create("/", "GET"), $next); + $this->assertNull($out); + } + + public function test_defaultFilter_detects_bots_and_spiders() + { + $mw = new ipinfocorelaravel(); + + $r1 = Request::create("/", "GET"); + $r1->headers->set("user-agent", "MySpider"); + $this->assertTrue($mw->defaultFilter($r1)); + + $r2 = Request::create("/", "GET"); + $r2->headers->set("user-agent", "someBOT/2.0"); + $this->assertTrue($mw->defaultFilter($r2)); + + $r3 = Request::create("/", "GET"); + $r3->headers->set("user-agent", "normal"); + $this->assertFalse($mw->defaultFilter($r3)); + + $r4 = Request::create("/", "GET"); + $this->assertFalse($mw->defaultFilter($r4)); + } +}