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-exporter.php
<?php

namespace Export_All_URLs;

defined('ABSPATH') || exit;

require_once plugin_dir_path(__FILE__) . 'constants.php';

/**
 * Output helpers for the three export targets: streamed CSV, streamed JSON,
 * and an in-page HTML table.
 *
 * The streaming helpers expose header senders plus per-row encoders so the
 * orchestrator can echo one row at a time (bounded memory) instead of building
 * the whole file in memory. No fopen/fwrite/fputcsv — the body is echoed.
 */
class EAU_Exporter
{
    /**
     * Neutralize spreadsheet formula injection.
     *
     * A cell beginning with = + - @ (or a leading tab/carriage return) can be
     * read as a formula by Excel / Google Sheets. Prefixing a single quote
     * forces it to be treated as text. Applied to CSV only.
     *
     * @param string $value
     * @return string
     */
    public function neutralize($value)
    {
        $value = (string) $value;
        if ($value === '') {
            return $value;
        }

        $first = substr($value, 0, 1);
        if (in_array($first, array('=', '+', '-', '@', "\t", "\r"), true)) {
            return "'" . $value;
        }

        return $value;
    }

    /**
     * Send the HTTP headers for a file download.
     *
     * @param string $content_type e.g. 'text/csv'.
     * @param string $filename     Base filename (no extension).
     * @param string $ext          Extension to append.
     */
    public function send_download_headers($content_type, $filename, $ext)
    {
        $filename = sanitize_file_name($filename);
        if ($filename === '') {
            $filename = 'export-all-urls';
        }

        // On sites with display_errors on, WordPress or other plugins can emit PHP
        // notices/warnings while we query. Anything already echoed sits in
        // PHP's output buffer and would otherwise be flushed into the file,
        // breaking the JSON parser and littering the CSV. Discard every open
        // buffer right before we send headers, and stop further "doing it wrong"
        // notices from printing into the body for the rest of this request (the
        // download exits immediately afterwards, so nothing legitimate is lost).
        while (ob_get_level() > 0) {
            ob_end_clean();
        }
        // 'doing_it_wrong_trigger_error' exists since WP 6.1; on older cores the
        // filter is simply never applied, so adding it is harmless. __return_false
        // (WP 3.0+) suppresses the underlying trigger_error() call.
        add_filter('doing_it_wrong_trigger_error', '__return_false', 99);

        nocache_headers();
        header('Content-Type: ' . $content_type . '; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $filename . '.' . $ext . '"');
    }

    /**
     * Encode one CSV record (RFC 4180): every field quoted, inner quotes doubled.
     *
     * @param array $fields
     * @return string
     */
    public function csv_line($fields)
    {
        $cells = array();
        foreach ($fields as $field) {
            $value = $this->neutralize($field);
            $cells[] = '"' . str_replace('"', '""', (string) $value) . '"';
        }
        return implode(',', $cells) . "\r\n";
    }

    /**
     * Encode one row as a JSON object keyed by the column labels.
     *
     * @param array $labels
     * @param array $row
     * @return string
     */
    public function json_record($labels, $row)
    {
        $record = array();
        foreach ($labels as $i => $label) {
            $record[(string) $label] = isset($row[$i]) ? $row[$i] : '';
        }
        // json_encode() (PHP 5.2+) is used directly so the plugin supports WordPress 3.6 (wp_json_encode is 4.1+).
        // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- intentional for WordPress 3.6 compatibility.
        return json_encode($record);
    }

    /**
     * Stream an already-materialized set of rows as a CSV or JSON download and exit.
     * Used for the (small, in-memory) snapshot diff export.
     *
     * @param array  $labels   Header labels.
     * @param array  $rows     Array of row arrays.
     * @param string $format   'csv' or 'json'.
     * @param string $filename Base filename (no extension).
     */
    public function stream_rows($labels, $rows, $format, $filename)
    {
        if ('json' === $format) {
            $this->send_download_headers('application/json', $filename, 'json');
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON download body, not HTML.
            echo '[';
            $first = true;
            foreach ($rows as $row) {
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON download body, not HTML.
                echo ($first ? '' : ',') . $this->json_record($labels, $row);
                $first = false;
            }
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON download body, not HTML.
            echo ']';
        } else {
            $this->send_download_headers('text/csv', $filename, 'csv');
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV BOM + header row, not HTML.
            echo "\xEF\xBB\xBF" . $this->csv_line($labels);
            foreach ($rows as $row) {
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download body, not HTML.
                echo $this->csv_line($row);
            }
        }
        exit;
    }

    /**
     * Render the on-page results table with client-side pagination controls.
     *
     * @param array $labels Header labels (already prefixed with the '#' column).
     * @param array $rows   Array of row arrays.
     * @param int   $total  Number of rows being shown.
     */
    public function render_html_table($labels, $rows, $total)
    {
        echo "<h1 align='center' style='padding: 10px 0;'><strong>" . esc_html__('Below is a list of Exported Data:', 'export-all-urls') . '</strong></h1>';
        echo "<h2 align='center' style='font-weight: normal;'>" . esc_html__('Total number of links', 'export-all-urls') . ': <strong>' . esc_html($total) . '</strong>.</h2>';

        // Results-per-page selector (handled in the browser by script.js).
        echo '<div class="eau-results-toolbar">';
        echo '<label class="eau-perpage-label">' . esc_html__('Results per page:', 'export-all-urls') . ' ';
        echo '<select class="eau-perpage">';
        foreach (array('100', '250', '500', '750', '1000') as $size) {
            echo '<option value="' . esc_attr($size) . '"' . selected($size, (string) Constants::DEFAULT_PER_PAGE, false) . '>' . esc_html($size) . '</option>';
        }
        echo '<option value="all">' . esc_html__('All', 'export-all-urls') . '</option>';
        echo '</select></label>';
        echo '</div>';

        echo '<table id="outputData" class="wp-list-table widefat fixed striped">';
        echo '<thead><tr>';
        foreach ($labels as $label) {
            echo '<th>' . esc_html(ucfirst($label)) . '</th>';
        }
        echo '</tr></thead><tbody>';

        $counter = 1;
        foreach ($rows as $row) {
            echo '<tr><td>' . (int) $counter . '</td>';
            foreach ($row as $cell) {
                echo '<td>' . esc_html($cell) . '</td>';
            }
            echo '</tr>';
            $counter++;
        }

        echo '</tbody></table>';

        // Pagination controls — wired up and shown by script.js only when needed.
        echo '<div class="eau-pagination" style="display:none">';
        echo '<button type="button" class="button eau-prev">' . esc_html__('Previous', 'export-all-urls') . '</button>';
        echo '<span class="eau-page-indicator"></span>';
        echo '<button type="button" class="button eau-next">' . esc_html__('Next', 'export-all-urls') . '</button>';
        echo '</div>';
    }
}