CGI-Info
view release on metacpan or search on metacpan
return self.{is_ai} = 0
## browser\_type
Returns a string classifying the visitor's client. The possible values are:
- `'mobile'` -- smartphone or tablet (checked first)
- `'ai'` -- known AI training or inference crawler (see ["is\_ai"](#is_ai))
- `'search'` -- search-engine crawler
- `'robot'` -- other automated client
- `'web'` -- ordinary desktop or laptop browser
use Carp;
use Template;
use CGI::Info;
my $info = CGI::Info->new();
my $dir = $info->rootdir() . '/templates/' . $info->browser_type();
my $filename = ref($info);
$filename =~ s/::/\//g;
lib/CGI/Info.pm view on Meta::CPAN
=over 4
=item * C<'mobile'> -- smartphone or tablet (checked first)
=item * C<'ai'> -- known AI training or inference crawler (see L</is_ai>)
=item * C<'search'> -- search-engine crawler
=item * C<'robot'> -- other automated client
=item * C<'web'> -- ordinary desktop or laptop browser
=back
use Carp;
use Template;
use CGI::Info;
my $info = CGI::Info->new();
my $dir = $info->rootdir() . '/templates/' . $info->browser_type();
t/function.t view on Meta::CPAN
status_ok => $STATUS_OK,
status_forbidden => $STATUS_FORBIDDEN,
status_not_found => $STATUS_NOT_FOUND,
status_method_na => $STATUS_METHOD_NA,
status_length_req => $STATUS_LENGTH_REQ,
status_too_large => $STATUS_TOO_LARGE,
status_unproc => $STATUS_UNPROC,
ua_iphone => $UA_IPHONE,
ua_android => $UA_ANDROID,
ua_ipad => $UA_IPAD,
ua_desktop => $UA_DESKTOP,
ua_gbot => $UA_GBOT,
ua_cbot => $UA_CBOT,
ua_claude_web => $UA_CLAUDE_WEB,
ua_gptbot => $UA_GPTBOT,
ua_chatgpt_user => $UA_CHATGPT_USER,
ua_cohere_ai => $UA_COHERE_AI,
ua_gptbot_sql => $UA_GPTBOT_SQL,
);
# ---------------------------------------------------------------------------
t/function.t view on Meta::CPAN
# Android UA detected as mobile
subtest 'is_mobile() - Android user-agent' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_USER_AGENT} = $config{ua_android};
$ENV{REMOTE_ADDR} = $config{good_ip};
ok(CGI::Info->new()->is_mobile(), 'Android UA detected as mobile');
};
# Desktop UA not detected as mobile
subtest 'is_mobile() - desktop user-agent not mobile' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_USER_AGENT} = $config{ua_desktop};
$ENV{REMOTE_ADDR} = $config{good_ip};
ok(!CGI::Info->new()->is_mobile(), 'desktop UA not detected as mobile');
};
# Sec-CH-UA-Mobile: ?1 header
subtest 'is_mobile() - Sec-CH-UA-Mobile hint' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_SEC_CH_UA_MOBILE} = '?1';
ok(CGI::Info->new()->is_mobile(), 'Sec-CH-UA-Mobile ?1 detected as mobile');
};
t/function.t view on Meta::CPAN
# 11. is_tablet()
# ============================================================
subtest 'is_tablet() - iPad user-agent' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_USER_AGENT} = $config{ua_ipad};
ok(CGI::Info->new()->is_tablet(), 'iPad UA detected as tablet');
};
subtest 'is_tablet() - desktop user-agent not a tablet' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (Windows NT 10.0)';
ok(!CGI::Info->new()->is_tablet(), 'desktop UA not detected as tablet');
};
# ============================================================
# 12. is_robot()
# ============================================================
# Googlebot may be classed as robot or search engine, both are correct
subtest 'is_robot() - Googlebot classed as robot or search engine' => sub {
plan tests => 1;
reset_env();
t/function.t view on Meta::CPAN
my $info = CGI::Info->new();
ok($info->is_ai(), 'ChatGPT-User => is_ai true');
ok($info->is_robot(), 'ChatGPT-User => is_robot true (is_robot calls is_ai internally)');
};
# IS_AI env override (use local so the override cannot bleed into later tests)
subtest 'is_ai() - IS_AI env override' => sub {
plan tests => 1;
reset_env();
local $ENV{IS_AI} = 1;
$ENV{HTTP_USER_AGENT} = $config{ua_desktop};
$ENV{REMOTE_ADDR} = $config{good_ip};
ok(CGI::Info->new()->is_ai(), 'IS_AI=1 env override forces is_ai true');
};
# Non-AI UA must not trigger is_ai
subtest 'is_ai() - desktop UA is not AI' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_USER_AGENT} = $config{ua_desktop};
$ENV{REMOTE_ADDR} = $config{good_ip};
ok(!CGI::Info->new()->is_ai(), 'desktop UA is not detected as AI crawler');
};
# No CGI environment => 0
subtest 'is_ai() - no CGI env returns 0' => sub {
plan tests => 1;
reset_env();
is(CGI::Info->new()->is_ai(), 0, 'no CGI env returns 0');
};
# Call-order invariant: is_robot() first, then is_ai()
t/function.t view on Meta::CPAN
# ============================================================
subtest 'browser_type() - mobile' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_USER_AGENT} = $config{ua_iphone};
$ENV{REMOTE_ADDR} = $config{good_ip};
is(CGI::Info->new()->browser_type(), 'mobile', 'iPhone browser_type is mobile');
};
subtest 'browser_type() - desktop is web' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_USER_AGENT} = $config{ua_desktop};
$ENV{REMOTE_ADDR} = $config{good_ip};
is(CGI::Info->new()->browser_type(), 'web', 'desktop browser_type is web');
};
# AI crawlers must be classified as 'ai', which is checked before 'robot'.
# Verifies the priority order: mobile > ai > search > robot > web.
subtest 'browser_type() - returns ai for known AI crawler' => sub {
plan tests => 1;
reset_env();
$ENV{HTTP_USER_AGENT} = $config{ua_cbot};
$ENV{REMOTE_ADDR} = $config{good_ip};
is(CGI::Info->new()->browser_type(), 'ai', 'ClaudeBot => browser_type is ai');
t/function.t view on Meta::CPAN
# Any internal use of for/foreach without a lexical variable could
# overwrite $_ in the caller's scope.
# ============================================================
subtest '$_ not clobbered by public methods' => sub {
plan tests => 6;
reset_env();
$ENV{GATEWAY_INTERFACE} = 'CGI/1.1';
$ENV{REQUEST_METHOD} = 'GET';
$ENV{QUERY_STRING} = 'x=1';
$ENV{HTTP_USER_AGENT} = $config{ua_desktop};
$ENV{REMOTE_ADDR} = $config{good_ip};
$ENV{HTTP_HOST} = 'example.com';
my $info = CGI::Info->new();
# Set $_ to a sentinel and verify each method leaves it unchanged
local $_ = 'SENTINEL';
$info->params();
is($_, 'SENTINEL', 'params() does not clobber $_');
t/integration.t view on Meta::CPAN
is($info->browser_type(), 'ai', 'browser_type() is ai for AI crawler');
ok(!$info->is_mobile(), 'is_mobile() false for robot');
ok(!$info->is_tablet(), 'is_tablet() false for robot');
# params() should still work for a robot (it only blocks on bad content)
my $params = $info->params();
ok(!defined($params) || defined($params->{q}),
'params accessible for robot with clean query');
};
subtest 'desktop browser: browser_type web, not mobile/tablet/robot' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120';
$ENV{REMOTE_ADDR} = '1.2.3.4';
my $info = CGI::Info->new();
ok(!$info->is_mobile(), 'desktop is not mobile');
ok(!$info->is_tablet(), 'desktop is not tablet');
ok(!$info->is_robot(), 'desktop is not robot');
is($info->browser_type(), 'web', 'desktop browser_type is web');
};
# ============================================================
# 10. Stateful: --mobile/--robot/--tablet/--search-engine ARGV flags
# Each flag sets the appropriate state AND params() still works
# ============================================================
subtest 'ARGV --mobile flag: is_mobile true, params still parsed' => sub {
reset_env();
local @ARGV = ('--mobile', 'section=news', 'limit=10');
t/integration.t view on Meta::CPAN
$ENV{QUERY_STRING} = 'action=save&title=My+Post&category=tech';
$ENV{HTTP_COOKIE} = 'sessionid=s3cr3t; csrf=tok3n';
my $info = CGI::Info->new(allow => {
action => qr/^(save|preview|delete)$/,
title => qr/^[\w\s\+]+$/,
category => qr/^[a-z]+$/,
});
# Browser classification
ok(!$info->is_mobile(), 'desktop Mac not mobile');
ok(!$info->is_robot(), 'Chrome not a robot');
is($info->browser_type(), 'web', 'browser_type is web');
# Site details
is($info->host_name(), 'www.myapp.example.com', 'host correct');
is($info->domain_name(), 'myapp.example.com', 'domain correct');
# Form params
my $p = $info->params();
ok(defined $p, 'params returned');
t/integration.t view on Meta::CPAN
$ENV{REMOTE_ADDR} = $AI_REMOTE;
$ENV{GATEWAY_INTERFACE} = 'CGI/1.1';
my $ai_info = CGI::Info->new();
ok($ai_info->is_ai(), 'first request (ClaudeBot): is_ai true');
ok($ai_info->is_robot(), 'first request (ClaudeBot): is_robot true');
# Simulate end of request; reset class-level stdin cache
CGI::Info->reset();
# Second request arrives with a desktop UA
$ENV{HTTP_USER_AGENT} = $UA_CHROME;
my $plain_info = CGI::Info->new();
ok(!$plain_info->is_ai(), 'second request (Chrome): is_ai false after reset');
ok(!$plain_info->is_robot(), 'second request (Chrome): is_robot false after reset');
};
# browser_type() documents 'mobile' as the highest priority class.
# Even with IS_AI=1 forcing is_ai() true, a mobile UA must still
# return 'mobile', not 'ai', because the mobile check runs first.
subtest 'is_ai(): browser_type() mobile priority beats IS_AI override' => sub {
# ---------------------------------------------------------------
# Claude-Web (alternative Anthropic headless-browser UA)
# ---------------------------------------------------------------
$ENV{HTTP_USER_AGENT} = 'Claude-Web/1.0';
$i = new_ok('CGI::Info');
ok($i->is_ai() == 1, 'Claude-Web => is_ai true');
ok($i->is_robot() == 1, 'Claude-Web => is_robot true (no "bot" token)');
# ---------------------------------------------------------------
# Ordinary desktop browser -- must NOT trigger is_ai
# ---------------------------------------------------------------
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
$i = new_ok('CGI::Info');
ok($i->is_ai() == 0, 'Chrome desktop UA => is_ai false');
ok($i->browser_type() ne 'ai', 'Chrome desktop UA => browser_type not ai');
# ---------------------------------------------------------------
# Googlebot -- is a robot (search engine) but NOT an AI trainer
# ---------------------------------------------------------------
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
$i = new_ok('CGI::Info');
ok($i->is_ai() == 0, 'Googlebot => is_ai false (search, not AI trainer)');
ok($i->is_robot() == 1, 'Googlebot => is_robot true');
# ---------------------------------------------------------------
ok(CGI::Info->new()->is_mobile(), 'iPhone UA is mobile');
};
subtest 'is_mobile() - true for Android user agent' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (Linux; Android 11; Pixel 5)';
$ENV{REMOTE_ADDR} = '1.2.3.4';
ok(CGI::Info->new()->is_mobile(), 'Android UA is mobile');
};
subtest 'is_mobile() - false for desktop user agent' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120';
$ENV{REMOTE_ADDR} = '1.2.3.4';
ok(!CGI::Info->new()->is_mobile(), 'desktop UA is not mobile');
};
subtest 'is_mobile() - overridden by IS_MOBILE=1' => sub {
reset_env();
$ENV{IS_MOBILE} = 1;
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (Windows NT 10.0)';
ok(CGI::Info->new()->is_mobile(), 'IS_MOBILE=1 overrides UA detection');
};
subtest 'is_mobile() - true via Sec-CH-UA-Mobile hint' => sub {
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X)';
ok(CGI::Info->new()->is_tablet(), 'iPad UA is tablet');
};
subtest 'is_tablet() - false for iPhone user agent' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)';
ok(!CGI::Info->new()->is_tablet(), 'iPhone UA is not a tablet');
};
subtest 'is_tablet() - false for desktop user agent' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)';
ok(!CGI::Info->new()->is_tablet(), 'desktop UA is not a tablet');
};
# ============================================================
# is_robot()
# POD: returns boolean; true for robots/crawlers;
# SQL injection in UA sets status 403 and returns true
# ============================================================
subtest 'is_robot() - false when no CGI environment' => sub {
reset_env();
};
# cohere-ai is another non-standard UA with no "bot" or "spider" component.
subtest 'is_ai() - true for cohere-ai UA' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = $UA_COHERE;
$ENV{REMOTE_ADDR} = $AI_REMOTE;
ok(CGI::Info->new()->is_ai(), 'cohere-ai UA => is_ai true');
};
# An ordinary desktop browser must not trigger is_ai().
subtest 'is_ai() - false for desktop browser UA' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = $UA_DESKTOP_WIN;
$ENV{REMOTE_ADDR} = $AI_REMOTE;
ok(!CGI::Info->new()->is_ai(), 'desktop UA => is_ai false');
delete $is_ai_ledger{'returns 0 for non-AI UA'};
};
# REMOTE_ADDR absent: the CGI environment is incomplete; method returns 0.
# This is consistent with is_robot() and is_search_engine() behaviour.
subtest 'is_ai() - false when REMOTE_ADDR absent' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = $UA_CLAUDE_BOT;
# No REMOTE_ADDR
ok(!CGI::Info->new()->is_ai(), 'absent REMOTE_ADDR => is_ai false');
# in that priority order
# ============================================================
subtest 'browser_type() - returns mobile for smartphone UA' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)';
$ENV{REMOTE_ADDR} = '1.2.3.4';
is(CGI::Info->new()->browser_type(), 'mobile', 'smartphone => mobile');
};
subtest 'browser_type() - returns web for desktop browser' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120';
$ENV{REMOTE_ADDR} = '1.2.3.4';
is(CGI::Info->new()->browser_type(), 'web', 'desktop => web');
};
subtest 'browser_type() - returns ai for known AI crawler' => sub {
reset_env();
$ENV{HTTP_USER_AGENT} = 'ClaudeBot/1.0';
$ENV{REMOTE_ADDR} = '1.2.3.4';
is(CGI::Info->new()->browser_type(), 'ai', 'bot => ai');
};
subtest 'browser_type() - returns robot for non-AI bot' => sub {
( run in 2.050 seconds using v1.01-cache-2.11-cpan-9789f410c06 )