PluginProbe ʕ •ᴥ•ʔ
Wordfence Security – Firewall, Malware Scan, and Login Security / 8.2.2
Wordfence Security – Firewall, Malware Scan, and Login Security v8.2.2
8.2.2 8.2.1 8.2.0 3.7.1 3.7.2 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.8.8 3.8.9 3.9.1 4.0.1 4.0.2 4.0.3 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.0.9 5.1.1 5.1.2 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.1 5.3.10 5.3.11 5.3.12 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8 5.3.9 6.0.1 6.0.10 6.0.11 6.0.12 6.0.14 6.0.15 6.0.16 6.0.17 6.0.18 6.0.19 6.0.2 6.0.20 6.0.21 6.0.22 6.0.23 6.0.24 6.0.25 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.0.8 6.0.9 6.1.1 6.1.10 6.1.11 6.1.12 6.1.14 6.1.15 6.1.16 6.1.17 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.1.9 6.2.0 6.2.1 6.2.10 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.7 6.2.8 6.2.9 6.3.0 6.3.1 6.3.10 6.3.11 6.3.12 6.3.14 6.3.15 6.3.16 6.3.17 6.3.18 6.3.19 6.3.2 6.3.20 6.3.21 6.3.22 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.3.8 6.3.9 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1.0 7.1.1 7.1.10 7.1.11 7.1.12 7.1.14 7.1.15 7.1.16 7.1.17 7.1.18 7.1.19 7.1.2 7.1.20 7.1.3 7.1.4 7.1.5 7.1.6 7.1.7 7.1.8 7.1.9 7.10.0 7.10.1 7.10.2 7.10.3 7.10.4 7.10.5 7.10.6 7.10.7 7.11.0 7.11.1 7.11.2 7.11.3 7.11.4 7.11.5 7.11.6 7.11.7 7.2.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.3.6 7.4.0 7.4.1 7.4.10 7.4.11 7.4.12 7.4.14 7.4.2 7.4.3 trunk 7.4.4 1.1 7.4.5 1.2 7.4.6 1.3 7.4.7 1.3.1 7.4.8 1.3.2 7.4.9 1.3.3 7.5.0 1.4.2 7.5.1 1.4.3 7.5.10 1.4.4 7.5.11 1.4.5 7.5.2 1.4.6 7.5.3 1.4.7 7.5.4 1.4.8 7.5.5 1.5.1 7.5.6 1.5.2 7.5.7 1.5.3 7.5.8 1.5.4 7.5.9 1.5.5 7.6.0 1.5.6 7.6.1 2.0.1 7.6.2 2.0.2 7.7.0 2.0.3 7.7.1 2.0.5 7.8.0 2.0.6 7.8.1 2.0.7 7.8.2 2.1.0 7.9.0 2.1.1 7.9.1 2.1.2 7.9.2 2.1.3 7.9.3 2.1.4 8.0.0 2.1.5 8.0.1 3.0.2 8.0.2 3.0.3 8.0.3 3.0.4 8.0.4 3.0.5 8.0.5 3.0.6 8.1.0 3.0.7 8.1.1 3.0.8 8.1.2 3.0.9 8.1.3 3.1.0 8.1.4 3.1.1 v1.4.1 3.1.2 3.1.4 3.1.6 3.2.1 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.1 3.4.4 3.4.5 3.5.1 3.5.2 3.6.1 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.6.9
wordfence / models / scanner / wfScanner.php
wordfence / models / scanner Last commit date
wfScanner.php 1 month ago
wfScanner.php
1382 lines
1 <?php
2
3 class wfScanner {
4 const SCAN_TYPE_QUICK = 'quick';
5 const SCAN_TYPE_LIMITED = 'limited';
6 const SCAN_TYPE_STANDARD = 'standard';
7 const SCAN_TYPE_HIGH_SENSITIVITY = 'highsensitivity';
8 const SCAN_TYPE_CUSTOM = 'custom';
9
10 const SCAN_SCHEDULING_MODE_AUTOMATIC = 'auto';
11 const SCAN_SCHEDULING_MODE_MANUAL = 'manual';
12
13 const MANUAL_SCHEDULING_ONCE_DAILY = 'onceDaily';
14 const MANUAL_SCHEDULING_TWICE_DAILY = 'twiceDaily';
15 const MANUAL_SCHEDULING_EVERY_OTHER_DAY = 'everyOtherDay';
16 const MANUAL_SCHEDULING_WEEKDAYS = 'weekdays';
17 const MANUAL_SCHEDULING_WEEKENDS = 'weekends';
18 const MANUAL_SCHEDULING_ODD_DAYS_WEEKENDS = 'oddDaysWE';
19 const MANUAL_SCHEDULING_CUSTOM = 'custom';
20
21 const SIGNATURE_MODE_PREMIUM = 'premium';
22 const SIGNATURE_MODE_COMMUNITY = 'community';
23
24 const STATUS_PENDING = 'pending';
25 const STATUS_RUNNING = 'running';
26 const STATUS_RUNNING_WARNING = 'running-warning';
27 const STATUS_COMPLETE_SUCCESS = 'complete-success';
28 const STATUS_COMPLETE_WARNING = 'complete-warning';
29 const STATUS_PREMIUM = 'premium';
30 const STATUS_DISABLED = 'disabled';
31
32 const STAGE_SPAMVERTISING_CHECKS = 'spamvertising';
33 const STAGE_SPAM_CHECK = 'spam';
34 const STAGE_BLACKLIST_CHECK = 'blacklist';
35 const STAGE_SERVER_STATE = 'server';
36 const STAGE_FILE_CHANGES = 'changes';
37 const STAGE_PUBLIC_FILES = 'public';
38 const STAGE_MALWARE_SCAN = 'malware';
39 const STAGE_CONTENT_SAFETY = 'content';
40 const STAGE_PASSWORD_STRENGTH = 'password';
41 const STAGE_VULNERABILITY_SCAN = 'vulnerability';
42 const STAGE_OPTIONS_AUDIT = 'options';
43
44 const SUMMARY_TOTAL_USERS = 'totalUsers';
45 const SUMMARY_TOTAL_PAGES = 'totalPages';
46 const SUMMARY_TOTAL_POSTS = 'totalPosts';
47 const SUMMARY_TOTAL_COMMENTS = 'totalComments';
48 const SUMMARY_TOTAL_CATEGORIES = 'totalCategories';
49 const SUMMARY_TOTAL_TABLES = 'totalTables';
50 const SUMMARY_TOTAL_ROWS = 'totalRows';
51 const SUMMARY_SCANNED_POSTS = 'scannedPosts';
52 const SUMMARY_SCANNED_COMMENTS = 'scannedComments';
53 const SUMMARY_SCANNED_FILES = 'scannedFiles';
54 const SUMMARY_SCANNED_PLUGINS = 'scannedPlugins';
55 const SUMMARY_SCANNED_THEMES = 'scannedThemes';
56 const SUMMARY_SCANNED_USERS = 'scannedUsers';
57 const SUMMARY_SCANNED_URLS = 'scannedURLs';
58
59 const CENTRAL_STAGE_UPDATE_THRESHOLD = 5;
60
61 private $_scanType = false;
62
63 private $_summary = false;
64 private $_destructRegistered = false;
65 private $_dirty = false;
66
67 /**
68 * Returns the singleton wfScanner with the user-configured scan type set.
69 *
70 * @return wfScanner
71 */
72 public static function shared() {
73 static $_scanner = null;
74 if ($_scanner === null) {
75 $_scanner = new wfScanner();
76 }
77 return $_scanner;
78 }
79
80 /**
81 * Schedules a cron rescheduling to happen at the end of the current process's execution.
82 */
83 public static function setNeedsRescheduling() {
84 static $willReschedule = false;
85 if (!$willReschedule) {
86 $willReschedule = true;
87 register_shutdown_function(array(self::shared(), 'scheduleScans'));
88 }
89 }
90
91 /**
92 * Returns whether or not the scan type passed is valid.
93 *
94 * @param $type
95 * @return bool
96 */
97 public static function isValidScanType($type) {
98 switch ($type) {
99 case self::SCAN_TYPE_QUICK:
100 case self::SCAN_TYPE_LIMITED:
101 case self::SCAN_TYPE_HIGH_SENSITIVITY:
102 case self::SCAN_TYPE_CUSTOM:
103 case self::SCAN_TYPE_STANDARD:
104 return true;
105 }
106 return false;
107 }
108
109 /**
110 * Returns the display string for the given type.
111 *
112 * @param string $type
113 * @return string
114 */
115 public static function displayScanType($type) {
116 switch ($type) {
117 case self::SCAN_TYPE_QUICK:
118 return __('Quick', 'wordfence');
119 case self::SCAN_TYPE_LIMITED:
120 return __('Limited', 'wordfence');
121 case self::SCAN_TYPE_HIGH_SENSITIVITY:
122 return __('High Sensitivity', 'wordfence');
123 case self::SCAN_TYPE_CUSTOM:
124 return __('Custom', 'wordfence');
125 case self::SCAN_TYPE_STANDARD:
126 default:
127 return __('Standard', 'wordfence');
128 }
129 }
130
131 /**
132 * Returns the display detail string for the given type.
133 *
134 * @param string $type
135 * @return string
136 */
137 public static function displayScanTypeDetail($type) {
138 switch ($type) {
139 case self::SCAN_TYPE_QUICK:
140 case self::SCAN_TYPE_LIMITED:
141 return __('Low resource utilization, limited detection capability', 'wordfence');
142 case self::SCAN_TYPE_HIGH_SENSITIVITY:
143 return __('Standard detection capability, chance of false positives', 'wordfence');
144 case self::SCAN_TYPE_CUSTOM:
145 return __('Custom scan options selected', 'wordfence');
146 case self::SCAN_TYPE_STANDARD:
147 default:
148 return __('Standard detection capability', 'wordfence');
149 }
150 }
151
152 /**
153 * Returns an array of the scan options (as keys) and the corresponding value for the quick scan type. All omitted
154 * scan stages are considered disabled.
155 *
156 * @return array
157 */
158 public static function quickScanTypeOptions() {
159 $oldVersions = true;
160 $wafStatus = true;
161 if (wfConfig::get('scanType') == self::SCAN_TYPE_CUSTOM) { //Obey the setting in custom if that's the true scan type
162 $oldVersions = wfConfig::get('scansEnabled_oldVersions');
163 $wafStatus = wfConfig::get('scansEnabled_wafStatus');
164 }
165
166 return array_merge(self::_inactiveScanOptions(), array(
167 'scansEnabled_oldVersions' => $oldVersions,
168 'scansEnabled_wafStatus' => $wafStatus,
169 ));
170 }
171
172 /**
173 * Returns an array of the scan options (as keys) and the corresponding value for the limited scan type.
174 *
175 * @return array
176 */
177 public static function limitedScanTypeOptions() {
178 return array_merge(self::_inactiveScanOptions(), array(
179 'scansEnabled_checkHowGetIPs' => true,
180 'scansEnabled_malware' => true,
181 'scansEnabled_fileContents' => true,
182 'scansEnabled_fileContentsGSB' => true,
183 'scansEnabled_suspiciousOptions' => true,
184 'scansEnabled_oldVersions' => true,
185 'scansEnabled_wafStatus' => true,
186 'lowResourceScansEnabled' => true,
187 'scan_exclude' => wfConfig::get('scan_exclude', ''),
188 'scan_include_extra' => wfConfig::get('scan_include_extra', ''),
189 'scansEnabled_geoipSupport' => true,
190 ));
191 }
192
193 /**
194 * Returns an array of the scan options (as keys) and the corresponding value for the standard scan type.
195 *
196 * @return array
197 */
198 public static function standardScanTypeOptions() {
199 return array_merge(self::_inactiveScanOptions(), array(
200 'spamvertizeCheck' => true,
201 'checkSpamIP' => true,
202 'scansEnabled_checkGSB' => true,
203 'scansEnabled_checkHowGetIPs' => true,
204 'scansEnabled_checkReadableConfig' => true,
205 'scansEnabled_suspectedFiles' => true,
206 'scansEnabled_core' => true,
207 'scansEnabled_coreUnknown' => true,
208 'scansEnabled_malware' => true,
209 'scansEnabled_fileContents' => true,
210 'scansEnabled_fileContentsGSB' => true,
211 'scansEnabled_posts' => true,
212 'scansEnabled_comments' => true,
213 'scansEnabled_suspiciousOptions' => true,
214 'scansEnabled_oldVersions' => true,
215 'scansEnabled_suspiciousAdminUsers' => true,
216 'scansEnabled_passwds' => true,
217 'scansEnabled_diskSpace' => true,
218 'scansEnabled_wafStatus' => true,
219 'scan_exclude' => wfConfig::get('scan_exclude', ''),
220 'scan_include_extra' => wfConfig::get('scan_include_extra', ''),
221 'scansEnabled_geoipSupport' => true,
222 ));
223 }
224
225 /**
226 * Returns an array of the scan options (as keys) and the corresponding value for the high sensitivity scan type.
227 *
228 * @return array
229 */
230 public static function highSensitivityScanTypeOptions() {
231 return array_merge(self::_inactiveScanOptions(), array(
232 'spamvertizeCheck' => true,
233 'checkSpamIP' => true,
234 'scansEnabled_checkGSB' => true,
235 'scansEnabled_checkHowGetIPs' => true,
236 'scansEnabled_checkReadableConfig' => true,
237 'scansEnabled_suspectedFiles' => true,
238 'scansEnabled_core' => true,
239 'scansEnabled_themes' => true,
240 'scansEnabled_plugins' => true,
241 'scansEnabled_coreUnknown' => true,
242 'scansEnabled_malware' => true,
243 'scansEnabled_fileContents' => true,
244 'scansEnabled_fileContentsGSB' => true,
245 'scansEnabled_posts' => true,
246 'scansEnabled_comments' => true,
247 'scansEnabled_suspiciousOptions' => true,
248 'scansEnabled_oldVersions' => true,
249 'scansEnabled_suspiciousAdminUsers' => true,
250 'scansEnabled_passwds' => true,
251 'scansEnabled_diskSpace' => true,
252 'scansEnabled_wafStatus' => true,
253 'other_scanOutside' => true,
254 'scansEnabled_scanImages' => true,
255 'scan_exclude' => wfConfig::get('scan_exclude', ''),
256 'scan_include_extra' => wfConfig::get('scan_include_extra', ''),
257 'scansEnabled_geoipSupport' => true,
258 ));
259 }
260
261 /**
262 * Returns an array of the scan options (as keys) and the corresponding value for the custom scan type.
263 *
264 * @return array
265 */
266 public static function customScanTypeOptions() {
267 $allOptions = self::_inactiveScanOptions();
268 foreach ($allOptions as $key => &$value) {
269 $value = wfConfig::get($key);
270 }
271
272 $allOptions['scansEnabled_geoipSupport'] = true;
273 $allOptions['scansEnabled_highSense'] = false; //deprecated
274
275 return $allOptions;
276 }
277
278 /**
279 * Returns an array of scan options and their inactive values for convenience in merging with the various scan type
280 * option arrays.
281 *
282 * @return array
283 */
284 protected static function _inactiveScanOptions() {
285 return array(
286 'spamvertizeCheck' => false,
287 'checkSpamIP' => false,
288 'scansEnabled_checkGSB' => false,
289 'scansEnabled_checkHowGetIPs' => false,
290 'scansEnabled_checkReadableConfig' => false,
291 'scansEnabled_suspectedFiles' => false,
292 'scansEnabled_core' => false,
293 'scansEnabled_themes' => false,
294 'scansEnabled_plugins' => false,
295 'scansEnabled_coreUnknown' => false,
296 'scansEnabled_malware' => false,
297 'scansEnabled_fileContents' => false,
298 'scan_include_extra' => '',
299 'scansEnabled_fileContentsGSB' => false,
300 'scansEnabled_posts' => false,
301 'scansEnabled_comments' => false,
302 'scansEnabled_suspiciousOptions' => false,
303 'scansEnabled_oldVersions' => false,
304 'scansEnabled_suspiciousAdminUsers' => false,
305 'scansEnabled_passwds' => false,
306 'scansEnabled_diskSpace' => false,
307 'scansEnabled_wafStatus' => false,
308 'other_scanOutside' => false,
309 'scansEnabled_scanImages' => false,
310 'scansEnabled_highSense' => false,
311 'lowResourceScansEnabled' => false,
312 'scan_exclude' => '',
313 'scansEnabled_geoipSupport' => false,
314 );
315 }
316
317 /**
318 * Returns the scan options only available to premium users.
319 *
320 * @return array
321 */
322 protected static function _premiumScanOptions() {
323 return array('spamvertizeCheck', 'checkSpamIP', 'scansEnabled_checkGSB');
324 }
325
326 /**
327 * Returns an array of weights for calculating the scan option status score.
328 *
329 * @return array
330 */
331 protected static function _scanOptionWeights() {
332 return array(
333 'spamvertizeCheck' => 0.05,
334 'checkSpamIP' => 0.05,
335 'scansEnabled_checkGSB' => 0.05,
336 'scansEnabled_checkHowGetIPs' => 0.05,
337 'scansEnabled_checkReadableConfig' => 0.05,
338 'scansEnabled_suspectedFiles' => 0.05,
339 'scansEnabled_core' => 0.05,
340 'scansEnabled_themes' => 0,
341 'scansEnabled_plugins' => 0,
342 'scansEnabled_coreUnknown' => 0.05,
343 'scansEnabled_malware' => 0.05,
344 'scansEnabled_fileContents' => 0.1,
345 'scan_include_extra' => 0,
346 'scansEnabled_fileContentsGSB' => 0.05,
347 'scansEnabled_posts' => 0.05,
348 'scansEnabled_comments' => 0.05,
349 'scansEnabled_suspiciousOptions' => 0.05,
350 'scansEnabled_oldVersions' => 0.1,
351 'scansEnabled_suspiciousAdminUsers' => 0.05,
352 'scansEnabled_passwds' => 0.05,
353 'scansEnabled_diskSpace' => 0.05,
354 'other_scanOutside' => 0,
355 'scansEnabled_scanImages' => 0,
356 'scansEnabled_highSense' => 0,
357 'lowResourceScansEnabled' => 0,
358 'scan_exclude' => 0,
359 'scansEnabled_geoipSupport' => 0,
360 'scansEnabled_wafStatus' => 0,
361 );
362 }
363
364 /**
365 * wfScanner constructor.
366 * @param int|bool $scanType If false, defaults to the config option `scanType`.
367 */
368 public function __construct($scanType = false) {
369 if ($scanType === false || !self::isValidScanType($scanType)) {
370 $this->_scanType = wfConfig::get('scanType');
371 }
372 else {
373 $this->_scanType = $scanType;
374 }
375 }
376
377 /**
378 * Returns whether or not the scanner will run as premium.
379 *
380 * @return bool
381 */
382 public function isPremiumScan() {
383 return !!wfConfig::get('isPaid');
384 }
385
386 /**
387 * Returns whether or not automatic scans will run.
388 *
389 * @return bool
390 */
391 public function isEnabled() {
392 return !!wfConfig::get('scheduledScansEnabled');
393 }
394
395 /**
396 * Returns whether or not a scan is running. A scan is considered running if the timestamp
397 * under wf_scanRunning is within WORDFENCE_MAX_SCAN_LOCK_TIME seconds of now.
398 *
399 * @return bool
400 */
401 public function isRunning() {
402 $scanRunning = wfConfig::get('wf_scanRunning');
403 return ($scanRunning && time() - $scanRunning < WORDFENCE_MAX_SCAN_LOCK_TIME);
404 }
405
406 /**
407 * Returns the current scan scheduling mode.
408 *
409 * @return string One of the SCAN_SCHEDULING_MODE_ constants
410 */
411 public function schedulingMode() {
412 if (wfConfig::get('isPaid') && wfConfig::get('schedMode') == 'manual') {
413 return self::SCAN_SCHEDULING_MODE_MANUAL;
414 }
415 return self::SCAN_SCHEDULING_MODE_AUTOMATIC;
416 }
417
418 /**
419 * Returns the manual scheduling type. This is only applicable when the scheduling mode is
420 * SCAN_SCHEDULING_MODE_MANUAL.
421 *
422 * @return string One of the MANUAL_SCHEDULING_ constants.
423 */
424 public function manualSchedulingType() {
425 return wfConfig::get('manualScanType', self::MANUAL_SCHEDULING_ONCE_DAILY);
426 }
427
428 /**
429 * Returns the start hour used for non-custom manual schedules. This is initially random but may be modified
430 * by the user later.
431 *
432 * @return int An hour number.
433 */
434 public function manualSchedulingStartHour() {
435 return wfConfig::get('schedStartHour');
436 }
437
438 /**
439 * Returns the currently defined custom schedule. This is only applicable when the scheduling mode is
440 * SCAN_SCHEDULING_MODE_MANUAL and the manual type is set to MANUAL_SCHEDULING_CUSTOM.
441 *
442 * @return array The array will be of the format array(0 => array(0 => false, 1 => false ... 23 => false), ... 6 => array(...))
443 */
444 public function customSchedule() {
445 $normalizedSchedule = array_fill(0, 7, array_fill(0, 24, false));
446 $storedSchedule = wfConfig::get_ser('scanSched', array());
447 if (is_array($storedSchedule) && !empty($storedSchedule) && is_array($storedSchedule[0])) {
448 foreach ($storedSchedule as $dayNumber => $day) {
449 foreach ($day as $hourNumber => $enabled) {
450 $normalizedSchedule[$dayNumber][$hourNumber] = wfUtils::truthyToBoolean($enabled);
451 }
452 }
453 }
454 return $normalizedSchedule;
455 }
456
457 public function shouldRunQuickScan() {
458 if (!$this->isEnabled()) {
459 return false;
460 }
461
462 if (time() - $this->lastQuickScanTime() < 79200) { //Do not run within 22 hours of a completed quick scan
463 return false;
464 }
465
466 $lastFullScanCompletion = (int) $this->lastScanTime();
467 if (time() - $lastFullScanCompletion < 43200) { //Do not run within 12 hours of a completed full scan
468 return false;
469 }
470
471 $nextFullScan = $this->nextScheduledScanTime();
472 if ($nextFullScan === false || $nextFullScan - time() < 3600) { //Scans are not running (e.g., custom schedule selected with no times configured) or if scheduled, then avoid running within 1 hour of a pending full scan
473 return false;
474 }
475
476 $now = time();
477 $tzOffset = wfUtils::formatLocalTime('Z', $now);
478 $currentDayOfWeekUTC = date('w', $now);
479 $currentHourUTC = date('G', $now);
480 $preferredHourUTC = false;
481
482 if ($this->schedulingMode() == wfScanner::SCAN_SCHEDULING_MODE_MANUAL) {
483 $manualType = $this->manualSchedulingType();
484 $preferredHourUTC = fmod(round(($this->manualSchedulingStartHour() * 3600 - $tzOffset) / 3600, 2), 24); //round() rather than floor() to account for fractional time zones
485 switch ($manualType) {
486 case self::MANUAL_SCHEDULING_ONCE_DAILY:
487 case self::MANUAL_SCHEDULING_EVERY_OTHER_DAY:
488 case self::MANUAL_SCHEDULING_WEEKDAYS:
489 case self::MANUAL_SCHEDULING_WEEKENDS:
490 case self::MANUAL_SCHEDULING_ODD_DAYS_WEEKENDS:
491 $preferredHourUTC = fmod($preferredHourUTC + 12, 24);
492 break;
493 case self::MANUAL_SCHEDULING_TWICE_DAILY:
494 $preferredHourUTC = fmod($preferredHourUTC + 6, 24); //When automatic scans run twice daily, possibly run a quick scan 6 hours offset (will only run if either scheduled one fails for some reason)
495 break;
496 case self::MANUAL_SCHEDULING_CUSTOM: //Iterate from the current day backwards and base it on the first time found, may or may not actually run depending on the spacing of the custom schedule
497 $preferredHourUTC = false;
498 $oneWeekSchedule = $this->customSchedule();
499 for ($i = 7; $i > 0; $i--) { //Sample sequence for `$currentDayOfWeekUTC == 2` => 2, 1, 0, 6, 5, 4, 3
500 $checkingDayNumber = ($currentDayOfWeekUTC + $i) % 7;
501 $day = $oneWeekSchedule[$checkingDayNumber];
502 $dayHour = array_search(true, $day);
503 if ($dayHour !== false) {
504 $preferredHourUTC = fmod(round(($dayHour * 3600 - $tzOffset) / 3600, 2) + 12, 24);
505 break;
506 }
507 }
508 break;
509 }
510
511 if ($preferredHourUTC !== false) { //The preferred hour of a manual scan schedule has been determined, run the quick scan at the desired offset if we're in that hour
512 return ($currentHourUTC >= $preferredHourUTC);
513 }
514 }
515
516 $noc1ScanSchedule = wfConfig::get_ser('noc1ScanSchedule', array());
517 if (count($noc1ScanSchedule)) {
518 $preferredHourUTC = (int) fmod( (fmod($noc1ScanSchedule[0], 86400) / 3600) + 12, 24);
519 return ($currentHourUTC >= $preferredHourUTC);
520 }
521
522 return false; //If we've reached this point, the scan config is in a weird state so just skip the quick scan
523 }
524
525 /**
526 * Returns an associative array containing the current state each scan stage and its corresponding status.
527 *
528 * @return array
529 */
530 public function stageStatus() {
531 $status = $this->_defaultStageStatuses();
532 $runningStatus = wfConfig::get_ser('scanStageStatuses', array(), false);
533 $status = array_merge($status, $runningStatus);
534
535 foreach ($status as $stage => &$value) { //Convert value array into status only
536 $value = $value['status'];
537 if (!$this->isRunning() && $value == self::STATUS_RUNNING) {
538 $value = self::STATUS_PENDING;
539 }
540 }
541
542 return $status;
543 }
544
545 /**
546 * Returns an array of all scan options for the given stage that are enabled.
547 *
548 * @param string $stage One of the STAGE_ constants
549 * @return array
550 */
551 private function _scanJobsForStage($stage) {
552 $always = array();
553 $options = array();
554 switch ($stage) {
555 case self::STAGE_SPAMVERTISING_CHECKS:
556 $options = array(
557 'spamvertizeCheck',
558 );
559 break;
560 case self::STAGE_SPAM_CHECK:
561 $options = array(
562 'checkSpamIP',
563 );
564 break;
565 case self::STAGE_BLACKLIST_CHECK:
566 $options = array(
567 'scansEnabled_checkGSB',
568 );
569 break;
570 case self::STAGE_SERVER_STATE:
571 if ($this->scanType() != self::SCAN_TYPE_QUICK) {
572 $always = array(
573 'checkSkippedFiles',
574 );
575 }
576 $options = array(
577 'scansEnabled_checkHowGetIPs',
578 'scansEnabled_diskSpace',
579 'scansEnabled_wafStatus',
580 'scansEnabled_geoipSupport',
581 );
582 break;
583 case self::STAGE_FILE_CHANGES:
584 $options = array(
585 'scansEnabled_core',
586 'scansEnabled_themes',
587 'scansEnabled_plugins',
588 'scansEnabled_coreUnknown',
589 );
590 break;
591 case self::STAGE_PUBLIC_FILES:
592 $options = array(
593 'scansEnabled_checkReadableConfig',
594 'scansEnabled_suspectedFiles',
595 );
596 break;
597 case self::STAGE_MALWARE_SCAN:
598 $options = array(
599 'scansEnabled_malware',
600 'scansEnabled_fileContents',
601 );
602 break;
603 case self::STAGE_CONTENT_SAFETY:
604 $options = array(
605 'scansEnabled_posts',
606 'scansEnabled_comments',
607 'scansEnabled_fileContentsGSB',
608 );
609 break;
610 case self::STAGE_PASSWORD_STRENGTH:
611 $options = array(
612 'scansEnabled_passwds',
613 );
614 break;
615 case self::STAGE_VULNERABILITY_SCAN:
616 $options = array(
617 'scansEnabled_oldVersions',
618 );
619 break;
620 case self::STAGE_OPTIONS_AUDIT:
621 $options = array(
622 'scansEnabled_suspiciousOptions',
623 'scansEnabled_suspiciousAdminUsers',
624 );
625 break;
626 }
627
628 $enabledOptions = $this->scanOptions();
629 $filteredOptions = array();
630 foreach ($options as $o) {
631 if (isset($enabledOptions[$o]) && $enabledOptions[$o]) {
632 $filteredOptions[] = $o;
633 }
634 }
635
636 return array_merge($filteredOptions, $always);
637 }
638
639 /**
640 * Returns an associative array containing each scan stage's default state. The keys are the stage identifiers and the value
641 * is an array in the format
642 * array(
643 * 'started' => the number of tasks for this stage that have started (initially 0),
644 * 'finished' => the number of tasks that have started and finished (initially 0),
645 * 'expected' => the expected number of tasks to run for this stage (based on the scan type and options enabled)
646 * )
647 *
648 * @return array
649 */
650 private function _defaultStageStatuses() {
651 $status = array(
652 self::STAGE_SPAMVERTISING_CHECKS => array('status' => ($this->isPremiumScan() ? self::STATUS_PENDING : self::STATUS_PREMIUM), 'started' => 0, 'finished' => 0, 'expected' => 0),
653 self::STAGE_SPAM_CHECK => array('status' => ($this->isPremiumScan() ? self::STATUS_PENDING : self::STATUS_PREMIUM), 'started' => 0, 'finished' => 0, 'expected' => 0),
654 self::STAGE_BLACKLIST_CHECK => array('status' => ($this->isPremiumScan() ? self::STATUS_PENDING : self::STATUS_PREMIUM), 'started' => 0, 'finished' => 0, 'expected' => 0),
655 self::STAGE_SERVER_STATE => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0),
656 self::STAGE_FILE_CHANGES => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0),
657 self::STAGE_PUBLIC_FILES => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0),
658 self::STAGE_MALWARE_SCAN => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0),
659 self::STAGE_CONTENT_SAFETY => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0),
660 self::STAGE_PASSWORD_STRENGTH => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0),
661 self::STAGE_VULNERABILITY_SCAN => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0),
662 self::STAGE_OPTIONS_AUDIT => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0),
663 );
664
665 foreach ($status as $stage => &$parameters) {
666 if ($parameters['status'] == self::STATUS_PREMIUM) {
667 continue;
668 }
669
670 $jobs = $this->_scanJobsForStage($stage);
671 if (count($jobs)) {
672 $parameters['expected'] = count($jobs);
673 }
674 else {
675 $parameters['status'] = self::STATUS_DISABLED;
676 }
677 }
678
679 return $status;
680 }
681
682 /**
683 * Resets the state of the scan stage status record.
684 */
685 public function resetStages() {
686 if ($this->scanType() == self::SCAN_TYPE_QUICK) { //Suppress for quick scans
687 return;
688 }
689 wfConfig::set_ser('scanStageStatuses', $this->_defaultStageStatuses(), false, wfConfig::DONT_AUTOLOAD);
690 }
691
692 private function _shouldForceUpdate($stageID) {
693 if ($stageID == wfScanner::STAGE_MALWARE_SCAN) {
694 return true;
695 }
696 return false;
697 }
698
699 /**
700 * Increments the stage started counter and marks it as running if not already in that state.
701 *
702 * @param string $stageID One of the STAGE_ constants
703 */
704 public function startStage($stageID) {
705 if ($this->scanType() == self::SCAN_TYPE_QUICK) { //Suppress for quick scans
706 return;
707 }
708
709 $runningStatus = wfConfig::get_ser('scanStageStatuses', array(), false);
710 if ($runningStatus[$stageID]['status'] != self::STATUS_RUNNING_WARNING) {
711 $runningStatus[$stageID]['status'] = self::STATUS_RUNNING;
712 }
713
714 $runningStatus[$stageID]['started'] += 1;
715 wfConfig::set_ser('scanStageStatuses', $runningStatus, false, wfConfig::DONT_AUTOLOAD);
716 if (wfCentral::isConnected() && ($this->_shouldForceUpdate($stageID) || (time() - wfConfig::getInt('lastScanStageStatusUpdate', 0)) > self::CENTRAL_STAGE_UPDATE_THRESHOLD)) {
717 wfCentral::updateScanStatus($runningStatus);
718 }
719 }
720
721 /**
722 * Increments the stage finished counter and updates the stage status according to whether it's fully finished or encountered a negative status.
723 *
724 * @param string $stageID One of the STAGE_ constants.
725 * @param string $status One of the wfIssues::STATUS_ constants
726 */
727 public function completeStage($stageID, $status) {
728 if ($this->scanType() == self::SCAN_TYPE_QUICK) { //Suppress for quick scans
729 return;
730 }
731
732 $runningStatus = wfConfig::get_ser('scanStageStatuses', array(), false);
733
734 if ($runningStatus[$stageID]['status'] == self::STATUS_RUNNING && ($status == wfIssues::STATE_PROBLEM)) {
735 $runningStatus[$stageID]['status'] = self::STATUS_RUNNING_WARNING;
736 }
737
738 $runningStatus[$stageID]['finished'] += 1;
739 if ($runningStatus[$stageID]['finished'] >= $runningStatus[$stageID]['expected']) {
740 if ($runningStatus[$stageID]['status'] == self::STATUS_RUNNING) {
741 $runningStatus[$stageID]['status'] = self::STATUS_COMPLETE_SUCCESS;
742 }
743 else {
744 $runningStatus[$stageID]['status'] = self::STATUS_COMPLETE_WARNING;
745 }
746 }
747
748 wfConfig::set_ser('scanStageStatuses', $runningStatus, false, wfConfig::DONT_AUTOLOAD);
749 if (wfCentral::isConnected()) {
750 $forceSend = true; //Force sending the last stage completion update even if the timing would otherwise prevent it
751 foreach ($runningStatus as $stageID => $stage) {
752 if ($runningStatus[$stageID]['finished'] < $runningStatus[$stageID]['expected']) {
753 $forceSend = false;
754 break;
755 }
756 }
757
758 if ($forceSend || (time() - wfConfig::getInt('lastScanStageStatusUpdate', 0)) > self::CENTRAL_STAGE_UPDATE_THRESHOLD) {
759 wfCentral::updateScanStatus($runningStatus);
760 }
761 }
762
763 }
764
765 /**
766 * Returns the selected type of the scan.
767 *
768 * @return string
769 */
770 public function scanType() {
771 switch ($this->_scanType) {
772 case self::SCAN_TYPE_QUICK://SCAN_TYPE_QUICK is not user-selectable
773 case self::SCAN_TYPE_LIMITED:
774 case self::SCAN_TYPE_STANDARD:
775 case self::SCAN_TYPE_HIGH_SENSITIVITY:
776 case self::SCAN_TYPE_CUSTOM:
777 return $this->_scanType;
778 }
779 return self::SCAN_TYPE_STANDARD;
780 }
781
782 /**
783 * Returns the display name for the selected type of the scan.
784 *
785 * @return string
786 */
787 public function scanTypeName() {
788 switch ($this->_scanType) {
789 case self::SCAN_TYPE_QUICK:
790 return __('Quick Scan', 'wordfence');
791 case self::SCAN_TYPE_LIMITED:
792 return __('Limited Scan', 'wordfence');
793 case self::SCAN_TYPE_HIGH_SENSITIVITY:
794 return __('High Sensitivity', 'wordfence');
795 case self::SCAN_TYPE_CUSTOM:
796 return __('Custom Scan', 'wordfence');
797 case self::SCAN_TYPE_STANDARD:
798 default:
799 return __('Standard Scan', 'wordfence');
800 }
801 }
802
803 /**
804 * Returns a normalized percentage (i.e., in the range [0, 1]) to the corresponding display percentage
805 * based on license type.
806 *
807 * @param float $percentage
808 * @return float
809 */
810 protected function _normalizedPercentageToDisplay($percentage) {
811 if ($this->isPremiumScan()) {
812 return round($percentage, 2);
813 }
814
815 return round($percentage * 0.70, 2);
816 }
817
818 /**
819 * Returns a normalized percentage (i.e., in the range [0, 1]) for the scan type status indicator.
820 *
821 * @return float
822 */
823 public function scanTypeStatus() {
824 $isFree = !wfConfig::get('isPaid');
825 $weights = self::_scanOptionWeights();
826 $options = $this->scanOptions();
827 $score = 0.0;
828 $premiumOptions = self::_premiumScanOptions();
829 foreach ($options as $key => $value) {
830 if ($isFree && array_search($key, $premiumOptions) !== false) {
831 continue;
832 }
833
834 if ($value) {
835 $score += $weights[$key];
836 }
837 }
838 return $this->_normalizedPercentageToDisplay($score);
839 }
840
841 public function scanTypeStatusList() {
842 $isFree = !wfConfig::get('isPaid');
843 $weights = self::_scanOptionWeights();
844 $options = $this->scanOptions();
845 $disabledOptionCount = 0;
846 $premiumDisabledOptionCount = 0;
847 $percentage = 0.0;
848 $premiumPercentage = 0.0;
849 $premiumOptions = self::_premiumScanOptions();
850 $statusList = array();
851 foreach ($options as $key => $value) {
852 if ($isFree && array_search($key, $premiumOptions) !== false) {
853 $premiumPercentage += $weights[$key];
854 $premiumDisabledOptionCount++;
855 continue;
856 }
857
858 if (!$value && $weights[$key] > 0) {
859 $percentage += $weights[$key];
860 $disabledOptionCount++;
861 }
862 }
863
864 $remainingPercentage = 1 - $this->scanTypeStatus();
865 if ($isFree) {
866 $remainingPercentage -= 0.30;
867 $statusList[] = array(
868 'percentage' => 0.30,
869 'title' => __('Enable Premium Scan Signatures.', 'wordfence'),
870 );
871 }
872
873 if ($premiumPercentage > 0) {
874 $subtraction = min($this->_normalizedPercentageToDisplay($premiumPercentage), $remainingPercentage);
875 $remainingPercentage -= $subtraction;
876 $statusList[] = array(
877 'percentage' => $subtraction,
878 'title' => __('Enable Premium Reputation Checks.', 'wordfence'),
879 );
880 }
881
882 if ($percentage > 0) {
883 $subtraction = min($this->_normalizedPercentageToDisplay($percentage), $remainingPercentage);
884 $statusList[] = array(
885 'percentage' => $subtraction,
886 'title' => sprintf(/* translators: option count */ _n('Enable %d scan option.', 'Enable %d scan options.', $disabledOptionCount,'wordfence'), number_format_i18n($disabledOptionCount)),
887 );
888 }
889
890 return $statusList;
891 }
892 /**
893 * Returns the malware signature feed that is in use.
894 *
895 * @return string
896 */
897 public function signatureMode() {
898 if ($this->isPremiumScan()) {
899 return self::SIGNATURE_MODE_PREMIUM;
900 }
901 return self::SIGNATURE_MODE_COMMUNITY;
902 }
903
904 /**
905 * Returns a normalized percentage (i.e., in the range [0, 1]) for the reputation status indicator.
906 *
907 * @return float
908 */
909 public function reputationStatus() {
910 $score = 0.0;
911 if ($this->isPremiumScan()) {
912 $options = $this->scanOptions();
913 if ($options['spamvertizeCheck']) { $score += 0.333; }
914 if ($options['checkSpamIP']) { $score += 0.333; }
915 if ($options['scansEnabled_checkGSB']) { $score += 0.333; }
916 }
917 return round($score, 2);
918 }
919
920 /**
921 * @return array
922 */
923 public function reputationStatusList() {
924 $statusList = array();
925 $options = $this->scanOptions();
926
927 $reputationChecks = array(
928 'spamvertizeCheck' => __('Enable scan option to check if this website is being "Spamvertised".', 'wordfence'),
929 'checkSpamIP' => __('Enable scan option to check if your website IP is generating spam.', 'wordfence'),
930 'scansEnabled_checkGSB' => __('Enable scan option to check if your website is on a domain blocklist.', 'wordfence'),
931 );
932
933 foreach ($reputationChecks as $option => $optionLabel) {
934 if (!$this->isPremiumScan() || !$options[$option]) {
935 $statusList[] = array(
936 'percentage' => round(1 / count($reputationChecks), 2),
937 'title' => $optionLabel,
938 );
939 }
940 }
941 return $statusList;
942 }
943
944 /**
945 * Returns the options for the configured scan type.
946 *
947 * @return array
948 */
949 public function scanOptions() {
950 switch ($this->scanType()) {
951 case self::SCAN_TYPE_QUICK:
952 return self::quickScanTypeOptions();
953 case self::SCAN_TYPE_LIMITED:
954 return self::limitedScanTypeOptions();
955 case self::SCAN_TYPE_STANDARD:
956 return self::standardScanTypeOptions();
957 case self::SCAN_TYPE_HIGH_SENSITIVITY:
958 return self::highSensitivityScanTypeOptions();
959 case self::SCAN_TYPE_CUSTOM:
960 return self::customScanTypeOptions();
961 }
962 }
963
964 /**
965 * Returns the array of jobs for the scan type.
966 *
967 * @return array
968 */
969 public function jobs() {
970 $options = $this->scanOptions();
971 $preferredOrder = array(
972 'checkSpamvertized' => array('spamvertizeCheck'),
973 'checkSpamIP' => array('checkSpamIP'),
974 'checkGSB' => array('scansEnabled_checkGSB'),
975 'checkHowGetIPs' => array('scansEnabled_checkHowGetIPs'),
976 'diskSpace' => array('scansEnabled_diskSpace'),
977 'wafStatus' => array('scansEnabled_wafStatus'),
978 'geoipSupport' => array('scansEnabled_geoipSupport'),
979 'checkSkippedFiles' => ($this->scanType() != self::SCAN_TYPE_QUICK), //Always runs except for quick
980 'knownFiles' => ($this->scanType() != self::SCAN_TYPE_QUICK), //Always runs except for quick, options are scansEnabled_core, scansEnabled_themes, scansEnabled_plugins, scansEnabled_coreUnknown, scansEnabled_malware
981 'checkReadableConfig' => array('scansEnabled_checkReadableConfig'),
982 'fileContents' => ($this->scanType() != self::SCAN_TYPE_QUICK), //Always runs except for quick, options are scansEnabled_fileContents and scansEnabled_fileContentsGSB
983 'suspectedFiles' => array('scansEnabled_suspectedFiles'),
984 'posts' => array('scansEnabled_posts'),
985 'comments' => array('scansEnabled_comments'),
986 'passwds' => array('scansEnabled_passwds'),
987 'oldVersions' => array('scansEnabled_oldVersions'),
988 'suspiciousAdminUsers' => array('scansEnabled_suspiciousAdminUsers'),
989 'suspiciousOptions' => array('scansEnabled_suspiciousOptions'),
990 );
991
992 $jobs = array();
993 foreach ($preferredOrder as $job => $enabler) {
994 if ($enabler === true) {
995 $jobs[] = $job;
996 }
997 else if (is_array($enabler)) {
998 foreach ($enabler as $o) {
999 if ($options[$o]) {
1000 $jobs[] = $job;
1001 break;
1002 }
1003 }
1004 }
1005 }
1006 return $jobs;
1007 }
1008
1009 /**
1010 * Returns whether or not the scanner should use its low resource mode.
1011 *
1012 * @return bool
1013 */
1014 public function useLowResourceScanning() {
1015 $options = $this->scanOptions();
1016 return $options['lowResourceScansEnabled'];
1017 }
1018
1019 /**
1020 * Returns the array of user-defined malware signatures for use by the scanner.
1021 *
1022 * @return array
1023 */
1024 public function userScanSignatures() {
1025 $options = $this->scanOptions();
1026 $value = $options['scan_include_extra'];
1027 $signatures = array();
1028 if (!empty($value)) {
1029 $regexs = explode("\n", $value);
1030 $id = 1000001;
1031 foreach ($regexs as $r) {
1032 $r = rtrim($r, "\r");
1033 if (preg_match('/' . $r . '/i', "") !== false) {
1034 $signatures[] = array($id++, time(), $r, __('User defined scan pattern', 'wordfence'));
1035 }
1036 }
1037 }
1038 return $signatures;
1039 }
1040
1041 /**
1042 * Returns whether or not the scanner should check files outside of the WordPress installation.
1043 *
1044 * @return bool
1045 */
1046 public function scanOutsideWordPress() {
1047 $options = $this->scanOptions();
1048 return $options['other_scanOutside'];
1049 }
1050
1051 /**
1052 * Returns the cleaned up array of user-excluded scan paths and patterns.
1053 *
1054 * @return array
1055 */
1056 public function userExclusions() {
1057 $options = $this->scanOptions();
1058 $value = $options['scan_exclude'];
1059 return explode("\n", wfUtils::cleanupOneEntryPerLine($value));
1060 }
1061
1062 /**
1063 * Fetches the scan summary items into the internal cache.
1064 */
1065 private function _fetchSummaryItems() {
1066 if ($this->_summary !== false) {
1067 return;
1068 }
1069
1070 $this->_summary = wfConfig::get_ser('wf_summaryItems', array());
1071 }
1072
1073 /**
1074 * Writes the scan summary cache to permanent storage.
1075 */
1076 private function _saveSummaryItems() {
1077 if ($this->_summary !== false && $this->_dirty) {
1078 $this->_summary['lastUpdate'] = time();
1079 wfConfig::set_ser('wf_summaryItems', $this->_summary);
1080 }
1081 $this->_dirty = false;
1082 }
1083
1084 /**
1085 * Saves the scan summary cache if it has been more than two seconds since the last update.
1086 *
1087 * @return bool Whether or not it saved.
1088 */
1089 private function _maybeSaveSummaryItems() {
1090 if ($this->_summary !== false && $this->_summary['lastUpdate'] < (time() - 2)) {
1091 $this->_saveSummaryItems();
1092 return true;
1093 }
1094 return false;
1095 }
1096
1097 /**
1098 * Populates the scan summary with the default counters.
1099 */
1100 public function resetSummaryItems() {
1101 global $wpdb;
1102
1103 $this->_summary = array();
1104 $this->_summary[self::SUMMARY_SCANNED_POSTS] = 0;
1105 $this->_summary[self::SUMMARY_SCANNED_COMMENTS] = 0;
1106 $this->_summary[self::SUMMARY_SCANNED_FILES] = 0;
1107 $this->_summary[self::SUMMARY_SCANNED_PLUGINS] = 0;
1108 $this->_summary[self::SUMMARY_SCANNED_THEMES] = 0;
1109 $this->_summary[self::SUMMARY_SCANNED_USERS] = 0;
1110 $this->_summary[self::SUMMARY_SCANNED_URLS] = 0;
1111
1112 $this->_dirty = true;
1113 $this->_saveSummaryItems();
1114 }
1115
1116 /**
1117 * Forces a save of the scan summary cache.
1118 */
1119 public function flushSummaryItems() {
1120 $this->_saveSummaryItems();
1121 }
1122
1123 /**
1124 * Returns the corresponding summary value for $key or $default if not found.
1125 *
1126 * @param $key
1127 * @param mixed $default The value returned if there is no value for $key.
1128 * @return mixed
1129 */
1130 public function getSummaryItem($key, $default = false) {
1131 $this->_fetchSummaryItems();
1132 if (isset($this->_summary[$key])) {
1133 return $this->_summary[$key];
1134 }
1135 return $default;
1136 }
1137
1138 /**
1139 * Returns an array of all summary items.
1140 *
1141 * @return array
1142 */
1143 public function getAllSummaryItems() {
1144 $this->_fetchSummaryItems();
1145 if (is_array($this->_summary)) {
1146 return $this->_summary;
1147 }
1148 return array();
1149 }
1150
1151 /**
1152 * Sets the summary item $key as $value.
1153 *
1154 * @param $key
1155 * @param $value
1156 */
1157 public function setSummaryItem($key, $value) {
1158 $this->_fetchSummaryItems();
1159 $this->_summary[$key] = $value;
1160 $this->_dirty = true;
1161
1162 if (!$this->_maybeSaveSummaryItems() && !$this->_destructRegistered) {
1163 register_shutdown_function(array($this, 'flushSummaryItems'));
1164 $this->_destructRegistered = true;
1165 }
1166 }
1167
1168 /**
1169 * Atomically increments the summary item under $key by $value.
1170 *
1171 * @param $key
1172 * @param int $value
1173 */
1174 public function incrementSummaryItem($key, $value = 1) {
1175 if ($value == 0) { return; }
1176 $this->_fetchSummaryItems();
1177 if (isset($this->_summary[$key])) {
1178 $this->_summary[$key] += $value;
1179 $this->_dirty = true;
1180
1181 if (!$this->_maybeSaveSummaryItems() && !$this->_destructRegistered) {
1182 register_shutdown_function(array($this, 'flushSummaryItems'));
1183 $this->_destructRegistered = true;
1184 }
1185 }
1186 }
1187
1188 /**
1189 * Schedules a single scan for the given time. If $originalTime is provided, it will be associated with the cron.
1190 *
1191 * @param $futureTime
1192 * @param bool|int $originalTime
1193 */
1194 public function scheduleSingleScan($futureTime, $originalTime = false) {
1195 if (is_main_site()) { // Removed ability to activate on network site in v5.3.12
1196 if ($originalTime === false) {
1197 $originalTime = $futureTime;
1198 }
1199 wp_schedule_single_event($futureTime, 'wordfence_start_scheduled_scan', array((int) $originalTime));
1200
1201 //Saving our own copy of the schedule because the wp-cron functions all require the args list to act
1202 $allScansScheduled = wfConfig::get_ser('allScansScheduled', array());
1203 $allScansScheduled[] = array('timestamp' => $futureTime, 'args' => array((int) $originalTime));
1204 wfConfig::set_ser('allScansScheduled', $allScansScheduled);
1205 }
1206 }
1207
1208 /**
1209 * Clears all scheduled scan cron jobs and re-creates them.
1210 */
1211 public function scheduleScans() {
1212 $this->unscheduleAllScans();
1213 if (!$this->isEnabled()) {
1214 return;
1215 }
1216
1217 if ($this->schedulingMode() == wfScanner::SCAN_SCHEDULING_MODE_MANUAL) {
1218 //Generate a two-week schedule
1219 $manualType = $this->manualSchedulingType();
1220 $preferredHour = $this->manualSchedulingStartHour();
1221 switch ($manualType) {
1222 case self::MANUAL_SCHEDULING_ONCE_DAILY:
1223 $schedule = array_fill(0, 14, array_fill(0, 24, 0));
1224 foreach ($schedule as $dayNumber => &$day) {
1225 $day[$preferredHour] = 1;
1226 }
1227 break;
1228 case self::MANUAL_SCHEDULING_TWICE_DAILY:
1229 $schedule = array_fill(0, 14, array_fill(0, 24, 0));
1230 foreach ($schedule as $dayNumber => &$day) {
1231 $day[$preferredHour] = 1;
1232 $day[fmod($preferredHour + 12, 24)] = 1;
1233 }
1234 break;
1235 case self::MANUAL_SCHEDULING_EVERY_OTHER_DAY:
1236 $baseDay = floor(time() / 86400);
1237 $schedule = array_fill(0, 14, array_fill(0, 24, 0));
1238 foreach ($schedule as $dayNumber => &$day) {
1239 if (($baseDay + $dayNumber) % 2) {
1240 $day[$preferredHour] = 1;
1241 }
1242 }
1243 break;
1244 case self::MANUAL_SCHEDULING_WEEKDAYS:
1245 $schedule = array_fill(0, 14, array_fill(0, 24, 0));
1246 foreach ($schedule as $dayNumber => &$day) {
1247 if ($dayNumber > 0 && $dayNumber < 6) {
1248 $day[$preferredHour] = 1;
1249 }
1250 }
1251 break;
1252 case self::MANUAL_SCHEDULING_WEEKENDS:
1253 $schedule = array_fill(0, 14, array_fill(0, 24, 0));
1254 foreach ($schedule as $dayNumber => &$day) {
1255 if ($dayNumber == 0 || $dayNumber == 6) {
1256 $day[$preferredHour] = 1;
1257 }
1258 }
1259 break;
1260 case self::MANUAL_SCHEDULING_ODD_DAYS_WEEKENDS:
1261 $schedule = array_fill(0, 14, array_fill(0, 24, 0));
1262 foreach ($schedule as $dayNumber => &$day) {
1263 if ($dayNumber == 0 || $dayNumber == 6 || ($dayNumber % 2)) {
1264 $day[$preferredHour] = 1;
1265 }
1266 }
1267 break;
1268 case self::MANUAL_SCHEDULING_CUSTOM:
1269 $oneWeekSchedule = $this->customSchedule();
1270 $schedule = array();
1271 foreach ($oneWeekSchedule as $day) { $schedule[] = $day; }
1272 foreach ($oneWeekSchedule as $day) { $schedule[] = $day; }
1273 break;
1274 }
1275
1276 $now = time();
1277 $tzOffset = wfUtils::formatLocalTime('Z', $now);
1278
1279 //Apply the time zone shift so the start times align to the server's time zone
1280 $shiftedSchedule = array_fill(0, 14, array());
1281 foreach ($schedule as $dayNumber => $day) {
1282 foreach ($day as $hourNumber => $enabled) {
1283 if ($enabled) {
1284 $effectiveHour = round(($hourNumber * 3600 - $tzOffset) / 3600); //round() rather than floor() to account for fractional time zones
1285 $wrappedHour = (int) fmod($effectiveHour + 24, 24);
1286 if ($effectiveHour < 0) {
1287 if ($dayNumber > 0) {
1288 $shiftedSchedule[$dayNumber - 1][$wrappedHour] = true;
1289 }
1290 }
1291 else if ($effectiveHour > 23) {
1292 if ($dayNumber < count($schedule) - 1) {
1293 $shiftedSchedule[$dayNumber + 1][$wrappedHour] = true;
1294 }
1295 }
1296 else {
1297 $shiftedSchedule[$dayNumber][$effectiveHour] = true;
1298 }
1299 }
1300 }
1301 }
1302 $schedule = $shiftedSchedule;
1303
1304 //Trim out all but an 8-day period
1305 $currentDayOfWeekUTC = date('w', $now);
1306 $currentHourUTC = date('G', $now);
1307 $periodStart = floor($now / 86400) * 86400 - $currentDayOfWeekUTC * 86400;
1308 $schedule = array_slice($schedule, $currentDayOfWeekUTC, null, true);
1309 $schedule = array_slice($schedule, 0, 8, true);
1310
1311 //Schedule them
1312 foreach ($schedule as $dayNumber => $day) {
1313 foreach ($day as $hourNumber => $enabled) {
1314 if ($enabled) {
1315 if ($dayNumber == $currentDayOfWeekUTC && $currentHourUTC > $hourNumber) { //It's today and we've already passed its hour, skip it
1316 continue;
1317 }
1318 else if ($dayNumber > 6 && ($dayNumber % 7) == $currentDayOfWeekUTC && $currentHourUTC <= $hourNumber) { //It's one week from today but beyond the current hour, skip it this cycle
1319 continue;
1320 }
1321
1322 $scanTime = $periodStart + $dayNumber * 86400 + $hourNumber * 3600 + wfWAFUtils::random_int(0, 3600);
1323 wordfence::status(4, 'info', sprintf(
1324 /* translators: 1. Day of week. 2. Hour of day. 3. Localized date. */
1325 __('Scheduled time for day %1$s hour %2$s is: %3$s', 'wordfence'),
1326 $dayNumber,
1327 $hourNumber,
1328 wfUtils::formatLocalTime('l jS \of F Y h:i:s A P', $scanTime)
1329 ));
1330 $this->scheduleSingleScan($scanTime);
1331 }
1332 }
1333 }
1334 }
1335 else {
1336 $noc1ScanSchedule = wfConfig::get_ser('noc1ScanSchedule', array());
1337 foreach ($noc1ScanSchedule as $timestamp) {
1338 $timestamp = wfUtils::denormalizedTime($timestamp);
1339 if ($timestamp > time()) {
1340 $this->scheduleSingleScan($timestamp);
1341 }
1342 }
1343 }
1344 }
1345
1346 public function unscheduleAllScans() {
1347 $allScansScheduled = wfConfig::get_ser('allScansScheduled', array());
1348 foreach ($allScansScheduled as $entry) {
1349 wp_unschedule_event($entry['timestamp'], 'wordfence_start_scheduled_scan', $entry['args']);
1350 }
1351 wp_clear_scheduled_hook('wordfence_start_scheduled_scan');
1352 wfConfig::set_ser('allScansScheduled', array());
1353 }
1354
1355 public function nextScheduledScanTime() {
1356 $nextTime = false;
1357 $cron = _get_cron_array();
1358 foreach($cron as $key => $val){
1359 if(isset($val['wordfence_start_scheduled_scan'])){
1360 $nextTime = $key;
1361 break;
1362 }
1363 }
1364 return $nextTime;
1365 }
1366
1367 public function lastScanTime() {
1368 return wfConfig::get('scanTime');
1369 }
1370
1371 public function recordLastScanTime() {
1372 wfConfig::set('scanTime', microtime(true));
1373 }
1374
1375 public function lastQuickScanTime() {
1376 return wfConfig::get('lastQuickScan', 0);
1377 }
1378
1379 public function recordLastQuickScanTime() {
1380 wfConfig::set('lastQuickScan', microtime(true));
1381 }
1382 }