<?php
if (!defined('ABSPATH')) exit; // Exit if accessed directly

abstract class ExportData
{
    protected $exportTo; // Set in constructor to one of 'browser', 'file', 'string'
    protected $stringData; // stringData so far, used if export string mode
    protected $tempFile; // handle to temp file (for export file mode)
    protected $tempFilename; // temp file name and path (for export file mode)
    public $filename; // file mode: the output file name; browser mode: file name for download; string mode: not used

    public function __construct($exportTo = "browser", $filename = "exportdata")
    {
        if (!in_array($exportTo, array('browser', 'file', 'string'))) {
            throw new Exception(sprintf('%s is not a valid ExportData export type', esc_html($exportTo)));
        }
        $this->exportTo = $exportTo;
        $this->filename = $filename;
    }

    public function initialize()
    {

        switch ($this->exportTo) {
            case 'browser':
                $this->sendHttpHeaders();
                break;
            case 'string':
                $this->stringData = '';
                break;
            case 'file':
                $this->tempFilename = tempnam(sys_get_temp_dir(), 'exportdata');
                $this->tempFile     = fopen($this->tempFilename, "w");
                break;
        }

        $this->write($this->generateHeader());
    }

    public function addRow($row)
    {
        $this->write($this->generateRow($row));
    }

    public function finalize()
    {

        $this->write($this->generateFooter());

        switch ($this->exportTo) {
            case 'browser':
                flush();
                break;
            case 'string':
                // do nothing
                break;
            case 'file':
                // close temp file and move it to correct location
                fclose($this->tempFile);
                rename($this->tempFilename, $this->filename);
                break;
        }
    }

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

    abstract public function sendHttpHeaders();

    protected function write($data)
    {
        switch ($this->exportTo) {
            case 'browser':
                echo $data; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                break;
            case 'string':
                $this->stringData .= $data;
                break;
            case 'file':
                fwrite($this->tempFile, $data);
                break;
        }
    }

    protected function generateHeader()
    {
        // can be overridden by subclass to return any data that goes at the top of the exported file
    }

    protected function generateFooter()
    {
        // can be overridden by subclass to return any data that goes at the bottom of the exported file
    }

    // In subclasses generateRow will take $row array and return string of it formatted for export type
    abstract protected function generateRow($row);

    /**
     * Escapes a single cell value for delimited (CSV/TSV) output.
     *
     * Neutralises spreadsheet formula injection by prefixing a value that
     * begins with a formula trigger (=, +, -, @, tab or carriage return) with a
     * single quote, and applies RFC 4180 quoting (wrap in the enclosure and
     * double any literal enclosure character inside the value).
     *
     * @param mixed  $value     The raw cell value.
     * @param string $enclosure The enclosure character (defaults to a double quote).
     * @return string
     */
    protected function escapeCell($value, $enclosure = '"')
    {
        $value = (string) ($value === null ? '' : $value);

        if ($value !== '' && in_array($value[0], array('=', '+', '-', '@', "\t", "\r"), true)) {
            $value = "'" . $value;
        }

        return $enclosure . str_replace($enclosure, $enclosure . $enclosure, $value) . $enclosure;
    }

}

/**
 * ExportDataTSV - Exports to TSV (tab separated value) format.
 */
class ExportDataTSV extends ExportData
{

    function generateRow($row)
    {
        foreach ($row as $key => $value) {
            // Neutralise formula injection and wrap the value in quotes,
            // doubling any embedded quote (RFC 4180).
            $row[$key] = $this->escapeCell($value);
        }
        return implode("\t", $row) . "\n";
    }

    function sendHttpHeaders()
    {
        header("Content-type: text/tab-separated-values");
        header("Content-Disposition: attachment; filename=" . basename($this->filename));
    }
}

/**
 * ExportDataCSV - Exports to CSV (comma separated value) format.
 */
class ExportDataCSV extends ExportData
{

