HEX
Server: Apache
System: Linux web5.visualcom.es 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: ensaco.es (10019)
PHP: 8.3.32
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/ensaco.es/httpdocs/wp-content/plugins/export-all-urls/classes/class-functions.php
<?php

namespace Export_All_URLs;

defined('ABSPATH') || exit;

require_once plugin_dir_path(__FILE__) . 'constants.php';
require_once plugin_dir_path(__FILE__) . 'class-fields.php';
require_once plugin_dir_path(__FILE__) . 'class-multilingual.php';
require_once plugin_dir_path(__FILE__) . 'class-query.php';
require_once plugin_dir_path(__FILE__) . 'class-exporter.php';

/**
 * Orchestrates an export run: query (in batches) -> build rows from the field
 * registry -> stream to the chosen writer.
 *
 * Large sites with limited resources were timing out, so rows are fetched in
 * fixed-size batches and streamed out one at a time. Peak memory stays bounded
 * no matter how many posts match, and term/meta caches are only primed when a
 * field that needs them is actually selected.
 */
class EAU_Functions
{
    /** @var EAU_Fields */
    private $fields;
    /** @var EAU_Exporter */
    private $exporter;
    /** @var EAU_Multilingual */
    private $multilingual;

    public function __construct()
    {
        $this->fields = new EAU_Fields();
        $this->exporter = new EAU_Exporter();
        $this->multilingual = new EAU_Multilingual();
    }

    public function fields()
    {
        return $this->fields;
    }

    public function multilingual()
    {
        return $this->multilingual;
    }

    /**
     * Iterate matched posts in batches, invoking $callback($row) for each row.
     *
     * @param array    $options  Sanitized options.
     * @param callable $callback Receives one row array per matched post.
     * @param int      $max      Optional hard cap on the number of rows produced.
     * @return int Number of rows produced.
     */
    public function each_row($options, $callback, $max = 0)
    {
        $scope = isset($options['lang_scope']) ? $options['lang_scope'] : 'all';
        $this->multilingual->begin_scope($scope);

        $need_terms = $this->needs_terms($options);

        $args_base = EAU_Query::build($options);
        $args_base['no_found_rows'] = true;                 // skip SQL_CALC_FOUND_ROWS (slow on big tables)
        $args_base['update_post_term_cache'] = $need_terms; // only prime caches we actually use
        $args_base['update_post_meta_cache'] = $this->needs_meta($options);

        $lang = $this->multilingual->query_lang($scope);    // Polylang per-query language
        if (null !== $lang) {
            $args_base['lang'] = $lang;
        }

        $batch = Constants::BATCH_SIZE;
        $start = ($options['offset'] === 'all' || $options['offset'] === '') ? 0 : (int) $options['offset'];
        $limit = ($options['post_per_page'] === 'all') ? -1 : (int) $options['post_per_page'];

        $fetched = 0;
        while (true) {
            $this_batch = $batch;
            if ($limit !== -1) {
                $remaining = $limit - $fetched;
                if ($remaining <= 0) {
                    break;
                }
                $this_batch = min($this_batch, $remaining);
            }
            if ($max > 0) {
                $cap_remaining = $max - $fetched;
                if ($cap_remaining <= 0) {
                    break;
                }
                $this_batch = min($this_batch, $cap_remaining);
            }

            $args = $args_base;
            $args['posts_per_page'] = $this_batch;
            $args['offset'] = $start + $fetched;

            $query = new \WP_Query($args);
            if (!$query->have_posts()) {
                wp_reset_postdata();
                break;
            }

            $in_batch = 0;
            while ($query->have_posts()) {
                $query->the_post();
                $post_id = get_the_ID();
                $context = $this->build_context($post_id, $need_terms);

                $row = array();
                foreach ($options['export_fields'] as $key) {
                    $row[] = $this->fields->value($key, $post_id, $options, $context);
                }
                call_user_func($callback, $row);

                $in_batch++;
                $fetched++;
                if ($max > 0 && $fetched >= $max) {
                    break;
                }
            }
            wp_reset_postdata();

            if ($max > 0 && $fetched >= $max) {
                break;
            }
            if ($in_batch < $this_batch) {
                break; // last (partial) batch — nothing more to fetch
            }
        }

        $this->multilingual->end_scope();

        return $fetched;
    }

