prefix . 'il_pilares') whose names and column lists // are constants under plugin control. Generic WordPress checks // for direct DB queries, caching and prepared-SQL interpolation // don't apply meaningfully here. See docs/LICENSING.md for the // full architecture rationale. define( 'FLINK_VERSION', '4.6.60' ); define( 'FLINK_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'FLINK_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'FLINK_PLUGIN_FILE', __FILE__ ); // ── Edition flag ──────────────────────────────────────────────────── // Identifies which variant of the plugin is running. The build script // flips this to 'lite' when generating the WordPress.org-bound ZIP; // the production codebase always ships as 'pro'. Code that needs to // behave differently between editions reads this constant directly // rather than guessing by feature detection — cheaper and explicit. // // Permitted values: 'pro' (full plugin) | 'lite' (WordPress.org). // // The Lite build also DELETES the entire premium subsystem (modules, // licensing client, automation library) so the constant alone is not // what gates premium features at runtime — the files are simply not // there. The constant exists for the cases where shared code needs // to enforce per-edition caps (e.g. 5 rules max in Lite). define( 'FLINK_EDITION', 'pro' ); // ── PRO upsell URL ────────────────────────────────────────────────── // Base URL the Lite edition's upsell cards point to. Each card appends // its own utm_campaign so we can track which feature drives the most // upgrade clicks. When the landing page goes live at a more specific // URL (e.g. https://4linking.com/pro), change this single constant // and every CTA in the Lite plugin updates with it. // // In PRO this constant exists but is never used (premium features are // available, no upsell needed). Defined here rather than inside the // build script so the source remains identical between editions. define( 'FLINK_PRO_URL', 'https://4linking.com/' ); // ── Internationalization ──────────────────────────────────────────── // Translation files live in /languages/ and follow the pattern // {text-domain}-{locale}.mo. WordPress picks the right .mo automatically // based on the site's locale setting. // require_once FLINK_PLUGIN_DIR . 'includes/class-database.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-prefix-migration.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-page-header.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-pilares.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-reglas.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-linker.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-stats.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-exclusiones.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-admin.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-ajax.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-export.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-link-stats.php'; require_once FLINK_PLUGIN_DIR . 'includes/class-slug-helper.php'; register_activation_hook( __FILE__, function() { // ── Prefix migration ───────────────────────────────────────── // Versions ≤ 4.5.1 used the prefix il_/IL_ throughout. From v4.6.15 // we use flink_/FLINK_ (longer, more distinctive, WordPress.org // guideline-compliant). Rename existing options/transients/ // post-meta/cron from il_* to flink_* in a single one-shot pass. // This is idempotent and safe to call multiple times. FLINK_Prefix_Migration::maybe_run(); FLINK_Database::install(); FLINK_Link_Stats::maybe_install(); // ── First-time activation onboarding ───────────────────────── // We use a sentinel option (il_first_activation_done) so this // block runs only ONCE per install lifetime, even if the user // deactivates and reactivates the plugin. Tweaking module // defaults on every reactivation would be confusing. if ( get_option( 'flink_first_activation_done', '0' ) !== '1' ) { // Schedule onboarding redirect on the next admin page load set_transient( 'flink_onboarding_redirect', 1, 60 ); update_option( 'flink_first_activation_done', '1' ); } } ); register_deactivation_hook( __FILE__, array( 'FLINK_Database', 'deactivate' ) ); // ── Defensive prefix migration on every load ──────────────────── // In addition to the activation hook (which catches the normal // install/update flow), we run the migration check on plugins_loaded // as a safety net for cases where the activation hook didn't fire — // e.g., manual file replacement via FTP, WP-CLI bulk operations, // or staging→production sync. The check is O(1) when already done // (single get_option call). Priority 1 so it runs before any other // flink_* hook tries to read state. add_action( 'plugins_loaded', array( 'FLINK_Prefix_Migration', 'maybe_run' ), 1 ); add_action( 'plugins_loaded', 'flink_init' ); // Handle export download (must run before headers are sent). // Both Free and Paid offer JSON backup and CSV export of rules, so // the handler lives outside any Paid-only block. add_action( 'admin_init', function() { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( isset( $_GET['flink_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. if ( $_GET['flink_action'] === 'export' ) { FLINK_Export::download(); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Nonce verified upstream in handler/page bootstrap. Value is sanitized at use site. } elseif ( $_GET['flink_action'] === 'export_rules_csv' ) { FLINK_Export::download_rules_csv(); } } } ); // Mark needs-processing when a post is published or updated add_action( 'publish_post', function() { update_option( 'flink_needs_processing', 1, false ); } ); add_action( 'publish_page', function() { update_option( 'flink_needs_processing', 1, false ); } ); function flink_init() { // Upgrade DB schema for existing installs FLINK_Database::maybe_upgrade(); // ── One-shot onboarding migration ───────────────────────────── // Reset the "hidden" flag for users upgrading from an earlier // version so they get to see the new welcome screen with demo // videos. Tracked by a sentinel option so the migration runs // exactly once even if the user later hides the screen. if ( get_option( 'flink_onboarding_v2_migrated', '0' ) !== '1' ) { delete_option( 'flink_onboarding_hidden' ); update_option( 'flink_onboarding_v2_migrated', '1' ); } // ── Drop legacy frontend-mode option ────────────────────────── // The "modo frontend" feature has been removed. We delete the // orphan option from any install that still has it. Sentinel // tracked separately so this can be replayed independently. if ( get_option( 'flink_modo_frontend_removed', '0' ) !== '1' ) { delete_option( 'flink_modo_frontend' ); update_option( 'flink_modo_frontend_removed', '1' ); } // ── Drop legacy activity-log table and option ───────────────── // The "Actividad" feature has been removed entirely. The il_log // table was a partial log that only tracked rule-based linking // and never recorded the rest of the plugin's actions. Removing // it eliminates dead code and recovers DB space. The "Mapa de // enlaces" page (link_stats) still shows the current state of // all internal links, so no usable information is lost. if ( get_option( 'flink_log_removed', '0' ) !== '1' ) { global $wpdb; $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}il_log" ); delete_option( 'flink_max_log_entries' ); update_option( 'flink_log_removed', '1' ); } // ── Origen/Destino split migration (v4.3.34) ───────────────── // Before 4.3.34 there was a single 'flink_origen_tipo' that controlled // BOTH where links could be inserted and where they could point to. // We split this into 'flink_origen_tipo' (sources) and 'flink_destino_tipo' // (targets) so users can have, for example, blog posts linking TO // pillar pages without those pillar pages being modified themselves. // // Migration policy: if il_destino_tipo doesn't exist yet, copy it // from il_origen_tipo. This preserves the previous behaviour // exactly — every install keeps working identical to before until // the user opts into the split via the new UI. if ( get_option( 'flink_destino_tipo_migrated', '0' ) !== '1' ) { if ( get_option( 'flink_destino_tipo', null ) === null ) { $existing_origen = get_option( 'flink_origen_tipo', 'post' ); update_option( 'flink_destino_tipo', $existing_origen ); } update_option( 'flink_destino_tipo_migrated', '1' ); } $admin = new FLINK_Admin(); $admin->init(); $ajax = new FLINK_Ajax(); $ajax->init(); $linker = new FLINK_Linker(); $linker->init(); // Global comments disabler (gated by il_disable_comments option) // Auto-upgrade DB schema on version bump. install() uses dbDelta which // is idempotent — safe to call on every version change. It will create // any missing table (like il_image_templates added in 3.9.2) without // affecting existing ones. $db_version = get_option( 'flink_db_version', '' ); if ( $db_version !== FLINK_VERSION ) { FLINK_Database::install(); } // Link-map cache table. Used by both editions (the "Mapa de enlaces" // page is in the core, not the paid pipeline) so it must live outside // any Paid-only block. FLINK_Link_Stats::maybe_install(); }