PluginProbe ʕ •ᴥ•ʔ
Wordfence Security – Firewall, Malware Scan, and Login Security / 8.0.5
Wordfence Security – Firewall, Malware Scan, and Login Security v8.0.5
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 / lib / wfIssues.php
wordfence / lib Last commit date
Diff 1 year ago audit-log 1 year ago dashboard 1 year ago rest-api 1 year ago .htaccess 7 years ago Diff.php 6 years ago GeoLite2-Country.mmdb 1 year ago IPTraf.php 1 year ago IPTrafList.php 1 year ago WFLSPHP52Compatability.php 6 years ago compat.php 8 years ago diffResult.php 1 year ago email_genericAlert.php 5 years ago email_newIssues.php 3 years ago email_unlockRequest.php 5 years ago email_unsubscribeRequest.php 4 years ago flags.php 7 years ago live_activity.php 4 years ago menu_dashboard.php 3 years ago menu_dashboard_options.php 3 years ago menu_firewall.php 3 years ago menu_firewall_blocking.php 4 years ago menu_firewall_blocking_options.php 3 years ago menu_firewall_waf.php 4 years ago menu_firewall_waf_options.php 3 years ago menu_install.php 3 years ago menu_options.php 1 year ago menu_scanner.php 2 years ago menu_scanner_credentials.php 1 year ago menu_scanner_options.php 3 years ago menu_support.php 1 year ago menu_tools.php 1 year ago menu_tools_auditlog.php 1 year ago menu_tools_diagnostic.php 1 year ago menu_tools_importExport.php 4 years ago menu_tools_livetraffic.php 2 years ago menu_tools_twoFactor.php 4 years ago menu_tools_whois.php 3 years ago menu_wordfence_central.php 3 years ago noc1.key 7 years ago sodium_compat_fast.php 2 years ago sysinfo.php 1 year ago viewFullActivityLog.php 4 years ago wf503.php 2 years ago wfAPI.php 1 year ago wfActivityReport.php 1 year ago wfAdminNoticeQueue.php 4 years ago wfAlerts.php 5 years ago wfArray.php 3 years ago wfAuditLog.php 1 year ago wfBrowscap.php 3 years ago wfBrowscapCache.php 7 years ago wfBulkCountries.php 2 years ago wfCache.php 3 years ago wfCentralAPI.php 1 year ago wfConfig.php 1 year ago wfCrawl.php 1 year ago wfCredentialsController.php 1 year ago wfCrypt.php 6 years ago wfCurlInterceptor.php 3 years ago wfDB.php 1 year ago wfDashboard.php 1 year ago wfDateLocalization.php 2 years ago wfDeactivationOption.php 3 years ago wfDiagnostic.php 1 year ago wfDict.php 8 years ago wfDirectoryIterator.php 7 years ago wfFileUtils.php 2 years ago wfHelperBin.php 11 years ago wfHelperString.php 1 year ago wfIPWhitelist.php 5 years ago wfImportExportController.php 5 years ago wfInaccessibleDirectoryException.php 2 years ago wfInvalidPathException.php 3 years ago wfIpLocation.php 3 years ago wfIpLocator.php 3 years ago wfIssues.php 1 year ago wfJWT.php 7 years ago wfLicense.php 3 years ago wfLockedOut.php 2 years ago wfLog.php 1 year ago wfMD5BloomFilter.php 8 years ago wfModuleController.php 7 years ago wfNotification.php 8 years ago wfOnboardingController.php 1 year ago wfPersistenceController.php 1 year ago wfRESTAPI.php 7 years ago wfScan.php 2 years ago wfScanEngine.php 1 year ago wfScanEntrypoint.php 3 years ago wfScanFile.php 1 year ago wfScanFileLink.php 3 years ago wfScanFileListItem.php 1 year ago wfScanFileProperties.php 1 year ago wfScanMonitor.php 2 years ago wfScanPath.php 3 years ago wfSchema.php 1 year ago wfStyle.php 1 year ago wfSupportController.php 1 year ago wfUnlockMsg.php 5 years ago wfUpdateCheck.php 1 year ago wfUtils.php 1 year ago wfVersionCheckController.php 3 years ago wfVersionSupport.php 1 year ago wfView.php 5 years ago wfViewResult.php 1 year ago wfWebsite.php 3 years ago wordfenceClass.php 1 year ago wordfenceConstants.php 1 year ago wordfenceHash.php 1 year ago wordfenceScanner.php 1 year ago wordfenceURLHoover.php 2 years ago
wfIssues.php
799 lines
1 <?php
2 require_once(dirname(__FILE__) . '/wfUtils.php');
3 class wfIssues {
4 //Possible responses from `addIssue`
5 const ISSUE_ADDED = 'a';
6 const ISSUE_UPDATED = 'u';
7 const ISSUE_DUPLICATE = 'd';
8 const ISSUE_IGNOREP = 'ip';
9 const ISSUE_IGNOREC = 'ic';
10
11 //Possible status message states
12 const STATUS_NONE = 'n'; //Default state before running
13
14 const STATUS_SKIPPED = 's'; //The scan job was skipped because it didn't need to run
15 const STATUS_IGNORED = 'i'; //The scan job found an issue, but it matched an entry in the ignore list
16
17 const STATUS_PROBLEM = 'p'; //The scan job found an issue
18 const STATUS_SECURE = 'r'; //The scan job found no issues
19
20 const STATUS_FAILED = 'f'; //The scan job failed
21 const STATUS_SUCCESS = 'c'; //The scan job succeeded
22
23 const STATUS_PAIDONLY = 'x';
24
25 //Possible scan failure types
26 const SCAN_FAILED_GENERAL = 'general';
27 const SCAN_FAILED_TIMEOUT = 'timeout';
28 const SCAN_FAILED_DURATION_REACHED = 'duration';
29 const SCAN_FAILED_VERSION_CHANGE = 'versionchange';
30 const SCAN_FAILED_FORK_FAILED = 'forkfailed';
31 const SCAN_FAILED_CALLBACK_TEST_FAILED = 'callbackfailed';
32 const SCAN_FAILED_START_TIMEOUT = 'starttimeout';
33
34 const SCAN_FAILED_API_SSL_UNAVAILABLE = 'sslunavailable';
35 const SCAN_FAILED_API_CALL_FAILED = 'apifailed';
36 const SCAN_FAILED_API_INVALID_RESPONSE = 'apiinvalid';
37 const SCAN_FAILED_API_ERROR_RESPONSE = 'apierror';
38
39 const SEVERITY_NONE = 0;
40 const SEVERITY_LOW = 25;
41 const SEVERITY_MEDIUM = 50;
42 const SEVERITY_HIGH = 75;
43 const SEVERITY_CRITICAL = 100;
44
45 const SCAN_STATUS_UPDATE_INTERVAL = 10; //Seconds
46
47 private $db = false;
48
49 //Properties that are serialized on sleep:
50 private $updateCalled = false;
51 private $issuesTable = '';
52 private $pendingIssuesTable = '';
53 private $maxIssues = 0;
54 private $newIssues = array();
55 public $totalIssues = 0;
56 public $totalIgnoredIssues = 0;
57 private $totalIssuesBySeverity = array();
58
59 public static $issueSeverities = array(
60 'checkGSB' => wfIssues::SEVERITY_CRITICAL,
61 'checkSpamIP' => wfIssues::SEVERITY_HIGH,
62 'spamvertizeCheck' => wfIssues::SEVERITY_CRITICAL,
63 'commentBadURL' => wfIssues::SEVERITY_LOW,
64 'postBadTitle' => wfIssues::SEVERITY_HIGH,
65 'postBadURL' => wfIssues::SEVERITY_HIGH,
66 'file' => wfIssues::SEVERITY_CRITICAL,
67 'timelimit' => wfIssues::SEVERITY_HIGH,
68 'checkHowGetIPs' => wfIssues::SEVERITY_HIGH,
69 'diskSpace' => wfIssues::SEVERITY_HIGH,
70 'wafStatus' => wfIssues::SEVERITY_CRITICAL,
71 'configReadable' => wfIssues::SEVERITY_CRITICAL,
72 'wfPluginVulnerable' => wfIssues::SEVERITY_HIGH,
73 'coreUnknown' => wfIssues::SEVERITY_HIGH,
74 'easyPasswordWeak' => wfIssues::SEVERITY_HIGH,
75 'knownfile' => wfIssues::SEVERITY_HIGH,
76 'optionBadURL' => wfIssues::SEVERITY_HIGH,
77 'publiclyAccessible' => wfIssues::SEVERITY_HIGH,
78 'suspiciousAdminUsers' => wfIssues::SEVERITY_HIGH,
79 'wfPluginAbandoned' => wfIssues::SEVERITY_MEDIUM,
80 'wfPluginRemoved' => wfIssues::SEVERITY_CRITICAL,
81 'wfPluginUpgrade' => wfIssues::SEVERITY_MEDIUM,
82 'wfThemeUpgrade' => wfIssues::SEVERITY_MEDIUM,
83 'wfUpgradeError' => wfIssues::SEVERITY_MEDIUM,
84 'wfUpgrade' => wfIssues::SEVERITY_HIGH,
85 'wpscan_directoryList' => wfIssues::SEVERITY_HIGH,
86 'wpscan_fullPathDiscl' => wfIssues::SEVERITY_HIGH,
87 );
88
89 public static function validIssueTypes() {
90 return array('checkHowGetIPs', 'checkSpamIP', 'commentBadURL', 'configReadable', 'coreUnknown', 'database', 'diskSpace', 'wafStatus', 'easyPassword', 'file', 'geoipSupport', 'knownfile', 'optionBadURL', 'postBadTitle', 'postBadURL', 'publiclyAccessible', 'spamvertizeCheck', 'suspiciousAdminUsers', 'timelimit', 'wfPluginAbandoned', 'wfPluginRemoved', 'wfPluginUpgrade', 'wfPluginVulnerable', 'wfThemeUpgrade', 'wfUpgradeError', 'wfUpgrade', 'wpscan_directoryList', 'wpscan_fullPathDiscl', 'skippedPaths');
91 }
92
93 public static function statusPrep(){
94 wfConfig::set_ser('wfStatusStartMsgs', array());
95 wordfence::status(10, 'info', "SUM_PREP:Preparing a new scan.");
96 wfIssues::updateScanStillRunning();
97 }
98
99 public static function statusStart($message) {
100 $statusStartMsgs = wfConfig::get_ser('wfStatusStartMsgs', array());
101 $statusStartMsgs[] = $message;
102 wfConfig::set_ser('wfStatusStartMsgs', $statusStartMsgs);
103 wordfence::status(10, 'info', 'SUM_START:' . $message);
104 wfIssues::updateScanStillRunning();
105 return count($statusStartMsgs) - 1;
106 }
107
108 public static function statusEnd($index, $state) {
109 $statusStartMsgs = wfConfig::get_ser('wfStatusStartMsgs', array());
110 if ($state == self::STATUS_SKIPPED) {
111 wordfence::status(10, 'info', 'SUM_ENDSKIPPED:' . $statusStartMsgs[$index]);
112 }
113 else if ($state == self::STATUS_IGNORED) {
114 wordfence::status(10, 'info', 'SUM_ENDIGNORED:' . $statusStartMsgs[$index]);
115 }
116 else if ($state == self::STATUS_PROBLEM) {
117 wordfence::status(10, 'info', 'SUM_ENDBAD:' . $statusStartMsgs[$index]);
118 }
119 else if ($state == self::STATUS_SECURE) {
120 wordfence::status(10, 'info', 'SUM_ENDOK:' . $statusStartMsgs[$index]);
121 }
122 else if ($state == self::STATUS_FAILED) {
123 wordfence::status(10, 'info', 'SUM_ENDFAILED:' . $statusStartMsgs[$index]);
124 }
125 else if ($state == self::STATUS_SUCCESS) {
126 wordfence::status(10, 'info', 'SUM_ENDSUCCESS:' . $statusStartMsgs[$index]);
127 }
128 wfIssues::updateScanStillRunning();
129 $statusStartMsgs[$index] = '';
130 wfConfig::set_ser('wfStatusStartMsgs', $statusStartMsgs);
131 }
132
133 public static function statusEndErr() {
134 $statusStartMsgs = wfConfig::get_ser('wfStatusStartMsgs', array());
135 for ($i = 0; $i < count($statusStartMsgs); $i++) {
136 if (empty($statusStartMsgs[$i]) === false) {
137 wordfence::status(10, 'info', 'SUM_ENDERR:' . $statusStartMsgs[$i]);
138 $statusStartMsgs[$i] = '';
139 }
140 }
141 wfIssues::updateScanStillRunning();
142 }
143
144 public static function statusPaidOnly($message) {
145 wordfence::status(10, 'info', "SUM_PAIDONLY:" . $message);
146 wfIssues::updateScanStillRunning();
147 }
148
149 public static function statusDisabled($message) {
150 wordfence::status(10, 'info', "SUM_DISABLED:" . $message);
151 wfIssues::updateScanStillRunning();
152 }
153
154 public static function updateScanStillRunning($running = true) {
155 static $lastUpdate = 0;
156 if ($running) {
157 $timestamp = time();
158 if ($timestamp - $lastUpdate < self::SCAN_STATUS_UPDATE_INTERVAL)
159 return;
160 $lastUpdate = $timestamp;
161 }
162 else {
163 $timestamp = 0;
164 }
165 wfConfig::set('wf_scanLastStatusTime', $timestamp);
166 }
167
168 /**
169 * Returns false if the scan has not been detected as failed. If it has, returns a constant corresponding to the reason.
170 *
171 * @return bool|string
172 */
173 public static function hasScanFailed() {
174 $lastStatusUpdate = self::lastScanStatusUpdate();
175 if ($lastStatusUpdate !== false && wfScanner::shared()->isRunning()) {
176 $threshold = WORDFENCE_SCAN_FAILURE_THRESHOLD;
177 if (time() - $lastStatusUpdate > $threshold) {
178 return self::SCAN_FAILED_TIMEOUT;
179 }
180 }
181
182 $scanStartAttempt = wfConfig::get('scanStartAttempt', 0);
183 if ($scanStartAttempt && time() - $scanStartAttempt > WORDFENCE_SCAN_START_FAILURE_THRESHOLD) {
184 return self::SCAN_FAILED_START_TIMEOUT;
185 }
186
187 $recordedFailure = wfConfig::get('lastScanFailureType');
188 switch ($recordedFailure) {
189 case self::SCAN_FAILED_GENERAL:
190 case self::SCAN_FAILED_DURATION_REACHED:
191 case self::SCAN_FAILED_VERSION_CHANGE:
192 case self::SCAN_FAILED_FORK_FAILED:
193 case self::SCAN_FAILED_CALLBACK_TEST_FAILED:
194 case self::SCAN_FAILED_API_SSL_UNAVAILABLE:
195 case self::SCAN_FAILED_API_CALL_FAILED:
196 case self::SCAN_FAILED_API_INVALID_RESPONSE:
197 case self::SCAN_FAILED_API_ERROR_RESPONSE:
198 return $recordedFailure;
199 }
200
201 return false;
202 }
203
204 /**
205 * Returns false if the scan has not been detected as timed out. If it has, it returns the timestamp of the last status update.
206 *
207 * @return bool|int
208 */
209 public static function lastScanStatusUpdate() {
210 if (wfConfig::get('wf_scanLastStatusTime', 0) === 0) {
211 return false;
212 }
213
214 $threshold = WORDFENCE_SCAN_FAILURE_THRESHOLD;
215 return (time() > wfConfig::get('wf_scanLastStatusTime', 0) + $threshold) ? wfConfig::get('wf_scanLastStatusTime', 0) : false;
216 }
217
218 /**
219 * Returns the singleton wfIssues.
220 *
221 * @return wfIssues
222 */
223 public static function shared() {
224 static $_issues = null;
225 if ($_issues === null) {
226 $_issues = new wfIssues();
227 }
228 return $_issues;
229 }
230
231 public function __sleep(){ //Same order here as vars above
232 return array('updateCalled', 'issuesTable', 'pendingIssuesTable', 'maxIssues', 'newIssues', 'totalIssues', 'totalIgnoredIssues', 'totalIssuesBySeverity');
233 }
234 public function __construct(){
235 $this->issuesTable = wfDB::networkTable('wfIssues');
236 $this->pendingIssuesTable = wfDB::networkTable('wfPendingIssues');
237 $this->maxIssues = wfConfig::get('scan_maxIssues', 0);
238 }
239 public function __wakeup(){
240 $this->db = new wfDB();
241 }
242
243 public function addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData, $alreadyHashed = false) {
244 return $this->_addIssue('issue', $type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData, $alreadyHashed);
245 }
246 public function addPendingIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData) {
247 return $this->_addIssue('pending', $type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData);
248 }
249
250 /**
251 * Create a new issue
252 *
253 * @param string $group The issue type (e.g., issue or pending
254 * @param string $type
255 * @param int $severity
256 * @param string $ignoreP string to compare against for permanent ignores
257 * @param string $ignoreC string to compare against for ignoring until something changes
258 * @param string $shortMsg
259 * @param string $longMsg
260 * @param array $templateData
261 * @param bool $alreadyHashed If true, don't re-hash $ignoreP and $ignoreC
262 * @return string One of the ISSUE_ constants
263 */
264 private function _addIssue($group, $type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData, $alreadyHashed = false) {
265 if ($group == 'pending') {
266 $table = $this->pendingIssuesTable;
267 }
268 else {
269 $table = $this->issuesTable;
270 }
271
272 if (!$alreadyHashed) {
273 $ignoreP = md5(wfUtils::ifnull($ignoreP));
274 $ignoreC = md5(wfUtils::ifnull($ignoreC));
275 }
276
277 $results = $this->getDB()->querySelect("SELECT id, status, ignoreP, ignoreC FROM {$table} WHERE (ignoreP = '%s' OR ignoreC = '%s')", $ignoreP, $ignoreC);
278 foreach ($results as $row) {
279 if ($row['status'] == 'new' && ($row['ignoreC'] == $ignoreC || $row['ignoreP'] == $ignoreP)) {
280 if ($type != 'file' && $type != 'database') { //Filter out duplicate new issues except for infected files because we want to see all infections even if file contents are identical
281 return self::ISSUE_DUPLICATE;
282 }
283 }
284
285 if ($row['status'] == 'ignoreP' && $row['ignoreP'] == $ignoreP) { $this->totalIgnoredIssues++; return self::ISSUE_IGNOREP; } //Always ignore
286 else if ($row['status'] == 'ignoreC' && $row['ignoreC'] == $ignoreC) { $this->totalIgnoredIssues++; return self::ISSUE_IGNOREC; } //Unchanged, ignore
287 else if ($row['status'] == 'ignoreC') {
288 $updateID = $row['id']; //Re-use the existing issue row
289 break;
290 }
291 }
292
293 if ($group != 'pending') {
294 if (!array_key_exists($severity, $this->totalIssuesBySeverity)) {
295 $this->totalIssuesBySeverity[$severity] = 0;
296 }
297 $this->totalIssuesBySeverity[$severity]++;
298 $this->totalIssues++;
299 if (empty($this->maxIssues) || $this->totalIssues <= $this->maxIssues)
300 {
301 $this->newIssues[] = array(
302 'type' => $type,
303 'severity' => $severity,
304 'ignoreP' => $ignoreP,
305 'ignoreC' => $ignoreC,
306 'shortMsg' => $shortMsg,
307 'longMsg' => $longMsg,
308 'tmplData' => $templateData
309 );
310 }
311 }
312
313 if (isset($updateID)) {
314 if ($group !== 'pending' && wfCentral::isConnected()) {
315 wfCentral::sendIssue(array(
316 'id' => $updateID,
317 'lastUpdated' => time(),
318 'type' => $type,
319 'severity' => $severity,
320 'ignoreP' => $ignoreP,
321 'ignoreC' => $ignoreC,
322 'shortMsg' => $shortMsg,
323 'longMsg' => $longMsg,
324 'data' => $templateData,
325 ));
326 }
327
328 $this->getDB()->queryWrite(
329 "UPDATE {$table} SET lastUpdated = UNIX_TIMESTAMP(), status = '%s', type = '%s', severity = %d, ignoreP = '%s', ignoreC = '%s', shortMsg = '%s', longMsg = '%s', data = '%s' WHERE id = %d",
330 'new',
331 $type,
332 $severity,
333 $ignoreP,
334 $ignoreC,
335 $shortMsg,
336 $longMsg,
337 serialize($templateData),
338 $updateID);
339
340
341 return self::ISSUE_UPDATED;
342 }
343
344 $this->getDB()->queryWrite("INSERT INTO {$table} (time, lastUpdated, status, type, severity, ignoreP, ignoreC, shortMsg, longMsg, data) VALUES (UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s')",
345 'new',
346 $type,
347 $severity,
348 $ignoreP,
349 $ignoreC,
350 $shortMsg,
351 $longMsg,
352 serialize($templateData));
353
354 if ($group !== 'pending' && wfCentral::isConnected()) {
355 global $wpdb;
356 wfCentral::sendIssue(array(
357 'id' => $wpdb->insert_id,
358 'status' => 'new',
359 'time' => time(),
360 'lastUpdated' => time(),
361 'type' => $type,
362 'severity' => $severity,
363 'ignoreP' => $ignoreP,
364 'ignoreC' => $ignoreC,
365 'shortMsg' => $shortMsg,
366 'longMsg' => $longMsg,
367 'data' => $templateData,
368 ));
369 }
370
371 return self::ISSUE_ADDED;
372 }
373 public function deleteIgnored(){
374 if (wfCentral::isConnected()) {
375 $result = $this->getDB()->querySelect("SELECT id from " . $this->issuesTable . " where status='ignoreP' or status='ignoreC'");
376 $issues = array();
377 foreach ($result as $row) {
378 $issues[] = $row['id'];
379 }
380 wfCentral::deleteIssues($issues);
381 }
382
383 $this->getDB()->queryWrite("delete from " . $this->issuesTable . " where status='ignoreP' or status='ignoreC'");
384 }
385 public function deleteNew($types = null) {
386 if (!is_array($types)) {
387 if (wfCentral::isConnected()) {
388 wfCentral::deleteNewIssues();
389 }
390
391 $this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new'");
392 }
393 else {
394 if (wfCentral::isConnected()) {
395 wfCentral::deleteIssueTypes($types, 'new');
396 }
397
398 $query = "DELETE FROM {$this->issuesTable} WHERE status = 'new' AND type IN (" . implode(',', array_fill(0, count($types), "'%s'")) . ")";
399 array_unshift($types, $query);
400 call_user_func_array(array($this->getDB(), 'queryWrite'), $types);
401 }
402 }
403 public function ignoreAllNew(){
404 if (wfCentral::isConnected()) {
405 $issues = $this->getDB()->querySelect('SELECT * FROM ' . $this->issuesTable . ' WHERE status=\'new\'');
406 if ($issues) {
407 wfCentral::sendIssues($issues);
408 }
409 }
410
411 $this->getDB()->queryWrite("update " . $this->issuesTable . " set status='ignoreC' where status='new'");
412 }
413 public function emailNewIssues($timeLimitReached = false, $scanController = false){
414 $level = wfConfig::getAlertLevel();
415 $emails = wfConfig::getAlertEmails();
416 if (!count($emails)) {
417 return;
418 }
419
420 $shortSiteURL = preg_replace('/^https?:\/\//i', '', site_url());
421 $subject = "[Wordfence Alert] Problems found on $shortSiteURL";
422
423 if(sizeof($emails) < 1){ return; }
424 if($level < 1){ return; }
425 $needsToAlert = false;
426 foreach ($this->totalIssuesBySeverity as $issueSeverity => $totalIssuesBySeverity) {
427 if ($issueSeverity >= $level && $totalIssuesBySeverity > 0) {
428 $needsToAlert = true;
429 break;
430 }
431 }
432 if (!$needsToAlert) {
433 return;
434 }
435 $emailedIssues = wfConfig::get_ser('emailedIssuesList', array());
436 if(! is_array($emailedIssues)){
437 $emailedIssues = array();
438 }
439 $overflowCount = $this->totalIssues - count($this->newIssues);
440 $finalIssues = array();
441 $previousIssues = array();
442 foreach($this->newIssues as $newIssue){
443 $alreadyEmailed = false;
444 foreach($emailedIssues as $emailedIssue){
445 if($newIssue['ignoreP'] == $emailedIssue['ignoreP'] || $newIssue['ignoreC'] == $emailedIssue['ignoreC']){
446 $alreadyEmailed = true;
447 $previousIssues[] = $newIssue;
448 break;
449 }
450 }
451 if(! $alreadyEmailed){
452 $finalIssues[] = $newIssue;
453 }
454 else {
455 $overflowCount--;
456 }
457 }
458 if(sizeof($finalIssues) < 1){ return; }
459
460 $this->newIssues = array();
461 $this->totalIssues = 0;
462
463 $totals = array();
464 foreach($finalIssues as $i){
465 $emailedIssues[] = array( 'ignoreC' => $i['ignoreC'], 'ignoreP' => $i['ignoreP'] );
466 if (!array_key_exists($i['severity'], $totals)) {
467 $totals[$i['severity']] = 0;
468 }
469 $totals[$i['severity']]++;
470 }
471 wfConfig::set_ser('emailedIssuesList', $emailedIssues, false, wfConfig::DONT_AUTOLOAD);
472 $needsToAlert = false;
473 foreach ($totals as $issueSeverity => $totalIssuesBySeverity) {
474 if ($issueSeverity >= $level && $totalIssuesBySeverity > 0) {
475 $needsToAlert = true;
476 break;
477 }
478 }
479 if (!$needsToAlert) {
480 return;
481 }
482
483 $content = wfUtils::tmpl('email_newIssues.php', array(
484 'isPaid' => wfConfig::get('isPaid'),
485 'issues' => $finalIssues,
486 'previousIssues' => $previousIssues,
487 'totals' => $totals,
488 'level' => $level,
489 'issuesNotShown' => $overflowCount,
490 'adminURL' => get_admin_url(),
491 'timeLimitReached' => $timeLimitReached,
492 'scanController' => ($scanController ? $scanController : wfScanner::shared()),
493 ));
494
495 foreach ($emails as $email) {
496 $uniqueContent = str_replace('<!-- ##UNSUBSCRIBE## -->', wp_kses(sprintf(__('No longer an administrator for this site? <a href="%s" target="_blank">Click here</a> to stop receiving security alerts.', 'wordfence'), wfUtils::getSiteBaseURL() . '?_wfsf=removeAlertEmail&jwt=' . wfUtils::generateJWT(array('email' => $email))), array('a'=>array('href'=>array(), 'target'=>array()))), $content);
497 wp_mail($email, $subject, $uniqueContent, 'Content-type: text/html');
498 }
499 }
500 public function clearEmailedStatus($issues) {
501 if (empty($issues)) { return; }
502
503 $emailed_issues = wfConfig::get_ser('emailedIssuesList', array());
504 if (!is_array($emailed_issues)) { return; }
505
506 $updated = array();
507 foreach ($emailed_issues as $ei) {
508 $cleared = false;
509 foreach ($issues as $issue) {
510 if ($issue['ignoreP'] == $ei['ignoreP'] || $issue['ignoreC'] == $ei['ignoreC']) {
511 //Discard this one
512 $cleared = true;
513 }
514 }
515 if (!$cleared) {
516 $updated[] = $ei;
517 }
518 }
519
520 wfConfig::set_ser('emailedIssuesList', $updated, false, wfConfig::DONT_AUTOLOAD);
521 }
522
523 public function deleteIssue($id) {
524 $this->deleteIssues(array($id));
525 }
526
527 public function deleteIssues($ids) {
528 $this->clearEmailedStatus($this->getIssuesByIDs($ids));
529 $idString = implode(',', array_map(function($id) { return intval($id); }, $ids));
530 $this->getDB()->queryWrite("DELETE FROM `{$this->issuesTable}` WHERE `id` IN ({$idString})");
531 if (wfCentral::isConnected()) {
532 wfCentral::deleteIssues($ids);
533 }
534 }
535
536 public function deleteUpdateIssues($type) {
537 $issues = $this->getDB()->querySelect("SELECT id, status, ignoreP, ignoreC FROM {$this->issuesTable} WHERE status = 'new' AND type = '%s'", $type);
538 $this->clearEmailedStatus($issues);
539
540 $this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new' AND type = '%s'", $type);
541
542 if (wfCentral::isConnected()) {
543 wfCentral::deleteIssueTypes(array($type));
544 }
545 }
546
547 public function deleteAllUpdateIssues() {
548 $issues = $this->getDB()->querySelect("SELECT id, status, ignoreP, ignoreC FROM {$this->issuesTable} WHERE status = 'new' AND (type = 'wfUpgrade' OR type = 'wfUpgradeError' OR type = 'wfPluginUpgrade' OR type = 'wfThemeUpgrade')");
549 $this->clearEmailedStatus($issues);
550
551 $this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new' AND (type = 'wfUpgrade' OR type = 'wfUpgradeError' OR type = 'wfPluginUpgrade' OR type = 'wfThemeUpgrade')");
552
553 if (wfCentral::isConnected()) {
554 wfCentral::deleteIssueTypes(array('wfUpgrade', 'wfUpgradeError', 'wfPluginUpgrade', 'wfThemeUpgrade'));
555 }
556 }
557
558 public function updateIssue($id, $status){ //ignoreC, ignoreP, delete or new
559 if($status == 'delete'){
560 if (wfCentral::isConnected()) {
561 wfCentral::deleteIssue($id);
562 }
563 $this->clearEmailedStatus(array($this->getIssueByID($id)));
564 $this->getDB()->queryWrite("delete from " . $this->issuesTable . " where id=%d", $id);
565 } else if($status == 'ignoreC' || $status == 'ignoreP' || $status == 'new'){
566 $this->getDB()->queryWrite("update " . $this->issuesTable . " set status='%s' where id=%d", $status, $id);
567
568 if (wfCentral::isConnected()) {
569 $issue = $this->getDB()->querySelect('SELECT * FROM ' . $this->issuesTable . ' where id=%d', $id);
570 if ($issue) {
571 wfCentral::sendIssues($issue);
572 }
573 }
574 }
575 }
576 public function getIssueByID($id) {
577 $rec = $this->getDB()->querySingleRec("select * from " . $this->issuesTable . " where id=%d", $id);
578 $rec['data'] = unserialize($rec['data']);
579 return $rec;
580 }
581 public function getIssuesByIDs($ids) {
582 $result = array();
583 $idString = implode(',', array_map(function($id) { return intval($id); }, $ids));
584 $rows = $this->getDB()->querySelect("SELECT * FROM `{$this->issuesTable}` WHERE `id` IN ({$idString})");
585 foreach ($rows as $r) {
586 $result[] = array_merge($r, array('data' => unserialize($r['data'])));
587 }
588 return $result;
589 }
590 public function getIssueCounts() {
591 global $wpdb;
592 $counts = $wpdb->get_results('SELECT COUNT(*) AS c, status FROM ' . $this->issuesTable . ' WHERE status = "new" OR status = "ignoreP" OR status = "ignoreC" GROUP BY status', ARRAY_A);
593 $result = array();
594 foreach ($counts as $row) {
595 $result[$row['status']] = $row['c'];
596 }
597 return $result;
598 }
599 public function getIssues($offset = 0, $limit = 100, $ignoredOffset = 0, $ignoredLimit = 100) {
600 /** @var wpdb $wpdb */
601 global $wpdb;
602
603 $siteCleaningTypes = array('file', 'checkGSB', 'checkSpamIP', 'commentBadURL', 'knownfile', 'optionBadURL', 'postBadTitle', 'postBadURL', 'spamvertizeCheck', 'suspiciousAdminUsers');
604 $sortTagging = 'CASE';
605 foreach ($siteCleaningTypes as $index => $t) {
606 $sortTagging .= ' WHEN type = \'' . esc_sql($t) . '\' THEN ' . ((int) $index);
607 }
608 $sortTagging .= ' ELSE 999 END';
609
610 $ret = array(
611 'new' => array(),
612 'ignored' => array()
613 );
614 $userIni = ini_get('user_ini.filename');
615 $q1 = $this->getDB()->querySelect("SELECT *, {$sortTagging} AS sortTag FROM " . $this->issuesTable . " WHERE status = 'new' ORDER BY severity DESC, sortTag ASC, type ASC, time DESC LIMIT %d,%d", $offset, $limit);
616 $q2 = $this->getDB()->querySelect("SELECT *, {$sortTagging} AS sortTag FROM " . $this->issuesTable . " WHERE status = 'ignoreP' OR status = 'ignoreC' ORDER BY severity DESC, sortTag ASC, type ASC, time DESC LIMIT %d,%d", $ignoredOffset, $ignoredLimit);
617 $q = array_merge($q1, $q2);
618 foreach($q as $i){
619 $i['data'] = unserialize($i['data']);
620 $i['timeAgo'] = wfUtils::makeTimeAgo(time() - $i['time']);
621 $i['displayTime'] = wfUtils::formatLocalTime(get_option('date_format') . ' ' . get_option('time_format'), $i['time']);
622 $i['longMsg'] = wp_kses($i['longMsg'], 'post');
623 if($i['status'] == 'new'){
624 $ret['new'][] = $i;
625 } else if($i['status'] == 'ignoreP' || $i['status'] == 'ignoreC'){
626 $ret['ignored'][] = $i;
627 } else {
628 error_log("Issue has bad status: " . $i['status']);
629 continue;
630 }
631 }
632 foreach($ret as $status => &$issueList){
633 for($i = 0; $i < sizeof($issueList); $i++){
634 if ($issueList[$i]['type'] == 'file' || $issueList[$i]['type'] == 'knownfile') {
635 if (array_key_exists('realFile', $issueList[$i]['data'])) {
636 $localFile = $issueList[$i]['data']['realFile'];
637 $issueList[$i]['data']['realFileToken'] = self::generateRealFileToken($localFile);
638 }
639 else {
640 $localFile = $issueList[$i]['data']['file'];
641 if ($localFile != '.htaccess' && $localFile != $userIni) {
642 $localFile = ABSPATH . '/' . preg_replace('/^[\.\/]+/', '', $localFile);
643 }
644 else {
645 $localFile = ABSPATH . '/' . $localFile;
646 }
647 }
648
649 if(file_exists($localFile)){
650 $issueList[$i]['data']['fileExists'] = true;
651 } else {
652 $issueList[$i]['data']['fileExists'] = '';
653 }
654 }
655 if ($issueList[$i]['type'] == 'database') {
656 $issueList[$i]['data']['optionExists'] = false;
657 if (!empty($issueList[$i]['data']['site_id'])) {
658 $table_options = wfDB::blogTable('options', $issueList[$i]['data']['site_id']);
659 $issueList[$i]['data']['optionExists'] = $wpdb->get_var($wpdb->prepare("SELECT count(*) FROM {$table_options} WHERE option_name = %s", $issueList[$i]['data']['option_name'])) > 0;
660 }
661 }
662 $issueList[$i]['issueIDX'] = $i;
663 if (isset($issueList[$i]['data']['cType'])) {
664 $issueList[$i]['data']['ucType'] = ucwords($issueList[$i]['data']['cType']);
665 }
666 }
667 }
668 return $ret; //array of lists of issues by status
669 }
670 public function getPendingIssues($offset = 0, $limit = 100){
671 /** @var wpdb $wpdb */
672 global $wpdb;
673 $issues = $this->getDB()->querySelect("SELECT * FROM {$this->pendingIssuesTable} ORDER BY id ASC LIMIT %d,%d", $offset, $limit);
674 foreach($issues as &$i){
675 $i['data'] = unserialize($i['data']);
676 }
677 return $issues;
678 }
679 public function getFixableIssueCount() {
680 global $wpdb;
681 $issues = $this->getDB()->querySelect("SELECT * FROM {$this->issuesTable} WHERE data LIKE '%s:6:\"canFix\";b:1;%'");
682 $count = 0;
683 foreach ($issues as $i) {
684 $i['data'] = unserialize($i['data']);
685 if (isset($i['data']['canFix']) && $i['data']['canFix']) {
686 $count++;
687 }
688 }
689 return $count;
690 }
691 public function getDeleteableIssueCount() {
692 global $wpdb;
693 $issues = $this->getDB()->querySelect("SELECT * FROM {$this->issuesTable} WHERE data LIKE '%s:9:\"canDelete\";b:1;%'");
694 $count = 0;
695 foreach ($issues as $i) {
696 $i['data'] = unserialize($i['data']);
697 if (isset($i['data']['canDelete']) && $i['data']['canDelete']) {
698 $count++;
699 }
700 }
701 return $count;
702 }
703 public function getIssueCount() {
704 return (int) $this->getDB()->querySingle("select COUNT(*) from " . $this->issuesTable . " WHERE status = 'new'");
705 }
706 public function getPendingIssueCount() {
707 return (int) $this->getDB()->querySingle("select COUNT(*) from " . $this->pendingIssuesTable . " WHERE status = 'new'");
708 }
709 public function getLastIssueUpdateTimestamp() {
710 return (int) $this->getDB()->querySingle("select MAX(lastUpdated) from " . $this->issuesTable);
711 }
712 public function reconcileUpgradeIssues($report = null, $useCachedValued = false) {
713 if ($report === null) {
714 $report = new wfActivityReport();
715 }
716
717 $updatesNeeded = $report->getUpdatesNeeded($useCachedValued);
718 if ($updatesNeeded) {
719 if (!$updatesNeeded['core']) {
720 $this->deleteUpdateIssues('wfUpgrade');
721 }
722
723 $issueIDs = array();
724 if ($updatesNeeded['plugins']) {
725 $upgradeNames = array();
726 foreach ($updatesNeeded['plugins'] as $p) {
727 $name = $p['Name'];
728 $upgradeNames[$name] = 1;
729 }
730 $upgradeIssues = $this->getDB()->querySelect("SELECT * FROM {$this->issuesTable} WHERE status = 'new' AND type = 'wfPluginUpgrade'");
731 foreach ($upgradeIssues as $issue) {
732 $data = unserialize($issue['data']);
733 $name = $data['Name'];
734 if (!isset($upgradeNames[$name])) { //Some plugins don't have a slug associated with them, so we anchor on the name
735 $issueIDs[] = $issue['id'];
736 }
737 }
738 }
739 else {
740 $this->deleteUpdateIssues('wfPluginUpgrade');
741 }
742
743 if ($updatesNeeded['themes']) {
744 $upgradeNames = array();
745 foreach ($updatesNeeded['themes'] as $t) {
746 $name = $t['Name'];
747 $upgradeNames[$name] = 1;
748 }
749 $upgradeIssues = $this->getDB()->querySelect("SELECT * FROM {$this->issuesTable} WHERE status = 'new' AND type = 'wfThemeUpgrade'");
750 foreach ($upgradeIssues as $issue) {
751 $data = unserialize($issue['data']);
752 $name = $data['Name'];
753 if (!isset($upgradeNames[$name])) { //Some themes don't have a slug associated with them, so we anchor on the name
754 $issueIDs[] = $issue['id'];
755 }
756 }
757 }
758 else {
759 $this->deleteUpdateIssues('wfThemeUpgrade');
760 }
761
762 if (!empty($issueIDs)) {
763 $this->deleteIssues($issueIDs);
764 }
765 }
766 else {
767 $this->deleteAllUpdateIssues();
768 }
769
770 wfScanEngine::refreshScanNotification($this);
771 }
772 private function getDB(){
773 if(! $this->db){
774 $this->db = new wfDB();
775 }
776 return $this->db;
777 }
778
779 /**
780 * @return string
781 */
782 public function getIssuesTable() {
783 return $this->issuesTable;
784 }
785
786 private static function getRealFileTokenKey($realFile) {
787 return 'wf-real-file-' . base64_encode($realFile);
788 }
789
790 private static function generateRealFileToken($realFile) {
791 $key = self::getRealFileTokenKey($realFile);
792 return wp_create_nonce($key);
793 }
794
795 public static function verifyRealFileToken($token, $realFile) {
796 $key = self::getRealFileTokenKey($realFile);
797 return wp_verify_nonce($token, $key);
798 }
799 }