From cc7f20b9119a61139b6b3223395162d26802f2f2 Mon Sep 17 00:00:00 2001
From: Jared Hancock <gravydish@gmail.com>
Date: Mon, 26 Mar 2012 16:18:26 -0500
Subject: [PATCH] Add export support for ticket search page

Adds a link to the scp/tickets.php page (Tickets) allowing for the export of
the data to CSV file. This is preliminary and still needs a few items:
  * An icon + button CSS definition
  * Support for alternate columns (staff assigned, etc.)

Also add beginnings of complex export support (PDF, JSON  and other
formats
to follow). CSV is fully supported for now.

And for brevity, add a Http::download method to allow for consistent and
browser-independent forced page content downloads
---
 include/class.export.php      | 135 ++++++++++++++++++++++++++++++++++
 include/class.http.php        |  22 ++++++
 include/staff/tickets.inc.php |   6 +-
 scp/tickets.php               |  10 +++
 4 files changed, 172 insertions(+), 1 deletion(-)
 create mode 100644 include/class.export.php

diff --git a/include/class.export.php b/include/class.export.php
new file mode 100644
index 000000000..7d6a7e7ec
--- /dev/null
+++ b/include/class.export.php
@@ -0,0 +1,135 @@
+<?php
+/*************************************************************************
+    class.export.php
+    
+    Exports stuff (details to follow)
+
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2012 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+class Export {
+    
+    /* static */ function dumpQuery($sql, $headers, $how='csv') {
+        $exporters = array(
+            'csv' => CsvResultsExporter,
+            'json' => JsonResultsExporter
+        );
+        $exp = new $exporters[$how]($sql, $headers);
+        return $exp->dump();
+    }
+
+    # XXX: Think about facilitated exporting. For instance, we might have a
+    #      TicketExporter, which will know how to formulate or lookup a
+    #      formatl query (SQL), and cooperate with the output process to add
+    #      extra (recursive) information. In this funciton, the top-level
+    #      SQL is exported, but for something like tickets, we will need to
+    #      export attached messages, reponses, and notes, as well as
+    #      attachments associated with each, ...
+    /* static */ function dumpTickets($sql, $how='csv') {
+        return self::dumpQuery($sql,
+            array(
+                'ticketID' =>       'Ticket Id',
+                'created' =>        'Date',
+                'subject' =>        'Subject',
+                'name' =>           'From',
+                'priority_desc' =>  'Priority',
+                'dept_name' =>      'Department',
+                'source' =>         'Source',
+                'status' =>         'Current Status'
+            ),
+            $how);
+    }
+
+    /* static */ function saveTickets($sql, $filename, $how='csv') {
+        ob_start();
+        self::dumpTickets($sql, $how);
+        $stuff = ob_get_contents();
+        ob_end_clean();
+        if ($stuff)
+            Http::download($filename, "text/$how", $stuff);
+
+        return false;
+    }
+}
+
+class ResultSetExporter {
+    function ResultSetExporter($sql, $headers) {
+        $this->headers = array_values($headers);
+        $this->_res = db_query($sql);
+        if ($row = db_fetch_array($this->_res)) {
+            $query_fields = array_keys($row);
+            $this->headers = array();
+            $this->keys = array();
+            $this->lookups = array();
+            foreach ($headers as $field=>$name) {
+                if (isset($row[$field])) {
+                    $this->headers[] = $name;
+                    $this->keys[] = $field;
+                    # Remember the location of this header in the query results
+                    # (column-wise) so we don't have to do hashtable lookups for every
+                    # column of every row.
+                    $this->lookups[] = array_search($field, $query_fields);
+                }
+            }
+            db_data_reset($this->_res);
+        }
+    }
+
+    function getHeaders() {
+        return $this->headers;
+    }
+
+    function next() {
+        if (!($row = db_fetch_row($this->_res)))
+            return false;
+
+        $record = array();
+        foreach ($this->lookups as $idx)
+            $record[] = $row[$idx];
+        return $record;
+    }
+
+    function nextArray() {
+        if (!($row = $this->next()))
+            return false;
+        return array_combine($this->keys, $row);
+    }
+
+    function dump() {
+        # Useful for debug output
+        while ($row=$this->nextArray()) {
+            var_dump($row);
+        }
+    }
+}
+
+class CsvResultsExporter extends ResultSetExporter {
+    function dump() {
+        echo '"' . implode('","', $this->getHeaders()) . "\"\n";
+        while ($row=$this->next()) {
+            foreach ($row as &$val)
+                # Escape enclosed double-quotes
+                $val = str_replace('"','""',$val);
+            echo '"' . implode('","', $row) . "\"\n";
+        }
+    }
+}
+
+class JsonResultsExporter extends ResultSetExporter {
+    function dump() {
+        require_once(INCLUDE_DIR.'class.json.php');
+        $exp = new JsonDataEncoder();
+        $rows = array();
+        while ($row=$this->nextArray()) {
+            $rows[] = $row;
+        }
+        echo $exp->encode($rows);
+    }
+}
diff --git a/include/class.http.php b/include/class.http.php
index d134e6b8f..37ad03027 100644
--- a/include/class.http.php
+++ b/include/class.http.php
@@ -50,5 +50,27 @@ class Http {
         }
         exit;
     }
