Plugin Directory

Changeset 3432780


Ignore:
Timestamp:
01/05/2026 02:16:16 PM (3 months ago)
Author:
surflabtech
Message:

v2.3.5

Location:
surflink
Files:
302 added
6 edited

Legend:

Unmodified
Added
Removed
  • surflink/trunk/assets/js/redirects.js

    r3430214 r3432780  
    781781  });
    782782
     783
     784  $(document).on("click", "#surfl-toggle-srpt-btn", function () {
     785    slideContent($(this), $("#surfl-srpt-advanced-options"));
     786  });
     787
    783788  //hard-linker toggle
    784789
     
    789794
    790795  //soft-linker toggle
    791 
    792796  $(document).on("click", "#surfl-toggle-soft-linker-btn", function () {
    793797    slideContent($(this), $("#surfl-soft-linker-advanced-options-one"));
     
    802806  // Search and Replace
    803807  $(document).on("click", "#surfl-goto-search-history", function () {
    804     const tab = document.querySelectorAll(".surfl-side-nav")[2];
     808    const tab = document.querySelectorAll(".surfl-side-nav")[3];
    805809    if (tab) {
    806810      tab.dispatchEvent(
  • surflink/trunk/assets/js/surfl.js

    r3430612 r3432780  
    334334 *
    335335 *
    336  ********* SR/SRURL
     336 ********* SR/SRURL/SRPT
    337337 *
    338338 *
     
    360360  $(document).on("change", "#surfl-srurl-dry-run", function () {
    361361    handleCheck($(this), $("#surfl-srurl-status-text"));
     362  });
     363
     364  //SRPT
     365  $(document).on("change", "#surfl-srpt-dry-run", function () {
     366    handleCheck($(this), $("#surfl-srpt-status-text"));
    362367  });
    363368
     
    502507
    503508 
     509
     510    // Handle URL Replace form submission via AJAX
     511  $(document).on("submit", "#surfl-srpt-replace-form", function (e) {
     512    e.preventDefault();
     513    const form = $(this);
     514    const submitButton = form.find("#submit-srpt-replace-form");
     515    const originalButtonText = submitButton.html();
     516    processBatch(
     517      form,
     518      submitButton,
     519      originalButtonText,
     520      "surfl_process_post_title_replace",
     521      "#surfl-srpt-replace-report-container-ajax"
     522    );
     523  });
    504524});
    505525
  • surflink/trunk/includes/class-surfl-fast-sr.php

    r3431922 r3432780  
    3535        add_action( 'wp_ajax_surfl_delete_srh', [$this, 'ajax_delete_srh'] );
    3636        add_action( 'wp_ajax_surfl_process_replace', [$this, 'ajax_process_replace'] );
     37        add_action( 'wp_ajax_surfl_process_post_title_replace', [$this, 'ajax_process_post_title_replace'] );
    3738    }
    3839
     
    187188                return $value;
    188189            }
     190        }
     191        // MULTILINGUAL & UTF-8 COMPATIBILITY
     192        if ( $this->case_insensitive ) {
     193            // Quote the search string so regex characters (like ., *, +) are treated as text
     194            $quoted_search = preg_quote( $search, '/' );
     195            // 'u' modifier = UTF-8, 'i' modifier = Case Insensitive
     196            $data = preg_replace( '/' . $quoted_search . '/iu', $replace, $value_str );
     197        } else {
     198            // Standard str_replace is usually fine for Case-Sensitive UTF-8
     199            $data = str_replace( $search, $replace, $value_str );
     200        }
     201        if ( $data !== $value ) {
     202            $this->collect_contents( $value, $data );
     203        }
     204        // Collect contents if modified
     205        return $data;
     206    }
     207
     208    private function replace_noserialized_value( $value, $search, $replace ) {
     209        // Ensure inputs are strings
     210        $value_str = (string) $value;
     211        // Use mb_stripos for UTF-8 aware position check
     212        if ( $this->case_insensitive ) {
     213            $found = mb_stripos( $value_str, $search );
     214        } else {
     215            $found = mb_strpos( $value_str, $search );
     216        }
     217        if ( $found === false ) {
     218            return $value;
    189219        }
    190220        // MULTILINGUAL & UTF-8 COMPATIBILITY
     
    613643        $is_numeric
    614644    ) {
     645    }
     646
     647    private function process_pt_batch(
     648        $table,
     649        $primary_key,
     650        $column,
     651        $category,
     652        $rows,
     653        $search,
     654        $replace,
     655        $dry_run
     656    ) {
     657        $case_statements = [];
     658        $ids = [];
     659        foreach ( $rows as $row ) {
     660            $original = $row[$column];
     661            $modified = $this->replace_noserialized_value( $original, $search, $replace );
     662            if ( $original !== $modified ) {
     663                $occ = $this->count_occurrences( $search, $original );
     664                if ( !isset( $this->report_details[$category]['columns'][$column]['occurrences'] ) ) {
     665                    $this->report_details[$category]['columns'][$column]['occurrences'] = 0;
     666                }
     667                if ( !isset( $this->report_details[$category]['columns'][$column]['rows_modified'] ) ) {
     668                    $this->report_details[$category]['columns'][$column]['rows_modified'] = 0;
     669                }
     670                $this->report_details[$category]['columns'][$column]['occurrences'] += $occ;
     671                $this->report_details[$category]['columns'][$column]['rows_modified']++;
     672                if ( !$dry_run ) {
     673                    $case_statements[] = $this->wpdb->prepare( "WHEN %d THEN %s", $row[$primary_key], $modified );
     674                }
     675                $ids[] = (int) $row[$primary_key];
     676            }
     677        }
     678        if ( !empty( $case_statements ) ) {
     679            if ( !$dry_run ) {
     680                $this->wpdb->query( 'START TRANSACTION' );
     681                $sql = "UPDATE {$table} SET {$column} = CASE {$primary_key} \r\n                        " . implode( ' ', $case_statements ) . " \r\n                        END WHERE {$primary_key} IN (" . implode( ',', array_unique( $ids ) ) . ")";
     682                $result = $this->wpdb->query( $sql );
     683                if ( $result === false ) {
     684                    $this->wpdb->query( 'ROLLBACK' );
     685                    throw new Exception("Update failed for column {$column} in table {$table}: " . $this->wpdb->last_error);
     686                }
     687                $this->wpdb->query( 'COMMIT' );
     688            }
     689            $this->reports[$category] = ($this->reports[$category] ?? 0) + count( $ids );
     690        }
    615691    }
    616692
     
    862938    }
    863939
     940    public function render_title_replace_ui() {
     941        if ( !current_user_can( 'manage_options' ) ) {
     942            wp_die( 'Access denied' );
     943        }
     944        require_once SURFL_PATH . "templates/surfl-post-title-replace.php";
     945    }
     946
    864947    public function ajax_process_url_replace() {
     948    }
     949
     950    private function generate_post_type_for_title( $categories ) {
     951        global $wpdb;
     952        $tasks = [];
     953        // Check if categories are provided
     954        if ( empty( $categories ) ) {
     955            return [];
     956        }
     957        // $categories now contains post type slugs (e.g., 'post', 'page', 'product')
     958        foreach ( $categories as $post_type ) {
     959            // Validate if the post type exists to be safe
     960            if ( !post_type_exists( $post_type ) ) {
     961                continue;
     962            }
     963            // Create the specific WHERE clause for this post type
     964            // We ignore: Trash, Auto Drafts, Inherit (revisions)
     965            $where_clause = $wpdb->prepare( "post_type = %s AND post_status NOT IN ('inherit', 'trash', 'auto-draft')", $post_type );
     966            // Get Post Type Object for a nice Label
     967            $pt_object = get_post_type_object( $post_type );
     968            $label = ( $pt_object ? $pt_object->labels->name : ucfirst( $post_type ) );
     969            $tasks[] = [
     970                'table'    => $wpdb->posts,
     971                'column'   => 'post_title',
     972                'where'    => $where_clause,
     973                'category' => $label,
     974            ];
     975        }
     976        return $tasks;
     977    }
     978
     979    public function ajax_process_post_title_replace() {
     980        check_ajax_referer( 'hyperdb_replace', 'nonce' );
     981        if ( !current_user_can( 'manage_options' ) ) {
     982            wp_send_json_error( [
     983                'message' => esc_html__( 'Access denied', 'surflink' ),
     984            ] );
     985        }
     986        $search = trim( wp_unslash( $_POST['search'] ) );
     987        $replace = trim( wp_unslash( $_POST['replace'] ) );
     988        $categories = ( isset( $_POST['categories'] ) ? array_map( 'sanitize_text_field', $_POST['categories'] ) : [] );
     989        if ( empty( $search ) || empty( $replace ) || empty( $categories ) ) {
     990            wp_send_json_error( [
     991                'message' => 'Missing Input',
     992            ] );
     993        }
     994        self::log( 'search: ' . $search );
     995        self::log( 'replace: ' . $replace );
     996        if ( $search === $replace ) {
     997            wp_send_json_error( [
     998                'message' => 'Search and Replace input are same',
     999            ] );
     1000        }
     1001        $dry_run = isset( $_POST['dry_run'] ) && $_POST['dry_run'] == '1';
     1002        $case_insensitive = isset( $_POST['case_insensitive'] ) && $_POST['case_insensitive'] == '1';
     1003        $offset = ( isset( $_POST['offset'] ) ? intval( $_POST['offset'] ) : 0 );
     1004        $current_task_index = ( isset( $_POST['current_table_index'] ) ? intval( $_POST['current_table_index'] ) : 0 );
     1005        $transient_key = 'surfl_replace_title_batch_state';
     1006        $state = get_transient( $transient_key );
     1007        if ( $offset === 0 && $current_task_index === 0 || $state === false ) {
     1008            // Initial call or state expired/not found
     1009            $tasks = $this->generate_post_type_for_title( $categories );
     1010            $state = [
     1011                'total_rows'          => 0,
     1012                'processed_rows'      => 0,
     1013                'current_task_index'  => 0,
     1014                'current_task_offset' => 0,
     1015                'tasks_to_process'    => $tasks,
     1016                'reports'             => [],
     1017                'report_details'      => [],
     1018                'modified_table'      => [],
     1019                'start_time'          => microtime( true ),
     1020                'history_id'          => false,
     1021            ];
     1022            // Calculate total rows for all tasks
     1023            foreach ( $tasks as $task ) {
     1024                $table = $task['table'];
     1025                $where = $task['where'];
     1026                $query = "SELECT COUNT(*) FROM {$table}";
     1027                if ( !empty( $where ) ) {
     1028                    $query .= " WHERE {$where}";
     1029                }
     1030                $state['total_rows'] += (int) $this->wpdb->get_var( $query );
     1031            }
     1032            $history_id = $this->insert_into_history_table(
     1033                $search,
     1034                $replace,
     1035                $dry_run,
     1036                $case_insensitive,
     1037                'srurl'
     1038            );
     1039            $state['history_id'] = ( $history_id ? intval( $history_id ) : false );
     1040        } else {
     1041            // Load existing report details and modified tables from state
     1042            $state['current_task_index'] = $current_task_index;
     1043            $this->report_details = $state['report_details'];
     1044            $this->modified_table = $state['modified_table'];
     1045            // in this case modified category
     1046        }
     1047        $is_complete = false;
     1048        $message = '';
     1049        $reports = $state['reports'];
     1050        $location = '';
     1051        // Process tasks in batches
     1052        if ( $state['current_task_index'] < count( $state['tasks_to_process'] ) ) {
     1053            $current_task = $state['tasks_to_process'][$state['current_task_index']];
     1054            $table = $current_task['table'];
     1055            $column = $current_task['column'];
     1056            $where = $current_task['where'];
     1057            $category = $current_task['category'];
     1058            $location = $category;
     1059            $rows = [];
     1060            $primary_key = 'ID';
     1061            $query = "SELECT {$primary_key}, {$column} FROM {$table}";
     1062            if ( !empty( $where ) ) {
     1063                $query .= " WHERE {$where}";
     1064            }
     1065            $query .= " LIMIT {$state['current_task_offset']}, {$this->batch_size}";
     1066            $rows = $this->wpdb->get_results( $query, ARRAY_A );
     1067            if ( !empty( $rows ) ) {
     1068                $this->case_insensitive = $case_insensitive;
     1069                $table_start = microtime( true );
     1070                $this->process_pt_batch(
     1071                    $table,
     1072                    $primary_key,
     1073                    $column,
     1074                    $category,
     1075                    $rows,
     1076                    $search,
     1077                    $replace,
     1078                    $dry_run
     1079                );
     1080                // Accumulate report details and modified tables
     1081                if ( !isset( $this->report_details[$category] ) ) {
     1082                    $this->report_details[$category] = [
     1083                        'columns' => [],
     1084                        'time'    => 0,
     1085                        'table'   => $table,
     1086                    ];
     1087                }
     1088                if ( !isset( $this->report_details[$category]['columns'][$column] ) ) {
     1089                    $this->report_details[$category]['columns'][$column] = [
     1090                        'occurrences'   => 0,
     1091                        'rows_modified' => 0,
     1092                    ];
     1093                }
     1094                if ( !isset( $this->report_details[$category]['time'] ) ) {
     1095                    $this->report_details[$category]['time'] = 0;
     1096                }
     1097                $this->report_details[$category]['time'] += microtime( true ) - $table_start;
     1098                self::log( "URL Replace - {$category} processing time: " . (microtime( true ) - $table_start) . " seconds." );
     1099                if ( !in_array( $category, $this->modified_table ) && ($this->report_details[$category]['columns'][$column]['occurrences'] > 0 || $this->report_details[$category]['columns'][$column]['rows_modified'] > 0) ) {
     1100                    $this->modified_table[] = $category;
     1101                }
     1102                $reports[$category] = ($reports[$category] ?? 0) + count( $rows );
     1103                // Count rows processed in this batch
     1104                $state['processed_rows'] += count( $rows );
     1105                $state['current_task_offset'] += $this->batch_size;
     1106                $message = sprintf(
     1107                    esc_html__( 'Processing URL replace for %s category,table %s, column %s.', 'surflink' ),
     1108                    $category,
     1109                    $table,
     1110                    $column
     1111                );
     1112                // Simplified message
     1113                self::log( "ajax_process_post_title_replace - Processing task: {$category}, Column {$column}. Offset: {$state['current_task_offset']}. Processed rows: {$state['processed_rows']}. Total rows: {$state['total_rows']}." );
     1114            } else {
     1115                // Current task finished, move to next
     1116                $message = sprintf(
     1117                    esc_html__( 'Finished URL replace for  %s category,table %s, column %s.', 'surflink' ),
     1118                    $category,
     1119                    $table,
     1120                    $column
     1121                );
     1122                self::log( "ajax_process_post_title_replace - Finished task for {$category} category, Column {$column}. Moving to next task." );
     1123                $state['current_task_index']++;
     1124                if ( $state['current_task_index'] >= count( $state['tasks_to_process'] ) ) {
     1125                    $is_complete = true;
     1126                }
     1127                $state['current_task_offset'] = 0;
     1128                // Reset offset for next task
     1129            }
     1130        } else {
     1131            $is_complete = true;
     1132            self::log( "ajax_process_post_title_replace - All tasks processed. Operation complete." );
     1133        }
     1134        $state['reports'] = $reports;
     1135        $state['report_details'] = $this->report_details;
     1136        // Save accumulated report details
     1137        $state['modified_table'] = $this->modified_table;
     1138        // Save accumulated modified tables
     1139        set_transient( $transient_key, $state, HOUR_IN_SECONDS );
     1140        $this->update_history_table( $state, $location, $is_complete );
     1141        if ( $is_complete ) {
     1142            $elapsed_time = round( microtime( true ) - $state['start_time'], 4 );
     1143            if ( empty( $reports['errors'] ) ) {
     1144                $msg = ( $dry_run ? "Dry run completed in {$elapsed_time} seconds. No changes were made." : "Operation completed in {$elapsed_time} seconds." );
     1145                $reports['success'][] = $msg;
     1146            }
     1147            $reports['dry_run'] = $dry_run;
     1148            $this->reports = $reports;
     1149            $this->reports['details'] = $this->report_details;
     1150            // Ensure final reports include accumulated details
     1151            ob_start();
     1152            require SURFL_PATH . "templates/surfl-sr-report.php";
     1153            $report_html = ob_get_clean();
     1154            ob_start();
     1155            $this->render_search_history();
     1156            $history_html = ob_get_clean();
     1157            delete_transient( $transient_key );
     1158            // Clean up transient
     1159            wp_send_json_success( [
     1160                'is_complete'         => true,
     1161                'report_html'         => $report_html,
     1162                'history_html'        => $history_html,
     1163                'reports_data'        => $this->reports,
     1164                'message'             => $msg,
     1165                'current_table_index' => $state['current_task_index'],
     1166            ] );
     1167        } else {
     1168            wp_send_json_success( [
     1169                'is_complete'         => false,
     1170                'offset'              => $state['current_task_offset'],
     1171                'total_rows'          => $state['total_rows'],
     1172                'processed_rows'      => $state['processed_rows'],
     1173                'message'             => $message,
     1174                'current_table'       => $current_task['category'] ?? '',
     1175                'current_table_index' => $state['current_task_index'],
     1176            ] );
     1177        }
    8651178    }
    8661179
  • surflink/trunk/includes/class-surfl-loader.php

    r3430214 r3432780  
    268268        );
    269269        $this->render_nav_item(
     270            $page_slug,
     271            'surfl-srpt-tab',
     272            'dashicons-edit-page',
     273            'Title Updater',
     274            $active_tab
     275        );
     276        $this->render_nav_item(
    270277            'surfl-search-replace',
    271278            'surfl-srh-tab',
     
    285292            set_transient( $tab_transient, $active_tab, 5 * MINUTE_IN_SECONDS );
    286293            $this->surfl_fast_sr->render_url_replace_ui();
     294        } elseif ( $active_tab === 'surfl-srpt-tab' ) {
     295            set_transient( $tab_transient, $active_tab, 5 * MINUTE_IN_SECONDS );
     296            $this->surfl_fast_sr->render_title_replace_ui();
    287297        } elseif ( $active_tab === 'surfl-srh-tab' ) {
    288298            set_transient( $tab_transient, $active_tab, 5 * MINUTE_IN_SECONDS );
  • surflink/trunk/readme.txt

    r3431922 r3432780  
    66**Requires PHP:** 7.4   
    77**Tested up to:** 6.9 
    8 **Stable tag:** 2.3.4
     8**Stable tag:** 2.3.5
    99**License:** GPLv3 or later 
    1010**License URI:** https://opensource.org/licenses/GPL-3.0 
     
    7070#### Free Version Includes:
    7171* **Search & Replace:** Standard DB search/replace, Dry Run, History Logs, "View Changes" diff.
     72* **Easy Title Updater:** Update post titles with a single click in post, page and custom post types. Dry Run, History Logs, "View Changes" diff.
    7273* **Redirects:** Add specific (single/bulk) redirects, 404 Logs, Specific 410 (Gone) status, Import/Export.
    7374* **Backup:** Manual Backup creation, Saved Backup logs, Manual Restore, Selective Restore.
     
    141142== Changelog ==
    142143
     144= 2.3.5 =
     145* New Feature: Title Updater ! Now you can easily update post titles in pages, posts and custom post types. Dry run is also supported for safe testing.Also includes History Logs.
     146
    143147= 2.3.4 =
    144148* Improved: search and replace now support handling nested serialized data.
     
    235239== Upgrade Notice ==
    236240
    237 Updated to version **2.0.0**, SurfLink is now structured into five powerful modules managed via a central Module Manager, keeping your admin interface clean and your site fast.
     241New Feature: Title Updater ! Now you can easily update post titles in pages, posts and custom post types. Dry run is also supported for safe testing.Also includes History Logs.
  • surflink/trunk/surf-link.php

    r3431922 r3432780  
    77 * Author: SurfLab
    88 * Author URI: https://surflabtech.com
    9  * Version: 2.3.4
     9 * Version: 2.3.5
    1010 * Text Domain: surflink
    1111 * License: GPL-3.0-or-later
     
    6767    }
    6868    if ( !defined( 'SURFL_VERSION' ) ) {
    69         define( 'SURFL_VERSION', '2.3.4' );
     69        define( 'SURFL_VERSION', '2.3.5' );
    7070    }
    7171    if ( !defined( 'SURFL_PLUGIN' ) ) {
     
    9999        }
    100100        if ( !defined( 'SURFL_VERSION' ) ) {
    101             define( 'SURFL_VERSION', '2.3.4' );
     101            define( 'SURFL_VERSION', '2.3.5' );
    102102        }
    103103        if ( !defined( 'SURFL_SITE_URL' ) ) {
Note: See TracChangeset for help on using the changeset viewer.