1<?php
2/**
3 *
4 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
5 * @author     Andreas Gohr <andi@splitbrain.org>
6 */
7
8/**
9 * Class syntax_plugin_data_table
10 */
11class syntax_plugin_data_table extends DokuWiki_Syntax_Plugin {
12
13    /**
14     * will hold the data helper plugin
15     *
16     * @var $dthlp helper_plugin_data
17     */
18    var $dthlp = null;
19
20    var $sums = array();
21
22    /**
23     * Constructor. Load helper plugin
24     */
25    function __construct() {
26        $this->dthlp = plugin_load('helper', 'data');
27    }
28
29    /**
30     * What kind of syntax are we?
31     */
32    function getType() {
33        return 'substition';
34    }
35
36    /**
37     * What about paragraphs?
38     */
39    function getPType() {
40        return 'block';
41    }
42
43    /**
44     * Where to sort in?
45     */
46    function getSort() {
47        return 155;
48    }
49
50    /**
51     * Connect pattern to lexer
52     */
53    function connectTo($mode) {
54        $this->Lexer->addSpecialPattern('----+ *datatable(?: [ a-zA-Z0-9_]*)?-+\n.*?\n----+', $mode, 'plugin_data_table');
55    }
56
57    /**
58     * Handle the match - parse the data
59     *
60     * This parsing is shared between the multiple different output/control
61     * syntaxes
62     *
63     * @param   string       $match   The text matched by the patterns
64     * @param   int          $state   The lexer state for the match
65     * @param   int          $pos     The character position of the matched text
66     * @param   Doku_Handler $handler The Doku_Handler object
67     * @return  bool|array Return an array with all data you want to use in render, false don't add an instruction
68     */
69    function handle($match, $state, $pos, Doku_Handler $handler) {
70        if(!$this->dthlp->ready()) return null;
71
72        // get lines and additional class
73        $lines = explode("\n", $match);
74        array_pop($lines);
75        $class = array_shift($lines);
76        $class = preg_replace('/^----+ *data[a-z]+/', '', $class);
77        $class = trim($class, '- ');
78
79        $data = array(
80            'classes'       => $class,
81            'limit'         => 0,
82            'dynfilters'    => false,
83            'summarize'     => false,
84            'rownumbers'    => (bool) $this->getConf('rownumbers'),
85            'sepbyheaders'  => false,
86            'headers'       => array(),
87            'widths'        => array(),
88            'filter'        => array()
89        );
90
91        // parse info
92        foreach($lines as $line) {
93            // ignore comments
94            $line = preg_replace('/(?<![&\\\\])#.*$/', '', $line);
95            $line = str_replace('\\#', '#', $line);
96            $line = trim($line);
97            if(empty($line)) continue;
98            $line = preg_split('/\s*:\s*/', $line, 2);
99            $line[0] = strtolower($line[0]);
100
101            $logic = 'OR';
102            // handle line commands (we allow various aliases here)
103            switch($line[0]) {
104                case 'select':
105                case 'cols':
106                case 'field':
107                case 'col':
108                    $cols = explode(',', $line[1]);
109                    foreach($cols as $col) {
110                        $col = trim($col);
111                        if(!$col) continue;
112                        $column = $this->dthlp->_column($col);
113                        $data['cols'][$column['key']] = $column;
114                    }
115                    break;
116                case 'title':
117                    $data['title'] = $line[1];
118                    break;
119                case 'head':
120                case 'header':
121                case 'headers':
122                    $cols = $this->parseValues($line[1]);
123                    $data['headers'] = array_merge($data['headers'], $cols);
124                    break;
125                case 'align':
126                    $cols = explode(',', $line[1]);
127                    foreach($cols as $col) {
128                        $col = trim(strtolower($col));
129                        if($col[0] == 'c') {
130                            $col = 'center';
131                        } elseif($col[0] == 'r') {
132                            $col = 'right';
133                        } else {
134                            $col = 'left';
135                        }
136                        $data['align'][] = $col;
137                    }
138                    break;
139                case 'widths':
140                    $cols = explode(',', $line[1]);
141                    foreach($cols as $col) {
142                        $col = trim($col);
143                        $data['widths'][] = $col;
144                    }
145                    break;
146                case 'min':
147                    $data['min'] = abs((int) $line[1]);
148                    break;
149                case 'limit':
150                case 'max':
151                    $data['limit'] = abs((int) $line[1]);
152                    break;
153                case 'order':
154                case 'sort':
155                    $column = $this->dthlp->_column($line[1]);
156                    $sort = $column['key'];
157                    if(substr($sort, 0, 1) == '^') {
158                        $data['sort'] = array(substr($sort, 1), 'DESC');
159                    } else {
160                        $data['sort'] = array($sort, 'ASC');
161                    }
162                    break;
163                case 'where':
164                case 'filter':
165                case 'filterand':
166                    /** @noinspection PhpMissingBreakStatementInspection */
167                case 'and':
168                    $logic = 'AND';
169                case 'filteror':
170                case 'or':
171                    if(!$logic) {
172                        $logic = 'OR';
173                    }
174                    $flt = $this->dthlp->_parse_filter($line[1]);
175                    if(is_array($flt)) {
176                        $flt['logic'] = $logic;
177                        $data['filter'][] = $flt;
178                    }
179                    break;
180                case 'page':
181                case 'target':
182                    $data['page'] = cleanID($line[1]);
183                    break;
184                case 'dynfilters':
185                    $data['dynfilters'] = (bool) $line[1];
186                    break;
187                case 'rownumbers':
188                    $data['rownumbers'] = (bool) $line[1];
189                    break;
190                case 'summarize':
191                    $data['summarize'] = (bool) $line[1];
192                    break;
193                case 'sepbyheaders':
194                    $data['sepbyheaders'] = (bool) $line[1];
195                    break;
196                default:
197                    msg("data plugin: unknown option '" . hsc($line[0]) . "'", -1);
198            }
199        }
200
201        // we need at least one column to display
202        if(!is_array($data['cols']) || !count($data['cols'])) {
203            msg('data plugin: no columns selected', -1);
204            return null;
205        }
206
207        // fill up headers with field names if necessary
208        $data['headers'] = (array) $data['headers'];
209        $cnth = count($data['headers']);
210        $cntf = count($data['cols']);
211        for($i = $cnth; $i < $cntf; $i++) {
212            $column = array_slice($data['cols'], $i, 1);
213            $columnprops = array_pop($column);
214            $data['headers'][] = $columnprops['title'];
215        }
216
217        $data['sql'] = $this->_buildSQL($data);
218
219        // Save current request params for comparison in updateSQL
220        $data['cur_param'] = $this->dthlp->_get_current_param(false);
221        return $data;
222    }
223
224    protected $before_item = '<tr>';
225    protected $after_item  = '</tr>';
226    protected $before_val  = '<td %s>';
227    protected $after_val   = '</td>';
228
229    /**
230     * Handles the actual output creation.
231     *
232     * @param   string        $format output format being rendered
233     * @param   Doku_Renderer $R      the current renderer object
234     * @param   array         $data   data created by handler()
235     * @return  boolean               rendered correctly? (however, returned value is not used at the moment)
236     */
237    function render($format, Doku_Renderer $R, $data) {
238        if($format != 'xhtml') return false;
239        /** @var Doku_Renderer_xhtml $R */
240
241        if(is_null($data)) return false;
242        if(!$this->dthlp->ready()) return false;
243        $sqlite = $this->dthlp->_getDB();
244        if(!$sqlite) return false;
245
246        $R->info['cache'] = false;
247
248        //reset counters
249        $this->sums = array();
250
251        if($this->hasRequestFilter() OR isset($_REQUEST['dataofs'])) {
252            $this->updateSQLwithQuery($data); // handles request params
253        }
254        $this->dthlp->_replacePlaceholdersInSQL($data);
255
256        // run query
257        $clist = array_keys($data['cols']);
258        $res = $sqlite->query($data['sql']);
259
260        $rows = $sqlite->res2arr($res);
261        $cnt = count($rows);
262
263        if($cnt === 0) {
264            $this->nullList($data, $clist, $R);
265            return true;
266        }
267
268        if($data['limit'] && $cnt > $data['limit']) {
269            $rows = array_slice($rows, 0, $data['limit']);
270        }
271
272        //build classnames per column
273        $classes = array();
274        $class_names_cache = array();
275        $offset = 0;
276        if($data['rownumbers']) {
277            $offset = 1; //rownumbers are in first column
278            $classes[] = $data['align'][0] . 'align rownumbers';
279        }
280        foreach($clist as $index => $col) {
281            $class = $data['align'][$index + $offset] . 'align';
282            $class .= ' ' . hsc(sectionID($col, $class_names_cache));
283            $classes[] = $class;
284        }
285
286        //start table/list
287        $R->doc .= $this->preList($clist, $data);
288
289        foreach($rows as $rownum => $row) {
290            // build data rows
291            $R->doc .= $this->before_item;
292
293            if($data['rownumbers']) {
294                $R->doc .= sprintf($this->before_val, 'class="' . $classes[0] . '"');
295                $R->doc .= $rownum + 1;
296                $R->doc .= $this->after_val;
297            }
298
299            foreach(array_values($row) as $num => $cval) {
300                $num_rn = $num + $offset;
301
302                $R->doc .= sprintf($this->beforeVal($data, $num_rn), 'class="' . $classes[$num_rn] . '"');
303                $R->doc .= $this->dthlp->_formatData(
304                    $data['cols'][$clist[$num]],
305                    $cval, $R
306                );
307                $R->doc .= $this->afterVal($data, $num_rn);
308
309                // clean currency symbols
310                $nval = str_replace('$€₤', '', $cval);
311                $nval = str_replace('/ [A-Z]{0,3}$/', '', $nval);
312                $nval = str_replace(',', '.', $nval);
313                $nval = trim($nval);
314
315                // summarize
316                if($data['summarize'] && is_numeric($nval)) {
317                    if(!isset($this->sums[$num])) {
318                        $this->sums[$num] = 0;
319                    }
320                    $this->sums[$num] += $nval;
321                }
322
323            }
324            $R->doc .= $this->after_item;
325        }
326        $R->doc .= $this->postList($data, $cnt);
327
328        return true;
329    }
330
331    /**
332     * Before value in table cell
333     *
334     * @param array $data  instructions by handler
335     * @param int   $colno column number
336     * @return string
337     */
338    protected function beforeVal(&$data, $colno) {
339        return $this->before_val;
340    }
341
342    /**
343     * After value in table cell
344     *
345     * @param array $data
346     * @param int   $colno
347     * @return string
348     */
349    protected function afterVal(&$data, $colno) {
350        return $this->after_val;
351    }
352
353    /**
354     * Create table header
355     *
356     * @param array $clist keys of the columns
357     * @param array $data  instruction by handler
358     * @return string html of table header
359     */
360    function preList($clist, $data) {
361        global $ID;
362        global $conf;
363
364        // Save current request params to not loose them
365        $cur_params = $this->dthlp->_get_current_param();
366
367        //show active filters
368        $text = '<div class="table dataaggregation">';
369        if(isset($_REQUEST['dataflt'])) {
370            $filters = $this->dthlp->_get_filters();
371            $fltrs = array();
372            foreach($filters as $filter) {
373                if(strpos($filter['compare'], 'LIKE') !== false) {
374                    if(strpos($filter['compare'], 'NOT') !== false) {
375                        $comparator_value = '!~' . str_replace('%', '*', $filter['value']);
376                    } else {
377                        $comparator_value = '*~' . str_replace('%', '', $filter['value']);
378                    }
379                    $fltrs[] = $filter['key'] . $comparator_value;
380                } else {
381                    $fltrs[] = $filter['key'] . $filter['compare'] . $filter['value'];
382                }
383            }
384
385            $text .= '<div class="filter">';
386            $text .= '<h4>' . sprintf($this->getLang('tablefilteredby'), hsc(implode(' & ', $fltrs))) . '</h4>';
387            $text .= '<div class="resetfilter">' .
388                '<a href="' . wl($ID) . '">' . $this->getLang('tableresetfilter') . '</a>' .
389                '</div>';
390            $text .= '</div>';
391        }
392        // build table
393        $text .= '<table class="inline dataplugin_table ' . $data['classes'] . '">';
394        // build column headers
395        $text .= '<tr>';
396
397        if($data['rownumbers']) {
398            $text .= '<th>#</th>';
399        }
400
401        foreach($data['headers'] as $num => $head) {
402            $ckey = $clist[$num];
403
404            $width = '';
405            if(isset($data['widths'][$num]) AND $data['widths'][$num] != '-') {
406                $width = ' style="width: ' . $data['widths'][$num] . ';"';
407            }
408            $text .= '<th' . $width . '>';
409
410            // add sort arrow
411            if(isset($data['sort']) && $ckey == $data['sort'][0]) {
412                if($data['sort'][1] == 'ASC') {
413                    $text .= '<span>&darr;</span> ';
414                    $ckey = '^' . $ckey;
415                } else {
416                    $text .= '<span>&uarr;</span> ';
417                }
418            }
419
420            // Clickable header for dynamic sorting
421            $text .= '<a href="' . wl($ID, array('datasrt' => $ckey) + $cur_params) .
422                '" title="' . $this->getLang('sort') . '">' . hsc($head) . '</a>';
423            $text .= '</th>';
424        }
425        $text .= '</tr>';
426
427        // Dynamic filters
428        if($data['dynfilters']) {
429            $text .= '<tr class="dataflt">';
430
431            if($data['rownumbers']) {
432                $text .= '<th></th>';
433            }
434
435            foreach($data['headers'] as $num => $head) {
436                $text .= '<th>';
437                $form = new Doku_Form(array('method' => 'GET'));
438                $form->_hidden = array();
439                if(!$conf['userewrite']) {
440                    $form->addHidden('id', $ID);
441                }
442
443                $key = 'dataflt[' . $data['cols'][$clist[$num]]['colname'] . '*~' . ']';
444                $val = isset($cur_params[$key]) ? $cur_params[$key] : '';
445
446                // Add current request params
447                foreach($cur_params as $c_key => $c_val) {
448                    if($c_val !== '' && $c_key !== $key) {
449                        $form->addHidden($c_key, $c_val);
450                    }
451                }
452
453                $form->addElement(form_makeField('text', $key, $val, ''));
454                $text .= $form->getForm();
455                $text .= '</th>';
456            }
457            $text .= '</tr>';
458        }
459
460        return $text;
461    }
462
463    /**
464     * Create an empty table
465     *
466     * @param array         $data  instruction by handler()
467     * @param array         $clist keys of the columns
468     * @param Doku_Renderer $R
469     */
470    function nullList($data, $clist, $R) {
471        $R->doc .= $this->preList($clist, $data);
472        $R->tablerow_open();
473        $R->tablecell_open(count($clist), 'center');
474        $R->cdata($this->getLang('none'));
475        $R->tablecell_close();
476        $R->tablerow_close();
477        $R->doc .= '</table></div>';
478    }
479
480    /**
481     * Create table footer
482     *
483     * @param array $data   instruction by handler()
484     * @param int   $rowcnt number of rows
485     * @return string html of table footer
486     */
487    function postList($data, $rowcnt) {
488        global $ID;
489        $text = '';
490        // if summarize was set, add sums
491        if($data['summarize']) {
492            $text .= '<tr>';
493            $len = count($data['cols']);
494
495            if($data['rownumbers']) $text .= '<td></td>';
496
497            for($i = 0; $i < $len; $i++) {
498                $text .= '<td class="' . $data['align'][$i] . 'align">';
499                if(!empty($this->sums[$i])) {
500                    $text .= '∑ ' . $this->sums[$i];
501                } else {
502                    $text .= '&nbsp;';
503                }
504                $text .= '</td>';
505            }
506            $text .= '<tr>';
507        }
508
509        // if limit was set, add control
510        if($data['limit']) {
511            $text .= '<tr><th colspan="' . (count($data['cols']) + ($data['rownumbers'] ? 1 : 0)) . '">';
512            $offset = (int) $_REQUEST['dataofs'];
513            if($offset) {
514                $prev = $offset - $data['limit'];
515                if($prev < 0) {
516                    $prev = 0;
517                }
518
519                // keep url params
520                $params = $this->dthlp->_a2ua('dataflt', $_REQUEST['dataflt']);
521                if(isset($_REQUEST['datasrt'])) {
522                    $params['datasrt'] = $_REQUEST['datasrt'];
523                }
524                $params['dataofs'] = $prev;
525
526                $text .= '<a href="' . wl($ID, $params) .
527                    '" title="' . $this->getLang('prev') .
528                    '" class="prev">' . $this->getLang('prev') . '</a>';
529            }
530
531            $text .= '&nbsp;';
532
533            if($rowcnt > $data['limit']) {
534                $next = $offset + $data['limit'];
535
536                // keep url params
537                $params = $this->dthlp->_a2ua('dataflt', $_REQUEST['dataflt']);
538                if(isset($_REQUEST['datasrt'])) {
539                    $params['datasrt'] = $_REQUEST['datasrt'];
540                }
541                $params['dataofs'] = $next;
542
543                $text .= '<a href="' . wl($ID, $params) .
544                    '" title="' . $this->getLang('next') .
545                    '" class="next">' . $this->getLang('next') . '</a>';
546            }
547            $text .= '</th></tr>';
548        }
549
550        $text .= '</table></div>';
551        return $text;
552    }
553
554    /**
555     * Builds the SQL query from the given data
556     *
557     * @param array &$data instruction by handler
558     * @return bool|string SQL query or false
559     */
560    function _buildSQL(&$data) {
561        $cnt = 0;
562        $tables = array();
563        $select = array();
564        $from = '';
565
566        $from2 = '';
567        $where2 = '1 = 1';
568
569        $sqlite = $this->dthlp->_getDB();
570        if(!$sqlite) return false;
571
572        // prepare the columns to show
573        foreach($data['cols'] as &$col) {
574            $key = $col['key'];
575            if($key == '%pageid%') {
576                // Prevent stripping of trailing zeros by forcing a CAST
577                $select[] = '" " || pages.page';
578            } elseif($key == '%class%') {
579                // Prevent stripping of trailing zeros by forcing a CAST
580                $select[] = '" " || pages.class';
581            } elseif($key == '%lastmod%') {
582                $select[] = 'pages.lastmod';
583            } elseif($key == '%title%') {
584                $select[] = "pages.page || '|' || pages.title";
585            } else {
586                if(!isset($tables[$key])) {
587                    $tables[$key] = 'T' . (++$cnt);
588                    $from .= ' LEFT JOIN data AS ' . $tables[$key] . ' ON ' . $tables[$key] . '.pid = W1.pid';
589                    $from .= ' AND ' . $tables[$key] . ".key = " . $sqlite->quote_string($key);
590                }
591                $type = $col['type'];
592                if(is_array($type)) {
593                    $type = $type['type'];
594                }
595                switch($type) {
596                    case 'pageid':
597                    case 'wiki':
598                        //note in multivalued case: adds pageid only to first value
599                        $select[] = "pages.page || '|' || group_concat(" . $tables[$key] . ".value,'\n')";
600                        break;
601                    default:
602                        // Prevent stripping of trailing zeros by forcing a CAST
603                        $select[] = 'group_concat(" " || ' . $tables[$key] . ".value,'\n')";
604                }
605            }
606        }
607        unset($col);
608
609        // prepare sorting
610        if(isset($data['sort'])) {
611            $col = $data['sort'][0];
612
613            if($col == '%pageid%') {
614                $order = 'ORDER BY pages.page ' . $data['sort'][1];
615            } elseif($col == '%class%') {
616                $order = 'ORDER BY pages.class ' . $data['sort'][1];
617            } elseif($col == '%title%') {
618                $order = 'ORDER BY pages.title ' . $data['sort'][1];
619            } elseif($col == '%lastmod%') {
620                $order = 'ORDER BY pages.lastmod ' . $data['sort'][1];
621            } else {
622                // sort by hidden column?
623                if(!$tables[$col]) {
624                    $tables[$col] = 'T' . (++$cnt);
625                    $from .= ' LEFT JOIN data AS ' . $tables[$col] . ' ON ' . $tables[$col] . '.pid = W1.pid';
626                    $from .= ' AND ' . $tables[$col] . ".key = " . $sqlite->quote_string($col);
627                }
628
629                $order = 'ORDER BY ' . $tables[$col] . '.value ' . $data['sort'][1];
630            }
631        } else {
632            $order = 'ORDER BY 1 ASC';
633        }
634
635        // may be disabled from config. as it decreases performance a lot
636        $use_dataresolve = $this->getConf('use_dataresolve');
637
638        // prepare filters
639        $cnt = 0;
640        if(is_array($data['filter']) && count($data['filter'])) {
641
642            foreach($data['filter'] as $filter) {
643                $col = $filter['key'];
644                $closecompare = ($filter['compare'] == 'IN(' ? ')' : '');
645
646                if($col == '%pageid%') {
647                    $where2 .= " " . $filter['logic'] . " pages.page " . $filter['compare'] . " '" . $filter['value'] . "'" . $closecompare;
648                } elseif($col == '%class%') {
649                    $where2 .= " " . $filter['logic'] . " pages.class " . $filter['compare'] . " '" . $filter['value'] . "'" . $closecompare;
650                } elseif($col == '%title%') {
651                    $where2 .= " " . $filter['logic'] . " pages.title " . $filter['compare'] . " '" . $filter['value'] . "'" . $closecompare;
652                } elseif($col == '%lastmod%') {
653                    # parse value to int?
654                    $filter['value'] = (int) strtotime($filter['value']);
655                    $where2 .= " " . $filter['logic'] . " pages.lastmod " . $filter['compare'] . " " . $filter['value'] . $closecompare;
656                } else {
657                    // filter by hidden column?
658                    $table = 'T' . (++$cnt);
659                    $from2 .= ' LEFT JOIN data AS ' . $table . ' ON ' . $table . '.pid = pages.pid';
660                    $from2 .= ' AND ' . $table . ".key = " . $sqlite->quote_string($col);
661
662                    // apply data resolving?
663                    if($use_dataresolve && $filter['colname'] && (substr($filter['compare'], -4) == 'LIKE')) {
664                        $where2 .= ' ' . $filter['logic'] . ' DATARESOLVE(' . $table . '.value,\'' . $sqlite->escape_string($filter['colname']) . '\') ' . $filter['compare'] .
665                            " '" . $filter['value'] . "'"; //value is already escaped
666                    } else {
667                        $where2 .= ' ' . $filter['logic'] . ' ' . $table . '.value ' . $filter['compare'] .
668                            " '" . $filter['value'] . "'" . $closecompare; //value is already escaped
669                    }
670                }
671            }
672        }
673
674        // build the query
675        $sql = "SELECT " . join(', ', $select) . "
676                FROM (
677                    SELECT DISTINCT pages.pid AS pid
678                    FROM pages $from2
679                    WHERE $where2
680                ) AS W1
681                $from
682                LEFT JOIN pages ON W1.pid=pages.pid
683                GROUP BY W1.pid
684                $order";
685
686        // offset and limit
687        if($data['limit']) {
688            $sql .= ' LIMIT ' . ($data['limit'] + 1);
689            // offset is added from REQUEST params in updateSQLwithQuery
690        }
691
692        return $sql;
693    }
694
695    /**
696     * Handle request paramaters, rebuild sql when needed
697     *
698     * @param array $data instruction by handler()
699     */
700    function updateSQLwithQuery(&$data) {
701        if($this->hasRequestFilter()) {
702            if(isset($_REQUEST['datasrt'])) {
703                if($_REQUEST['datasrt'][0] == '^') {
704                    $data['sort'] = array(substr($_REQUEST['datasrt'], 1), 'DESC');
705                } else {
706                    $data['sort'] = array($_REQUEST['datasrt'], 'ASC');
707                }
708            }
709
710            // add request filters
711            $data['filter'] = array_merge($data['filter'], $this->dthlp->_get_filters());
712
713            // Rebuild SQL FIXME do this smarter & faster
714            $data['sql'] = $this->_buildSQL($data);
715        }
716
717        if($data['limit'] && (int) $_REQUEST['dataofs']) {
718            $data['sql'] .= ' OFFSET ' . ((int) $_REQUEST['dataofs']);
719        }
720    }
721
722    /**
723     * Check whether a sort or filter request parameters are available
724     *
725     * @return bool
726     */
727    function hasRequestFilter() {
728        return isset($_REQUEST['datasrt']) || isset($_REQUEST['dataflt']);
729    }
730
731    /**
732     * Split values at the commas,
733     * - Wrap with quotes to escape comma, quotes escaped by two quotes
734     * - Within quotes spaces are stored.
735     *
736     * @param string $line
737     * @return array
738     */
739    protected function parseValues($line) {
740        $values = array();
741        $inQuote = false;
742        $escapedQuote = false;
743        $value = '';
744
745        $len = strlen($line);
746        for($i = 0; $i < $len; $i++) {
747            if($line[$i] == '"') {
748                if($inQuote) {
749                    if($escapedQuote) {
750                        $value .= '"';
751                        $escapedQuote = false;
752                        continue;
753                    }
754                    if($line[$i + 1] == '"') {
755                        $escapedQuote = true;
756                        continue;
757                    }
758                    array_push($values, $value);
759                    $inQuote = false;
760                    $value = '';
761                    continue;
762
763                } else {
764                    $inQuote = true;
765                    $value = ''; //don't store stuff before the opening quote
766                    continue;
767                }
768            } else if($line[$i] == ',') {
769                if($inQuote) {
770                    $value .= ',';
771                    continue;
772                } else {
773                    if(strlen($value) < 1) {
774                        continue;
775                    }
776                    array_push($values, trim($value));
777                    $value = '';
778                    continue;
779                }
780            }
781
782            $value .= $line[$i];
783        }
784        if(strlen($value) > 0) {
785            array_push($values, trim($value));
786        }
787        return $values;
788    }
789}
790
791