<?php
/**
 * Plugin Name: SEO Fury
 * Description: All-in-one SEO plugin: meta titles & descriptions, Open Graph & Twitter Cards, XML & HTML sitemaps, schema markup (JSON-LD), redirects, robots.txt editor, IndexNow, breadcrumbs, internal link analysis, SSL/HTTPS redirect, and WooCommerce SEO. Easy migration from Yoast, Rank Math, AIOSEO, and SEOPress.
 * Version: 1.1.2
 * Author: SEO Fury Team
 * Text Domain: seo-fury
 * Domain Path: /languages
 * License: GPL v2 or later
 */

if (!defined('ABSPATH')) {
    exit;
}

// Prevent fatal error if another copy of SEO Fury is already active
if (defined('SEO_FURY_VERSION')) {
    add_action('admin_notices', function() {
        $plugin_basename = plugin_basename(__FILE__);
        echo '<div class="notice notice-error"><p>';
        printf(
            /* translators: %s: plugin file path */
            esc_html__('SEO Fury: Another copy of the plugin is already active. Please deactivate the old version first. This copy (%s) has been automatically deactivated.', 'seo-fury'),
            '<code>' . esc_html($plugin_basename) . '</code>'
        );
        echo '</p></div>';
    });
    // Silently deactivate this duplicate copy
    add_action('admin_init', function() {
        deactivate_plugins(plugin_basename(__FILE__));
        // Remove "Plugin activated" notice to avoid confusion
        if (isset($_GET['activate'])) {
            unset($_GET['activate']);
        }
    });
    return;
}

define('SEO_FURY_VERSION', '1.1.2');
define('SEO_FURY_PATH', plugin_dir_path(__FILE__));
define('SEO_FURY_URL', plugin_dir_url(__FILE__));
define('SEO_FURY_FREE', true);

/**
 * Return the required plugin include files that are missing from this install.
 *
 * A partial or corrupted upload (interrupted FTP transfer, an incomplete
 * package, or files not committed to a release) can leave class-core.php
 * referencing files that were never copied — which triggers an uncaught fatal
 * error such as "Failed opening required ...class-xxx.php". We parse the
 * require_once statements out of class-core.php so this integrity check always
 * stays in sync with the real dependency list, without maintaining a duplicate
 * array here.
 *
 * @return string[] Relative paths (from the plugin root) of the missing files.
 */
function seo_fury_get_missing_files() {
    static $cache = null;
    if ( $cache !== null ) {
        return $cache;
    }

    $core_rel = 'includes/class-core.php';
    $core     = SEO_FURY_PATH . $core_rel;

    if ( ! file_exists( $core ) ) {
        $cache = array( $core_rel );
        return $cache;
    }

    $source  = file_get_contents( $core );
    $missing = array();

    if ( $source && preg_match_all( '#includes/(class-[a-z0-9\-]+\.php)#i', $source, $matches ) ) {
        foreach ( array_unique( $matches[1] ) as $file ) {
            if ( ! file_exists( SEO_FURY_PATH . 'includes/' . $file ) ) {
                $missing[] = 'includes/' . $file;
            }
        }
    }

    $cache = $missing;
    return $cache;
}

/**
 * Admin notice shown when the install is incomplete (see seo_fury_get_missing_files).
 */
function seo_fury_missing_files_notice() {
    $missing = seo_fury_get_missing_files();
    if ( empty( $missing ) ) {
        return;
    }
    echo '<div class="notice notice-error"><p>';
    echo wp_kses_post(
        sprintf(
            /* translators: %s: comma-separated list of missing file paths */
            __( '<strong>SEO Fury</strong> is not running because some of its files are missing (incomplete upload). Please delete the plugin folder entirely and reinstall the full package. Missing files: %s', 'seo-fury' ),
            '<code>' . implode( '</code>, <code>', array_map( 'esc_html', $missing ) ) . '</code>'
        )
    );
    echo '</p></div>';
}

