'Apeari', 'callback' => [__CLASS__, 'export']]; return $items; }); add_filter('wp_privacy_personal_data_erasers', static function ($items) { $items['apeari'] = ['eraser_friendly_name' => 'Apeari', 'callback' => [__CLASS__, 'erase']]; return $items; }); add_action('apeari_cleanup', [__CLASS__, 'cleanup']); add_action('deleted_user', [__CLASS__, 'erase_user']); add_action('wpmu_delete_user', [__CLASS__, 'erase_user']); add_action( 'remove_user_from_blog', static function ($user, $site) { APEARI_Sites::run((int) $site, static fn() => self::erase_site((int) $user)); }, 10, 2, ); } public static function cache_key(): string { return 'apeari_cache_' . get_current_blog_id(); } public static function track(string $key): void { $keys = get_user_meta(get_current_user_id(), self::cache_key(), true); $keys = is_array($keys) ? $keys : []; foreach ($keys as $cached => $expires) { if ($expires <= time()) { if (str_starts_with($cached, 'apeari_')) { delete_transient($cached); } unset($keys[$cached]); } } $keys[$key] = time() + 2 * HOUR_IN_SECONDS; if (count($keys) > 256) { $oldest = array_key_first($keys); delete_transient($oldest); unset($keys[$oldest]); } update_user_meta(get_current_user_id(), self::cache_key(), $keys); } public static function policy(): void { if (function_exists('wp_add_privacy_policy_content')) { wp_add_privacy_policy_content( 'Apeari', wp_kses_post( '

' . esc_html__( 'Apeari pozwala administratorowi edytować treść Gutenberga i tworzyć szkice stron lub wpisów. Po włączeniu AI polecenia, ostatnie wiadomości i potrzebny kontekst bloków są przesyłane bezpośrednio do skonfigurowanego OpenAI lub Anthropic albo, po osobnym włączeniu usługi Apeari, do apeari.com i jego dostawcy OpenAI. Apeari otrzymuje adres domeny i opis wybranych bloków; wyniki usługi są przechowywane do 30 dni, niezależnie od lokalnej historii. Wycofanie zgody zatrzymuje kolejne wywołania. Regulamin i powierzenie danych: https://apeari.com/regulamin; prywatność: https://apeari.com/polityka-prywatnosci. Klucze API są szyfrowane w bazie WordPressa. Każda wiadomość rozmowy wygasa po 7 dniach od zapisania; odświeżenie sesji nie przedłuża tego terminu. Sesje szkicu wygasają po 2 godzinach bezczynności, a dane zadań tworzenia treści po 7 dniach od ostatniego kroku lub zmiany stanu. Wygasłe dane nie są udostępniane przez edytor; ich fizyczne usuwanie wykonuje WP-Cron, który wymaga odwiedzin lub systemowego harmonogramu. Zdjęcia wybierane są z istniejącej biblioteki; nie przesyłamy plików zdjęć do AI ani nie wyszukujemy ich w internecie. Wtyczka używa sessionStorage do przywracania sesji i lokalnej preferencji zamknięcia ekranu PRO. Nie stosuje analityki ani śledzenia na apeari.com. Eksport i usuwanie danych obejmują witryny sieci multisite. Właściciel strony powinien określić właściwą podstawę przekazywania danych i uzupełnić informacje o używanym dostawcy.', 'apeari', ) . '

' . '

' . esc_html__('Prywatność OpenAI', 'apeari') . ' · ' . esc_html__('Prywatność Anthropic', 'apeari') . '

', ), ); } } private static function site_page(int $page): array { if (!is_multisite()) { return ['site' => $page === 1 ? get_current_blog_id() : null, 'done' => true]; } $ids = get_sites([ 'fields' => 'ids', 'number' => 2, 'offset' => max(0, $page - 1), 'orderby' => 'id', 'order' => 'ASC', ]); return ['site' => isset($ids[0]) ? (int) $ids[0] : null, 'done' => count($ids) < 2]; } public static function export(string $email, int $page = 1): array { $user = get_user_by('email', $email); $cursor = self::site_page(max(1, $page)); $items = $user && $cursor['site'] ? APEARI_Sites::run($cursor['site'], static fn() => self::export_site($user->ID)) : []; return ['data' => $items, 'done' => !$user || $cursor['done']]; } private static function export_site(int $user): array { $items = []; $site = get_current_blog_id(); $add = static function (string $id, string $label, $data) use (&$items, $site) { $items[] = [ 'group_id' => 'apeari', 'group_label' => 'Apeari', 'item_id' => 'site-' . $site . '-' . $id, 'data' => [ ['name' => __('Witryna', 'apeari'), 'value' => home_url('/')], ['name' => $label, 'value' => wp_json_encode($data, JSON_UNESCAPED_UNICODE)], ], ]; }; $archive = get_user_meta($user, APEARI_Conversations::key(), true); $threads = APEARI_Conversations::prune(is_array($archive) ? $archive['conversations'] ?? [] : []); if ($threads) { $add('conversations', __('Rozmowy', 'apeari'), $threads); } $job = get_option('apeari_content_job_' . $user); if ( is_array($job) && (int) ($job['owner'] ?? 0) === $user && ($job['updatedAt'] ?? 0) > time() - 7 * DAY_IN_SECONDS ) { $add('content-job', __('Zadanie tworzenia treści', 'apeari'), $job); } $keys = get_user_meta($user, self::cache_key(), true); foreach (is_array($keys) ? $keys : [] as $key => $expires) { if (!str_starts_with($key, 'apeari_session_') || $expires <= time()) { continue; } $session = get_transient($key); if (is_array($session) && (int) ($session['user'] ?? 0) === $user && !empty($session['drafts'])) { $add($key, __('Prywatne szkice', 'apeari'), $session['drafts']); } } $intro = get_user_meta($user, 'apeari_pro_intro_dismissed_' . $site, true); if ($intro) { $add('pro-intro', __('Zamknięcie ekranu PRO', 'apeari'), true); } if (get_user_meta($user, 'apeari_editor_tour_seen_' . $site, true)) { $add('editor-tour', __('Wyświetlenie poradnika edytora', 'apeari'), true); } return $items; } public static function erase(string $email, int $page = 1): array { $user = get_user_by('email', $email); $cursor = self::site_page(max(1, $page)); $removed = $user && $cursor['site'] ? APEARI_Sites::run($cursor['site'], static fn() => self::erase_site($user->ID)) : false; return [ 'items_removed' => $removed, 'items_retained' => false, 'messages' => [], 'done' => !$user || $cursor['done'], ]; } public static function erase_user(int $user): void { foreach (APEARI_Sites::ids() as $site) { APEARI_Sites::run($site, static fn() => self::erase_site($user)); } } private static function erase_site(int $user): bool { $removed = delete_user_meta($user, APEARI_Conversations::key()); return self::erase_sessions($user) || $removed; } public static function erase_sessions(int $user): bool { global $wpdb; $removed = false; $keys = get_user_meta($user, self::cache_key(), true); foreach (is_array($keys) ? $keys : [] as $key => $expires) { if (str_starts_with($key, 'apeari_')) { delete_transient($key); $removed = true; } } $removed = delete_user_meta($user, self::cache_key()) || $removed; $removed = delete_user_meta($user, 'apeari_pro_intro_dismissed_' . get_current_blog_id()) || $removed; $removed = delete_user_meta($user, 'apeari_editor_tour_seen_' . get_current_blog_id()) || $removed; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT option_name,option_value FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like('_transient_apeari_') . '%', ), ); foreach ($rows as $row) { $data = maybe_unserialize($row->option_value); if (is_array($data) && (int) ($data['user'] ?? 0) === $user) { delete_transient(substr($row->option_name, strlen('_transient_'))); $removed = true; } } return delete_option('apeari_content_job_' . $user) || $removed; } public static function cleanup(): void { $proof = get_option(APEARI_Ownership::OPTION); if (is_array($proof) && ($proof['expires'] ?? 0) <= time()) { delete_option(APEARI_Ownership::OPTION); delete_option('apeari_secret_ownership'); } global $wpdb; $indexes = $wpdb->get_results( $wpdb->prepare( "SELECT user_id,meta_value FROM {$wpdb->usermeta} WHERE meta_key = %s", self::cache_key(), ), ); foreach ($indexes as $index) { $values = maybe_unserialize($index->meta_value); $values = is_array($values) ? $values : []; foreach ($values as $key => $expires) { if (!str_starts_with($key, 'apeari_')) { unset($values[$key]); continue; } if ($expires <= time()) { delete_transient($key); unset($values[$key]); } elseif (str_starts_with($key, 'apeari_session_')) { $session = get_transient($key); if (is_array($session)) { set_transient($key, APEARI_Workspace::normalize($session), max(1, $expires - time())); } } } if ($values) { update_user_meta((int) $index->user_id, self::cache_key(), $values); } else { delete_user_meta((int) $index->user_id, self::cache_key()); } } $rows = $wpdb->get_results( $wpdb->prepare( "SELECT user_id,meta_value FROM {$wpdb->usermeta} WHERE meta_key = %s", APEARI_Conversations::key(), ), ); foreach ($rows as $row) { $data = maybe_unserialize($row->meta_value); $threads = APEARI_Conversations::prune(is_array($data) ? $data['conversations'] ?? [] : []); if (!$threads) { delete_user_meta((int) $row->user_id, APEARI_Conversations::key()); } else { $data['conversations'] = $threads; update_user_meta((int) $row->user_id, APEARI_Conversations::key(), wp_slash($data)); } } $jobs = $wpdb->get_results( $wpdb->prepare( "SELECT option_name,option_value FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like('apeari_content_job_') . '%', ), ); foreach ($jobs as $job) { $data = maybe_unserialize($job->option_value); if ( !is_array($data) || !get_userdata((int) ($data['owner'] ?? 0)) || ($data['updatedAt'] ?? 0) <= time() - 7 * DAY_IN_SECONDS ) { delete_option($job->option_name); } } $expired = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s AND CAST(option_value AS UNSIGNED) <= %d", $wpdb->esc_like('_transient_timeout_apeari_') . '%', time(), ), ); foreach ($expired as $name) { delete_transient(substr($name, strlen('_transient_timeout_'))); } } }