PageRenderTime 68ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Adapto/Handler/Import.php

http://github.com/egeniq/adapto
PHP | 1504 lines | 1036 code | 151 blank | 317 comment | 188 complexity | 281d0b3fce025d4c8d0b333e5f2a642e MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * This file is part of the Adapto Toolkit.
  4. * Detailed copyright and licensing information can be found
  5. * in the doc/COPYRIGHT and doc/LICENSE files which should be
  6. * included in the distribution.
  7. *
  8. * @package adapto
  9. * @subpackage handlers
  10. *
  11. * @copyright (c)2004 Ivo Jansch
  12. * @copyright (c)2004 Ibuildings.nl BV
  13. * @license http://www.achievo.org/atk/licensing ATK Open Source License
  14. *
  15. */
  16. /**
  17. * Handler for the 'import' action of an entity. The import action is a
  18. * generic tool for importing CSV files into a table.
  19. *
  20. * @author ijansch
  21. * @package adapto
  22. * @subpackage handlers
  23. *
  24. */
  25. class Adapto_Handler_Import extends Adapto_ActionHandler
  26. {
  27. public $m_importEntity; // defaulted to public
  28. /**
  29. * The action handler.
  30. * @param bool Always true
  31. */
  32. function action_import()
  33. {
  34. global $Adapto_VARS;
  35. //need to keep the postdata after a AF_LARGE selection in the allfield
  36. if (!isset($this->m_postvars["phase"]) && isset($Adapto_VARS['atkformdata']))
  37. foreach ($Adapto_VARS['atkformdata'] as $key => $value)
  38. $this->m_postvars[$key] = $value;
  39. $keys = array();
  40. //need to keep the selected item after an importerror
  41. if (is_array($Adapto_VARS['allFields']))
  42. $keys = array_keys($Adapto_VARS['allFields']);
  43. foreach ($keys as $key) {
  44. if (!isset($Adapto_VARS[$Adapto_VARS['allFields'][$key] . "_newsel"]))
  45. $Adapto_VARS[$Adapto_VARS['allFields'][$key] . "_newsel"] = $Adapto_VARS[$Adapto_VARS['allFields'][$key]];
  46. }
  47. $phase = ($this->m_postvars["phase"] != "" ? $this->m_postvars["phase"] : "init");
  48. switch ($phase) {
  49. case "init":
  50. $this->doInit();
  51. break;
  52. case "upload":
  53. $this->doUpload();
  54. break;
  55. case "process":
  56. $this->doProcess();
  57. break;
  58. }
  59. }
  60. /**
  61. * Sets the entity for this handler. Implicitly sets the
  62. * import entity too!
  63. *
  64. * @param atkEntity $entity entity instance
  65. *
  66. * @see setImportEntity
  67. */
  68. function setEntity(&$entity)
  69. {
  70. parent::setEntity($entity);
  71. $this->setImportEntity($entity);
  72. }
  73. /**
  74. * Sets the import entity. By default this is the same entity
  75. * as set by setEntity, but if you call this method after the setEntity
  76. * call you can override the import entity.
  77. *
  78. * @param atkEntity $entity entity instance
  79. *
  80. * @see setEntity
  81. */
  82. function setImportEntity(&$entity)
  83. {
  84. $this->m_importEntity = &$entity;
  85. }
  86. /**
  87. * Create import page for the given phase.
  88. *
  89. * @param string $phase import phase (init, upload, process)
  90. * @param string $content page content
  91. */
  92. function importPage($phase, $content)
  93. {
  94. $controller = &atkController::getInstance();
  95. $action = $controller->getPhpFile() . '?' . SID;
  96. $formStart = '<form id="entryform" name="entryform" enctype="multipart/form-data" action="' . $action . '" method="post">'
  97. . session_form(atkLevel() == 0 ? SESSION_NESTED : SESSION_REPLACE) . '<input type="hidden" name="atkentitytype" value="'
  98. . $this->m_entity->atkEntityType() . '" />' . '<input type="hidden" name="atkaction" value="' . $this->m_entity->m_action . '" />'
  99. . $controller->getHiddenVarsString();
  100. $buttons = $this->invoke('getImportButtons', $phase);
  101. $ui = &$this->m_entity->getUi();
  102. $page = &$this->m_entity->getPage();
  103. $this->m_entity->addStyle("style.css");
  104. $params = $this->m_entity->getDefaultActionParams(false);
  105. $params['header'] = $this->invoke('importHeader', $phase);
  106. $params['formstart'] = $formStart;
  107. $params['content'] = $content;
  108. $params['buttons'] = $buttons;
  109. $output = $ui->renderAction('import', $params);
  110. $params = array();
  111. $params['title'] = $this->m_entity->actionTitle('import');
  112. $params['content'] = $output;
  113. $output = $ui->renderBox($params);
  114. $output = $this->m_entity->renderActionPage('import', $output);
  115. $page->addContent($output);
  116. }
  117. /**
  118. * Import header.
  119. *
  120. * @param string $phase import phase ('init', 'upload', 'process', 'analyze')
  121. */
  122. function importHeader($phase)
  123. {
  124. return '';
  125. }
  126. /**
  127. * Get import buttons.
  128. *
  129. * @param string $phase import phase ('init', 'upload', 'process', 'analyze')
  130. */
  131. function getImportButtons($phase)
  132. {
  133. $result = array();
  134. if ($phase == 'init') {
  135. $result[] = '<input class="btn" type="submit" value="' . $this->m_entity->text("import_upload") . '">';
  136. } else if ($phase == 'analyze') {
  137. $result[] = '<input type="submit" class="btn" name="analyse" value="' . $this->m_entity->text("import_analyse") . '">';
  138. $result[] = '<input type="submit" class="btn" name="import" value="' . $this->m_entity->text("import_import") . '"> ';
  139. }
  140. if (atkLevel() > 0) {
  141. $result[] = atkButton($this->m_entity->text("cancel", "atk"), "", SESSION_BACK, true);
  142. }
  143. return $result;
  144. }
  145. /**
  146. * This function shows a form to upload a .csv
  147. * @param bool Always true
  148. */
  149. function doInit()
  150. {
  151. $content = '
  152. <input type="hidden" name="phase" value="upload">
  153. <table border="0">
  154. <tr>
  155. <td style="text-align: left">
  156. ' . $this->m_entity->text("import_upload_explanation")
  157. . '
  158. <br /><br />
  159. <input type="file" name="csvfile">
  160. </td>
  161. </tr>
  162. </table>';
  163. $this->invoke('importPage', 'init', $content);
  164. }
  165. /**
  166. * This function takes care of uploaded file
  167. */
  168. function doUpload()
  169. {
  170. $fileid = uniqid("file_");
  171. $filename = $this->getTmpFileDestination($fileid);
  172. if (!move_uploaded_file($_FILES['csvfile']['tmp_name'], $filename)) {
  173. $this->m_entity->redirect($this->m_entity->feedbackUrl("import", ACTION_FAILED));
  174. } else {
  175. // file uploaded
  176. $this->doAnalyze($fileid);
  177. }
  178. }
  179. /**
  180. * This function checks if there is enough information to import the date
  181. * else it wil shows a form to set how the file wil be imported
  182. */
  183. function doProcess()
  184. {
  185. $filename = $this->getTmpFileDestination($this->m_postvars["fileid"]);
  186. if ($this->m_postvars["import"] != "") {
  187. $this->doImport($filename);
  188. } else {
  189. // reanalyze
  190. $this->doAnalyze($this->m_postvars["fileid"]);
  191. }
  192. }
  193. /**
  194. * This function shows a form where the user can choose the mapping of the column,
  195. * an allfield and if the first record must be past over
  196. *
  197. * @param string $fileid the id of the uploaded file
  198. * @param array $importerrors An array with the import errors
  199. */
  200. function doAnalyze($fileid, $importerrors = array())
  201. {
  202. $sessionMgr = &atkGetSessionManager();
  203. $filename = $this->getTmpFileDestination($fileid);
  204. $rows = $this->getSampleRows($filename);
  205. $delimiter = $sessionMgr->pageVar("delimiter");
  206. if ($delimiter == "")
  207. $delimiter = $this->estimateDelimiter($rows);
  208. $enclosure = $sessionMgr->pageVar("enclosure");
  209. if ($enclosure == "")
  210. $enclosure = $this->estimateEnclosure($rows);
  211. $allFields = $sessionMgr->pageVar("allFields");
  212. if ($allFields == "")
  213. $allFields = array();
  214. $skipfirstrow = $this->m_postvars['skipfirstrow'];
  215. $doupdate = $this->m_postvars['doupdate'];
  216. $updatekey1 = $this->m_postvars['updatekey1'];
  217. $onfalseidentifier = $this->m_postvars['onfalseid'];
  218. $novalidatefirst = $this->m_postvars['novalidatefirst'];
  219. $columncount = $this->estimateColumnCount($rows, $delimiter);
  220. $csv_data = $this->fgetcsvfromarray($rows, $columncount, $delimiter, $enclosure);
  221. $col_map = $this->m_postvars["col_map"];
  222. if (!is_array($col_map)) {
  223. // init colmap
  224. $col_map = $this->initColmap($csv_data[0], $matchFound);
  225. }
  226. if ($skipfirstrow === null) {
  227. $skipfirstrow = $matchFound;
  228. }
  229. if ($columncount > count($col_map)) {
  230. // fill with ignored
  231. for ($i = 0, $_i = ($columncount - count($col_map)); $i < $_i; $i++)
  232. $col_map[] = "-";
  233. }
  234. $rowCount = $this->getRowCount($filename, $skipfirstrow);
  235. // Display sample
  236. $sample = atktext("import_sample") . ':<br><br><table class="recordlist">' . $this->_getAnalyseSample($columncount, $col_map, $csv_data, $skipfirstrow);
  237. $content = '
  238. <input type="hidden" name="phase" value="process">
  239. <div style="text-align: left; margin-left: 10px;">
  240. ' . $this->_getAnalyseHeader($fileid, $columncount, $delimiter, $enclosure, $rowCount) . '
  241. <br />
  242. ' . $this->_getErrors($importerrors) . '
  243. ' . $sample . '
  244. <br />
  245. ' . $this->_getAnalyseExtraOptions($skipfirstrow, $doupdate, $updatekey1, $onfalseidentifier, $allFields, $novalidatefirst) . '
  246. </div>';
  247. $page = &$this->m_entity->getPage();
  248. $theme = Adapto_ClassLoader::getInstance("Adapto_Ui_Theme");
  249. $page->register_style($theme->stylePath("recordlist.css"));
  250. $this->invoke('importPage', 'analyze', $content);
  251. }
  252. /**
  253. * Transforms the $importerrors array into displayable HTML
  254. *
  255. * @todo make this use templates
  256. *
  257. * @param Array $importerrors A special array with arrays in it
  258. * $importerrors[0] are general errors, other than that
  259. * the numbers stand for recordnumbers
  260. * @return String HTML table with the errors
  261. */
  262. function _getErrors($importerrors)
  263. {
  264. if (is_array($importerrors)) {
  265. $content .= "\n<table>";
  266. $errorCount = 0;
  267. foreach ($importerrors as $record => $errors) {
  268. $errorCount++;
  269. if ($errorCount > Adapto_Config::getGlobal("showmaximporterrors", 50))
  270. break;
  271. if ($record == 0 && Adapto_value_in_array($errors)) {
  272. $content .= "<tr><td colSpan=2>";
  273. foreach ($errors as $error) {
  274. if (!empty($error))
  275. $content .= "<span class=\"error\">" . text($error['msg']) . $error['spec'] . "</span><br />";
  276. }
  277. $content .= "</td></tr>";
  278. } else if (Adapto_value_in_array($errors)) {
  279. $content .= "<tr><td valign=\"top\" class=\"error\">";
  280. $content .= "<b>Record $record:</b>&nbsp;";
  281. $content .= "</td><td valign=\"top\" class=\"error\">";
  282. $counter = 0;
  283. for ($counter = 0; $counter < count($errors) && $counter < Adapto_Config::getGlobal("showmaximporterrors", 50); $counter++) {
  284. $content .= $this->m_entity->text($errors[$counter]['msg']) . $errors[$counter]['spec'] . "<br />";
  285. }
  286. $content .= "</td></tr>";
  287. }
  288. }
  289. $content .= "</tr></table><br />";
  290. }
  291. return $content;
  292. }
  293. /**
  294. * Returns the HTML header for the 'analyse' mode of the import handler
  295. * @param String $fileid The 'id' (name) of the file we are importing
  296. * @param String $columncount The number of columns we have
  297. * @param String $delimiter The delimiter in the file
  298. * @param String $enclosure The enclosure in the file
  299. * @param int $rowcount The number of rows in the CSV file
  300. * @return String The HTML header
  301. */
  302. function _getAnalyseHeader($fileid, $columncount, $delimiter, $enclosure, $rowcount)
  303. {
  304. $content = '<br>';
  305. $content .= '<input type="hidden" name="fileid" value="' . $fileid . '">';
  306. $content .= '<input type="hidden" name="columncount" value="' . $columncount . '">';
  307. $content .= '<table border="0">';
  308. $content .= '<tr><td>' . text("delimiter") . ': </td><td><input type="text" size="2" name="delimiter" value="' . Adapto_htmlentities($delimiter)
  309. . '"></td></tr>';
  310. $content .= '<tr><td>' . text("enclosure") . ': </td><td><input type="text" size="2" name="enclosure" value="' . Adapto_htmlentities($enclosure)
  311. . '"></td></tr>';
  312. $content .= '<tr><td>' . atktext("import_detectedcolumns") . ': </td><td>' . $columncount . '</td></tr>';
  313. $content .= '<tr><td>' . atktext("import_detectedrows") . ': </td><td>' . $rowcount . '</td></tr>';
  314. $content .= '</table>';
  315. return $content;
  316. }
  317. /**
  318. * Returns a sample of the analysis
  319. * @param String $columncount The number of columns we have
  320. * @param String $col_map A mapping of the column
  321. * @param String $csv_data The CSV data
  322. * @param String $skipfirstrow Wether or not to skip the first row
  323. */
  324. function _getAnalyseSample($columncount, $col_map, $csv_data, $skipfirstrow)
  325. {
  326. // header
  327. $sample .= '<tr>';
  328. for ($j = 1; $j <= $columncount; $j++) {
  329. $sample .= '<th>';
  330. $sample .= ucfirst(atktext("column")) . ' ' . $j;
  331. $sample .= '</th>';
  332. }
  333. $sample .= '</tr>';
  334. // column assign
  335. $sample .= '<tr>';
  336. for ($j = 0; $j < $columncount; $j++) {
  337. $sample .= '<th>';
  338. $sample .= $this->getAttributeSelector($j, $col_map[$j]);
  339. $sample .= '</th>';
  340. }
  341. $sample .= '</tr>';
  342. // sample data
  343. for ($i = 0; $i < count($csv_data); $i++) {
  344. $line = $csv_data[$i];
  345. $sample .= '<tr class="row' . (($i % 2) + 1) . '">';
  346. for ($j = 0; $j < $columncount; $j++) {
  347. if ($i == 0 && $skipfirstrow) {
  348. $sample .= '<th>';
  349. $sample .= atktext(trim($line[$j]));
  350. } else {
  351. $sample .= '<td>';
  352. if ($col_map[$j] != "" && $col_map[$j] != "-") {
  353. $display = $this->_getSampleValue($col_map[$j], trim($line[$j]));
  354. if ($display)
  355. $sample .= $display;
  356. else
  357. $sample .= atktext($col_map[$j]);
  358. if ((string) $display !== (string) $line[$j]) {
  359. // Also display raw value so we can verify
  360. $sample .= ' <i style="color: #777777">(' . trim($line[$j]) . ")</i>";
  361. }
  362. } else if ($col_map[$j] == "-") {
  363. // ignoring.
  364. $sample .= '<div style="color: #777777">' . trim($line[$j]) . '</div>';
  365. } else {
  366. $sample .= trim($line[$j]);
  367. }
  368. }
  369. $sample .= ($i == 0 && $skipfirstrow) ? '</th>' : '</td>';
  370. }
  371. $sample .= '</tr>';
  372. }
  373. $sample .= '</table>';
  374. return $sample;
  375. }
  376. /**
  377. * Gets the displayable value for the attribute
  378. * @param String $attributename The name of the attribute
  379. * @param String $value The value of the attribute
  380. * @return String The displayable value for the attribute
  381. */
  382. function _getSampleValue($attributename, $value)
  383. {
  384. $attr = &$this->getUsableAttribute($attributename);
  385. if (method_exists($attr, "parseTime"))
  386. $newval = $attr->parseTime($value);
  387. else
  388. $newval = $attr->parseStringValue($value);
  389. if (method_exists($attr, "createDestination")) {
  390. $attr->createDestination();
  391. // If we can create a destination, then we can be reasonably sure it's a relation
  392. // and importing in a relation is a different matter altogether
  393. $searchresults = $attr->m_destInstance->searchDb($newval);
  394. if (count($searchresults) == 1) {
  395. $atkval = array(
  396. $attributename => array($attr->m_destInstance->primaryKeyField() => $searchresults[0][$attr->m_destInstance->primaryKeyField()]));
  397. }
  398. } else {
  399. $atkval = array($attributename => $newval);
  400. }
  401. return $attr->display($atkval);
  402. }
  403. /**
  404. * Returns the extra options of the importhandler
  405. * @param String $skipfirstrow Wether or not to skip the first row
  406. * @param String $doupdate Wether or not to do an update
  407. * @param String $updatekey1 The key to update on
  408. * @param String $onfalseidentifier What to do on a false identifier
  409. * @param String $allFields The fields to import
  410. * @param Bool $novalidatefirst Validate before the import
  411. * @return String The HTML with the extra options
  412. */
  413. function _getAnalyseExtraOptions($skipfirstrow, $doupdate, $updatekey1, $onfalseidentifier, $allFields, $novalidatefirst)
  414. {
  415. $content .= '<br /><table id="importoptions">';
  416. $content .= ' <tr>';
  417. $content .= ' <td>';
  418. foreach ($allFields as $allfield) {
  419. if (!$this->m_postvars[$allfield])
  420. $noallfieldvalue = true;
  421. }
  422. if (empty($allFields) || !$noallfieldvalue)
  423. $allFields[] = '';
  424. foreach ($allFields as $allField) {
  425. $content .= atktext("import_allfield") . ': </td><td>' . $this->getAttributeSelector(0, $allField, "allFields[]");
  426. if ($allField != "") {
  427. $attr = $this->getUsableAttribute($allField);
  428. if (is_object($attr)) {
  429. $fakeeditarray = array($allField => $this->m_postvars[$allField]);
  430. $content .= ' ' . atktext("value") . ': ' . $attr->edit($fakeeditarray, "", "edit") . '<br/>';
  431. }
  432. }
  433. $content .= '</td></tr><tr><td>';
  434. }
  435. $content .= atktext("import_skipfirstrow") . ': </td><td><input type="checkbox" name="skipfirstrow" class="atkcheckbox" value="1" '
  436. . ($skipfirstrow ? "CHECKED" : "") . '/>';
  437. $content .= '</td></tr><tr><td>';
  438. $content .= atktext("import_doupdate") . ': </td><td> <input type="checkbox" name="doupdate" class="atkcheckbox" value="1" '
  439. . ($doupdate ? "CHECKED" : "") . '/>';
  440. $content .= '</td></tr><tr><td>';
  441. $content .= atktext("import_update_key") . ': </td><td>' . $this->getAttributeSelector(0, $updatekey1, "updatekey1", 2) . '</td>';
  442. $content .= '</td></tr><tr><td>';
  443. $content .= atktext("import_onfalseidentifier") . ': </td><td> <input type="checkbox" name="onfalseid" class="atkcheckbox" value="1" '
  444. . ($onfalseidentifier ? "CHECKED" : "") . '/>';
  445. $content .= '</td></tr><tr><td>';
  446. $content .= atktext("import_novalidatefirst") . ': </td><td> <input type="checkbox" name="novalidatefirst" class="atkcheckbox" value="1" '
  447. . ($novalidatefirst ? "CHECKED" : "") . '/>';
  448. $content .= ' </td>';
  449. $content .= ' </tr>';
  450. $content .= '</table><br /><br />';
  451. return $content;
  452. }
  453. /**
  454. * Get the destination of the uploaded csv-file
  455. * @param string $fileid The id of the file
  456. * @return string The path of the file
  457. */
  458. function getTmpFileDestination($fileid)
  459. {
  460. return Adapto_Config::getGlobal("atktempdir") . "csv_import_$fileid.csv";
  461. }
  462. /**
  463. * Get data from each line
  464. * @param Array $arr An array with the lines from the CSV file
  465. * @param int $columncount The number of columns in the file
  466. * @param String $delimiterChar The delimeter character
  467. * @param String $enclosureChar The enclosure character
  468. * @return Array An array with the CSV data
  469. */
  470. function fgetcsvfromarray($arr, $columncount, $delimiterChar = ',', $enclosureChar = '"')
  471. {
  472. $result = array();
  473. foreach ($arr as $line) {
  474. $result[] = $this->fgetcsvfromline($line, $columncount, $delimiterChar, $enclosureChar);
  475. }
  476. return $result;
  477. }
  478. /**
  479. * Gets the char which is used for enclosure in the csv-file
  480. * @param Array $rows The rows from the csv-file
  481. * @return String The enclosure
  482. */
  483. function estimateDelimiter($rows)
  484. {
  485. if (!is_array($rows) || count($rows) == 0)
  486. return ",";
  487. if (strpos($rows[0], ";") !== false)
  488. return ";";
  489. if (strpos($rows[0], ",") !== false)
  490. return ",";
  491. if (strpos($rows[0], ":") !== false)
  492. return ":";
  493. else
  494. return ";";
  495. }
  496. /**
  497. * Gets the char which is used for enclosure in the csv-file
  498. * @param Array $rows The rows from the csv-file
  499. * @return String The enclosure
  500. */
  501. function estimateEnclosure($rows)
  502. {
  503. if (!is_array($rows) || count($rows) == 0)
  504. return '"';
  505. if (substr_count($rows[0], '"') >= 2)
  506. return '"';
  507. return '';
  508. }
  509. /**
  510. * Counts the number of columns in the first row
  511. * @param Array $rows The rows from the csv-file
  512. * @param String $delimiter The char which seperate the fields
  513. * @return int The number of columns
  514. */
  515. function estimateColumnCount($rows, $delimiter)
  516. {
  517. if (!is_array($rows) || count($rows) == 0)
  518. return 0;
  519. if ($delimiter == "")
  520. return 1;
  521. return (substr_count($rows[0], $delimiter) + 1);
  522. }
  523. /**
  524. * Get the first 5 lines from the csv-file
  525. * @param String $file The path to the csv-file
  526. * @return Array The 5 lines from the csv file
  527. */
  528. function getSampleRows($file)
  529. {
  530. $result = array();
  531. $fp = fopen($file, "r");
  532. for ($i = 0; $i < 5; $i++) {
  533. $line = fgets($fp);
  534. if ($line !== false) {
  535. $result[] = $line;
  536. }
  537. }
  538. fclose($fp);
  539. return $result;
  540. }
  541. /**
  542. * Returns the CSV line count.
  543. *
  544. * @param string $file the path to the csv-file
  545. * @param bool $skipFirstRow Skip the first row?
  546. * @return int row count
  547. */
  548. function getRowCount($file, $skipFirstRow)
  549. {
  550. $count = 0;
  551. $fp = fopen($file, "r");
  552. while ($line = fgets($fp)) {
  553. if (trim($line) == "")
  554. continue;
  555. $count++;
  556. }
  557. return $count - ($count > 0 && $skipFirstRow ? 1 : 0);
  558. }
  559. function fgetcsvfromline($line, $columncount, $delimiterChar = ',', $enclosureChar = '"')
  560. {
  561. $line = trim($line);
  562. // if we haven't got an enclosure char, the only thing we can do is
  563. // splitting it using the delimiterChar - no further action needed
  564. if (!$enclosureChar) {
  565. return explode($delimiterChar, $line);
  566. }
  567. if ($line{0} == $delimiterChar) {
  568. $line = $enclosureChar . $enclosureChar . $line;
  569. }
  570. if (substr($line, -1) == $delimiterChar)
  571. $line .= $enclosureChar . $enclosureChar;
  572. $reDelimiterChar = preg_quote($delimiterChar, '/');
  573. $reEnclosureChar = preg_quote($enclosureChar, '/');
  574. // Some exports don't enclose empty or numeric fields with the enclosureChar. Let's fix
  575. // that first so we can use one preg_split statement that works in those cases too.
  576. // loop until all occurrences are replaced. Contains an infinite loop prevention.
  577. for ($fix = "", $i = 0, $_i = substr_count($line, $delimiterChar); $fix != $line && $i < $_i; $i++) {
  578. if ($fix != "")
  579. $line = $fix;
  580. $pattern = '/' . $reDelimiterChar . '([^\\\\' . $reDelimiterChar . $reEnclosureChar . ']*)' . $reDelimiterChar . '/';
  581. $fix = preg_replace($pattern, $delimiterChar . $enclosureChar . '\\1' . $enclosureChar . $delimiterChar, $line);
  582. }
  583. $line = $fix;
  584. // fix an unquoted string at line end, if any
  585. $pattern = '/' . $reDelimiterChar . '([^\\\\' . $reDelimiterChar . $reEnclosureChar . ']*)$/';
  586. $line = preg_replace($pattern, $delimiterChar . $enclosureChar . '\\1' . $enclosureChar, $line);
  587. // chop the first and last enclosures so they aren't split at
  588. $start = (($line[0] == $enclosureChar) ? 1 : 0);
  589. if ($line[strlen($line) - 1] == $enclosureChar) {
  590. $line = substr($line, $start, -1);
  591. } else {
  592. $line = substr($line, $start);
  593. }
  594. // now split by delimiter
  595. $expression = '/' . $reEnclosureChar . ' *' . $reDelimiterChar . '*' . $reEnclosureChar . '/';
  596. return preg_split($expression, $line);
  597. }
  598. /**
  599. * Gives all the attributes that can be used for the import
  600. * @param bool $obligatoryOnly if false then give all attributes, if true then give only the obligatory ones
  601. * defaults to false
  602. * @return Array the attributes
  603. */
  604. function getUsableAttributes($obligatoryOnly = false)
  605. {
  606. $attrs = array();
  607. foreach (array_keys($this->m_importEntity->m_attribList) as $attribname) {
  608. $attrib = &$this->m_importEntity->getAttribute($attribname);
  609. if ($this->integrateAttribute($attrib)) {
  610. $attrib->createDestination();
  611. foreach (array_keys($attrib->m_destInstance->m_attribList) as $relattribname) {
  612. $relattrib = &$attrib->m_destInstance->getAttribute($relattribname);
  613. if ($this->_usableForImport($obligatoryOnly, $relattrib)) {
  614. $attrs[] = $relattribname;
  615. }
  616. }
  617. } else {
  618. if ($this->_usableForImport($obligatoryOnly, $attrib)) {
  619. $attrs[] = $attribname;
  620. }
  621. }
  622. }
  623. return $attrs;
  624. }
  625. /**
  626. * Check if an attribute is usable for import.
  627. * @param bool $obligatoryOnly Wether or not we should concider obligatory attributes
  628. * @param Object &$attrib The attribute
  629. * @return bool Wether or not the attribute is usable for import
  630. */
  631. function _usableForImport($obligatoryOnly, &$attrib)
  632. {
  633. return ((!$obligatoryOnly || $this->isObligatory($attrib)) && !$attrib->hasFlag(AF_AUTOINCREMENT) && !$this->isHide($attrib)
  634. && !is_a($attrib, 'atkdummyattribute'));
  635. }
  636. /**
  637. * Gives all obligatory attributes
  638. *
  639. * Same as getUsableAttributes with parameter true
  640. * @return Array An array with all the obligatory attributes
  641. */
  642. function getObligatoryAttributes()
  643. {
  644. return $this->getUsableAttributes(true);
  645. }
  646. /**
  647. * Checks whether the attribute is obligatory
  648. * @param Object $attr The attribute to check
  649. * @return boolean The result of the check
  650. */
  651. function isObligatory($attr)
  652. {
  653. return ($attr->hasFlag(AF_OBLIGATORY) && !$this->isHide($attr));
  654. }
  655. /**
  656. * Checks whether the attribute is hiden by a flag
  657. * @param Object $attr The attribute to check
  658. * @return boolean The result of the check
  659. */
  660. function isHide($attr)
  661. {
  662. return (($attr->hasFlag(AF_HIDE) || ($attr->hasFlag(AF_HIDE_ADD) && $attr->hasFlag(AF_HIDE_EDIT))) && !$attr->hasFlag(AF_FORCE_LOAD));
  663. }
  664. /**
  665. * Checks whether the attribute has the flag AF_ONETOONE_INTEGRATE
  666. * @param Object $attr The attribute to check
  667. * @return boolean The result of the check
  668. */
  669. function integrateAttribute($attr)
  670. {
  671. return in_array(get_class($attr), array("atkonetoonerelation", "atksecurerelation")) && $attr->hasFlag(AF_ONETOONE_INTEGRATE);
  672. }
  673. /**
  674. * Get al attributes from the import entity that have the flag AF_ONETOONE_INTEGRATE
  675. * @return array A list with all attributes from the import entity that have the flag AF_ONETOONE_INTEGRATE
  676. */
  677. function getIntegratedAttributes()
  678. {
  679. $attrs = array();
  680. foreach (array_keys($this->m_importEntity->m_attribList) as $attribname) {
  681. $attrib = &$this->m_importEntity->getAttribute($attribname);
  682. if ($this->integrateAttribute($attrib)) {
  683. $attrs[] = $attribname;
  684. }
  685. }
  686. return $attrs;
  687. }
  688. /**
  689. * Check whether the attribute is part of a relation
  690. * @param String $attrname name of the attribute
  691. * @return mixed false if not, relation name if yes
  692. */
  693. function isRelationAttribute($attrname)
  694. {
  695. if (array_key_exists($attrname, $this->m_importEntity->m_attribList))
  696. return false;
  697. foreach ($this->getIntegratedAttributes() as $attr) {
  698. $relattr = $this->m_importEntity->getAttribute($attr);
  699. $relattr->createDestination();
  700. if (array_key_exists($attrname, $relattr->m_destInstance->m_attribList))
  701. return $attr;
  702. }
  703. return false;
  704. }
  705. /**
  706. * Check whether the attribute has a relation (only manytoonerelations)
  707. * @param String $attrname name of the attribute
  708. * @return boolean result of the check
  709. */
  710. function hasRelationAttribute($attrname)
  711. {
  712. return in_array(get_class($this->getUsableAttribute($attrname)), array("atkmanytoonerelation", "atkmanytoonetreerelation"));
  713. }
  714. /**
  715. * Get the real attribute (instance) by his name
  716. * @param String $name name of the attribute
  717. * @return object instance of the attribute
  718. */
  719. function &getUsableAttribute($name)
  720. {
  721. if (array_key_exists($name, $this->m_importEntity->m_attribList))
  722. return $this->m_importEntity->getAttribute($name);
  723. foreach ($this->getIntegratedAttributes() as $attr) {
  724. $relattr = $this->m_importEntity->getAttribute($attr);
  725. $relattr->createDestination();
  726. if (array_key_exists($name, $relattr->m_destInstance->m_attribList))
  727. return $relattr->m_destInstance->getAttribute($name);
  728. }
  729. return null;
  730. }
  731. /**
  732. * Add one value to the record
  733. * @param Array $record the record wich will be changed
  734. * @param String $attrname the name of the attribute
  735. * @param String $value the value of that attribute
  736. */
  737. function addToRecord(&$record, $attrname, $value)
  738. {
  739. $attr = &$this->getUsableAttribute($attrname);
  740. if (!is_object($attr))
  741. return;
  742. foreach ($this->getIntegratedAttributes() as $intattr) {
  743. if (!isset($record[$intattr]))
  744. $record[$intattr] = array('mode' => "add", 'atkaction' => "save");
  745. }
  746. $record[$attrname] = $value;
  747. }
  748. /**
  749. * Returns a dropdownlist with all possible field in the importentity
  750. * @param int $index the number of the column
  751. * @param String $value the name of the attribute that is selected in the list (if empty then select the last one)
  752. * @param String $othername if set, use a other name for the dropdown, else use the name "col_map[index]"
  753. * @param int $emptycol mode for empty column (0 = no empty column, 1= empty column, 2= an 'ignore this column' (default))
  754. * @return String the html-code for the dropdownlist (<select>...</sekect>)
  755. */
  756. function getAttributeSelector($index = 0, $value = "", $othername = "", $emptycol = 2)
  757. {
  758. if (!$othername)
  759. $res = '<select name="col_map[' . $index . ']">';
  760. else
  761. $res = '<select name="' . $othername . '" onchange="entryform.submit()">';
  762. $j = 0;
  763. $hasoneselected = false;
  764. $attrs = $this->getUsableAttributes();
  765. foreach ($attrs as $attribname) {
  766. $attr = &$this->getUsableAttribute($attribname);
  767. $label = $attr->label();
  768. $selected = "";
  769. if ($value != "" && $value == $attribname) {
  770. // select the next.
  771. $selected = "selected";
  772. $hasoneselected = true;
  773. }
  774. $res .= '<option value="' . $attribname . '" ' . $selected . '>' . $label . "\n";
  775. $j++;
  776. }
  777. if ($emptycol == 2)
  778. $res .= '<option value="-" ' . (($value == "-" || !$hasoneselected) ? "selected" : "") . ' style="font-style: italic">'
  779. . atktext("import_ignorecolumn");
  780. elseif ($emptycol == 1)
  781. $res .= '<option value="" ' . ((!$value || !$hasoneselected) ? "selected" : "") . '>';
  782. $res .= '</select>';
  783. return $res;
  784. }
  785. /**
  786. * The same als the php function array_search, but now much better.
  787. * This function is not case sensitive
  788. * @param Array $array The array to search through
  789. * @param mixed $value The value to search for
  790. * @return mixed The key if it is in the array, else false
  791. */
  792. function inArray($array, $value)
  793. {
  794. foreach ($array as $key => $item) {
  795. if (strtolower($item) == strtolower($value))
  796. return $key;
  797. if (strtolower($item) == strtolower(atktext($value, $this->m_entity->m_module, $this->m_entity->m_type)))
  798. return $key;
  799. }
  800. return false;
  801. }
  802. /**
  803. * Make a record of translations of the given attributes
  804. * @param Array $attributes The attributes to translate
  805. * @return Array The result of the translation
  806. */
  807. function getAttributesTranslation($attributes)
  808. {
  809. $result = array();
  810. foreach ($attributes as $attribute) {
  811. $attr = &$this->getUsableAttribute($attribute);
  812. $label = $attr->label();
  813. $result[] = $label;
  814. }
  815. return $result;
  816. }
  817. /**
  818. * Tries to make a default col_map with the first record of the csv-file
  819. * @param Array $firstRecord The first record of the CSV file
  820. * @param Bool &$matchFound Found a match?
  821. * @return Array The default col_map
  822. */
  823. function initColmap($firstRecord, &$matchFound)
  824. {
  825. $result = array();
  826. $attributes = $this->getUsableAttributes();
  827. $translations = $this->getAttributesTranslation($attributes);
  828. $matchFound = false;
  829. foreach ($firstRecord as $value) {
  830. $key = $this->inArray($attributes, $value);
  831. if ($key) {
  832. $result[] = $attributes[$key];
  833. $matchFound = true;
  834. } else {
  835. //checks the translation
  836. $key = $this->inArray($translations, $value);
  837. if ($key !== false) {
  838. $result[] = $attributes[$key];
  839. $matchFound = true;
  840. } else
  841. $result[] = "-";
  842. }
  843. }
  844. return $result;
  845. }
  846. /**
  847. * Add the allField to the col_map array
  848. * but only if a valid field is selected
  849. * @param Array $col_map The map of columns (!stub)
  850. * @return mixed The value for the field to use with all records
  851. */
  852. function getAllFieldsValues(&$col_map)
  853. {
  854. $allFields = $this->m_postvars["allFields"];
  855. foreach ($allFields as $key => $allField) {
  856. if ($allField != "") {
  857. $attr = &$this->getUsableAttribute($allField);
  858. if ($attr) {
  859. $col_map[] = $allField;
  860. }
  861. //get the value from the postvars
  862. $allFieldValue = $this->m_postvars[$allField];
  863. if (strstr($allFieldValue, "=")) {
  864. $allFieldValue = substr(strstr($allFieldValue, "="), 2);
  865. $allFieldValue = substr($allFieldValue, 0, Adapto_strlen($allFieldValue) - 1);
  866. }
  867. $allFieldsValues[$allField] = $allFieldValue;
  868. }
  869. }
  870. return $allFieldsValues;
  871. }
  872. /**
  873. * The real import function actually imports the importfile
  874. *
  875. * @param Bool $nopost
  876. */
  877. function doImport($nopost = false)
  878. {
  879. ini_set('max_execution_time', 300);
  880. $db = &$this->m_importEntity->getDb();
  881. $fileid = $this->m_postvars["fileid"];
  882. $file = $this->getTmpFileDestination($fileid);
  883. $validated = $this->getValidatedRecords($file);
  884. if (!$this->m_postvars['novalidatefirst'] && $this->showErrors($validated['importerrors'])) {
  885. $db->rollback();
  886. return;
  887. }
  888. $this->addRecords($validated['importerrors'], $validated['validatedrecs']);
  889. if (!$this->m_postvars['novalidatefirst'] && $this->showErrors($validated['importerrors'])) {
  890. $db->rollback();
  891. return;
  892. }
  893. $db->commit();
  894. // clean-up
  895. @unlink($file);
  896. // clear recordlist cache
  897. $this->clearCache();
  898. // register message
  899. $messageQueue = &atkMessageQueue::getInstance();
  900. $count = count((array) $validated['validatedrecs']['add']) + count((array) $validated['validatedrecs']['update']);
  901. if ($count == 0) {
  902. $messageQueue->addMessage(sprintf($this->m_entity->text('no_records_to_import'), $count), AMQ_GENERAL);
  903. } else if ($count == 1) {
  904. $messageQueue->addMessage($this->m_entity->text('successfully_imported_one_record'), AMQ_SUCCESS);
  905. } else {
  906. $messageQueue->addMessage(sprintf($this->m_entity->text('successfully_imported_x_records'), $count), AMQ_SUCCESS);
  907. }
  908. $this->m_entity->redirect();
  909. }
  910. /**
  911. * Get the validated records
  912. *
  913. * @param String $file The import csv file
  914. * @return Array with importerrors and validatedrecs
  915. */
  916. function getValidatedRecords($file)
  917. {
  918. $enclosure = $this->m_postvars["enclosure"];
  919. $delimiter = $this->m_postvars["delimiter"];
  920. $columncount = $this->m_postvars["columncount"];
  921. $skipfirstrow = $this->m_postvars['skipfirstrow'];
  922. $allFields = $this->m_postvars["allFields"];
  923. $col_map = $this->m_postvars["col_map"];
  924. $allFieldsValues = $this->getAllFieldsValues($col_map);
  925. $initial_values = $this->m_importEntity->initial_values();
  926. $validatedrecs = array();
  927. $validatedrecs["add"] = array();
  928. $validatedrecs["update"] = array();
  929. $importerrors = array();
  930. $importerrors[0] = array();
  931. $importerrors[0] = array_merge($importerrors[0], $this->checkImport($col_map, $initial_values));
  932. $allfielderror = $this->checkAllFields($allFields, $allFieldsValues);
  933. if ($allfielderror) {
  934. $importerrors[0][] = $allfielderror;
  935. }
  936. if (count($importerrors[0]) > 0) {
  937. // don't start importing if even the minimum requirements haven't been met
  938. return array('importerrors' => &$importerrors, 'validatedrecs' => array());
  939. }
  940. static $mb_converting_exists = null;
  941. if (!isset($mb_converting_exists)) {
  942. $mb_converting_exists = function_exists("mb_convert_encoding");
  943. Adapto_Util_Debugger::debug('Checking function_exists("mb_convert_encoding")');
  944. }
  945. static $atkCharset = null;
  946. if (!isset($atkCharset)) {
  947. $atkCharset = atkGetCharset();
  948. Adapto_Util_Debugger::debug('setting atkcharset static!');
  949. }
  950. //copy the csv in a record and add it to the db
  951. $fp = fopen($file, "r");
  952. if ($skipfirstrow == "1")
  953. $line = fgets($fp);
  954. for ($line = fgets($fp), $counter = 1; $line !== false; $line = fgets($fp), $counter++) {
  955. Adapto_Util_Debugger::debug("Validating record nr. $counter");
  956. //if we have an empty line, pass it
  957. if (trim($line) == "")
  958. continue;
  959. //large import are a problem for the maximum execution time, so we want to set for each
  960. //loop of the for-loop an maximum execution time
  961. set_time_limit(60);
  962. Adapto_Util_Debugger::debug('set_time_limit(60)');
  963. if ($atkCharset != '' && $mb_converting_exists)
  964. $line = mb_convert_encoding($line, $atkCharset);
  965. $data = $this->fgetcsvfromline($line, $columncount, $delimiter, $enclosure);
  966. $rec = $initial_values;
  967. for ($i = 0, $_i = count($col_map); $i < $_i; $i++) {
  968. if ($col_map[$i] != "-") {
  969. if (!in_array($col_map[$i], $allFields))// column is mapped
  970. {
  971. $value = $this->_getAttributeValue($col_map[$i], $allFields, $data[$i], $importerrors, $counter, $rec);
  972. } else //this is the allField
  973. {
  974. $value = $allFieldsValues[$col_map[$i]];
  975. }
  976. $this->addToRecord($rec, $col_map[$i], $value);
  977. }
  978. }
  979. $this->validateRecord($rec, $validatedrecs, $importerrors, $counter);
  980. }
  981. // close file
  982. @fclose($fp);
  983. return array('importerrors' => &$importerrors, 'validatedrecs' => &$validatedrecs);
  984. }
  985. /**
  986. * Gets the ATK value of the attribute
  987. *
  988. * @param String $attributename The name of the attribute
  989. * @param Array $allFields Array with all the fields
  990. * @param mixed $value The value from the CSV file
  991. * @param Array &$importerrors Any import errors which may occur or may have occured
  992. * @param Integer $counter The counter of the validatedrecords
  993. * @param Array $rec The record
  994. *
  995. * @return mixed The ATK value of the field
  996. */
  997. function _getAttributeValue($attributename, $allFields, $value, &$importerrors, $counter, $rec)
  998. {
  999. $updatekey1 = $this->m_postvars['updatekey1'];
  1000. $attr = &$this->getUsableAttribute($attributename);
  1001. if (method_exists($attr, "createDestination") && $attr->createDestination() && !in_array($attributename, $allFields)) {
  1002. $primaryKeyAttr = $attr->m_destInstance->getAttribute($attr->m_destInstance->primaryKeyField());
  1003. $isNumeric = $attr->hasFlag(AF_AUTO_INCREMENT) || is_a($primaryKeyAttr, 'atknumberattribute');
  1004. $relationselect = array();
  1005. // this check only works if either the primary key column is non-numeric or the given value is numeric
  1006. if (!$isNumeric || is_numeric($value)) {
  1007. $relationselect = $attr->m_destInstance
  1008. ->selectDb($attr->m_destInstance->m_table . "." . $attr->m_destInstance->primaryKeyField() . ' = \'' . escapeSQL($value) . "'");
  1009. }
  1010. if (count($relationselect) == 0 || count($relationselect) > 1) {
  1011. static $searchresults = array();
  1012. if (!array_key_exists($attributename, $searchresults)
  1013. || (array_key_exists($attributename, $searchresults) && !array_key_exists($value, $searchresults[$attributename]))) {
  1014. Adapto_Util_Debugger::debug("Caching attributeValue result for $attributename ($value)");
  1015. $searchresults[$attributename][$value] = $attr->m_destInstance->searchDb($value);
  1016. }
  1017. if (count($searchresults[$attributename][$value]) == 1) {
  1018. $value = array(
  1019. $attr->m_destInstance->primaryKeyField() => $searchresults[$attributename][$value][0][$attr->m_destInstance->primaryKeyField()]);
  1020. } else {
  1021. $relation = $this->isRelationAttribute($attributename);
  1022. if ($relation)
  1023. $rec[$relation][$attributename] = $value;
  1024. else
  1025. $rec[$attributename] = $value;
  1026. $importerrors[$counter][] = array("msg" => atktext("error_formdataerror"),
  1027. "spec" => sprintf(atktext("import_nonunique_identifier"), $this->getValueFromRecord($rec, $attributename)));
  1028. }
  1029. }
  1030. } else if (is_object($attr) && method_exists($attr, "parseStringValue")) {
  1031. $value = $attr->parseStringValue($value);
  1032. } else {
  1033. $value = trim($value);
  1034. }
  1035. return $value;
  1036. }
  1037. /**
  1038. * Determines wether or not errors occurred and shows the analyze screen if errors occurred.
  1039. * @param Array $importerrors An array with the errors that occurred
  1040. * @param Array $extraerror An extra error, if we found errors
  1041. * @return bool Wether or not errors occurred
  1042. */
  1043. function showErrors($importerrors, $extraerror = null)
  1044. {
  1045. foreach ($importerrors as $importerror) {
  1046. if (is_array($importerror) && !empty($importerror[0])) {
  1047. $errorfound = true;
  1048. }
  1049. }
  1050. if ($errorfound) {
  1051. if ($extraerror)
  1052. $importerrors[0][] = $extraerror;
  1053. $this->doAnalyze($this->m_postvars["fileid"], $importerrors);
  1054. return true;
  1055. }
  1056. }
  1057. /**
  1058. * Adds the validated records but checks for errors first
  1059. *
  1060. * @param Array $importerrors Errors that occurred during validation of importfile
  1061. * @param Array $validatedrecs Records that were validated
  1062. */
  1063. function addRecords(&$importerrors, &$validatedrecs)
  1064. {
  1065. $counter = 0;
  1066. foreach ($validatedrecs as $action => $validrecs) {
  1067. foreach ($validrecs as $validrec) {
  1068. $counter++;
  1069. Adapto_Util_Debugger::debug("Doing $action for record nr $counter");
  1070. $this->$action($validrec);
  1071. if (!empty($validrec['atkerror'])) {
  1072. foreach ($validrec['atkerror'] as $atkerror) {
  1073. $importerrors[$counter][] = array("msg" => "Fouten gedetecteerd op rij $counter: ", "spec" => $atkerror['msg']);
  1074. }
  1075. }
  1076. unset($validrec);
  1077. }
  1078. unset($validrecs);
  1079. }
  1080. unset($validatedrecs);
  1081. }
  1082. /**
  1083. * Add a valid record to the db
  1084. * @param Array $record The record to add
  1085. * @return bool Wether or not there were errors
  1086. */
  1087. function add(&$record)
  1088. {
  1089. $this->m_importEntity->preAdd($record);
  1090. if (isset($record['atkerror'])) {
  1091. return false;
  1092. }
  1093. $this->m_importEntity->addDb($record);
  1094. if (isset($record['atkerror'])) {
  1095. return false;
  1096. }
  1097. return true;
  1098. }
  1099. /**
  1100. * Update a record in the db
  1101. * @param Array $record the record to update
  1102. * @return bool Wether or not there were errors
  1103. */
  1104. function update(&$record)
  1105. {
  1106. $this->m_importEntity->preUpdate($record);
  1107. if (isset($record['atkerror'])) {
  1108. return false;
  1109. }
  1110. $this->m_importEntity->updateDb($record);
  1111. if (isset($record['atkerror'])) {
  1112. return false;
  1113. }
  1114. return true;
  1115. }
  1116. /**
  1117. * Check whether the record if valide to import
  1118. * @param array $record the record
  1119. * @return bool Wether or not there were errors
  1120. */
  1121. function validate(&$record)
  1122. {
  1123. if ($this->m_postvars['doupdate'])
  1124. $mode = "update";
  1125. else
  1126. $mode = "add";
  1127. $this->m_importEntity->validate($record, $mode);
  1128. foreach (array_keys($record) as $key) {
  1129. $error = $error || (is_array($record[$key]) && array_key_exists('atkerror', $record[$key]) && count($record[$key]['atkerror']) > 0);
  1130. }
  1131. if (isset($error)) {
  1132. return false;
  1133. } else {
  1134. return true;
  1135. }
  1136. }
  1137. /**
  1138. * Checks the import by the col_map and the initial_values
  1139. * Check if all obligatory fields are used in the col_map or the initial_values
  1140. * Check if there are no fields used twice
  1141. *
  1142. * @param array $col_map The use of the fields for the columns in the csv
  1143. * @param array $initial_values The initial_values of the importentity
  1144. * @return array An array with errors, if there are any
  1145. */
  1146. function checkImport($col_map, $initial_values)
  1147. {
  1148. $errors = array();
  1149. //get the unused obligatory fields
  1150. $unused = array_values(array_diff($this->getObligatoryAttributes(), $col_map));
  1151. $this
  1152. ->_returnErrors(array_values(array_diff($unused, array_keys($initial_values))), "import_error_fieldisobligatory",
  1153. "import_error_fieldsareobligatory", $errors);
  1154. $this->_returnErrors($this->_getDuplicateColumns($col_map), "import_error_fieldusedtwice", "import_error_fieldsusedtwice", $errors);
  1155. return $errors;
  1156. }
  1157. /**
  1158. * Checks if there are errors and if there are then it adds it to the collection
  1159. * @param Array $errors The errors to check
  1160. * @param String $singleerror The language code to use for a single error
  1161. * @param String $doubleerror The language code to use for multiple errors
  1162. * @param Array &$collection The collection of errors thus far
  1163. */
  1164. function _returnErrors($errors, $singleerror, $doubleerror, &$collection)
  1165. {
  1166. if (count($errors) > 0) {
  1167. $msg = atktext((count($errors) == 1) ? $singleerror : $doubleerror) . ": ";
  1168. foreach ($errors as $key => $field) {
  1169. $attr = &$this->getUsableAttribute($field);
  1170. $errors[$key] = $attr->label();

Large files files are truncated, but you can click here to view the full file