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();