| |
| <?php
|
| final class Huttons_News_Importer
|
| {
|
| private const CRON_HOOK = 'huttons_news_hourly';
|
| private const CATALOG_URL = 'https://www.huttonsgroup.com/property-news';
|
| private const MINIMUM_DATE = '2026-01-01';
|
| private const POST_TYPE = 'insight';
|
| private const SOURCE_META = 'source';
|
| private const ARTICLE_ID_META = '_huttons_article_id';
|
| private const LOCK_KEY = 'huttons_news_import_lock';
|
|
|
| public static function init(): void
|
| {
|
| add_action(self::CRON_HOOK, [self::class, 'run']);
|
| add_filter('cron_schedules', [self::class, 'ensure_hourly_schedule']);
|
| add_action('init', [self::class, 'ensure_scheduled'], 100);
|
| }
|
|
|
| public static function ensure_scheduled(): void
|
| {
|
| if (!wp_next_scheduled(self::CRON_HOOK)) {
|
| wp_schedule_event(time() + 60, 'hourly', self::CRON_HOOK);
|
| }
|
| }
|
|
|
| public static function ensure_hourly_schedule(array $schedules): array
|
| {
|
| if (!isset($schedules['hourly'])) {
|
| $schedules['hourly'] = [
|
| 'interval' => HOUR_IN_SECONDS,
|
| 'display' => __('Once Hourly', 'huttons-news-importer'),
|
| ];
|
| }
|
|
|
| return $schedules;
|
| }
|
|
|
| public static function run()
|
| {
|
| if (get_transient(self::LOCK_KEY)) {
|
| return new WP_Error('huttons_import_locked', 'A Huttons import is already running.');
|
| }
|
|
|
| set_transient(self::LOCK_KEY, 1, 15 * MINUTE_IN_SECONDS);
|
|
|
| try {
|
| if (!post_type_exists(self::POST_TYPE)) {
|
| return new WP_Error('huttons_missing_post_type', 'The insight post type does not exist.');
|
| }
|
|
|
| $articles = self::fetch_catalog();
|
| if (is_wp_error($articles)) {
|
| return $articles;
|
| }
|
|
|
| foreach ($articles as $article) {
|
| if (self::already_imported($article)) {
|
| continue;
|
| }
|
|
|
| return self::import_article($article);
|
| }
|
|
|
| return 0;
|
| } finally {
|
| delete_transient(self::LOCK_KEY);
|
| }
|
| }
|
|
|
| private static function fetch_catalog()
|
| {
|
| $response = wp_remote_get(self::CATALOG_URL, self::request_args());
|
| if (is_wp_error($response)) {
|
| return $response;
|
| }
|
|
|
| $status = wp_remote_retrieve_response_code($response);
|
| if ($status !== 200) {
|
| return new WP_Error('huttons_catalog_http_error', 'Huttons catalog returned HTTP ' . $status . '.');
|
| }
|
|
|
| $payload = self::extract_inertia_payload(wp_remote_retrieve_body($response));
|
| if (is_wp_error($payload)) {
|
| return $payload;
|
| }
|
|
|
| $collections = [];
|
| self::find_article_collections($payload, $collections);
|
| if (!$collections) {
|
| return new WP_Error('huttons_no_articles', 'No articles were found in the Huttons page data.');
|
| }
|
|
|
| usort($collections, static function (array $a, array $b): int {
|
| return count($b) <=> count($a);
|
| });
|
|
|
| $articles = [];
|
| foreach ($collections[0] as $raw) {
|
| $url = !empty($raw['link']) ? esc_url_raw($raw['link']) : '';
|
| if (!$url && !empty($raw['slug'])) {
|
| $url = 'https://www.huttonsgroup.com/article/' . sanitize_title($raw['slug']);
|
| }
|
| if (!$url || empty($raw['title'])) {
|
| continue;
|
| }
|
|
|
| $published_date = self::normalise_date($raw['date'] ?? '');
|
| if (!$published_date || $published_date < self::MINIMUM_DATE) {
|
| continue;
|
| }
|
|
|
| $articles[] = [
|
| 'id' => isset($raw['id']) ? absint($raw['id']) : 0,
|
| 'title' => html_entity_decode(wp_strip_all_tags($raw['title']), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
| 'date' => $published_date,
|
| 'url' => $url,
|
| 'image' => !empty($raw['image']) ? esc_url_raw($raw['image']) : '',
|
| 'source' => !empty($raw['source']) ? sanitize_text_field($raw['source']) : 'Huttons Group',
|
| 'category' => !empty($raw['category']) ? sanitize_text_field($raw['category']) : '',
|
| 'slug' => !empty($raw['slug']) ? sanitize_title($raw['slug']) : sanitize_title($raw['title']),
|
| ];
|
| }
|
|
|
| usort($articles, static function (array $a, array $b): int {
|
| return strcmp($b['date'], $a['date']);
|
| });
|
|
|
| return $articles;
|
| }
|
|
|
| private static function import_article(array $article)
|
| {
|
| $body = self::fetch_article_body($article['url']);
|
| if (is_wp_error($body)) {
|
| return $body;
|
| }
|
|
|
| $body = self::remove_content_after_end_marker($body);
|
|
|
| $source_link = sprintf(
|
| '<p><em>Source: <a href="%s" rel="nofollow noopener" target="_blank">Huttons Group</a></em></p>',
|
| esc_url($article['url'])
|
| );
|
|
|
| $post_date = $article['date'] ? $article['date'] . ' 12:00:00' : current_time('mysql');
|
| $post_id = wp_insert_post([
|
| 'post_type' => self::POST_TYPE,
|
| 'post_status' => 'publish',
|
| 'post_title' => $article['title'],
|
| 'post_name' => $article['slug'],
|
| 'post_content' => wp_kses_post($body . "\n" . $source_link),
|
| 'post_excerpt' => '',
|
| 'post_date' => $post_date,
|
| ], true);
|
|
|
| if (is_wp_error($post_id)) {
|
| return $post_id;
|
| }
|
|
|
| update_post_meta($post_id, self::SOURCE_META, $article['url']);
|
| if ($article['id']) {
|
| update_post_meta($post_id, self::ARTICLE_ID_META, $article['id']);
|
| }
|
|
|
| if (!empty($article['image'])) {
|
| self::set_featured_image($post_id, $article['image'], $article['title']);
|
| }
|
|
|
| return $post_id;
|
| }
|
|
|
| private static function remove_content_after_end_marker(string $content): string
|
| {
|
| $position = strpos($content, '- END -');
|
| if ($position !== false) {
|
| $block_start = false;
|
| foreach (['<p', '<div', '<section', '<article', '<h1', '<h2', '<h3', '<h4', '<h5', '<h6'] as $opening_tag) {
|
| $candidate = strripos(substr($content, 0, $position), $opening_tag);
|
| if ($candidate !== false && ($block_start === false || $candidate > $block_start)) {
|
| $block_start = $candidate;
|
| }
|
| }
|
|
|
| $content = substr($content, 0, $block_start !== false ? $block_start : $position);
|
| }
|
|
|
| return force_balance_tags(trim($content));
|
| }
|
|
|
| private static function set_featured_image(int $post_id, string $image_url, string $title): void
|
| {
|
| require_once ABSPATH . 'wp-admin/includes/file.php';
|
| require_once ABSPATH . 'wp-admin/includes/media.php';
|
| require_once ABSPATH . 'wp-admin/includes/image.php';
|
|
|
| if (strpos($image_url, '//') === 0) {
|
| $image_url = 'https:' . $image_url;
|
| } elseif (strpos($image_url, '/') === 0) {
|
| $image_url = 'https://www.huttonsgroup.com' . $image_url;
|
| }
|
|
|
| $temporary_file = download_url($image_url, 30);
|
| if (is_wp_error($temporary_file)) {
|
| return;
|
| }
|
|
|
| $path = (string) wp_parse_url($image_url, PHP_URL_PATH);
|
| $filename = sanitize_file_name(wp_basename($path));
|
| if (!$filename || !pathinfo($filename, PATHINFO_EXTENSION)) {
|
| $filename = sanitize_title($title) . '.jpg';
|
| }
|
|
|
| $file = [
|
| 'name' => $filename,
|
| 'tmp_name' => $temporary_file,
|
| ];
|
| $attachment_id = media_handle_sideload($file, $post_id, $title);
|
|
|
| if (is_wp_error($attachment_id)) {
|
| @unlink($temporary_file);
|
| return;
|
| }
|
|
|
| set_post_thumbnail($post_id, $attachment_id);
|
| update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($title));
|
| }
|
|
|
| private static function fetch_article_body(string $url)
|
| {
|
| $response = wp_remote_get($url, self::request_args());
|
| if (is_wp_error($response)) {
|
| return $response;
|
| }
|
|
|
| $status = wp_remote_retrieve_response_code($response);
|
| if ($status !== 200) {
|
| return new WP_Error('huttons_article_http_error', 'Huttons article returned HTTP ' . $status . '.');
|
| }
|
|
|
| $html = wp_remote_retrieve_body($response);
|
| $payload = self::extract_inertia_payload($html);
|
| if (!is_wp_error($payload)) {
|
| $content = self::find_largest_content($payload);
|
| if ($content) {
|
| return wp_kses_post($content);
|
| }
|
| }
|
|
|
| if (class_exists('DOMDocument')) {
|
| $document = new DOMDocument();
|
| libxml_use_internal_errors(true);
|
| $loaded = $document->loadHTML('<meta charset="utf-8">' . $html);
|
| libxml_clear_errors();
|
| if ($loaded) {
|
| $xpath = new DOMXPath($document);
|
| foreach (['//article', '//main'] as $query) {
|
| $nodes = $xpath->query($query);
|
| if ($nodes && $nodes->length) {
|
| return wp_kses_post(self::inner_html($nodes->item(0)));
|
| }
|
| }
|
| }
|
| }
|
|
|
| return '<p>Read the full article at Huttons Group.</p>';
|
| }
|
|
|
| private static function extract_inertia_payload(string $html)
|
| {
|
| if (!preg_match('/data-page=(?:"([^"]+)"|\'([^\']+)\')/s', $html, $matches)) {
|
| return new WP_Error('huttons_missing_payload', 'The Huttons Inertia page payload was not found.');
|
| }
|
|
|
| $encoded = $matches[1] !== '' ? $matches[1] : $matches[2];
|
| $decoded = html_entity_decode($encoded, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
| $payload = json_decode($decoded, true);
|
|
|
| return is_array($payload)
|
| ? $payload
|
| : new WP_Error('huttons_invalid_payload', 'The Huttons page payload was invalid JSON.');
|
| }
|
|
|
| private static function find_article_collections($value, array &$collections): void
|
| {
|
| if (!is_array($value)) {
|
| return;
|
| }
|
|
|
| if (self::is_list($value)) {
|
| $matches = array_values(array_filter($value, static function ($item): bool {
|
| return is_array($item) && !empty($item['title']) && (!empty($item['link']) || !empty($item['slug']));
|
| }));
|
| if ($matches && count($matches) >= max(1, intdiv(count($value), 2))) {
|
| $collections[] = $matches;
|
| return;
|
| }
|
| }
|
|
|
| foreach ($value as $child) {
|
| self::find_article_collections($child, $collections);
|
| }
|
| }
|
|
|
| private static function find_largest_content($value): string
|
| {
|
| $candidates = [];
|
| self::collect_content($value, $candidates);
|
| if (!$candidates) {
|
| return '';
|
| }
|
| usort($candidates, static function (string $a, string $b): int {
|
| return strlen($b) <=> strlen($a);
|
| });
|
| return $candidates[0];
|
| }
|
|
|
| private static function collect_content($value, array &$candidates): void
|
| {
|
| if (!is_array($value)) {
|
| return;
|
| }
|
| foreach ($value as $key => $child) {
|
| if (is_string($child) && in_array((string) $key, ['content', 'body', 'article_content'], true) && strlen(wp_strip_all_tags($child)) > 100) {
|
| $candidates[] = $child;
|
| } elseif (is_array($child)) {
|
| self::collect_content($child, $candidates);
|
| }
|
| }
|
| }
|
|
|
| private static function already_imported(array $article): bool
|
| {
|
| $existing = get_posts([
|
| 'post_type' => self::POST_TYPE,
|
| 'post_status' => 'any',
|
| 'posts_per_page' => 1,
|
| 'fields' => 'ids',
|
| 'meta_key' => self::SOURCE_META,
|
| 'meta_value' => $article['url'],
|
| 'no_found_rows' => true,
|
| ]);
|
|
|
| if ($existing) {
|
| return true;
|
| }
|
|
|
| return (bool) get_page_by_path($article['slug'], OBJECT, self::POST_TYPE);
|
| }
|
|
|
| private static function normalise_date(string $date): string
|
| {
|
| $parsed = DateTime::createFromFormat('d/m/Y', $date);
|
| return $parsed ? $parsed->format('Y-m-d') : '';
|
| }
|
|
|
| private static function is_list(array $value): bool
|
| {
|
| if ($value === []) {
|
| return true;
|
| }
|
|
|
| return array_keys($value) === range(0, count($value) - 1);
|
| }
|
|
|
| private static function request_args(): array
|
| {
|
| return [
|
| 'timeout' => 30,
|
| 'redirection' => 5,
|
| 'user-agent' => 'EzyHomes-Huttons-News-Importer/1.0; ' . home_url('/'),
|
| 'headers' => ['Accept' => 'text/html,application/xhtml+xml'],
|
| ];
|
| }
|
|
|
| private static function inner_html(DOMNode $node): string
|
| {
|
| $html = '';
|
| foreach ($node->childNodes as $child) {
|
| $html .= $node->ownerDocument->saveHTML($child);
|
| }
|
| return $html;
|
| }
|
| }
|
|
|
| Huttons_News_Importer::init();
|
|
|
| if (defined('WP_CLI') && WP_CLI) {
|
| WP_CLI::add_command('huttons-news import-one', static function (): void {
|
| $result = Huttons_News_Importer::run();
|
| if (is_wp_error($result)) {
|
| WP_CLI::error($result->get_error_message());
|
| } elseif ($result === 0) {
|
| WP_CLI::success('No unimported Huttons articles remain.');
|
| } else {
|
| WP_CLI::success('Imported insight post #' . $result . '.');
|
| }
|
| });
|
| }
|
| |
| |
Comments