public static function runInstall()
    {
        if (self::$runInstallCalled) {
            return;
        }
        self::$runInstallCalled = true;
        if (function_exists('ignore_user_abort')) {
            ignore_user_abort(true);
        }
        $previous_version = get_option('wordfence_version', '0.0.0');
        update_option('wordfence_version', WORDFENCE_VERSION);
        //In case we have a fatal error we don't want to keep running install.
        //EVERYTHING HERE MUST BE IDEMPOTENT
        //Remove old legacy cron job if exists
        wp_clear_scheduled_hook('wordfence_scheduled_scan');
        $schema = new wfSchema();
        $schema->createAll();
        //if not exists
        /** @var wpdb $wpdb */
        global $wpdb;
        //6.1.15
        $configTable = "{$wpdb->base_prefix}wfConfig";
        $hasAutoload = $wpdb->get_col($wpdb->prepare(<<<SQL
SELECT * FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE()
AND COLUMN_NAME='autoload'
AND TABLE_NAME=%s
SQL
, $configTable));
        if (!$hasAutoload) {
            $wpdb->query("ALTER TABLE {$configTable} ADD COLUMN autoload ENUM('no', 'yes') NOT NULL DEFAULT 'yes'");
            $wpdb->query("UPDATE {$configTable} SET autoload = 'no' WHERE name = 'wfsd_engine' OR name LIKE 'wordfence_chunked_%'");
        }
        wfConfig::setDefaults();
        //If not set
        $restOfSite = wfConfig::get('cbl_restOfSiteBlocked', 'notset');
        if ($restOfSite == 'notset') {
            wfConfig::set('cbl_restOfSiteBlocked', '1');
        }
        //Install new schedule. If schedule config is blank it will install the default 'auto' schedule.
        wordfence::scheduleScans();
        if (wfConfig::get('autoUpdate') == '1') {
            wfConfig::enableAutoUpdate();
            //Sets up the cron
        }
        if (!wfConfig::get('apiKey')) {
            $api = new wfAPI('', wfUtils::getWPVersion());
            try {
                $keyData = $api->call('get_anon_api_key');
                if ($keyData['ok'] && $keyData['apiKey']) {
                    wfConfig::set('apiKey', $keyData['apiKey']);
                } else {
                    throw new Exception("Could not understand the response we received from the Wordfence servers when applying for a free API key.");
                }
            } catch (Exception $e) {
                error_log("Could not fetch free API key from Wordfence: " . $e->getMessage());
                return;
            }
        }
        wp_clear_scheduled_hook('wordfence_daily_cron');
        wp_clear_scheduled_hook('wordfence_hourly_cron');
        if (is_main_site()) {
            wp_schedule_event(time(), 'daily', 'wordfence_daily_cron');
            //'daily'
            wp_schedule_event(time(), 'hourly', 'wordfence_hourly_cron');
        }
        $db = new wfDB();
        if ($db->columnExists('wfHits', 'HTTPHeaders')) {
            //Upgrade from 3.0.4
            $prefix = $wpdb->base_prefix;
            $count = $db->querySingle("select count(*) as cnt from {$prefix}" . "wfHits");
            if ($count > 20000) {
                $db->queryWrite("delete from {$prefix}" . "wfHits order by id asc limit " . ($count - 20000));
            }
            $db->dropColumn('wfHits', 'HTTPHeaders');
        }
        //Upgrading from 1.5.6 or earlier needs:
        $db->createKeyIfNotExists('wfStatus', 'level', 'k2');
        if (wfConfig::get('isPaid') == 'free') {
            wfConfig::set('isPaid', '');
        }
        //End upgrade from 1.5.6
        $prefix = $wpdb->base_prefix;
        $db->queryWriteIgnoreError("alter table {$prefix}" . "wfConfig modify column val longblob");
        $db->queryWriteIgnoreError("alter table {$prefix}" . "wfBlocks add column permanent tinyint UNSIGNED default 0");
        $db->queryWriteIgnoreError("alter table {$prefix}" . "wfStatus modify column msg varchar(1000) NOT NULL");
        //3.1.2 to 3.1.4
        $db->queryWriteIgnoreError("alter table {$prefix}" . "wfBlocks modify column blockedTime bigint signed NOT NULL");
        //3.2.1 to 3.2.2
        $db->queryWriteIgnoreError("alter table {$prefix}" . "wfLockedOut modify column blockedTime bigint signed NOT NULL");
        $db->queryWriteIgnoreError("drop table if exists {$prefix}" . "wfFileQueue");
        $db->queryWriteIgnoreError("drop table if exists {$prefix}" . "wfFileChanges");
        $result = $wpdb->get_row("SHOW FIELDS FROM {$prefix}wfStatus where field = 'id'");
        if (!$result || strtolower($result->Key) != 'pri') {
            //Adding primary key to this table because some backup apps use primary key during backup.
            $db->queryWriteIgnoreError("alter table {$prefix}wfStatus add id bigint UNSIGNED NOT NULL auto_increment PRIMARY KEY");
        }
        $optScanEnabled = $db->querySingle("select val from {$prefix}" . "wfConfig where name='scansEnabled_options'");
        if ($optScanEnabled != '0' && $optScanEnabled != '1') {
            $db->queryWrite("update {$prefix}" . "wfConfig set val='1' where name='scansEnabled_options'");
        }
        $optScanEnabled = $db->querySingle("select val from {$prefix}" . "wfConfig where name='scansEnabled_heartbleed'");
        if ($optScanEnabled != '0' && $optScanEnabled != '1') {
            //Enable heartbleed if no value is set.
            wfConfig::set('scansEnabled_heartbleed', 1);
        }
        if (wfConfig::get('cacheType') == 'php' || wfConfig::get('cacheType') == 'falcon') {
            wfCache::removeCacheDirectoryHtaccess();
        }
        // IPv6 schema changes for 6.0.1
        $tables_with_ips = array('wfCrawlers', 'wfBadLeechers', 'wfBlockedIPLog', 'wfBlocks', 'wfHits', 'wfLeechers', 'wfLockedOut', 'wfLocs', 'wfLogins', 'wfReverseCache', 'wfScanners', 'wfThrottleLog', 'wfVulnScanners');
        foreach ($tables_with_ips as $ip_table) {
            $result = $wpdb->get_row("SHOW FIELDS FROM {$prefix}{$ip_table} where field = 'IP'");
            if (!$result || strtolower($result->Type) == 'binary(16)') {
                continue;
            }
            $db->queryWriteIgnoreError("ALTER TABLE {$prefix}{$ip_table} MODIFY IP BINARY(16)");
            // Just to be sure we don't corrupt the data if the alter fails.
            $result = $wpdb->get_row("SHOW FIELDS FROM {$prefix}{$ip_table} where field = 'IP'");
            if (!$result || strtolower($result->Type) != 'binary(16)') {
                continue;
            }
            $db->queryWriteIgnoreError("UPDATE {$prefix}{$ip_table} SET IP = CONCAT(LPAD(CHAR(0xff, 0xff), 12, CHAR(0)), LPAD(\n\tCHAR(\n\t\tCAST(IP as UNSIGNED) >> 24 & 0xFF,\n\t\tCAST(IP as UNSIGNED) >> 16 & 0xFF,\n\t\tCAST(IP as UNSIGNED) >> 8 & 0xFF,\n\t\tCAST(IP as UNSIGNED) & 0xFF\n\t),\n\t4,\n\tCHAR(0)\n))");
        }
        // Fix the data in the country column.
        // TODO: add version check so this doesn't run on every update.
        $ip_results = $wpdb->get_results("SELECT * FROM `{$prefix}wfBlockedIPLog` GROUP BY IP");
        if ($ip_results) {
            foreach ($ip_results as $ip_row) {
                $wpdb->query($wpdb->prepare("UPDATE `{$prefix}wfBlockedIPLog` SET countryCode = %s WHERE IP = %s", wfUtils::IP2Country(wfUtils::inet_ntop($ip_row->IP)), $ip_row->IP));
            }
        }
        if (wfConfig::get('other_hideWPVersion')) {
            wfUtils::hideReadme();
        }
        $colsFor610 = array('attackLogTime' => '`attackLogTime` double(17,6) unsigned NOT NULL AFTER `id`', 'statusCode' => '`statusCode` int(11) NOT NULL DEFAULT 0 AFTER `jsRun`', 'action' => "`action` varchar(64) NOT NULL DEFAULT '' AFTER `UA`", 'actionDescription' => '`actionDescription` text AFTER `action`', 'actionData' => '`actionData` text AFTER `actionDescription`');
        $hitTable = $wpdb->base_prefix . 'wfHits';
        foreach ($colsFor610 as $col => $colDefintion) {
            $count = $wpdb->get_col($wpdb->prepare(<<<SQL
SELECT * FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE()
AND COLUMN_NAME=%s
AND TABLE_NAME=%s
SQL
, $col, $hitTable));
            if (!$count) {
                $wpdb->query("ALTER TABLE {$hitTable} ADD COLUMN {$colDefintion}");
            }
        }
        $has404 = $wpdb->get_col($wpdb->prepare(<<<SQL
SELECT * FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE()
AND COLUMN_NAME='is404'
AND TABLE_NAME=%s
SQL
, $hitTable));
        if ($has404) {
            $wpdb->query(<<<SQL
UPDATE {$hitTable}
SET statusCode= CASE
WHEN is404=1 THEN 404
ELSE 200
END
SQL
);
            $wpdb->query("ALTER TABLE {$hitTable} DROP COLUMN `is404`");
        }
        $loginsTable = "{$wpdb->base_prefix}wfLogins";
        $hasHitID = $wpdb->get_col($wpdb->prepare(<<<SQL
SELECT * FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE()
AND COLUMN_NAME='hitID'
AND TABLE_NAME=%s
SQL
, $loginsTable));
        if (!$hasHitID) {
            $wpdb->query("ALTER TABLE {$loginsTable} ADD COLUMN hitID int(11) DEFAULT NULL AFTER `id`, ADD INDEX(hitID)");
        }
        if (!WFWAF_SUBDIRECTORY_INSTALL) {
            wfWAFConfig::set('wafDisabled', false);
        }
        // Call this before creating the index in cases where the wp-cron isn't running.
        self::trimWfHits();
        $hitsTable = "{$wpdb->base_prefix}wfHits";
        $hasAttackLogTimeIndex = $wpdb->get_var($wpdb->prepare(<<<SQL
SELECT COLUMN_KEY FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = %s
AND COLUMN_NAME = 'attackLogTime'
SQL
, $hitsTable));
        if (!$hasAttackLogTimeIndex) {
            $wpdb->query("ALTER TABLE {$hitsTable} ADD INDEX `attackLogTime` (`attackLogTime`)");
        }
        //6.1.16
        $allowed404s = wfConfig::get('allowed404s', '');
        if (!wfConfig::get('allowed404s6116Migration', false)) {
            if (!preg_match('/(?:^|\\b)browserconfig\\.xml(?:\\b|$)/i', $allowed404s)) {
                if (strlen($allowed404s) > 0) {
                    $allowed404s .= "\n";
                }
                $allowed404s .= "/browserconfig.xml";
                wfConfig::set('allowed404s', $allowed404s);
            }
            wfConfig::set('allowed404s6116Migration', 1);
        }
        if (wfConfig::get('email_summary_interval') == 'biweekly') {
            wfConfig::set('email_summary_interval', 'weekly');
        }
        //6.2.0
        wfConfig::migrateCodeExecutionForUploadsPHP7();
        //6.2.1
        if ((wfConfig::get('cacheType') == 'php' || wfConfig::get('cacheType') == 'falcon') && !wfConfig::get('wf621HadFalconEnabled')) {
            wfConfig::set('wf621HadFalconEnabled', true);
            wp_schedule_single_event(time(), 'wordfence_sendFalconDeprecationNotice');
        }
        //6.2.3
        if (!WFWAF_SUBDIRECTORY_INSTALL && class_exists('wfWAFIPBlocksController')) {
            wfWAFIPBlocksController::synchronizeConfigSettings();
        }
        //Must be the final line
    }