Plugin Directory

Changeset 3334399


Ignore:
Timestamp:
07/26/2025 12:33:24 AM (7 months ago)
Author:
bitslip6
Message:

checkin resolution for CVE-2025-6722

Location:
bitfire/trunk
Files:
14 edited

Legend:

Unmodified
Added
Removed
  • bitfire/trunk/bitfire-admin.php

    r3057065 r3334399  
    1717use const BitFire\FILE_W;
    1818use const BitFire\INFO;
    19 use const BitFire\WAF_INI;
    2019use const BitFire\WAF_ROOT;
    2120use const BitFire\WAF_SRC;
  • bitfire/trunk/bitfire-plugin.php

    r3250587 r3334399  
    2424 * Description:       Only RASP firewall for WordPress. Stop malware, redirects, back-doors and account takeover. 100% bot blocking, backups, malware cleaner.
    2525 * Description:       Only RASP firewall for WordPress. Stop malware, redirects, back-doors and account takeover. 100% bot blocking, backups, malware cleaner.
    26  * Version:           4.5
     26 * Version:           4.6
    2727 * Author:            BitFire.co
    2828 * License:           AGPL-3.0+
  • bitfire/trunk/readme.txt

    r3250641 r3334399  
    55Tags: security, firewall, malware scanner, waf, activity log
    66Requires at least: 5.0.0
    7 Tested up to: 6.7.2
    8 Stable tag: 4.5.0
     7Tested up to: 6.8.2
     8Stable tag: 4.6.0
    99Requires PHP: 7.4
    1010License: AGPLv3 or later
     
    220220
    221221== Changelog ==
     222
     223= 4.6 =
     224 * Address a potential information disclosure issue on miss-configured web servers with WordPress core file changes. Early July WordFence
     225   issued CVE-2025-6722 for BitFire. The WordFence team expressed concern that web servers with directory listings enabled
     226    (a miss-configuration present on <1% of servers globally) AND had also deleted the WordPress core file wp-content/plugins/index.php
     227    could expose a hidden directory containing filtered web log data for BitFire and firewall configuration settings. While this exact
     228    setup requires multiple security misconfigurations to be present on an affected system - WordPress requested the plugin be
     229    removed from the official WordPress plugin repository. After several weeks of discussion about possible resolutions the following
     230    changes were accepted as mitigation. The upgrade will apply these changes automatically:
     231    1. The configuration and log data was moved from /wp-content/plugins/bitfire_RANDOM to /wp-content/uploads/bitfire_RANDOM per the WordPress team
     232    2. The length of the random directory name was increased from 10 characters to 12 characters.
     233    3. A third method of protection was added with a .htaccess file to restrict access for web servers that support .htaccess files.
     234    4. A new check was added to ensure that the file wp-content/uploads/index.php is always present to prevent directory listings
     235       that could expose the path to this hidden directory.
    222236
    223237= 4.5 =
  • bitfire/trunk/src/api.php

    r3250657 r3334399  
    5252use function ThreadFin\HTTP\httpp;
    5353use function ThreadFin\icontains;
     54use function ThreadFin\make_config_loader;
    5455use function ThreadFin\trace;
    5556use function ThreadFin\ƒ_id;
     
    541542    $name = $request->post["param"];
    542543
     544    $config_file = make_config_loader()->run()->read_out();
     545
    543546    // remove all lines with $name[]
    544     $file_no_array = FileData::new(WAF_INI)->read()->filter(function($line) use ($name) {
     547    $file_no_array = FileData::new($config_file)->read()->filter(function($line) use ($name) {
    545548        return ! contains($line, "{$name}[]");
    546549    });
     
    556559
    557560    // write the new file
    558     $effect->file(new FileMod(WAF_INI, join("", $file_no_array->lines)));
     561    $effect->file(new FileMod($config_file, join("", $file_no_array->lines)));
    559562
    560563    // remove the old cache entry and force a new parse
     
    719722    file_recurse(\BitFire\WAF_ROOT."data/bitfire-{$v}", function (string $x) use ($v) {
    720723        $base = basename($x);
    721         if (is_file($x) && $base != "config.ini") {
     724        if (is_file($x) && ends_with($x, "config.ini")) {
    722725            $root = str_replace(\BitFire\WAF_ROOT."data/bitfire-{$v}/", "", $x);
    723726            if (!rename($x, \BitFire\WAF_ROOT . $root)) { debug("unable to rename [%s] - %s", $x, $root); }
     
    782785    $p1 = hash("sha3-256", $request->post['pass1']??'');
    783786    debug("pass sha3-256 %s ", $p1);
    784     $pass = file_replace(\BitFire\WAF_INI, "password = 'default'", "password = '$p1'")->run()->num_errors() == 0;
     787
     788    $config_file = make_config_loader()->run()->read_out();
     789    $pass = file_replace($config_file, "password = 'default'", "password = '$p1'")->run()->num_errors() == 0;
    785790    CacheStorage::get_instance()->save_data("parse_ini", null, -86400);
    786     exit(($pass) ? "success" : "unable to write to: " . \BitFire\WAF_INI);
     791    exit(($pass) ? "success" : "unable to write to: $config_file");
    787792}
    788793
     
    850855function toggle_config_value(\BitFire\Request $request) : Effect {
    851856
     857    $config_file = make_config_loader()->run()->read_out();
    852858    // handle fixing write permissions
    853859    if ($request->post["param"] == "unlock_config") {
    854         $result = chmod(\BitFire\WAF_INI, 0664);
    855         return Effect::new()->api(true, "updated 2", ["file" => WAF_INI, "mode" => 0664, "result" => $result]);
     860        $result = chmod($config_file, 0664);
     861        return Effect::new()->api(true, "updated 2", ["file" => $config_file, "mode" => 0664, "result" => $result]);
    856862    }
    857863    // handle toggle on/off to values
     
    861867    }
    862868
    863     debug("update config [%s]", WAF_INI);
    864 
    865     // ugly fix for missing valid domain line
    866     $config = FileData::new(WAF_INI)->read();
    867 
    868     if ($config->num_lines < 1) {
    869         file_replace(WAF_INI, "; domain_fix_line", "valid_domains[] = \"\"\n; domain_fix_line")->run();
    870     }
     869    debug("update config [%s]", $config_file);
    871870
    872871    // update the config file
     
    12481247    $effect = Effect::new()->exit(true);
    12491248    $weblog_file = get_hidden_file("weblog.bin");
    1250     $fh = fopen($weblog_file, "rb");
     1249    if (file_exists($weblog_file)) {
     1250        $fh = fopen($weblog_file, "rb");
     1251    }
    12511252    if (!$fh) {
    12521253        return $effect->api(false, "unable to open $weblog_file");
  • bitfire/trunk/src/bitfire.php

    r3250641 r3334399  
    264264    }
    265265
     266    $config_file = \ThreadFin\make_config_loader()->run()->read_out();
    266267    $raw_pw = $_SERVER["PHP_AUTH_PW"]??'';
    267268    // read any recovery passwords
     
    274275            // set the password and unlock the config file
    275276            $password = trim(file_get_contents($file));
    276             @chmod(WAF_INI, FILE_RW);
     277            @chmod($config_file, FILE_RW);
    277278        }
    278279    }
  • bitfire/trunk/src/botfilter.php

    r3212335 r3334399  
    12571257
    12581258    // create a new bot if we could not load one from the remote server (bitfire.co down?)
    1259     if (!empty($request) && empty($bot_data)) {
     1259    if (empty($bot_data)) {
    12601260        trace('BOT_NEW');
    12611261        $bot_data = new BotSimpleInfo($agent->trim);
    12621262        $bot_data->agent_trim = $agent->trim;
    1263         $bot_data->ips = [$request->ip => $request->classification];
     1263        if (!empty($request)) {
     1264            $bot_data->ips = [$request->ip => $request->classification];
     1265        }
    12641266        $bot_data->category = 'Auto Learn';
    12651267        $bot_data->name = '';
     
    12771279
    12781280    // bot_data will now have known bots and unknown bots, manual mode will be set
    1279     $bot_data->agent = $agent->agent_text;
     1281    if (empty($bot_data->agent)) {
     1282        $bot_data->agent = $agent->agent_text;
     1283    }
    12801284
    12811285
  • bitfire/trunk/src/dashboard.php

    r3250641 r3334399  
    6868use function ThreadFin\un_json;
    6969use function ThreadFin\at;
     70use function ThreadFin\make_config_loader;
    7071
    7172require_once \BitFire\WAF_SRC . "api.php";
     
    170171{
    171172    static $result = NULL;
     173    $config_file = make_config_loader()->run()->read_out();
    172174    if ($result === NULL) {
    173         $result = is_writeable(\BitFire\WAF_INI) && is_writeable(\BitFire\WAF_ROOT . "config.ini.php");
     175        $result = is_writeable($config_file) && is_writeable(\BitFire\WAF_ROOT . "config.ini.php");
    174176    }
    175177    return ($result) ? " " : "disabled ";
     
    233235
    234236    $content = CFG::str("cms_content_url");
     237    $is_default = CFG::enabled('default_config');
    235238    $variables['license'] = CFG::str('pro_key', "unlicensed");
    236239    $variables['font_path'] = (defined("WPINC") && !empty($content)) ? "$content/plugins/bitfire/public" : "https://bitfire.co/dash/fonts/cerebrisans";
     
    250253    $variables['sym_version'] = BITFIRE_SYM_VER;
    251254    $variables['showfree_class'] = $is_free ? "" : "hidden";
     255    $variables['showerror_class'] = $is_default ? "" : "hidden";
     256
    252257    $variables['hidefree_class'] = $is_free ? "hidden" : "";
    253258    $variables['release'] = (($is_free)  ? "FREE" : "PRO") . " Release " . BITFIRE_SYM_VER;
     
    313318    if ($auth->read_status() == 302) { return; }
    314319
     320    $config_file = make_config_loader()->run()->read_out();
    315321    // load the scanner config
    316322    $raw_scan_config = CFG::arr("malware_config");
    317323    if (empty($raw_scan_config)) {
    318324        $raw_scan_config = ["unknown_core:1", "standard_scan:false", "access_time:1", "random_name_per:50", "line_limit:12000", "freq_limit:768", "random_name_per:75", "fn_freq_limit:512", "fn_line_limit:2048", "fn_random_name_per:60",  "includes:0", "var_fn:1", "call_func:1", "wp_func:0", "extra_regex:"];
    319         $eff = update_ini_fn(ƒixl('\BitFireSvr\array_to_ini', 'malware_config', $raw_scan_config), WAF_INI, true);
     325        $eff = update_ini_fn(ƒixl('\BitFireSvr\array_to_ini', 'malware_config', $raw_scan_config), $config_file, true);
    320326        $eff->run();
    321327    }
     
    419425function serve_settings()
    420426{
     427    $ini_file = make_config_loader()->run()->read_out();   
     428
    421429    // authentication guard
    422430    $auth = validate_auth();
     
    448456
    449457    $policy = CFG::arr("csp_policy");
     458
    450459
    451460    //"dashboard_path" => $dashboard_path,
     
    463472        "cor_policy" => (CFG::str("cor_policy") == "same-site") ? true : false,
    464473        //"theme_css" => file_get_contents(\BitFire\WAF_ROOT."public/theme.min.css"). file_get_contents(\BitFire\WAF_ROOT."public/theme.bundle.css"),
    465         "valid_domains_html" => list_text_inputs("valid_domains"),
    466474        "hide_shmop" => (function_exists("shmop_open")) ? "" : "hidden",
    467475        "hide_apcu" => (function_exists("apcu_store")) ? "" : "hidden",
     
    472480        "disabled" => $disabled,
    473481        "info" => $info,
    474         "waf_ini" => WAF_INI,
     482        "waf_ini" => $ini_file,
    475483        "mfa_class" => (defined("WPINC")) ? "text-muted" : "text-danger"
    476484    )))->run();
     
    624632        $bot = hydrate_any_bot_file($file);
    625633
    626         if (is_array($bot->ips)) {
     634        if (!empty($bot) && is_array($bot->ips)) {
    627635            foreach ($bot->ips as $ip => $unused_class) {
    628636                $ip_counter[$ip] = ($ip_counter[$ip] ?? 0) + 1;
     
    632640
    633641
    634         if (!$bot) {
     642        if (!$bot && file_exists($file)) {
    635643            unlink($file);
    636644            return false;
  • bitfire/trunk/src/server.php

    r3212330 r3334399  
    4545    public $crawler_id; // udger code, to remove from old bot data...
    4646
     47    public $manual_mode;
     48
    4749    public function __construct($agent)
    4850    {
     
    8688use const BitFire\STATUS_OK;
    8789use const BitFire\STATUS_FAIL;
    88 use const BitFire\WAF_INI;
    8990use const BitFire\WAF_ROOT;
    9091use const BitFire\WAF_SRC;
     92use const ThreadFin\CUCKOO_MEM_CHUNK;
     93use const ThreadFin\CUCKOO_STAT_SIZE;
    9194use const ThreadFin\DAY;
    9295use const ThreadFin\DS;
    93 
    94 
    9596
    9697use function BitFire\parse_agent;
     
    118119use function ThreadFin\trace;
    119120use function ThreadFin\at;
     121use function ThreadFin\make_ini_info;
    120122use function ThreadFin\utc_date;
    121123use function ThreadFin\utc_time;
     
    132134const ACCESS_URL_URI = 13;
    133135
    134 const CONFIG_KEY_NAMES = [ "bitfire_enabled","allow_ip_block","security_headers_enabled","enforce_ssl_1year","csp_policy_enabled","csp_default","csp_policy","csp_uri","pro_key","rasp_filesystem","max_cache_age","web_filter_enabled","spam_filter_enabled","xss_block","sql_block","file_block","block_profanity","filtered_logging","allowed_methods","whitelist_enable","blacklist_enable","require_full_browser","honeypot_url","check_domain","valid_domains","valid_domains[]","ignore_bot_urls","rate_limit","rr_5m","cache_type","cookies_enabled","wordfence_emulation","report_file","block_file","debug_file","debug_header","send_errors","dashboard_usage","browser_cookie","dashboard_path","encryption_key","secret","password","cms_root","cms_content_url","cms_content_dir","debug","skip_local_bots","response_code","ip_header","dns_service","short_block_time","medium_block_time","long_block_time","cache_ini_files","root_restrict","configured","log_everything","ip_lookups","remote_tech_allow","tech_public_key","nag_ignore","verify_http_code", "block_profanity"];
    135 
     136const CONFIG_KEY_NAMES = [ "bitfire_enabled","allow_ip_block","security_headers_enabled","enforce_ssl_1year","csp_policy_enabled","csp_default","csp_policy","csp_uri","pro_key","rasp_filesystem","max_cache_age","web_filter_enabled","spam_filter_enabled","xss_block","sql_block","file_block","block_profanity","filtered_logging","allowed_methods","whitelist_enable","blacklist_enable","require_full_browser","honeypot_url","check_domain","ignore_bot_urls","rate_limit","rr_5m","cache_type","cookies_enabled","wordfence_emulation","report_file","block_file","debug_file","debug_header","send_errors","dashboard_usage","browser_cookie","dashboard_path","encryption_key","secret","password","cms_root","cms_content_url","cms_content_dir","debug","skip_local_bots","response_code","ip_header","dns_service","short_block_time","medium_block_time","long_block_time","cache_ini_files","root_restrict","configured","log_everything","ip_lookups","remote_tech_allow","tech_public_key","nag_ignore","verify_http_code", "block_profanity"];
    136137
    137138// helpers
     
    334335function update_ini_fn(callable $fn, string $filename = "", bool $append = false) : Effect {
    335336    if (empty($filename)) {
    336         $filename = (defined("\BitFire\WAF_INI")) ? \BitFire\WAF_INI : make_config_loader()->run()->read_out();
     337        $filename = make_config_loader()->run()->read_out();
     338        // file if we can not find it...
     339        if (empty($filename) || !file_exists($filename)) {
     340            return new Effect();
     341        }
    337342    }
    338343
     
    421426    $fn = (ƒixl("preg_replace", $search, $replace));
    422427
     428    $filename = make_config_loader()->run()->read_out();
     429
    423430    // replace the parameter
    424     if (icontains(FileData::new(WAF_INI)->read()->raw(), "$param")) {
    425         $effect = update_ini_fn($fn, WAF_INI);
     431    if (icontains(FileData::new($filename)->read()->raw(), "$param")) {
     432        $effect = update_ini_fn($fn, $filename);
    426433    }
    427434    // append the parameter
    428435    else {
    429         $effect = update_ini_fn(ƒ_id($replace), WAF_INI, true);
     436        $effect = update_ini_fn(ƒ_id($replace), $filename, true);
    430437    }
    431438
     
    470477    }
    471478
     479    $config_file = make_config_loader()->run()->read_out();
     480    if (empty($config_file)) {
     481        return Effect::$NULL;
     482    }
     483
    472484    // if we already have the content, skip
    473     $content = file_get_contents(WAF_INI);
     485    if (file_exists($config_file)) {
     486        $content = file_get_contents($config_file);
     487    }
     488    if (empty($content)) {
     489        return Effect::$NULL;
     490    }
    474491    if (contains($content, $param)) {
    475492        return Effect::$NULL;
     
    489506    $line = preg_replace("/^\n\n/", "\n", trim($line));
    490507
    491     return Effect::new()->file(new FileMod(WAF_INI, $content . $line));
     508    return Effect::new()->file(new FileMod($config_file, $content . $line));
    492509}
    493510
     
    508525    $ini_test = FileData::new($ini_src);
    509526    // FILESYSTEM GUARDS
    510     if (! $ini_test->exists) { return $e->exit(false, STATUS_EEXIST, "$ini_src does not exist!"); }
     527    if (! $ini_test->exists) { return $e->exit(false, STATUS_EEXIST, "config file $ini_src does not exist!"); }
    511528    if (! $ini_test->readable || ! $ini_test->writeable) {
    512529        if (!@chmod($ini_src, FILE_RW)) {
     
    630647        $info["cookies"] = "not enabled.  none found. <= 1";
    631648    }
    632 
    633     $host = filter_input(INPUT_SERVER, "HTTP_HOST", FILTER_SANITIZE_URL);
    634     $domain = at($host, ":", 0);
    635     $info["domain_value"] = $domain;
    636     $domain = join(".", array_slice(explode(".", $domain), -2));
    637 
    638     $e->chain(update_ini_value("valid_domains[]", $domain, "default"));
    639649
    640650    // configure dynamic exceptions
     
    816826    }
    817827    $ini = "$root/".ini_get("user_ini.filename");
    818     $hta = "$root/.htaccess";
    819828    $extra = "";
    820829    $note = "";
    821     $status = false;
     830    $status = STATUS_FAIL;
     831
     832    // make sure the config has been setup!
     833    $config_file = make_config_loader()->run()->read_out();
    822834
    823835
     
    828840        $ip = filter_input(INPUT_SERVER, CFG::str_up("ip_header", "REMOTE_ADDR"), FILTER_VALIDATE_IP);
    829841        $block_file = \BitFire\BLOCK_DIR . DS . $ip;
    830         $effect->chain(update_config(\BitFire\WAF_INI));
     842        $effect->chain(update_config($config_file));
    831843        $effect->chain(update_ini_value("configured", "true")); // MUST SYNC WITH UPDATE_CONFIG CALLS (WP)
    832844        $effect->chain(Effect::new()->file(new FileMod(\BitFire\WAF_ROOT."install.log", "configured server settings. rare condition.",  FILE_RW, 0, true)));
     
    843855
    844856    // force WordFence compatibility mode if running on WP ENGINE and WordFence is not installed, emulate WordFence
     857    // WPEngine only allows creation of the root file wordfence-waf.php, all others will be blocked.
    845858    // don't run this check if we are being run from the activation page (request will be null)
    846     if (CFG::enabled("wordfence_emulation")) {
    847         $cms_root = cms_root();
    848         $waf_load = "$cms_root/wordfence-waf.php";
    849         $effect->exit(false, STATUS_EEXIST, "WPEngine hosting. UNINSTALL WordFence before enabling always on.");
    850         // we are on wordpress, found the dir and it exists
    851         if (!empty($cms_root) && file_exists($cms_root)) {
    852             // wordfence is not installed, and the autoload file does not exist, lets inject ours
    853             if (!file_exists(CFG::str("cms_content_dir")."plugins/wordfence") && !file_exists($waf_load)) {
    854                 $self = dirname(__DIR__) . "/startup.php";
    855                 if (file_exists($self)) {
    856                     $effect->file(new FileMod($waf_load, "<?"."php include_once '$self'; ?>\n"))
    857                         ->status(STATUS_OK)
    858                         ->out("WPEngine hosting. WordFence WAF emulation enabled. Always on protected.");
    859                 } else {
    860                     $effect->exit(false, STATUS_ENOENT, "Critical error, unable to locate BitFire startup script. Please re-install.");
    861                 }
    862             }
     859    // wp-content located: check on the boot strap file
     860    $waf_load_file = CFG::enabled("wordfence_emulation") ? "$root/wordfence-waf.php" : "$root/bitfire-waf.php";
     861    $extra = "This may take up to " . ini_get("user_ini.cache_ttl") . " seconds to take effect (cache clear time)";
     862
     863    // boot strap exists, lets verify it's content
     864    if (file_exists($waf_load_file)) {
     865        $content = file_get_contents($waf_load_file);
     866        if (\str_contains(strtolower($content), "wordfence")) {
     867            $note = "WordFence WAF already installed at $waf_load_file. Please uninstall WordFence before enabling BitFire always on protection.";
     868        }
     869        else if (! \str_contains(strtolower($content), "bitfire")) {
     870            $note = "Unknown content in $waf_load_file. manually remove this file from " . $_SERVER['DOCUMENT_ROOT'] . "/.user.ini FIRST, THEN SECOND remove $waf_load_file and retry. Consult support at [email protected] for help, this can damage your web site.";
    863871        } else {
    864             $effect->exit(false, STATUS_ENOENT, "Critical error, unable to locate WordPress root directory.");
    865         }
    866     }
    867 
    868     // NOT WPE
     872            $status = STATUS_OK;
     873        }
     874    }
     875    // make the new boot strap file
    869876    else {
    870         // handle NGINX and other cases
    871         $root_path = dirname(__DIR__) . DS;
    872         $content = "\nauto_prepend_file = \"{$root_path}startup.php\"\n";
    873         $status = (\BitFireSvr\install_file($ini, $content) ? true : false);
    874         $file = $ini;
    875         $extra = "This may take up to " . ini_get("user_ini.cache_ttl") . " seconds to take effect (cache clear time)";
    876         $note = ($status == "success") ?
    877             "BitFire was added to auto start in [$ini]. $extra" :
    878             "Unable to add BitFire to auto start.  check permissions on file [$file]";
    879     }
    880 
    881     $effect->chain(Effect::new()->file(new FileMod(\BitFire\WAF_ROOT."install.log", join(", ", debug(null))."\n$note\n", FILE_RW, 0, true)));
     877        $full_path = realpath(WAF_ROOT . "startup.php");
     878        // only make the bootstrap file if we can find the plugin startup file
     879        if ($full_path) {
     880            $content = "<?php\n// THIS FILE LOADS BITFIRE FROM .user.ini NO NOT REMOVE!\nif (file_exists($full_path)) { include_once '$full_path'; } ?>\n";
     881            $effect->file(new FileMod($waf_load_file, $content));
     882            $effect->run();
     883            $status = $effect->num_errors() == 0 ? STATUS_OK : STATUS_FAIL;
     884        } else {
     885            $note = "Critical error, unable to locate BitFire startup script. Please re-install.";
     886        }
     887    }
     888
     889    // bootstrap is looking good and was written successfully!
     890    if ($status == STATUS_OK && empty($effect->read_errors())) {
     891        $note = "BitFire always on protection installed. DO NOT MANUALLY REMOVE THE $waf_load_file FILE! Contact support at [email protected] for support\n";
     892        $ini_content = "\nauto_prepend_file = \"{$root_path}startup.php\"\n";
     893        $status = (\BitFireSvr\install_file($ini, $ini_content) ? STATUS_OK : STATUS_EACCES);
     894    }
     895
     896    $effect->chain(Effect::new()->file(new FileMod(get_hidden_file("install.log"), join(", ", debug(null))."\n$note\n", FILE_RW, 0, true)));
    882897    return $effect->exit(false)->api($status, $note)->status((($status) ? STATUS_OK : STATUS_FAIL));
    883898}
     
    902917        $sem = sem_get(0x228AAAE7, 1, 0660, $opt);
    903918        if (!empty($sem)) { sem_remove($sem); }
     919    }
     920    // remove any dangling shmop cache
     921    if (function_exists('shmop_delete')) {
     922        $token = Config::int("cache_token", 1234560);
     923        $mem_end = ((4096 + 1) * CUCKOO_MEM_CHUNK) + CUCKOO_STAT_SIZE;
     924        $memory = shmop_open($token, "w", 0644, $mem_end);
     925        if (!empty($memory)) {
     926            shmop_delete($memory);
     927        }
    904928    }
    905929
     
    938962    // remove all configuration...
    939963    do_for_each(glob(get_hidden_file("*"), GLOB_NOSORT), [$effect, 'unlink']);
    940     $effect->unlink(get_hidden_file(""));
     964    do_for_each(glob(get_hidden_file("bots", GLOB_NOSORT)), [$effect, 'unlink']);
     965    do_for_each(glob(get_hidden_file("params", GLOB_NOSORT)), [$effect, 'unlink']);
     966    do_for_each(glob(get_hidden_file("quick_map", GLOB_NOSORT)), [$effect, 'unlink']);
     967    $config_dir_path = get_hidden_file("");
     968    $effect->unlink($config_dir_path);
    941969
    942970    $note = ($status == "success") ?
     
    946974    $effect->out(json_encode(array('status' => $status, 'note' => $note, 'method' => $method, 'path' => $path)));
    947975
     976    file_put_contents("/tmp/uninstall.log", print_r($effect, true));
    948977
    949978    return $effect;
     
    10991128    }
    11001129
     1130    // make sure the config has been setup!
     1131    $config_file = make_config_loader()->run()->read_out();
     1132
    11011133    $effect = \BitFireSvr\update_ini_value("bitfire_enabled", "true");
    11021134    debug("configured: [%d]", CFG::enabled("configured"));
    11031135
    1104     $effect->chain(update_config(\BitFire\WAF_INI));
     1136    $effect->chain(update_config($config_file));
    11051137    // make sure we run auto configure and install auto start
    11061138    // update configured after check for install.  allows install on deactivate - activate
     
    12461278}
    12471279
     1280/**
     1281 *
     1282 * perform a basic check to make sure that the config directory is looking good.
     1283 * @param string $path
     1284 * @return bool - true if the config is okay, false if not
     1285 */
     1286function verify_config(string $path) : bool {
     1287    $files = glob($path . "/*");
     1288    if (count($files) < 11) {
     1289        return false;
     1290    }
     1291
     1292    $have_config = $have_bots = false;
     1293    foreach($files as $file) {
     1294        if (ends_with($file, "config.ini")) {
     1295            $have_config = true;
     1296        }
     1297        if (ends_with($file, "bots")) {
     1298            $have_bots = true;
     1299        }
     1300    }
     1301
     1302    return $have_config && $have_bots;
     1303}
     1304
     1305/**
     1306 * Migrate the configuration directory to the new location.
     1307 * @param string $orig_key - the old key - if empty, a new key will be generated
     1308 * @return string - the new key if migration passed, empty string on failure
     1309 */
     1310function migrate_config_dir(string $orig_key) : string {
     1311    // create a new key if we are using the default key
     1312    static $random_key = '';
     1313    if (empty($random_key)) { $random_key = random_str(12); }
     1314
     1315    $new_key = (empty($orig_key)) ? $random_key : $orig_key;
     1316
     1317    // this is already defined in wp-includes/load.php, I'm just paranoid
     1318    if (defined('ABSPATH') && !defined('WP_CONTENT_DIR')) { define('WP_CONTENT_DIR', ABSPATH . 'wp-content'); }
     1319
     1320    // don't allow a double migration
     1321    $migration_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . "bitfire_$new_key";
     1322    if (file_exists($migration_path) && is_dir($migration_path)) {
     1323        // if the migration path already exists, we are done
     1324        return $new_key;
     1325    }
     1326
     1327    $old_path = $old_key = '';
     1328    // look in the old location for the config directory
     1329    $old_configs = glob(WP_CONTENT_DIR . "/plugins/bitfire_??????????");
     1330    // found an old config in the old location, let's move it.
     1331    if (count($old_configs) > 0) {
     1332        // we have an old config, so we will migrate it
     1333        $old_path = realpath($old_configs[0]);
     1334        if ($old_path && verify_config($old_path)) {
     1335            // the old config is good, we are going to use it, lets move the
     1336            // hidden_config directory so we don't accidentally use that some other time
     1337            rename(WAF_ROOT . "hidden_config", WAF_ROOT . "orig_config");
     1338            $tmp     = basename($old_path);
     1339            $parts   = explode("_", $tmp);
     1340            $old_key = $parts[1];
     1341        }
     1342    }
     1343
     1344    // next, look for an "OLD" config in the NEW location...
     1345    $old_configs = glob(WP_CONTENT_DIR . "/uploads/bitfire_????????????");
     1346    // found an old config in the old location, let's move it.
     1347    if (count($old_configs) > 0) {
     1348        // we have an old config, so we will migrate it
     1349        $old_path = realpath($old_configs[0]);
     1350        if ($old_path && verify_config($old_path)) {
     1351            // the old config is good, we are going to use it, lets move the
     1352            // hidden_config directory so we don't accidentally use that some other time
     1353            rename(WAF_ROOT . "hidden_config", WAF_ROOT . "orig_config");
     1354            $tmp     = basename($old_path);
     1355            $parts   = explode("_", $tmp);
     1356            $old_key = $parts[1];
     1357        }
     1358    }
     1359
     1360    // no valid old config found, look for the default config (could be a new install)
     1361    if (empty($old_path) || empty($old_key)) {
     1362        $default_path = WP_CONTENT_DIR . "/plugins/bitfire/hidden_config";
     1363        // we still have an old config, so we will migrate it
     1364        if (file_exists($default_path)) {
     1365            $old_path = realpath($default_path);
     1366        }
     1367    }
     1368
     1369    $success = false;
     1370    // move the old configuration to the new configuration directory
     1371    if ($old_path && file_exists($old_path) && is_dir($old_path)) {
     1372        $success = rename($old_path, $migration_path);
     1373        if ($success) {
     1374            $effect = make_ini_info($new_key)->run();
     1375
     1376            if ($effect->num_errors() <= 0) {
     1377                return $new_key;
     1378            }
     1379        }
     1380    }
     1381
     1382    // looks like ini_info wasn't updated correctly (stat cache?). try to find the already migrated config...
     1383    $old_configs = glob(WP_CONTENT_DIR . DIRECTORY_SEPARATOR . "uploads/bitfire_????????????", GLOB_ONLYDIR);
     1384    if ($old_configs) {
     1385        $config = array_shift($old_configs);
     1386        $secret = substr(basename($config), -12);
     1387        $effect = make_ini_info($secret)->run();
     1388        return $secret;
     1389    }
     1390
     1391    return '';
     1392}
    12481393
    12491394function upgrade($upgrade=null, $extra=null) {
     
    13391484    update_ini_value("tech_public_key", "b39a09eb3095c54fd346a2f3c8a13a8f143a1b3fe26b49c286389c55cec73c3e")->run();
    13401485
    1341     file_put_contents(dirname(WAF_INI)."/browser_allow.json", "{\n'ip': {},\n'ua': {}\n }", LOCK_EX);
     1486    $browser_allow_file = get_hidden_file("browser_allow.json");
     1487    if (!file_exists($browser_allow_file)) {
     1488        file_put_contents($browser_allow_file, "{\n'ip': {},\n'ua': {}\n }", LOCK_EX);
     1489    }
    13421490
    13431491    // convert old format bots to BotSimpleInfo
     
    16631811
    16641812    // write to a temp file and make sure it loads
    1665     file_put_contents($config_file . ".tmp", $file->raw() . "\n");
    1666     $success = parse_ini_file($config_file . ".tmp");
    1667     // there is an error, return the mapped config data, will try again later
    1668     if ($success == false) {
    1669         return $data;
     1813    $raw     = $file->raw();
     1814    $success = false;
     1815    $raw_len = strlen($raw);
     1816    if ($raw_len > 1024) {
     1817        $bytes = file_put_contents($config_file . ".tmp", $raw . "\n");
     1818        if ($bytes < $raw_len) {
     1819            $success = parse_ini_file($config_file . ".tmp");
     1820            // there is an error, return the mapped config data, will try again later
     1821            if ($success == false) {
     1822                return $data;
     1823            }
     1824        }
    16701825    }
    16711826
  • bitfire/trunk/src/util.php

    r3250641 r3334399  
    3434use function BitFire\on_err;
    3535use function BitFireChars\save_config2;
     36use function BitFirePlugin\file_type;
    3637use function BitFireSvr\update_ini_value;
    3738use function ThreadFin\HTTP\http;
     
    386387    static $last = 0; if (is_null($msg)) { return $last; }
    387388    $last = microtime(true); trace($msg);
     389}
     390// emergency logger in case config can not load
     391function emerg(string $msg) :void {
     392    file_put_contents("/tmp/bitfire_emerg.log", date('Y-m-d H:i:s') . " " . $msg . "\n", FILE_APPEND);
    388393}
    389394function dbg($x, $msg="") {$m=htmlspecialchars($msg); $z=(php_sapi_name() == "cli") ? print_r($x, true) : htmlspecialchars(print_r($x, true)); echo "<pre>\n[$m]\n($z)\n" . join("\n", debug(null)) . "\n" . debug(trace(null));
     
    968973        // write all effect files
    969974        foreach ($this->file_outs as $file) {
     975
    970976            assert(!empty($file->filename), "can't write to null file: " . en_json($file));
    971977            $len = strlen($file->content);
     
    10141020                }
    10151021            }
    1016         }
     1022
     1023            // we need to clear the entry from the stat cache so we can read the updated file
     1024            clearstatcache(true, $file->filename);
     1025        }
     1026
    10171027
    10181028        // allowable: backup files, WordFence waf loader if it is an emulation file
     
    15751585function make_config_loader() : Effect {
    15761586    $effect = Effect::new();
    1577     if (defined("BitFire\WAF_INI")) { return $effect->out(\BitFire\WAF_INI)->hide_output(); }
    1578 
    1579 
    1580     // FIRST, lets verify that we already have a valid config
    1581     // if so we bail out early here...
    1582     $parent = dirname(WAF_ROOT, 1);
     1587    static $path = "";
     1588    if (!empty($path)) { return $effect->out($path)->hide_output(); }
     1589
     1590
     1591    // normal path, load the secret key are return the path to the config file
    15831592    $file = \BitFire\WAF_ROOT."ini_info.php";
     1593    $secret_key = "";
    15841594    if (file_exists($file)) {
    1585         $secret_key = "";
     1595        // including the ini_info.php file will define $secret_key and $ini_type
    15861596        include $file;
    1587         $config_file = $parent . "/bitfire_{$secret_key}/config.ini";
    1588         if (file_exists($config_file)) {
    1589             define("BitFire\WAF_INI", $config_file);
     1597        $config_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . "uploads/bitfire_{$secret_key}/config.ini";
     1598        $config_file = realpath($config_path);
     1599        // normal case after secret directory move
     1600        if ($config_file) {
     1601            $path = $config_file;
    15901602            return $effect->out($config_file)->hide_output();
    15911603        }
    15921604    }
    1593     // ini_info was invalid, reset the key
    1594     $secret_key = "";
    1595 
    1596     // we don't know where the config is because there is no ini_info file
    1597     // probably a first run, or a new install, lets find it
    1598     // find all old configs
    1599     $config_dirs = glob("{$parent}/bitfire_??????????");
    1600 
    1601     // get the creation/modification time so we can find most recent
    1602     $dir_with_time = array_map(function($dir) {
    1603         return [ "dir" => $dir, "time" => filemtime($dir) ];
    1604     }, $config_dirs);
    1605     usort($dir_with_time, function($a, $b) {
    1606         return $a["time"] - $b["time"];
    1607     });
    1608     // if we have existing dirs, then lets use the most recent config
    1609     if (count($dir_with_time) > 0) {
    1610         $newest = array_pop($dir_with_time);
    1611         if (preg_match("/bitfire_(\w+)/", $newest["dir"], $matches)) {
    1612             $secret_key = $matches[1];
    1613             while($next = array_pop($dir_with_time)) {
    1614                 // delete all but the newest
    1615                 $effect->unlink($next["dir"]);
    1616             }
    1617         }
    1618     }
    1619     // no old configs, lets create a new one
    1620     if (empty($secret_key)) {
    1621         $secret_key = random_str(10);
    1622         // check if the hidden config has not yet been moved and move it
    1623         $path = $parent . "/bitfire_{$secret_key}/";
    1624         $orig_config = WAF_ROOT . "hidden_config";
    1625         if (file_exists($orig_config)) {
    1626             rename($orig_config, $path);
    1627         }
    1628     }
    1629 
    1630     // we should have a secret key by now, lets update the ini_info file
    1631     if (!empty($secret_key)) {
    1632         $markup = "<?"."php \$secret_key = '$secret_key'; ";
     1605
     1606    // make sure we have the server modification functions available and migrate an old config
     1607    require_once WAF_ROOT . "/src/server.php";
     1608    $new_key = \BitFireSvr\migrate_config_dir($secret_key);
     1609    $config_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . "uploads/bitfire_{$new_key}/config.ini";
     1610
     1611    // we should have a valid config at this location now
     1612    $config_file = realpath($config_path);
     1613
     1614    // normal case after secret directory move
     1615    if (file_exists($config_file)) {
     1616        $path = $config_file;
     1617        return $effect->out($config_file)->hide_output();
     1618    }
     1619
     1620    // something has gone very wrong at this point. (probably file permissions)
     1621    // return the default config file
     1622    $orig_config = WAF_ROOT . "hidden_config";
     1623    return $effect->out($orig_config)->hide_output();
     1624}
     1625
     1626/**
     1627 * create an effect to write the ini_info.php file, will auto determine the cache type if one is not given
     1628 * @param string $secret_key
     1629 * @param string $ini_type
     1630 * @return Effect
     1631 */
     1632function make_ini_info(string $secret_key, string $ini_type = '') : Effect {
     1633    $effect = Effect::new();
     1634    // attempt to use shared memory if available
     1635    if (empty($ini_type)) {
    16331636        if (function_exists("shmop_open")) {
    1634             $markup .= '$ini_type = "shmop";';
    1635         } else {
    1636             $markup .= '$ini_type = "opcache";';
    1637         }
    1638         $effect->file(new FileMod(\BitFire\WAF_ROOT."ini_info.php", $markup));
    1639     }
    1640 
    1641     $path = $parent . "/bitfire_{$secret_key}/";
    1642     define("BitFire\WAF_INI", $path . "config.ini");
    1643     return $effect->out($path . "config.ini")->hide_output();
    1644 }
     1637            $id = @shmop_open(12345, 'c', 0666, 1470008);
     1638            if (!empty($id)) {
     1639                @shmop_delete($id);
     1640                $ini_type = "shmop";
     1641            }
     1642        }
     1643    }
     1644    // if we don't have shmop, then we use opcache
     1645    if (empty($ini_type)) {
     1646        $ini_type = "opcache";
     1647    }
     1648    // if we have a secret key, then we write the ini_info.php file
     1649    $markup = "<?php \$secret_key = '$secret_key'; \$ini_type = '$ini_type';";
     1650    $effect->file(new FileMod(\BitFire\WAF_ROOT."ini_info.php", $markup));
     1651    return $effect;
     1652}
     1653
    16451654
    16461655
     
    16521661 * @return string - the realpath to the file
    16531662 */
    1654 function get_hidden_file(string $file_name, ?string $secret_key = null) : string {
     1663function get_hidden_file(string $file_name) : string {
    16551664    static $path = null;
    16561665    //if (php_sapi_name() === "cli") { return getcwd() . "/$file_name"; }
    16571666
    1658     // use the secret key passed to us
    1659     if (!empty($secret_key)) {
    1660         $parent = dirname(WAF_ROOT, 1);
    1661         $path = realpath($parent . "/bitfire_{$secret_key}/") . "/";
    1662     }
    16631667    // fall back to the secret key in the ini_info file
    16641668    if (empty($path)) {
    1665         $path = dirname(make_config_loader()->read_out(), 1) . "/";
    1666     }
    1667     return $path . $file_name;
     1669        // make config loader returns the path to the config file
     1670        $config_file = make_config_loader()->run()->read_out();
     1671        // get the path to the hidden directory
     1672        $test = dirname($config_file);
     1673        // we found the config, save that path in the static variable for future use
     1674        if (file_exists($test) && WAF_ROOT != $test) {
     1675            $path = $test . DIRECTORY_SEPARATOR;
     1676        }
     1677
     1678        $config_parent = dirname($test) . DIRECTORY_SEPARATOR;
     1679        // make sure that the index.php file exists in the wp-content/uploads directory to hide the configuration
     1680        if ($config_parent . "index.php") {
     1681            file_put_contents($config_parent . "index.php", "<?php\n//Silence is golden.", LOCK_EX);
     1682        }
     1683
     1684    }
     1685    // very odd case (SHOULD NEVER HAPPEN) where the config file is not found, we use the default path
     1686    if (empty($path)) {
     1687        $path = WAF_ROOT;
     1688    }
     1689    // if we are requesting an empty file name, we just return the path (since they are requesting the hidden directory path)
     1690    if ($file_name == "") {
     1691        return $path;
     1692    }
     1693
     1694    $file_path = $path . $file_name;
     1695    return $file_path; // hold on to your butts!
     1696}
     1697
     1698/**
     1699 * a fail safe fall back config if we can't find one
     1700 * @return array
     1701 */
     1702function default_config() : array {
     1703    return [
     1704        "bitfire_enabled" => false,
     1705        "allow_ip_block" => false,
     1706        "security_headers_enabled" => false,
     1707        "log_everything" => false,
     1708        "web_filter_enabled" => false,
     1709        "require_full_browser" => false,
     1710        "whitelist_enable" => false,
     1711        "blacklist_enable" => false,
     1712        "cache_type" => "nop",
     1713        "ip_header" => "remote_addr",
     1714        "default_config" => true,
     1715        "wizard" => true
     1716    ];
    16681717}
    16691718
     
    16761725    //$ini_type = "opcache";
    16771726
    1678     $loader = make_config_loader()->run();
    1679     $config_file = $loader->read_out();
     1727   
     1728
     1729    $config_file = make_config_loader()->run()->read_out();
    16801730    $cache_config_file = $config_file . ".php";
    16811731
    16821732    // return a core config with everything off if the config file is not found...
    16831733    if (!file_exists($config_file)) {
    1684         return [
    1685             "bitfire_enabled" => false,
    1686             "allow_ip_block" => false,
    1687             "security_headers_enabled" => false,
    1688             "log_everything" => false,
    1689             "web_filter_enabled" => false,
    1690             "require_full_browser" => false,
    1691             "whitelist_enable" => false,
    1692             "blacklist_enable" => false,
    1693             "cache_type" => "nop",
    1694             "ip_header" => "remote_addr",
    1695             "wizard" => false
    1696         ];
     1734        return default_config();
    16971735    }
    16981736
     
    17041742    if (!file_exists($cache_config_file) || filemtime($cache_config_file) < $mod_time) {
    17051743       
    1706         $config = parse_ini_file($config_file, false, INI_SCANNER_TYPED);
    1707         //$c = count($config);
    1708         //die("config [$c]\n");
     1744        // support for json configs...
     1745        if (strpos($config_file, ".json") !== false) {
     1746            $config = json_decode(file_get_contents($config_file), true);
     1747        } else {
     1748            $config = parse_ini_file($config_file, false, INI_SCANNER_TYPED);
     1749        }
     1750
     1751        // write the php cached config file
    17091752        if (is_array($config) && count($config) > 20) {
    17101753            // ensure that passwords are always hashed
     
    17201763            $exp = time() + 86400*7;
    17211764            $data = "<?php \$value = $s; \$priority = $priority; \$success = (time() < $exp);";
    1722             file_put_contents($cache_config_file, $data, LOCK_EX) == strlen($data);
    1723         }
     1765            file_put_contents($cache_config_file, $data, LOCK_EX);
     1766        }
     1767        // TODO: we need to add a notification that the config file is invalid
    17241768        else {
    1725             require_once WAF_SRC . "server.php";
    1726             $config = save_config2($config_file);
     1769            return default_config();
    17271770        }
    17281771    }
     
    17301773    if (file_exists($cache_config_file)) {
    17311774        include $cache_config_file;
     1775        // normal case, we have a valid config
    17321776        if (isset($value) && count($value) > 20) {
    17331777            $config = $value;
  • bitfire/trunk/src/webfilter.php

    r3250587 r3334399  
    106106
    107107            // update keys and values
    108             $key_file = \BitFire\WAF_ROOT."data/keys2.raw";
    109             $value_file = \BitFire\WAF_ROOT."data/values2.raw";
    110             $f1 = $key_file;    //get_hidden_file("keys2.txt");
    111             $f2 = $value_file;  //get_hidden_file("values2.txt");
     108            $key_file = get_hidden_file("keys2.raw");
     109            $value_file = get_hidden_file("values2.raw");
    112110            $exp_time = time() - DAY;
    113111            if (!file_exists($key_file) || filemtime($key_file) < $exp_time || !file_exists($value_file) || filemtime($value_file) < $exp_time) {
     
    121119            trace("KEY.{$c1} VAL.{$c2}");
    122120            if ($c1 <= 1 || $c2 <= 1) {
    123                 update_raw($f1, $f2)->run();
     121                update_raw($key_file, $value_file)->run();
    124122            }
    125123            else {
  • bitfire/trunk/startup.php

    r3111384 r3334399  
    8787    }, ARRAY_FILTER_USE_BOTH);
    8888
    89     // user is not logged in, so we will run the firewall code here.
    90     // if they are logged in, the code will run from the wordpress handler so that we
    91     // have access to the user functions
    92     if (empty(CFG::str("cms_root")) || count($auth_cookies) < 2) {
     89    // if no auth cookies are present or not running in WordPress we can run early
     90    // if this code skips - it will be called on plugin init later.
     91    // inspect() prevents double runs
     92    if (!defined('ABSPATH') || count($auth_cookies) < 2) {
    9393        $bitfire = \Bitfire\BitFire::get_instance();
    9494        $bitfire->inspect();
  • bitfire/trunk/uninstall.php

    r3250641 r3334399  
    3939        $file = ini_get("auto_prepend_file");
    4040        if (!empty($file) && contains($file, "bitfire")) {
    41             $seconds = -1;//$exp - time();
     41            $removed = removeBitFireBlock($_SERVER['DOCUMENT_ROOT'] . '/.user.ini');
     42            $seconds = -1; // some servers dont let us read this ini setting, so just display the default
    4243            if ($seconds < 0 || $seconds > 10000) { $seconds = 300; }
    43             die("must wait up to $seconds seconds for user.ini cache to expire, or restart php process.");
     44            die("Auto startup script has been removed but is still loaded in cache. Please wait up to $seconds seconds for .user.ini cache to expire and auto startup to unload. FAILURE TO REMOVE THE STARTUP SCRIPT FROM .user.ini WILL RESULT IN SERVER CRASH. If you think this is in error - manually edit " . $_SERVER['DOCUMENT_ROOT'] . "/.user.ini in your webroot directory and remove the bitfire startup script manually. email [email protected] if you need assistance.");
    4445        }
    4546
     
    5354}
    5455
     56
     57/**
     58 * manually remove the bitfire startup script.
     59 * @param string $filepath
     60 * @return bool
     61 */
     62function removeBitFireBlock(string $filepath): bool {
     63    if (!file_exists($filepath) || !is_readable($filepath) || !is_writable($filepath)) {
     64        return false;
     65    }
     66
     67    $content = file_get_contents($filepath);
     68    if ($content === false) {
     69        return false;
     70    }
     71
     72    // Remove everything between #BEGIN BitFire and #END BitFire (including those lines)
     73    $pattern = '/^#BEGIN BitFire.*?#END BitFire\s*/ms';
     74    $modified = preg_replace($pattern, '', $content);
     75
     76    if ($modified === null) {
     77        return false; // preg_replace failed
     78    }
     79
     80    return file_put_contents($filepath, $modified) !== false;
     81}
  • bitfire/trunk/views/traffic.html

    r3250587 r3334399  
    459459          This free version of BitFire uses OFFLINE data analysis to flag and identify traffic patterns from log data.
    460460          To secure your site and block this traffic in real-time, please upgrade to realtime generative AI blocking.
    461           <a href="https://bitfire.co/purchase" target="_blank" style="text-decoration: underline;">Upgrade to Real-Time blocking</a>
     461          <a href="https://bitfire.co/pricing" target="_blank" style="text-decoration: underline;">Upgrade to Real-Time blocking</a>
    462462        </td>
    463463      </tr>
    464464    </table>
     465  <table style="margin-top:.25rem" class="{{showerror_class}}">
     466      <tr>
     467        <td style="font-size:1rem;border:2px solid #F33;padding:.25rem 1rem;">
     468          <h2 style="margin-bottom:.25rem;float:left;margin-right:2rem;">Note:</h2>
     469          ERROR: configuration data wp-content/uploads/bitfire_RANDOM has been removed! Software is in-operable. Please re-install.
     470        </td>
     471      </tr>
     472    </table>
     473
    465474
    466475  <!-- CARDS -->
  • bitfire/trunk/views/wizard.html

    r3234339 r3334399  
    286286          </div>
    287287          <div class="modal-body">
    288             <p>Auto configuration complete. Click finish to download IP database (1 min).</p>
     288            <p>Auto configuration complete. Click Finish -> Tour Dashboard to download IP database (wait ~1 min for IP database to download).</p>
    289289
    290290            <div id="spin" class="text-success hidden spinner-border left mt-1 mr-2 " style="margin-left: 0" role="status"></div>
Note: See TracChangeset for help on using the changeset viewer.