ACC SHELL
<?php
require_once('wfUtils.php');
class wfIssues {
//Possible responses from `addIssue`
const ISSUE_ADDED = 'a';
const ISSUE_UPDATED = 'u';
const ISSUE_DUPLICATE = 'd';
const ISSUE_IGNOREP = 'ip';
const ISSUE_IGNOREC = 'ic';
//Possible status message states
const STATUS_NONE = 'n';
const STATUS_SKIPPED = 's';
const STATUS_IGNORED = 'i';
const STATUS_PROBLEM = 'p';
const STATUS_SECURE = 'r';
const STATUS_FAILED = 'f';
const STATUS_SUCCESS = 'c';
const STATUS_PAIDONLY = 'x';
private $db = false;
//Properties that are serialized on sleep:
private $updateCalled = false;
private $issuesTable = '';
private $pendingIssuesTable = '';
private $maxIssues = 0;
private $newIssues = array();
public $totalIssues = 0;
public $totalCriticalIssues = 0;
public $totalWarningIssues = 0;
public $totalIgnoredIssues = 0;
public static function statusPrep(){
wfConfig::set_ser('wfStatusStartMsgs', array());
wordfence::status(10, 'info', "SUM_PREP:Preparing a new scan.");
wfIssues::updateScanStillRunning();
}
public static function statusStart($message) {
$statusStartMsgs = wfConfig::get_ser('wfStatusStartMsgs', array());
$statusStartMsgs[] = $message;
wfConfig::set_ser('wfStatusStartMsgs', $statusStartMsgs);
wordfence::status(10, 'info', 'SUM_START:' . $message);
wfIssues::updateScanStillRunning();
return count($statusStartMsgs) - 1;
}
public static function statusEnd($index, $state) {
$statusStartMsgs = wfConfig::get_ser('wfStatusStartMsgs', array());
if ($state == self::STATUS_SKIPPED) {
wordfence::status(10, 'info', 'SUM_ENDSKIPPED:' . $statusStartMsgs[$index]);
}
else if ($state == self::STATUS_IGNORED) {
wordfence::status(10, 'info', 'SUM_ENDIGNORED:' . $statusStartMsgs[$index]);
}
else if ($state == self::STATUS_PROBLEM) {
wordfence::status(10, 'info', 'SUM_ENDBAD:' . $statusStartMsgs[$index]);
}
else if ($state == self::STATUS_SECURE) {
wordfence::status(10, 'info', 'SUM_ENDOK:' . $statusStartMsgs[$index]);
}
else if ($state == self::STATUS_FAILED) {
wordfence::status(10, 'info', 'SUM_ENDFAILED:' . $statusStartMsgs[$index]);
}
else if ($state == self::STATUS_SUCCESS) {
wordfence::status(10, 'info', 'SUM_ENDSUCCESS:' . $statusStartMsgs[$index]);
}
wfIssues::updateScanStillRunning();
$statusStartMsgs[$index] = '';
wfConfig::set_ser('wfStatusStartMsgs', $statusStartMsgs);
}
public static function statusEndErr() {
$statusStartMsgs = wfConfig::get_ser('wfStatusStartMsgs', array());
for ($i = 0; $i < count($statusStartMsgs); $i++) {
if (empty($statusStartMsgs[$i]) === false) {
wordfence::status(10, 'info', 'SUM_ENDERR:' . $statusStartMsgs[$i]);
$statusStartMsgs[$i] = '';
}
}
wfIssues::updateScanStillRunning();
}
public static function statusPaidOnly($message) {
wordfence::status(10, 'info', "SUM_PAIDONLY:" . $message);
wfIssues::updateScanStillRunning();
}
public static function statusDisabled($message) {
wordfence::status(10, 'info', "SUM_DISABLED:" . $message);
wfIssues::updateScanStillRunning();
}
public static function updateScanStillRunning($running = true) {
$timestamp = time();
if (!$running) {
$timestamp = 0;
}
wfConfig::set('wf_scanLastStatusTime', $timestamp);
}
/**
* Returns false if the scan has not been detected as failing. If it has, it returns the timestamp of the last status update.
*
* @return bool|int
*/
public static function hasScanFailed() {
if (wfConfig::get('wf_scanLastStatusTime', 0) === 0) {
return false;
}
$threshold = WORDFENCE_SCAN_FAILURE_THRESHOLD;
return (time() > wfConfig::get('wf_scanLastStatusTime', 0) + $threshold) ? wfConfig::get('wf_scanLastStatusTime', 0) : false;
}
public function __sleep(){ //Same order here as vars above
return array('updateCalled', 'issuesTable', 'pendingIssuesTable', 'maxIssues', 'newIssues', 'totalIssues', 'totalCriticalIssues', 'totalWarningIssues', 'totalIgnoredIssues');
}
public function __construct(){
global $wpdb;
$this->issuesTable = $wpdb->base_prefix . 'wfIssues';
$this->pendingIssuesTable = $wpdb->base_prefix . 'wfPendingIssues';
$this->maxIssues = wfConfig::get('scan_maxIssues', 0);
}
public function __wakeup(){
$this->db = new wfDB();
}
public function addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData, $alreadyHashed = false) {
return $this->_addIssue('issue', $type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData, $alreadyHashed);
}
public function addPendingIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData) {
return $this->_addIssue('pending', $type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData);
}
/**
* Create a new issue
*
* @param string $group The issue type (e.g., issue or pending
* @param string $type
* @param int $severity
* @param string $ignoreP string to compare against for permanent ignores
* @param string $ignoreC string to compare against for ignoring until something changes
* @param string $shortMsg
* @param string $longMsg
* @param array $templateData
* @param bool $alreadyHashed If true, don't re-hash $ignoreP and $ignoreC
* @return string One of the ISSUE_ constants
*/
private function _addIssue($group, $type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData, $alreadyHashed = false) {
if ($group == 'pending') {
$table = $this->pendingIssuesTable;
}
else {
$table = $this->issuesTable;
}
if (!$alreadyHashed) {
$ignoreP = md5($ignoreP);
$ignoreC = md5($ignoreC);
}
$results = $this->getDB()->querySelect("SELECT id, status, ignoreP, ignoreC FROM {$table} WHERE (ignoreP = '%s' OR ignoreC = '%s')", $ignoreP, $ignoreC);
foreach ($results as $row) {
if ($row['status'] == 'new' && ($row['ignoreC'] == $ignoreC || $row['ignoreP'] == $ignoreP)) {
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
return self::ISSUE_DUPLICATE;
}
}
if ($row['status'] == 'ignoreP' && $row['ignoreP'] == $ignoreP) { $this->totalIgnoredIssues++; return self::ISSUE_IGNOREP; } //Always ignore
else if ($row['status'] == 'ignoreC' && $row['ignoreC'] == $ignoreC) { $this->totalIgnoredIssues++; return self::ISSUE_IGNOREC; } //Unchanged, ignore
else if ($row['status'] == 'ignoreC') {
$updateID = $row['id']; //Re-use the existing issue row
break;
}
}
if ($group != 'pending') {
if ($severity == 1) {
$this->totalCriticalIssues++;
}
else if ($severity == 2) {
$this->totalWarningIssues++;
}
$this->totalIssues++;
if (empty($this->maxIssues) || $this->totalIssues <= $this->maxIssues)
{
$this->newIssues[] = array(
'type' => $type,
'severity' => $severity,
'ignoreP' => $ignoreP,
'ignoreC' => $ignoreC,
'shortMsg' => $shortMsg,
'longMsg' => $longMsg,
'tmplData' => $templateData
);
}
}
if (isset($updateID)) {
$this->getDB()->queryWrite(
"UPDATE {$table} SET status = '%s', type = '%s', severity = %d, ignoreP = '%s', ignoreC = '%s', shortMsg = '%s', longMsg = '%s', data = '%s' WHERE id = %d",
'new',
$type,
$severity,
$ignoreP,
$ignoreC,
$shortMsg,
$longMsg,
serialize($templateData),
$updateID);
return self::ISSUE_UPDATED;
}
$this->getDB()->queryWrite("INSERT INTO {$table} (time, status, type, severity, ignoreP, ignoreC, shortMsg, longMsg, data) VALUES (unix_timestamp(), '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s')",
'new',
$type,
$severity,
$ignoreP,
$ignoreC,
$shortMsg,
$longMsg,
serialize($templateData));
return self::ISSUE_ADDED;
}
public function deleteIgnored(){
$this->getDB()->queryWrite("delete from " . $this->issuesTable . " where status='ignoreP' or status='ignoreC'");
}
public function deleteNew($types = null) {
if (!is_array($types)) {
$this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new'");
}
else {
$query = "DELETE FROM {$this->issuesTable} WHERE status = 'new' AND type IN (" . implode(',', array_fill(0, count($types), "'%s'")) . ")";
array_unshift($types, $query);
call_user_func_array(array($this->getDB(), 'queryWrite'), $types);
}
}
public function ignoreAllNew(){
$this->getDB()->queryWrite("update " . $this->issuesTable . " set status='ignoreC' where status='new'");
}
public function emailNewIssues($timeLimitReached = false){
$level = wfConfig::getAlertLevel();
$emails = wfConfig::getAlertEmails();
$shortSiteURL = preg_replace('/^https?:\/\//i', '', site_url());
$subject = "[Wordfence Alert] Problems found on $shortSiteURL";
if(sizeof($emails) < 1){ return; }
if($level < 1){ return; }
if($level == 2 && $this->totalCriticalIssues < 1 && $this->totalWarningIssues < 1){ return; }
if($level == 1 && $this->totalCriticalIssues < 1){ return; }
$emailedIssues = wfConfig::get_ser('emailedIssuesList', array());
if(! is_array($emailedIssues)){
$emailedIssues = array();
}
$overflowCount = $this->totalIssues - count($this->newIssues);
$finalIssues = array();
foreach($this->newIssues as $newIssue){
$alreadyEmailed = false;
foreach($emailedIssues as $emailedIssue){
if($newIssue['ignoreP'] == $emailedIssue['ignoreP'] || $newIssue['ignoreC'] == $emailedIssue['ignoreC']){
$alreadyEmailed = true;
break;
}
}
if(! $alreadyEmailed){
$finalIssues[] = $newIssue;
}
else {
$overflowCount--;
}
}
if(sizeof($finalIssues) < 1){ return; }
$this->newIssues = array();
$this->totalIssues = 0;
$totalWarningIssues = 0;
$totalCriticalIssues = 0;
foreach($finalIssues as $i){
$emailedIssues[] = array( 'ignoreC' => $i['ignoreC'], 'ignoreP' => $i['ignoreP'] );
if($i['severity'] == 1){
$totalCriticalIssues++;
} else if($i['severity'] == 2){
$totalWarningIssues++;
}
}
wfConfig::set_ser('emailedIssuesList', $emailedIssues);
if($level == 2 && $totalCriticalIssues < 1 && $totalWarningIssues < 1){ return; }
if($level == 1 && $totalCriticalIssues < 1){ return; }
$content = wfUtils::tmpl('email_newIssues.php', array(
'isPaid' => wfConfig::get('isPaid'),
'issues' => $finalIssues,
'totalCriticalIssues' => $totalCriticalIssues,
'totalWarningIssues' => $totalWarningIssues,
'level' => $level,
'issuesNotShown' => $overflowCount,
'adminURL' => get_admin_url(),
'timeLimitReached' => $timeLimitReached,
));
wp_mail(implode(',', $emails), $subject, $content, 'Content-type: text/html');
}
public function deleteIssue($id){
$this->getDB()->queryWrite("delete from " . $this->issuesTable . " where id=%d", $id);
}
public function updateIssue($id, $status){ //ignoreC, ignoreP, delete or new
if($status == 'delete'){
$this->getDB()->queryWrite("delete from " . $this->issuesTable . " where id=%d", $id);
} else if($status == 'ignoreC' || $status == 'ignoreP' || $status == 'new'){
$this->getDB()->queryWrite("update " . $this->issuesTable . " set status='%s' where id=%d", $status, $id);
}
}
public function getIssueByID($id){
$rec = $this->getDB()->querySingleRec("select * from " . $this->issuesTable . " where id=%d", $id);
$rec['data'] = unserialize($rec['data']);
return $rec;
}
public function getIssueCounts() {
global $wpdb;
$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);
$result = array();
foreach ($counts as $row) {
$result[$row['status']] = $row['c'];
}
return $result;
}
public function getIssues($offset = 0, $limit = 100){
/** @var wpdb $wpdb */
global $wpdb;
$ret = array(
'new' => array(),
'ignored' => array()
);
$userIni = ini_get('user_ini.filename');
$q1 = $this->getDB()->querySelect("select * from " . $this->issuesTable . " order by time desc LIMIT %d,%d", $offset, $limit);
foreach($q1 as $i){
$i['data'] = unserialize($i['data']);
$i['timeAgo'] = wfUtils::makeTimeAgo(time() - $i['time']);
$i['longMsg'] = wp_kses($i['longMsg'], 'post');
if($i['status'] == 'new'){
$ret['new'][] = $i;
} else if($i['status'] == 'ignoreP' || $i['status'] == 'ignoreC'){
$ret['ignored'][] = $i;
} else {
error_log("Issue has bad status: " . $i['status']);
continue;
}
}
foreach($ret as $status => &$issueList){
for($i = 0; $i < sizeof($issueList); $i++){
if ($issueList[$i]['type'] == 'file' || $issueList[$i]['type'] == 'knownfile') {
$localFile = $issueList[$i]['data']['file'];
if ($localFile != '.htaccess' && $localFile != $userIni) {
$localFile = ABSPATH . '/' . preg_replace('/^[\.\/]+/', '', $localFile);
}
else {
$localFile = ABSPATH . '/' . $localFile;
}
if(file_exists($localFile)){
$issueList[$i]['data']['fileExists'] = true;
} else {
$issueList[$i]['data']['fileExists'] = '';
}
}
if ($issueList[$i]['type'] == 'database') {
$issueList[$i]['data']['optionExists'] = false;
if (!empty($issueList[$i]['data']['site_id'])) {
$prefix = $wpdb->get_blog_prefix($issueList[$i]['data']['site_id']);
$issueList[$i]['data']['optionExists'] = $wpdb->get_var($wpdb->prepare("SELECT count(*) FROM {$prefix}options WHERE option_name = %s", $issueList[$i]['data']['option_name'])) > 0;
}
}
$issueList[$i]['issueIDX'] = $i;
}
}
return $ret; //array of lists of issues by status
}
public function getPendingIssues($offset = 0, $limit = 100){
/** @var wpdb $wpdb */
global $wpdb;
$issues = $this->getDB()->querySelect("SELECT * FROM {$this->pendingIssuesTable} ORDER BY id ASC LIMIT %d,%d", $offset, $limit);
foreach($issues as &$i){
$i['data'] = unserialize($i['data']);
}
return $issues;
}
public function getIssueCount() {
return (int) $this->getDB()->querySingle("select COUNT(*) from " . $this->issuesTable . " WHERE status = 'new'");
}
public function getPendingIssueCount() {
return (int) $this->getDB()->querySingle("select COUNT(*) from " . $this->pendingIssuesTable . " WHERE status = 'new'");
}
public function reconcileUpgradeIssues($report = null, $useCachedValued = false) {
if ($report === null) {
$report = new wfActivityReport();
}
$updatesNeeded = $report->getUpdatesNeeded($useCachedValued);
if ($updatesNeeded) {
if (!$updatesNeeded['core']) {
$this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new' AND type = 'wfUpgrade'");
}
if ($updatesNeeded['plugins']) {
$upgradeNames = array();
foreach ($updatesNeeded['plugins'] as $p) {
$name = $p['Name'];
$upgradeNames[$name] = 1;
}
$upgradeIssues = $this->getDB()->querySelect("SELECT * FROM {$this->issuesTable} WHERE status = 'new' AND type = 'wfPluginUpgrade'");
foreach ($upgradeIssues as $issue) {
$data = unserialize($issue['data']);
$name = $data['Name'];
if (!isset($upgradeNames[$name])) { //Some plugins don't have a slug associated with them, so we anchor on the name
$this->deleteIssue($issue['id']);
}
}
}
else {
$this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new' AND type = 'wfPluginUpgrade'");
}
if ($updatesNeeded['themes']) {
$upgradeNames = array();
foreach ($updatesNeeded['themes'] as $t) {
$name = $t['Name'];
$upgradeNames[$name] = 1;
}
$upgradeIssues = $this->getDB()->querySelect("SELECT * FROM {$this->issuesTable} WHERE status = 'new' AND type = 'wfThemeUpgrade'");
foreach ($upgradeIssues as $issue) {
$data = unserialize($issue['data']);
$name = $data['Name'];
if (!isset($upgradeNames[$name])) { //Some themes don't have a slug associated with them, so we anchor on the name
$this->deleteIssue($issue['id']);
}
}
}
else {
$this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new' AND type = 'wfThemeUpgrade'");
}
}
else {
$this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new' AND (type = 'wfUpgrade' OR type = 'wfPluginUpgrade' OR type = 'wfThemeUpgrade')");
}
wfScanEngine::refreshScanNotification($this);
}
public function updateSummaryItem($key, $val){
$arr = wfConfig::get_ser('wf_summaryItems', array());
$arr[$key] = $val;
$arr['lastUpdate'] = time();
wfConfig::set_ser('wf_summaryItems', $arr);
}
public function getSummaryItem($key){
$arr = wfConfig::get_ser('wf_summaryItems', array());
if(array_key_exists($key, $arr)){
return $arr[$key];
} else { return ''; }
}
public function summaryUpdateRequired(){
$last = $this->getSummaryItem('lastUpdate');
if( (! $last) || (time() - $last > (86400 * 7))){
return true;
}
return false;
}
public function getSummaryItems(){
if(! $this->updateCalled){
$this->updateCalled = true;
$this->updateSummaryItems();
}
$arr = wfConfig::get_ser('wf_summaryItems', array());
//$arr['scanTimeAgo'] = wfUtils::makeTimeAgo(sprintf('%.0f', time() - $arr['scanTime']));
$arr['scanRunning'] = wfUtils::isScanRunning() ? '1' : '0';
$arr['scheduledScansEnabled'] = wfConfig::get('scheduledScansEnabled');
$secsToGo = wp_next_scheduled('wordfence_scheduled_scan') - time();
if($secsToGo < 1){
$nextRun = 'now';
} else {
$nextRun = wfUtils::makeTimeAgo($secsToGo) . ' from now';
}
$arr['nextRun'] = $nextRun;
$arr['totalCritical'] = $this->getDB()->querySingle("select count(*) as cnt from " . $this->issuesTable . " where status='new' and severity=1");
$arr['totalWarning'] = $this->getDB()->querySingle("select count(*) as cnt from " . $this->issuesTable . " where status='new' and severity=2");
return $arr;
}
private function updateSummaryItems(){
global $wpdb;
$dat = array();
$users = $wpdb->get_col("SELECT $wpdb->users.ID FROM $wpdb->users");
$dat['totalUsers'] = sizeof($users);
$res1 = $wpdb->get_col("SELECT count(*) as cnt FROM $wpdb->posts where post_type='page' and post_status NOT IN ('auto-draft')"); $dat['totalPages'] = $res1['0'];
$res1 = $wpdb->get_col("SELECT count(*) as cnt FROM $wpdb->posts where post_type='post' and post_status NOT IN ('auto-draft')"); $dat['totalPosts'] = $res1['0'];
$res1 = $wpdb->get_col("SELECT count(*) as cnt FROM $wpdb->comments"); $dat['totalComments'] = $res1['0'];
$res1 = $wpdb->get_col("SELECT count(*) as cnt FROM $wpdb->term_taxonomy where taxonomy='category'"); $dat['totalCategories'] = $res1['0'];
$res1 = $wpdb->get_col("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE()"); $dat['totalTables'] = sizeof($res1);
$totalRows = 0;
foreach($res1 as $table){
$res2 = $wpdb->get_col("select count(*) from `$table`");
if(isset($res2[0]) ){
$totalRows += $res2[0];
}
}
$dat['totalRows'] = $totalRows;
$arr = wfConfig::get_ser('wf_summaryItems', array());
foreach($dat as $key => $val){
$arr[$key] = $val;
}
wfConfig::set_ser('wf_summaryItems', $arr);
}
public function setScanTimeNow(){
$this->updateSummaryItem('scanTime', microtime(true));
}
public function getScanTime(){
return $this->getSummaryItem('scanTime');
}
private function getDB(){
if(! $this->db){
$this->db = new wfDB();
}
return $this->db;
}
}
?>
ACC SHELL 2018