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