+
+    function download($filename, $type, $data=null) {
+        header('Pragma: public');
+        header('Expires: 0');
+        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+        header('Cache-Control: public');
+        header('Content-Type: '.$type);
+        $user_agent = strtolower ($_SERVER['HTTP_USER_AGENT']);
+        if (strpos($user_agent,'msie') !== false
+                && strpos($user_agent,'win') !== false) {
+            header('Content-Disposition: filename="'.basename($filename).'";');
+        } else {
+            header('Content-Disposition: attachment; filename="'
+                .basename($filename).'"');
+        }
+        header('Content-Transfer-Encoding: binary');
+        if ($data !== null) {
+            header('Content-Length: '.strlen($data));
+            print $data;
+            exit;
+        }
+    }
 }
 ?>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index 3bfc8156a..14d0514ec 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -237,6 +237,8 @@ $qfrom.=' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (ticket.priority_id=pri.pri
 
 $query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 //echo $query;
+$hash = md5($query);
+$_SESSION['search_'.$hash] = $query;
 $res = db_query($query);
 $showing=db_num_rows($res)?$pageNav->showing():"";
 if(!$results_type) {
@@ -486,7 +488,9 @@ $basic_display=!isset($_REQUEST['advance_search'])?true:false;
     </table>
     <?php
     if($num>0){ //if we actually had any tickets returned.
-        echo '<div>&nbsp;Page:'.$pageNav->getPageLinks().'&nbsp;</div>';
+        echo '<div>&nbsp;Page:'.$pageNav->getPageLinks().'&nbsp;';
+        echo '<a class="export-csv" href="?a=export&h='
+            .$hash.'&status='.$_REQUEST['status'] .'">Export</a></div>';
     ?>
         <?php
          if($thisstaff->canManageTickets()) { ?>
diff --git a/scp/tickets.php b/scp/tickets.php
index da9dfa97e..3d271e123 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -460,6 +460,16 @@ if($ticket) {
     $inc = 'tickets.inc.php';
     if($_REQUEST['a']=='open' && $thisstaff->canCreateTickets())
         $inc = 'ticket-open.inc.php';
+    elseif($_REQUEST['a'] == 'export') {
+        require_once(INCLUDE_DIR.'class.export.php');
+        $ts = strftime('%Y%m%d');
+        if (!($token=$_REQUEST['h']))
+            $errors['err'] = 'Query token required';
+        elseif (!($query=$_SESSION['search_'.$token]))
+            $errors['err'] = 'Query token not found';
+        elseif (!Export::saveTickets($query, "tickets-$ts.csv", 'csv'))
+            $errors['err'] = 'Internal error: Unable to dump query results';
+    }
     elseif(!$_POST && $_REQUEST['a']!='search'  && ($min=$thisstaff->getRefreshRate()))
         define('AUTO_REFRESH',1); //set refresh rate if the user has it configured
 }
-- 
GitLab