Changeset 3751893
- Timestamp:
- 10/17/2026 12:24:00 PM (2 weeks ago)
- Location:
- agentimus
- Files:
-
- 22 edited
- 1 copied
-
tags/1.51.9 (copied) (copied from agentimus/trunk )
-
tags/1.51.9/agentimus.php (modified) (2 diffs)
-
tags/1.51.9/inc/Abilities/Registrar.php (modified) (1 diff)
-
tags/1.51.9/inc/Admin.php (modified) (1 diff)
-
tags/1.51.9/inc/Cloudflare/Client.php (modified) (1 diff)
-
tags/1.51.9/inc/Cloudflare/Conflicts.php (modified) (8 diffs)
-
tags/1.51.9/inc/Cloudflare/Module.php (modified) (6 diffs)
-
tags/1.51.9/inc/Cloudflare/SpoofCheck.php (modified) (2 diffs)
-
tags/1.51.9/inc/Cloudflare/Table.php (modified) (17 diffs)
-
tags/1.51.9/readme.txt (modified) (3 diffs)
-
tags/1.51.9/uninstall.php (modified) (1 diff)
-
tags/1.51.9/vendor/composer/installed.php (modified) (2 diffs)
-
trunk/agentimus.php (modified) (2 diffs)
-
trunk/inc/Abilities/Registrar.php (modified) (1 diff)
-
trunk/inc/Admin.php (modified) (1 diff)
-
trunk/inc/Cloudflare/Client.php (modified) (1 diff)
-
trunk/inc/Cloudflare/Conflicts.php (modified) (8 diffs)
-
trunk/inc/Cloudflare/Module.php (modified) (6 diffs)
-
trunk/inc/Cloudflare/SpoofCheck.php (modified) (2 diffs)
-
trunk/inc/Cloudflare/Table.php (modified) (17 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/uninstall.php (modified) (1 diff)
-
trunk/vendor/composer/installed.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
agentimus/tags/1.51.9/agentimus.php
r3750928 r3751893 4 4 * Plugin URI: https://github.com/heera/agentimus 5 5 * Description: Make your site agent-ready (llms.txt, markdown, JSON-LD, a /.well-known discovery document, AI-crawl controls, agent-activity log) AND let the AI tools you use operate it over MCP — read your data and, opt-in, draft/edit/publish posts. Lightweight, no SEO bloat. 6 * Version: 1.51. 86 * Version: 1.51.9 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 7.4 … … 20 20 defined( 'ABSPATH' ) || exit; 21 21 22 define( 'AGENTIMUS_VERSION', '1.51. 8' );22 define( 'AGENTIMUS_VERSION', '1.51.9' ); 23 23 define( 'AGENTIMUS_FILE', __FILE__ ); 24 24 define( 'AGENTIMUS_DIR', plugin_dir_path( __FILE__ ) ); -
agentimus/tags/1.51.9/inc/Abilities/Registrar.php
r3750405 r3751893 587 587 'origin' => self::i( 'Requests that reached this server.' ), 588 588 'blocked' => self::i( 'Requests the edge turned away without contacting this server.' ), 589 'probes' => self::i( 'Of the blocked requests, how many asked for a file no reader fetches (a .env, a .git folder, a wp-config copy): a scanner using this crawler\'s name. Never counted as the edge turning this crawler away.' ), 589 590 'served' => self::i( 'Requests that got a page: a 2xx the edge let through, not counting /robots.txt reads. Passing the edge is not taking content — a request the origin answered 403 or 404 took nothing.' ), 590 591 'bytes' => self::i(), -
agentimus/tags/1.51.9/inc/Admin.php
r3750928 r3751893 993 993 // ⭐ A security-only release gets ONE row (1.51.1): the fix is the 994 994 // whole release, and there is one thing an owner may want to do. 995 // ⭐ So does a one-fix release (1.51.8) — plain-permalink term pages 996 // named in the log and the search screens. 995 // ⭐ So does a one-fix release (1.51.9) — a scanner's probes no longer 996 // raise the Cloudflare warning. ⛔ No company is named: the release 997 // text names no specific bot (his ruling, 1.51.3 and 1.51.4). 997 998 array( 998 'icon' => ' page',999 'title' => ' Tag Pages Named on Plain Permalinks',1000 'text' => ' On a site with plain permalinks, the Request Log called every tag and category page Homepage. It now shows the tag or category name, here and in your search results.',999 'icon' => 'shield', 1000 'title' => 'Scanners No Longer Trip the Cloudflare Warning', 1001 'text' => 'A scanner that used an AI crawler\'s name to look for secret files like .env could make it look as if Cloudflare was blocking that company. Those requests are now counted apart, and only real blocks raise the warning.', 1001 1002 ), 1002 1003 ), -
agentimus/tags/1.51.9/inc/Cloudflare/Client.php
r3722083 r3751893 215 215 216 216 /** 217 * Hourly edge-refused requests for a window, one row per (hour, user agent, 218 * path) — the paths {@see Module::aggregate()} needs to tell a scanner's 219 * probe from a reader being turned away. The filter is the same "blocked" 220 * test as {@see blocked_sources()}. The paths are read here, classified in 221 * the poll and never stored: the table keeps only the count. 222 * 223 * @param string $token API token, plaintext. 224 * @param string $zone_id Zone tag. 225 * @param int $since Window start, Unix UTC. 226 * @param int $until Window end, Unix UTC. 227 * @return array { rows?: array<int,array{hour:string,ua:string,path:string,requests:int}>, error?: string } 228 */ 229 public function blocked_paths( $token, $zone_id, $since, $until ) { 230 $query = 'query ($zone: String!, $since: Time!, $until: Time!) { 231 viewer { 232 zones(filter: { zoneTag: $zone }) { 233 httpRequestsAdaptiveGroups( 234 limit: ' . self::GRAPHQL_LIMIT . ', 235 filter: { datetime_geq: $since, datetime_lt: $until, requestSource: "eyeball", edgeResponseStatus_in: [403, 429], originResponseStatus: 0 }, 236 orderBy: [count_DESC] 237 ) { 238 count 239 dimensions { datetimeHour userAgent clientRequestPath } 240 } 241 } 242 } 243 }'; 244 245 $out = $this->graphql( $token, $query, array( 246 'zone' => (string) $zone_id, 247 'since' => gmdate( 'Y-m-d\TH:i:s\Z', (int) $since ), 248 'until' => gmdate( 'Y-m-d\TH:i:s\Z', (int) $until ), 249 ) ); 250 if ( isset( $out['error'] ) ) { 251 return $out; 252 } 253 254 // The expected container, or it's an error — never "real empty data". 255 if ( ! isset( $out['json']['data']['viewer']['zones'][0]['httpRequestsAdaptiveGroups'] ) ) { 256 return array( 'error' => __( 'Cloudflare returned an unexpected response shape.', 'agentimus' ) ); 257 } 258 259 $rows = array(); 260 foreach ( (array) $out['json']['data']['viewer']['zones'][0]['httpRequestsAdaptiveGroups'] as $g ) { 261 if ( ! isset( $g['dimensions'] ) ) { 262 continue; 263 } 264 $d = (array) $g['dimensions']; 265 $rows[] = array( 266 'hour' => (string) ( isset( $d['datetimeHour'] ) ? $d['datetimeHour'] : '' ), 267 'ua' => (string) ( isset( $d['userAgent'] ) ? $d['userAgent'] : '' ), 268 'path' => (string) ( isset( $d['clientRequestPath'] ) ? $d['clientRequestPath'] : '' ), 269 'requests' => (int) ( isset( $g['count'] ) ? $g['count'] : 0 ), 270 ); 271 } 272 273 return array( 'rows' => $rows ); 274 } 275 276 /** 217 277 * Hourly /robots.txt requests for a window, in the same row shape as 218 278 * {@see hourly_traffic()}. Those rows carry no path, so a crawler reading -
agentimus/tags/1.51.9/inc/Cloudflare/Conflicts.php
r3722083 r3751893 62 62 * the owner fixing the cause, instead of shouting for a week about history. 63 63 * 64 * @param array $crawlers Per-crawler totals: { ua, name, operator, requests, cached, origin, blocked, served }.64 * @param array $crawlers Per-crawler totals: { ua, name, operator, requests, cached, origin, blocked, probes?, served }. 65 65 * @param array $policy The owner's declarations: 66 66 * - ai_input (bool) robots Content-Signal ai-input. … … 69 69 * - allowed_agents (array) user-agent rules the OWNER allowed in the review queue. 70 70 * @param int $days The window the totals cover, for the copy. 71 * @param array|null $recent Last-24h per-crawler map { ua => { blocked, p assed, served } }71 * @param array|null $recent Last-24h per-crawler map { ua => { blocked, probes?, passed, served } } 72 72 * (see Table::recent()), or null to treat all 73 73 * evidence as current. … … 94 94 continue; 95 95 } 96 $blocked = (int) $c['blocked']; 97 $requests = (int) $c['requests']; 96 // A blocked probe is no reader turned away ({@see refused()}) — and 97 // no reading attempt either, so it leaves the share's base too. 98 $blocked = self::refused( $c ); 99 $requests = (int) $c['requests'] - self::probes( $c ); 98 100 $op = (string) $c['operator']; 99 101 if ( '' === $op || $blocked < self::MIN_BLOCKED || $blocked < $requests * self::MIN_BLOCKED_SHARE ) { … … 103 105 // blocking fires — otherwise the fix already landed and the warning 104 106 // would just be nagging about history. 105 if ( null !== $recent && empty( $recent[ $token ]['blocked']) ) {107 if ( null !== $recent && ( ! isset( $recent[ $token ] ) || 0 === self::refused( $recent[ $token ] ) ) ) { 106 108 continue; 107 109 } … … 111 113 $per_operator[ $op ]['blocked'] += $blocked; 112 114 $per_operator[ $op ]['requests'] += $requests; 113 $per_operator[ $op ]['recent'] += null !== $recent ? (int) $recent[ $token ]['blocked']: 0;115 $per_operator[ $op ]['recent'] += null !== $recent ? self::refused( $recent[ $token ] ) : 0; 114 116 } 115 117 … … 294 296 295 297 /** 298 * The requests the edge turned away that were a READER being refused: the 299 * blocked count less the scanner probes in it ({@see Module::is_probe_path()}). 300 * A row stored before probes were counted has none, and counts as before. 301 * 302 * @param array $row Any totals row carrying `blocked` and optionally `probes`. 303 * @return int 304 */ 305 public static function refused( array $row ) { 306 return max( 0, ( isset( $row['blocked'] ) ? (int) $row['blocked'] : 0 ) - self::probes( $row ) ); 307 } 308 309 /** 310 * The scanner probes within a row's blocked count, never more than it. 311 * 312 * @param array $row Any totals row. 313 * @return int 314 */ 315 private static function probes( array $row ) { 316 $blocked = isset( $row['blocked'] ) ? (int) $row['blocked'] : 0; 317 return min( $blocked, isset( $row['probes'] ) ? max( 0, (int) $row['probes'] ) : 0 ); 318 } 319 320 /** 296 321 * The slug an operator's warn conflict carries in its id 297 322 * ("edge-blocks-<slug>"). One transform, shared with every reader that has … … 330 355 * Pure — no DB, no time() — so it unit-tests standalone, like detect(). 331 356 * 332 * @param array $daily Day => ua => { requests, blocked, p assed, served }, oldest first ({@see Table::daily()}).357 * @param array $daily Day => ua => { requests, blocked, probes?, passed, served }, oldest first ({@see Table::daily()}). 333 358 * @param string $id The conflict id to date. 334 359 * @param array $operators ua => operator name, for the per-operator rollup a warn conflict uses. … … 367 392 continue; 368 393 } 369 $sums['blocked'] += (int) $t['blocked'];370 $sums['requests'] += (int) $t['requests'] ;394 $sums['blocked'] += self::refused( $t ); 395 $sums['requests'] += (int) $t['requests'] - self::probes( $t ); 371 396 } 372 397 return $sums['blocked'] >= self::MIN_BLOCKED -
agentimus/tags/1.51.9/inc/Cloudflare/Module.php
r3722083 r3751893 152 152 $since = $latest > 0 ? $latest - self::OVERLAP_SECONDS : $now - self::BACKFILL_SECONDS; 153 153 $since = max( $since, $now - self::BACKFILL_SECONDS ); 154 // ⛔ An hour stored before probes were counted keeps saying "no probes" 155 // until it is re-read — and the overlap only re-reads two hours, so a 156 // scanner's morning would go on firing the warning for a day and 157 // inflating the week for seven. Once, re-read everything Cloudflare 158 // still holds; a re-read hour replaces its row. 159 $backfill = ! get_option( Table::PROBES_OPTION ); 160 if ( $backfill ) { 161 $since = $now - self::BACKFILL_SECONDS; 162 } 154 163 155 164 while ( $since < $now ) { … … 164 173 } 165 174 } 175 if ( ! isset( $out['error'] ) ) { 176 // Same rule for the blocked paths: without them every scanner probe 177 // would be stored as a reader turned away. 178 $paths = $this->client->blocked_paths( $token, $zone, $since, $until ); 179 if ( isset( $paths['error'] ) ) { 180 $out = $paths; 181 } 182 } 166 183 if ( isset( $out['error'] ) ) { 167 184 // Keep the last good data; the panel shows the numbers' age and this note. … … 169 186 return; 170 187 } 171 Table::upsert( self::aggregate( (array) $out['rows'], (array) $robots['rows'] ) );188 Table::upsert( self::aggregate( (array) $out['rows'], (array) $robots['rows'], (array) $paths['rows'] ) ); 172 189 $since = $until; 173 190 } 174 191 175 192 $this->settings->record_poll( '' ); 193 if ( $backfill ) { 194 update_option( Table::PROBES_OPTION, 1, false ); 195 } 176 196 Table::prune(); 177 197 … … 200 220 * answers 403 or 404 took nothing. The training notice counts this. 201 221 * 222 * Within `blocked`, `probes` counts the requests for a path no reader asks 223 * for ({@see is_probe_path()}). ⛔⛔ A BLOCKED PROBE IS NOT A READER TURNED 224 * AWAY. On heera.it (2026-10-17) "Cloudflare is blocking Anthropic" and 225 * "…You.com" were one scanner on Google Cloud asking for .env files under 226 * both names, stopped by his own bad-paths rule — the third time after 227 * OpenAI (08-29, 09-17). Neither company publishes addresses to check, so 228 * the spoof check could only say "unknown"; the path says it plainly. 229 * 202 230 * @param array $raw Rows from Client::hourly_traffic(). 203 231 * @param array $robots Rows from Client::robots_fetches() for the same window. 232 * @param array $paths Rows from Client::blocked_paths() for the same window. 204 233 * @return array<int,array> Rows shaped for Table::upsert(). 205 234 */ 206 public static function aggregate( array $raw, array $robots = array() ) {235 public static function aggregate( array $raw, array $robots = array(), array $paths = array() ) { 207 236 $cached_statuses = array( 'hit', 'stale', 'updating', 'revalidated' ); 208 237 $buckets = array(); … … 227 256 'origin' => 0, 228 257 'blocked' => 0, 258 'probes' => 0, 229 259 'served' => 0, 230 260 'bytes' => 0, … … 257 287 } 258 288 289 foreach ( $paths as $row ) { 290 $key = self::hour_key( (string) $row['hour'] ) . '|' . self::crawler_token( (string) $row['ua'] ); 291 if ( isset( $buckets[ $key ] ) && self::is_probe_path( (string) $row['path'] ) ) { 292 $buckets[ $key ]['probes'] += (int) $row['requests']; 293 } 294 } 295 // Two reads of the same window can disagree by a few requests; a probe 296 // count is a share of `blocked` and never more than it. 297 foreach ( $buckets as $key => $bucket ) { 298 $buckets[ $key ]['probes'] = min( $bucket['probes'], $bucket['blocked'] ); 299 } 300 259 301 return array_values( $buckets ); 302 } 303 304 /** 305 * Whether a request path is a scanner's probe — a secret or a config file 306 * that no reader of the site, human or AI, ever asks for. 307 * 308 * Deliberately narrow: hidden files and folders (/.env, /.git/config, 309 * /.aws/credentials — every one except /.well-known/), anything ending in 310 * .env, wp-config.* copies, database dumps and editor backups, phpinfo, and 311 * Vite's /@fs/ file-read hole. An ordinary page, a feed or robots.txt is 312 * never a probe. Decoded first, so /admin%2F.env is caught like /admin/.env. 313 * 314 * @param string $path Request path, as Cloudflare reports it. 315 * @return bool 316 */ 317 public static function is_probe_path( $path ) { 318 $path = strtolower( (string) $path ); 319 for ( $i = 0; $i < 2 && false !== strpos( $path, '%' ); $i++ ) { 320 $path = rawurldecode( $path ); 321 } 322 $path = (string) strtok( $path, '?' ); 323 324 foreach ( explode( '/', str_replace( '\\', '/', $path ) ) as $segment ) { 325 if ( '' === $segment || '.' === $segment || '..' === $segment || '.well-known' === $segment ) { 326 continue; 327 } 328 if ( '.' === $segment[0] || '@fs' === $segment || 0 === strpos( $segment, 'wp-config' ) || 0 === strpos( $segment, 'phpinfo' ) ) { 329 return true; 330 } 331 if ( preg_match( '/\.(env|sql|bak|swp)$/', $segment ) ) { 332 return true; 333 } 334 } 335 return false; 260 336 } 261 337 -
agentimus/tags/1.51.9/inc/Cloudflare/SpoofCheck.php
r3709876 r3751893 184 184 * @param array $crawlers Rows with { ua, operator }. 185 185 * @param array $policy The declarations {@see Conflicts::detect()} takes. 186 * @param array $recent ua => { blocked, p assed } ({@see Table::recent()}).186 * @param array $recent ua => { blocked, probes?, passed } ({@see Table::recent()}). 187 187 * @return array<string,int> operator => blocked requests. 188 188 */ … … 196 196 } 197 197 $out[ $op ] = ( isset( $out[ $op ] ) ? $out[ $op ] : 0 ) 198 + ( isset( $recent[ $ua ] ['blocked'] ) ? (int) $recent[ $ua ]['blocked']: 0 );198 + ( isset( $recent[ $ua ] ) ? Conflicts::refused( (array) $recent[ $ua ] ) : 0 ); 199 199 } 200 200 return $out; -
agentimus/tags/1.51.9/inc/Cloudflare/Table.php
r3722083 r3751893 3 3 * Edge aggregates table — one row per (UTC hour, crawler), holding the counts 4 4 * the panel shows: total edge requests, cache-served, origin-served, blocked 5 * at the edge , and bytes out.5 * at the edge (and how many of those were scanner probes), and bytes out. 6 6 * 7 7 * Cloudflare's Free plan forgets these numbers within days; this table is why … … 20 20 21 21 /** @var string Schema version — bump on any structural change. */ 22 const VERSION = ' 2';22 const VERSION = '3'; 23 23 24 24 /** @var string Option recording the installed schema version. */ 25 25 const VERSION_OPTION = 'agentimus_edge_db_version'; 26 27 /** 28 * @var string Option set once a poll has re-read the whole backfill window 29 * since probes began to be counted. Until then every stored hour reads as 30 * no probes, so the poll re-reads as far back as it can 31 * ({@see Module::run_poll()}). Keyed to the count, not the schema version: 32 * a site already at version 3 from a dev build needs it too. 33 */ 34 const PROBES_OPTION = 'agentimus_edge_probes_backfilled'; 26 35 27 36 /** @var int Age-based retention (days). At ~15 crawlers × 24 rows/day this is small. */ … … 75 84 origin int(10) unsigned NOT NULL DEFAULT 0, 76 85 blocked int(10) unsigned NOT NULL DEFAULT 0, 86 probes int(10) unsigned NOT NULL DEFAULT 0, 77 87 served int(10) unsigned NOT NULL DEFAULT 0, 78 88 bytes bigint(20) unsigned NOT NULL DEFAULT 0, … … 85 95 // can silently skip an ADD COLUMN on a drifted schema, and stamping the 86 96 // version anyway would break every query forever. 87 foreach ( array( 'hour_at', 'ua', 'requests', 'cached', 'origin', 'blocked', ' served', 'bytes' ) as $column ) {97 foreach ( array( 'hour_at', 'ua', 'requests', 'cached', 'origin', 'blocked', 'probes', 'served', 'bytes' ) as $column ) { 88 98 $found = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM $table LIKE %s", $column ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from our own prefix helper. 89 99 if ( null === $found ) { … … 100 110 * double-count). 101 111 * 102 * @param array $rows Rows shaped { hour_at, ua, requests, cached, origin, blocked, served, bytes }.112 * @param array $rows Rows shaped { hour_at, ua, requests, cached, origin, blocked, probes?, served, bytes }. 103 113 * @return void 104 114 */ … … 112 122 foreach ( $rows as $row ) { 113 123 $values[] = $wpdb->prepare( 114 '(%s,%s,%d,%d,%d,%d,%d,%d )',124 '(%s,%s,%d,%d,%d,%d,%d,%d,%d)', 115 125 (string) $row['hour_at'], 116 126 (string) $row['ua'], … … 119 129 (int) $row['origin'], 120 130 (int) $row['blocked'], 131 isset( $row['probes'] ) ? (int) $row['probes'] : 0, 121 132 (int) $row['served'], 122 133 (int) $row['bytes'] … … 124 135 } 125 136 $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- every tuple prepared above; table name is our own. 126 "INSERT INTO $table (hour_at,ua,requests,cached,origin,blocked, served,bytes) VALUES " . implode( ',', $values ) . '137 "INSERT INTO $table (hour_at,ua,requests,cached,origin,blocked,probes,served,bytes) VALUES " . implode( ',', $values ) . ' 127 138 ON DUPLICATE KEY UPDATE 128 139 requests = VALUES(requests), cached = VALUES(cached), origin = VALUES(origin), 129 blocked = VALUES(blocked), served = VALUES(served), bytes = VALUES(bytes)'140 blocked = VALUES(blocked), probes = VALUES(probes), served = VALUES(served), bytes = VALUES(bytes)' 130 141 ); 131 142 } … … 148 159 * 149 160 * @param int $days Window length in days. 150 * @return array<int,array{ua:string,requests:int,cached:int,origin:int,blocked:int, served:int,bytes:int}>161 * @return array<int,array{ua:string,requests:int,cached:int,origin:int,blocked:int,probes:int,served:int,bytes:int}> 151 162 */ 152 163 public static function summary( $days ) { … … 157 168 "SELECT ua, 158 169 SUM(requests) AS requests, SUM(cached) AS cached, SUM(origin) AS origin, 159 SUM(blocked) AS blocked, SUM( served) AS served, SUM(bytes) AS bytes170 SUM(blocked) AS blocked, SUM(probes) AS probes, SUM(served) AS served, SUM(bytes) AS bytes 160 171 FROM $table WHERE hour_at >= %s GROUP BY ua ORDER BY requests DESC", 161 172 $since … … 170 181 'origin' => (int) $row['origin'], 171 182 'blocked' => (int) $row['blocked'], 183 'probes' => (int) $row['probes'], 172 184 'served' => (int) $row['served'], 173 185 'bytes' => (int) $row['bytes'], … … 182 194 * 183 195 * @param int $hours Window length in hours. 184 * @return array<string,array{blocked:int,p assed:int,served:int}> Keyed by crawler token.196 * @return array<string,array{blocked:int,probes:int,passed:int,served:int}> Keyed by crawler token. 185 197 */ 186 198 public static function recent( $hours ) { … … 189 201 $since = gmdate( 'Y-m-d H:i:s', time() - ( max( 1, (int) $hours ) * HOUR_IN_SECONDS ) ); 190 202 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- our own table. 191 "SELECT ua, SUM(blocked) AS blocked, SUM( cached) + SUM(origin) AS passed, SUM(served) AS served203 "SELECT ua, SUM(blocked) AS blocked, SUM(probes) AS probes, SUM(cached) + SUM(origin) AS passed, SUM(served) AS served 192 204 FROM $table WHERE hour_at >= %s GROUP BY ua", 193 205 $since … … 198 210 $out[ (string) $row['ua'] ] = array( 199 211 'blocked' => (int) $row['blocked'], 212 'probes' => (int) $row['probes'], 200 213 'passed' => (int) $row['passed'], 201 214 'served' => (int) $row['served'], … … 222 235 * 223 236 * @param int $days How far back to read. 224 * @return array<string,array<string,array{requests:int,blocked:int,p assed:int,served:int}>>237 * @return array<string,array<string,array{requests:int,blocked:int,probes:int,passed:int,served:int}>> 225 238 * day (Y-m-d, UTC) => ua => totals. 226 239 */ … … 231 244 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- our own table. 232 245 "SELECT DATE(hour_at) AS day, ua, 233 SUM(requests) AS requests, SUM(blocked) AS blocked, 246 SUM(requests) AS requests, SUM(blocked) AS blocked, SUM(probes) AS probes, 234 247 SUM(cached) + SUM(origin) AS passed, SUM(served) AS served 235 248 FROM $table WHERE hour_at >= %s GROUP BY day, ua ORDER BY day ASC", … … 242 255 'requests' => (int) $row['requests'], 243 256 'blocked' => (int) $row['blocked'], 257 'probes' => (int) $row['probes'], 244 258 'passed' => (int) $row['passed'], 245 259 'served' => (int) $row['served'], -
agentimus/tags/1.51.9/readme.txt
r3750928 r3751893 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.51. 87 Stable tag: 1.51.9 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 298 298 299 299 == Changelog == 300 301 = 1.51.9 = 302 * Fixed: the Cloudflare warning "Cloudflare is blocking [company], but your policy says allow" could fire when Cloudflare was only stopping a scanner that used an AI crawler's name to look for secret files such as ".env". A blocked request for a file no reader asks for is now counted as a probe and no longer counts toward that warning. The edge numbers still show every blocked request, and the first update re-checks the last three days, so a warning a scanner raised in that time clears the next time the numbers update. 300 303 301 304 = 1.51.8 = … … 409 412 == Upgrade Notice == 410 413 414 = 1.51.9 = 415 The Cloudflare warning no longer counts scanners that use an AI crawler's name to look for secret files. Adds one column to the edge table. No breaking changes. 416 411 417 = 1.51.8 = 412 418 On sites with plain permalinks, tag and category pages are named correctly instead of "Homepage". No breaking changes. -
agentimus/tags/1.51.9/uninstall.php
r3662264 r3751893 99 99 delete_option( 'agentimus_cloudflare' ); 100 100 delete_option( 'agentimus_edge_db_version' ); 101 delete_option( 'agentimus_edge_probes_backfilled' ); 101 102 delete_option( 'agentimus_edge_lock' ); 102 103 wp_clear_scheduled_hook( 'agentimus_edge_poll' ); -
agentimus/tags/1.51.9/vendor/composer/installed.php
r3750928 r3751893 2 2 'root' => array( 3 3 'name' => 'heera/agentimus', 4 'pretty_version' => '1.51. 8',5 'version' => '1.51. 8.0',6 'reference' => ' 2f7226fbcc2ef9982ef5a3756711a4e0a86a4655',4 'pretty_version' => '1.51.9', 5 'version' => '1.51.9.0', 6 'reference' => 'b06236c81f3b4ab1aa387dfbe919014919239bad', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', … … 21 21 ), 22 22 'heera/agentimus' => array( 23 'pretty_version' => '1.51. 8',24 'version' => '1.51. 8.0',25 'reference' => ' 2f7226fbcc2ef9982ef5a3756711a4e0a86a4655',23 'pretty_version' => '1.51.9', 24 'version' => '1.51.9.0', 25 'reference' => 'b06236c81f3b4ab1aa387dfbe919014919239bad', 26 26 'type' => 'wordpress-plugin', 27 27 'install_path' => __DIR__ . '/../../', -
agentimus/trunk/agentimus.php
r3750928 r3751893 4 4 * Plugin URI: https://github.com/heera/agentimus 5 5 * Description: Make your site agent-ready (llms.txt, markdown, JSON-LD, a /.well-known discovery document, AI-crawl controls, agent-activity log) AND let the AI tools you use operate it over MCP — read your data and, opt-in, draft/edit/publish posts. Lightweight, no SEO bloat. 6 * Version: 1.51. 86 * Version: 1.51.9 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 7.4 … … 20 20 defined( 'ABSPATH' ) || exit; 21 21 22 define( 'AGENTIMUS_VERSION', '1.51. 8' );22 define( 'AGENTIMUS_VERSION', '1.51.9' ); 23 23 define( 'AGENTIMUS_FILE', __FILE__ ); 24 24 define( 'AGENTIMUS_DIR', plugin_dir_path( __FILE__ ) ); -
agentimus/trunk/inc/Abilities/Registrar.php
r3750405 r3751893 587 587 'origin' => self::i( 'Requests that reached this server.' ), 588 588 'blocked' => self::i( 'Requests the edge turned away without contacting this server.' ), 589 'probes' => self::i( 'Of the blocked requests, how many asked for a file no reader fetches (a .env, a .git folder, a wp-config copy): a scanner using this crawler\'s name. Never counted as the edge turning this crawler away.' ), 589 590 'served' => self::i( 'Requests that got a page: a 2xx the edge let through, not counting /robots.txt reads. Passing the edge is not taking content — a request the origin answered 403 or 404 took nothing.' ), 590 591 'bytes' => self::i(), -
agentimus/trunk/inc/Admin.php
r3750928 r3751893 993 993 // ⭐ A security-only release gets ONE row (1.51.1): the fix is the 994 994 // whole release, and there is one thing an owner may want to do. 995 // ⭐ So does a one-fix release (1.51.8) — plain-permalink term pages 996 // named in the log and the search screens. 995 // ⭐ So does a one-fix release (1.51.9) — a scanner's probes no longer 996 // raise the Cloudflare warning. ⛔ No company is named: the release 997 // text names no specific bot (his ruling, 1.51.3 and 1.51.4). 997 998 array( 998 'icon' => ' page',999 'title' => ' Tag Pages Named on Plain Permalinks',1000 'text' => ' On a site with plain permalinks, the Request Log called every tag and category page Homepage. It now shows the tag or category name, here and in your search results.',999 'icon' => 'shield', 1000 'title' => 'Scanners No Longer Trip the Cloudflare Warning', 1001 'text' => 'A scanner that used an AI crawler\'s name to look for secret files like .env could make it look as if Cloudflare was blocking that company. Those requests are now counted apart, and only real blocks raise the warning.', 1001 1002 ), 1002 1003 ), -
agentimus/trunk/inc/Cloudflare/Client.php
r3722083 r3751893 215 215 216 216 /** 217 * Hourly edge-refused requests for a window, one row per (hour, user agent, 218 * path) — the paths {@see Module::aggregate()} needs to tell a scanner's 219 * probe from a reader being turned away. The filter is the same "blocked" 220 * test as {@see blocked_sources()}. The paths are read here, classified in 221 * the poll and never stored: the table keeps only the count. 222 * 223 * @param string $token API token, plaintext. 224 * @param string $zone_id Zone tag. 225 * @param int $since Window start, Unix UTC. 226 * @param int $until Window end, Unix UTC. 227 * @return array { rows?: array<int,array{hour:string,ua:string,path:string,requests:int}>, error?: string } 228 */ 229 public function blocked_paths( $token, $zone_id, $since, $until ) { 230 $query = 'query ($zone: String!, $since: Time!, $until: Time!) { 231 viewer { 232 zones(filter: { zoneTag: $zone }) { 233 httpRequestsAdaptiveGroups( 234 limit: ' . self::GRAPHQL_LIMIT . ', 235 filter: { datetime_geq: $since, datetime_lt: $until, requestSource: "eyeball", edgeResponseStatus_in: [403, 429], originResponseStatus: 0 }, 236 orderBy: [count_DESC] 237 ) { 238 count 239 dimensions { datetimeHour userAgent clientRequestPath } 240 } 241 } 242 } 243 }'; 244 245 $out = $this->graphql( $token, $query, array( 246 'zone' => (string) $zone_id, 247 'since' => gmdate( 'Y-m-d\TH:i:s\Z', (int) $since ), 248 'until' => gmdate( 'Y-m-d\TH:i:s\Z', (int) $until ), 249 ) ); 250 if ( isset( $out['error'] ) ) { 251 return $out; 252 } 253 254 // The expected container, or it's an error — never "real empty data". 255 if ( ! isset( $out['json']['data']['viewer']['zones'][0]['httpRequestsAdaptiveGroups'] ) ) { 256 return array( 'error' => __( 'Cloudflare returned an unexpected response shape.', 'agentimus' ) ); 257 } 258 259 $rows = array(); 260 foreach ( (array) $out['json']['data']['viewer']['zones'][0]['httpRequestsAdaptiveGroups'] as $g ) { 261 if ( ! isset( $g['dimensions'] ) ) { 262 continue; 263 } 264 $d = (array) $g['dimensions']; 265 $rows[] = array( 266 'hour' => (string) ( isset( $d['datetimeHour'] ) ? $d['datetimeHour'] : '' ), 267 'ua' => (string) ( isset( $d['userAgent'] ) ? $d['userAgent'] : '' ), 268 'path' => (string) ( isset( $d['clientRequestPath'] ) ? $d['clientRequestPath'] : '' ), 269 'requests' => (int) ( isset( $g['count'] ) ? $g['count'] : 0 ), 270 ); 271 } 272 273 return array( 'rows' => $rows ); 274 } 275 276 /** 217 277 * Hourly /robots.txt requests for a window, in the same row shape as 218 278 * {@see hourly_traffic()}. Those rows carry no path, so a crawler reading -
agentimus/trunk/inc/Cloudflare/Conflicts.php
r3722083 r3751893 62 62 * the owner fixing the cause, instead of shouting for a week about history. 63 63 * 64 * @param array $crawlers Per-crawler totals: { ua, name, operator, requests, cached, origin, blocked, served }.64 * @param array $crawlers Per-crawler totals: { ua, name, operator, requests, cached, origin, blocked, probes?, served }. 65 65 * @param array $policy The owner's declarations: 66 66 * - ai_input (bool) robots Content-Signal ai-input. … … 69 69 * - allowed_agents (array) user-agent rules the OWNER allowed in the review queue. 70 70 * @param int $days The window the totals cover, for the copy. 71 * @param array|null $recent Last-24h per-crawler map { ua => { blocked, p assed, served } }71 * @param array|null $recent Last-24h per-crawler map { ua => { blocked, probes?, passed, served } } 72 72 * (see Table::recent()), or null to treat all 73 73 * evidence as current. … … 94 94 continue; 95 95 } 96 $blocked = (int) $c['blocked']; 97 $requests = (int) $c['requests']; 96 // A blocked probe is no reader turned away ({@see refused()}) — and 97 // no reading attempt either, so it leaves the share's base too. 98 $blocked = self::refused( $c ); 99 $requests = (int) $c['requests'] - self::probes( $c ); 98 100 $op = (string) $c['operator']; 99 101 if ( '' === $op || $blocked < self::MIN_BLOCKED || $blocked < $requests * self::MIN_BLOCKED_SHARE ) { … … 103 105 // blocking fires — otherwise the fix already landed and the warning 104 106 // would just be nagging about history. 105 if ( null !== $recent && empty( $recent[ $token ]['blocked']) ) {107 if ( null !== $recent && ( ! isset( $recent[ $token ] ) || 0 === self::refused( $recent[ $token ] ) ) ) { 106 108 continue; 107 109 } … … 111 113 $per_operator[ $op ]['blocked'] += $blocked; 112 114 $per_operator[ $op ]['requests'] += $requests; 113 $per_operator[ $op ]['recent'] += null !== $recent ? (int) $recent[ $token ]['blocked']: 0;115 $per_operator[ $op ]['recent'] += null !== $recent ? self::refused( $recent[ $token ] ) : 0; 114 116 } 115 117 … … 294 296 295 297 /** 298 * The requests the edge turned away that were a READER being refused: the 299 * blocked count less the scanner probes in it ({@see Module::is_probe_path()}). 300 * A row stored before probes were counted has none, and counts as before. 301 * 302 * @param array $row Any totals row carrying `blocked` and optionally `probes`. 303 * @return int 304 */ 305 public static function refused( array $row ) { 306 return max( 0, ( isset( $row['blocked'] ) ? (int) $row['blocked'] : 0 ) - self::probes( $row ) ); 307 } 308 309 /** 310 * The scanner probes within a row's blocked count, never more than it. 311 * 312 * @param array $row Any totals row. 313 * @return int 314 */ 315 private static function probes( array $row ) { 316 $blocked = isset( $row['blocked'] ) ? (int) $row['blocked'] : 0; 317 return min( $blocked, isset( $row['probes'] ) ? max( 0, (int) $row['probes'] ) : 0 ); 318 } 319 320 /** 296 321 * The slug an operator's warn conflict carries in its id 297 322 * ("edge-blocks-<slug>"). One transform, shared with every reader that has … … 330 355 * Pure — no DB, no time() — so it unit-tests standalone, like detect(). 331 356 * 332 * @param array $daily Day => ua => { requests, blocked, p assed, served }, oldest first ({@see Table::daily()}).357 * @param array $daily Day => ua => { requests, blocked, probes?, passed, served }, oldest first ({@see Table::daily()}). 333 358 * @param string $id The conflict id to date. 334 359 * @param array $operators ua => operator name, for the per-operator rollup a warn conflict uses. … … 367 392 continue; 368 393 } 369 $sums['blocked'] += (int) $t['blocked'];370 $sums['requests'] += (int) $t['requests'] ;394 $sums['blocked'] += self::refused( $t ); 395 $sums['requests'] += (int) $t['requests'] - self::probes( $t ); 371 396 } 372 397 return $sums['blocked'] >= self::MIN_BLOCKED -
agentimus/trunk/inc/Cloudflare/Module.php
r3722083 r3751893 152 152 $since = $latest > 0 ? $latest - self::OVERLAP_SECONDS : $now - self::BACKFILL_SECONDS; 153 153 $since = max( $since, $now - self::BACKFILL_SECONDS ); 154 // ⛔ An hour stored before probes were counted keeps saying "no probes" 155 // until it is re-read — and the overlap only re-reads two hours, so a 156 // scanner's morning would go on firing the warning for a day and 157 // inflating the week for seven. Once, re-read everything Cloudflare 158 // still holds; a re-read hour replaces its row. 159 $backfill = ! get_option( Table::PROBES_OPTION ); 160 if ( $backfill ) { 161 $since = $now - self::BACKFILL_SECONDS; 162 } 154 163 155 164 while ( $since < $now ) { … … 164 173 } 165 174 } 175 if ( ! isset( $out['error'] ) ) { 176 // Same rule for the blocked paths: without them every scanner probe 177 // would be stored as a reader turned away. 178 $paths = $this->client->blocked_paths( $token, $zone, $since, $until ); 179 if ( isset( $paths['error'] ) ) { 180 $out = $paths; 181 } 182 } 166 183 if ( isset( $out['error'] ) ) { 167 184 // Keep the last good data; the panel shows the numbers' age and this note. … … 169 186 return; 170 187 } 171 Table::upsert( self::aggregate( (array) $out['rows'], (array) $robots['rows'] ) );188 Table::upsert( self::aggregate( (array) $out['rows'], (array) $robots['rows'], (array) $paths['rows'] ) ); 172 189 $since = $until; 173 190 } 174 191 175 192 $this->settings->record_poll( '' ); 193 if ( $backfill ) { 194 update_option( Table::PROBES_OPTION, 1, false ); 195 } 176 196 Table::prune(); 177 197 … … 200 220 * answers 403 or 404 took nothing. The training notice counts this. 201 221 * 222 * Within `blocked`, `probes` counts the requests for a path no reader asks 223 * for ({@see is_probe_path()}). ⛔⛔ A BLOCKED PROBE IS NOT A READER TURNED 224 * AWAY. On heera.it (2026-10-17) "Cloudflare is blocking Anthropic" and 225 * "…You.com" were one scanner on Google Cloud asking for .env files under 226 * both names, stopped by his own bad-paths rule — the third time after 227 * OpenAI (08-29, 09-17). Neither company publishes addresses to check, so 228 * the spoof check could only say "unknown"; the path says it plainly. 229 * 202 230 * @param array $raw Rows from Client::hourly_traffic(). 203 231 * @param array $robots Rows from Client::robots_fetches() for the same window. 232 * @param array $paths Rows from Client::blocked_paths() for the same window. 204 233 * @return array<int,array> Rows shaped for Table::upsert(). 205 234 */ 206 public static function aggregate( array $raw, array $robots = array() ) {235 public static function aggregate( array $raw, array $robots = array(), array $paths = array() ) { 207 236 $cached_statuses = array( 'hit', 'stale', 'updating', 'revalidated' ); 208 237 $buckets = array(); … … 227 256 'origin' => 0, 228 257 'blocked' => 0, 258 'probes' => 0, 229 259 'served' => 0, 230 260 'bytes' => 0, … … 257 287 } 258 288 289 foreach ( $paths as $row ) { 290 $key = self::hour_key( (string) $row['hour'] ) . '|' . self::crawler_token( (string) $row['ua'] ); 291 if ( isset( $buckets[ $key ] ) && self::is_probe_path( (string) $row['path'] ) ) { 292 $buckets[ $key ]['probes'] += (int) $row['requests']; 293 } 294 } 295 // Two reads of the same window can disagree by a few requests; a probe 296 // count is a share of `blocked` and never more than it. 297 foreach ( $buckets as $key => $bucket ) { 298 $buckets[ $key ]['probes'] = min( $bucket['probes'], $bucket['blocked'] ); 299 } 300 259 301 return array_values( $buckets ); 302 } 303 304 /** 305 * Whether a request path is a scanner's probe — a secret or a config file 306 * that no reader of the site, human or AI, ever asks for. 307 * 308 * Deliberately narrow: hidden files and folders (/.env, /.git/config, 309 * /.aws/credentials — every one except /.well-known/), anything ending in 310 * .env, wp-config.* copies, database dumps and editor backups, phpinfo, and 311 * Vite's /@fs/ file-read hole. An ordinary page, a feed or robots.txt is 312 * never a probe. Decoded first, so /admin%2F.env is caught like /admin/.env. 313 * 314 * @param string $path Request path, as Cloudflare reports it. 315 * @return bool 316 */ 317 public static function is_probe_path( $path ) { 318 $path = strtolower( (string) $path ); 319 for ( $i = 0; $i < 2 && false !== strpos( $path, '%' ); $i++ ) { 320 $path = rawurldecode( $path ); 321 } 322 $path = (string) strtok( $path, '?' ); 323 324 foreach ( explode( '/', str_replace( '\\', '/', $path ) ) as $segment ) { 325 if ( '' === $segment || '.' === $segment || '..' === $segment || '.well-known' === $segment ) { 326 continue; 327 } 328 if ( '.' === $segment[0] || '@fs' === $segment || 0 === strpos( $segment, 'wp-config' ) || 0 === strpos( $segment, 'phpinfo' ) ) { 329 return true; 330 } 331 if ( preg_match( '/\.(env|sql|bak|swp)$/', $segment ) ) { 332 return true; 333 } 334 } 335 return false; 260 336 } 261 337 -
agentimus/trunk/inc/Cloudflare/SpoofCheck.php
r3709876 r3751893 184 184 * @param array $crawlers Rows with { ua, operator }. 185 185 * @param array $policy The declarations {@see Conflicts::detect()} takes. 186 * @param array $recent ua => { blocked, p assed } ({@see Table::recent()}).186 * @param array $recent ua => { blocked, probes?, passed } ({@see Table::recent()}). 187 187 * @return array<string,int> operator => blocked requests. 188 188 */ … … 196 196 } 197 197 $out[ $op ] = ( isset( $out[ $op ] ) ? $out[ $op ] : 0 ) 198 + ( isset( $recent[ $ua ] ['blocked'] ) ? (int) $recent[ $ua ]['blocked']: 0 );198 + ( isset( $recent[ $ua ] ) ? Conflicts::refused( (array) $recent[ $ua ] ) : 0 ); 199 199 } 200 200 return $out; -
agentimus/trunk/inc/Cloudflare/Table.php
r3722083 r3751893 3 3 * Edge aggregates table — one row per (UTC hour, crawler), holding the counts 4 4 * the panel shows: total edge requests, cache-served, origin-served, blocked 5 * at the edge , and bytes out.5 * at the edge (and how many of those were scanner probes), and bytes out. 6 6 * 7 7 * Cloudflare's Free plan forgets these numbers within days; this table is why … … 20 20 21 21 /** @var string Schema version — bump on any structural change. */ 22 const VERSION = ' 2';22 const VERSION = '3'; 23 23 24 24 /** @var string Option recording the installed schema version. */ 25 25 const VERSION_OPTION = 'agentimus_edge_db_version'; 26 27 /** 28 * @var string Option set once a poll has re-read the whole backfill window 29 * since probes began to be counted. Until then every stored hour reads as 30 * no probes, so the poll re-reads as far back as it can 31 * ({@see Module::run_poll()}). Keyed to the count, not the schema version: 32 * a site already at version 3 from a dev build needs it too. 33 */ 34 const PROBES_OPTION = 'agentimus_edge_probes_backfilled'; 26 35 27 36 /** @var int Age-based retention (days). At ~15 crawlers × 24 rows/day this is small. */ … … 75 84 origin int(10) unsigned NOT NULL DEFAULT 0, 76 85 blocked int(10) unsigned NOT NULL DEFAULT 0, 86 probes int(10) unsigned NOT NULL DEFAULT 0, 77 87 served int(10) unsigned NOT NULL DEFAULT 0, 78 88 bytes bigint(20) unsigned NOT NULL DEFAULT 0, … … 85 95 // can silently skip an ADD COLUMN on a drifted schema, and stamping the 86 96 // version anyway would break every query forever. 87 foreach ( array( 'hour_at', 'ua', 'requests', 'cached', 'origin', 'blocked', ' served', 'bytes' ) as $column ) {97 foreach ( array( 'hour_at', 'ua', 'requests', 'cached', 'origin', 'blocked', 'probes', 'served', 'bytes' ) as $column ) { 88 98 $found = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM $table LIKE %s", $column ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from our own prefix helper. 89 99 if ( null === $found ) { … … 100 110 * double-count). 101 111 * 102 * @param array $rows Rows shaped { hour_at, ua, requests, cached, origin, blocked, served, bytes }.112 * @param array $rows Rows shaped { hour_at, ua, requests, cached, origin, blocked, probes?, served, bytes }. 103 113 * @return void 104 114 */ … … 112 122 foreach ( $rows as $row ) { 113 123 $values[] = $wpdb->prepare( 114 '(%s,%s,%d,%d,%d,%d,%d,%d )',124 '(%s,%s,%d,%d,%d,%d,%d,%d,%d)', 115 125 (string) $row['hour_at'], 116 126 (string) $row['ua'], … … 119 129 (int) $row['origin'], 120 130 (int) $row['blocked'], 131 isset( $row['probes'] ) ? (int) $row['probes'] : 0, 121 132 (int) $row['served'], 122 133 (int) $row['bytes'] … … 124 135 } 125 136 $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- every tuple prepared above; table name is our own. 126 "INSERT INTO $table (hour_at,ua,requests,cached,origin,blocked, served,bytes) VALUES " . implode( ',', $values ) . '137 "INSERT INTO $table (hour_at,ua,requests,cached,origin,blocked,probes,served,bytes) VALUES " . implode( ',', $values ) . ' 127 138 ON DUPLICATE KEY UPDATE 128 139 requests = VALUES(requests), cached = VALUES(cached), origin = VALUES(origin), 129 blocked = VALUES(blocked), served = VALUES(served), bytes = VALUES(bytes)'140 blocked = VALUES(blocked), probes = VALUES(probes), served = VALUES(served), bytes = VALUES(bytes)' 130 141 ); 131 142 } … … 148 159 * 149 160 * @param int $days Window length in days. 150 * @return array<int,array{ua:string,requests:int,cached:int,origin:int,blocked:int, served:int,bytes:int}>161 * @return array<int,array{ua:string,requests:int,cached:int,origin:int,blocked:int,probes:int,served:int,bytes:int}> 151 162 */ 152 163 public static function summary( $days ) { … … 157 168 "SELECT ua, 158 169 SUM(requests) AS requests, SUM(cached) AS cached, SUM(origin) AS origin, 159 SUM(blocked) AS blocked, SUM( served) AS served, SUM(bytes) AS bytes170 SUM(blocked) AS blocked, SUM(probes) AS probes, SUM(served) AS served, SUM(bytes) AS bytes 160 171 FROM $table WHERE hour_at >= %s GROUP BY ua ORDER BY requests DESC", 161 172 $since … … 170 181 'origin' => (int) $row['origin'], 171 182 'blocked' => (int) $row['blocked'], 183 'probes' => (int) $row['probes'], 172 184 'served' => (int) $row['served'], 173 185 'bytes' => (int) $row['bytes'], … … 182 194 * 183 195 * @param int $hours Window length in hours. 184 * @return array<string,array{blocked:int,p assed:int,served:int}> Keyed by crawler token.196 * @return array<string,array{blocked:int,probes:int,passed:int,served:int}> Keyed by crawler token. 185 197 */ 186 198 public static function recent( $hours ) { … … 189 201 $since = gmdate( 'Y-m-d H:i:s', time() - ( max( 1, (int) $hours ) * HOUR_IN_SECONDS ) ); 190 202 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- our own table. 191 "SELECT ua, SUM(blocked) AS blocked, SUM( cached) + SUM(origin) AS passed, SUM(served) AS served203 "SELECT ua, SUM(blocked) AS blocked, SUM(probes) AS probes, SUM(cached) + SUM(origin) AS passed, SUM(served) AS served 192 204 FROM $table WHERE hour_at >= %s GROUP BY ua", 193 205 $since … … 198 210 $out[ (string) $row['ua'] ] = array( 199 211 'blocked' => (int) $row['blocked'], 212 'probes' => (int) $row['probes'], 200 213 'passed' => (int) $row['passed'], 201 214 'served' => (int) $row['served'], … … 222 235 * 223 236 * @param int $days How far back to read. 224 * @return array<string,array<string,array{requests:int,blocked:int,p assed:int,served:int}>>237 * @return array<string,array<string,array{requests:int,blocked:int,probes:int,passed:int,served:int}>> 225 238 * day (Y-m-d, UTC) => ua => totals. 226 239 */ … … 231 244 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- our own table. 232 245 "SELECT DATE(hour_at) AS day, ua, 233 SUM(requests) AS requests, SUM(blocked) AS blocked, 246 SUM(requests) AS requests, SUM(blocked) AS blocked, SUM(probes) AS probes, 234 247 SUM(cached) + SUM(origin) AS passed, SUM(served) AS served 235 248 FROM $table WHERE hour_at >= %s GROUP BY day, ua ORDER BY day ASC", … … 242 255 'requests' => (int) $row['requests'], 243 256 'blocked' => (int) $row['blocked'], 257 'probes' => (int) $row['probes'], 244 258 'passed' => (int) $row['passed'], 245 259 'served' => (int) $row['served'], -
agentimus/trunk/readme.txt
r3750928 r3751893 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.51. 87 Stable tag: 1.51.9 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 298 298 299 299 == Changelog == 300 301 = 1.51.9 = 302 * Fixed: the Cloudflare warning "Cloudflare is blocking [company], but your policy says allow" could fire when Cloudflare was only stopping a scanner that used an AI crawler's name to look for secret files such as ".env". A blocked request for a file no reader asks for is now counted as a probe and no longer counts toward that warning. The edge numbers still show every blocked request, and the first update re-checks the last three days, so a warning a scanner raised in that time clears the next time the numbers update. 300 303 301 304 = 1.51.8 = … … 409 412 == Upgrade Notice == 410 413 414 = 1.51.9 = 415 The Cloudflare warning no longer counts scanners that use an AI crawler's name to look for secret files. Adds one column to the edge table. No breaking changes. 416 411 417 = 1.51.8 = 412 418 On sites with plain permalinks, tag and category pages are named correctly instead of "Homepage". No breaking changes. -
agentimus/trunk/uninstall.php
r3662264 r3751893 99 99 delete_option( 'agentimus_cloudflare' ); 100 100 delete_option( 'agentimus_edge_db_version' ); 101 delete_option( 'agentimus_edge_probes_backfilled' ); 101 102 delete_option( 'agentimus_edge_lock' ); 102 103 wp_clear_scheduled_hook( 'agentimus_edge_poll' ); -
agentimus/trunk/vendor/composer/installed.php
r3750928 r3751893 2 2 'root' => array( 3 3 'name' => 'heera/agentimus', 4 'pretty_version' => '1.51. 8',5 'version' => '1.51. 8.0',6 'reference' => ' 2f7226fbcc2ef9982ef5a3756711a4e0a86a4655',4 'pretty_version' => '1.51.9', 5 'version' => '1.51.9.0', 6 'reference' => 'b06236c81f3b4ab1aa387dfbe919014919239bad', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', … … 21 21 ), 22 22 'heera/agentimus' => array( 23 'pretty_version' => '1.51. 8',24 'version' => '1.51. 8.0',25 'reference' => ' 2f7226fbcc2ef9982ef5a3756711a4e0a86a4655',23 'pretty_version' => '1.51.9', 24 'version' => '1.51.9.0', 25 'reference' => 'b06236c81f3b4ab1aa387dfbe919014919239bad', 26 26 'type' => 'wordpress-plugin', 27 27 'install_path' => __DIR__ . '/../../',
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)