403Webshell
Server IP : 104.21.21.239  /  Your IP : 216.73.217.143
Web Server : Apache/2.4.68 (Amazon Linux) OpenSSL/3.5.5
System : Linux ip-172-31-69-123.ec2.internal 6.1.176-223.369.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Jul 24 13:34:27 UTC 2026 x86_64
User : ec2-user ( 1000)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /home/tgnew/wp-content/plugins/em-location-deduper/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/tgnew/wp-content/plugins/em-location-deduper/em-location-deduper.php
<?php
/**
 * Plugin Name: EM Location Deduper
 * Description: De-duplicate Events Manager locations with stronger normalization, multiple strategies (name/address/coords/slug), multisite awareness, and diagnostics preview.
 * Version:     1.1.0
 * Author:      Sam Later - LatCom Systems
 * License:     GPL-2.0+
 */

if (!defined('ABSPATH')) exit;

class EM_Location_Deduper {
    const MENU_SLUG = 'em-location-deduper';
    const NONCE     = 'em_location_deduper_nonce';
    const LOCK_KEY  = 'em_location_deduper_lock';

    public function __construct() {
        add_action('admin_menu', [$this, 'register_page']);
    }

    public function register_page() {
        add_management_page(
            'EM Location Deduper',
            'EM Location Deduper',
            'manage_options',
            self::MENU_SLUG,
            [$this, 'render_page']
        );
    }

    private function tables() {
        global $wpdb;
        $loc = $wpdb->prefix . 'em_locations';
        $evt = $wpdb->prefix . 'em_events';
        return [$loc, $evt];
    }

    private function column_exists($table, $col) {
        global $wpdb;
        $sql = $wpdb->prepare("SHOW COLUMNS FROM {$table} LIKE %s", $col);
        return (bool) $wpdb->get_var($sql);
    }

    private function acquire_lock($ttl = 120) {
        if (get_transient(self::LOCK_KEY)) return false;
        set_transient(self::LOCK_KEY, 1, $ttl);
        return true;
    }
    private function release_lock() {
        delete_transient(self::LOCK_KEY);
    }

	private function checkbox_field( $name, $checked, $text ) {
		// Hidden fallback so unchecked boxes submit "0"
		echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="0">';
		echo '<label><input type="checkbox" name="' . esc_attr( $name ) . '" value="1"' . checked( $checked, true, false ) . '> ' . esc_html( $text ) . '</label>';
	}

    // Stronger normalization: lowercase, trim, collapse spaces, strip punctuation, remove accents.
    private function normalize_strong($val) {
        $s = (string) $val;
        $s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        // remove accents
        if (function_exists('iconv')) {
            $t = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
            if ($t !== false) $s = $t;
        }
        $s = strtolower($s);
        // replace non-breaking space with regular
        $s = str_replace("\xC2\xA0", ' ', $s);
        // remove punctuation (keep digits/letters/spaces)
        $s = preg_replace('/[^a-z0-9\s]/', ' ', $s);
        // collapse whitespace
        $s = trim(preg_replace('/\s+/', ' ', $s));
        return $s;
    }

    private function make_key($row, $strategy) {
        // Pre-normalized values
        $name     = $this->normalize_strong($row->location_name ?? '');
        $address  = $this->normalize_strong($row->location_address ?? '');
        $town     = $this->normalize_strong($row->location_town ?? '');
        $state    = $this->normalize_strong($row->location_state ?? '');
        $postcode = $this->normalize_strong($row->location_postcode ?? '');
        $country  = $this->normalize_strong($row->location_country ?? '');
        $slug     = $this->normalize_strong($row->location_slug ?? '');

        switch ($strategy) {
            case 'strict':   return implode('|', [$name, $address, $town, $state, $postcode, $country]);
            case 'medium':   return implode('|', [$name, $address]);
            case 'loose':    return strtolower(trim((string)$row->location_name));
            case 'slug':     return $slug;
            case 'coords':
                $lat = isset($row->location_latitude) ? round((float)$row->location_latitude, 5) : '';
                $lng = isset($row->location_longitude) ? round((float)$row->location_longitude, 5) : '';
                return $lat . '|' . $lng;
            default:         return $name;
        }
    }

    public function render_page() {
        if (!current_user_can('manage_options')) wp_die('Insufficient permissions.');

        $defaults = [
            'dry_run'         => '1',
            'strategy'        => 'medium', // safer default
            'merge_fields'    => '1',
            'trash_cpt_posts' => '1',
            'limit'           => '0',
            'preview'         => '1',
            'preview_max'     => '25',
        ];
        $state = wp_parse_args($_POST ?: [], $defaults);

        $ran = false;
        $report = [];
        $preview = [];

        if (!empty($_POST) && check_admin_referer(self::NONCE)) {
            if (isset($_POST['do_preview'])) {
                $preview = $this->diagnostics_preview([
                    'strategy' => sanitize_text_field($state['strategy']),
                    'preview_max' => max(1, (int)$state['preview_max']),
                ]);
            } else {
                $ran = true;
                $report = $this->run([
                    'dry_run'         => !empty($state['dry_run']),
                    'strategy'        => sanitize_text_field($state['strategy']),
                    'merge_fields'    => !empty($state['merge_fields']),
                    'trash_cpt_posts' => !empty($state['trash_cpt_posts']),
                    'limit'           => max(0, (int)$state['limit']),
                ]);
            }
        }

        ?>
        <div class="wrap">
            <h1>EM Location Deduper</h1>
            <p>Find and merge duplicate Events Manager locations. Start by running a Preview to confirm the groups look right.</p>
            <form method="post">
                <?php wp_nonce_field(self::NONCE); ?>
                <table class="form-table" role="presentation">
                    <tr>
                        <th scope="row"><label for="strategy">Matching strategy</label></th>
                        <td>
                            <select name="strategy" id="strategy">
                                <option value="strict"  <?php selected($state['strategy'], 'strict');  ?>>Strict — name+address+town+state+postcode+country</option>
                                <option value="medium"  <?php selected($state['strategy'], 'medium');  ?>>Medium — name+address</option>
                                <option value="loose"   <?php selected($state['strategy'], 'loose');   ?>>Loose — name only</option>
                                <option value="coords"  <?php selected($state['strategy'], 'coords');  ?>>Coordinates — latitude/longitude (rounded)</option>
                                <option value="slug"    <?php selected($state['strategy'], 'slug');    ?>>Slug — location_slug</option>
                            </select>
                            <p class="description">If “no duplicates”, try <em>medium</em>, then <em>loose</em> or <em>coords</em>.</p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row">Dry run</th>
                        <td>
							<?php $this->checkbox_field( 'dry_run', ! empty( $state['dry_run'] ), 'Preview changes without modifying the database' );?> 
                        </td>
                    </tr>
                    <tr>
                        <th scope="row">Merge missing fields</th>
                        <td>
							<?php $this->checkbox_field( 'merge_fields', ! empty( $state['merge_fields'] ), 'Fill empty canonical fields from duplicates (address, town, state, postcode, country, lat/lng, phone, url)' );?> 
                        </td>
                    </tr>
                    <tr>
                        <th scope="row">Trash duplicate CPT posts</th>
                        <td>
							<?php $this->checkbox_field( 'trash_cpt_posts', ! empty( $state['trash_cpt_posts'] ), 'If a duplicate location has a linked post_id, move that post to trash' );?> 
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="limit">Max groups to process</label></th>
                        <td>
                            <input type="number" name="limit" id="limit" min="0" step="1" value="<?php echo esc_attr($state['limit']); ?>" placeholder="0 (no limit)">
                        </td>
                    </tr>
                </table>
                <p>
                    <button class="button button-secondary" name="do_preview" value="1">Preview duplicates</button>
                    <label style="margin-left:10px;">Show
                        <input type="number" name="preview_max" min="1" step="1" value="<?php echo esc_attr($state['preview_max']); ?>" style="width:80px;"> groups
                    </label>
                    <?php submit_button('Run Deduper', 'primary', '', false); ?>
                </p>
            </form>

            <?php if (!empty($preview)) : ?>
                <hr><h2>Diagnostics preview</h2>
                <p><strong>Strategy:</strong> <?php echo esc_html($preview['strategy']); ?> — <strong>Groups found:</strong> <?php echo intval($preview['groups_found']); ?></p>
                <?php if (!empty($preview['groups'])): ?>
                    <ol>
                        <?php foreach ($preview['groups'] as $g): ?>
                            <li>
                                <strong>Key:</strong> <?php echo esc_html($g['key']); ?> —
                                <strong>Members:</strong> <?php echo intval($g['count']); ?><br>
                                <code><?php echo esc_html(implode(" | ", $g['sample_labels'])); ?></code>
                            </li>
                        <?php endforeach; ?>
                    </ol>
                <?php else: ?>
                    <p>No duplicate groups found with this strategy.</p>
                <?php endif; ?>
            <?php endif; ?>

            <?php if (!empty($report)) : ?>
                <hr><h2>Report</h2>
                <?php echo $this->render_report($report); ?>
            <?php endif; ?>
        </div>
        <?php
    }

    private function render_report($r) {
        ob_start();
        if (!empty($r['errors'])) {
            echo '<div class="notice notice-error"><p><strong>Errors:</strong></p><ul>';
            foreach ($r['errors'] as $e) echo '<li>' . esc_html($e) . '</li>';
            echo '</ul></div>';
        }
        echo '<p><strong>Dry run:</strong> ' . (!empty($r['dry_run']) ? 'Yes' : 'No') . '</p>';
        echo '<p><strong>Strategy:</strong> ' . esc_html($r['strategy']) . '</p>';
        echo '<p><strong>Groups found:</strong> ' . intval($r['groups_found']) . '</p>';
        echo '<p><strong>Groups processed:</strong> ' . intval($r['groups_processed']) . '</p>';
        echo '<p><strong>Events reassigned:</strong> ' . intval($r['events_reassigned']) . '</p>';
        echo '<p><strong>Locations deleted:</strong> ' . intval($r['locations_deleted']) . '</p>';
        echo '<p><strong>Canonical fields updated:</strong> ' . intval($r['canon_updates']) . '</p>';
        if (!empty($r['details'])) {
            echo '<h3>Details</h3><ol>';
            foreach ($r['details'] as $d) echo '<li><pre style="white-space:pre-wrap;">' . esc_html($d) . '</pre></li>';
            echo '</ol>';
        }
        return ob_get_clean();
    }

    private function diagnostics_preview($args) {
        global $wpdb;
        list($em_locations, $em_events) = $this->tables();
        $strategy = in_array($args['strategy'], ['strict','medium','loose','coords','slug'], true) ? $args['strategy'] : 'medium';
        $limit = max(1, (int)$args['preview_max']);

        // Fetch needed columns (including slug + blog_id if present)
        $has_blog = $this->column_exists($em_locations, 'blog_id');
        $cols = "location_id, post_id, location_name, location_address, location_town, location_state, location_postcode, location_country, location_slug, location_latitude, location_longitude" . ($has_blog ? ", blog_id" : "");
        $where = $has_blog ? $wpdb->prepare("WHERE blog_id IN (0, %d)", get_current_blog_id()) : "";

        $rows = $wpdb->get_results("SELECT {$cols} FROM {$em_locations} {$where}");
		error_log( 'Found rows: ' . count($rows) );
        $groups = [];
        foreach ($rows as $row) {
            $key = $this->make_key($row, $strategy);
            if ($key === '' || $key === '|' || $key === '||') continue;
            $label = sprintf('#%d %s / %s', $row->location_id, $row->location_name, $row->location_address);
            $groups[$key]['items'][] = $label;
        }
        $dupes = [];
        foreach ($groups as $key => $g) {
            $count = count($g['items']);
            if ($count > 1) {
                $dupes[] = [
                    'key' => $key,
                    'count' => $count,
                    'sample_labels' => array_slice($g['items'], 0, 3)
                ];
            }
        }
        usort($dupes, function($a,$b){ return $b['count'] <=> $a['count']; });
        return [
            'strategy' => $strategy,
            'groups_found' => count($dupes),
            'groups' => array_slice($dupes, 0, $limit),
        ];
    }

