]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Controllers/CsvController.php
Fixes, run CS on PHP 7.4
[galette.git] / galette / lib / Galette / Controllers / CsvController.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Galette CSV controller
7 *
8 * PHP version 5
9 *
10 * Copyright © 2019-2020 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Controllers
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2019-2020 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @version SVN: $Id$
34 * @link http://galette.tuxfamily.org
35 * @since Available since 0.9.4dev - 2019-12-06
36 */
37
38 namespace Galette\Controllers;
39
40 use Slim\Http\Request;
41 use Slim\Http\Response;
42 use Galette\Entity\ImportModel;
43 use Galette\Filters\MembersList;
44 use Galette\IO\Csv;
45 use Galette\IO\CsvIn;
46 use Galette\IO\CsvOut;
47 use Galette\IO\MembersCsv;
48 use Galette\Repository\DynamicFieldsSet;
49 use Analog\Analog;
50
51 /**
52 * Galette CSV controller
53 *
54 * @category Controllers
55 * @name CsvController
56 * @package Galette
57 * @author Johan Cwiklinski <johan@x-tnd.be>
58 * @copyright 2019-2020 The Galette Team
59 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
60 * @link http://galette.tuxfamily.org
61 * @since Available since 0.9.4dev - 2019-12-06
62 */
63
64 class CsvController extends AbstractController
65 {
66 /**
67 * Send response
68 *
69 * @param Response $response PSR Response
70 * @param string $filepath File path on disk
71 * @param string $filename File name for output
72 *
73 * @return Response
74 */
75 protected function sendResponse(Response $response, $filepath, $filename) :Response
76 {
77 if (file_exists($filepath)) {
78 $response = $response->withHeader('Content-Description', 'File Transfer')
79 ->withHeader('Content-Type', 'text/csv')
80 ->withHeader('Content-Disposition', 'attachment;filename="' . $filename . '"')
81 ->withHeader('Pragma', 'no-cache')
82 ->withHeader('Content-Transfer-Encoding', 'binary')
83 ->withHeader('Expires', '0')
84 ->withHeader('Cache-Control', 'must-revalidate')
85 ->withHeader('Pragma', 'public');
86
87 $stream = fopen('php://memory', 'r+');
88 fwrite($stream, file_get_contents($filepath));
89 rewind($stream);
90
91 return $response->withBody(new \Slim\Http\Stream($stream));
92 } else {
93 Analog::log(
94 'A request has been made to get a CSV file named `' .
95 $filename .'` that does not exists (' . $filepath . ').',
96 Analog::WARNING
97 );
98 $notFound = $this->notFoundHandler;
99 return $notFound($request, $response);
100 }
101 }
102
103 /**
104 * Exports page
105 *
106 * @param Request $request PSR Request
107 * @param Response $response PSR Response
108 *
109 * @return Response
110 */
111 public function export(Request $request, Response $response) :Response
112 {
113 $csv = new CsvOut();
114
115 $tables_list = $this->zdb->getTables();
116 $parameted = $csv->getParametedExports();
117 $existing = $csv->getExisting();
118
119 // display page
120 $this->view->render(
121 $response,
122 'export.tpl',
123 array(
124 'page_title' => _T("CVS database Export"),
125 'tables_list' => $tables_list,
126 'written' => $this->flash->getMessage('written_exports'),
127 'existing' => $existing,
128 'parameted' => $parameted
129 )
130 );
131 return $response;
132 }
133
134 /**
135 * Proceed exports
136 *
137 * @param Request $request PSR Request
138 * @param Response $response PSR Response
139 *
140 * @return Response
141 */
142 public function doExport(Request $request, Response $response) :Response
143 {
144 $post = $request->getParsedBody();
145 $csv = new CsvOut();
146 $written = [];
147
148 if (isset($post['export_tables']) && $post['export_tables'] != '') {
149 foreach ($post['export_tables'] as $table) {
150 $select = $this->zdb->sql->select($table);
151 $results = $this->zdb->execute($select);
152
153 if ($results->count() > 0) {
154 $filename = $table . '_full.csv';
155 $filepath = CsvOut::DEFAULT_DIRECTORY . $filename;
156 $fp = fopen($filepath, 'w');
157 if ($fp) {
158 $res = $csv->export(
159 $results,
160 Csv::DEFAULT_SEPARATOR,
161 Csv::DEFAULT_QUOTE,
162 true,
163 $fp
164 );
165 fclose($fp);
166 $written[] = [
167 'name' => $table,
168 'file' => $filename
169 ];
170 }
171 } else {
172 $this->flash->addMessage(
173 'warning_detected',
174 str_replace(
175 '%table',
176 $table,
177 _T("Table %table is empty, and has not been exported.")
178 )
179 );
180 }
181 }
182 }
183
184 if (isset($post['export_parameted']) && $post['export_parameted'] != '') {
185 foreach ($post['export_parameted'] as $p) {
186 $res = $csv->runParametedExport($p);
187 $pn = $csv->getParamedtedExportName($p);
188 switch ($res) {
189 case Csv::FILE_NOT_WRITABLE:
190 $this->flash->addMessage(
191 'error_detected',
192 str_replace(
193 '%export',
194 $pn,
195 _T("Export file could not be write on disk for '%export'. Make sure web server can write in the exports directory.")
196 )
197 );
198 break;
199 case Csv::DB_ERROR:
200 $this->flash->addMessage(
201 'error_detected',
202 str_replace(
203 '%export',
204 $pn,
205 _T("An error occurred running parameted export '%export'.")
206 )
207 );
208 break;
209 case false:
210 $this->flash->addMessage(
211 'error_detected',
212 str_replace(
213 '%export',
214 $pn,
215 _T("An error occurred running parameted export '%export'. Please check the logs.")
216 )
217 );
218 break;
219 default:
220 //no error, file has been writted to disk
221 $written[] = [
222 'name' => $pn,
223 'file' => (string)$res
224 ];
225 break;
226 }
227 }
228 }
229
230 if (count($written)) {
231 foreach ($written as $ex) {
232 $path = $this->router->pathFor('getCsv', ['type' => 'export', 'file' => $ex['file']]);
233 $this->flash->addMessage(
234 'written_exports',
235 '<a href="' . $path . '">' . $ex['name'] . ' (' . $ex['file'] . ')</a>'
236 );
237 }
238 }
239
240 return $response
241 ->withStatus(301)
242 ->withHeader('Location', $this->router->pathFor('export'));
243 }
244
245 /**
246 * Imports page
247 *
248 * @param Request $request PSR Request
249 * @param Response $response PSR Response
250 *
251 * @return Response
252 */
253 public function import(Request $request, Response $response) :Response
254 {
255 $csv = new CsvIn($this->zdb);
256 $existing = $csv->getExisting();
257 $dryrun = true;
258
259 // display page
260 $this->view->render(
261 $response,
262 'import.tpl',
263 array(
264 'page_title' => _T("CSV members import"),
265 'existing' => $existing,
266 'dryrun' => $dryrun,
267 'import_file' => $this->session->import_file
268 )
269 );
270 return $response;
271 }
272
273 /**
274 * Proceed imports
275 *
276 * @param Request $request PSR Request
277 * @param Response $response PSR Response
278 *
279 * @return Response
280 */
281 public function doImports(Request $request, Response $response) :Response
282 {
283 $csv = new CsvIn($this->zdb);
284 $post = $request->getParsedBody();
285 $dryrun = isset($post['dryrun']);
286
287 //store selected file to dispaly again in UI
288 $this->session->import_file = $post['import_file'];
289
290 $res = $csv->import(
291 $this->zdb,
292 $this->preferences,
293 $this->history,
294 $post['import_file'],
295 $this->members_fields,
296 $this->members_fields_cats,
297 $dryrun
298 );
299 if ($res !== true) {
300 if ($res < 0) {
301 $this->flash->addMessage(
302 'error_detected',
303 $csv->getErrorMessage($res)
304 );
305 if (count($csv->getErrors()) > 0) {
306 foreach ($csv->getErrors() as $error) {
307 $this->flash->addMessage(
308 'error_detected',
309 $error
310 );
311 }
312 }
313 } else {
314 $this->flash->addMessage(
315 'error_detected',
316 _T("An error occurred importing the file :(")
317 );
318 }
319 } else {
320 if ($this->session->import_file && !$dryrun) {
321 $this->session->import_file = null;
322 }
323 $this->flash->addMessage(
324 'success_detected',
325 str_replace(
326 '%filename%',
327 $post['import_file'],
328 _T("File '%filename%' has been successfully imported :)")
329 )
330 );
331 }
332 return $response
333 ->withStatus(301)
334 ->withHeader('Location', $this->router->pathFor('import'));
335 }
336
337 /**
338 * Get CSV file (imports or exports)
339 *
340 * @param Request $request PSR Request
341 * @param Response $response PSR Response
342 *
343 * @return Response
344 */
345 public function uploadImportFile(Request $request, Response $response) :Response
346 {
347 $csv = new CsvIn($this->zdb);
348 if (isset($_FILES['new_file'])) {
349 if ($_FILES['new_file']['error'] === UPLOAD_ERR_OK) {
350 if ($_FILES['new_file']['tmp_name'] !='') {
351 if (is_uploaded_file($_FILES['new_file']['tmp_name'])) {
352 $res = $csv->store($_FILES['new_file']);
353 if ($res < 0) {
354 $this->flash->addMessage(
355 'error_detected',
356 $csv->getErrorMessage($res)
357 );
358 } else {
359 $this->flash->addMessage(
360 'success_detected',
361 _T("Your file has been successfully uploaded!")
362 );
363 }
364 }
365 }
366 } elseif ($_FILES['new_file']['error'] !== UPLOAD_ERR_NO_FILE) {
367 Analog::log(
368 $csv->getPhpErrorMessage($_FILES['new_file']['error']),
369 Analog::WARNING
370 );
371 $this->flash->addMessage(
372 'error_detected',
373 $csv->getPhpErrorMessage(
374 $_FILES['new_file']['error']
375 )
376 );
377 } elseif (isset($_POST['upload'])) {
378 $this->flash->addMessage(
379 'error_detected',
380 _T("No files has been seleted for upload!")
381 );
382 }
383 } else {
384 $this->flash->addMessage(
385 'warning_detected',
386 _T("No files has been uploaded!")
387 );
388 }
389
390 return $response
391 ->withStatus(301)
392 ->withHeader('Location', $this->router->pathFor('import'));
393 }
394
395 /**
396 * Get CSV file (imports or exports)
397 *
398 * @param Request $request PSR Request
399 * @param Response $response PSR Response
400 * @param array $args Request arguments
401 *
402 * @return Response
403 */
404 public function getFile(Request $request, Response $response, array $args = []) :Response
405 {
406 $filename = $args['file'];
407
408 //Exports main contain user confidential data, they're accessible only for
409 //admins or staff members
410 if ($this->login->isAdmin() || $this->login->isStaff()) {
411 $filepath = $args['type'] === 'export' ?
412 CsvOut::DEFAULT_DIRECTORY :
413 CsvIn::DEFAULT_DIRECTORY;
414 $filepath .= $filename;
415 return $this->sendResponse($response, $filepath, $filename);
416 } else {
417 Analog::log(
418 'A non authorized person asked to retrieve ' . $args['type'] . ' file named `' .
419 $filename . '`. Access has not been granted.',
420 Analog::WARNING
421 );
422 $error = $this->errorHandler;
423 return $error(
424 $request,
425 $response->withStatus(403)
426 );
427 }
428 }
429
430 /**
431 * Remove CSV file confirmation (imports or exports)
432 *
433 * @param Request $request PSR Request
434 * @param Response $response PSR Response
435 * @param array $args Request arguments
436 *
437 * @return Response
438 */
439 public function confirmRemoveFile(Request $request, Response $response, array $args = []) :Response
440 {
441 $data = [
442 'id' => $args['id'],
443 'redirect_uri' => $this->router->pathFor($args['type'])
444 ];
445
446 // display page
447 $this->view->render(
448 $response,
449 'confirm_removal.tpl',
450 array(
451 'mode' => $request->isXhr() ? 'ajax' : '',
452 'page_title' => sprintf(
453 _T('Remove %1$s file %2$s'),
454 $args['type'],
455 $args['file']
456 ),
457 'form_url' => $this->router->pathFor(
458 'doRemoveCsv',
459 [
460 'type' => $args['type'],
461 'file' => $args['file']
462 ]
463 ),
464 'cancel_uri' => $data['redirect_uri'],
465 'data' => $data
466 )
467 );
468 return $response;
469 }
470
471 /**
472 * Remove CSV file (imports or exports)
473 *
474 * @param Request $request PSR Request
475 * @param Response $response PSR Response
476 * @param array $args Request arguments
477 *
478 * @return Response
479 */
480 public function removeFile(Request $request, Response $response, array $args = []) :Response
481 {
482 $post = $request->getParsedBody();
483 $ajax = isset($post['ajax']) && $post['ajax'] === 'true';
484 $success = false;
485
486 $uri = isset($post['redirect_uri']) ?
487 $post['redirect_uri'] :
488 $this->router->pathFor('slash');
489
490 if (!isset($post['confirm'])) {
491 $this->flash->addMessage(
492 'error_detected',
493 _T("Removal has not been confirmed!")
494 );
495 } else {
496 $csv = $args['type'] === 'export' ?
497 new CsvOut() :
498 new CsvIn($this->zdb);
499 $res = $csv->remove($args['file']);
500 if ($res === true) {
501 $success = true;
502 $this->flash->addMessage(
503 'success_detected',
504 str_replace(
505 '%export',
506 $args['file'],
507 _T("'%export' file has been removed from disk.")
508 )
509 );
510 } else {
511 $success = false;
512 $this->flash->addMessage(
513 'error_detected',
514 str_replace(
515 '%export',
516 $args['file'],
517 _T("Cannot remove '%export' from disk :/")
518 )
519 );
520 }
521 }
522
523 if (!$ajax) {
524 return $response
525 ->withStatus(301)
526 ->withHeader('Location', $uri);
527 } else {
528 return $response->withJson(
529 [
530 'success' => $success
531 ]
532 );
533 }
534 }
535
536 /**
537 * Import model page
538 *
539 * @param Request $request PSR Request
540 * @param Response $response PSR Response
541 * @param array $args Request arguments
542 *
543 * @return Response
544 */
545 public function importModel(Request $request, Response $response, array $args = []) :Response
546 {
547 $model = new ImportModel();
548 $model->load();
549
550 if (isset($request->getQueryParams()['remove'])) {
551 $model->remove($this->zdb);
552 $model->load();
553 }
554
555 $csv = new CsvIn($this->zdb);
556
557 /** FIXME:
558 * - set fields that should not be part of import
559 */
560 $fields = $model->getFields();
561 $defaults = $csv->getDefaultFields();
562 $defaults_loaded = false;
563
564 if ($fields === null) {
565 $fields = $defaults;
566 $defaults_loaded = true;
567 }
568
569 $import_fields = $this->members_form_fields;
570 //get dynamic fields
571 $dynamic_import_fields = [];
572 $fieldset = new DynamicFieldsSet($this->zdb, $this->login);
573 $dfields = $fieldset->getList('adh');
574 foreach ($dfields as $field) {
575 if ($field->hasData() && !$field instanceof \Galette\DynamicFields\File) {
576 $dynamic_import_fields['dynfield_' . $field->getId()] = [
577 'label' => __($field->getname())
578 ];
579 }
580 }
581 //we do not want to import id_adh. Never.
582 unset($import_fields['id_adh']);
583 $import_fields += $dynamic_import_fields;
584
585 // display page
586 $this->view->render(
587 $response,
588 'import_model.tpl',
589 array(
590 'page_title' => _T("CSV import model"),
591 'fields' => $fields,
592 'model' => $model,
593 'defaults' => $defaults,
594 'members_fields' => $import_fields,
595 'defaults_loaded' => $defaults_loaded
596 )
597 );
598 return $response;
599 }
600
601 /**
602 * Get CSV import model file
603 *
604 * @param Request $request PSR Request
605 * @param Response $response PSR Response
606 * @param array $args Request arguments
607 *
608 * @return Response
609 */
610 public function getImportModel(Request $request, Response $response, array $args = []) :Response
611 {
612 $model = new ImportModel();
613 $model->load();
614
615 $csv = new CsvIn($this->zdb);
616
617 $fields = $model->getFields();
618 $defaults = $csv->getDefaultFields();
619 $defaults_loaded = false;
620
621 if ($fields === null) {
622 $fields = $defaults;
623 $defaults_loaded = true;
624 }
625
626 $ocsv = new CsvOut();
627 $res = $ocsv->export(
628 $fields,
629 Csv::DEFAULT_SEPARATOR,
630 Csv::DEFAULT_QUOTE,
631 $fields
632 );
633 $filename = _T("galette_import_model.csv");
634
635 $response = $response->withHeader('Content-Description', 'File Transfer')
636 ->withHeader('Content-Type', 'text/csv')
637 ->withHeader('Content-Disposition', 'attachment;filename="' . $filename . '"')
638 ->withHeader('Pragma', 'no-cache')
639 ->withHeader('Content-Transfer-Encoding', 'binary')
640 ->withHeader('Expires', '0')
641 ->withHeader('Cache-Control', 'must-revalidate')
642 ->withHeader('Pragma', 'public');
643
644 $stream = fopen('php://memory', 'r+');
645 fwrite($stream, $res);
646 rewind($stream);
647
648 return $response->withBody(new \Slim\Http\Stream($stream));
649 }
650
651 /**
652 * Store CSV model
653 *
654 * @param Request $request PSR Request
655 * @param Response $response PSR Response
656 * @param array $args Request arguments
657 *
658 * @return Response
659 */
660 public function storeModel(Request $request, Response $response, array $args = []) :Response
661 {
662 $model = new ImportModel();
663 $model->load();
664
665 $model->setFields($request->getParsedBody()['fields']);
666 $res = $model->store($this->zdb);
667 if ($res === true) {
668 $this->flash->addMessage(
669 'success_detected',
670 _T("Import model has been successfully stored :)")
671 );
672 } else {
673 $this->flash->addMessage(
674 'error_detected',
675 _T("Import model has not been stored :(")
676 );
677 }
678
679 return $response
680 ->withStatus(301)
681 ->withHeader('Location', $this->router->pathFor('importModel'));
682 }
683
684 /**
685 * Members CSV exports
686 *
687 * @param Request $request PSR Request
688 * @param Response $response PSR Response
689 * @param array $args Request arguments
690 *
691 * @return Response
692 */
693 public function membersExport(Request $request, Response $response, array $args = []) :Response
694 {
695 $post = $request->getParsedBody();
696 $get = $request->getQueryParams();
697
698 $session_var = $post['session_var'] ?? $get['session_var'] ?? 'filter_members';
699
700 if (isset($this->session->$session_var)) {
701 $filters = $this->session->$session_var;
702 } else {
703 $filters = new MembersList();
704 }
705
706 $csv = new MembersCsv(
707 $this->zdb,
708 $this->login,
709 $this->members_fields,
710 $this->fields_config
711 );
712 $csv->exportMembers($filters);
713
714 $filepath = $csv->getPath();
715 $filename = $csv->getFileName();
716
717 return $this->sendResponse($response, $filepath, $filename);
718 }
719 }