Home / Widgets / Huttons News Importer
Duplicate Snippet

Embed Snippet on Your Site

Huttons News Importer

<10
Code Preview
php
<?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

Add a Comment