PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.4.1
Defender Security – Malware Scanner, Login Security & Firewall v5.4.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-bot-trap.php

class-bot-trap.php in Defender Security – Malware Scanner, Login Security & Firewall 5.4.1, at src/component/class-bot-trap.php

257 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles bot trap functionality.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_Defender\Component;
11 use WP_Defender\Model\Setting\User_Agent_Lockout;
12 use WP_Defender\Model\Lockout_Log;
13 use WP_Defender\Traits\Country;
14
15 /**
16 * Handles operations to insert a weekly rotating hash URL into the footer,
17 * and blocking IP addresses that access this URL.
18 */
19 class Bot_Trap extends Component {
20 use Country;
21
22 public const URL_HASH_KEY = 'wpdef_bot_trap_url_hash';
23 public const SCENARIO_BOT_TRAP = 'bot_trap';
24
25 /**
26 * The path to the robots.txt file.
27 *
28 * @var string
29 */
30 private $robots_file;
31
32 /**
33 * The start comment for the robots.txt.
34 *
35 * @var string
36 */
37 private $start_comment = '# WP Defender - Begin';
38
39 /**
40 * The end comment for the robots.txt.
41 *
42 * @var string
43 */
44 private $end_comment = '# WP Defender - End';
45
46 /**
47 * The model for handling the data.
48 *
49 * @var User_Agent_Lockout
50 */
51 protected $model;
52
53 /**
54 * Constructor for the Bot_Trap class.
55 * Initializes the model and sets the path to the robots.txt file.
56 *
57 * @param User_Agent_Lockout $model The model instance for bot trap functionality.
58 */
59 public function __construct( User_Agent_Lockout $model ) {
60 $this->model = $model;
61 $this->robots_file = ABSPATH . 'robots.txt';
62 }
63
64 /**
65 * Check if the bot trap is enabled.
66 */
67 public function is_enabled(): bool {
68 return $this->model->enabled && $this->model->bot_trap_enabled;
69 }
70
71 /**
72 * Get the hash for the bot trap URL.
73 *
74 * @return string|false The hash if set, false otherwise.
75 */
76 public function get_hash() {
77 return get_site_option( self::URL_HASH_KEY, false );
78 }
79
80 /**
81 * Set the hash for the bot trap URL.
82 *
83 * @param string $hash The hash to set, should be a 16-character hexadecimal string.
84 */
85 private function set_hash( string $hash ) {
86 update_site_option( self::URL_HASH_KEY, $hash );
87 }
88
89 /**
90 * Rotate the hash for the bot trap URL.
91 */
92 public function rotate_hash() {
93 $new_hash = $this->generate_hash();
94 $this->set_hash( $new_hash );
95
96 flush_rewrite_rules();
97
98 $this->remove_rule();
99 $this->inject_rule();
100 }
101
102 /**
103 * Generate a new hash for the bot trap URL.
104 *
105 * @return string|false A 16-character hexadecimal string or false on failure.
106 */
107 private function generate_hash() {
108 return substr( bin2hex( Crypt::random_bytes( 32 ) ), 0, 16 );
109 }
110
111 /**
112 * Handle the robots.txt rule.
113 */
114 public function handle_robots_txt() {
115 if ( $this->is_enabled() ) {
116 $this->inject_rule();
117 } else {
118 $this->remove_rule();
119 }
120 }
121
122 /**
123 * Get rule block for robots.txt.
124 *
125 * @param string $eol The end-of-line character to use, defaults to "\n".
126 *
127 * @return string The block of text to be added to the robots.txt file.
128 */
129 public function get_block( string $eol = "\n" ): string {
130 $disallow_path = $this->get_hash();
131
132 return $this->start_comment . $eol .
133 'User-agent: *' . $eol .
134 "Disallow: {$disallow_path}" . $eol .
135 $this->end_comment;
136 }
137
138 /**
139 * Inject the bot trap rule into the robots.txt.
140 */
141 public function inject_rule() {
142 global $wp_filesystem;
143 // Initialize the WP filesystem, no more using 'file-put-contents' function.
144 if ( empty( $wp_filesystem ) ) {
145 require_once ABSPATH . '/wp-admin/includes/file.php';
146 WP_Filesystem();
147 }
148
149 if ( $wp_filesystem->exists( $this->robots_file ) ) {
150 // Inject rule into physical robots.txt file.
151 $contents = $wp_filesystem->get_contents( $this->robots_file );
152 $eol = $this->detect_line_ending( $contents );
153
154 if ( strpos( $contents, $this->start_comment ) === false ) {
155 $block = $eol . $this->get_block( $eol ) . $eol;
156 $contents .= $block;
157
158 $dir_name = pathinfo( $this->robots_file, PATHINFO_DIRNAME );
159 if ( $wp_filesystem->is_writable( $dir_name ) ) {
160 $wp_filesystem->put_contents( $this->robots_file, $contents );
161 }
162 }
163 } else {
164 // Inject virtual rule via filter.
165 add_filter(
166 'robots_txt',
167 function ( $output ) {
168 return $output . "\n" . $this->get_block();
169 },
170 10,
171 2
172 );
173 }
174 }
175
176 /**
177 * Remove the bot trap rule from the robots.txt.
178 */
179 public function remove_rule() {
180 global $wp_filesystem;
181 // Initialize the WP filesystem, no more using 'file-put-contents' function.
182 if ( empty( $wp_filesystem ) ) {
183 require_once ABSPATH . '/wp-admin/includes/file.php';
184 WP_Filesystem();
185 }
186
187 if ( ! $wp_filesystem->exists( $this->robots_file ) ) {
188 return;
189 }
190
191 $contents = $wp_filesystem->get_contents( $this->robots_file );
192 $eol = $this->detect_line_ending( $contents );
193
194 // Remove only the plugin block.
195 $pattern = '/\\s*' . preg_quote( $this->start_comment, '/' ) .
196 '.*?' . preg_quote( $this->end_comment, '/' ) . '\\s*/s';
197
198 $new_contents = preg_replace( $pattern, '', $contents );
199 $new_contents = trim( $new_contents );
200
201 if ( '' === $new_contents ) {
202 // File only had plugin block — delete it.
203 wp_delete_file( $this->robots_file );
204 } else {
205 $dir_name = pathinfo( $this->robots_file, PATHINFO_DIRNAME );
206 if ( $wp_filesystem->is_writable( $dir_name ) ) {
207 $wp_filesystem->put_contents( $this->robots_file, $new_contents . $eol );
208 }
209 }
210 }
211
212 /**
213 * Log the event into db, we will use the data in logs page later.
214 *
215 * @param string $ip The IP address involved in the event.
216 * @param string $uri The URI that was accessed.
217 * @param string $scenario The scenario under which the event is logged.
218 */
219 public function log_event( $ip, $uri, $scenario ) {
220 $model = new Lockout_Log();
221 $model->ip = $ip;
222 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
223 $model->user_agent = isset( $user_agent ) ? User_Agent::fast_cleaning( $user_agent ) : null;
224 $model->date = time();
225 $model->tried = $uri;
226 $model->blog_id = get_current_blog_id();
227
228 $ip_to_country = $this->ip_to_country( $ip );
229
230 if ( ! empty( $ip_to_country ) && isset( $ip_to_country['iso'] ) ) {
231 $model->country_iso_code = $ip_to_country['iso'];
232 }
233
234 switch ( $scenario ) {
235 case self::SCENARIO_BOT_TRAP:
236 default:
237 $model->type = Lockout_Log::LOCKOUT_BOT_TRAP;
238 $model->log = esc_html__( 'Locked out by Bot Trap', 'defender-security' );
239 break;
240 }
241 $model->save();
242 }
243
244 /**
245 * Creates a lockout for a blocked IP.
246 *
247 * @param Lockout_Ip $model The lockout IP model.
248 * @param string $message The lockout message.
249 * @param int $time The timestamp when the lockout will be lifted.
250 */
251 public function create_blocked_lockout( &$model, $message, $time ) {
252 $model->lockout_message = $message;
253 $model->release_time = $time;
254 $model->save();
255 }
256 }
257