• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JayBizzle / Crawler-Detect / 29077650452

10 Jul 2026 07:45AM UTC coverage: 95.455% (-4.5%) from 100.0%
29077650452

Pull #616

github

web-flow
Merge 65957a0bf into ed842b98a
Pull Request #616: Add signatures for Palo Alto Cortex Xpanse, Infrawatch, ModatScanner,…

42 of 44 relevant lines covered (95.45%)

14.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.35
/src/CrawlerDetect.php
1
<?php
2

3
/*
4
 * This file is part of Crawler Detect - the web crawler detection library.
5
 *
6
 * (c) Mark Beech <m@rkbee.ch>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11

12
namespace Jaybizzle\CrawlerDetect;
13

14
use Jaybizzle\CrawlerDetect\Fixtures\AbstractProvider;
15
use Jaybizzle\CrawlerDetect\Fixtures\Crawlers;
16
use Jaybizzle\CrawlerDetect\Fixtures\Exclusions;
17
use Jaybizzle\CrawlerDetect\Fixtures\Headers;
18

19
class CrawlerDetect
20
{
21
    /**
22
     * The user agent.
23
     *
24
     * @var string|null
25
     */
26
    protected $userAgent;
27

28
    /**
29
     * Headers that contain a user agent.
30
     *
31
     * @var array
32
     */
33
    protected $httpHeaders = [];
34

35
    /**
36
     * Store regex matches.
37
     *
38
     * @var array
39
     */
40
    protected $matches = [];
41

42
    /**
43
     * Crawlers object.
44
     *
45
     * @var \Jaybizzle\CrawlerDetect\Fixtures\Crawlers
46
     */
47
    protected $crawlers;
48

49
    /**
50
     * Exclusions object.
51
     *
52
     * @var \Jaybizzle\CrawlerDetect\Fixtures\Exclusions
53
     */
54
    protected $exclusions;
55

56
    /**
57
     * Headers object.
58
     *
59
     * @var \Jaybizzle\CrawlerDetect\Fixtures\Headers
60
     */
61
    protected $uaHttpHeaders;
62

63
    /**
64
     * The compiled regex string.
65
     *
66
     * @var string
67
     */
68
    protected $compiledRegex;
69

70
    /**
71
     * The compiled exclusions regex string.
72
     *
73
     * @var string
74
     */
75
    protected $compiledExclusions;
76

77
    /**
78
     * Cache of compiled regex strings keyed by fixture class name, shared
79
     * across instances so per-request `new CrawlerDetect` calls don't
80
     * re-implode the (~1500-entry) pattern list each time.
81
     *
82
     * @var array<string, string>
83
     */
84
    protected static $compileCache = [];
85

86
    /**
87
     * Class constructor.
88
     */
89
    public function __construct(?array $headers = null, $userAgent = null)
90
    {
91
        $this->crawlers = new Crawlers;
19✔
92
        $this->exclusions = new Exclusions;
19✔
93
        $this->uaHttpHeaders = new Headers;
19✔
94

95
        $this->compiledRegex = $this->compileFixtureRegex($this->crawlers);
19✔
96
        $this->compiledExclusions = $this->compileFixtureRegex($this->exclusions);
19✔
97

98
        $this->setHttpHeaders($headers);
19✔
99
        $this->setUserAgent($userAgent);
19✔
100
    }
101

102
    /**
103
     * Compile and memoize the regex for a fixture provider.
104
     *
105
     * The pattern list is a fixed class property, so the class name is a
106
     * sufficient cache key — cheaper than hashing the patterns themselves.
107
     *
108
     * @return string
109
     */
110
    protected function compileFixtureRegex(AbstractProvider $fixture)
111
    {
112
        $class = get_class($fixture);
19✔
113

114
        if (! isset(self::$compileCache[$class])) {
19✔
115
            self::$compileCache[$class] = $this->compileRegex($fixture->getAll());
1✔
116
        }
117

118
        return self::$compileCache[$class];
19✔
119
    }
120

121
    /**
122
     * Compile the regex patterns into one regex string.
123
     *
124
     * A non-capturing group is used because callers only need the full
125
     * match (preg_match's $matches[0]), not a back-reference.
126
     *
127
     * @param  array  $patterns
128
     * @return string
129
     */
130
    public function compileRegex($patterns)
131
    {
132
        return '(?:'.implode('|', $patterns).')';
1✔
133
    }
134

135
    /**
136
     * Set HTTP headers.
137
     *
138
     * @param  array|null  $httpHeaders
139
     */
140
    public function setHttpHeaders($httpHeaders = null)
141
    {
142
        // Use global _SERVER if $httpHeaders aren't defined.
143
        if (! is_array($httpHeaders) || ! count($httpHeaders)) {
19✔
144
            $httpHeaders = $_SERVER;
19✔
145
        }
146

147
        // Clear existing headers.
148
        $this->httpHeaders = [];
19✔
149

150
        // Only save HTTP headers. In PHP land, that means
151
        // only _SERVER vars that start with HTTP_.
152
        foreach ($httpHeaders as $key => $value) {
19✔
153
            if (strpos($key, 'HTTP_') === 0) {
19✔
154
                $this->httpHeaders[$key] = $value;
2✔
155
            }
156
        }
157
    }
158

159
    /**
160
     * Return user agent headers.
161
     *
162
     * @return array
163
     */
164
    public function getUaHttpHeaders()
165
    {
166
        return $this->uaHttpHeaders->getAll();
19✔
167
    }
168

169
    /**
170
     * Set the user agent.
171
     *
172
     * @param  string|null  $userAgent
173
     */
174
    public function setUserAgent($userAgent = null)
175
    {
176
        if (is_null($userAgent)) {
19✔
177
            $userAgent = '';
19✔
178

179
            foreach ($this->getUaHttpHeaders() as $altHeader) {
19✔
180
                if (isset($this->httpHeaders[$altHeader])) {
19✔
181
                    $userAgent .= $this->httpHeaders[$altHeader].' ';
2✔
182
                }
183
            }
184

185
            // If no headers were found, keep it as null.
186
            if ($userAgent === '') {
19✔
187
                $userAgent = null;
19✔
188
            }
189
        }
190

191
        return $this->userAgent = $userAgent;
19✔
192
    }
193

194
    /**
195
     * Check user agent string against the regex.
196
     *
197
     * @param  string|null  $userAgent
198
     * @return bool
199
     */
200
    public function isCrawler($userAgent = null)
201
    {
202
        $this->matches = [];
15✔
203

204
        $agent = preg_replace(
15✔
205
            "/{$this->compiledExclusions}/i",
15✔
206
            '',
15✔
207
            $userAgent ?: $this->userAgent ?: ''
15✔
208
        );
15✔
209

210
        if ($agent === null || trim($agent) === '') {
15✔
211
            return false;
4✔
212
        }
213

214
        $agent = trim($agent);
13✔
215

216
        $result = preg_match("/{$this->compiledRegex}/i", $agent, $this->matches);
13✔
217

218
        if ($result === false) {
13✔
219
            $this->matches = [];
×
220

221
            return false;
×
222
        }
223

224
        return (bool) $result;
13✔
225
    }
226

227
    /**
228
     * Return the matches.
229
     *
230
     * @return string|null
231
     */
232
    public function getMatches()
233
    {
234
        return isset($this->matches[0]) ? $this->matches[0] : null;
5✔
235
    }
236

237
    /**
238
     * @return string|null
239
     */
240
    public function getUserAgent()
241
    {
242
        return $this->userAgent;
2✔
243
    }
244
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc