plugin_name = $plugin_name;
$this->version = $version;
$this->core = $core;
if ( ! class_exists('AILM_Pool_Generator') ) {
require_once dirname( __DIR__ ) . '/includes/class-ailm-pool-generator.php';
}
$this->pool_generator = new AILM_Pool_Generator();
add_action( 'admin_notices', array( $this, 'display_promo_banner' ) );
}
/**
* Displays a promotional banner on the plugin's settings page.
* The banner is dismissible on a per-session basis using sessionStorage.
*/
public function display_promo_banner() {
// Only show on our plugin's pages
$screen = get_current_screen();
if ( ! $screen || strpos( $screen->id, 'page_ailinkmaster-internal-auto-manager' ) === false ) {
return;
}
?>
plugin_name, array( $this, 'display_plugin_setup_page' ) );
}
public function handle_admin_actions() {
// This handles the dismiss notice link
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$ailm_action = isset($_GET['ailm_action']) ? sanitize_text_field(wp_unslash($_GET['ailm_action'])) : '';
if ( $ailm_action === 'dismiss_simulation' ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$sim_id = isset( $_GET['simulation_id'] ) ? sanitize_key( $_GET['simulation_id'] ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_key( $_GET['_wpnonce'] ) : '';
if ( $sim_id && $nonce && wp_verify_nonce( $nonce, 'ailm_dismiss_' . $sim_id ) ) {
delete_transient( $sim_id );
wp_safe_redirect( remove_query_arg( array('ailm_action', 'simulation_id', '_wpnonce') ) );
exit;
}
}
if ( empty($_POST['submit_action']) ) return;
$action = sanitize_key($_POST['submit_action']);
$base_redirect_url = admin_url('options-general.php?page=' . $this->plugin_name);
check_admin_referer('ailm_form_nonce');
if ( strpos( $action, 'ailm_delete_rule_' ) === 0 ) {
$rule_id_to_delete = substr( $action, strlen( 'ailm_delete_rule_' ) );
if ( ! empty( $rule_id_to_delete ) ) {
$all_rules = get_option( 'ailm_rules', [] );
$new_rules = [];
foreach ( $all_rules as $rule ) {
if ( isset( $rule['id'] ) && $rule['id'] === $rule_id_to_delete ) {
continue;
}
$new_rules[] = $rule;
}
update_option( 'ailm_rules', $new_rules );
}
wp_safe_redirect( add_query_arg( 'ailm_notice', 'rule_deleted', $base_redirect_url ) );
exit;
}
if ( $action === 'ailm_purge_cache' ) {
$this->purge_cache();
wp_safe_redirect( add_query_arg( 'ailm_notice', 'cache_purged', $base_redirect_url ) );
exit;
}
if ( $action === 'ailm_reset_inbound' ) {
$this->reset_inbound_counters();
wp_safe_redirect( add_query_arg( 'ailm_notice', 'counters_reset', $base_redirect_url ) );
exit;
}
if ( $action === 'ailm_unfreeze_all' ) {
$this->unfreeze_all_links();
wp_safe_redirect( add_query_arg( 'ailm_notice', 'all_unfrozen', $base_redirect_url ) );
exit;
}
if ( $action === 'ailm_save_settings' || $action === 'ailm_run_shuffled_simulation' || $action === 'ailm_add_new_rule' ) {
if ($action === 'ailm_save_settings' || $action === 'ailm_add_new_rule') {
$this->save_settings( $action === 'ailm_add_new_rule' );
}
$is_shuffled = ($action === 'ailm_run_shuffled_simulation');
$simulation_data = $this->run_simulation($is_shuffled);
$transient_id = 'ailm_sim_' . wp_generate_password(12, false);
set_transient($transient_id, $simulation_data, HOUR_IN_SECONDS);
$redirect_url = add_query_arg( 'simulation_id', $transient_id, $base_redirect_url );
if ($action === 'ailm_save_settings') {
$redirect_url = add_query_arg( 'ailm_notice', 'settings_saved', $redirect_url );
}
wp_safe_redirect( $redirect_url );
exit;
}
if ( $action === 'ailm_start_freeze_job' ) {
$this->core->start_freeze_job();
$redirect_url = add_query_arg( 'ailm_notice', 'freeze_started', $base_redirect_url );
wp_safe_redirect( $redirect_url );
exit;
}
}
public function display_plugin_setup_page() {
if ( ! current_user_can( 'manage_options' ) ) return;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only
if ( isset($_GET['ailm_notice']) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only
$this->display_admin_notices(sanitize_key($_GET['ailm_notice']));
}
$this->display_progress_notice();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only
if ( isset($_GET['simulation_id']) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only
$sim_id = sanitize_key($_GET['simulation_id']);
$sim_data = get_transient($sim_id);
if ($sim_data) {
$this->display_simulation_notice($sim_data, $sim_id);
}
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab navigation
$active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'settings';
?>
AI Internal Links (vversion ); ?>)
render_settings_tab();
break;
case 'stats':
$this->render_stats_tab();
break;
case 'scout':
if ( ! class_exists( 'AILM_Scout_Admin' ) ) {
require_once dirname( __DIR__ ) . '/modules/404-scout/class-ailm-scout-admin.php';
}
$scout_admin = new AILM_Scout_Admin();
$scout_admin->render_tab();
break;
}
?>
0 ? round(($processed / $total) * 100) : 0;
?>
Freeze Job in Progress...
The plugin is processing articles in the background. This notice will refresh automatically.
/ posts processed.
Freeze Job Complete!
All links have been processed and frozen. You can now see the results in the "Frozen Links Report" tab.
'dismiss_simulation',
'simulation_id' => $sim_id
]),
'ailm_dismiss_' . $sim_id
);
?>
Simulation Results
Based on your rules, here is a proposal for link distribution. This is a simulation, no links have been created yet.
- Total Links to Create:
- Articles Receiving Links:
- Orphan Articles Remaining:
Top 5 Categories Targeted:
None';
} else {
$cat_strings = [];
foreach ($sim_data['top_cats'] as $cat => $count) {
$cat_strings[] = esc_html($cat) . ' (' . esc_html( number_format_i18n( $count ) ) . ')';
}
echo wp_kses_post( implode(', ', $cat_strings) );
}
?>
['class' => 'notice-success', 'message' => 'Settings saved! A new simulation has been generated below.'],
'cache_purged' => ['class' => 'notice-success', 'message' => 'Pool cache cleared!'],
'counters_reset' => ['class' => 'notice-warning', 'message' => 'Inbound link counters reset to 0!'],
'all_unfrozen' => ['class' => 'notice-warning', 'message' => 'ALL LINKS UNFROZEN! You can now apply a new plan.'],
'freeze_started' => ['class' => 'notice-info', 'message' => 'Freeze job started! This will run in the background. You can check the Frozen Links Report in a few minutes to see progress.'],
'rule_deleted' => ['class' => 'notice-success', 'message' => 'Rule deleted successfully.'],
];
if (isset($notices[$notice_key])) {
$notice = $notices[$notice_key];
echo '' . esc_html($notice['message']) . '
';
}
}
private function purge_cache() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_ailm_pool_%' ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_timeout_ailm_pool_%' ) );
}
private function reset_inbound_counters() {
delete_post_meta_by_key( '_ailm_inbound_count' );
}
private function unfreeze_all_links() {
delete_post_meta_by_key( '_ailm_frozen_map' );
delete_post_meta_by_key( '_ailm_inbound_count' );
}
private function save_settings( $is_new_rule = false ) {
// Explicitly sanitize and save options to satisfy static analysis and security checks.
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( isset( $_POST['ailm_mode'] ) ) update_option( 'ailm_mode', sanitize_text_field( wp_unslash( $_POST['ailm_mode'] ) ) );
if ( isset( $_POST['ailm_utm_source'] ) ) update_option( 'ailm_utm_source', sanitize_text_field( wp_unslash( $_POST['ailm_utm_source'] ) ) );
if ( isset( $_POST['ailm_utm_medium'] ) ) update_option( 'ailm_utm_medium', sanitize_text_field( wp_unslash( $_POST['ailm_utm_medium'] ) ) );
if ( isset( $_POST['ailm_utm_campaign'] ) ) update_option( 'ailm_utm_campaign', sanitize_text_field( wp_unslash( $_POST['ailm_utm_campaign'] ) ) );
if ( isset( $_POST['ailm_exclude_urls'] ) ) update_option( 'ailm_exclude_urls', sanitize_text_field( wp_unslash( $_POST['ailm_exclude_urls'] ) ) );
if ( isset( $_POST['ailm_hybrid_days'] ) ) update_option( 'ailm_hybrid_days', intval( wp_unslash( $_POST['ailm_hybrid_days'] ) ) );
if ( isset( $_POST['ailm_global_limit'] ) ) update_option( 'ailm_global_limit', intval( wp_unslash( $_POST['ailm_global_limit'] ) ) );
if ( isset( $_POST['ailm_min_chars'] ) ) update_option( 'ailm_min_chars', intval( wp_unslash( $_POST['ailm_min_chars'] ) ) );
if ( isset( $_POST['ailm_skip_chars'] ) ) update_option( 'ailm_skip_chars', intval( wp_unslash( $_POST['ailm_skip_chars'] ) ) );
if ( isset( $_POST['ailm_cache_hours'] ) ) update_option( 'ailm_cache_hours', intval( wp_unslash( $_POST['ailm_cache_hours'] ) ) );
if ( isset( $_POST['ailm_target_limit'] ) ) update_option( 'ailm_target_limit', intval( wp_unslash( $_POST['ailm_target_limit'] ) ) );
if ( isset( $_POST['ailm_pool_size'] ) ) update_option( 'ailm_pool_size', intval( wp_unslash( $_POST['ailm_pool_size'] ) ) );
update_option( 'ailm_debug_mode', isset( $_POST['ailm_debug_mode'] ) ? 1 : 0 );
update_option( 'ailm_enable_utm', isset( $_POST['ailm_enable_utm'] ) ? 1 : 0 );
update_option( 'ailm_count_manual_links', isset( $_POST['ailm_count_manual_links'] ) ? 1 : 0 );
// phpcs:enable WordPress.Security.NonceVerification.Missing
$clean_rules = [];
if ( isset( $_POST['rules'] ) && is_array( $_POST['rules'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
$rule_count = 0;
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
foreach ( wp_unslash($_POST['rules']) as $r ) {
if ($rule_count >= self::MAX_RULES) break;
if (isset($r['delete'])) continue;
if (empty($r['id']) && empty($r['keywords'])) continue;
$raw_keywords = $r['keywords'] ?? '';
$keyword_lines = preg_split( '/\r\n|\r|\n/', $raw_keywords );
$limited_keywords = array_slice($keyword_lines, 0, self::MAX_KEYWORDS_PER_RULE);
$sanitized_keywords = sanitize_textarea_field(implode("\n", $limited_keywords));
$mixes = [];
if ( !empty( $r['mix'] ) && is_array( $r['mix'] ) ) {
foreach ( $r['mix'] as $m ) {
if ( !empty($m['cat']) && !empty($m['pct']) ) {
$mixes[] = [ 'cat' => intval( $m['cat'] ), 'pct' => intval( $m['pct'] ) ];
}
}
}
$clean_rules[] = [
'id' => sanitize_text_field($r['id'] ?? uniqid()),
'cat_id' => intval($r['cat_id'] ?? 0),
'keywords' => $sanitized_keywords,
'donor_age' => intval($r['donor_age'] ?? 30),
'target_age' => intval($r['target_age'] ?? 7),
'limit_total' => intval($r['limit_total'] ?? 5),
'limit_per_word_default' => intval($r['limit_per_word_default'] ?? 1),
'mix_rules' => $mixes,
];
$rule_count++;
}
}
if ( $is_new_rule && count($clean_rules) < self::MAX_RULES ) {
$clean_rules[] = [
'id' => uniqid(), 'cat_id' => 0, 'keywords' => '', 'donor_age' => 30, 'target_age' => 7,
'limit_total' => 5, 'limit_per_word_default' => 1, 'mix_rules' => [],
];
}
update_option( 'ailm_rules', $clean_rules );
}
private function render_settings_tab() {
$options = [
'ailm_mode' => get_option('ailm_mode', 'static'), 'ailm_hybrid_days' => get_option('ailm_hybrid_days', 30),
'ailm_debug_mode' => get_option('ailm_debug_mode', 0), 'ailm_exclude_urls' => get_option('ailm_exclude_urls', ''),
'ailm_global_limit' => get_option('ailm_global_limit', 10), 'ailm_target_limit' => get_option('ailm_target_limit', 10),
'ailm_min_chars' => get_option('ailm_min_chars', 2000), 'ailm_skip_chars' => get_option('ailm_skip_chars', 700),
'ailm_cache_hours' => get_option('ailm_cache_hours', 24), 'ailm_pool_size' => get_option('ailm_pool_size', 50),
'ailm_enable_utm' => get_option('ailm_enable_utm', 0), 'ailm_utm_source' => get_option('ailm_utm_source', 'auto_link'),
'ailm_utm_medium' => get_option('ailm_utm_medium', 'internal'), 'ailm_utm_campaign' => get_option('ailm_utm_campaign', 'seo_auto'),
'ailm_count_manual_links' => get_option('ailm_count_manual_links', 0),
];
$rules = get_option( 'ailm_rules', [] );
$categories = get_categories( [ 'hide_empty' => 0 ] );
?>
render_frozen_links_report(); ?>
Frozen Links Report
';
$frozen_posts = get_posts([
'post_type' => 'any',
'posts_per_page' => -1,
'meta_key' => '_ailm_frozen_map',
'fields' => 'ids',
'no_found_rows' => true,
]);
if ( empty( $frozen_posts ) ) {
echo 'No frozen links found in the database. Links are frozen when a user or bot visits a page for the first time after rules are applied.
';
return;
}
$stats = ['urls' => [], 'cats' => [], 'tags' => [], 'total_links' => 0];
foreach ( $frozen_posts as $pid ) {
$map = get_post_meta( $pid, '_ailm_frozen_map', true );
if ( !is_array($map) || empty($map) ) continue;
foreach($map as $link_item) {
$url = strtok($link_item['url'], '?');
if (empty($url)) continue;
if ( !isset($stats['urls'][$url]) ) {
$stats['urls'][$url] = 0;
}
$stats['urls'][$url]++;
$stats['total_links']++;
}
}
$final_stats = [];
foreach ($stats['urls'] as $url => $count) {
$id = url_to_postid($url);
$final_stats[] = ['url' => $url, 'title' => $id ? get_the_title($id) : $url, 'count' => $count];
if (!$id) continue;
$terms = get_the_terms($id, 'category');
if ($terms && !is_wp_error($terms)) foreach ($terms as $t) $stats['cats'][$t->name] = ($stats['cats'][$t->name] ?? 0) + $count;
$terms = get_the_terms($id, 'post_tag');
if ($terms && !is_wp_error($terms)) foreach ($terms as $t) $stats['tags'][$t->name] = ($stats['tags'][$t->name] ?? 0) + $count;
}
usort( $final_stats, function ( $a, $b ) { return $b['count'] - $a['count']; } );
arsort( $stats['cats'] );
arsort( $stats['tags'] );
echo '';
echo '
';
printf(
'
Top Recipients (from Frozen Links)
Total frozen links: %s | Total unique articles linked to: %s
',
esc_html( number_format_i18n( $stats['total_links'] ) ),
esc_html( count( $stats['urls'] ) )
);
echo '
| Article | Links |
';
foreach (array_slice($final_stats, 0, 100) as $row) {
printf(
'%s %s | %s |
',
esc_url($row['url']),
esc_html($row['title']),
esc_html($row['url']),
esc_html(number_format_i18n($row['count']))
);
}
echo '
';
echo '
By Category
| Category | Links |
';
foreach(array_slice($stats['cats'], 0, 20, true) as $name => $cnt) echo '| '.esc_html($name).' | '.esc_html(number_format_i18n($cnt)).' |
';
echo '
';
echo '
By Tag
| Tag | Links |
';
foreach(array_slice($stats['tags'], 0, 20, true) as $name => $cnt) echo '| '.esc_html($name).' | '.esc_html(number_format_i18n($cnt)).' |
';
echo '
';
echo '
';
$all_post_ids = get_posts(['numberposts' => -1, 'post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids']);
$total_posts = count($all_post_ids);
$orphan_count = 0;
$orphan_list_html = '';
$recipient_urls = array_keys($stats['urls']);
foreach ($all_post_ids as $pid) {
$post_url = get_permalink($pid);
if (!in_array($post_url, $recipient_urls)) {
$orphan_count++;
if ($orphan_count <= 100) {
$orphan_list_html .= sprintf(
'%s',
esc_url($post_url),
esc_html(get_the_title($pid))
);
}
}
}
echo '';
echo '
Orphans (No Inbound Links)
';
printf(
'
Total Articles: %s | Orphan Articles: %s
',
esc_html( number_format_i18n( $total_posts ) ),
esc_html( number_format_i18n( $orphan_count ) )
);
if ($orphan_count > 0) {
// The content of $orphan_list_html is escaped during creation, wp_kses_post provides an extra layer of security.
echo '
' . wp_kses_post( $orphan_list_html ) . '
';
} else {
echo '
No orphans found based on existing frozen links.
';
}
echo '
';
echo '
Back';
}
private function run_simulation($is_shuffled = false) {
$rules = get_option( 'ailm_rules', [] );
$simulation_stats = ['urls' => [], 'cats' => [], 'total_links' => 0, 'simulated_inbound' => []];
if ( empty( $rules ) ) {
$all_post_ids = get_posts(['post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1]);
return [
'total_links' => 0,
'total_recipients' => 0,
'orphan_count' => count($all_post_ids),
'top_cats' => []
];
}
$global_limit = get_option( 'ailm_global_limit', 10 );
$target_limit = get_option( 'ailm_target_limit', 10 );
$min_chars = get_option( 'ailm_min_chars', 2000 );
$skip_chars = get_option( 'ailm_skip_chars', 700 );
$pool_size = get_option( 'ailm_pool_size', 50 );
$prepared_rules = [];
foreach ( $rules as $rule ) {
if ( empty( trim( $rule['keywords'] ) ) ) continue;
$target_urls = $this->pool_generator->generate_pool( $rule, $pool_size, true, $is_shuffled );
if ( empty( $target_urls ) ) continue;
$rule['target_pool'] = $target_urls;
$rule['pool_size'] = count($target_urls);
$raw_lines = preg_split( '/\r\n|\r|\n/', $rule['keywords'] );
$word_limit_map = [];
$clean_kw_regex = [];
$default_limit = $rule['limit_per_word_default'] ?? 1;
foreach ( $raw_lines as $line ) {
$parts = explode( '|', $line );
$word_raw = trim( $parts[0] );
if ( empty($word_raw) ) continue;
$key = mb_strtolower( $word_raw, 'UTF-8' );
if ( !isset( $word_limit_map[$key] ) ) {
$word_limit_map[$key] = isset($parts[1]) ? intval(trim($parts[1])) : $default_limit;
$clean_kw_regex[] = preg_quote( $word_raw, '/' );
}
}
usort($clean_kw_regex, function($a, $b) { return strlen($b) - strlen($a); });
if ( ! empty( $clean_kw_regex ) ) {
$rule['regex_pattern'] = sprintf( '/\b(%s)\b/iu', implode( '|', $clean_kw_regex ) );
$rule['word_limits'] = $word_limit_map;
$prepared_rules[] = $rule;
}
}
if ( empty( $prepared_rules ) ) {
$all_post_ids = get_posts(['post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1]);
return [ 'total_links' => 0, 'total_recipients' => 0, 'orphan_count' => count($all_post_ids), 'top_cats' => [] ];
}
$min_donor_age = min(array_column($prepared_rules, 'donor_age'));
$donors = get_posts(['post_type' => 'post', 'posts_per_page' => -1, 'post_status' => 'publish', 'date_query' => [['before' => $min_donor_age . ' days ago']]]);
foreach ($donors as $post) {
$content_text = wp_strip_all_tags( $post->post_content );
if ( mb_strlen( $content_text, 'UTF-8' ) < $min_chars ) continue;
$post_age_days = floor( ( time() - strtotime( $post->post_date ) ) / 86400 );
$cats_ids = wp_get_post_categories( $post->ID );
$content_text = mb_substr( $content_text, $skip_chars );
$post_total_links = 0;
foreach ($prepared_rules as $rule) {
if ( $post_total_links >= $global_limit ) break;
if ( $rule['cat_id'] != 0 && !in_array( $rule['cat_id'], $cats_ids ) ) continue;
if ( $post_age_days < $rule['donor_age'] ) continue;
$counter_total = 0; $counter_words = [];
$limit_total = $rule['limit_total'] ?? 5;
if ( preg_match_all( $rule['regex_pattern'], $content_text, $matches ) ) {
foreach ( $matches[0] as $found_word ) {
if ( $counter_total >= $limit_total || $post_total_links >= $global_limit ) break;
$word_lower = mb_strtolower( $found_word, 'UTF-8' );
if ( ($counter_words[$word_lower] ?? 0) >= ($rule['word_limits'][$word_lower] ?? 1) ) continue;
if (empty($rule['target_pool'])) continue;
$pool_index = ( $post->ID + $counter_total ) % $rule['pool_size'];
$target_url = $rule['target_pool'][ $pool_index ];
$t_id = url_to_postid( $target_url );
if ( $t_id ) {
$real_db_count = (int) get_post_meta( $t_id, '_ailm_inbound_count', true );
$sim_count = $simulation_stats['simulated_inbound'][$t_id] ?? 0;
if ( ($real_db_count + $sim_count) >= $target_limit ) continue;
$simulation_stats['simulated_inbound'][$t_id] = $sim_count + 1;
}
$simulation_stats['urls'][$target_url] = ($simulation_stats['urls'][$target_url] ?? 0) + 1;
$simulation_stats['total_links']++;
$counter_total++;
$counter_words[$word_lower] = ($counter_words[$word_lower] ?? 0) + 1;
$post_total_links++;
}
}
}
}
$all_post_ids = get_posts(['numberposts' => -1, 'post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids']);
$orphan_count = 0;
foreach ($all_post_ids as $pid) {
$post_url = get_permalink($pid);
if (!isset($simulation_stats['urls'][$post_url]) && !isset($simulation_stats['simulated_inbound'][$pid])) {
$orphan_count++;
}
}
$cats_by_count = [];
foreach ($simulation_stats['urls'] as $url => $count) {
$id = url_to_postid($url);
if (!$id) continue;
$terms = get_the_terms($id, 'category');
if ($terms && !is_wp_error($terms)) foreach ($terms as $t) $cats_by_count[$t->name] = ($cats_by_count[$t->name] ?? 0) + $count;
}
arsort($cats_by_count);
return [
'total_links' => $simulation_stats['total_links'],
'total_recipients' => count($simulation_stats['urls']),
'orphan_count' => $orphan_count,
'top_cats' => array_slice($cats_by_count, 0, 5, true),
];
}
}