    function generateRow($row)
    {
        foreach ($row as $key => $value) {
            // Neutralise formula injection and wrap the value in quotes,
            // doubling any embedded quote (RFC 4180).
            $row[$key] = $this->escapeCell($value);
        }
        return implode(",", $row) . "\n";
    }

    function sendHttpHeaders()
    {
        header("Content-type: text/csv");
        header("Content-Disposition: attachment; filename=" . basename($this->filename));
    }
}

/**
 * Class ExportDataExcel
 */
class ExportDataExcel extends ExportData
{

    const XmlHeader = "<?xml version=\"1.0\" encoding=\"%s\"?\>\n<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\" xmlns:x=\"urn:schemas-microsoft-com:office:excel\" xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\" xmlns:html=\"http://www.w3.org/TR/REC-html40\">";
    const XmlFooter = "</Workbook>";

    public $encoding = 'UTF-8'; // encoding type to specify in file.
    // Note that you're on your own for making sure your data is actually encoded to this encoding

    public $title = 'Sheet1'; // title for Worksheet

    function generateHeader()
    {

        // workbook header
        $output = stripslashes(sprintf(self::XmlHeader, $this->encoding)) . "\n";

        // Set up styles
        $output .= "<Styles>\n";
        $output .= "<Style ss:ID=\"sDT\"><NumberFormat ss:Format=\"Short Date\"/></Style>\n";
        $output .= "</Styles>\n";

        // worksheet header
        $output .= sprintf("<Worksheet ss:Name=\"%s\">\n    <Table>\n", htmlentities($this->title));

        return $output;
    }

    function generateFooter()
    {
        $output = '';

        // worksheet footer
        $output .= "    </Table>\n</Worksheet>\n";

        // workbook footer
        $output .= self::XmlFooter;

        return $output;
    }

    function generateRow($row)
    {
        $output = '';
        $output .= "        <Row>\n";
        foreach ($row as $k => $v) {
            $output .= $this->generateCell($v);
        }
        $output .= "        </Row>\n";
        return $output;
    }

    private function generateCell($item)
    {
        $output = '';
        $style  = '';

        // Tell Excel to treat as a number. Note that Excel only stores roughly 15 digits, so keep
        // as text if number is longer than that.
        if (preg_match("/^-?\d+(?:[.,]\d+)?$/", $item) && (strlen($item) < 15)) {
            $type = 'Number';
        }
        // Sniff for valid dates; should look something like 2010-07-14 or 7/14/2010 etc. Can
        // also have an optional time after the date.
        //
        // Note we want to be very strict in what we consider a date. There is the possibility
        // of really screwing up the data if we try to reformat a string that was not actually
        // intended to represent a date.
        elseif (preg_match("/^(\d{1,2}|\d{4})[\/\-]\d{1,2}[\/\-](\d{1,2}|\d{4})([^\d].+)?$/", $item) &&
            ($timestamp = strtotime($item)) &&
            ($timestamp > 0) &&
            ($timestamp < strtotime('+500 years'))) {
            $type  = 'DateTime';
            $item  = strftime("%Y-%m-%dT%H:%M:%S", $timestamp);
            $style = 'sDT'; // defined in header; tells excel to format date for display
        } else {
            $type = 'String';
        }

        $item   = str_replace('&#039;', '&apos;', htmlspecialchars($item, ENT_QUOTES));
        $output .= "            ";
        $output .= $style ? "<Cell ss:StyleID=\"$style\">" : "<Cell>";
        $output .= sprintf("<Data ss:Type=\"%s\">%s</Data>", $type, $item);
        $output .= "</Cell>\n";

        return $output;
    }

    function sendHttpHeaders()
    {
        header("Content-Type: application/vnd.ms-excel; charset=" . $this->encoding);
        header("Content-Disposition: inline; filename=\"" . basename($this->filename) . "\"");
    }
}