ParseCSV.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240
  1. <?php
  2. namespace External;
  3. class ParseCSV
  4. {
  5. /*
  6. Class: parseCSV v0.4.3 beta
  7. https://github.com/parsecsv/parsecsv-for-php
  8. Fully conforms to the specifications lined out on wikipedia:
  9. - http://en.wikipedia.org/wiki/Comma-separated_values
  10. Based on the concept of Ming Hong Ng's CsvFileParser class:
  11. - http://minghong.blogspot.com/2006/07/csv-parser-for-php.html
  12. (The MIT license)
  13. Copyright (c) 2014 Jim Myhrberg.
  14. Permission is hereby granted, free of charge, to any person obtaining a copy
  15. of this software and associated documentation files (the "Software"), to deal
  16. in the Software without restriction, including without limitation the rights
  17. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  18. copies of the Software, and to permit persons to whom the Software is
  19. furnished to do so, subject to the following conditions:
  20. The above copyright notice and this permission notice shall be included in
  21. all copies or substantial portions of the Software.
  22. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  23. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  24. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  25. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  26. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  27. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  28. THE SOFTWARE.
  29. Code Examples
  30. ----------------
  31. # general usage
  32. $csv = new parseCSV('data.csv');
  33. print_r($csv->data);
  34. ----------------
  35. # tab delimited, and encoding conversion
  36. $csv = new parseCSV();
  37. $csv->encoding('UTF-16', 'UTF-8');
  38. $csv->delimiter = "\t";
  39. $csv->parse('data.tsv');
  40. print_r($csv->data);
  41. ----------------
  42. # auto-detect delimiter character
  43. $csv = new parseCSV();
  44. $csv->auto('data.csv');
  45. print_r($csv->data);
  46. ----------------
  47. # modify data in a csv file
  48. $csv = new parseCSV();
  49. $csv->sort_by = 'id';
  50. $csv->parse('data.csv');
  51. # "4" is the value of the "id" column of the CSV row
  52. $csv->data[4] = array('firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@doe.com');
  53. $csv->save();
  54. ----------------
  55. # add row/entry to end of CSV file
  56. # - only recommended when you know the extact sctructure of the file
  57. $csv = new parseCSV();
  58. $csv->save('data.csv', array(array('1986', 'Home', 'Nowhere', '')), true);
  59. ----------------
  60. # convert 2D array to csv data and send headers
  61. # to browser to treat output as a file and download it
  62. $csv = new parseCSV();
  63. $csv->output('movies.csv', $array, array('field 1', 'field 2'), ',');
  64. ----------------
  65. */
  66. /**
  67. * Configuration
  68. * - set these options with $object->var_name = 'value';
  69. */
  70. /**
  71. * Heading
  72. * Use first line/entry as field names
  73. *
  74. * @access public
  75. * @var bool
  76. */
  77. public $heading = true;
  78. /**
  79. * Fields
  80. * Override field names
  81. *
  82. * @access public
  83. * @var array
  84. */
  85. public $fields = array();
  86. /**
  87. * Sort By
  88. * Sort csv by this field
  89. *
  90. * @access public
  91. * @var string
  92. */
  93. public $sort_by = null;
  94. /**
  95. * Sort Reverse
  96. * Reverse the sort function
  97. *
  98. * @access public
  99. * @var bool
  100. */
  101. public $sort_reverse = false;
  102. /**
  103. * Sort Type
  104. * Sort behavior passed to sort methods
  105. *
  106. * regular = SORT_REGULAR
  107. * numeric = SORT_NUMERIC
  108. * string = SORT_STRING
  109. *
  110. * @access public
  111. * @var string
  112. */
  113. public $sort_type = null;
  114. /**
  115. * Delimiter
  116. * Delimiter character
  117. *
  118. * @access public
  119. * @var string
  120. */
  121. public $delimiter = ',';
  122. /**
  123. * Enclosure
  124. * Enclosure character
  125. *
  126. * @access public
  127. * @var string
  128. */
  129. public $enclosure = '"';
  130. /**
  131. * Enclose All
  132. * Force enclosing all columns
  133. *
  134. * @access public
  135. * @var bool
  136. */
  137. public $enclose_all = false;
  138. /**
  139. * Conditions
  140. * Basic SQL-Like conditions for row matching
  141. *
  142. * @access public
  143. * @var string
  144. */
  145. public $conditions = null;
  146. /**
  147. * Offset
  148. * Number of rows to ignore from beginning of data
  149. *
  150. * @access public
  151. * @var int
  152. */
  153. public $offset = null;
  154. /**
  155. * Limit
  156. * Limits the number of returned rows to the specified amount
  157. *
  158. * @access public
  159. * @var int
  160. */
  161. public $limit = null;
  162. /**
  163. * Auto Depth
  164. * Number of rows to analyze when attempting to auto-detect delimiter
  165. *
  166. * @access public
  167. * @var int
  168. */
  169. public $auto_depth = 15;
  170. /**
  171. * Auto Non Charts
  172. * Characters that should be ignored when attempting to auto-detect delimiter
  173. *
  174. * @access public
  175. * @var string
  176. */
  177. public $auto_non_chars = "a-zA-Z0-9\n\r";
  178. /**
  179. * Auto Preferred
  180. * preferred delimiter characters, only used when all filtering method
  181. * returns multiple possible delimiters (happens very rarely)
  182. *
  183. * @access public
  184. * @var string
  185. */
  186. public $auto_preferred = ",;\t.:|";
  187. /**
  188. * Convert Encoding
  189. * Should we convert the csv encoding?
  190. *
  191. * @access public
  192. * @var bool
  193. */
  194. public $convert_encoding = false;
  195. /**
  196. * Input Encoding
  197. * Set the input encoding
  198. *
  199. * @access public
  200. * @var string
  201. */
  202. public $input_encoding = 'ISO-8859-1';
  203. /**
  204. * Output Encoding
  205. * Set the output encoding
  206. *
  207. * @access public
  208. * @var string
  209. */
  210. public $output_encoding = 'ISO-8859-1';
  211. /**
  212. * Linefeed
  213. * Line feed characters used by unparse, save, and output methods
  214. *
  215. * @access public
  216. * @var string
  217. */
  218. public $linefeed = "\r";
  219. /**
  220. * Output Delimiter
  221. * Sets the output delimiter used by the output method
  222. *
  223. * @access public
  224. * @var string
  225. */
  226. public $output_delimiter = ',';
  227. /**
  228. * Output filename
  229. * Sets the output filename
  230. *
  231. * @access public
  232. * @var string
  233. */
  234. public $output_filename = 'data.csv';
  235. /**
  236. * Keep File Data
  237. * keep raw file data in memory after successful parsing (useful for debugging)
  238. *
  239. * @access public
  240. * @var bool
  241. */
  242. public $keep_file_data = false;
  243. /**
  244. * Internal variables
  245. */
  246. /**
  247. * File
  248. * Current Filename
  249. *
  250. * @access public
  251. * @var string
  252. */
  253. public $file;
  254. /**
  255. * File Data
  256. * Current file data
  257. *
  258. * @access public
  259. * @var string
  260. */
  261. public $file_data;
  262. /**
  263. * Error
  264. * Contains the error code if one occured
  265. *
  266. * 0 = No errors found. Everything should be fine :)
  267. * 1 = Hopefully correctable syntax error was found.
  268. * 2 = Enclosure character (double quote by default)
  269. * was found in non-enclosed field. This means
  270. * the file is either corrupt, or does not
  271. * standard CSV formatting. Please validate
  272. * the parsed data yourself.
  273. *
  274. * @access public
  275. * @var int
  276. */
  277. public $error = 0;
  278. /**
  279. * Error Information
  280. * Detailed error information
  281. *
  282. * @access public
  283. * @var array
  284. */
  285. public $error_info = array();
  286. /**
  287. * Titles
  288. * CSV titles if they exists
  289. *
  290. * @access public
  291. * @var array
  292. */
  293. public $titles = array();
  294. /**
  295. * Data
  296. * Two dimensional array of CSV data
  297. *
  298. * @access public
  299. * @var array
  300. */
  301. public $data = array();
  302. /**
  303. * Constructor
  304. * Class constructor
  305. *
  306. * @access public
  307. *
  308. * @param [string] input The CSV string or a direct filepath
  309. * @param [integer] offset Number of rows to ignore from the beginning of the data
  310. * @param [integer] limit Limits the number of returned rows to specified amount
  311. * @param [string] conditions Basic SQL-like conditions for row matching
  312. */
  313. public function __construct($input = null, $offset = null, $limit = null, $conditions = null, $keep_file_data = null)
  314. {
  315. if (!is_null($offset)) {
  316. $this->offset = $offset;
  317. }
  318. if (!is_null($limit)) {
  319. $this->limit = $limit;
  320. }
  321. if (!is_null($conditions)) {
  322. $this->conditions = $conditions;
  323. }
  324. if (!is_null($keep_file_data)) {
  325. $this->keep_file_data = $keep_file_data;
  326. }
  327. if (!empty($input)) {
  328. $this->parse($input);
  329. }
  330. }
  331. // ==============================================
  332. // ----- [ Main Functions ] ---------------------
  333. // ==============================================
  334. /**
  335. * Parse
  336. * Parse a CSV file or string
  337. *
  338. * @access public
  339. *
  340. * @param [string] input The CSV string or a direct filepath
  341. * @param [integer] offset Number of rows to ignore from the beginning of the data
  342. * @param [integer] limit Limits the number of returned rows to specified amount
  343. * @param [string] conditions Basic SQL-like conditions for row matching
  344. *
  345. * @return [bool]
  346. */
  347. public function parse($input = null, $offset = null, $limit = null, $conditions = null)
  348. {
  349. if (is_null($input)) {
  350. $input = $this->file;
  351. }
  352. if (!empty($input)) {
  353. if (!is_null($offset)) {
  354. $this->offset = $offset;
  355. }
  356. if (!is_null($limit)) {
  357. $this->limit = $limit;
  358. }
  359. if (!is_null($conditions)) {
  360. $this->conditions = $conditions;
  361. }
  362. if (is_readable($input)) {
  363. $this->data = $this->parse_file($input);
  364. }
  365. else {
  366. $this->file_data = &$input;
  367. $this->data = $this->parse_string();
  368. }
  369. if ($this->data === false) {
  370. return false;
  371. }
  372. }
  373. return true;
  374. }
  375. /**
  376. * Save
  377. * Save changes, or write a new file and/or data
  378. *
  379. * @access public
  380. *
  381. * @param [string] $file File location to save to
  382. * @param [array] $data 2D array of data
  383. * @param [bool] $append Append current data to end of target CSV, if file exists
  384. * @param [array] $fields Field names
  385. *
  386. * @return [bool]
  387. */
  388. public function save($file = null, $data = array(), $append = false, $fields = array())
  389. {
  390. if (empty($file)) {
  391. $file = &$this->file;
  392. }
  393. $mode = ($append) ? 'at' : 'wt';
  394. $is_php = (preg_match('/\.php$/i', $file)) ? true : false;
  395. return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode);
  396. }
  397. /**
  398. * Output
  399. * Generate a CSV based string for output.
  400. *
  401. * @access public
  402. *
  403. * @param [string] $filename If specified, headers and data will be output directly to browser as a downloable file
  404. * @param [array] $data 2D array with data
  405. * @param [array] $fields Field names
  406. * @param [type] $delimiter delimiter used to separate data
  407. *
  408. * @return [string]
  409. */
  410. public function output($filename = null, $data = array(), $fields = array(), $delimiter = null)
  411. {
  412. if (empty($filename)) {
  413. $filename = $this->output_filename;
  414. }
  415. if ($delimiter === null) {
  416. $delimiter = $this->output_delimiter;
  417. }
  418. $data = $this->unparse($data, $fields, null, null, $delimiter);
  419. if (!is_null($filename)) {
  420. header('Content-type: application/csv');
  421. header('Content-Length: ' . strlen($data));
  422. header('Cache-Control: no-cache, must-revalidate');
  423. header('Pragma: no-cache');
  424. header('Expires: 0');
  425. header('Content-Disposition: attachment; filename="' . $filename . '"; modification-date="' . date('r') . '";');
  426. echo $data;
  427. }
  428. return $data;
  429. }
  430. /**
  431. * Encoding
  432. * Convert character encoding
  433. *
  434. * @access public
  435. *
  436. * @param [string] $input Input character encoding, uses default if left blank
  437. * @param [string] $output Output character encoding, uses default if left blank
  438. */
  439. public function encoding($input = null, $output = null)
  440. {
  441. $this->convert_encoding = true;
  442. if (!is_null($input)) {
  443. $this->input_encoding = $input;
  444. }
  445. if (!is_null($output)) {
  446. $this->output_encoding = $output;
  447. }
  448. }
  449. /**
  450. * Auto
  451. * Auto-Detect Delimiter: Find delimiter by analyzing a specific number of
  452. * rows to determine most probable delimiter character
  453. *
  454. * @access public
  455. *
  456. * @param [string] $file Local CSV file
  457. * @param [bool] $parse True/false parse file directly
  458. * @param [int] $search_depth Number of rows to analyze
  459. * @param [string] $preferred Preferred delimiter characters
  460. * @param [string] $enclosure Enclosure character, default is double quote (").
  461. *
  462. * @return [string]
  463. */
  464. public function auto($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null)
  465. {
  466. if (is_null($file)) {
  467. $file = $this->file;
  468. }
  469. if (empty($search_depth)) {
  470. $search_depth = $this->auto_depth;
  471. }
  472. if (is_null($enclosure)) {
  473. $enclosure = $this->enclosure;
  474. }
  475. if (is_null($preferred)) {
  476. $preferred = $this->auto_preferred;
  477. }
  478. if (empty($this->file_data)) {
  479. if ($this->_check_data($file)) {
  480. $data = &$this->file_data;
  481. }
  482. else {
  483. return false;
  484. }
  485. }
  486. else {
  487. $data = &$this->file_data;
  488. }
  489. $chars = array();
  490. $strlen = strlen($data);
  491. $enclosed = false;
  492. $n = 1;
  493. $to_end = true;
  494. // walk specific depth finding posssible delimiter characters
  495. for ($i = 0; $i < $strlen; $i++) {
  496. $ch = $data{$i};
  497. $nch = (isset($data{$i + 1})) ? $data{$i + 1} : false;
  498. $pch = (isset($data{$i - 1})) ? $data{$i - 1} : false;
  499. // open and closing quotes
  500. if ($ch == $enclosure) {
  501. if (!$enclosed || $nch != $enclosure) {
  502. $enclosed = ($enclosed) ? false : true;
  503. }
  504. elseif ($enclosed) {
  505. $i++;
  506. }
  507. // end of row
  508. }
  509. elseif (($ch == "\n" && $pch != "\r" || $ch == "\r") && !$enclosed) {
  510. if ($n >= $search_depth) {
  511. $strlen = 0;
  512. $to_end = false;
  513. }
  514. else {
  515. $n++;
  516. }
  517. // count character
  518. }
  519. elseif (!$enclosed) {
  520. if (!preg_match('/[' . preg_quote($this->auto_non_chars, '/') . ']/i', $ch)) {
  521. if (!isset($chars[$ch][$n])) {
  522. $chars[$ch][$n] = 1;
  523. }
  524. else {
  525. $chars[$ch][$n]++;
  526. }
  527. }
  528. }
  529. }
  530. // filtering
  531. $depth = ($to_end) ? $n - 1 : $n;
  532. $filtered = array();
  533. foreach ($chars as $char => $value) {
  534. if ($match = $this->_check_count($char, $value, $depth, $preferred)) {
  535. $filtered[$match] = $char;
  536. }
  537. }
  538. // capture most probable delimiter
  539. ksort($filtered);
  540. $this->delimiter = reset($filtered);
  541. // parse data
  542. if ($parse) {
  543. $this->data = $this->parse_string();
  544. }
  545. return $this->delimiter;
  546. }
  547. // ==============================================
  548. // ----- [ Core Functions ] ---------------------
  549. // ==============================================
  550. /**
  551. * Parse File
  552. * Read file to string and call parse_string()
  553. *
  554. * @access public
  555. *
  556. * @param [string] $file Local CSV file
  557. *
  558. * @return [array|bool]
  559. */
  560. public function parse_file($file = null)
  561. {
  562. if (is_null($file)) {
  563. $file = $this->file;
  564. }
  565. if (empty($this->file_data)) {
  566. $this->load_data($file);
  567. }
  568. return (!empty($this->file_data)) ? $this->parse_string() : false;
  569. }
  570. /**
  571. * Parse CSV strings to arrays
  572. *
  573. * @access public
  574. *
  575. * @param data CSV string
  576. *
  577. * @return 2D array with CSV data, or false on failure
  578. */
  579. public function parse_string($data = null)
  580. {
  581. if (empty($data)) {
  582. if ($this->_check_data()) {
  583. $data = &$this->file_data;
  584. }
  585. else {
  586. return false;
  587. }
  588. }
  589. $white_spaces = str_replace($this->delimiter, '', " \t\x0B\0");
  590. $rows = array();
  591. $row = array();
  592. $row_count = 0;
  593. $current = '';
  594. $head = (!empty($this->fields)) ? $this->fields : array();
  595. $col = 0;
  596. $enclosed = false;
  597. $was_enclosed = false;
  598. $strlen = strlen($data);
  599. // force the parser to process end of data as a character (false) when
  600. // data does not end with a line feed or carriage return character.
  601. $lch = $data{$strlen - 1};
  602. if ($lch != "\n" && $lch != "\r") {
  603. $strlen++;
  604. }
  605. // walk through each character
  606. for ($i = 0; $i < $strlen; $i++) {
  607. $ch = (isset($data{$i})) ? $data{$i} : false;
  608. $nch = (isset($data{$i + 1})) ? $data{$i + 1} : false;
  609. $pch = (isset($data{$i - 1})) ? $data{$i - 1} : false;
  610. // open/close quotes, and inline quotes
  611. if ($ch == $this->enclosure) {
  612. if (!$enclosed) {
  613. if (ltrim($current, $white_spaces) == '') {
  614. $enclosed = true;
  615. $was_enclosed = true;
  616. }
  617. else {
  618. $this->error = 2;
  619. $error_row = count($rows) + 1;
  620. $error_col = $col + 1;
  621. if (!isset($this->error_info[$error_row . '-' . $error_col])) {
  622. $this->error_info[$error_row . '-' . $error_col] = array(
  623. 'type' => 2,
  624. 'info' => 'Syntax error found on row ' . $error_row . '. Non-enclosed fields can not contain double-quotes.',
  625. 'row' => $error_row,
  626. 'field' => $error_col,
  627. 'field_name' => (!empty($head[$col])) ? $head[$col] : null,
  628. );
  629. }
  630. $current .= $ch;
  631. }
  632. }
  633. elseif ($nch == $this->enclosure) {
  634. $current .= $ch;
  635. $i++;
  636. }
  637. elseif ($nch != $this->delimiter && $nch != "\r" && $nch != "\n") {
  638. for ($x = ($i + 1); isset($data{$x}) && ltrim($data{$x}, $white_spaces) == ''; $x++) {
  639. }
  640. if ($data{$x} == $this->delimiter) {
  641. $enclosed = false;
  642. $i = $x;
  643. }
  644. else {
  645. if ($this->error < 1) {
  646. $this->error = 1;
  647. }
  648. $error_row = count($rows) + 1;
  649. $error_col = $col + 1;
  650. if (!isset($this->error_info[$error_row . '-' . $error_col])) {
  651. $this->error_info[$error_row . '-' . $error_col] = array(
  652. 'type' => 1,
  653. 'info' =>
  654. 'Syntax error found on row ' . (count($rows) + 1) . '. ' .
  655. 'A single double-quote was found within an enclosed string. ' .
  656. 'Enclosed double-quotes must be escaped with a second double-quote.',
  657. 'row' => count($rows) + 1,
  658. 'field' => $col + 1,
  659. 'field_name' => (!empty($head[$col])) ? $head[$col] : null,
  660. );
  661. }
  662. $current .= $ch;
  663. $enclosed = false;
  664. }
  665. }
  666. else {
  667. $enclosed = false;
  668. }
  669. // end of field/row/csv
  670. }
  671. elseif (($ch == $this->delimiter || $ch == "\n" || $ch == "\r" || $ch === false) && !$enclosed) {
  672. $key = (!empty($head[$col])) ? $head[$col] : $col;
  673. $row[$key] = ($was_enclosed) ? $current : trim($current);
  674. $current = '';
  675. $was_enclosed = false;
  676. $col++;
  677. // end of row
  678. if ($ch == "\n" || $ch == "\r" || $ch === false) {
  679. if ($this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions)) {
  680. if ($this->heading && empty($head)) {
  681. $head = $row;
  682. }
  683. elseif (empty($this->fields) || (!empty($this->fields) && (($this->heading && $row_count > 0) || !$this->heading))) {
  684. if (!empty($this->sort_by) && !empty($row[$this->sort_by])) {
  685. if (isset($rows[$row[$this->sort_by]])) {
  686. $rows[$row[$this->sort_by] . '_0'] = &$rows[$row[$this->sort_by]];
  687. unset($rows[$row[$this->sort_by]]);
  688. for ($sn = 1; isset($rows[$row[$this->sort_by] . '_' . $sn]); $sn++) {
  689. }
  690. $rows[$row[$this->sort_by] . '_' . $sn] = $row;
  691. }
  692. else {
  693. $rows[$row[$this->sort_by]] = $row;
  694. }
  695. }
  696. else {
  697. $rows[] = $row;
  698. }
  699. }
  700. }
  701. $row = array();
  702. $col = 0;
  703. $row_count++;
  704. if ($this->sort_by === null && $this->limit !== null && count($rows) == $this->limit) {
  705. $i = $strlen;
  706. }
  707. if ($ch == "\r" && $nch == "\n") {
  708. $i++;
  709. }
  710. }
  711. // append character to current field
  712. }
  713. else {
  714. $current .= $ch;
  715. }
  716. }
  717. $this->titles = $head;
  718. if (!empty($this->sort_by)) {
  719. $sort_type = SORT_REGULAR;
  720. if ($this->sort_type == 'numeric') {
  721. $sort_type = SORT_NUMERIC;
  722. }
  723. elseif ($this->sort_type == 'string') {
  724. $sort_type = SORT_STRING;
  725. }
  726. ($this->sort_reverse) ? krsort($rows, $sort_type) : ksort($rows, $sort_type);
  727. if ($this->offset !== null || $this->limit !== null) {
  728. $rows = array_slice($rows, ($this->offset === null ? 0 : $this->offset), $this->limit, true);
  729. }
  730. }
  731. if (!$this->keep_file_data) {
  732. $this->file_data = null;
  733. }
  734. return $rows;
  735. }
  736. /**
  737. * Create CSV data from array
  738. *
  739. * @access public
  740. *
  741. * @param data 2D array with data
  742. * @param fields field names
  743. * @param append if true, field names will not be output
  744. * @param is_php if a php die() call should be put on the first
  745. * line of the file, this is later ignored when read.
  746. * @param delimiter field delimiter to use
  747. *
  748. * @return CSV data (text string)
  749. */
  750. public function unparse($data = array(), $fields = array(), $append = false, $is_php = false, $delimiter = null)
  751. {
  752. if (!is_array($data) || empty($data)) {
  753. $data = &$this->data;
  754. }
  755. if (!is_array($fields) || empty($fields)) {
  756. $fields = &$this->titles;
  757. }
  758. if ($delimiter === null) {
  759. $delimiter = $this->delimiter;
  760. }
  761. $string = ($is_php) ? "<?php header('Status: 403'); die(' '); ?>" . $this->linefeed : '';
  762. $entry = array();
  763. // create heading
  764. if ($this->heading && !$append && !empty($fields)) {
  765. foreach ($fields as $key => $value) {
  766. $entry[] = $this->_enclose_value($value, $delimiter);
  767. }
  768. $string .= implode($delimiter, $entry) . $this->linefeed;
  769. $entry = array();
  770. }
  771. // create data
  772. foreach ($data as $key => $row) {
  773. foreach ($row as $field => $value) {
  774. $entry[] = $this->_enclose_value($value, $delimiter);
  775. }
  776. $string .= implode($delimiter, $entry) . $this->linefeed;
  777. $entry = array();
  778. }
  779. if ($this->convert_encoding) {
  780. $string = iconv($this->input_encoding, $this->output_encoding, $string);
  781. }
  782. return $string;
  783. }
  784. /**
  785. * Load local file or string
  786. *
  787. * @access public
  788. *
  789. * @param input local CSV file
  790. *
  791. * @return true or false
  792. */
  793. public function load_data($input = null)
  794. {
  795. $data = null;
  796. $file = null;
  797. if (is_null($input)) {
  798. $file = $this->file;
  799. }
  800. elseif (file_exists($input)) {
  801. $file = $input;
  802. }
  803. else {
  804. $data = $input;
  805. }
  806. if (!empty($data) || $data = $this->_rfile($file)) {
  807. if ($this->file != $file) {
  808. $this->file = $file;
  809. }
  810. if (preg_match('/\.php$/i', $file) && preg_match('/<\?.*?\?>(.*)/ims', $data, $strip)) {
  811. $data = ltrim($strip[1]);
  812. }
  813. if ($this->convert_encoding) {
  814. $data = iconv($this->input_encoding, $this->output_encoding, $data);
  815. }
  816. if (substr($data, -1) != "\n") {
  817. $data .= "\n";
  818. }
  819. $this->file_data = &$data;
  820. return true;
  821. }
  822. return false;
  823. }
  824. // ==============================================
  825. // ----- [ Internal Functions ] -----------------
  826. // ==============================================
  827. /**
  828. * Validate a row against specified conditions
  829. *
  830. * @access protected
  831. *
  832. * @param row array with values from a row
  833. * @param conditions specified conditions that the row must match
  834. *
  835. * @return true of false
  836. */
  837. protected function _validate_row_conditions($row = array(), $conditions = null)
  838. {
  839. if (!empty($row)) {
  840. if (!empty($conditions)) {
  841. $conditions = (strpos($conditions, ' OR ') !== false) ? explode(' OR ', $conditions) : array($conditions);
  842. $or = '';
  843. foreach ($conditions as $key => $value) {
  844. if (strpos($value, ' AND ') !== false) {
  845. $value = explode(' AND ', $value);
  846. $and = '';
  847. foreach ($value as $k => $v) {
  848. $and .= $this->_validate_row_condition($row, $v);
  849. }
  850. $or .= (strpos($and, '0') !== false) ? '0' : '1';
  851. }
  852. else {
  853. $or .= $this->_validate_row_condition($row, $value);
  854. }
  855. }
  856. return (strpos($or, '1') !== false) ? true : false;
  857. }
  858. return true;
  859. }
  860. return false;
  861. }
  862. /**
  863. * Validate a row against a single condition
  864. *
  865. * @access protected
  866. *
  867. * @param row array with values from a row
  868. * @param condition specified condition that the row must match
  869. *
  870. * @return true of false
  871. */
  872. protected function _validate_row_condition($row, $condition)
  873. {
  874. $operators = array(
  875. '=', 'equals', 'is',
  876. '!=', 'is not',
  877. '<', 'is less than',
  878. '>', 'is greater than',
  879. '<=', 'is less than or equals',
  880. '>=', 'is greater than or equals',
  881. 'contains',
  882. 'does not contain',
  883. );
  884. $operators_regex = array();
  885. foreach ($operators as $value) {
  886. $operators_regex[] = preg_quote($value, '/');
  887. }
  888. $operators_regex = implode('|', $operators_regex);
  889. if (preg_match('/^(.+) (' . $operators_regex . ') (.+)$/i', trim($condition), $capture)) {
  890. $field = $capture[1];
  891. $op = $capture[2];
  892. $value = $capture[3];
  893. if (preg_match('/^([\'\"]{1})(.*)([\'\"]{1})$/i', $value, $capture)) {
  894. if ($capture[1] == $capture[3]) {
  895. $value = $capture[2];
  896. $value = str_replace("\\n", "\n", $value);
  897. $value = str_replace("\\r", "\r", $value);
  898. $value = str_replace("\\t", "\t", $value);
  899. $value = stripslashes($value);
  900. }
  901. }
  902. if (array_key_exists($field, $row)) {
  903. if (($op == '=' || $op == 'equals' || $op == 'is') && $row[$field] == $value) {
  904. return '1';
  905. }
  906. elseif (($op == '!=' || $op == 'is not') && $row[$field] != $value) {
  907. return '1';
  908. }
  909. elseif (($op == '<' || $op == 'is less than') && $row[$field] < $value) {
  910. return '1';
  911. }
  912. elseif (($op == '>' || $op == 'is greater than') && $row[$field] > $value) {
  913. return '1';
  914. }
  915. elseif (($op == '<=' || $op == 'is less than or equals') && $row[$field] <= $value) {
  916. return '1';
  917. }
  918. elseif (($op == '>=' || $op == 'is greater than or equals') && $row[$field] >= $value) {
  919. return '1';
  920. }
  921. elseif ($op == 'contains' && preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) {
  922. return '1';
  923. }
  924. elseif ($op == 'does not contain' && !preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) {
  925. return '1';
  926. }
  927. else {
  928. return '0';
  929. }
  930. }
  931. }
  932. return '1';
  933. }
  934. /**
  935. * Validates if the row is within the offset or not if sorting is disabled
  936. *
  937. * @access protected
  938. *
  939. * @param current_row the current row number being processed
  940. *
  941. * @return true of false
  942. */
  943. protected function _validate_offset($current_row)
  944. {
  945. if ($this->sort_by === null && $this->offset !== null && $current_row < $this->offset) {
  946. return false;
  947. }
  948. return true;
  949. }
  950. /**
  951. * Enclose values if needed
  952. * - only used by unparse()
  953. *
  954. * @access protected
  955. *
  956. * @param value string to process
  957. *
  958. * @return Processed value
  959. */
  960. protected function _enclose_value($value = null, $delimiter = null)
  961. {
  962. if (is_null($delimiter)) {
  963. $delimiter = $this->delimiter;
  964. }
  965. if ($value !== null && $value != '') {
  966. $delimiter_quoted = preg_quote($delimiter, '/');
  967. $enclosure_quoted = preg_quote($this->enclosure, '/');
  968. if (preg_match("/" . $delimiter_quoted . "|" . $enclosure_quoted . "|\n|\r/i", $value) || ($value{0} == ' ' || substr($value, -1) == ' ') || $this->enclose_all) {
  969. $value = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $value);
  970. $value = $this->enclosure . $value . $this->enclosure;
  971. }
  972. }
  973. return $value;
  974. }
  975. /**
  976. * Check file data
  977. *
  978. * @access protected
  979. *
  980. * @param file local filename
  981. *
  982. * @return true or false
  983. */
  984. protected function _check_data($file = null)
  985. {
  986. if (empty($this->file_data)) {
  987. if (is_null($file)) {
  988. $file = $this->file;
  989. }
  990. return $this->load_data($file);
  991. }
  992. return true;
  993. }
  994. /**
  995. * Check if passed info might be delimiter
  996. * Only used by find_delimiter
  997. *
  998. * @access protected
  999. *
  1000. * @param [type] $char [description]
  1001. * @param [type] $array [description]
  1002. * @param [type] $depth [description]
  1003. * @param [type] $preferred [description]
  1004. *
  1005. * @return special string used for delimiter selection, or false
  1006. */
  1007. protected function _check_count($char, $array, $depth, $preferred)
  1008. {
  1009. if ($depth == count($array)) {
  1010. $first = null;
  1011. $equal = null;
  1012. $almost = false;
  1013. foreach ($array as $key => $value) {
  1014. if ($first == null) {
  1015. $first = $value;
  1016. }
  1017. elseif ($value == $first && $equal !== false) {
  1018. $equal = true;
  1019. }
  1020. elseif ($value == $first + 1 && $equal !== false) {
  1021. $equal = true;
  1022. $almost = true;
  1023. }
  1024. else {
  1025. $equal = false;
  1026. }
  1027. }
  1028. if ($equal) {
  1029. $match = ($almost) ? 2 : 1;
  1030. $pref = strpos($preferred, $char);
  1031. $pref = ($pref !== false) ? str_pad($pref, 3, '0', STR_PAD_LEFT) : '999';
  1032. return $pref . $match . '.' . (99999 - str_pad($first, 5, '0', STR_PAD_LEFT));
  1033. }
  1034. else {
  1035. return false;
  1036. }
  1037. }
  1038. }
  1039. /**
  1040. * Read local file
  1041. *
  1042. * @access protected
  1043. *
  1044. * @param file local filename
  1045. *
  1046. * @return Data from file, or false on failure
  1047. */
  1048. protected function _rfile($file = null)
  1049. {
  1050. if (is_readable($file)) {
  1051. if (!($fh = fopen($file, 'r'))) {
  1052. return false;
  1053. }
  1054. $data = fread($fh, filesize($file));
  1055. fclose($fh);
  1056. return $data;
  1057. }
  1058. return false;
  1059. }
  1060. /**
  1061. * Write to local file
  1062. *
  1063. * @access protected
  1064. *
  1065. * @param file local filename
  1066. * @param string data to write to file
  1067. * @param mode fopen() mode
  1068. * @param lock flock() mode
  1069. *
  1070. * @return true or false
  1071. */
  1072. protected function _wfile($file, $string = '', $mode = 'wb', $lock = 2)
  1073. {
  1074. if ($fp = fopen($file, $mode)) {
  1075. flock($fp, $lock);
  1076. $re = fwrite($fp, $string);
  1077. $re2 = fclose($fp);
  1078. if ($re != false && $re2 != false) {
  1079. return true;
  1080. }
  1081. }
  1082. return false;
  1083. }
  1084. }