requirements()) return; Linkbraid_Database::create_tables(); if (get_option('lbraid_db_version') !== LINKBRAID_VERSION) return; set_transient('lbraid_activation_redirect', 1, 60); } public function deactivate() { wp_clear_scheduled_hook('lbraid_job_tick'); } public function init() { if (!$this->requirements()) { add_action('admin_notices', function() { echo '

' . esc_html__('Linkbraid: the PHP mbstring and DOM extensions are required. Enable them in your hosting panel.', 'linkbraid') . '

'; }); return; } if (get_option('lbraid_db_version') !== LINKBRAID_VERSION) { if (is_admin() && current_user_can('manage_options')) { Linkbraid_Database::create_tables(); } if (get_option('lbraid_db_version') !== LINKBRAID_VERSION) { add_action('admin_notices', function() { echo '

' . esc_html__('Linkbraid: the database migration failed. Check CREATE/ALTER permissions and the database log.', 'linkbraid') . '

'; }); return; } } // Initialize components if (class_exists('Linkbraid_Updater')) new Linkbraid_Updater(); new Linkbraid_Admin(); new Linkbraid_Ajax_Handler(); new Linkbraid_Rest_API(); new Linkbraid_Jobs(); add_action('save_post', [$this, 'changed'], 100, 1); add_action('added_post_meta', [$this, 'meta_changed'], 10, 3); add_action('updated_post_meta', [$this, 'meta_changed'], 10, 3); add_action('deleted_post_meta', [$this, 'meta_changed'], 10, 3); // Post-duplication plugins copy every meta row; the guard drops inherited Linkbraid state. add_action('added_post_meta', [$this, 'meta_guard'], 10, 4); add_action('before_delete_post', [$this, 'remove']); // ACF field edits change the scannable text too (post_id may be 'options' — changed() ignores it). add_action('acf/save_post', [$this, 'changed'], 100); // Term archive URLs are cached; refresh the map when taxonomy terms change. foreach (['created_term', 'edited_term', 'delete_term'] as $hook) { add_action($hook, function () { delete_transient(Linkbraid_Terms::URL_CACHE); }); } // External-link attributes: render-time only, the stored content stays clean. if (!is_admin() && Linkbraid_Attributes::enabled()) { add_filter('the_content', ['Linkbraid_Attributes', 'filter_content'], 99); // ACF wysiwyg fields render outside the_content — decorate them the same way. add_filter('acf/format_value/type=wysiwyg', function ($value) { return is_string($value) ? Linkbraid_Attributes::filter_content($value) : $value; }, 99); } // Front-end click counting (opt-in; sendBeacon, no impact on navigation) if (!is_admin() && Linkbraid_Clicks::enabled()) { add_action('wp_enqueue_scripts', function () { wp_enqueue_script('linkbraid-clicks', LINKBRAID_URL . 'public/click.js', [], LINKBRAID_VERSION, true); wp_localize_script('linkbraid-clicks', 'lbraidClicks', [ 'endpoint' => admin_url('admin-ajax.php'), 'post' => (string) get_queried_object_id(), ]); }); } } private function requirements() { return version_compare(PHP_VERSION, '7.4', '>=') && function_exists('mb_strlen') && class_exists('DOMDocument'); } public function meta_changed($meta_id, $post_id, $key) { if (in_array($key, ['_elementor_data', '_elementor_edit_mode', '_fl_builder_data'], true)) $this->changed($post_id); } public function changed($id) { if (wp_is_post_revision($id) || wp_is_post_autosave($id)) return; $post = get_post($id); if (!$post || !in_array($post->post_type, lbraid_post_types(), true)) return; // Duplicators that copy meta with raw SQL bypass meta_guard(); heal on the next save. Linkbraid_Database::repair_post_meta($id); update_post_meta($id, '_lbraid_index_stale', 1); if ($post->post_status === 'publish') Linkbraid_Database::refresh_existing_links($id); else $this->remove($id); } public function meta_guard($meta_id, $post_id, $key, $value) { if (strpos((string) $key, '_lbraid_') !== 0) return; if (in_array($key, Linkbraid_Database::SINGLE_META, true)) { Linkbraid_Database::repair_post_meta($post_id); return; } // A change log copied from another post must not offer an undo of foreign content. if ($key === '_lbraid_link_history' && is_array($value) && !empty($value['suggestion_id'])) { global $wpdb; $source = (int) $wpdb->get_var($wpdb->prepare("SELECT source_post_id FROM {$wpdb->prefix}lbraid_link_suggestions WHERE id = %d", $value['suggestion_id'])); if ($source && $source !== (int) $post_id) delete_metadata_by_mid('post', (int) $meta_id); } } public function remove($id) { global $wpdb; $wpdb->delete($wpdb->prefix . 'lbraid_post_index', ['post_id' => $id]); $wpdb->delete($wpdb->prefix . 'lbraid_external_links', ['source_post_id' => $id]); foreach (['source_post_id', 'target_post_id'] as $column) { $wpdb->delete($wpdb->prefix . 'lbraid_existing_links', [$column => $id]); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $column iterates the fixed pair above; the id is a placeholder. $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}lbraid_link_suggestions WHERE $column = %d AND status = 'pending'", $id)); } } } Linkbraid_Plugin::instance();