CGI-Info

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

        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 {

t/is_ai.t  view on Meta::CPAN


	# ---------------------------------------------------------------
	# 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');

	# ---------------------------------------------------------------

t/unit.t  view on Meta::CPAN

	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 {

t/unit.t  view on Meta::CPAN

	$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();

t/unit.t  view on Meta::CPAN

};

# 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');

t/unit.t  view on Meta::CPAN

#      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 1.612 second using v1.01-cache-2.11-cpan-9789f410c06 )