    private function run($args) {
        global $wpdb;
        list($em_locations, $em_events) = $this->tables();

        $dry_run         = !empty($args['dry_run']);
        $strategy        = in_array($args['strategy'], ['strict','medium','loose','coords','slug'], true) ? $args['strategy'] : 'medium';
        $merge_fields    = !empty($args['merge_fields']);
        $trash_posts     = !empty($args['trash_cpt_posts']);
        $limit_groups    = max(0, (int)$args['limit']);

        $r = [
            'dry_run'           => $dry_run,
            'strategy'          => $strategy,
            'groups_found'      => 0,
            'groups_processed'  => 0,
            'events_reassigned' => 0,
            'locations_deleted' => 0,
            'canon_updates'     => 0,
            'details'           => [],
            'errors'            => [],
        ];

        if (!$this->acquire_lock()) {
            $r['errors'][] = 'Another dedupe job is running. Try again shortly.';
            return $r;
        }

        try {
            $has_blog = $this->column_exists($em_locations, 'blog_id');
            $cols = "location_id, post_id, location_name, location_address, location_town, location_state, location_postcode, location_region, location_country, location_latitude, location_longitude, location_slug" . ($has_blog ? ", blog_id" : "");
            $where = $has_blog ? $wpdb->prepare("WHERE blog_id IN (0, %d)", get_current_blog_id()) : "";

            $rows = $wpdb->get_results("SELECT {$cols} FROM {$em_locations} {$where}");
			error_log( 'Found rows: ' . count($rows) );
			error_log( 'SQL: ' . "SELECT {$cols} FROM {$em_locations} {$where}" );

            if (!is_array($rows)) throw new RuntimeException('Failed to read em_locations.');

            // Group by key
            $groups = [];
            foreach ($rows as $row) {
                $key = $this->make_key($row, $strategy);
                if ($key === '' || $key === '|' || $key === '||') continue;
                $groups[$key][] = $row;
            }

            // Keep only groups with >1
            $dupe_groups = array_filter($groups, function($list){ return count($list) > 1; });
            $r['groups_found'] = count($dupe_groups);

            $processed = 0;
            foreach ($dupe_groups as $key => $list) {
                if ($limit_groups && $processed >= $limit_groups) break;

                // Count events per location
                $stats = [];
                foreach ($list as $row) {
                    $stats[$row->location_id] = (int) $wpdb->get_var(
                        $wpdb->prepare("SELECT COUNT(*) FROM {$em_events} WHERE location_id = %d", $row->location_id)
                    );
                }

				// First try to find slug without numeric suffixes
				$canonical = null;
				$slug_pattern = '/-\d+$/'; // matches -1, -2 etc. at end of slug

				$ix = -1;
				foreach ($list as $row) {
					$ix++;
					$slug = isset($row->location_slug) ? $row->location_slug : '';
					if ($slug !== '' && !preg_match($slug_pattern, $slug)) {
						// Exact base slug found, pick this and stop
						$canonical = $row;
						$dupes = $list;
						unset($dupes[$ix]);
						break;
					}
				}

				// If no base slug found, fall back to most events / lowest ID
				if (!$canonical) {
					usort($list, function($a,$b) use ($stats){
						$cmp = $stats[$b->location_id] <=> $stats[$a->location_id];
						return $cmp !== 0 ? $cmp : ($a->location_id <=> $b->location_id);
					});
					$canonical = $list[0];
					$dupes = array_slice($list, 1);
				}

                $detail = [];
                $detail[] = "Group key: {$key}";
                $detail[] = "Canonical: #{$canonical->location_id} \"{$canonical->location_name}\" (events: {$stats[$canonical->location_id]})";

                // Reassign events
                $dupe_ids = array_map(function($d){ return (int)$d->location_id; }, $dupes);
                $reassigned = 0;
                if (!empty($dupe_ids)) {
                    $in = implode(',', array_fill(0, count($dupe_ids), '%d'));
                    $event_ids = $wpdb->get_col($wpdb->prepare("SELECT event_id FROM {$em_events} WHERE location_id IN ($in)", $dupe_ids));
                    $reassigned = count($event_ids);
                    if (!$dry_run && $reassigned > 0) {
                        $params = array_merge([(int)$canonical->location_id], $dupe_ids);
                        $wpdb->query($wpdb->prepare("UPDATE {$em_events} SET location_id = %d WHERE location_id IN ($in)", $params));
						// Update post meta for the event CPT
						$post_ids = $wpdb->get_col(
							$wpdb->prepare("SELECT post_id FROM {$wpdb->prefix}em_events WHERE location_id = %d", $canonical->location_id)
						);
						foreach ( $post_ids as $event_post_id ) {
							update_post_meta( $event_post_id, '_location_id', (int) $canonical->location_id );
							// Optional: also sync name/slug if you want them fresh
							update_post_meta( $event_post_id, '_location_name', $canonical->location_name );
							update_post_meta( $event_post_id, '_location_slug', $canonical->location_slugslug );
						}
                    }
                }
                $r['events_reassigned'] += $reassigned;
                $detail[] = "Events reassigned: {$reassigned}";

                // Merge fields
                $updated_fields = 0;
                if ($merge_fields) {
                    $fields = [
                        'location_address','location_town','location_state','location_postcode','location_country',
                        'location_latitude','location_longitude'
                    ];
                    $updates = [];
                    foreach ($fields as $f) {
                        $cur = $canonical->{$f};
                        if ($cur === null || $cur === '' || $cur === '0') {
                            foreach ($dupes as $d) {
                                $val = $d->{$f};
                                if ($val !== null && $val !== '' && $val !== '0') { $updates[$f] = $val; break; }
                            }
                        }
                    }
                    if (!empty($updates) && !$dry_run) {
                        $set = [];
                        $vals = [];
                        foreach ($updates as $k => $v) { $set[] = "{$k} = %s"; $vals[] = $v; }
                        $vals[] = (int)$canonical->location_id;
                        $wpdb->query($wpdb->prepare("UPDATE {$em_locations} SET " . implode(', ', $set) . " WHERE location_id = %d", $vals));
                        $updated_fields = count($updates);
                        $r['canon_updates'] += $updated_fields;
                    }
                }
                if ($updated_fields) $detail[] = "Canonical fields merged: {$updated_fields}";

                // Trash CPT posts and delete dupe rows
                $deleted = 0;
                foreach ($dupes as $d) {
                    $detail[] = sprintf('Duplicate: #%d "%s" (events: %d)', $d->location_id, $d->location_name, $stats[$d->location_id]);
                    if (!$dry_run && $trash_posts && !empty($d->post_id)) {
                        $post = get_post((int)$d->post_id);
                        if ($post && $post->post_status !== 'trash') wp_trash_post($post->ID);
                    }
                    if (!$dry_run) {
                        $wpdb->delete($em_locations, ['location_id' => (int)$d->location_id], ['%d']);
                        $deleted++;
                    }
                }
                $r['locations_deleted'] += $deleted;
                if ($deleted) $detail[] = "Duplicates deleted: {$deleted}";

                $r['details'][] = implode("\n", $detail);
                $processed++;
            }
            $r['groups_processed'] = $processed;

        } catch (Throwable $e) {
            $r['errors'][] = $e->getMessage();
        } finally {
            $this->release_lock();
        }

        return $r;
    }
}

new EM_Location_Deduper();

Youez - 2016 - github.com/yon3zu
LinuXploit