File: /var/www/html/wp-content/plugins/delete-duplicate-posts/delete-duplicate-posts.php
<?php
/*
Plugin Name: Delete Duplicate Posts
Plugin Script: delete-duplicate-posts.php
Plugin URI: https://cleverplugins.com
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.
Version: 5.0.3
Author: cleverplugins.com
Author URI: https://cleverplugins.com
Min WP Version: 4.7
Max WP Version: 6.9.1
Text Domain: delete-duplicate-posts
Domain Path: /languages
*/
namespace DeleteDuplicatePosts;
if ( !defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/vendor/autoload.php';
if ( function_exists( '\\DeleteDuplicatePosts\\ddp_fs' ) ) {
\DeleteDuplicatePosts\ddp_fs()->set_basename( false, __FILE__ );
} else {
// DO NOT REMOVE THIS IF, IT IS ESSENTIAL FOR THE `function_exists` CALL ABOVE TO PROPERLY WORK.
if ( !function_exists( '\\DeleteDuplicatePosts\\ddp_fs' ) ) {
// Create a helper function for easy SDK access.
function ddp_fs() {
global $ddp_fs;
if ( !isset( $ddp_fs ) ) {
// Activate multisite network integration.
if ( !defined( 'WP_FS__PRODUCT_925_MULTISITE' ) ) {
define( 'WP_FS__PRODUCT_925_MULTISITE', true );
}
// Include Freemius SDK.
// SDK is auto-loaded through composer
$ddp_fs = fs_dynamic_init( array(
'id' => '925',
'slug' => 'delete-duplicate-posts',
'type' => 'plugin',
'public_key' => 'pk_0af9f9e83f00e23728a55430a57dd',
'is_premium' => false,
'premium_suffix' => 'Pro',
'has_addons' => false,
'has_paid_plans' => true,
'menu' => array(
'slug' => 'delete-duplicate-posts',
'first-path' => 'tools.php?page=delete-duplicate-posts&welcome-message=true',
'contact' => false,
'support' => false,
'parent' => array(
'slug' => 'tools.php',
),
),
'is_live' => true,
'is_org_compliant' => true,
) );
}
return $ddp_fs;
}
// Init Freemius.
ddp_fs();
// Signal that SDK was initiated.
do_action( 'ddp_fs_loaded' );
}
ddp_fs()->add_action( 'after_uninstall', 'ddp_fs_uninstall_cleanup' );
}
/**
* Cleans up when uninstalling
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Tuesday, January 12th, 2021.
* @return void
*/
function ddp_fs_uninstall_cleanup() {
global $wpdb;
$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'ddp_log' );
$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'ddp_redirects' );
delete_option( 'ddp_deleted_duplicates' );
delete_option( 'delete_duplicate_posts_options_v4' );
}
if ( !class_exists( __NAMESPACE__ . '\\Delete_Duplicate_Posts' ) ) {
class Delete_Duplicate_Posts {
public static $options_name = 'delete_duplicate_posts_options_v4';
public $localization_domain = 'delete-duplicate-posts';
public static $options = null;
public function __construct() {
// Adds extra permissions to Freemius
if ( function_exists( 'ddp_fs' ) ) {
ddp_fs()->add_filter( 'permission_list', array(__CLASS__, 'add_freemius_extra_permission') );
}
global $ddp_fs;
add_action(
'admin_head',
array(__CLASS__, 'set_custom_help_content'),
1,
2
);
self::get_options();
add_action( 'wp_ajax_ddp_get_loglines', array(__CLASS__, 'return_loglines_ajax') );
add_action( 'wp_ajax_ddp_get_duplicates', array(__CLASS__, 'return_duplicates_ajax') );
add_action( 'init', array(__CLASS__, 'do_init') );
add_action( 'wp_ajax_ddp_delete_duplicates', array(__CLASS__, 'delete_duplicates_ajax') );
// loads admin notices
add_action( 'admin_menu', array($this, 'admin_menu_link') );
add_action( 'admin_enqueue_scripts', array($this, 'admin_enqueue_scripts') );
add_action(
'wp_insert_site',
array($this, 'on_create_blog'),
99999,
1
);
add_filter( 'wpmu_drop_tables', array($this, 'on_delete_blog') );
register_activation_hook( __FILE__, array($this, 'install') );
add_action( 'ddp_cron', array($this, 'cleandupes') );
add_action( 'cron_schedules', array($this, 'add_cron_intervals') );
}
/**
* do_init.
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Monday, October 28th, 2024.
* @access public static
* @return void
*/
public static function do_init() {
global $ddp_fs;
$determine_locale = determine_locale();
}
/**
* delete_duplicates_ajax.
*
* @author Lars Koudal
* @author Unknown
* @since v0.0.1
* @version v1.0.0 Tuesday, January 12th, 2021.
* @version v1.0.1 Tuesday, October 31st, 2023.
* @version v1.0.2 Wednesday, November 1st, 2023.
* @version v1.0.3 Tuesday, April 2nd, 2024.
* @version v1.0.4 Tuesday, May 7th, 2024.
* @access public static
* @param boolean $return_data Default: false
* @return mixed
*/
public static function delete_duplicates_ajax( $return_data = false ) {
// Check user permissions
if ( !current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'You do not have sufficient permissions to perform this action.', 'delete-duplicate-posts' ) );
return;
}
// Verify the AJAX request, to prevent processing requests external of the site.
check_ajax_referer( 'cp_ddp_delete_loglines' );
// Log the cleaning action
self::log( __( 'Cleaning duplicates', 'delete-duplicate-posts' ) );
// Validate the POST data
$checked_posts = $_POST['checked_posts'] ?? null;
if ( empty( $checked_posts ) || !is_array( $checked_posts ) ) {
wp_send_json_error( __( 'No duplicates were selected?', 'delete-duplicate-posts' ) );
return;
}
// Process and sanitize the checked posts
$cleaned_posts = array();
foreach ( $checked_posts as $cp ) {
if ( !empty( $cp['ID'] ) && !empty( $cp['orgID'] ) && is_numeric( $cp['ID'] ) && is_numeric( $cp['orgID'] ) ) {
$cleaned_posts[] = array(
'ID' => intval( $cp['ID'] ),
'orgID' => intval( $cp['orgID'] ),
);
}
}
// Check if any valid posts were found
if ( empty( $cleaned_posts ) ) {
wp_send_json_error( __( 'Invalid duplicates selected.', 'delete-duplicate-posts' ) );
return;
}
// Attempt to clean duplicates and handle possible failures
$result = self::cleandupes( true, $cleaned_posts );
if ( !$result ) {
$errorMessage = __( 'Error deleting duplicates.', 'delete-duplicate-posts' );
// Assuming $result is an array or object that could be serialized safely:
if ( is_array( $result ) || is_object( $result ) ) {
$errorData = array(
'additional_info' => json_encode( $result ),
);
wp_send_json_error( $errorMessage, $errorData );
} elseif ( is_string( $result ) ) {
// Sanitize the string to be safe for output
$errorData = array(
'additional_info' => esc_html( $result ),
);
wp_send_json_error( $errorMessage, $errorData );
} else {
// If result is not an array, object, or string, or if you want to keep the message generic
wp_send_json_error( $errorMessage );
}
return;
}
// Optionally return success data
if ( $return_data ) {
wp_send_json_success( array(
'message' => __( 'Duplicates deleted successfully.', 'delete-duplicate-posts' ),
) );
}
}
/**
* Returns log lines via AJAX.
*
* @author Lars Koudal
* @author Unknown
* @since v0.0.1
* @version v1.0.0 Tuesday, January 12th, 2021.
* @version v1.0.1 Wednesday, November 1st, 2023.
* @access public static
* @param boolean $return Default: false
* @return void|array
*/
public static function return_loglines_ajax( $return_data = false ) {
if ( !current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'You do not have sufficient permissions to perform this action.', 'delete-duplicate-posts' ) );
return;
}
check_ajax_referer( 'cp_ddp_return_loglines' );
$currstep = ( filter_input( INPUT_POST, 'step', FILTER_SANITIZE_NUMBER_INT ) ?: 0 );
++$currstep;
$json_response = array(
'step' => $currstep,
);
global $wpdb;
$loglines = $wpdb->get_results( "SELECT datime, note FROM {$wpdb->prefix}ddp_log ORDER BY datime DESC LIMIT 100;" );
if ( !empty( $loglines ) ) {
$json_response['results'] = $loglines;
} else {
$json_response['msg'] = __( 'Error: Log is empty.. do something :-)', 'delete-duplicate-posts' );
if ( $return_data ) {
return $json_response;
}
wp_send_json_error( $json_response );
return;
// Make sure to exit here to prevent sending multiple responses.
}
if ( $return_data ) {
return $json_response;
}
wp_send_json_success( $json_response );
}
/**
* return_duplicates_ajax.
*
* @author Lars Koudal
* @author Unknown
* @since v0.0.1
* @version v1.0.0 Tuesday, January 12th, 2021.
* @version v1.0.1 Wednesday, November 1st, 2023.
* @access public static
* @return void
*/
public static function return_duplicates_ajax() {
check_ajax_referer( 'cp_ddp_return_duplicates', true );
if ( !current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'You do not have sufficient permissions to perform this action.', 'delete-duplicate-posts' ) );
return;
}
// Get duplicates
$duplicates = self::return_duplicates( true );
// Initialize DataTables response array
$response = array(
'draw' => ( isset( $_POST['draw'] ) ? intval( $_POST['draw'] ) : 0 ),
'recordsTotal' => 0,
'recordsFiltered' => 0,
'data' => array(),
);
if ( !empty( $duplicates ) && isset( $duplicates['dupes'] ) ) {
$response['recordsTotal'] = $duplicates['dupescount'];
$response['recordsFiltered'] = $duplicates['dupescount'];
foreach ( $duplicates['dupes'] as $dupe ) {
$title = ( '' === $dupe['title'] ? '-empty-' : $dupe['title'] );
$orgTitle = ( '' === $dupe['orgtitle'] ? '-empty-' : $dupe['orgtitle'] );
$permalink = esc_url( get_permalink( $dupe['ID'] ) );
$orgPermalink = esc_url( get_permalink( $dupe['orgID'] ) );
$response['data'][] = array(
'ID' => esc_html( $dupe['ID'] ),
'orgID' => esc_html( $dupe['orgID'] ),
'duplicate' => sprintf(
'<a href="%s" target="_blank">%s</a> (ID #%s) <br><small>%s Type: %s Status: %s</small>',
esc_url( $permalink ),
esc_html( $title ),
esc_html( $dupe['ID'] ),
esc_html( $dupe['why'] ),
esc_html( $dupe['type'] ),
esc_html( $dupe['status'] )
),
'original' => sprintf(
'<a href="%s" target="_blank">%s</a> (ID #%s)',
esc_url( $orgPermalink ),
esc_html( $orgTitle ),
esc_html( $dupe['orgID'] )
),
);
}
}
wp_send_json( $response );
exit;
}
/**
* Converts a number to relevant unit size
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 24th, 2021.
* @param mixed $size
* @return void
*/
public static function pretty_value( $size ) {
if ( $size <= 0 || !is_numeric( $size ) ) {
return '0 b';
}
$unit = array(
'b',
'kb',
'mb',
'gb',
'tb',
'pb'
);
$log = log( $size, 1024 );
$i = floor( $log );
$i = max( 0, min( (int) $i, count( $unit ) - 1 ) );
$num = $size / pow( 1024, $i );
$calc = round( $num, 2 ) . ' ' . $unit[$i];
return $calc;
}
/**
* Returns duplicates based on current settings - internal, not used via AJAX
*
* @author Lars Koudal
* @author Unknown
* @since v0.0.1
* @version v1.0.0 Tuesday, January 12th, 2021.
* @version v1.0.1 Wednesday, November 1st, 2023.
* @access public static
* @param boolean $return Default: false
* @return void
*/
public static function return_duplicates( $return = false ) {
self::timerstart( 'return_duplicates' );
$options = self::get_options();
$comparemethod = 'titlecompare';
$return_duplicates_time = false;
global $ddp_fs;
$json_response = array();
// @ check compare method - maybe change lookup routine?
global $wpdb;
$table_name = $wpdb->prefix . 'posts';
$resultslimit = $options['ddp_resultslimit'];
$viewlimit = intval( $resultslimit );
if ( 0 === $viewlimit ) {
$viewlimit = 9999;
}
$ddp_pts_arr = $options['ddp_pts'];
if ( isset( $ddp_pts_arr ) && is_array( $ddp_pts_arr ) ) {
$ddp_pts = '"' . implode( '","', $ddp_pts_arr ) . '"';
} else {
$ddp_pts = '';
}
$ddp_pts = rtrim( $ddp_pts, ',' );
$post_stati = '"publish"';
$order = $options['ddp_keep'];
// verify default value has been set
if ( 'oldest' !== $order ) {
// two choices, if its not the first its the second...
$options['ddp_keep'] = 'latest';
$order = 'latest';
}
if ( 'oldest' === $order ) {
$minmax = 'MIN(id)';
}
if ( 'latest' === $order ) {
$minmax = 'MAX(id)';
}
$ddpstatuscnt = array();
$dupescount = 0;
if ( '' !== $ddp_pts ) {
$thisquery = false;
// **** Compare by title ****
if ( 'titlecompare' === $comparemethod ) {
$limit = ( isset( $_POST['length'] ) ? intval( $_POST['length'] ) : 10 );
// Default to 10 if not set
$offset = ( isset( $_POST['start'] ) ? intval( $_POST['start'] ) : 0 );
// Default to 0 if not set
if ( !wp_doing_ajax() && $return ) {
$limit = $options['ddp_resultslimit'];
// for returning results for cron job.
}
$wpdb->query( 'SET SQL_BIG_SELECTS=1' );
$resultsoutput = ' LIMIT ' . intval( $limit ) . ' OFFSET ' . intval( $offset );
if ( $options['ddp_debug'] ) {
self::log( 'DEBUG: SQL - Setting SET SQL_BIG_SELECTS=1' );
}
$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}";
if ( $options['ddp_debug'] ) {
self::log( 'DEBUG: SQL ' . esc_attr( $thisquery ) );
}
$json_response['lookup_query'] = $thisquery;
$dupes = $wpdb->get_results( $thisquery, ARRAY_A );
// here we get total dupes - not cute, but the other approach not working.
$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";
$total_dupes = $wpdb->get_var( $total_dupes_query );
if ( $options['ddp_debug'] ) {
self::log( 'DEBUG: SQL total_dupes_query ' . esc_attr( $total_dupes_query ) );
}
if ( '' !== $wpdb->last_error ) {
$last_error = htmlspecialchars( $wpdb->last_error, ENT_QUOTES );
$json_response['lookup_error'] = htmlspecialchars( $wpdb->last_error, ENT_QUOTES );
self::log( sprintf(
/* translators: 1: Database error message, 2: SQL query */
__( 'Look up error: %1$s %2$s', 'delete-duplicate-posts' ),
$last_error,
$total_dupes_query
) );
}
if ( $dupes ) {
$json_response['dupescount'] = $total_dupes;
$stepcount = 0;
foreach ( $dupes as $dupe ) {
$mystatus = $dupe['post_status'];
if ( isset( $ddpstatuscnt[$mystatus] ) ) {
$ddpstatuscnt[$mystatus] = $ddpstatuscnt[$mystatus] + 1;
} else {
$ddpstatuscnt[$mystatus] = 1;
}
// Only save the dupes
if ( $dupe['ID'] !== $dupe['save_this_post_id'] ) {
$dupedetails = array(
'ID' => $dupe['ID'],
'permalink' => get_permalink( $dupe['ID'] ),
'title' => $dupe['post_title'],
'type' => $dupe['post_type'],
'orgID' => $dupe['save_this_post_id'],
'orgtitle' => $dupe['post_title'],
'orgpermalink' => get_permalink( $dupe['save_this_post_id'] ),
'status' => $dupe['post_status'],
'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'] ),
);
$json_response['dupes'][] = $dupedetails;
}
++$stepcount;
}
}
}
$statusdata = '';
if ( is_array( $ddpstatuscnt ) && count( $ddpstatuscnt ) > 1 ) {
$statusdata .= '(';
foreach ( $ddpstatuscnt as $key => $dsc ) {
$statusdata .= $key . ': ' . number_format_i18n( $dsc ) . ', ';
}
$statusdata = rtrim( $statusdata, ', ' );
$statusdata .= ')';
}
$return_duplicates_time = self::timerstop( 'return_duplicates' );
if ( $options['ddp_debug'] ) {
$max = 5;
if ( isset( $json_response['dupes'] ) ) {
$idlist = array();
$step = 0;
foreach ( $json_response['dupes'] as $dupe ) {
if ( $step <= $max ) {
$details = '';
if ( isset( $dupe['ID'] ) ) {
$details .= 'ID: ' . $dupe['ID'] . ' ';
}
if ( isset( $dupe['title'] ) ) {
$details .= ' title: "' . $dupe['title'] . '" ';
}
if ( isset( $dupe['permalink'] ) ) {
$details .= 'Permalink: ' . $dupe['permalink'] . ' ';
}
if ( isset( $dupe['status'] ) ) {
$details .= 'Status: ' . $dupe['status'] . ' ';
}
if ( isset( $dupe['type'] ) ) {
$details .= 'Type: ' . $dupe['type'] . ' ';
}
if ( isset( $dupe['orgID'] ) ) {
$details .= 'orgID: ' . $dupe['orgID'] . ' ';
}
if ( isset( $dupe['orgtitle'] ) ) {
$details .= 'orgtitle: ' . $dupe['orgtitle'] . ' ';
}
if ( isset( $dupe['orgpermalink'] ) ) {
$details .= ' orgpermalink: ' . $dupe['orgpermalink'] . ' ';
}
self::log( $details );
}
++$step;
}
}
}
if ( isset( $json_response['dupes'] ) ) {
self::log( sprintf(
/* translators: 1: Number of duplicates, 2: Time in seconds, 3: Status data, 4: Memory usage */
__( 'Duplicates found: %1$d, Time: %2$s sec. %3$s Mem usage: %4$s', 'delete-duplicate-posts' ),
count( $json_response['dupes'] ),
$return_duplicates_time,
$statusdata,
self::pretty_value( memory_get_peak_usage( true ) )
) );
}
} else {
$json_response['msg'] = __( 'Error: Choose post types to check.', 'delete-duplicate-posts' );
$return_duplicates_time = self::timerstop( 'return_duplicates' );
$json_response['time'] = $return_duplicates_time . ' sec';
if ( $return ) {
return $json_response;
}
wp_send_json_error( $json_response );
}
if ( !$return_duplicates_time ) {
$return_duplicates_time = self::timerstop( 'return_duplicates' );
}
if ( isset( $json_response['dupescount'] ) ) {
$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 ) );
}
if ( $return ) {
return $json_response;
}
wp_send_json_success( $json_response );
}
/**
* create_redirect.
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Friday, July 2nd, 2021.
* @version v1.0.1 Tuesday, October 18th, 2022.
* @access public static
* @param mixed $inurl
* @param mixed $targeturl
* @param integer $code Default: 301
* @return void
*/
public static function create_redirect( $inurl, $targeturl, $code = 301 ) {
global $wpdb, $ddp_fs;
}
/**
* Return default options
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Friday, July 2nd, 2021.
* @access public static
* @return mixed
*/
public static function default_options() {
$defaults = array(
'ddp_running' => 'false',
'ddp_keep' => 'oldest',
'ddp_limit' => 50,
'ddp_pts' => array('post', 'page'),
'ddp_statusmail_recipient' => '',
'ddp_statusmail' => 0,
'ddp_resultslimit' => 0,
'ddp_enabled' => 0,
'ddp_pstati' => array('publish'),
'ddp_debug' => 0,
'ddp_redirects' => 0,
);
return $defaults;
}
/**
* get plugin's options
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @return mixed
*/
public static function get_options() {
if ( null !== self::$options ) {
return self::$options;
}
$options = get_option( self::$options_name, array() );
if ( !is_array( $options ) ) {
$options = array();
}
$options = array_merge( self::default_options(), $options );
return $options;
}
/**
* add_freemius_extra_permission.
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param mixed $permissions
* @return mixed
*/
public static function add_freemius_extra_permission( $permissions ) {
$permissions['helpscout'] = array(
'icon-class' => 'dashicons dashicons-sos',
'label' => 'Help Scout',
'desc' => __( 'Rendering Help Scouts beacon for easy help and support', 'delete-duplicate-posts' ),
'priority' => 16,
);
$permissions['newsletter'] = array(
'icon-class' => 'dashicons dashicons-email-alt2',
'label' => 'Newsletter',
'desc' => __( 'Your email is added to cleverplugins.com newsletter. Unsubscribe any time.', 'delete-duplicate-posts' ),
'priority' => 18,
);
return $permissions;
}
/**
* Fetch plugin version from plugin PHP header
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @return mixed
*/
public static function get_plugin_version() {
$plugin_data = get_file_data( __FILE__, array(
'version' => 'Version',
), 'plugin' );
return $plugin_data['version'];
}
/**
* timerstart.
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param mixed $watchname
* @return void
*/
public static function timerstart( $watchname ) {
set_transient( 'ddp_' . $watchname, microtime( true ), 60 * 60 * 1 );
}
/**
* timerstop.
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param mixed $watchname
* @param integer $digits Default: 3
* @return mixed
*/
public static function timerstop( $watchname, $digits = 3 ) {
$return = round( microtime( true ) - get_transient( 'ddp_' . $watchname ), $digits );
delete_transient( 'ddp_' . $watchname );
return $return;
}
/**
* Clean duplicates - not AJAX version
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param boolean $manualrun Default: false
* @param mixed $to_delete Default: array()
* @return void
*/
public static function cleandupes( $manualrun = false, $to_delete = array() ) {
global $wpdb, $ddp_fs;
self::timerstart( 'ddp_totaltime' );
// start total timer
$options = self::get_options();
$options['ddp_running'] = true;
self::save_options( $options );
if ( !$manualrun ) {
self::log( __( 'Automatic CRON job running.', 'delete-duplicate-posts' ) );
} else {
self::log( __( 'Manually cleaning.', 'delete-duplicate-posts' ) );
}
// what to do with a manual run - no notices
if ( count( $to_delete ) > 0 ) {
$lookup_arr = array();
foreach ( $to_delete as $td ) {
$new_item = array();
$new_item['ID'] = $td['ID'];
$new_item['orgID'] = $td['orgID'];
$new_item['type'] = get_post_type( $td['ID'] );
$new_item['title'] = get_the_title( $td['ID'] );
$lookup_arr['dupes'][] = $new_item;
}
$dupes = $lookup_arr;
} else {
$dupes = self::return_duplicates( true );
}
$resultnote = '';
$dispcount = 0;
if ( isset( $dupes['dupes'] ) ) {
foreach ( $dupes['dupes'] as $dupe ) {
$postid = $dupe['ID'];
$title = substr( $dupe['title'], 0, 35 );
if ( $postid ) {
self::timerstart( 'deletepost_' . $postid );
/* @todo - implement a premium option to permanently delete or just use the WP setting.
Options: Use WP setting (default), Delete Permantently, Trash posts (if enabled in WP)
*/
$deleteresult = wp_trash_post( $postid );
$timespent = self::timerstop( 'deletepost_' . $postid );
++$dispcount;
$totaldeleted = get_option( 'ddp_deleted_duplicates' );
if ( false !== $totaldeleted ) {
++$totaldeleted;
update_option( 'ddp_deleted_duplicates', $totaldeleted, false );
} else {
update_option( 'ddp_deleted_duplicates', 1, false );
}
if ( $options['ddp_debug'] ) {
// translators: Debug notice. 1: type of duplicate. 2: The title of the post. 3: The ID. 4: Time spent deleting.
self::log( sprintf(
__( 'DEBUG: Deleted %1$s %2$s (id: %3$s) in %4$s sec.', 'delete-duplicate-posts' ),
$dupe['type'],
$title,
$postid,
$timespent
) );
}
}
}
}
$totaltimespent = self::timerstop( 'ddp_totaltime' );
self::log( sprintf( __( 'A total of %1$s duplicate posts were deleted in %2$s sec.', 'delete-duplicate-posts' ), $dispcount, $totaltimespent ) );
$json_response = array(
'totaltimespent' => $totaltimespent,
'deleted' => $dispcount,
);
// Mail logic...
if ( 0 < $dispcount && $options['ddp_statusmail'] ) {
$blogurl = esc_url( site_url() );
$recipient = sanitize_email( $options['ddp_statusmail_recipient'] );
// translators: 1: Number of deleted posts, 2: Blog URL.
$messagebody = sprintf( __( 'Hi Admin, I have deleted <strong>%1$d</strong> duplicated posts on your blog, %2$s.', 'delete-duplicate-posts' ), $dispcount, $blogurl );
$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' );
$messagebody .= ' <a href="https://cleverplugins.com/delete-duplicate-posts/" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Delete Duplicate Posts', 'delete-duplicate-posts' ) . '</a>';
$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>';
$mailstatus = false;
if ( is_email( $recipient ) ) {
$subject = __( 'Deleted Duplicate Posts Status', 'delete-duplicate-posts' );
$mailstatus = wp_mail( $recipient, $subject, wp_kses_post( $messagebody ) );
if ( $options['ddp_debug'] ) {
// translators: %s: Email recipient.
self::log( sprintf( __( 'DEBUG: Sending email to: %s', 'delete-duplicate-posts' ), $recipient ) );
}
if ( $mailstatus ) {
// translators: %s: Email recipient.
self::log( sprintf( __( 'Status email sent to %s.', 'delete-duplicate-posts' ), $recipient ) );
}
} else {
// translators: %s: Email address.
self::log( sprintf( __( 'Not a valid email %s.', 'delete-duplicate-posts' ), $recipient ) );
}
}
$options['ddp_running'] = false;
self::save_options( $options );
// Lets return a response
if ( 0 === $manualrun && !wp_doing_ajax() ) {
$json_response = array(
'msg' => sprintf( esc_html__( 'A total of %s duplicates were deleted.', 'delete-duplicate-posts' ), intval( $dispcount ) ),
);
}
wp_send_json_success( $json_response );
}
/**
* add_cron_intervals.
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param mixed $schedules
* @return mixed
*/
public static function add_cron_intervals( $schedules ) {
$schedules['5min'] = array(
'interval' => 300,
'display' => __( 'Every 5 minutes', 'delete-duplicate-posts' ),
);
$schedules['10min'] = array(
'interval' => 600,
'display' => __( 'Every 10 minutes', 'delete-duplicate-posts' ),
);
$schedules['15min'] = array(
'interval' => 900,
'display' => __( 'Every 15 minutes', 'delete-duplicate-posts' ),
);
$schedules['30min'] = array(
'interval' => 1800,
'display' => __( 'Every 30 minutes', 'delete-duplicate-posts' ),
);
return $schedules;
}
/**
* Log a notification to the database
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Monday, January 11th, 2021.
* @access public static
* @param mixed $text
* @return void
*/
public static function log( $text ) {
global $wpdb;
$ddp_logtable = $wpdb->prefix . 'ddp_log';
// Insert log entry
$insert_result = $wpdb->insert( $ddp_logtable, array(
'datime' => current_time( 'mysql' ),
'note' => $text,
), array('%s', '%s') );
if ( false === $insert_result ) {
// Handle error appropriately (e.g., log or throw exception)
return;
}
// Efficiently check if row count exceeds 1000 and delete old entries
$row_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$ddp_logtable};" );
if ( $row_count > 1000 ) {
$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)" );
}
}
/**
* Enqueues scripts and styles
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Monday, January 11th, 2021.
* @access public static
* @return void
*/
public static function admin_enqueue_scripts() {
$screen = get_current_screen();
if ( is_object( $screen ) && 'tools_page_delete-duplicate-posts' === $screen->id ) {
$pluginver = self::get_plugin_version();
wp_enqueue_script( 'jquery' );
wp_enqueue_style(
'delete-duplicate-posts',
plugins_url( '/css/delete-duplicate-posts.css', __FILE__ ),
array(),
$pluginver
);
wp_enqueue_script(
'dataTables',
// Unique handle for your script
plugin_dir_url( __FILE__ ) . 'js/DataTables/datatables.js',
// Path to your script file
array('jquery'),
// Dependencies, if any. This script depends on jQuery
$pluginver,
array(
'in_footer' => true,
)
);
wp_enqueue_style(
'dataTables',
plugins_url( '/js/DataTables/datatables.css', __FILE__ ),
array(),
$pluginver
);
wp_register_script(
'delete-duplicate-posts',
plugins_url( '/js/delete-duplicate-posts.js', __FILE__ ),
array('jquery', 'dataTables'),
$pluginver,
true
);
$js_vars = array(
'nonce' => wp_create_nonce( 'cp_ddp_return_duplicates' ),
'loglines_nonce' => wp_create_nonce( 'cp_ddp_return_loglines' ),
'deletedupes_nonce' => wp_create_nonce( 'cp_ddp_delete_loglines' ),
'text_areyousure' => __( 'Are you sure you want to delete duplicates? There is no undo feature.', 'delete-duplicate-posts' ),
'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' ),
'fromUrlTitle' => __( 'From URL', 'delete-duplicate-posts' ),
'targetUrlTitle' => __( 'Target URL', 'delete-duplicate-posts' ),
'refreshingText' => __( 'Refreshing...', 'delete-duplicate-posts' ),
'refreshText' => __( 'Refresh', 'delete-duplicate-posts' ),
'errorDetailsText' => __( 'Error details: ', 'delete-duplicate-posts' ),
'redirectsErrorText' => __( 'Redirects DataTables error occurred. ', 'delete-duplicate-posts' ),
'processingMessage' => __( 'Looking for duplicates', 'delete-duplicate-posts' ),
'requestTimeText' => __( 'Request: ', 'delete-duplicate-posts' ),
'failedToLoadDataText' => __( 'Failed to load data. ', 'delete-duplicate-posts' ),
'duplicateTitle' => __( 'Duplicate', 'delete-duplicate-posts' ),
'originalTitle' => __( 'Original', 'delete-duplicate-posts' ),
'selectRowAlert' => __( 'Please select at least one row to delete.', 'delete-duplicate-posts' ),
'serverResponseText' => __( 'Response from the server: ', 'delete-duplicate-posts' ),
'errorOccurredText' => __( 'An error occurred: ', 'delete-duplicate-posts' ),
'deleteSelectedText' => __( 'Delete Selected', 'delete-duplicate-posts' ),
'selectVisibleText' => __( 'Select Visible', 'delete-duplicate-posts' ),
'selectNoneText' => __( 'Select None', 'delete-duplicate-posts' ),
'somethingWentWrongText' => __( 'Something went wrong.', 'delete-duplicate-posts' ),
);
wp_localize_script( 'delete-duplicate-posts', 'cp_ddp', $js_vars );
wp_enqueue_script( 'delete-duplicate-posts' );
}
}
/**
* Create plugin tables
*
* @author Lars Koudal
* @author Unknown
* @since v0.0.1
* @version v1.0.0 Monday, January 11th, 2021.
* @version v1.0.1 Sunday, July 17th, 2022.
* @version v1.0.2 Sunday, December 3rd, 2023.
* @access public static
* @return void
*/
public static function create_table() {
global $wpdb, $ddp_fs;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$log_table_name = $wpdb->prefix . 'ddp_log';
$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;";
dbDelta( $sql_log );
$redirects_table_name = $wpdb->prefix . 'ddp_redirects';
$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;";
dbDelta( $sql_redirects );
// Additional plugin setup
$options = self::get_options();
self::save_options( $options );
wp_clear_scheduled_hook( 'ddp_cron' );
self::log( __( 'Plugin activated.', 'delete-duplicate-posts' ) );
}
/**
* Install routines - create database and default options
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param mixed $network_wide
* @return void
*/
public static function install( $network_wide ) {
global $wpdb;
require_once ABSPATH . '/wp-admin/includes/upgrade.php';
if ( is_multisite() && $network_wide ) {
// Get all blogs in the network and activate plugin on each one
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}" );
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
self::create_table();
restore_current_blog();
}
} else {
self::create_table();
}
}
/**
* Creating table when a new blog is created
* https://sudarmuthu.com/blog/how-to-properly-create-tables-in-wordpress-multisite-plugins/
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @version v1.0.1 Sunday, May 14th, 2023.
* @access public static
* @param mixed $new_site
* @return void
*/
public static function on_create_blog( $new_site ) {
if ( is_plugin_active_for_network( 'delete-duplicate-posts/delete-duplicate-posts.php' ) ) {
switch_to_blog( $new_site->blog_id );
self::create_table();
restore_current_blog();
}
}
/**
* Deleting the table whenever a blog is deleted
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param mixed $tables
* @return mixed
*/
public static function on_delete_blog( $tables ) {
global $wpdb, $ddp_fs;
$tables[] = $wpdb->prefix . 'ddp_log';
return $tables;
}
/**
* Saves options
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param mixed $newoptions
* @return mixed
*/
public static function save_options( $newoptions ) {
self::$options = null;
return update_option( 'delete_duplicate_posts_options_v4', $newoptions );
}
/**
* Adds link to menu under Tools
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @return void
*/
public static function admin_menu_link() {
// only for admins
if ( !current_user_can( 'manage_options' ) ) {
return;
}
add_management_page(
'Delete Duplicate Posts',
'Delete Duplicate Posts',
'edit_posts',
'delete-duplicate-posts',
array(__NAMESPACE__ . '\\Delete_Duplicate_Posts', 'admin_options_page'),
41
);
add_filter(
'plugin_action_links_' . plugin_basename( __FILE__ ),
array(__NAMESPACE__ . '\\Delete_Duplicate_Posts', 'filter_plugin_actions'),
10,
2
);
}
/**
* filter_plugin_actions.
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @param mixed $links
* @param mixed $file
* @return mixed
*/
public static function filter_plugin_actions( $links, $file ) {
$settings_link = '<a href="tools.php?page=delete-duplicate-posts">' . __( 'Settings', 'delete-duplicate-posts' ) . '</a>';
array_unshift( $links, $settings_link );
// before other links
return $links;
}
/**
* Adds help content to plugin page
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @access public static
* @return void
*/
public static function set_custom_help_content() {
$screen = get_current_screen();
if ( 'tools_page_delete-duplicate-posts' === $screen->id ) {
$screen->add_help_tab( array(
'id' => 'ddp_help',
'title' => __( 'Usage and FAQ', 'delete-duplicate-posts' ),
'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>",
) );
}
}
/**
* admin_options_page.
*
* @author Lars Koudal
* @since v0.0.1
* @version v1.0.0 Thursday, June 9th, 2022.
* @version v1.0.1 Thursday, June 9th, 2022.
* @access public static
* @return void
*/
public static function admin_options_page() {
global $ddp_fs, $wpdb;
// DELETE NOW
if ( isset( $_POST['deleteduplicateposts_delete'] ) && isset( $_POST['_wpnonce'] ) ) {
if ( wp_verify_nonce( $_POST['_wpnonce'], 'ddp-clean-now' ) ) {
self::cleandupes( 1 );
// use the value 1 to indicate it is being run manually.
}
}
// RUN NOW!!
if ( isset( $_POST['ddp_runnow'] ) ) {
if ( !wp_verify_nonce( $_POST['_wpnonce'], 'ddp-update-options' ) ) {
die( esc_html( __( 'Whoops! Some error occured, try again, please!', 'delete-duplicate-posts' ) ) );
}
}
// SAVING OPTIONS
if ( isset( $_POST['delete_duplicate_posts_save'] ) ) {
if ( !wp_verify_nonce( $_POST['_wpnonce'], 'ddp-update-options' ) ) {
die( esc_html( __( 'Whoops! There was a problem with the data you posted. Please go back and try again.', 'delete-duplicate-posts' ) ) );
}
$posttypes = array();
if ( isset( $_POST['ddp_pts'] ) ) {
$option_array = $_POST['ddp_pts'];
$option_count = count( $option_array );
for ($i = 0; $i < $option_count; $i++) {
$posttypes[] = sanitize_text_field( $option_array[$i] );
}
}
if ( isset( $_POST['ddp_enabled'] ) ) {
$options['ddp_enabled'] = ( 'on' === $_POST['ddp_enabled'] ? true : false );
} else {
$options['ddp_enabled'] = false;
}
$options['ddp_statusmail'] = ( isset( $_POST['ddp_statusmail'] ) && 'on' === $_POST['ddp_statusmail'] ? true : false );
$options['ddp_debug'] = ( isset( $_POST['ddp_debug'] ) && 'on' === $_POST['ddp_debug'] ? true : false );
if ( isset( $_POST['ddp_statusmail_recipient'] ) ) {
$options['ddp_statusmail_recipient'] = sanitize_text_field( $_POST['ddp_statusmail_recipient'] );
}
if ( isset( $_POST['ddp_schedule'] ) ) {
$options['ddp_schedule'] = sanitize_text_field( $_POST['ddp_schedule'] );
}
if ( isset( $_POST['ddp_keep'] ) ) {
$options['ddp_keep'] = sanitize_text_field( $_POST['ddp_keep'] );
}
if ( isset( $_POST['ddp_method'] ) ) {
$options['ddp_method'] = sanitize_text_field( $_POST['ddp_method'] );
}
if ( isset( $_POST['ddp_resultslimit'] ) ) {
$options['ddp_resultslimit'] = sanitize_text_field( $_POST['ddp_resultslimit'] );
}
// 301 redirects
if ( isset( $_POST['ddp_redirects'] ) ) {
$options['ddp_redirects'] = ( 'on' === $_POST['ddp_redirects'] ? true : false );
} else {
$options['ddp_redirects'] = false;
}
$options['ddp_pts'] = $posttypes;
// Previously sanitized
if ( isset( $_POST['ddp_limit'] ) ) {
$options['ddp_limit'] = sanitize_text_field( $_POST['ddp_limit'] );
}
self::save_options( $options );
if ( isset( self::$options['ddp_enabled'] ) ) {
wp_clear_scheduled_hook( 'ddp_cron' );
$interval = self::$options['ddp_schedule'];
if ( !$interval ) {
$interval = 'hourly';
}
$nextscheduled = wp_next_scheduled( 'ddp_cron' );
if ( !$nextscheduled ) {
wp_schedule_event( time(), $interval, 'ddp_cron' );
}
}
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( __( 'Settings saved.', 'delete-duplicate-posts' ) ) . '</p></div>';
}
// CLEARING THE LOG
if ( isset( $_POST['ddp_clearlog'] ) ) {
if ( !wp_verify_nonce( $_POST['_wpnonce'], 'ddp_clearlog_nonce' ) ) {
die( esc_html( __( 'Whoops! Some error occured, try again, please!', 'delete-duplicate-posts' ) ) );
}
$table_name_log = $wpdb->prefix . 'ddp_log';
$wpdb->query( "TRUNCATE {$table_name_log};" );
//phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
echo '<div class="updated"><p>' . esc_html( __( 'The log was cleared.', 'delete-duplicate-posts' ) ) . '</p></div>';
}
// REACTIVATE THE DATABASE
if ( isset( $_POST['ddp_reactivate'] ) ) {
if ( !wp_verify_nonce( $_POST['_wpnonce'], 'ddp_reactivate_nonce' ) ) {
die( esc_html( __( 'Whoops! Some error occured, try again, please!', 'delete-duplicate-posts' ) ) );
}
self::install( false );
self::log( 'Reinstalled databases' );
}
$options = self::get_options();
$css_classes = ' free';
$display_ads = true;
?>
<div class="wrap fs-section <?php
echo esc_attr( $css_classes );
?>">
<h1>Delete Duplicate Posts <span>v. <?php
echo esc_html( self::get_plugin_version() );
?></span></h1>
<?php
$totaldeleted = get_option( 'ddp_deleted_duplicates' );
// check the code for get param 'welcome-message' er sat til true
?>
<h2 class="nav-tab-wrapper">
<a href="#duplicates-tab" class="nav-tab fs-tab nav-tab-active home"><?php
esc_html_e( 'Duplicates', 'delete-duplicate-posts' );
?></a>
<a href="#log-tab" class="nav-tab"><?php
esc_html_e( 'Log', 'delete-duplicate-posts' );
?></a>
<a href="#settings-tab" class="nav-tab"><?php
esc_html_e( 'Settings', 'delete-duplicate-posts' );
?></a>
<a href="#redirects-tab" class="nav-tab<?php
echo ( $ddp_fs->is__premium_only() && $ddp_fs->can_use_premium_code() ? '' : ' pro' );
?>"><?php
esc_html_e( 'Redirects', 'delete-duplicate-posts' );
?></a>
</h2>
<div class="ddp_content_wrapper">
<div class="ddp_content_cell">
<div id="delete-duplicate-posts-tabs">
<div id="duplicates-tab" class="tab-content">
<div id="ddp-dashboard">
<?php
if ( $options['ddp_enabled'] ) {
$interval = $options['ddp_schedule'];
if ( !$interval ) {
$interval = 'hourly';
}
$nextscheduled = wp_next_scheduled( 'ddp_cron' );
if ( !$nextscheduled ) {
// plugin active, but the cron needs to be activated also..
$options['last_interval'] = $interval;
self::save_options( $options );
wp_schedule_event( time(), $interval, 'ddp_cron' );
//}
}
} else {
wp_unschedule_hook( 'ddp_cron' );
}
$totaldeleted = get_option( 'ddp_deleted_duplicates' );
?>
<div class="statusdiv">
<div class="statusmessage"></div>
<div class="errormessage"></div>
<div class="dupelist">
<div id="requestTime"></div>
<table id="ddp_dupetable" class="wp-list-table widefat fixed striped table-view-list"></table>
</div>
</div>
<?php
if ( false !== $totaldeleted && 0 < $totaldeleted && $display_ads ) {
$totaldeleted = number_format_i18n( $totaldeleted );
?>
<div id="cp-ddp-reviewlink" data-dismissible="ddp-leavereview-180" class="updated notice notice-success">
<h3>
<?php
/* translators: %s: Total number of deleted duplicates */
printf( esc_html__( '%s duplicates deleted!', 'delete-duplicate-posts' ), esc_html( $totaldeleted ) );
?>
</h3>
<p>
<?php
/* translators: %s: Total number of deleted duplicates */
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 ) );
?>
</p>
<p>
<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
esc_html_e( 'Ok, you deserve it', 'delete-duplicate-posts' );
?></a>
</p>
</div>
<?php
}
?>
<?php
$my_current_user = wp_get_current_user();
?>
<div class="ddpnewsletter">
<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>
<form class="ml-block-form" action="https://assets.mailerlite.com/jsonp/16490/forms/106309157552916248/subscribe" data-code="" method="post" target="_blank">
<input type="text" class="form-control" data-inputmask="" name="fields[name]" placeholder="Name" autocomplete="given-name" value="<?php
echo esc_html( $my_current_user->display_name );
?>" required="required">
<input type="email" class="form-control" data-inputmask="" name="fields[email]" placeholder="Email" autocomplete="email" value="<?php
echo esc_html( $my_current_user->user_email );
?>" required="required">
<input type="hidden" name="fields[signupsource]" value="PluginInstall">
<input type="hidden" name="ml-submit" value="1">
<input type="hidden" name="anticsrf" value="true">
<button type="submit" class="button button-secondary">Subscribe</button>
</form>
<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>
</div>
<?php
$display_promotion = true;
if ( $display_promotion ) {
?>
<div class="innerpromotion ddppro">
<h3><span class="dashicons dashicons-star-filled"></span> Upgrade to Delete Duplicate Posts Pro <span class="dashicons dashicons-star-filled"></span></h3>
<ul class="linklist">
<li><strong>301 Redirects for Deleted Posts:</strong> Seamlessly redirect deleted posts to original pages.</li>
<li><strong>Search by Duplicate Post Meta:</strong> Advanced search options for identifying duplicates based on post metadata.</li>
<li><strong>Automatic Deletion:</strong> Automatic management of duplicate posts.</li>
<li><strong>Email Notifications:</strong> Alerts when duplicates are found and removed.</li>
<li><strong>Search Any Post Status:</strong> Deeper search capabilities, including unpublished posts.</li>
<li><strong>Filter and Delete Unpublished Duplicates:</strong> Proactively manage and prevent unseen duplicates.</li>
<li><strong>WooCommerce Compatibility:</strong> Look for and delete duplicate products with same SKUs. (Custom post meta)</li>
<li><strong>Support: </strong> We appreciate your support!</li>
</ul>
<p><strong></strong></p>
<?php
$target_url = 'https://checkout.freemius.com/mode/dialog/plugin/925/plan/9473/licenses/1/?billing_cycle=annually';
$lifetime_url = 'https://checkout.freemius.com/mode/dialog/plugin/925/plan/9473/licenses/1/?billing_cycle=lifetime';
if ( $my_current_user->user_email ) {
$target_url = add_query_arg( 'user_email', $my_current_user->user_email, $target_url );
$lifetime_url = add_query_arg( 'user_email', $my_current_user->user_email, $lifetime_url );
}
?>
<a href="<?php
echo esc_url( $target_url );
?>" class="ddpprobutton button button-primary button-hero" target="_blank">Only $29.99 /year - <?php
esc_html_e( 'Click here', 'delete-duplicate-posts' );
?></a>
<p>
<center><em>OR get <a href="<?php
echo esc_url( $lifetime_url );
?>" target="_blank">a lifetime license for only $59.99</a></em> - You can transfer your license to other websites.</center>
</p>
<div class="moneybackguarantee">
<p><strong>Money Back Guarantee!</strong></p>
<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>
</div>
</div><!-- .sidebarrow -->
<?php
}
?>
</div><!-- #dashboard -->
</div>
<div id="log-tab" class="tab-content" style="display: none;">
<div id="log">
<h3><?php
esc_html_e( 'The Log', 'delete-duplicate-posts' );
?></h3>
<div class="spinner is-active"></div>
<ul class="large-text" name="ddp_log" id="ddp_log"></ul>
</div>
<p>
<form method="post" id="ddp_clearlog">
<?php
wp_nonce_field( 'ddp_clearlog_nonce' );
?>
<input class="button-secondary" type="submit" name="ddp_clearlog" value="<?php
esc_html_e( 'Reset log', 'delete-duplicate-posts' );
?>" />
</form>
</p>
</div>
<div id="settings-tab" class="tab-content" style="display: none;">
<div id="ddp-configuration">
<h3><?php
esc_html_e( 'Settings', 'delete-duplicate-posts' );
?></h3>
<p>
<?php
$nextscheduled = wp_next_scheduled( 'ddp_cron' );
if ( $nextscheduled ) {
?>
<div class="notice notice-info is-dismissible">
<h3><span class="dashicons dashicons-saved"></span> <?php
esc_html_e( 'Automatically Deleting Duplicates', 'delete-duplicate-posts' );
?></h3>
<?php
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>';
echo '<p class="center">';
printf(
// translators: Showing when the next check happens and what the current time is
esc_html( __( 'Next automated check %1$s. Current time %2$s', 'delete-duplicate-posts' ) ),
'<strong>' . esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $nextscheduled ) ) . '</strong>',
'<strong>' . esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), time() ) ) . '</strong>'
);
echo '</p>';
?>
</div>
<?php
}
?>
</p>
<form method="post" id="delete_duplicate_posts_options">
<?php
wp_nonce_field( 'ddp-update-options' );
?>
<table width="100%" cellspacing="2" cellpadding="5" class="form-table">
<tr valign="top">
<th><label for="ddp_pts"><?php
esc_html_e( 'Which post types?:', 'delete-duplicate-posts' );
?></label>
</th>
<td>
<?php
$builtin = array('post', 'page', 'attachment');
$args = array(
'public' => true,
'_builtin' => false,
);
$output = 'names';
$operator = 'and';
$post_types = get_post_types( $args, $output, $operator );
$post_types = array_merge( $builtin, $post_types );
$checked_post_types = $options['ddp_pts'];
if ( $post_types ) {
?>
<ul class="radio">
<?php
$step = 0;
if ( !is_array( $checked_post_types ) ) {
$checked_post_types = array();
}
foreach ( $post_types as $pt ) {
$checked = array_search( $pt, $checked_post_types, true );
?>
<li><input type="checkbox" name="ddp_pts[]" id="ddp_pt-<?php
echo esc_attr( $step );
?>" value="<?php
echo esc_html( $pt );
?>"
<?php
if ( false !== $checked ) {
echo ' checked';
}
?>
/>
<label for="ddp_pt-<?php
echo esc_attr( $step );
?>"><?php
echo esc_html( $pt );
?></label>
<?php
// Count for each post type
$postinfo = wp_count_posts( $pt );
$othercount = 0;
foreach ( $postinfo as $pi ) {
$othercount = $othercount + intval( $pi );
}
// translators: Total number of deleted duplicates
echo '<small>' . sprintf( esc_html__( '(%s total found)', 'delete-duplicate-posts' ), esc_html( number_format_i18n( $othercount ) ) ) . '</small>';
?>
</li>
<?php
++$step;
}
?>
</ul>
<?php
}
?>
<p class="description">
<?php
esc_html_e( 'Choose which post types to scan for duplicates.', 'delete-duplicate-posts' );
?>
</p>
</td>
</tr>
<tr>
<th><label for="ddp_pstati"><?php
esc_html_e( 'Post status', 'delete-duplicate-posts' );
?></label>
</th>
<td>
<?php
$stati = array(
'publish' => (object) array(
'label' => 'Published',
'show_in_admin_status_list' => true,
),
);
$checked_post_stati = $options['ddp_pstati'];
if ( $stati ) {
?>
<ul class="checkbox">
<?php
$staticount = count( $stati );
foreach ( $stati as $key => $st ) {
if ( $st->show_in_admin_status_list ) {
$checked = array_search( $key, $checked_post_stati, true );
?>
<li>
<input type="checkbox" name="ddp_pstati[]" id="ddp_pstatus-<?php
echo esc_attr( $key );
?>" value="<?php
echo esc_attr( $key );
?>"
<?php
if ( false !== $checked ) {
echo ' checked';
}
if ( 1 === $staticount ) {
echo ' disabled';
}
?>
/><label for="ddp_pstatus-<?php
echo esc_attr( $key );
?>">
<?php
echo esc_html( $key . ' (' . $st->label . ')' );
if ( 'trash' === $key ) {
echo ' <small>' . esc_html__( 'Warning: Enabling this can give false results. Only enable if you know what you are doing.', 'delete-duplicate-posts' ) . '</small>';
}
?>
</label>
</li>
<?php
++$step;
}
}
?>
</ul>
<?php
}
?>
</td>
</tr>
<?php
$comparemethod = 'titlecompare';
global $ddp_fs;
?>
<tr valign="top">
<th><?php
esc_html_e( 'Comparison Method', 'delete-duplicate-posts' );
?></th>
<td>
<ul class="ddpcomparemethod">
<li>
<label>
<input type="radio" name="ddp_method" value="titlecompare" <?php
checked( 'titlecompare', $comparemethod );
?> />
<?php
esc_html_e( 'Compare by title (default)', 'delete-duplicate-posts' );
?>
<span class="optiondesc"><?php
esc_html_e( 'Looks at the title of the post itself.', 'delete-duplicate-posts' );
?></span>
</label>
</li>
<?php
global $ddp_fs;
?>
</ul>
</td>
</tr>
<tr>
<th><label for="ddp_keep"><?php
esc_html_e( 'Delete which posts?:', 'delete-duplicate-posts' );
?></label></th>
<td>
<select name="ddp_keep" id="ddp_keep">
<option value="oldest"
<?php
if ( 'oldest' === $options['ddp_keep'] ) {
echo 'selected="selected"';
}
?>
><?php
esc_html_e( 'Keep oldest', 'delete-duplicate-posts' );
?></option>
<option value="latest"
<?php
if ( 'latest' === $options['ddp_keep'] ) {
echo 'selected="selected"';
}
?>
><?php
esc_html_e( 'Keep latest', 'delete-duplicate-posts' );
?></option>
</select>
<p class="description">
<?php
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' );
?>
</p>
</td>
</tr>
<?php
?>
<tr>
<td colspan="2">
<hr>
<h3><?php
esc_html_e( 'Delete Duplicates Automatically', 'delete-duplicate-posts' );
?></h3>
</td>
</tr>
<tr valign="top">
<th><?php
esc_html_e( 'Enable automatic deletion?:', 'delete-duplicate-posts' );
?>
</th>
<td><label for="ddp_enabled">
<input type="checkbox" id="ddp_enabled" name="ddp_enabled"
<?php
if ( true === $options['ddp_enabled'] ) {
echo 'checked="checked"';
}
?>
>
<p class="description">
<?php
esc_html_e( 'Clean duplicates automatically.', 'delete-duplicate-posts' );
?></p>
</label>
</td>
</tr>
<tr>
<th><label for="ddp_resultslimit"><?php
esc_html_e( 'How many:', 'delete-duplicate-posts' );
?></label>
</th>
<td>
<?php
$dupe_options = array(
0 => __( 'No limit', 'delete-duplicate-posts' ),
10000 => number_format_i18n( '10000' ),
5000 => number_format_i18n( '5000' ),
2500 => number_format_i18n( '2500' ),
1000 => number_format_i18n( '1000' ),
500 => '500',
250 => '250',
100 => '100',
50 => '50',
10 => '10',
);
?>
<select name="ddp_resultslimit" id="ddp_resultslimit">
<?php
foreach ( $dupe_options as $key => $label ) {
?>
<option value="<?php
echo esc_attr( $key );
?>" <?php
selected( $options['ddp_resultslimit'], $key );
?>>
<?php
echo esc_attr( $label );
?></option>
<?php
}
?>
</select>
<p class="description">
<?php
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' );
?><br>
<strong><?php
esc_html_e( 'This only applies to automatic (CRON) jobs.', 'delete-duplicate-posts' );
?></strong>
</p>
</td>
</tr>
<tr>
<th><label for="ddp_schedule"><?php
esc_html_e( 'How often?:', 'delete-duplicate-posts' );
?></label>
</th>
<td>
<select name="ddp_schedule" id="ddp_schedule">
<?php
$schedules = wp_get_schedules();
if ( $schedules ) {
foreach ( $schedules as $key => $sch ) {
?>
<option value="<?php
echo esc_attr( $key );
?>"
<?php
if ( isset( $options['ddp_schedule'] ) && esc_attr( $key ) === $options['ddp_schedule'] ) {
echo esc_html( 'selected="selected"' );
}
?>
><?php
echo esc_html( $sch['display'] );
?></option>
<?php
}
}
?>
</select>
<p class="description">
<?php
esc_html_e( 'How often should the cron job run?', 'delete-duplicate-posts' );
?></p>
</td>
</tr>
<tr>
<td colspan="2">
<hr>
</td>
</tr>
<tr>
<th><?php
esc_html_e( 'Send status mail?:', 'delete-duplicate-posts' );
?></th>
<td>
<label for="ddp_statusmail">
<input type="checkbox" id="ddp_statusmail" name="ddp_statusmail"
<?php
if ( isset( $options['ddp_statusmail'] ) && true === $options['ddp_statusmail'] ) {
?>
checked="checked"
<?php
}
?>
>
<p class="description">
<?php
esc_html_e( 'Sends a status email if duplicates have been found.', 'delete-duplicate-posts' );
?>
</p>
</label>
</td>
</tr>
<tr>
<th><?php
esc_html_e( 'Email recipient:', 'delete-duplicate-posts' );
?></th>
<td>
<label for="ddp_statusmail_recipient">
<input type="text" class="regular-text" id="ddp_statusmail_recipient" name="ddp_statusmail_recipient" value="<?php
echo esc_html( $options['ddp_statusmail_recipient'] );
?>">
<p class="description">
<?php
esc_html_e( 'Who should get the notification email.', 'delete-duplicate-posts' );
?></p>
</label>
</td>
</tr>
<tr>
<td colspan="2">
<hr>
</td>
</tr>
<tr>
<th><?php
esc_html_e( 'Enable debug logging?:', 'delete-duplicate-posts' );
?></th>
<td>
<label for="ddp_debug">
<input type="checkbox" id="ddp_debug" name="ddp_debug"
<?php
if ( isset( $options['ddp_debug'] ) && true === $options['ddp_debug'] ) {
echo 'checked="checked"';
}
?>
>
<p class="description">
<?php
esc_html_e( 'Should only be enabled if debugging a problem.', 'delete-duplicate-posts' );
?>
</p>
</label>
</td>
</tr>
<th colspan=2><input type="submit" class="button-primary" name="delete_duplicate_posts_save" value="<?php
esc_html_e( 'Save Settings', 'delete-duplicate-posts' );
?>" /></th>
</tr>
</table>
</form>
</div><!-- #configuration -->
</div>
<div id="redirects-tab" class="tab-content<?php
echo ( $ddp_fs->is__premium_only() && $ddp_fs->can_use_premium_code() ? '' : ' pro' );
?>" style="display: none;">
<?php
if ( $ddp_fs->is__premium_only() && $ddp_fs->can_use_premium_code() ) {
?>
<h3><?php
esc_html_e( 'Redirects', 'delete-duplicate-posts' );
?></h3>
<p><?php
esc_html_e( 'This table shows all redirects created by the plugin.', 'delete-duplicate-posts' );
?></p>
<table id="ddp_redirtable" class="wp-list-table widefat fixed striped table-view-list">
<thead>
<tr>
<th><?php
esc_html_e( 'ID', 'delete-duplicate-posts' );
?></th>
<th><?php
esc_html_e( 'From URL', 'delete-duplicate-posts' );
?></th>
<th><?php
esc_html_e( 'Target URL', 'delete-duplicate-posts' );
?></th>
</tr>
</thead>
<tbody>
<!-- DataTables will populate this -->
</tbody>
</table>
<?php
} else {
?>
<h3><?php
esc_html_e( 'Redirects', 'delete-duplicate-posts' );
?></h3>
<p><?php
esc_html_e( 'Redirects are a premium feature. Please upgrade to access this functionality.', 'delete-duplicate-posts' );
?></p>
<?php
}
?>
</div>
</div>
</div>
<?php
include_once 'sidebar.php';
if ( function_exists( 'ddp_fs' ) ) {
global $ddp_fs;
}
?>
</div>
</div>
<script>
jQuery(document).ready(function($) {
const navTabWrapper = $('.nav-tab-wrapper');
const currentTabs = $('.nav-tab-wrapper a');
currentTabs.each(function() {
$(this).on('click', function(e) {
const href = $(this).attr('href');
console.log('Clicked tab href:', href);
try {
if (!href.startsWith('#')) {
e.preventDefault(); // Prevent default tab behavior for full URLs
window.location.href = href; // Load the page to the URL in the same window
} else {
e.preventDefault(); // Prevent default anchor behavior
console.log('Switching to tab:', href);
// Switch as a regular tab for href starting with '#'
currentTabs.removeClass('nav-tab-active');
$(this).addClass('nav-tab-active');
// Hide all tab content
$('.tab-content').hide();
// Show the content for the clicked tab
$(href).show();
}
} catch (error) {
console.error('Error occurred:', error);
}
});
});
// Initially hide all tab content except the active one
$('.tab-content').hide();
$('.nav-tab-active').each(function() {
const activeHref = $(this).attr('href');
$(activeHref).show();
});
});
</script>
<?php
}
}
//End Class
}
if ( class_exists( __NAMESPACE__ . '\\Delete_Duplicate_Posts' ) ) {
$delete_duplicate_posts_var = new Delete_Duplicate_Posts();
}