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>↓</span> '; 414 $ckey = '^' . $ckey; 415 } else { 416 $text .= '<span>↑</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 .= ' '; 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 .= ' '; 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