/**
 * Suppress WP 6.7+ _load_textdomain_just_in_time notices for third-party
 * plugins (e.g. WooCommerce) that load their text domain before the `init`
 * action. This is a known compatibility issue with WP 6.7+ and cannot be
 * fixed from our side — the notice would confuse users into thinking SEO
 * Fury is at fault.
 */
add_filter( 'doing_it_wrong_trigger_error', function ( $trigger, $function_name ) {
    if ( '_load_textdomain_just_in_time' === $function_name ) {
        return false;
    }
    return $trigger;
}, 10, 2 );

/**
 * Start output buffering on all admin requests.
 * Prevents third-party plugin notices from breaking redirects with
 * "headers already sent" errors on options.php and other pages.
 */
if ( is_admin() || ( defined( 'WP_ADMIN' ) && WP_ADMIN ) ) {
    ob_start();
}

/**
 * Clean stray output before our AJAX handlers run.
 * Third-party plugins may output notices during loading that corrupt JSON.
 */
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
    add_action( 'admin_init', function () {
        $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
        if ( strpos( $action, 'seo_fury_' ) === 0 ) {
            if ( ob_get_level() ) {
                ob_clean();
            }
        }
    }, 0 );
}


register_activation_hook(__FILE__, 'seo_fury_activate');

function seo_fury_activate() {
    // Check if another copy of the plugin is active
    $this_plugin = plugin_basename(__FILE__);
    $active_plugins = get_option('active_plugins', array());
    foreach ($active_plugins as $active_plugin) {
        // Another plugin with the same main class file but different directory
        if ($active_plugin !== $this_plugin && basename($active_plugin) === 'seo-fury.php') {
            wp_die(
                esc_html__('SEO Fury is already installed and active in another folder. Please deactivate or delete the old version before activating this one.', 'seo-fury'),
                esc_html__('Plugin Activation Error', 'seo-fury'),
                array('back_link' => true)
            );
        }
    }

    // Abort activation cleanly if the package is incomplete, instead of dying
    // with a cryptic uncatchable "Failed opening required ..." fatal error.
    $missing = seo_fury_get_missing_files();
    if ( ! empty( $missing ) ) {
        wp_die(
            wp_kses_post(
                sprintf(
                    /* translators: %s: list of missing file paths */
                    __( 'SEO Fury cannot be activated because some of its files are missing. This usually means the plugin was uploaded incompletely. Please delete the plugin folder entirely and reinstall the full package.<br><br>Missing files:<br><code>%s</code>', 'seo-fury' ),
                    implode( '<br>', array_map( 'esc_html', $missing ) )
                )
            ),
            esc_html__( 'Plugin Activation Error', 'seo-fury' ),
            array( 'back_link' => true )
        );
    }

    require_once SEO_FURY_PATH . 'includes/class-core.php';

    SEO_Fury_Core::activate();
}

register_deactivation_hook(__FILE__, 'seo_fury_deactivate');

function seo_fury_deactivate() {
    flush_rewrite_rules();
}

require_once SEO_FURY_PATH . 'includes/class-core.php';

/**
 * Finds the best available translation file for a given locale.
 * Priority: community translation (wp-content/languages/plugins/) → bundled machine translation.
 * Supports .l10n.php format (WP 6.5+) and classic .mo.
 *
 * @param  string      $locale  e.g. ru_RU, de_DE
 * @return string|null          Full path to the file, or null if nothing found
 */
function seo_fury_resolve_translation_file( $locale ) {
    $community_base = WP_LANG_DIR . '/plugins/seo-fury-' . $locale;

    if ( file_exists( $community_base . '.l10n.php' ) ) {
        return $community_base . '.l10n.php';
    }

    if ( file_exists( $community_base . '.mo' ) ) {
        return $community_base . '.mo';
    }

    $plugin_mo = SEO_FURY_PATH . 'languages/seo-fury-' . $locale . '.mo';
    if ( file_exists( $plugin_mo ) ) {
        return $plugin_mo;
    }

    return null;
}

