WP user -> user_login,
* then export/anonymize plugin log rows associated with that username (via lookup table).
*/
class ABJ_404_Solution_Privacy {
const EXPORTER_ID = 'abj404-solution';
const ERASER_ID = 'abj404-solution';
/** @return void */
public static function init(): void {
// Add privacy policy content in wp-admin.
add_action('admin_init', array(__CLASS__, 'addPrivacyPolicyContent'));
// Register exporter/eraser with WP privacy tools.
add_filter('wp_privacy_personal_data_exporters', array(__CLASS__, 'registerExporter'));
add_filter('wp_privacy_personal_data_erasers', array(__CLASS__, 'registerEraser'));
}
/** @return void */
public static function addPrivacyPolicyContent(): void {
if (!function_exists('wp_add_privacy_policy_content')) {
return;
}
$content = '
' . esc_html__(
'404 Solution logs 404 hits and redirects to help you diagnose broken links and configure redirects.',
'404-solution'
) . '
';
$content .= '' . esc_html__(
'Log entries may include requested URLs, referrers, timestamps, and an IP address. By default, IP addresses are anonymized; you can enable raw IP logging in the plugin settings.',
'404-solution'
) . '
';
$content .= '' . esc_html__(
'You can export or erase personal data via WordPress Tools > Export/Erase Personal Data.',
'404-solution'
) . '
';
wp_add_privacy_policy_content('404 Solution', wp_kses_post($content));
}
/**
* @param array $exporters
* @return array
*/
public static function registerExporter(array $exporters): array {
$exporters[self::EXPORTER_ID] = array(
'exporter_friendly_name' => __('404 Solution Logs', '404-solution'),
'callback' => array(__CLASS__, 'exporter'),
);
return $exporters;
}
/**
* @param array $erasers
* @return array
*/
public static function registerEraser(array $erasers): array {
$erasers[self::ERASER_ID] = array(
'eraser_friendly_name' => __('404 Solution Logs', '404-solution'),
'callback' => array(__CLASS__, 'eraser'),
);
return $erasers;
}
/** @return ABJ_404_Solution_LogsRepository */
private static function resolveLogsRepo() {
if (class_exists('ABJ_404_Solution_ServiceContainer')) {
$svc = ABJ_404_Solution_ServiceContainer::safeGet('logs_repository');
if ($svc instanceof ABJ_404_Solution_LogsRepository) {
return $svc;
}
}
return abj_service('logs_repository');
}
/**
* @param mixed $email_address
* @return string|null
*/
private static function getUsernameFromEmail($email_address) {
if (!function_exists('get_user_by')) {
return null;
}
$email_address = is_string($email_address) ? trim($email_address) : '';
if ($email_address === '') {
return null;
}
$user = ABJ_404_Solution_UserRef::fromWpUser(get_user_by('email', $email_address));
if ($user === null) {
return null;
}
$username = $user->getLogin();
return $username !== '' ? $username : null;
}
/**
* @param string $email_address
* @param int $page
* @return array{data: array, done: bool}
*/
public static function exporter(string $email_address, int $page = 1): array {
$username = self::getUsernameFromEmail($email_address);
if ($username === null) {
return array(
'data' => array(),
'done' => true,
);
}
$dao = self::resolveLogsRepo();
$perPage = 50;
$page = max(1, absint($page));
$rows = method_exists($dao, 'getLogsv2RowsForLookupValue')
? $dao->getLogsv2RowsForLookupValue($username, $page, $perPage)
: array();
$data = array();
foreach ($rows as $row) {
$rowIdRaw = isset($row['id']) ? $row['id'] : null;
$rowId = (is_scalar($rowIdRaw) ? (string)$rowIdRaw : uniqid('', true));
$tsRaw = isset($row['timestamp']) ? $row['timestamp'] : '';
$urlRaw = isset($row['requested_url']) ? $row['requested_url'] : '';
$destRaw = isset($row['dest_url']) ? $row['dest_url'] : '';
$refRaw = isset($row['referrer']) ? $row['referrer'] : '';
$ipRaw = isset($row['user_ip']) ? $row['user_ip'] : '';
$data[] = array(
'group_id' => 'abj404_solution_logs',
'group_label' => __('404 Solution Logs', '404-solution'),
'item_id' => 'abj404_log_' . $rowId,
'data' => array(
array(
'name' => __('Timestamp', '404-solution'),
'value' => is_scalar($tsRaw) ? (string)$tsRaw : '',
),
array(
'name' => __('Requested URL', '404-solution'),
'value' => is_scalar($urlRaw) ? (string)$urlRaw : '',
),
array(
'name' => __('Destination URL', '404-solution'),
'value' => is_scalar($destRaw) ? (string)$destRaw : '',
),
array(
'name' => __('Referrer', '404-solution'),
'value' => is_scalar($refRaw) ? (string)$refRaw : '',
),
array(
'name' => __('IP Address', '404-solution'),
'value' => is_scalar($ipRaw) ? (string)$ipRaw : '',
),
),
);
}
$done = count((array)$rows) < $perPage;
return array(
'data' => $data,
'done' => $done,
);
}
/**
* @param string $email_address
* @param int $page
* @return array{items_removed: bool, items_retained: bool, messages: array, done: bool}
*/
public static function eraser(string $email_address, int $page = 1): array {
$username = self::getUsernameFromEmail($email_address);
if ($username === null) {
return array(
'items_removed' => false,
'items_retained' => false,
'messages' => array(),
'done' => true,
);
}
$dao = self::resolveLogsRepo();
$perPage = 100;
$page = max(1, absint($page));
$ids = method_exists($dao, 'getLogsv2IdsForLookupValue')
? $dao->getLogsv2IdsForLookupValue($username, $page, $perPage)
: array();
if (empty($ids)) {
return array(
'items_removed' => false,
'items_retained' => false,
'messages' => array(),
'done' => true,
);
}
$ok = method_exists($dao, 'anonymizeLogsv2RowsByIds')
? $dao->anonymizeLogsv2RowsByIds($ids)
: false;
$removed = $ok ? true : false;
$retained = $ok ? false : true;
return array(
'items_removed' => $removed,
'items_retained' => $retained,
'messages' => $ok ? array() : array(__('Some items could not be anonymized.', '404-solution')),
'done' => count($ids) < $perPage,
);
}
}