No visitor values.
*/
public static function browser_keys(): array {
return array(
'session' => 'tagpilot_session',
'rule_prefix' => 'tagpilot_rule_',
'pending_cookie' => 'tagpilot_has_pending',
);
}
/** Clear a consumed former flag cookie without creating tracking state.
*
* @param string $name Current flag-cookie name.
*/
public static function forget_cookie( string $name ): void {
$old = str_replace( 'pixelpair_', 'tagpilot_', $name );
unset( $_COOKIE[ $old ] );
if ( ! headers_sent() ) {
setcookie(
$old,
'',
array(
'expires' => time() - HOUR_IN_SECONDS,
'path' => '/',
'secure' => is_ssl(),
'httponly' => 'pixelpair_pending' === $name,
'samesite' => 'Lax',
)
);
}
}
/** Explain a recoverable table collision without exposing stored data. */
public static function notice(): void {
$screen = get_current_screen();
if ( $screen && in_array( $screen->id, array( 'plugins', 'toplevel_page_pixelpair' ), true ) && current_user_can( 'manage_options' ) ) {
echo '
' . esc_html__( 'PixelPair could not finish its data migration. Both old and new tracking tables may exist, or the database rejected a write. Tracking is paused. Ask your administrator to preserve both copies, resolve the database conflict, then reload this page to retry.', 'pixelpair' ) . '
';
}
}
/** Disable the previous plugin on this site without affecting unrelated sites. */
public static function retire_plugin(): void {
$old = 'tagpilot/tagpilot.php';
$active = (array) get_option( 'active_plugins', array() );
if ( in_array( $old, $active, true ) ) {
update_option( 'active_plugins', array_values( array_diff( $active, array( $old ) ) ) );
}
$network = (array) get_site_option( 'active_sitewide_plugins', array() );
if ( isset( $network[ $old ], $network['pixelpair/pixelpair.php'] ) ) {
unset( $network[ $old ] );
update_site_option( 'active_sitewide_plugins', $network );
}
// WordPress may already have loaded both main files for this request.
global $wp_filter;
foreach ( $wp_filter as $hook => $handler ) {
foreach ( $handler->callbacks as $priority => $callbacks ) {
foreach ( $callbacks as $callback ) {
$fn = $callback['function'];
$class = is_array( $fn ) ? ( is_object( $fn[0] ) ? get_class( $fn[0] ) : $fn[0] ) : '';
$file = $fn instanceof \Closure ? ( new \ReflectionFunction( $fn ) )->getFileName() : '';
if ( 0 === strpos( $class, 'TagPilot\\' ) || ( $file && false !== strpos( wp_normalize_path( $file ), '/tagpilot/' ) ) || 0 === strpos( $hook, 'tagpilot_' ) ) {
remove_filter( $hook, $fn, $priority );
}
}
}
}
}
/** Remove old scheduler entries during deactivation/uninstall. */
public static function clear_legacy_jobs(): void {
foreach ( (array) _get_cron_array() as $hooks ) {
foreach ( array_keys( $hooks ) as $hook ) {
if ( 0 === strpos( $hook, 'tagpilot_' ) ) {
wp_unschedule_hook( $hook );
}
}
}
if ( function_exists( 'as_unschedule_all_actions' ) ) {
as_unschedule_all_actions( 'tagpilot_deliver', null, 'tagpilot' );
}
}
/** Read uninstall retention even if an upgrade has not run.
*
* @return bool Owner's existing retention preference.
*/
public static function retain(): bool {
$settings = get_option( 'pixelpair_settings', get_option( 'tagpilot_settings', array() ) );
return is_array( $settings ) && ! empty( $settings['keep_data_on_uninstall'] );
}
/** Remove only both identities' owned data after retention is checked. */
public static function uninstall(): void {
global $wpdb;
foreach ( array( 'tagpilot', 'pixelpair' ) as $name ) {
foreach ( array( $name . '_', '_transient_' . $name . '_', '_transient_timeout_' . $name . '_' ) as $prefix ) {
$keys = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( $prefix ) . '%' ) );
foreach ( $keys as $key ) {
delete_option( $key );
}
}
foreach ( self::meta_tables() as $type => $spec ) {
list( $table, $id, $owner ) = $spec;
if ( ! self::table_exists( $table ) ) {
continue;
}
do {
$rows = $wpdb->get_results( $wpdb->prepare( 'SELECT %i,%i FROM %i WHERE meta_key LIKE %s LIMIT 100', $id, $owner, $table, $wpdb->esc_like( '_' . $name . '_' ) . '%' ), ARRAY_A );
foreach ( $rows as $row ) {
if ( false === $wpdb->delete( $table, array( $id => $row[ $id ] ) ) ) {
break 2;
}
self::clear_meta_cache( $type, (int) $row[ $owner ] );
}
$count = count( $rows );
} while ( 100 === $count );
}
$tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $wpdb->prefix . $name . '_' ) . '%' ) );
foreach ( $tables as $table ) {
$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table ) );
}
foreach ( array_keys( wp_roles()->roles ) as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
$role->remove_cap( 'manage_' . $name );
}
}
}
}
/** Copy the former identity once per site before services read settings.
*
* @return bool Whether runtime services can safely use the new tables.
*/
public static function identity(): bool {
if ( '1' === get_option( 'pixelpair_identity_version' ) ) {
if ( '1' !== get_option( 'pixelpair_legacy_batch_done' ) ) {
self::schedule_batch();
}
return true;
}
global $wpdb;
$tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $wpdb->prefix . 'tagpilot_' ) . '%' ) );
$legacy = ! empty( $tables ) || defined( 'TAGPILOT_FILE' );
foreach ( $tables as $old ) {
$new = $wpdb->prefix . 'pixelpair_' . substr( $old, strlen( $wpdb->prefix . 'tagpilot_' ) );
if ( self::table_exists( $new ) || false === $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i RENAME TO %i', $old, $new ) ) || ! self::table_exists( $new ) || self::table_exists( $old ) ) {
return false;
}
}
foreach ( array( 'tagpilot_', '_transient_tagpilot_', '_transient_timeout_tagpilot_' ) as $prefix ) {
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT option_name,autoload FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( $prefix ) . '%' ), ARRAY_A );
$legacy = $legacy || ! empty( $rows );
foreach ( $rows as $row ) {
$old = $row['option_name'];
$new = str_replace( 'tagpilot_', 'pixelpair_', $old );
$missing = new \stdClass();
if ( get_option( $new, $missing ) === $missing ) {
$value = get_option( $old );
if ( 'pixelpair_settings' === $new && is_array( $value ) ) {
foreach ( array( 'google_data_layer', 'gtm_data_layer' ) as $field ) {
if ( 'tagpilotDataLayer' === ( $value[ $field ] ?? '' ) ) {
$value[ $field ] = 'pixelpairDataLayer';
}
}
}
if ( in_array( $new, array( 'pixelpair_secrets', 'pixelpair_tiktok_secrets' ), true ) && is_array( $value ) && ! empty( $value['token'] ) ) {
$value['token'] = self::reencrypt( $value['token'] );
}
add_option( $new, $value, '', in_array( $row['autoload'], array( 'yes', 'on', 'auto', 'auto-on' ), true ) );
if ( get_option( $new, $missing ) !== $value ) {
return false;
}
}
if ( ! delete_option( $old ) && false !== get_option( $old ) ) {
return false;
}
}
}
foreach ( array_keys( wp_roles()->roles ) as $name ) {
$role = get_role( $name );
if ( $role && $role->has_cap( 'manage_tagpilot' ) ) {
$role->add_cap( 'manage_pixelpair' );
$role->remove_cap( 'manage_tagpilot' );
}
}
foreach ( (array) _get_cron_array() as $time => $hooks ) {
foreach ( $hooks as $hook => $events ) {
if ( 0 !== strpos( $hook, 'tagpilot_' ) ) {
continue;
}
$new = 'pixelpair_' . substr( $hook, strlen( 'tagpilot_' ) );
foreach ( $events as $event ) {
if ( ! wp_get_scheduled_event( $new, $event['args'], (int) $time ) ) {
$ok = $event['schedule'] ? wp_schedule_event( (int) $time, $event['schedule'], $new, $event['args'] ) : wp_schedule_single_event( (int) $time, $new, $event['args'] );
if ( ! $ok ) {
return false;
}
}
if ( false === wp_unschedule_event( (int) $time, $hook, $event['args'] ) ) {
return false;
}
}
}
}
if ( function_exists( 'as_unschedule_all_actions' ) ) {
as_unschedule_all_actions( 'tagpilot_deliver', null, 'tagpilot' );
}
if ( $legacy ) {
// Deleting the inactive old plugin must not erase metadata awaiting a batch.
update_option(
'tagpilot_settings',
array(
'keep_data_on_uninstall' => true,
'pixelpair_migration_retention' => true,
),
false
);
}
self::schedule_batch();
update_option( 'pixelpair_identity_version', '1', false );
/** Updated option value after the write.
*
* @var mixed $version
*/
$version = get_option( 'pixelpair_identity_version' );
return '1' === $version;
}
/** Decode authenticated old ciphertext without weakening its site binding.
*
* @param string $value Former encrypted value.
* @return string|null Plaintext only when authenticated for this site.
*/
public static function decrypt_legacy( string $value ): ?string {
if ( 0 !== strpos( $value, 'v1:' ) ) {
return null;
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Authenticated ciphertext, never executable input.
$data = base64_decode( substr( $value, 3 ), true );
if ( false === $data || strlen( $data ) < 28 ) {
return null;
}
$context = 'tagpilot:' . get_current_blog_id();
$key = hash( 'sha256', (string) constant( 'AUTH_KEY' ) . (string) constant( 'SECURE_AUTH_SALT' ) . $context, true );
$plain = openssl_decrypt( substr( $data, 28 ), 'aes-256-gcm', $key, OPENSSL_RAW_DATA, substr( $data, 0, 12 ), substr( $data, 12, 16 ), $context );
return false === $plain ? null : $plain;
}
/** Keep unreadable values intact for recovery after a site-key repair.
*
* @param string $value Stored ciphertext.
* @return string Current ciphertext or the unchanged original.
*/
private static function reencrypt( string $value ): string {
$plain = self::decrypt_legacy( $value );
return null === $plain ? $value : Crypto::encrypt( $plain );
}
/** Run shared bookkeeping on the main site, also across multiple networks.
*
* @param callable $operation Option operation.
* @return mixed Operation result, with original blog context restored.
*/
private static function shared( callable $operation ) {
$site = is_multisite() ? (int) get_main_site_id( get_main_network_id() ) : get_current_blog_id();
$result = null;
\PixelPair\Support\Sites::on_site(
$site,
static function () use ( $operation, &$result ): void {
$result = $operation();
}
);
return $result;
}
/** Resume at most 100 rows with a shared lease for the global user-meta table. */
public static function batch(): void {
$key = 'pixelpair_legacy_batch_lock';
$lease = array(
'until' => time() + 300,
'token' => wp_generate_uuid4(),
);
$locked = self::shared(
static function () use ( $key, $lease ): bool {
global $wpdb;
$before = get_option( $key );
if ( is_array( $before ) && ( $before['until'] ?? 0 ) < time() ) {
$wpdb->delete(
$wpdb->options,
array(
'option_name' => $key,
'option_value' => maybe_serialize( $before ),
)
);
wp_cache_delete( $key, 'options' );
}
return add_option( $key, $lease, '', false );
}
);
if ( ! $locked ) {
self::schedule_batch();
return; }
try {
self::batch_rows(); } finally {
self::shared(
static function () use ( $key, $lease ): void {
global $wpdb;
$wpdb->delete(
$wpdb->options,
array(
'option_name' => $key,
'option_value' => maybe_serialize( $lease ),
)
);
wp_cache_delete( $key, 'options' );
}
);
}
}
/** Persist group precedence before changing any metadata.
*
* @param array> $groups Incomplete metadata groups.
* @return bool Whether the write is visible.
*/
private static function save_groups( array $groups ): bool {
return (bool) self::shared(
static function () use ( $groups ): bool {
update_option( 'pixelpair_legacy_meta_groups', $groups, false );
return get_option( 'pixelpair_legacy_meta_groups' ) === $groups;
}
);
}
/** Migrate rows while holding the site-local batch lease. */
private static function batch_rows(): void {
global $wpdb;
if ( ! self::identity() ) {
self::schedule_batch();
return;
}
$remaining = 100;
$groups = (array) self::shared(
static function () {
return get_option( 'pixelpair_legacy_meta_groups', array() );
}
);
foreach ( self::meta_tables() as $type => $spec ) {
list( $table, $id, $owner ) = $spec;
if ( ! self::table_exists( $table ) || $remaining < 1 ) {
continue;
}
$rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE meta_key LIKE %s ORDER BY %i LIMIT %d', $table, $wpdb->esc_like( '_tagpilot_' ) . '%', $id, $remaining ), ARRAY_A );
foreach ( $rows as $row ) {
$new = '_pixelpair_' . substr( $row['meta_key'], strlen( '_tagpilot_' ) );
$group = hash( 'sha256', $table . ':' . $row[ $owner ] . ':' . $new );
if ( ! array_key_exists( $group, $groups ) ) {
$groups[ $group ] = array(
'existing' => (bool) $wpdb->get_var( $wpdb->prepare( 'SELECT %i FROM %i WHERE %i=%d AND meta_key=%s LIMIT 1', $id, $table, $owner, $row[ $owner ], $new ) ),
'last' => (int) $wpdb->get_var( $wpdb->prepare( 'SELECT MAX(%i) FROM %i WHERE %i=%d AND meta_key=%s', $id, $table, $owner, $row[ $owner ], $row['meta_key'] ) ),
);
if ( ! self::save_groups( $groups ) ) {
self::schedule_batch();
return; }
}
// Migrated source rows retain their IDs. A later destination ID is a new write,
// including a concurrent consent withdrawal, and must win atomically.
$cutoff = $groups[ $group ]['existing'] ? 0 : $groups[ $group ]['last'];
if ( ! $groups[ $group ]['existing'] ) {
$value = '_pixelpair_context' === $new ? self::reencrypt( $row['meta_value'] ) : $row['meta_value'];
$wpdb->query(
$wpdb->prepare(
'UPDATE %i AS source LEFT JOIN %i AS target ON target.%i=source.%i AND target.meta_key=%s AND target.%i>%d SET source.meta_key=%s,source.meta_value=%s WHERE source.%i=%d AND source.meta_key=%s AND source.meta_value=%s AND target.%i IS NULL',
$table,
$table,
$owner,
$owner,
$new,
$id,
$cutoff,
$new,
$value,
$id,
$row[ $id ],
$row['meta_key'],
$row['meta_value'],
$id
)
);
}
$wpdb->query(
$wpdb->prepare(
'DELETE source FROM %i AS source INNER JOIN %i AS target ON target.%i=source.%i AND target.meta_key=%s AND target.%i>%d WHERE source.%i=%d AND source.meta_key=%s AND source.meta_value=%s',
$table,
$table,
$owner,
$owner,
$new,
$id,
$cutoff,
$id,
$row[ $id ],
$row['meta_key'],
$row['meta_value']
)
);
self::clear_meta_cache( $type, (int) $row[ $owner ] );
if ( ! $wpdb->get_var( $wpdb->prepare( 'SELECT %i FROM %i WHERE %i=%d AND meta_key=%s LIMIT 1', $id, $table, $owner, $row[ $owner ], $row['meta_key'] ) ) ) {
unset( $groups[ $group ] );
self::save_groups( $groups );
}
--$remaining;
}
}
$table = $wpdb->prefix . 'pixelpair_jobs';
if ( $remaining > 0 && self::table_exists( $table ) ) {
$rows = $wpdb->get_results( $wpdb->prepare( 'SELECT id,payload FROM %i WHERE payload LIKE %s AND id>%d ORDER BY id LIMIT %d', $table, 'v1:%', (int) get_option( 'pixelpair_legacy_queue_cursor', 0 ), $remaining ), ARRAY_A );
foreach ( $rows as $row ) {
$replacement = self::reencrypt( $row['payload'] );
$wpdb->update(
$table,
array( 'payload' => $replacement ),
array(
'id' => $row['id'],
'payload' => $row['payload'],
)
);
$stored = $wpdb->get_var( $wpdb->prepare( 'SELECT payload FROM %i WHERE id=%d', $table, $row['id'] ) );
if ( $replacement !== $row['payload'] && $stored === $row['payload'] ) {
self::schedule_batch();
return; }
update_option( 'pixelpair_legacy_queue_cursor', (int) $row['id'], false );
--$remaining;
}
}
if ( $remaining < 100 ) {
self::schedule_batch();
} else {
update_option( 'pixelpair_legacy_batch_done', '1', false );
$old = get_option( 'tagpilot_settings' );
if ( is_array( $old ) && ! empty( $old['pixelpair_migration_retention'] ) ) {
delete_option( 'tagpilot_settings' );
}
}
}
/** Read current or unprocessed legacy metadata through its native API.
*
* @param string $type post, user, woo or edd.
* @param mixed $entity Object or native ID.
* @param string $key Current plugin key.
* @return mixed Stored value, with destination precedence.
*/
public static function meta( string $type, $entity, string $key ) {
$old = str_replace( '_pixelpair_', '_tagpilot_', $key );
if ( 'woo' === $type ) {
return $entity->get_meta( $entity->meta_exists( $key ) ? $key : $old );
}
if ( 'edd' === $type ) {
$all = edd_get_order_meta( (int) $entity, $key, false );
return $all ? edd_get_order_meta( (int) $entity, $key, true ) : edd_get_order_meta( (int) $entity, $old, true );
}
return get_metadata( $type, (int) $entity, metadata_exists( $type, (int) $entity, $key ) ? $key : $old, true );
}
/** Erase both identities so an unprocessed legacy value cannot reappear.
*
* @param string $type Store provider.
* @param mixed $entity Order or ID.
* @param string $key Current key.
*/
public static function erase_meta( string $type, $entity, string $key ): void {
foreach ( array( $key, str_replace( '_pixelpair_', '_tagpilot_', $key ) ) as $name ) {
if ( 'woo' === $type ) {
$entity->delete_meta_data( $name );
} else {
edd_delete_order_meta( (int) $entity, $name );
}
}
}
/** Move a live cart-session value on access, retaining destination values.
*
* @param object $session Native session service.
* @param string $key Current session key.
* @param mixed $fallback Default for absent keys.
* @return mixed Session value.
*/
public static function session( $session, string $key, $fallback = null ) {
$value = $session->get( $key );
if ( null !== $value && false !== $value ) {
return $value;
}
$old = str_replace( 'pixelpair_', 'tagpilot_', $key );
$value = $session->get( $old );
if ( null !== $value && false !== $value ) {
$session->set( $key, $value );
$session->set( $old, null );
}
return null === $value || false === $value ? $fallback : $value;
}
/** Read continuity cookies without writing before consent is decided.
*
* @param string $name Current cookie name.
* @return string Untrusted bounded value; callers validate its protocol.
*/
public static function cookie( string $name ): string {
$old = str_replace( 'pixelpair_', 'tagpilot_', $name );
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Read-only compatibility; exact protocol validation belongs to each caller.
$value = $_COOKIE[ $name ] ?? $_COOKIE[ $old ] ?? '';
return is_string( $value ) && strlen( $value ) <= 4096 ? wp_unslash( $value ) : '';
}
/** Rewrite only plugin cookies on an already consented uncached collection path. */
public static function cookies(): void {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Names and bounded values are checked below; no platform cookies are changed.
foreach ( $_COOKIE as $old => $value ) {
if ( ! preg_match( '/\A_?tagpilot_[a-z0-9_]+\z/', $old ) || ! is_string( $value ) || strlen( $value ) > 4096 ) {
continue;
}
$new = str_replace( 'tagpilot_', 'pixelpair_', $old );
if ( 'pixelpair_visitor' === $new && ! preg_match( '/\A[a-f0-9]{32}\z/', $value ) ) {
continue;
}
$options = array(
'expires' => time() + 90 * DAY_IN_SECONDS,
'path' => '/',
'secure' => is_ssl(),
'httponly' => ! in_array( $new, array( 'pixelpair_has_pending', 'pixelpair_commerce_consent' ), true ),
'samesite' => 'Lax',
);
if ( ! isset( $_COOKIE[ $new ] ) ) {
$_COOKIE[ $new ] = $value;
if ( ! headers_sent() ) {
setcookie( $new, wp_unslash( $value ), $options );
}
}
unset( $_COOKIE[ $old ] );
$options['expires'] = time() - HOUR_IN_SECONDS;
if ( ! headers_sent() ) {
setcookie( $old, '', $options );
}
}
}
/** Schedule a resumable batch without putting data in cron arguments. */
private static function schedule_batch(): void {
if ( ! wp_next_scheduled( 'pixelpair_legacy_migrate' ) ) {
wp_schedule_single_event( time() + 60, 'pixelpair_legacy_migrate' );
}
}
/** Native metadata storage, including HPOS even when WooCommerce is disabled.
*
* @return array Table, primary key, owner key.
*/
private static function meta_tables(): array {
global $wpdb;
return array(
'post' => array( $wpdb->postmeta, 'meta_id', 'post_id' ),
'user' => array( $wpdb->usermeta, 'umeta_id', 'user_id' ),
'woo' => array( $wpdb->prefix . 'wc_orders_meta', 'id', 'order_id' ),
'edd' => array( $wpdb->prefix . 'edd_ordermeta', 'meta_id', 'edd_order_id' ),
);
}
/** Clear native metadata caches after an atomic key rename.
*
* @param string $type Storage kind.
* @param int $id Native owner ID.
*/
private static function clear_meta_cache( string $type, int $id ): void {
wp_cache_delete( $id, $type . '_meta' );
if ( 'edd' === $type ) {
wp_cache_delete( $id, 'edd_order_meta' );
}
if ( in_array( $type, array( 'woo', 'post' ), true ) && class_exists( '\WC_Cache_Helper' ) ) {
\WC_Cache_Helper::invalidate_cache_group( 'orders' );
\WC_Cache_Helper::invalidate_cache_group( 'order_' . $id );
}
}
/** Check an exact internally generated table name.
*
* @param string $table Owned table name.
* @return bool Whether it exists.
* @phpstan-impure
*/
private static function table_exists( string $table ): bool {
global $wpdb;
return $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) );
}
/** Apply only the schema shipped with this version. */
public static function run(): void {
if ( ! self::identity() ) {
return;
}
self::schedule();
if ( '7' === get_option( 'pixelpair_db_version' ) ) {
return;
}
$stored = get_option( 'pixelpair_settings', array() );
$values = array_replace( Defaults::all(), is_array( $stored ) ? $stored : array() );
foreach ( $values['automatic_events'] as $key => &$event ) {
if ( ! isset( $event['platforms']['tiktok'] ) && isset( Defaults::all()['automatic_events'][ $key ] ) ) {
$event['platforms']['tiktok'] = Defaults::all()['automatic_events'][ $key ]['platforms']['tiktok'];
}
} unset( $event );
update_option( 'pixelpair_settings', $values, true );
\PixelPair\Events\Tables::install();
add_option( 'pixelpair_rules', array(), '', true );
update_option( 'pixelpair_db_version', '7', false );
// Older core releases retain the previous cache location when autoload changes.
wp_cache_delete( 'pixelpair_settings', 'options' );
wp_cache_delete( 'pixelpair_db_version', 'options' );
wp_cache_delete( 'alloptions', 'options' );
}
/** Restore housekeeping after reactivation as well as upgrades. */
private static function schedule(): void {
if ( ! wp_next_scheduled( 'pixelpair_queue_sweep' ) ) {
wp_schedule_event( time() + 60, 'hourly', 'pixelpair_queue_sweep' );
}
if ( ! wp_next_scheduled( 'pixelpair_log_purge' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'pixelpair_log_purge' );
}
}
}