/**
 * Load plugin textdomain early to avoid _load_textdomain_just_in_time notices
 * in WP 6.7+ which can cause memory exhaustion via recursive Notice translation.
 * Must run BEFORE module constructors that use __().
 */
function seo_fury_load_textdomain() {
    $plugin_lang = get_option( 'seo_fury_plugin_language', '' );

    if ( $plugin_lang === 'en_US' ) {
        return;
    }

    $locale = ! empty( $plugin_lang ) ? $plugin_lang : determine_locale();
    $file   = seo_fury_resolve_translation_file( $locale );

    if ( $file ) {
        load_textdomain( 'seo-fury', $file );
    }
}

function seo_fury_init() {
    // Bail gracefully if the install is incomplete (missing files) so a broken
    // upload shows an admin notice instead of white-screening the whole site.
    if ( ! empty( seo_fury_get_missing_files() ) ) {
        if ( is_admin() ) {
            add_action( 'admin_notices', 'seo_fury_missing_files_notice' );
        }
        return;
    }

    // Load translations first to prevent WP 6.7+ _load_textdomain_just_in_time warnings
    seo_fury_load_textdomain();

    global $wpdb;

    $redirects_table = $wpdb->prefix . 'seo_redirects';
    $indexnow_table = $wpdb->prefix . 'seo_indexnow_log';

    SEO_Fury_Core::get_instance($redirects_table, $indexnow_table);
}
add_action('plugins_loaded', 'seo_fury_init');

// Re-load textdomain on init for language override support
add_action( 'init', function () {
    $plugin_lang = get_option( 'seo_fury_plugin_language', '' );

    if ( ! empty( $plugin_lang ) && $plugin_lang !== 'en_US' ) {
        unload_textdomain( 'seo-fury' );
        $file = seo_fury_resolve_translation_file( $plugin_lang );
        if ( $file ) {
            load_textdomain( 'seo-fury', $file );
        }
    }
}, 1 );

function seo_fury_is_white_label_enabled() {
    if ( ! apply_filters( 'seo_fury_is_pro', false ) ) {
        return false;
    }
    return (bool) get_option( 'seo_fury_white_label_enabled', 0 );
}

function seo_fury_get_brand_name() {
    $default = 'SEO Fury';
    if (!seo_fury_is_white_label_enabled()) {
        return $default;
    }
    $name = get_option('seo_fury_white_label_name', $default);
    $name = is_string($name) ? trim($name) : $default;
    return $name !== '' ? $name : $default;
}

function seo_fury_hide_branding() {
    return seo_fury_is_white_label_enabled() && get_option('seo_fury_white_label_hide_links', 0);
}

/**
 * Unicode-aware word count.
 *
 * PHP's str_word_count() only recognises ASCII letters, so Cyrillic,
 * Chinese, Arabic, etc. are silently ignored. This helper uses a
 * Unicode regex that matches sequences of letters and digits in any script.
 *
 * @param  string $text Plain text (HTML should be stripped beforehand).
 * @return int          Number of words.
 */
function seo_fury_word_count( $text ) {
    if ( empty( $text ) || ! is_string( $text ) ) {
        return 0;
    }
    return preg_match_all( '/[\p{L}\p{N}\p{M}]+/u', $text );
}

/**
 * Neutralise CSV formula injection before writing a cell.
 *
 * Spreadsheet apps (Excel, Google Sheets, LibreOffice) evaluate any cell that
 * begins with =, +, -, @, or a tab/carriage-return as a formula. Since exported
 * values (redirect URLs, 404 paths) can be attacker-influenced, we prefix such
 * cells with a single quote so they are rendered as literal text.
 *
 * @param  mixed $value Cell value.
 * @return mixed        Safe value (strings starting with a risky char get a leading apostrophe).
 */
function seo_fury_csv_safe( $value ) {
    if ( ! is_string( $value ) || $value === '' ) {
        return $value;
    }
    if ( preg_match( '/^[=+\-@\t\r]/', $value ) ) {
        return "'" . $value;
    }
    return $value;
}