    /**
     * Stream a CSV/JSON download. Must run before any output (admin-post.php).
     *
     * @param array $options
     */
    public function stream($options)
    {
        if (!$this->has_any($options)) {
            wp_die(
                esc_html__('No result found in that range, please reselect and try again!', 'export-all-urls'),
                esc_html__('Export All URLs', 'export-all-urls'),
                array('back_link' => true)
            );
        }

        $labels = $this->fields->labels_for($options['export_fields']);
        $exporter = $this->exporter;

        if ($options['export_type'] === 'json') {
            $exporter->send_download_headers('application/json', $options['csv_name'], 'json');
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON download body, not HTML.
            echo '[';
            $first = true;
            $this->each_row($options, function ($row) use ($exporter, $labels, &$first) {
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON download body, not HTML.
                echo ($first ? '' : ',') . $exporter->json_record($labels, $row);
                $first = false;
            });
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON download body, not HTML.
            echo ']';
        } else {
            $exporter->send_download_headers('text/csv', $options['csv_name'], 'csv');
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV BOM + header row, not HTML.
            echo "\xEF\xBB\xBF" . $exporter->csv_line($labels);
            $this->each_row($options, function ($row) use ($exporter) {
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download body, not HTML.
                echo $exporter->csv_line($row);
            });
        }

        exit;
    }

    /**
     * Render every matched row inline as an HTML table, paginated in the browser.
     *
     * @param array $options
     */
    public function render_here($options)
    {
        $rows = array();
        $this->each_row($options, function ($row) use (&$rows) {
            $rows[] = $row;
        });

        if (empty($rows)) {
            echo "<div class='notice notice-error' style='width: 93%'>" . esc_html__('No result found in that range, please reselect and try again!', 'export-all-urls') . '</div>';
            return;
        }

        $labels = $this->fields->labels_for($options['export_fields'], true);
        $this->exporter->render_html_table($labels, $rows, count($rows));
    }

    /**
     * Cheap existence check (one ID, no caches) so we can fail before sending
     * download headers when nothing matches.
     */
    public function has_any($options)
    {
        $scope = isset($options['lang_scope']) ? $options['lang_scope'] : 'all';
        $this->multilingual->begin_scope($scope);

        $args = EAU_Query::build($options);
        $args['posts_per_page'] = 1;
        $args['fields'] = 'ids';
        $args['no_found_rows'] = true;
        $args['update_post_term_cache'] = false;
        $args['update_post_meta_cache'] = false;
        $args['offset'] = ($options['offset'] === 'all' || $options['offset'] === '') ? 0 : (int) $options['offset'];

        $lang = $this->multilingual->query_lang($scope);
        if (null !== $lang) {
            $args['lang'] = $lang;
        }

        $query = new \WP_Query($args);
        $has = $query->have_posts();
        wp_reset_postdata();

        $this->multilingual->end_scope();

        return $has;
    }

    /**
     * Whether any selected field needs taxonomy terms.
     */
    private function needs_terms($options)
    {
        foreach (array('categories', 'category_urls', 'tags', 'tag_urls') as $field) {
            if (in_array($field, $options['export_fields'], true)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Whether any selected field needs post meta (the featured image lives in meta).
     */
    private function needs_meta($options)
    {
        return in_array('featured_image', $options['export_fields'], true);
    }

    /**
     * Precompute the per-post data shared by the field callbacks. Taxonomy terms
     * are only fetched when a taxonomy field is selected.
     */
    private function build_context($post_id, $need_terms)
    {
        $terms = array();
        if ($need_terms) {
            $post_type = get_post_type($post_id);
            foreach (get_object_taxonomies($post_type) as $taxonomy) {
                if (strpos($taxonomy, 'cat') !== false || strpos($taxonomy, 'tag') !== false) {
                    $found = get_the_terms($post_id, $taxonomy);
                    if ($found && !is_wp_error($found)) {
                        $terms[$taxonomy] = $found;
                    }
                }
            }
        }

        return array('terms' => $terms, 'ml' => $this->multilingual);
    }
}