PluginProbe
Delete Duplicate Posts / 5.0.3
Delete Duplicate Posts v5.0.3
trunk 5.0.3 5.1
delete-duplicate-posts / delete-duplicate-posts.php

delete-duplicate-posts.php in Delete Duplicate Posts 5.0.3, at delete-duplicate-posts.php

1,966 lines 84.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 Plugin Name: Delete Duplicate Posts
5 Plugin Script: delete-duplicate-posts.php
6 Plugin URI: https://cleverplugins.com
7 Description: Remove duplicate blogposts on your blog! Searches and removes duplicate posts and their post meta tags. You can delete posts, pages and other Custom Post Types enabled on your website.
8 Version: 5.0.3
9 Author: cleverplugins.com
10 Author URI: https://cleverplugins.com
11 Min WP Version: 4.7
12 Max WP Version: 6.9.1
13 Text Domain: delete-duplicate-posts
14 Domain Path: /languages
15 */
16 namespace DeleteDuplicatePosts;
17
18 if ( !defined( 'ABSPATH' ) ) {
19 exit;
20 }
21 require_once __DIR__ . '/vendor/autoload.php';
22 if ( function_exists( '\\DeleteDuplicatePosts\\ddp_fs' ) ) {
23 \DeleteDuplicatePosts\ddp_fs()->set_basename( false, __FILE__ );
24 } else {
25 // DO NOT REMOVE THIS IF, IT IS ESSENTIAL FOR THE `function_exists` CALL ABOVE TO PROPERLY WORK.
26 if ( !function_exists( '\\DeleteDuplicatePosts\\ddp_fs' ) ) {
27 // Create a helper function for easy SDK access.
28 function ddp_fs() {
29 global $ddp_fs;
30 if ( !isset( $ddp_fs ) ) {
31 // Activate multisite network integration.
32 if ( !defined( 'WP_FS__PRODUCT_925_MULTISITE' ) ) {
33 define( 'WP_FS__PRODUCT_925_MULTISITE', true );
34 }
35 // Include Freemius SDK.
36 // SDK is auto-loaded through composer
37 $ddp_fs = fs_dynamic_init( array(
38 'id' => '925',
39 'slug' => 'delete-duplicate-posts',
40 'type' => 'plugin',
41 'public_key' => 'pk_0af9f9e83f00e23728a55430a57dd',
42 'is_premium' => false,
43 'premium_suffix' => 'Pro',
44 'has_addons' => false,
45 'has_paid_plans' => true,
46 'menu' => array(
47 'slug' => 'delete-duplicate-posts',
48 'first-path' => 'tools.php?page=delete-duplicate-posts&welcome-message=true',
49 'contact' => false,
50 'support' => false,
51 'parent' => array(
52 'slug' => 'tools.php',
53 ),
54 ),
55 'is_live' => true,
56 'is_org_compliant' => true,
57 ) );
58 }
59 return $ddp_fs;
60 }
61
62 // Init Freemius.
63 ddp_fs();
64 // Signal that SDK was initiated.
65 do_action( 'ddp_fs_loaded' );
66 }
67 ddp_fs()->add_action( 'after_uninstall', 'ddp_fs_uninstall_cleanup' );
68 }
69 /**
70 * Cleans up when uninstalling
71 *
72 * @author Lars Koudal
73 * @since v0.0.1
74 * @version v1.0.0 Tuesday, January 12th, 2021.
75 * @return void
76 */
77 function ddp_fs_uninstall_cleanup() {
78 global $wpdb;
79 $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'ddp_log' );
80 $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'ddp_redirects' );
81 delete_option( 'ddp_deleted_duplicates' );
82 delete_option( 'delete_duplicate_posts_options_v4' );
83 }
84
85 if ( !class_exists( __NAMESPACE__ . '\\Delete_Duplicate_Posts' ) ) {
86 class Delete_Duplicate_Posts {
87 public static $options_name = 'delete_duplicate_posts_options_v4';
88
89 public $localization_domain = 'delete-duplicate-posts';
90
91 public static $options = null;
92
93 public function __construct() {
94 // Adds extra permissions to Freemius
95 if ( function_exists( 'ddp_fs' ) ) {
96 ddp_fs()->add_filter( 'permission_list', array(__CLASS__, 'add_freemius_extra_permission') );
97 }
98 global $ddp_fs;
99 add_action(
100 'admin_head',
101 array(__CLASS__, 'set_custom_help_content'),
102 1,
103 2
104 );
105 self::get_options();
106 add_action( 'wp_ajax_ddp_get_loglines', array(__CLASS__, 'return_loglines_ajax') );
107 add_action( 'wp_ajax_ddp_get_duplicates', array(__CLASS__, 'return_duplicates_ajax') );
108 add_action( 'init', array(__CLASS__, 'do_init') );
109 add_action( 'wp_ajax_ddp_delete_duplicates', array(__CLASS__, 'delete_duplicates_ajax') );
110 // loads admin notices
111 add_action( 'admin_menu', array($this, 'admin_menu_link') );
112 add_action( 'admin_enqueue_scripts', array($this, 'admin_enqueue_scripts') );
113 add_action(
114 'wp_insert_site',
115 array($this, 'on_create_blog'),
116 99999,
117 1
118 );
119 add_filter( 'wpmu_drop_tables', array($this, 'on_delete_blog') );
120 register_activation_hook( __FILE__, array($this, 'install') );
121 add_action( 'ddp_cron', array($this, 'cleandupes') );
122 add_action( 'cron_schedules', array($this, 'add_cron_intervals') );
123 }
124
125 /**
126 * do_init.
127 *
128 * @author Lars Koudal
129 * @since v0.0.1
130 * @version v1.0.0 Monday, October 28th, 2024.
131 * @access public static
132 * @return void
133 */
134 public static function do_init() {
135 global $ddp_fs;
136 $determine_locale = determine_locale();
137 }
138
139 /**
140 * delete_duplicates_ajax.
141 *
142 * @author Lars Koudal
143 * @author Unknown
144 * @since v0.0.1
145 * @version v1.0.0 Tuesday, January 12th, 2021.
146 * @version v1.0.1 Tuesday, October 31st, 2023.
147 * @version v1.0.2 Wednesday, November 1st, 2023.
148 * @version v1.0.3 Tuesday, April 2nd, 2024.
149 * @version v1.0.4 Tuesday, May 7th, 2024.
150 * @access public static
151 * @param boolean $return_data Default: false
152 * @return mixed
153 */
154 public static function delete_duplicates_ajax( $return_data = false ) {
155 // Check user permissions
156 if ( !current_user_can( 'manage_options' ) ) {
157 wp_send_json_error( __( 'You do not have sufficient permissions to perform this action.', 'delete-duplicate-posts' ) );
158 return;
159 }
160 // Verify the AJAX request, to prevent processing requests external of the site.
161 check_ajax_referer( 'cp_ddp_delete_loglines' );
162 // Log the cleaning action
163 self::log( __( 'Cleaning duplicates', 'delete-duplicate-posts' ) );
164 // Validate the POST data
165 $checked_posts = $_POST['checked_posts'] ?? null;
166 if ( empty( $checked_posts ) || !is_array( $checked_posts ) ) {
167 wp_send_json_error( __( 'No duplicates were selected?', 'delete-duplicate-posts' ) );
168 return;
169 }
170 // Process and sanitize the checked posts
171 $cleaned_posts = array();
172 foreach ( $checked_posts as $cp ) {
173 if ( !empty( $cp['ID'] ) && !empty( $cp['orgID'] ) && is_numeric( $cp['ID'] ) && is_numeric( $cp['orgID'] ) ) {
174 $cleaned_posts[] = array(
175 'ID' => intval( $cp['ID'] ),
176 'orgID' => intval( $cp['orgID'] ),
177 );
178 }
179 }
180 // Check if any valid posts were found
181 if ( empty( $cleaned_posts ) ) {
182 wp_send_json_error( __( 'Invalid duplicates selected.', 'delete-duplicate-posts' ) );
183 return;
184 }
185 // Attempt to clean duplicates and handle possible failures
186 $result = self::cleandupes( true, $cleaned_posts );
187 if ( !$result ) {
188 $errorMessage = __( 'Error deleting duplicates.', 'delete-duplicate-posts' );
189 // Assuming $result is an array or object that could be serialized safely:
190 if ( is_array( $result ) || is_object( $result ) ) {
191 $errorData = array(
192 'additional_info' => json_encode( $result ),
193 );
194 wp_send_json_error( $errorMessage, $errorData );
195 } elseif ( is_string( $result ) ) {
196 // Sanitize the string to be safe for output
197 $errorData = array(
198 'additional_info' => esc_html( $result ),
199 );
200 wp_send_json_error( $errorMessage, $errorData );
201 } else {
202 // If result is not an array, object, or string, or if you want to keep the message generic
203 wp_send_json_error( $errorMessage );
204 }
205 return;
206 }
207 // Optionally return success data
208 if ( $return_data ) {
209 wp_send_json_success( array(
210 'message' => __( 'Duplicates deleted successfully.', 'delete-duplicate-posts' ),
211 ) );
212 }
213 }
214
215 /**
216 * Returns log lines via AJAX.
217 *
218 * @author Lars Koudal
219 * @author Unknown
220 * @since v0.0.1
221 * @version v1.0.0 Tuesday, January 12th, 2021.
222 * @version v1.0.1 Wednesday, November 1st, 2023.
223 * @access public static
224 * @param boolean $return Default: false
225 * @return void|array
226 */
227 public static function return_loglines_ajax( $return_data = false ) {
228 if ( !current_user_can( 'manage_options' ) ) {
229 wp_send_json_error( __( 'You do not have sufficient permissions to perform this action.', 'delete-duplicate-posts' ) );
230 return;
231 }
232 check_ajax_referer( 'cp_ddp_return_loglines' );
233 $currstep = ( filter_input( INPUT_POST, 'step', FILTER_SANITIZE_NUMBER_INT ) ?: 0 );
234 ++$currstep;
235 $json_response = array(
236 'step' => $currstep,
237 );
238 global $wpdb;
239 $loglines = $wpdb->get_results( "SELECT datime, note FROM {$wpdb->prefix}ddp_log ORDER BY datime DESC LIMIT 100;" );
240 if ( !empty( $loglines ) ) {
241 $json_response['results'] = $loglines;
242 } else {
243 $json_response['msg'] = __( 'Error: Log is empty.. do something :-)', 'delete-duplicate-posts' );
244 if ( $return_data ) {
245 return $json_response;
246 }
247 wp_send_json_error( $json_response );
248 return;
249 // Make sure to exit here to prevent sending multiple responses.
250 }
251 if ( $return_data ) {
252 return $json_response;
253 }
254 wp_send_json_success( $json_response );
255 }
256
257 /**
258 * return_duplicates_ajax.
259 *
260 * @author Lars Koudal
261 * @author Unknown
262 * @since v0.0.1
263 * @version v1.0.0 Tuesday, January 12th, 2021.
264 * @version v1.0.1 Wednesday, November 1st, 2023.
265 * @access public static
266 * @return void
267 */
268 public static function return_duplicates_ajax() {
269 check_ajax_referer( 'cp_ddp_return_duplicates', true );
270 if ( !current_user_can( 'manage_options' ) ) {
271 wp_send_json_error( __( 'You do not have sufficient permissions to perform this action.', 'delete-duplicate-posts' ) );
272 return;
273 }
274 // Get duplicates
275 $duplicates = self::return_duplicates( true );
276 // Initialize DataTables response array
277 $response = array(
278 'draw' => ( isset( $_POST['draw'] ) ? intval( $_POST['draw'] ) : 0 ),
279 'recordsTotal' => 0,
280 'recordsFiltered' => 0,
281 'data' => array(),
282 );
283 if ( !empty( $duplicates ) && isset( $duplicates['dupes'] ) ) {
284 $response['recordsTotal'] = $duplicates['dupescount'];
285 $response['recordsFiltered'] = $duplicates['dupescount'];
286 foreach ( $duplicates['dupes'] as $dupe ) {
287 $title = ( '' === $dupe['title'] ? '-empty-' : $dupe['title'] );
288 $orgTitle = ( '' === $dupe['orgtitle'] ? '-empty-' : $dupe['orgtitle'] );
289 $permalink = esc_url( get_permalink( $dupe['ID'] ) );
290 $orgPermalink = esc_url( get_permalink( $dupe['orgID'] ) );
291 $response['data'][] = array(
292 'ID' => esc_html( $dupe['ID'] ),
293 'orgID' => esc_html( $dupe['orgID'] ),
294 'duplicate' => sprintf(
295 '<a href="%s" target="_blank">%s</a> (ID #%s) <br><small>%s Type: %s Status: %s</small>',
296 esc_url( $permalink ),
297 esc_html( $title ),
298 esc_html( $dupe['ID'] ),
299 esc_html( $dupe['why'] ),
300 esc_html( $dupe['type'] ),
301 esc_html( $dupe['status'] )
302 ),
303 'original' => sprintf(
304 '<a href="%s" target="_blank">%s</a> (ID #%s)',
305 esc_url( $orgPermalink ),
306 esc_html( $orgTitle ),
307 esc_html( $dupe['orgID'] )
308 ),
309 );
310 }
311 }
312 wp_send_json( $response );
313 exit;
314 }
315
316 /**
317 * Converts a number to relevant unit size
318 *
319 * @author Lars Koudal
320 * @since v0.0.1
321 * @version v1.0.0 Thursday, June 24th, 2021.
322 * @param mixed $size
323 * @return void
324 */
325 public static function pretty_value( $size ) {
326 if ( $size <= 0 || !is_numeric( $size ) ) {
327 return '0 b';
328 }
329 $unit = array(
330 'b',
331 'kb',
332 'mb',
333 'gb',
334 'tb',
335 'pb'
336 );
337 $log = log( $size, 1024 );
338 $i = floor( $log );
339 $i = max( 0, min( (int) $i, count( $unit ) - 1 ) );
340 $num = $size / pow( 1024, $i );
341 $calc = round( $num, 2 ) . ' ' . $unit[$i];
342 return $calc;
343 }
344
345 /**
346 * Returns duplicates based on current settings - internal, not used via AJAX
347 *
348 * @author Lars Koudal
349 * @author Unknown
350 * @since v0.0.1
351 * @version v1.0.0 Tuesday, January 12th, 2021.
352 * @version v1.0.1 Wednesday, November 1st, 2023.
353 * @access public static
354 * @param boolean $return Default: false
355 * @return void
356 */
357 public static function return_duplicates( $return = false ) {
358 self::timerstart( 'return_duplicates' );
359 $options = self::get_options();
360 $comparemethod = 'titlecompare';
361 $return_duplicates_time = false;
362 global $ddp_fs;
363 $json_response = array();
364 // @ check compare method - maybe change lookup routine?
365 global $wpdb;
366 $table_name = $wpdb->prefix . 'posts';
367 $resultslimit = $options['ddp_resultslimit'];
368 $viewlimit = intval( $resultslimit );
369 if ( 0 === $viewlimit ) {
370 $viewlimit = 9999;
371 }
372 $ddp_pts_arr = $options['ddp_pts'];
373 if ( isset( $ddp_pts_arr ) && is_array( $ddp_pts_arr ) ) {
374 $ddp_pts = '"' . implode( '","', $ddp_pts_arr ) . '"';
375 } else {
376 $ddp_pts = '';
377 }
378 $ddp_pts = rtrim( $ddp_pts, ',' );
379 $post_stati = '"publish"';
380 $order = $options['ddp_keep'];
381 // verify default value has been set
382 if ( 'oldest' !== $order ) {
383 // two choices, if its not the first its the second...
384 $options['ddp_keep'] = 'latest';
385 $order = 'latest';
386 }
387 if ( 'oldest' === $order ) {
388 $minmax = 'MIN(id)';
389 }
390 if ( 'latest' === $order ) {
391 $minmax = 'MAX(id)';
392 }
393 $ddpstatuscnt = array();
394 $dupescount = 0;
395 if ( '' !== $ddp_pts ) {
396 $thisquery = false;
397 // **** Compare by title ****
398 if ( 'titlecompare' === $comparemethod ) {
399 $limit = ( isset( $_POST['length'] ) ? intval( $_POST['length'] ) : 10 );
400 // Default to 10 if not set
401 $offset = ( isset( $_POST['start'] ) ? intval( $_POST['start'] ) : 0 );
402 // Default to 0 if not set
403 if ( !wp_doing_ajax() && $return ) {
404 $limit = $options['ddp_resultslimit'];
405 // for returning results for cron job.
406 }
407 $wpdb->query( 'SET SQL_BIG_SELECTS=1' );
408 $resultsoutput = ' LIMIT ' . intval( $limit ) . ' OFFSET ' . intval( $offset );
409 if ( $options['ddp_debug'] ) {
410 self::log( 'DEBUG: SQL - Setting SET SQL_BIG_SELECTS=1' );
411 }
412 $thisquery = "SELECT * FROM (\n\t\t\t\t\t\t\t\t\t\t\t\t\tSELECT t1.ID, t1.post_title, t1.post_type, t1.post_status, save_this_post_id \n\t\t\t\t\t\t\t\t\t\t\t\t\tFROM {$table_name} AS t1 \n\t\t\t\t\t\t\t\t\t\t\t\t\tINNER JOIN ( \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tSELECT post_title, {$minmax} AS save_this_post_id \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tFROM {$table_name} \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tWHERE post_type IN ( {$ddp_pts} ) \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tAND post_status = 'publish' \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tGROUP BY post_title \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tHAVING COUNT(*) > 1 \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) AS t2 ON t1.post_title = t2.post_title \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tWHERE t1.post_status = 'publish'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tORDER BY t1.post_title, t1.post_date DESC\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) AS derived_table\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tWHERE ID != save_this_post_id\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{$resultsoutput}";
413 if ( $options['ddp_debug'] ) {
414 self::log( 'DEBUG: SQL ' . esc_attr( $thisquery ) );
415 }
416 $json_response['lookup_query'] = $thisquery;
417 $dupes = $wpdb->get_results( $thisquery, ARRAY_A );
418 // here we get total dupes - not cute, but the other approach not working.
419 $total_dupes_query = "SELECT COUNT(*) FROM (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tSELECT t1.ID, t1.post_title, t1.post_type, t1.post_status, save_this_post_id \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tFROM {$table_name} AS t1 \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tINNER JOIN ( \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tSELECT post_title, {$minmax} AS save_this_post_id \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tFROM {$table_name} \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tWHERE post_type IN ( {$ddp_pts} ) \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tAND post_type NOT IN ('nav_menu_item') \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tAND post_status IN ( {$post_stati} ) \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tGROUP BY post_title \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tHAVING COUNT(*)>1 \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) AS t2 \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tON t1.post_title = t2.post_title \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tAND post_status IN ( {$post_stati} )\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) AS derived_table\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tWHERE ID != save_this_post_id";
420 $total_dupes = $wpdb->get_var( $total_dupes_query );
421 if ( $options['ddp_debug'] ) {
422 self::log( 'DEBUG: SQL total_dupes_query ' . esc_attr( $total_dupes_query ) );
423 }
424 if ( '' !== $wpdb->last_error ) {
425 $last_error = htmlspecialchars( $wpdb->last_error, ENT_QUOTES );
426 $json_response['lookup_error'] = htmlspecialchars( $wpdb->last_error, ENT_QUOTES );
427 self::log( sprintf(
428 /* translators: 1: Database error message, 2: SQL query */
429 __( 'Look up error: %1$s %2$s', 'delete-duplicate-posts' ),
430 $last_error,
431 $total_dupes_query
432 ) );
433 }
434 if ( $dupes ) {
435 $json_response['dupescount'] = $total_dupes;
436 $stepcount = 0;
437 foreach ( $dupes as $dupe ) {
438 $mystatus = $dupe['post_status'];
439 if ( isset( $ddpstatuscnt[$mystatus] ) ) {
440 $ddpstatuscnt[$mystatus] = $ddpstatuscnt[$mystatus] + 1;
441 } else {
442 $ddpstatuscnt[$mystatus] = 1;
443 }
444 // Only save the dupes
445 if ( $dupe['ID'] !== $dupe['save_this_post_id'] ) {
446 $dupedetails = array(
447 'ID' => $dupe['ID'],
448 'permalink' => get_permalink( $dupe['ID'] ),
449 'title' => $dupe['post_title'],
450 'type' => $dupe['post_type'],
451 'orgID' => $dupe['save_this_post_id'],
452 'orgtitle' => $dupe['post_title'],
453 'orgpermalink' => get_permalink( $dupe['save_this_post_id'] ),
454 'status' => $dupe['post_status'],
455 'why' => sprintf( __( 'Post ID %1$s has the same title as Post ID %2$s', 'delete-duplicate-posts' ), $dupe['ID'], $dupe['save_this_post_id'] ),
456 );
457 $json_response['dupes'][] = $dupedetails;
458 }
459 ++$stepcount;
460 }
461 }
462 }
463 $statusdata = '';
464 if ( is_array( $ddpstatuscnt ) && count( $ddpstatuscnt ) > 1 ) {
465 $statusdata .= '(';
466 foreach ( $ddpstatuscnt as $key => $dsc ) {
467 $statusdata .= $key . ': ' . number_format_i18n( $dsc ) . ', ';
468 }
469 $statusdata = rtrim( $statusdata, ', ' );
470 $statusdata .= ')';
471 }
472 $return_duplicates_time = self::timerstop( 'return_duplicates' );
473 if ( $options['ddp_debug'] ) {
474 $max = 5;
475 if ( isset( $json_response['dupes'] ) ) {
476 $idlist = array();
477 $step = 0;
478 foreach ( $json_response['dupes'] as $dupe ) {
479 if ( $step <= $max ) {
480 $details = '';
481 if ( isset( $dupe['ID'] ) ) {
482 $details .= 'ID: ' . $dupe['ID'] . ' ';
483 }
484 if ( isset( $dupe['title'] ) ) {
485 $details .= ' title: "' . $dupe['title'] . '" ';
486 }
487 if ( isset( $dupe['permalink'] ) ) {
488 $details .= 'Permalink: ' . $dupe['permalink'] . ' ';
489 }
490 if ( isset( $dupe['status'] ) ) {
491 $details .= 'Status: ' . $dupe['status'] . ' ';
492 }
493 if ( isset( $dupe['type'] ) ) {
494 $details .= 'Type: ' . $dupe['type'] . ' ';
495 }
496 if ( isset( $dupe['orgID'] ) ) {
497 $details .= 'orgID: ' . $dupe['orgID'] . ' ';
498 }
499 if ( isset( $dupe['orgtitle'] ) ) {
500 $details .= 'orgtitle: ' . $dupe['orgtitle'] . ' ';
501 }
502 if ( isset( $dupe['orgpermalink'] ) ) {
503 $details .= ' orgpermalink: ' . $dupe['orgpermalink'] . ' ';
504 }
505 self::log( $details );
506 }
507 ++$step;
508 }
509 }
510 }
511 if ( isset( $json_response['dupes'] ) ) {
512 self::log( sprintf(
513 /* translators: 1: Number of duplicates, 2: Time in seconds, 3: Status data, 4: Memory usage */
514 __( 'Duplicates found: %1$d, Time: %2$s sec. %3$s Mem usage: %4$s', 'delete-duplicate-posts' ),
515 count( $json_response['dupes'] ),
516 $return_duplicates_time,
517 $statusdata,
518 self::pretty_value( memory_get_peak_usage( true ) )
519 ) );
520 }
521 } else {
522 $json_response['msg'] = __( 'Error: Choose post types to check.', 'delete-duplicate-posts' );
523 $return_duplicates_time = self::timerstop( 'return_duplicates' );
524 $json_response['time'] = $return_duplicates_time . ' sec';
525 if ( $return ) {
526 return $json_response;
527 }
528 wp_send_json_error( $json_response );
529 }
530 if ( !$return_duplicates_time ) {
531 $return_duplicates_time = self::timerstop( 'return_duplicates' );
532 }
533 if ( isset( $json_response['dupescount'] ) ) {
534 $json_response['msg'] = sprintf( __( 'Duplicates found: %1$s. Time: %2$s sec.', 'delete-duplicate-posts' ), number_format_i18n( $json_response['dupescount'] ), esc_html( $return_duplicates_time ) );
535 }
536 if ( $return ) {
537 return $json_response;
538 }
539 wp_send_json_success( $json_response );
540 }
541
542 /**
543 * create_redirect.
544 *
545 * @author Lars Koudal
546 * @since v0.0.1
547 * @version v1.0.0 Friday, July 2nd, 2021.
548 * @version v1.0.1 Tuesday, October 18th, 2022.
549 * @access public static
550 * @param mixed $inurl
551 * @param mixed $targeturl
552 * @param integer $code Default: 301
553 * @return void
554 */
555 public static function create_redirect( $inurl, $targeturl, $code = 301 ) {
556 global $wpdb, $ddp_fs;
557 }
558
559 /**
560 * Return default options
561 *
562 * @author Lars Koudal
563 * @since v0.0.1
564 * @version v1.0.0 Friday, July 2nd, 2021.
565 * @access public static
566 * @return mixed
567 */
568 public static function default_options() {
569 $defaults = array(
570 'ddp_running' => 'false',
571 'ddp_keep' => 'oldest',
572 'ddp_limit' => 50,
573 'ddp_pts' => array('post', 'page'),
574 'ddp_statusmail_recipient' => '',
575 'ddp_statusmail' => 0,
576 'ddp_resultslimit' => 0,
577 'ddp_enabled' => 0,
578 'ddp_pstati' => array('publish'),
579 'ddp_debug' => 0,
580 'ddp_redirects' => 0,
581 );
582 return $defaults;
583 }
584
585 /**
586 * get plugin's options
587 *
588 * @author Lars Koudal
589 * @since v0.0.1
590 * @version v1.0.0 Thursday, June 9th, 2022.
591 * @access public static
592 * @return mixed
593 */
594 public static function get_options() {
595 if ( null !== self::$options ) {
596 return self::$options;
597 }
598 $options = get_option( self::$options_name, array() );
599 if ( !is_array( $options ) ) {
600 $options = array();
601 }
602 $options = array_merge( self::default_options(), $options );
603 return $options;
604 }
605
606 /**
607 * add_freemius_extra_permission.
608 *
609 * @author Lars Koudal
610 * @since v0.0.1
611 * @version v1.0.0 Thursday, June 9th, 2022.
612 * @access public static
613 * @param mixed $permissions
614 * @return mixed
615 */
616 public static function add_freemius_extra_permission( $permissions ) {
617 $permissions['helpscout'] = array(
618 'icon-class' => 'dashicons dashicons-sos',
619 'label' => 'Help Scout',
620 'desc' => __( 'Rendering Help Scouts beacon for easy help and support', 'delete-duplicate-posts' ),
621 'priority' => 16,
622 );
623 $permissions['newsletter'] = array(
624 'icon-class' => 'dashicons dashicons-email-alt2',
625 'label' => 'Newsletter',
626 'desc' => __( 'Your email is added to cleverplugins.com newsletter. Unsubscribe any time.', 'delete-duplicate-posts' ),
627 'priority' => 18,
628 );
629 return $permissions;
630 }
631
632 /**
633 * Fetch plugin version from plugin PHP header
634 *
635 * @author Lars Koudal
636 * @since v0.0.1
637 * @version v1.0.0 Thursday, June 9th, 2022.
638 * @access public static
639 * @return mixed
640 */
641 public static function get_plugin_version() {
642 $plugin_data = get_file_data( __FILE__, array(
643 'version' => 'Version',
644 ), 'plugin' );
645 return $plugin_data['version'];
646 }
647
648 /**
649 * timerstart.
650 *
651 * @author Lars Koudal
652 * @since v0.0.1
653 * @version v1.0.0 Thursday, June 9th, 2022.
654 * @access public static
655 * @param mixed $watchname
656 * @return void
657 */
658 public static function timerstart( $watchname ) {
659 set_transient( 'ddp_' . $watchname, microtime( true ), 60 * 60 * 1 );
660 }
661
662 /**
663 * timerstop.
664 *
665 * @author Lars Koudal
666 * @since v0.0.1
667 * @version v1.0.0 Thursday, June 9th, 2022.
668 * @access public static
669 * @param mixed $watchname
670 * @param integer $digits Default: 3
671 * @return mixed
672 */
673 public static function timerstop( $watchname, $digits = 3 ) {
674 $return = round( microtime( true ) - get_transient( 'ddp_' . $watchname ), $digits );
675 delete_transient( 'ddp_' . $watchname );
676 return $return;
677 }
678
679 /**
680 * Clean duplicates - not AJAX version
681 *
682 * @author Lars Koudal
683 * @since v0.0.1
684 * @version v1.0.0 Thursday, June 9th, 2022.
685 * @access public static
686 * @param boolean $manualrun Default: false
687 * @param mixed $to_delete Default: array()
688 * @return void
689 */
690 public static function cleandupes( $manualrun = false, $to_delete = array() ) {
691 global $wpdb, $ddp_fs;
692 self::timerstart( 'ddp_totaltime' );
693 // start total timer
694 $options = self::get_options();
695 $options['ddp_running'] = true;
696 self::save_options( $options );
697 if ( !$manualrun ) {
698 self::log( __( 'Automatic CRON job running.', 'delete-duplicate-posts' ) );
699 } else {
700 self::log( __( 'Manually cleaning.', 'delete-duplicate-posts' ) );
701 }
702 // what to do with a manual run - no notices
703 if ( count( $to_delete ) > 0 ) {
704 $lookup_arr = array();
705 foreach ( $to_delete as $td ) {
706 $new_item = array();
707 $new_item['ID'] = $td['ID'];
708 $new_item['orgID'] = $td['orgID'];
709 $new_item['type'] = get_post_type( $td['ID'] );
710 $new_item['title'] = get_the_title( $td['ID'] );
711 $lookup_arr['dupes'][] = $new_item;
712 }
713 $dupes = $lookup_arr;
714 } else {
715 $dupes = self::return_duplicates( true );
716 }
717 $resultnote = '';
718 $dispcount = 0;
719 if ( isset( $dupes['dupes'] ) ) {
720 foreach ( $dupes['dupes'] as $dupe ) {
721 $postid = $dupe['ID'];
722 $title = substr( $dupe['title'], 0, 35 );
723 if ( $postid ) {
724 self::timerstart( 'deletepost_' . $postid );
725 /* @todo - implement a premium option to permanently delete or just use the WP setting.
726
727 Options: Use WP setting (default), Delete Permantently, Trash posts (if enabled in WP)
728
729 */
730 $deleteresult = wp_trash_post( $postid );
731 $timespent = self::timerstop( 'deletepost_' . $postid );
732 ++$dispcount;
733 $totaldeleted = get_option( 'ddp_deleted_duplicates' );
734 if ( false !== $totaldeleted ) {
735 ++$totaldeleted;
736 update_option( 'ddp_deleted_duplicates', $totaldeleted, false );
737 } else {
738 update_option( 'ddp_deleted_duplicates', 1, false );
739 }
740 if ( $options['ddp_debug'] ) {
741 // translators: Debug notice. 1: type of duplicate. 2: The title of the post. 3: The ID. 4: Time spent deleting.
742 self::log( sprintf(
743 __( 'DEBUG: Deleted %1$s %2$s (id: %3$s) in %4$s sec.', 'delete-duplicate-posts' ),
744 $dupe['type'],
745 $title,
746 $postid,
747 $timespent
748 ) );
749 }
750 }
751 }
752 }
753 $totaltimespent = self::timerstop( 'ddp_totaltime' );
754 self::log( sprintf( __( 'A total of %1$s duplicate posts were deleted in %2$s sec.', 'delete-duplicate-posts' ), $dispcount, $totaltimespent ) );
755 $json_response = array(
756 'totaltimespent' => $totaltimespent,
757 'deleted' => $dispcount,
758 );
759 // Mail logic...
760 if ( 0 < $dispcount && $options['ddp_statusmail'] ) {
761 $blogurl = esc_url( site_url() );
762 $recipient = sanitize_email( $options['ddp_statusmail_recipient'] );
763 // translators: 1: Number of deleted posts, 2: Blog URL.
764 $messagebody = sprintf( __( 'Hi Admin, I have deleted <strong>%1$d</strong> duplicated posts on your blog, %2$s.', 'delete-duplicate-posts' ), $dispcount, $blogurl );
765 $messagebody .= '<br><br>' . esc_html__( 'You are receiving this e-mail because you have turned on e-mail notifications by the plugin', 'delete-duplicate-posts' );
766 $messagebody .= ' <a href="https://cleverplugins.com/delete-duplicate-posts/" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Delete Duplicate Posts', 'delete-duplicate-posts' ) . '</a>';
767 $messagebody .= '<br><br>' . esc_html__( 'Made by', 'delete-duplicate-posts' ) . " <a href='https://cleverplugins.com' target='_blank' rel='noopener noreferrer'>" . esc_html__( 'cleverplugins.com', 'delete-duplicate-posts' ) . '</a>';
768 $mailstatus = false;
769 if ( is_email( $recipient ) ) {
770 $subject = __( 'Deleted Duplicate Posts Status', 'delete-duplicate-posts' );
771 $mailstatus = wp_mail( $recipient, $subject, wp_kses_post( $messagebody ) );
772 if ( $options['ddp_debug'] ) {
773 // translators: %s: Email recipient.
774 self::log( sprintf( __( 'DEBUG: Sending email to: %s', 'delete-duplicate-posts' ), $recipient ) );
775 }
776 if ( $mailstatus ) {
777 // translators: %s: Email recipient.
778 self::log( sprintf( __( 'Status email sent to %s.', 'delete-duplicate-posts' ), $recipient ) );
779 }
780 } else {
781 // translators: %s: Email address.
782 self::log( sprintf( __( 'Not a valid email %s.', 'delete-duplicate-posts' ), $recipient ) );
783 }
784 }
785 $options['ddp_running'] = false;
786 self::save_options( $options );
787 // Lets return a response
788 if ( 0 === $manualrun && !wp_doing_ajax() ) {
789 $json_response = array(
790 'msg' => sprintf( esc_html__( 'A total of %s duplicates were deleted.', 'delete-duplicate-posts' ), intval( $dispcount ) ),
791 );
792 }
793 wp_send_json_success( $json_response );
794 }
795
796 /**
797 * add_cron_intervals.
798 *
799 * @author Lars Koudal
800 * @since v0.0.1
801 * @version v1.0.0 Thursday, June 9th, 2022.
802 * @access public static
803 * @param mixed $schedules
804 * @return mixed
805 */
806 public static function add_cron_intervals( $schedules ) {
807 $schedules['5min'] = array(
808 'interval' => 300,
809 'display' => __( 'Every 5 minutes', 'delete-duplicate-posts' ),
810 );
811 $schedules['10min'] = array(
812 'interval' => 600,
813 'display' => __( 'Every 10 minutes', 'delete-duplicate-posts' ),
814 );
815 $schedules['15min'] = array(
816 'interval' => 900,
817 'display' => __( 'Every 15 minutes', 'delete-duplicate-posts' ),
818 );
819 $schedules['30min'] = array(
820 'interval' => 1800,
821 'display' => __( 'Every 30 minutes', 'delete-duplicate-posts' ),
822 );
823 return $schedules;
824 }
825
826 /**
827 * Log a notification to the database
828 *
829 * @author Lars Koudal
830 * @since v0.0.1
831 * @version v1.0.0 Monday, January 11th, 2021.
832 * @access public static
833 * @param mixed $text
834 * @return void
835 */
836 public static function log( $text ) {
837 global $wpdb;
838 $ddp_logtable = $wpdb->prefix . 'ddp_log';
839 // Insert log entry
840 $insert_result = $wpdb->insert( $ddp_logtable, array(
841 'datime' => current_time( 'mysql' ),
842 'note' => $text,
843 ), array('%s', '%s') );
844 if ( false === $insert_result ) {
845 // Handle error appropriately (e.g., log or throw exception)
846 return;
847 }
848 // Efficiently check if row count exceeds 1000 and delete old entries
849 $row_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$ddp_logtable};" );
850 if ( $row_count > 1000 ) {
851 $wpdb->query( "DELETE FROM {$ddp_logtable} WHERE id NOT IN (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tSELECT id FROM (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tSELECT id FROM {$ddp_logtable} ORDER BY datime DESC LIMIT 500\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) AS sub\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)" );
852 }
853 }
854
855 /**
856 * Enqueues scripts and styles
857 *
858 * @author Lars Koudal
859 * @since v0.0.1
860 * @version v1.0.0 Monday, January 11th, 2021.
861 * @access public static
862 * @return void
863 */
864 public static function admin_enqueue_scripts() {
865 $screen = get_current_screen();
866 if ( is_object( $screen ) && 'tools_page_delete-duplicate-posts' === $screen->id ) {
867 $pluginver = self::get_plugin_version();
868 wp_enqueue_script( 'jquery' );
869 wp_enqueue_style(
870 'delete-duplicate-posts',
871 plugins_url( '/css/delete-duplicate-posts.css', __FILE__ ),
872 array(),
873 $pluginver
874 );
875 wp_enqueue_script(
876 'dataTables',
877 // Unique handle for your script
878 plugin_dir_url( __FILE__ ) . 'js/DataTables/datatables.js',
879 // Path to your script file
880 array('jquery'),
881 // Dependencies, if any. This script depends on jQuery
882 $pluginver,
883 array(
884 'in_footer' => true,
885 )
886 );
887 wp_enqueue_style(
888 'dataTables',
889 plugins_url( '/js/DataTables/datatables.css', __FILE__ ),
890 array(),
891 $pluginver
892 );
893 wp_register_script(
894 'delete-duplicate-posts',
895 plugins_url( '/js/delete-duplicate-posts.js', __FILE__ ),
896 array('jquery', 'dataTables'),
897 $pluginver,
898 true
899 );
900 $js_vars = array(
901 'nonce' => wp_create_nonce( 'cp_ddp_return_duplicates' ),
902 'loglines_nonce' => wp_create_nonce( 'cp_ddp_return_loglines' ),
903 'deletedupes_nonce' => wp_create_nonce( 'cp_ddp_delete_loglines' ),
904 'text_areyousure' => __( 'Are you sure you want to delete duplicates? There is no undo feature.', 'delete-duplicate-posts' ),
905 'text_selectsomething' => __( 'You have to select which duplicates to delete. Tip: You can click the top or bottom checkbox to select all.', 'delete-duplicate-posts' ),
906 'fromUrlTitle' => __( 'From URL', 'delete-duplicate-posts' ),
907 'targetUrlTitle' => __( 'Target URL', 'delete-duplicate-posts' ),
908 'refreshingText' => __( 'Refreshing...', 'delete-duplicate-posts' ),
909 'refreshText' => __( 'Refresh', 'delete-duplicate-posts' ),
910 'errorDetailsText' => __( 'Error details: ', 'delete-duplicate-posts' ),
911 'redirectsErrorText' => __( 'Redirects DataTables error occurred. ', 'delete-duplicate-posts' ),
912 'processingMessage' => __( 'Looking for duplicates', 'delete-duplicate-posts' ),
913 'requestTimeText' => __( 'Request: ', 'delete-duplicate-posts' ),
914 'failedToLoadDataText' => __( 'Failed to load data. ', 'delete-duplicate-posts' ),
915 'duplicateTitle' => __( 'Duplicate', 'delete-duplicate-posts' ),
916 'originalTitle' => __( 'Original', 'delete-duplicate-posts' ),
917 'selectRowAlert' => __( 'Please select at least one row to delete.', 'delete-duplicate-posts' ),
918 'serverResponseText' => __( 'Response from the server: ', 'delete-duplicate-posts' ),
919 'errorOccurredText' => __( 'An error occurred: ', 'delete-duplicate-posts' ),
920 'deleteSelectedText' => __( 'Delete Selected', 'delete-duplicate-posts' ),
921 'selectVisibleText' => __( 'Select Visible', 'delete-duplicate-posts' ),
922 'selectNoneText' => __( 'Select None', 'delete-duplicate-posts' ),
923 'somethingWentWrongText' => __( 'Something went wrong.', 'delete-duplicate-posts' ),
924 );
925 wp_localize_script( 'delete-duplicate-posts', 'cp_ddp', $js_vars );
926 wp_enqueue_script( 'delete-duplicate-posts' );
927 }
928 }
929
930 /**
931 * Create plugin tables
932 *
933 * @author Lars Koudal
934 * @author Unknown
935 * @since v0.0.1
936 * @version v1.0.0 Monday, January 11th, 2021.
937 * @version v1.0.1 Sunday, July 17th, 2022.
938 * @version v1.0.2 Sunday, December 3rd, 2023.
939 * @access public static
940 * @return void
941 */
942 public static function create_table() {
943 global $wpdb, $ddp_fs;
944 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
945 $log_table_name = $wpdb->prefix . 'ddp_log';
946 $sql_log = "CREATE TABLE {$log_table_name} (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid bigint(20) NOT NULL AUTO_INCREMENT,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdatime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnote tinytext NOT NULL,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tPRIMARY KEY (id)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
947 dbDelta( $sql_log );
948 $redirects_table_name = $wpdb->prefix . 'ddp_redirects';
949 $sql_redirects = "CREATE TABLE {$redirects_table_name} (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tid bigint(20) NOT NULL AUTO_INCREMENT,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdatime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tinurl varchar(1024) NOT NULL,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttargeturl varchar(1024) NOT NULL,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thttpcode varchar(3) DEFAULT NULL,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tPRIMARY KEY (id)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
950 dbDelta( $sql_redirects );
951 // Additional plugin setup
952 $options = self::get_options();
953 self::save_options( $options );
954 wp_clear_scheduled_hook( 'ddp_cron' );
955 self::log( __( 'Plugin activated.', 'delete-duplicate-posts' ) );
956 }
957
958 /**
959 * Install routines - create database and default options
960 *
961 * @author Lars Koudal
962 * @since v0.0.1
963 * @version v1.0.0 Thursday, June 9th, 2022.
964 * @access public static
965 * @param mixed $network_wide
966 * @return void
967 */
968 public static function install( $network_wide ) {
969 global $wpdb;
970 require_once ABSPATH . '/wp-admin/includes/upgrade.php';
971 if ( is_multisite() && $network_wide ) {
972 // Get all blogs in the network and activate plugin on each one
973 $blog_ids = $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}" );
974 foreach ( $blog_ids as $blog_id ) {
975 switch_to_blog( $blog_id );
976 self::create_table();
977 restore_current_blog();
978 }
979 } else {
980 self::create_table();
981 }
982 }
983
984 /**
985 * Creating table when a new blog is created
986 * https://sudarmuthu.com/blog/how-to-properly-create-tables-in-wordpress-multisite-plugins/
987 *
988 * @author Lars Koudal
989 * @since v0.0.1
990 * @version v1.0.0 Thursday, June 9th, 2022.
991 * @version v1.0.1 Sunday, May 14th, 2023.
992 * @access public static
993 * @param mixed $new_site
994 * @return void
995 */
996 public static function on_create_blog( $new_site ) {
997 if ( is_plugin_active_for_network( 'delete-duplicate-posts/delete-duplicate-posts.php' ) ) {
998 switch_to_blog( $new_site->blog_id );
999 self::create_table();
1000 restore_current_blog();
1001 }
1002 }
1003
1004 /**
1005 * Deleting the table whenever a blog is deleted
1006 *
1007 * @author Lars Koudal
1008 * @since v0.0.1
1009 * @version v1.0.0 Thursday, June 9th, 2022.
1010 * @access public static
1011 * @param mixed $tables
1012 * @return mixed
1013 */
1014 public static function on_delete_blog( $tables ) {
1015 global $wpdb, $ddp_fs;
1016 $tables[] = $wpdb->prefix . 'ddp_log';
1017 return $tables;
1018 }
1019
1020 /**
1021 * Saves options
1022 *
1023 * @author Lars Koudal
1024 * @since v0.0.1
1025 * @version v1.0.0 Thursday, June 9th, 2022.
1026 * @access public static
1027 * @param mixed $newoptions
1028 * @return mixed
1029 */
1030 public static function save_options( $newoptions ) {
1031 self::$options = null;
1032 return update_option( 'delete_duplicate_posts_options_v4', $newoptions );
1033 }
1034
1035 /**
1036 * Adds link to menu under Tools
1037 *
1038 * @author Lars Koudal
1039 * @since v0.0.1
1040 * @version v1.0.0 Thursday, June 9th, 2022.
1041 * @access public static
1042 * @return void
1043 */
1044 public static function admin_menu_link() {
1045 // only for admins
1046 if ( !current_user_can( 'manage_options' ) ) {
1047 return;
1048 }
1049 add_management_page(
1050 'Delete Duplicate Posts',
1051 'Delete Duplicate Posts',
1052 'edit_posts',
1053 'delete-duplicate-posts',
1054 array(__NAMESPACE__ . '\\Delete_Duplicate_Posts', 'admin_options_page'),
1055 41
1056 );
1057 add_filter(
1058 'plugin_action_links_' . plugin_basename( __FILE__ ),
1059 array(__NAMESPACE__ . '\\Delete_Duplicate_Posts', 'filter_plugin_actions'),
1060 10,
1061 2
1062 );
1063 }
1064
1065 /**
1066 * filter_plugin_actions.
1067 *
1068 * @author Lars Koudal
1069 * @since v0.0.1
1070 * @version v1.0.0 Thursday, June 9th, 2022.
1071 * @access public static
1072 * @param mixed $links
1073 * @param mixed $file
1074 * @return mixed
1075 */
1076 public static function filter_plugin_actions( $links, $file ) {
1077 $settings_link = '<a href="tools.php?page=delete-duplicate-posts">' . __( 'Settings', 'delete-duplicate-posts' ) . '</a>';
1078 array_unshift( $links, $settings_link );
1079 // before other links
1080 return $links;
1081 }
1082
1083 /**
1084 * Adds help content to plugin page
1085 *
1086 * @author Lars Koudal
1087 * @since v0.0.1
1088 * @version v1.0.0 Thursday, June 9th, 2022.
1089 * @access public static
1090 * @return void
1091 */
1092 public static function set_custom_help_content() {
1093 $screen = get_current_screen();
1094 if ( 'tools_page_delete-duplicate-posts' === $screen->id ) {
1095 $screen->add_help_tab( array(
1096 'id' => 'ddp_help',
1097 'title' => __( 'Usage and FAQ', 'delete-duplicate-posts' ),
1098 'content' => '<h4>' . __( 'What does this plugin do?', 'delete-duplicate-posts' ) . '</h4><p>' . __( 'Helps you clean duplicate posts from your blog. The plugin checks for blogposts on your blog with the same title.', 'delete-duplicate-posts' ) . '</p><p>' . __( "It can run automatically via WordPress's own internal CRON-system, or you can run it automatically.", 'delete-duplicate-posts' ) . '</p><p>' . __( 'It also has a nice feature that can send you an e-mail when Delete Duplicate Posts finds and deletes something (if you have turned on the CRON feature).', 'delete-duplicate-posts' ) . '</p><h4>' . __( 'Help! Something was deleted that was not supposed to be deleted!', 'delete-duplicate-posts' ) . '</h4><p>' . __( 'I am sorry for that, I can only recommend you restore the database you took just before you ran this plugin.', 'delete-duplicate-posts' ) . '</p><p>' . __( 'If you run this plugin, manually or automatically, it is at your OWN risk!', 'delete-duplicate-posts' ) . '</p><p>' . __( 'We have done our best to avoid deleting something that should not be deleted, but if it happens, there is nothing we can do to help you.', 'delete-duplicate-posts' ) . "</p><p><a href='https://cleverplugins.com' target='_blank'>cleverplugins.com</a>.</p>",
1099 ) );
1100 }
1101 }
1102
1103 /**
1104 * admin_options_page.
1105 *
1106 * @author Lars Koudal
1107 * @since v0.0.1
1108 * @version v1.0.0 Thursday, June 9th, 2022.
1109 * @version v1.0.1 Thursday, June 9th, 2022.
1110 * @access public static
1111 * @return void
1112 */
1113 public static function admin_options_page() {
1114 global $ddp_fs, $wpdb;
1115 // DELETE NOW
1116 if ( isset( $_POST['deleteduplicateposts_delete'] ) && isset( $_POST['_wpnonce'] ) ) {
1117 if ( wp_verify_nonce( $_POST['_wpnonce'], 'ddp-clean-now' ) ) {
1118 self::cleandupes( 1 );
1119 // use the value 1 to indicate it is being run manually.
1120 }
1121 }
1122 // RUN NOW!!
1123 if ( isset( $_POST['ddp_runnow'] ) ) {
1124 if ( !wp_verify_nonce( $_POST['_wpnonce'], 'ddp-update-options' ) ) {
1125 die( esc_html( __( 'Whoops! Some error occured, try again, please!', 'delete-duplicate-posts' ) ) );
1126 }
1127 }
1128 // SAVING OPTIONS
1129 if ( isset( $_POST['delete_duplicate_posts_save'] ) ) {
1130 if ( !wp_verify_nonce( $_POST['_wpnonce'], 'ddp-update-options' ) ) {
1131 die( esc_html( __( 'Whoops! There was a problem with the data you posted. Please go back and try again.', 'delete-duplicate-posts' ) ) );
1132 }
1133 $posttypes = array();
1134 if ( isset( $_POST['ddp_pts'] ) ) {
1135 $option_array = $_POST['ddp_pts'];
1136 $option_count = count( $option_array );
1137 for ($i = 0; $i < $option_count; $i++) {
1138 $posttypes[] = sanitize_text_field( $option_array[$i] );
1139 }
1140 }
1141 if ( isset( $_POST['ddp_enabled'] ) ) {
1142 $options['ddp_enabled'] = ( 'on' === $_POST['ddp_enabled'] ? true : false );
1143 } else {
1144 $options['ddp_enabled'] = false;
1145 }
1146 $options['ddp_statusmail'] = ( isset( $_POST['ddp_statusmail'] ) && 'on' === $_POST['ddp_statusmail'] ? true : false );
1147 $options['ddp_debug'] = ( isset( $_POST['ddp_debug'] ) && 'on' === $_POST['ddp_debug'] ? true : false );
1148 if ( isset( $_POST['ddp_statusmail_recipient'] ) ) {
1149 $options['ddp_statusmail_recipient'] = sanitize_text_field( $_POST['ddp_statusmail_recipient'] );
1150 }
1151 if ( isset( $_POST['ddp_schedule'] ) ) {
1152 $options['ddp_schedule'] = sanitize_text_field( $_POST['ddp_schedule'] );
1153 }
1154 if ( isset( $_POST['ddp_keep'] ) ) {
1155 $options['ddp_keep'] = sanitize_text_field( $_POST['ddp_keep'] );
1156 }
1157 if ( isset( $_POST['ddp_method'] ) ) {
1158 $options['ddp_method'] = sanitize_text_field( $_POST['ddp_method'] );
1159 }
1160 if ( isset( $_POST['ddp_resultslimit'] ) ) {
1161 $options['ddp_resultslimit'] = sanitize_text_field( $_POST['ddp_resultslimit'] );
1162 }
1163 // 301 redirects
1164 if ( isset( $_POST['ddp_redirects'] ) ) {
1165 $options['ddp_redirects'] = ( 'on' === $_POST['ddp_redirects'] ? true : false );
1166 } else {
1167 $options['ddp_redirects'] = false;
1168 }
1169 $options['ddp_pts'] = $posttypes;
1170 // Previously sanitized
1171 if ( isset( $_POST['ddp_limit'] ) ) {
1172 $options['ddp_limit'] = sanitize_text_field( $_POST['ddp_limit'] );
1173 }
1174 self::save_options( $options );
1175 if ( isset( self::$options['ddp_enabled'] ) ) {
1176 wp_clear_scheduled_hook( 'ddp_cron' );
1177 $interval = self::$options['ddp_schedule'];
1178 if ( !$interval ) {
1179 $interval = 'hourly';
1180 }
1181 $nextscheduled = wp_next_scheduled( 'ddp_cron' );
1182 if ( !$nextscheduled ) {
1183 wp_schedule_event( time(), $interval, 'ddp_cron' );
1184 }
1185 }
1186 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( __( 'Settings saved.', 'delete-duplicate-posts' ) ) . '</p></div>';
1187 }
1188 // CLEARING THE LOG
1189 if ( isset( $_POST['ddp_clearlog'] ) ) {
1190 if ( !wp_verify_nonce( $_POST['_wpnonce'], 'ddp_clearlog_nonce' ) ) {
1191 die( esc_html( __( 'Whoops! Some error occured, try again, please!', 'delete-duplicate-posts' ) ) );
1192 }
1193 $table_name_log = $wpdb->prefix . 'ddp_log';
1194 $wpdb->query( "TRUNCATE {$table_name_log};" );
1195 //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1196 echo '<div class="updated"><p>' . esc_html( __( 'The log was cleared.', 'delete-duplicate-posts' ) ) . '</p></div>';
1197 }
1198 // REACTIVATE THE DATABASE
1199 if ( isset( $_POST['ddp_reactivate'] ) ) {
1200 if ( !wp_verify_nonce( $_POST['_wpnonce'], 'ddp_reactivate_nonce' ) ) {
1201 die( esc_html( __( 'Whoops! Some error occured, try again, please!', 'delete-duplicate-posts' ) ) );
1202 }
1203 self::install( false );
1204 self::log( 'Reinstalled databases' );
1205 }
1206 $options = self::get_options();
1207 $css_classes = ' free';
1208 $display_ads = true;
1209 ?>
1210
1211 <div class="wrap fs-section <?php
1212 echo esc_attr( $css_classes );
1213 ?>">
1214 <h1>Delete Duplicate Posts <span>v. <?php
1215 echo esc_html( self::get_plugin_version() );
1216 ?></span></h1>
1217 <?php
1218 $totaldeleted = get_option( 'ddp_deleted_duplicates' );
1219 // check the code for get param 'welcome-message' er sat til true
1220 ?>
1221
1222 <h2 class="nav-tab-wrapper">
1223 <a href="#duplicates-tab" class="nav-tab fs-tab nav-tab-active home"><?php
1224 esc_html_e( 'Duplicates', 'delete-duplicate-posts' );
1225 ?></a>
1226 <a href="#log-tab" class="nav-tab"><?php
1227 esc_html_e( 'Log', 'delete-duplicate-posts' );
1228 ?></a>
1229 <a href="#settings-tab" class="nav-tab"><?php
1230 esc_html_e( 'Settings', 'delete-duplicate-posts' );
1231 ?></a>
1232 <a href="#redirects-tab" class="nav-tab<?php
1233 echo ( $ddp_fs->is__premium_only() && $ddp_fs->can_use_premium_code() ? '' : ' pro' );
1234 ?>"><?php
1235 esc_html_e( 'Redirects', 'delete-duplicate-posts' );
1236 ?></a>
1237 </h2>
1238
1239 <div class="ddp_content_wrapper">
1240 <div class="ddp_content_cell">
1241 <div id="delete-duplicate-posts-tabs">
1242 <div id="duplicates-tab" class="tab-content">
1243 <div id="ddp-dashboard">
1244 <?php
1245 if ( $options['ddp_enabled'] ) {
1246 $interval = $options['ddp_schedule'];
1247 if ( !$interval ) {
1248 $interval = 'hourly';
1249 }
1250 $nextscheduled = wp_next_scheduled( 'ddp_cron' );
1251 if ( !$nextscheduled ) {
1252 // plugin active, but the cron needs to be activated also..
1253 $options['last_interval'] = $interval;
1254 self::save_options( $options );
1255 wp_schedule_event( time(), $interval, 'ddp_cron' );
1256 //}
1257 }
1258 } else {
1259 wp_unschedule_hook( 'ddp_cron' );
1260 }
1261 $totaldeleted = get_option( 'ddp_deleted_duplicates' );
1262 ?>
1263 <div class="statusdiv">
1264 <div class="statusmessage"></div>
1265 <div class="errormessage"></div>
1266 <div class="dupelist">
1267 <div id="requestTime"></div>
1268 <table id="ddp_dupetable" class="wp-list-table widefat fixed striped table-view-list"></table>
1269 </div>
1270 </div>
1271 <?php
1272 if ( false !== $totaldeleted && 0 < $totaldeleted && $display_ads ) {
1273 $totaldeleted = number_format_i18n( $totaldeleted );
1274 ?>
1275 <div id="cp-ddp-reviewlink" data-dismissible="ddp-leavereview-180" class="updated notice notice-success">
1276 <h3>
1277 <?php
1278 /* translators: %s: Total number of deleted duplicates */
1279 printf( esc_html__( '%s duplicates deleted!', 'delete-duplicate-posts' ), esc_html( $totaldeleted ) );
1280 ?>
1281 </h3>
1282 <p>
1283 <?php
1284 /* translators: %s: Total number of deleted duplicates */
1285 printf( esc_html__( "Hey, I noticed this plugin has deleted %s duplicate posts for you - that's awesome! Could you please do me a BIG favor and give it a 5-star rating on WordPress? Just to help us spread the word and boost our motivation.", 'delete-duplicate-posts' ), esc_html( $totaldeleted ) );
1286 ?>
1287 </p>
1288 <p>
1289 <a href="https://wordpress.org/support/plugin/delete-duplicate-posts/reviews/?filter=5#new-post" class="button-secondary button button-small" target="_blank" rel="noopener"><?php
1290 esc_html_e( 'Ok, you deserve it', 'delete-duplicate-posts' );
1291 ?></a>
1292 </p>
1293 </div>
1294 <?php
1295 }
1296 ?>
1297 <?php
1298 $my_current_user = wp_get_current_user();
1299 ?>
1300
1301 <div class="ddpnewsletter">
1302 <p>Sign up to our newsletter to receive the latest tips and updates directly to your inbox. Ensure your WordPress site remains efficient and duplicate-free!</p>
1303 <form class="ml-block-form" action="https://assets.mailerlite.com/jsonp/16490/forms/106309157552916248/subscribe" data-code="" method="post" target="_blank">
1304 <input type="text" class="form-control" data-inputmask="" name="fields[name]" placeholder="Name" autocomplete="given-name" value="<?php
1305 echo esc_html( $my_current_user->display_name );
1306 ?>" required="required">
1307 <input type="email" class="form-control" data-inputmask="" name="fields[email]" placeholder="Email" autocomplete="email" value="<?php
1308 echo esc_html( $my_current_user->user_email );
1309 ?>" required="required">
1310 <input type="hidden" name="fields[signupsource]" value="PluginInstall">
1311 <input type="hidden" name="ml-submit" value="1">
1312 <input type="hidden" name="anticsrf" value="true">
1313 <button type="submit" class="button button-secondary">Subscribe</button>
1314
1315 </form>
1316 <p class="ppolicy">You can unsubscribe anytime. For more details, review our <a href="https://cleverplugins.com/privacy-policy/" target="_blank" class="privacy-policy" rel="noopener">Privacy Policy</a>.</p>
1317 </div>
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327 <?php
1328 $display_promotion = true;
1329 if ( $display_promotion ) {
1330 ?>
1331 <div class="innerpromotion ddppro">
1332 <h3><span class="dashicons dashicons-star-filled"></span> Upgrade to Delete Duplicate Posts Pro <span class="dashicons dashicons-star-filled"></span></h3>
1333 <ul class="linklist">
1334 <li><strong>301 Redirects for Deleted Posts:</strong> Seamlessly redirect deleted posts to original pages.</li>
1335 <li><strong>Search by Duplicate Post Meta:</strong> Advanced search options for identifying duplicates based on post metadata.</li>
1336 <li><strong>Automatic Deletion:</strong> Automatic management of duplicate posts.</li>
1337 <li><strong>Email Notifications:</strong> Alerts when duplicates are found and removed.</li>
1338 <li><strong>Search Any Post Status:</strong> Deeper search capabilities, including unpublished posts.</li>
1339 <li><strong>Filter and Delete Unpublished Duplicates:</strong> Proactively manage and prevent unseen duplicates.</li>
1340 <li><strong>WooCommerce Compatibility:</strong> Look for and delete duplicate products with same SKUs. (Custom post meta)</li>
1341 <li><strong>Support: </strong> We appreciate your support!</li>
1342 </ul>
1343 <p><strong></strong></p>
1344
1345 <?php
1346 $target_url = 'https://checkout.freemius.com/mode/dialog/plugin/925/plan/9473/licenses/1/?billing_cycle=annually';
1347 $lifetime_url = 'https://checkout.freemius.com/mode/dialog/plugin/925/plan/9473/licenses/1/?billing_cycle=lifetime';
1348 if ( $my_current_user->user_email ) {
1349 $target_url = add_query_arg( 'user_email', $my_current_user->user_email, $target_url );
1350 $lifetime_url = add_query_arg( 'user_email', $my_current_user->user_email, $lifetime_url );
1351 }
1352 ?>
1353 <a href="<?php
1354 echo esc_url( $target_url );
1355 ?>" class="ddpprobutton button button-primary button-hero" target="_blank">Only $29.99 /year - <?php
1356 esc_html_e( 'Click here', 'delete-duplicate-posts' );
1357 ?></a>
1358 <p>
1359 <center><em>OR get <a href="<?php
1360 echo esc_url( $lifetime_url );
1361 ?>" target="_blank">a lifetime license for only $59.99</a></em> - You can transfer your license to other websites.</center>
1362 </p>
1363 <div class="moneybackguarantee">
1364 <p><strong>Money Back Guarantee!</strong></p>
1365 <p>You are fully protected by our 100% Money Back Guarantee. If during the next 30 days you experience an issue that makes the plugin unusable and we are unable to resolve it, we'll happily consider offering a full refund of your money.</p>
1366 </div>
1367 </div><!-- .sidebarrow -->
1368
1369 <?php
1370 }
1371 ?>
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382 </div><!-- #dashboard -->
1383 </div>
1384
1385 <div id="log-tab" class="tab-content" style="display: none;">
1386 <div id="log">
1387 <h3><?php
1388 esc_html_e( 'The Log', 'delete-duplicate-posts' );
1389 ?></h3>
1390 <div class="spinner is-active"></div>
1391 <ul class="large-text" name="ddp_log" id="ddp_log"></ul>
1392 </div>
1393 <p>
1394 <form method="post" id="ddp_clearlog">
1395 <?php
1396 wp_nonce_field( 'ddp_clearlog_nonce' );
1397 ?>
1398 <input class="button-secondary" type="submit" name="ddp_clearlog" value="<?php
1399 esc_html_e( 'Reset log', 'delete-duplicate-posts' );
1400 ?>" />
1401 </form>
1402 </p>
1403 </div>
1404
1405 <div id="settings-tab" class="tab-content" style="display: none;">
1406 <div id="ddp-configuration">
1407 <h3><?php
1408 esc_html_e( 'Settings', 'delete-duplicate-posts' );
1409 ?></h3>
1410 <p>
1411 <?php
1412 $nextscheduled = wp_next_scheduled( 'ddp_cron' );
1413 if ( $nextscheduled ) {
1414 ?>
1415 <div class="notice notice-info is-dismissible">
1416 <h3><span class="dashicons dashicons-saved"></span> <?php
1417 esc_html_e( 'Automatically Deleting Duplicates', 'delete-duplicate-posts' );
1418 ?></h3>
1419 <?php
1420 echo '<p class="cronstatus center">' . esc_html__( 'You have enabled automatic deletion, so I am running on automatic. I will take care of everything...', 'delete-duplicate-posts' ) . '</p>';
1421 echo '<p class="center">';
1422 printf(
1423 // translators: Showing when the next check happens and what the current time is
1424 esc_html( __( 'Next automated check %1$s. Current time %2$s', 'delete-duplicate-posts' ) ),
1425 '<strong>' . esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $nextscheduled ) ) . '</strong>',
1426 '<strong>' . esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), time() ) ) . '</strong>'
1427 );
1428 echo '</p>';
1429 ?>
1430 </div>
1431 <?php
1432 }
1433 ?>
1434 </p>
1435 <form method="post" id="delete_duplicate_posts_options">
1436 <?php
1437 wp_nonce_field( 'ddp-update-options' );
1438 ?>
1439 <table width="100%" cellspacing="2" cellpadding="5" class="form-table">
1440 <tr valign="top">
1441 <th><label for="ddp_pts"><?php
1442 esc_html_e( 'Which post types?:', 'delete-duplicate-posts' );
1443 ?></label>
1444 </th>
1445 <td>
1446 <?php
1447 $builtin = array('post', 'page', 'attachment');
1448 $args = array(
1449 'public' => true,
1450 '_builtin' => false,
1451 );
1452 $output = 'names';
1453 $operator = 'and';
1454 $post_types = get_post_types( $args, $output, $operator );
1455 $post_types = array_merge( $builtin, $post_types );
1456 $checked_post_types = $options['ddp_pts'];
1457 if ( $post_types ) {
1458 ?>
1459 <ul class="radio">
1460 <?php
1461 $step = 0;
1462 if ( !is_array( $checked_post_types ) ) {
1463 $checked_post_types = array();
1464 }
1465 foreach ( $post_types as $pt ) {
1466 $checked = array_search( $pt, $checked_post_types, true );
1467 ?>
1468 <li><input type="checkbox" name="ddp_pts[]" id="ddp_pt-<?php
1469 echo esc_attr( $step );
1470 ?>" value="<?php
1471 echo esc_html( $pt );
1472 ?>"
1473 <?php
1474 if ( false !== $checked ) {
1475 echo ' checked';
1476 }
1477 ?>
1478 />
1479 <label for="ddp_pt-<?php
1480 echo esc_attr( $step );
1481 ?>"><?php
1482 echo esc_html( $pt );
1483 ?></label>
1484 <?php
1485 // Count for each post type
1486 $postinfo = wp_count_posts( $pt );
1487 $othercount = 0;
1488 foreach ( $postinfo as $pi ) {
1489 $othercount = $othercount + intval( $pi );
1490 }
1491 // translators: Total number of deleted duplicates
1492 echo '<small>' . sprintf( esc_html__( '(%s total found)', 'delete-duplicate-posts' ), esc_html( number_format_i18n( $othercount ) ) ) . '</small>';
1493 ?>
1494 </li>
1495 <?php
1496 ++$step;
1497 }
1498 ?>
1499 </ul>
1500 <?php
1501 }
1502 ?>
1503 <p class="description">
1504 <?php
1505 esc_html_e( 'Choose which post types to scan for duplicates.', 'delete-duplicate-posts' );
1506 ?>
1507 </p>
1508 </td>
1509 </tr>
1510
1511 <tr>
1512 <th><label for="ddp_pstati"><?php
1513 esc_html_e( 'Post status', 'delete-duplicate-posts' );
1514 ?></label>
1515 </th>
1516 <td>
1517 <?php
1518 $stati = array(
1519 'publish' => (object) array(
1520 'label' => 'Published',
1521 'show_in_admin_status_list' => true,
1522 ),
1523 );
1524 $checked_post_stati = $options['ddp_pstati'];
1525 if ( $stati ) {
1526 ?>
1527 <ul class="checkbox">
1528 <?php
1529 $staticount = count( $stati );
1530 foreach ( $stati as $key => $st ) {
1531 if ( $st->show_in_admin_status_list ) {
1532 $checked = array_search( $key, $checked_post_stati, true );
1533 ?>
1534 <li>
1535 <input type="checkbox" name="ddp_pstati[]" id="ddp_pstatus-<?php
1536 echo esc_attr( $key );
1537 ?>" value="<?php
1538 echo esc_attr( $key );
1539 ?>"
1540 <?php
1541 if ( false !== $checked ) {
1542 echo ' checked';
1543 }
1544 if ( 1 === $staticount ) {
1545 echo ' disabled';
1546 }
1547 ?>
1548 /><label for="ddp_pstatus-<?php
1549 echo esc_attr( $key );
1550 ?>">
1551 <?php
1552 echo esc_html( $key . ' (' . $st->label . ')' );
1553 if ( 'trash' === $key ) {
1554 echo ' <small>' . esc_html__( 'Warning: Enabling this can give false results. Only enable if you know what you are doing.', 'delete-duplicate-posts' ) . '</small>';
1555 }
1556 ?>
1557 </label>
1558 </li>
1559 <?php
1560 ++$step;
1561 }
1562 }
1563 ?>
1564 </ul>
1565 <?php
1566 }
1567 ?>
1568 </td>
1569
1570
1571 </tr>
1572 <?php
1573 $comparemethod = 'titlecompare';
1574 global $ddp_fs;
1575 ?>
1576 <tr valign="top">
1577 <th><?php
1578 esc_html_e( 'Comparison Method', 'delete-duplicate-posts' );
1579 ?></th>
1580 <td>
1581 <ul class="ddpcomparemethod">
1582
1583 <li>
1584 <label>
1585 <input type="radio" name="ddp_method" value="titlecompare" <?php
1586 checked( 'titlecompare', $comparemethod );
1587 ?> />
1588 <?php
1589 esc_html_e( 'Compare by title (default)', 'delete-duplicate-posts' );
1590 ?>
1591 <span class="optiondesc"><?php
1592 esc_html_e( 'Looks at the title of the post itself.', 'delete-duplicate-posts' );
1593 ?></span>
1594 </label>
1595
1596 </li>
1597
1598 <?php
1599 global $ddp_fs;
1600 ?>
1601 </ul>
1602 </td>
1603 </tr>
1604 <tr>
1605 <th><label for="ddp_keep"><?php
1606 esc_html_e( 'Delete which posts?:', 'delete-duplicate-posts' );
1607 ?></label></th>
1608 <td>
1609
1610 <select name="ddp_keep" id="ddp_keep">
1611 <option value="oldest"
1612 <?php
1613 if ( 'oldest' === $options['ddp_keep'] ) {
1614 echo 'selected="selected"';
1615 }
1616 ?>
1617 ><?php
1618 esc_html_e( 'Keep oldest', 'delete-duplicate-posts' );
1619 ?></option>
1620 <option value="latest"
1621 <?php
1622 if ( 'latest' === $options['ddp_keep'] ) {
1623 echo 'selected="selected"';
1624 }
1625 ?>
1626 ><?php
1627 esc_html_e( 'Keep latest', 'delete-duplicate-posts' );
1628 ?></option>
1629 </select>
1630 <p class="description">
1631 <?php
1632 esc_html_e( 'Keep the oldest or the latest version of duplicates? Default is keeping the oldest, and deleting any subsequent duplicate posts', 'delete-duplicate-posts' );
1633 ?>
1634 </p>
1635 </td>
1636 </tr>
1637
1638
1639
1640
1641
1642
1643
1644
1645 <?php
1646 ?>
1647 <tr>
1648 <td colspan="2">
1649 <hr>
1650 <h3><?php
1651 esc_html_e( 'Delete Duplicates Automatically', 'delete-duplicate-posts' );
1652 ?></h3>
1653 </td>
1654 </tr>
1655
1656 <tr valign="top">
1657 <th><?php
1658 esc_html_e( 'Enable automatic deletion?:', 'delete-duplicate-posts' );
1659 ?>
1660 </th>
1661 <td><label for="ddp_enabled">
1662 <input type="checkbox" id="ddp_enabled" name="ddp_enabled"
1663 <?php
1664 if ( true === $options['ddp_enabled'] ) {
1665 echo 'checked="checked"';
1666 }
1667 ?>
1668 >
1669 <p class="description">
1670 <?php
1671 esc_html_e( 'Clean duplicates automatically.', 'delete-duplicate-posts' );
1672 ?></p>
1673 </label>
1674 </td>
1675 </tr>
1676
1677 <tr>
1678 <th><label for="ddp_resultslimit"><?php
1679 esc_html_e( 'How many:', 'delete-duplicate-posts' );
1680 ?></label>
1681 </th>
1682 <td>
1683
1684 <?php
1685 $dupe_options = array(
1686 0 => __( 'No limit', 'delete-duplicate-posts' ),
1687 10000 => number_format_i18n( '10000' ),
1688 5000 => number_format_i18n( '5000' ),
1689 2500 => number_format_i18n( '2500' ),
1690 1000 => number_format_i18n( '1000' ),
1691 500 => '500',
1692 250 => '250',
1693 100 => '100',
1694 50 => '50',
1695 10 => '10',
1696 );
1697 ?>
1698 <select name="ddp_resultslimit" id="ddp_resultslimit">
1699 <?php
1700 foreach ( $dupe_options as $key => $label ) {
1701 ?>
1702 <option value="<?php
1703 echo esc_attr( $key );
1704 ?>" <?php
1705 selected( $options['ddp_resultslimit'], $key );
1706 ?>>
1707 <?php
1708 echo esc_attr( $label );
1709 ?></option>
1710 <?php
1711 }
1712 ?>
1713 </select>
1714
1715 <p class="description">
1716 <?php
1717 esc_html_e( 'If you have many duplicates, the plugin might time out before finding them all. Try limiting the amount of duplicates here. Default: Unlimited.', 'delete-duplicate-posts' );
1718 ?><br>
1719 <strong><?php
1720 esc_html_e( 'This only applies to automatic (CRON) jobs.', 'delete-duplicate-posts' );
1721 ?></strong>
1722 </p>
1723 </td>
1724 </tr>
1725
1726 <tr>
1727 <th><label for="ddp_schedule"><?php
1728 esc_html_e( 'How often?:', 'delete-duplicate-posts' );
1729 ?></label>
1730 </th>
1731 <td>
1732
1733 <select name="ddp_schedule" id="ddp_schedule">
1734 <?php
1735 $schedules = wp_get_schedules();
1736 if ( $schedules ) {
1737 foreach ( $schedules as $key => $sch ) {
1738 ?>
1739 <option value="<?php
1740 echo esc_attr( $key );
1741 ?>"
1742 <?php
1743 if ( isset( $options['ddp_schedule'] ) && esc_attr( $key ) === $options['ddp_schedule'] ) {
1744 echo esc_html( 'selected="selected"' );
1745 }
1746 ?>
1747 ><?php
1748 echo esc_html( $sch['display'] );
1749 ?></option>
1750 <?php
1751 }
1752 }
1753 ?>
1754 </select>
1755 <p class="description">
1756 <?php
1757 esc_html_e( 'How often should the cron job run?', 'delete-duplicate-posts' );
1758 ?></p>
1759 </td>
1760 </tr>
1761 <tr>
1762 <td colspan="2">
1763 <hr>
1764 </td>
1765 </tr>
1766
1767 <tr>
1768 <th><?php
1769 esc_html_e( 'Send status mail?:', 'delete-duplicate-posts' );
1770 ?></th>
1771 <td>
1772 <label for="ddp_statusmail">
1773 <input type="checkbox" id="ddp_statusmail" name="ddp_statusmail"
1774 <?php
1775 if ( isset( $options['ddp_statusmail'] ) && true === $options['ddp_statusmail'] ) {
1776 ?>
1777 checked="checked"
1778 <?php
1779 }
1780 ?>
1781 >
1782 <p class="description">
1783 <?php
1784 esc_html_e( 'Sends a status email if duplicates have been found.', 'delete-duplicate-posts' );
1785 ?>
1786 </p>
1787 </label>
1788 </td>
1789 </tr>
1790
1791 <tr>
1792 <th><?php
1793 esc_html_e( 'Email recipient:', 'delete-duplicate-posts' );
1794 ?></th>
1795 <td>
1796 <label for="ddp_statusmail_recipient">
1797
1798 <input type="text" class="regular-text" id="ddp_statusmail_recipient" name="ddp_statusmail_recipient" value="<?php
1799 echo esc_html( $options['ddp_statusmail_recipient'] );
1800 ?>">
1801 <p class="description">
1802 <?php
1803 esc_html_e( 'Who should get the notification email.', 'delete-duplicate-posts' );
1804 ?></p>
1805 </label>
1806 </td>
1807 </tr>
1808
1809
1810
1811 <tr>
1812 <td colspan="2">
1813 <hr>
1814 </td>
1815 </tr>
1816
1817 <tr>
1818 <th><?php
1819 esc_html_e( 'Enable debug logging?:', 'delete-duplicate-posts' );
1820 ?></th>
1821 <td>
1822 <label for="ddp_debug">
1823 <input type="checkbox" id="ddp_debug" name="ddp_debug"
1824 <?php
1825 if ( isset( $options['ddp_debug'] ) && true === $options['ddp_debug'] ) {
1826 echo 'checked="checked"';
1827 }
1828 ?>
1829 >
1830 <p class="description">
1831 <?php
1832 esc_html_e( 'Should only be enabled if debugging a problem.', 'delete-duplicate-posts' );
1833 ?>
1834 </p>
1835 </label>
1836 </td>
1837 </tr>
1838 <th colspan=2><input type="submit" class="button-primary" name="delete_duplicate_posts_save" value="<?php
1839 esc_html_e( 'Save Settings', 'delete-duplicate-posts' );
1840 ?>" /></th>
1841 </tr>
1842 </table>
1843 </form>
1844 </div><!-- #configuration -->
1845 </div>
1846
1847
1848 <div id="redirects-tab" class="tab-content<?php
1849 echo ( $ddp_fs->is__premium_only() && $ddp_fs->can_use_premium_code() ? '' : ' pro' );
1850 ?>" style="display: none;">
1851 <?php
1852 if ( $ddp_fs->is__premium_only() && $ddp_fs->can_use_premium_code() ) {
1853 ?>
1854 <h3><?php
1855 esc_html_e( 'Redirects', 'delete-duplicate-posts' );
1856 ?></h3>
1857 <p><?php
1858 esc_html_e( 'This table shows all redirects created by the plugin.', 'delete-duplicate-posts' );
1859 ?></p>
1860 <table id="ddp_redirtable" class="wp-list-table widefat fixed striped table-view-list">
1861 <thead>
1862 <tr>
1863 <th><?php
1864 esc_html_e( 'ID', 'delete-duplicate-posts' );
1865 ?></th>
1866 <th><?php
1867 esc_html_e( 'From URL', 'delete-duplicate-posts' );
1868 ?></th>
1869 <th><?php
1870 esc_html_e( 'Target URL', 'delete-duplicate-posts' );
1871 ?></th>
1872 </tr>
1873 </thead>
1874 <tbody>
1875 <!-- DataTables will populate this -->
1876 </tbody>
1877 </table>
1878 <?php
1879 } else {
1880 ?>
1881 <h3><?php
1882 esc_html_e( 'Redirects', 'delete-duplicate-posts' );
1883 ?></h3>
1884 <p><?php
1885 esc_html_e( 'Redirects are a premium feature. Please upgrade to access this functionality.', 'delete-duplicate-posts' );
1886 ?></p>
1887 <?php
1888 }
1889 ?>
1890 </div>
1891
1892 </div>
1893 </div>
1894
1895 <?php
1896 include_once 'sidebar.php';
1897 if ( function_exists( 'ddp_fs' ) ) {
1898 global $ddp_fs;
1899 }
1900 ?>
1901 </div>
1902
1903 </div>
1904
1905
1906
1907 <script>
1908 jQuery(document).ready(function($) {
1909 const navTabWrapper = $('.nav-tab-wrapper');
1910 const currentTabs = $('.nav-tab-wrapper a');
1911
1912 currentTabs.each(function() {
1913 $(this).on('click', function(e) {
1914 const href = $(this).attr('href');
1915 console.log('Clicked tab href:', href);
1916
1917 try {
1918 if (!href.startsWith('#')) {
1919 e.preventDefault(); // Prevent default tab behavior for full URLs
1920 window.location.href = href; // Load the page to the URL in the same window
1921 } else {
1922 e.preventDefault(); // Prevent default anchor behavior
1923 console.log('Switching to tab:', href);
1924
1925 // Switch as a regular tab for href starting with '#'
1926 currentTabs.removeClass('nav-tab-active');
1927 $(this).addClass('nav-tab-active');
1928
1929 // Hide all tab content
1930 $('.tab-content').hide();
1931
1932 // Show the content for the clicked tab
1933 $(href).show();
1934 }
1935 } catch (error) {
1936 console.error('Error occurred:', error);
1937 }
1938 });
1939 });
1940
1941 // Initially hide all tab content except the active one
1942 $('.tab-content').hide();
1943 $('.nav-tab-active').each(function() {
1944 const activeHref = $(this).attr('href');
1945 $(activeHref).show();
1946 });
1947 });
1948 </script>
1949
1950
1951
1952
1953
1954
1955
1956
1957 <?php
1958 }
1959
1960 }
1961
1962 //End Class
1963 }
1964 if ( class_exists( __NAMESPACE__ . '\\Delete_Duplicate_Posts' ) ) {
1965 $delete_duplicate_posts_var = new Delete_Duplicate_Posts();
1966 }