' . esc_html__('Satelo is installed. Join in one click to get articles about your site on similar sites.', 'satelo') . ' ' . esc_html__('Connect now', 'satelo') . '
' . esc_html($m[1]) . '
' . esc_html($label) . '
' . "\n" . $html; } // Category / tags / SEO description. $cat = $o['category']; if (!empty($a['category_id']) && term_exists((int) $a['category_id'], 'category')) { $cat = (int) $a['category_id']; } $tags = array_map('sanitize_text_field', array_slice((array) ($a['tags'] ?? []), 0, 10)); // Status: auto-placement publishes immediately; manual mode leaves a draft for review. $status = $o['auto'] ? 'publish' : 'draft'; if (in_array($prev_status, ['publish', 'future'], true)) { $status = $prev_status; // updating a live article must never take it offline (12-month guarantee) } $date = null; if ($status === 'publish' && $prev_status !== 'publish' && !empty($a['publish_at'])) { $t = strtotime((string) $a['publish_at']); if ($t && $t > time() + 60) { $status = 'future'; $date = gmdate('Y-m-d H:i:s', $t); } } $upd = [ 'ID' => $post_id, 'post_title' => $title, 'post_content' => $html, 'post_status' => $status, ]; if ($date) { $upd['post_date_gmt'] = $date; $upd['post_date'] = get_date_from_gmt($date); } wp_update_post($upd); if ($cat) { wp_set_post_categories($post_id, [$cat]); } if ($tags) { wp_set_post_tags($post_id, $tags); } if (!empty($a['meta_description'])) { $desc = sanitize_text_field($a['meta_description']); update_post_meta($post_id, '_satelo_meta_description', $desc); update_post_meta($post_id, '_yoast_wpseo_metadesc', $desc); // Yoast SEO update_post_meta($post_id, 'rank_math_description', $desc); // Rank Math } return new WP_REST_Response([ 'ok' => true, 'post_id' => $post_id, 'status' => get_post_status($post_id), 'url' => get_permalink($post_id), ]); } public static function rest_unpublish(WP_REST_Request $req) { $a = (array) $req->get_json_params(); $pid = self::find_post(sanitize_text_field($a['id'] ?? '')); if (!$pid) { return new WP_Error('satelo_not_found', 'Article not found', ['status' => 404]); } wp_trash_post($pid); return new WP_REST_Response(['ok' => true, 'post_id' => $pid]); } /* ---------------- helpers ---------------- */ private static function find_post(string $sid): int { if ($sid === '') { return 0; } $q = get_posts(['post_type' => 'post', 'post_status' => 'any', 'meta_key' => '_satelo_id', 'meta_value' => $sid, 'fields' => 'ids', 'numberposts' => 1]); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- single lookup by our unique article id. return $q ? (int) $q[0] : 0; } private static function first_admin(): int { $u = get_users(['role' => 'administrator', 'number' => 1, 'fields' => 'ID']); return $u ? (int) $u[0] : 1; } private static function sideload(string $url, int $post_id, string $alt, string $credit): int { $tmp = download_url($url, 20); if (is_wp_error($tmp)) { return 0; } $type = wp_check_filetype_and_ext($tmp, basename(wp_parse_url($url, PHP_URL_PATH)) ?: 'image.jpg'); $mime = function_exists('mime_content_type') ? (string) mime_content_type($tmp) : (string) ($type['type'] ?? ''); if (strpos($mime, 'image/') !== 0) { wp_delete_file($tmp); return 0; } $ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'][$mime] ?? 'jpg'; $file = ['name' => 'satelo-' . substr(md5($url), 0, 10) . '.' . $ext, 'tmp_name' => $tmp]; $att = media_handle_sideload($file, $post_id, $alt ?: null); if (is_wp_error($att)) { wp_delete_file($tmp); return 0; } if ($alt) { update_post_meta($att, '_wp_attachment_image_alt', $alt); } if ($credit) { wp_update_post(['ID' => $att, 'post_excerpt' => $credit]); // caption = photo credit } return (int) $att; } /** External links get the agreed rel; links to this site stay untouched. */ private static function apply_link_rel(string $html, string $rel): string { if (!class_exists('WP_HTML_Tag_Processor')) { return $html; } $host = wp_parse_url(home_url(), PHP_URL_HOST); $p = new WP_HTML_Tag_Processor($html); while ($p->next_tag('a')) { $href = (string) $p->get_attribute('href'); $h = wp_parse_url($href, PHP_URL_HOST); if (!$h || $h === $host) { continue; } $p->set_attribute('rel', $rel === 'dofollow' ? 'noopener' : $rel . ' noopener'); $p->set_attribute('target', '_blank'); } return $p->get_updated_html(); } /** Signed call from this site to Satelo (handshake, heartbeat, revoke). */ private static function call(string $action, array $data) { $o = self::opt(); if (!$o['site_id'] || !$o['secret']) { return false; } $body = wp_json_encode(['site_id' => $o['site_id'], 'action' => $action, 'data' => $data]); $ts = time(); $res = wp_remote_post(self::API . '/' . $action, [ 'timeout' => 10, 'headers' => [ 'Content-Type' => 'application/json', 'X-Satelo-Site' => $o['site_id'], 'X-Satelo-Timestamp' => (string) $ts, 'X-Satelo-Signature' => hash_hmac('sha256', $ts . '.' . $body, $o['secret']), ], 'body' => $body, ]); return !is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200; } public static function heartbeat(): void { self::call('heartbeat', self::status_payload() + ['pageviews' => self::pageviews()]); } /* ---------------- pageview counter ---------------- */ /** Daily token so the counter only accepts hits from pages of this site (works with page caches for ~3 days). */ private static function hit_token(int $daysAgo = 0): string { return substr(hash_hmac('sha256', gmdate('Y-m-d', time() - $daysAgo * 86400), (string) self::opt('secret')), 0, 16); } public static function beacon(): void { if (!self::opt('connected') || is_admin() || is_preview() || is_feed() || current_user_can('edit_posts')) { return; } $url = esc_url_raw(rest_url(self::NS . '/hit')) . (strpos(rest_url(), '?') === false ? '?' : '&') . 't=' . self::hit_token(); wp_enqueue_script('satelo-beacon', plugins_url('assets/beacon.js', __FILE__), [], self::VERSION, ['in_footer' => true, 'strategy' => 'defer']); wp_add_inline_script('satelo-beacon', 'window.sateloBeacon = ' . wp_json_encode(['url' => $url]) . ';', 'before'); } public static function rest_hit(WP_REST_Request $req): WP_REST_Response { $t = (string) $req->get_param('t'); $ok = $t !== '' && (hash_equals(self::hit_token(0), $t) || hash_equals(self::hit_token(1), $t) || hash_equals(self::hit_token(2), $t)); $ua = (string) $req->get_header('user_agent'); if ($ok && $ua !== '' && !preg_match('/bot|crawl|spider|slurp|preview|headless|monitor|curl|wget|python|http-client|lighthouse/i', $ua)) { $pv = (array) get_option('satelo_pv', []); $day = gmdate('Y-m-d'); $pv[$day] = (int) ($pv[$day] ?? 0) + 1; if (count($pv) > 40) { krsort($pv); $pv = array_slice($pv, 0, 35, true); } update_option('satelo_pv', $pv, false); } return new WP_REST_Response(null, 204); } /** Last 35 days of pageviews: ['YYYY-MM-DD' => count]. */ public static function pageviews(): array { $pv = (array) get_option('satelo_pv', []); krsort($pv); return array_slice($pv, 0, 35, true); } } Satelo_Connector::init();