diff --git a/composer.json b/composer.json index 9adaf3f7c9..dc4f2b0ffd 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "gedmo/doctrine-extensions" : "~2.3.0", "alchemy/google-plus-api-client" : "~0.6.2", "alchemy/geonames-api-consumer" : "~0.1.0", - "guzzle/guzzle" : "~3.6", + "goodby/csv" : "~1.0", + "guzzle/guzzle" : "~3.0", "igorw/get-in" : "~1.0", "imagine/imagine" : "0.6.x-dev@dev", "ircmaxell/random-lib" : "~1.0", diff --git a/composer.lock b/composer.lock index ece5645a0c..a3e26ca189 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "88bbe728663cb23ecfba31d240af17c3", + "hash": "2b68364c4ebb09b6f5c3902e0afdcc1a", "packages": [ { "name": "alchemy-fr/tcpdf-clone", @@ -1369,6 +1369,64 @@ "tree", "uploadable" ], + "time": "2013-08-18 07:18:44" + }, + { + "name": "goodby/csv", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/goodby/csv.git", + "reference": "da672802985d196cae767da29b11618a676496b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/goodby/csv/zipball/da672802985d196cae767da29b11618a676496b0", + "reference": "da672802985d196cae767da29b11618a676496b0", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.2" + }, + "require-dev": { + "mikey179/vfsstream": ">=1.1.0", + "mockery/mockery": ">=0.7.2", + "phpunit/phpunit": "3.7.*", + "suin/php-expose": ">=1.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Goodby\\CSV": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "suin", + "email": "suinyeze@gmail.com", + "homepage": "https://www.facebook.com/suinyeze", + "role": "Developer, Renaming Specialist" + }, + { + "name": "reoring", + "email": "mori.reo@gmail.com", + "homepage": "https://www.facebook.com/reoring", + "role": "Developer" + } + ], + "description": "CSV import/export library", + "homepage": "https://github.com/goodby/csv", + "keywords": [ + "csv", + "export", + "import" + ], + "time": "2013-11-22 19:10:34" "time": "2014-01-12 16:34:06" }, { diff --git a/lib/Alchemy/Phrasea/Application.php b/lib/Alchemy/Phrasea/Application.php index 5c3dfdffe0..543b21a0a2 100644 --- a/lib/Alchemy/Phrasea/Application.php +++ b/lib/Alchemy/Phrasea/Application.php @@ -57,7 +57,6 @@ use Alchemy\Phrasea\Controller\Prod\UsrLists; use Alchemy\Phrasea\Controller\Prod\WorkZone; use Alchemy\Phrasea\Controller\Report\Activity as ReportActivity; use Alchemy\Phrasea\Controller\Report\Informations as ReportInformations; -use Alchemy\Phrasea\Controller\Report\Export as ReportExport; use Alchemy\Phrasea\Controller\Report\Root as ReportRoot; use Alchemy\Phrasea\Controller\Root\Account; use Alchemy\Phrasea\Controller\Root\Developers; @@ -87,6 +86,7 @@ use Alchemy\Phrasea\Core\Provider\CacheServiceProvider; use Alchemy\Phrasea\Core\Provider\CacheConnectionServiceProvider; use Alchemy\Phrasea\Core\Provider\ConfigurationServiceProvider; use Alchemy\Phrasea\Core\Provider\ConfigurationTesterServiceProvider; +use Alchemy\Phrasea\Core\Provider\CSVServiceProvider; use Alchemy\Phrasea\Core\Provider\ConvertersServiceProvider; use Alchemy\Phrasea\Core\Provider\FileServeServiceProvider; use Alchemy\Phrasea\Core\Provider\FeedServiceProvider; @@ -221,6 +221,7 @@ class Application extends SilexApplication $this->register(new ConfigurationServiceProvider()); $this->register(new ConfigurationTesterServiceProvider); $this->register(new ConvertersServiceProvider()); + $this->register(new CSVServiceProvider()); $this->register(new RegistrationServiceProvider()); $this->register(new CacheServiceProvider()); $this->register(new CacheConnectionServiceProvider()); @@ -880,7 +881,6 @@ class Application extends SilexApplication $this->mount('/report/', new ReportRoot()); $this->mount('/report/activity', new ReportActivity()); $this->mount('/report/informations', new ReportInformations()); - $this->mount('/report/export', new ReportExport()); $this->mount('/thesaurus', new Thesaurus()); $this->mount('/xmlhttp', new ThesaurusXMLHttp()); diff --git a/lib/Alchemy/Phrasea/Controller/Admin/Users.php b/lib/Alchemy/Phrasea/Controller/Admin/Users.php index b530963b6c..5e51e9deee 100644 --- a/lib/Alchemy/Phrasea/Controller/Admin/Users.php +++ b/lib/Alchemy/Phrasea/Controller/Admin/Users.php @@ -11,6 +11,7 @@ namespace Alchemy\Phrasea\Controller\Admin; +use Alchemy\Phrasea\Core\Response\CSVFileResponse; use Alchemy\Phrasea\Helper\User as UserHelper; use Alchemy\Phrasea\Model\Entities\FtpCredential; use Alchemy\Phrasea\Model\Entities\User; @@ -148,8 +149,6 @@ class Users implements ControllerProviderInterface })->bind('admin_users_search'); $controllers->post('/search/export/', function () use ($app) { - $request = $app['request']; - $users = new UserHelper\Manage($app, $app['request']); $userTable = [ @@ -194,10 +193,10 @@ class Users implements ControllerProviderInterface ]; } - $CSVDatas = \format::arr_to_csv($userTable); - - $response = new Response($CSVDatas, 200, ['Content-Type' => 'text/csv']); - $response->headers->set('Content-Disposition', 'attachment; filename=export.csv'); + $filename = sprintf('user_export_%s.csv', date('Ymd')); + $response = new CSVFileResponse($filename, function() use ($app, $userTable) { + $app['csv.exporter']->export('php://output', $userTable); + }); return $response; })->bind('admin_users_search_export'); @@ -224,7 +223,7 @@ class Users implements ControllerProviderInterface $have_not_right = $request->query->get('have_not_right') ? : []; $on_base = $request->query->get('on_base') ? : []; - $elligible_users = $user_query + $eligible_users = $user_query ->on_sbas_where_i_am($app['acl']->get($app['authentication']->getUser()), $rights) ->like(\User_Query::LIKE_EMAIL, $like_value) ->like(\User_Query::LIKE_FIRSTNAME, $like_value) @@ -239,7 +238,7 @@ class Users implements ControllerProviderInterface $datas = []; - foreach ($elligible_users as $user) { + foreach ($eligible_users as $user) { $datas[] = [ 'email' => $user->getEmail() ? : '', 'login' => $user->getLogin() ? : '', @@ -287,7 +286,7 @@ class Users implements ControllerProviderInterface $on_base = $request->request->get('base_id') ? : null; $on_sbas = $request->request->get('sbas_id') ? : null; - $elligible_users = $user_query->on_bases_where_i_am($app['acl']->get($app['authentication']->getUser()), ['canadmin']) + $eligible_users = $user_query->on_bases_where_i_am($app['acl']->get($app['authentication']->getUser()), ['canadmin']) ->like($like_field, $like_value) ->on_base_ids($on_base) ->on_sbas_ids($on_sbas); @@ -314,10 +313,10 @@ class Users implements ControllerProviderInterface $app->trans('admin::compte-utilisateur activite'), ]; do { - $elligible_users->limit($offset, 20); + $eligible_users->limit($offset, 20); $offset += 20; - $results = $elligible_users->execute()->get_results(); + $results = $eligible_users->execute()->get_results(); foreach ($results as $user) { $buffer[] = [ @@ -341,14 +340,10 @@ class Users implements ControllerProviderInterface } } while (count($results) > 0); - $out = \format::arr_to_csv($buffer); - - $response = new Response($out, 200, [ - 'Content-type' => 'text/csv', - 'Content-Disposition' => 'attachment; filename=export.csv', - ]); - - $response->setCharset('UTF-8'); + $filename = sprintf('user_export_%s.csv', date('Ymd')); + $response = new CSVFileResponse($filename, function() use ($app, $buffer) { + $app['csv.exporter']->export('php://output', $buffer); + }); return $response; })->bind('admin_users_export_csv'); @@ -521,6 +516,7 @@ class Users implements ControllerProviderInterface })->bind('users_display_import_file'); $controllers->post('/import/file/', function (Application $app, Request $request) { + if ((null === $file = $request->files->get('files')) || !$file->isValid()) { return $app->redirectPath('users_display_import_file', ['error' => 'file-invalid']); } @@ -534,7 +530,11 @@ class Users implements ControllerProviderInterface ]; $nbUsrToAdd = 0; - $lines = \format::csv_to_arr($file->getPathname()); + $lines = array(); + $app['csv.interpreter']->addObserver(function(array $row) use (&$lines) { + $lines[] = $row; + }); + $app['csv.lexer']->parse($file->getPathname(), $app['csv.interpreter']); $roughColumns = array_shift($lines); diff --git a/lib/Alchemy/Phrasea/Controller/Report/Activity.php b/lib/Alchemy/Phrasea/Controller/Report/Activity.php index 696b0c8214..daa452bc82 100644 --- a/lib/Alchemy/Phrasea/Controller/Report/Activity.php +++ b/lib/Alchemy/Phrasea/Controller/Report/Activity.php @@ -11,6 +11,8 @@ namespace Alchemy\Phrasea\Controller\Report; +use Alchemy\Phrasea\Core\Response\CSVFileResponse; +use Goodby\CSV\Export\Standard\Collection\CallbackCollection; use Silex\Application; use Silex\ControllerProviderInterface; use Symfony\Component\HttpFoundation\Request; @@ -100,29 +102,23 @@ class Activity implements ControllerProviderInterface $activity->setHasLimit(false); $activity->getConnexionBase(false, $request->request->get('on', 'user')); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); - } else { - $report = $activity->getConnexionBase(false, $request->request->get('on', 'user')); - - return $app->json([ - 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ - 'result' => isset($report['report']) ? $report['report'] : $report, - 'is_infouser' => false, - 'is_nav' => false, - 'is_groupby' => false, - 'is_plot' => false, - 'is_doc' => false - ]), - 'display_nav' => false, - 'title' => false - ]); + return $this->getCSVResponse($app, $activity, 'activity_connection_base'); } + + $report = $activity->getConnexionBase(false, $request->request->get('on', 'user')); + + return $app->json([ + 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ + 'result' => isset($report['report']) ? $report['report'] : $report, + 'is_infouser' => false, + 'is_nav' => false, + 'is_groupby' => false, + 'is_plot' => false, + 'is_doc' => false + ]), + 'display_nav' => false, + 'title' => false + ]); } /** @@ -166,27 +162,21 @@ class Activity implements ControllerProviderInterface if ($request->request->get('printcsv') == 'on') { $activity->setHasLimit(false); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); - } else { - return $app->json([ - 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ - 'result' => isset($report['report']) ? $report['report'] : $report, - 'is_infouser' => false, - 'is_nav' => false, - 'is_groupby' => false, - 'is_plot' => false, - 'is_doc' => false - ]), - 'display_nav' => false, - 'title' => false - ]); + return $this->getCSVResponse($app, $activity, 'activity_detail_download'); } + + return $app->json([ + 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ + 'result' => isset($report['report']) ? $report['report'] : $report, + 'is_infouser' => false, + 'is_nav' => false, + 'is_groupby' => false, + 'is_plot' => false, + 'is_doc' => false + ]), + 'display_nav' => false, + 'title' => false + ]); } /** @@ -222,29 +212,23 @@ class Activity implements ControllerProviderInterface $activity->getTopQuestion($conf); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); - } else { - $report = $activity->getTopQuestion($conf); - - return $app->json([ - 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ - 'result' => isset($report['report']) ? $report['report'] : $report, - 'is_infouser' => false, - 'is_nav' => false, - 'is_groupby' => false, - 'is_plot' => false, - 'is_doc' => false - ]), - 'display_nav' => false, - 'title' => false - ]); + return $this->getCSVResponse($app, $activity, 'activity_questions_best_of'); } + + $report = $activity->getTopQuestion($conf); + + return $app->json(array( + 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', array( + 'result' => isset($report['report']) ? $report['report'] : $report, + 'is_infouser' => false, + 'is_nav' => false, + 'is_groupby' => false, + 'is_plot' => false, + 'is_doc' => false + )), + 'display_nav' => false, + 'title' => false + )); } /** @@ -287,29 +271,23 @@ class Activity implements ControllerProviderInterface $activity->getTopQuestion($conf, true); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); - } else { - $report = $activity->getTopQuestion($conf, true); - - return $app->json([ - 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ - 'result' => isset($report['report']) ? $report['report'] : $report, - 'is_infouser' => false, - 'is_nav' => false, - 'is_groupby' => false, - 'is_plot' => false, - 'is_doc' => false - ]), - 'display_nav' => false, - 'title' => false - ]); + return $this->getCSVResponse($app, $activity, 'activity_top_ten_questions'); } + + $report = $activity->getTopQuestion($conf, true); + + return $app->json(array( + 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', array( + 'result' => isset($report['report']) ? $report['report'] : $report, + 'is_infouser' => false, + 'is_nav' => false, + 'is_groupby' => false, + 'is_plot' => false, + 'is_doc' => false + )), + 'display_nav' => false, + 'title' => false + )); } /** @@ -331,33 +309,31 @@ class Activity implements ControllerProviderInterface $activity->setConfig(false); - $report = $activity->getActivityPerHours(); + if ($request->request->get('printcsv') == 'on') { - $activity->setHasLimit(false); - $activity->setPrettyString(false); + $activity->setHasLimit(false); + $activity->setPrettyString(false); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } + $activity->getActivityPerHours(); - return $app->json(['rs' => $csv]); - } else { - return $app->json([ - 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ - 'result' => isset($report['report']) ? $report['report'] : $report, - 'is_infouser' => false, - 'is_nav' => false, - 'is_groupby' => false, - 'is_plot' => true, - 'is_doc' => false - ]), - 'display_nav' => false, - 'title' => false - ]); + return $this->getCSVResponse($app, $activity, 'activity_per_hours'); } + + $report = $activity->getActivityPerHours(); + + return $app->json(array( + 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', array( + 'result' => isset($report['report']) ? $report['report'] : $report, + 'is_infouser' => false, + 'is_nav' => false, + 'is_groupby' => false, + 'is_plot' => true, + 'is_doc' => false + )), + 'display_nav' => false, + 'title' => false + )); } /** @@ -395,33 +371,29 @@ class Activity implements ControllerProviderInterface $activity->setConfig(false); - $report = $activity->getDownloadByBaseByDay($conf); - if ($request->request->get('printcsv') == 'on') { - $activity->setHasLimit(false); - $activity->setPrettyString(false); + $activity->setHasLimit(false); + $activity->setPrettyString(false); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } + $activity->getDownloadByBaseByDay($conf); - return $app->json(['rs' => $csv]); - } else { - return $app->json([ - 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ - 'result' => isset($report['report']) ? $report['report'] : $report, - 'is_infouser' => false, - 'is_nav' => false, - 'is_groupby' => false, - 'is_plot' => false, - 'is_doc' => false - ]), - 'display_nav' => false, - 'title' => false - ]); + return $this->getCSVResponse($app, $activity, 'activity_db_by_base_by_day'); } + + $report = $activity->getDownloadByBaseByDay($conf); + + return $app->json(array( + 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', array( + 'result' => isset($report['report']) ? $report['report'] : $report, + 'is_infouser' => false, + 'is_nav' => false, + 'is_groupby' => false, + 'is_plot' => false, + 'is_doc' => false + )), + 'display_nav' => false, + 'title' => false + )); } /** @@ -456,13 +428,9 @@ class Activity implements ControllerProviderInterface $activity->setHasLimit(false); $activity->setPrettyString(false); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } + $this->doReport($app, $request, $activity, $conf); - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $activity, 'activity_pushed_documents'); } $report = $this->doReport($app, $request, $activity, $conf); @@ -521,13 +489,9 @@ class Activity implements ControllerProviderInterface $activity->setHasLimit(false); $activity->setPrettyString(false); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } + $this->doReport($app, $request, $activity, $conf); - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $activity, 'activity_added_documents'); } $report = $this->doReport($app, $request, $activity, $conf); @@ -586,13 +550,9 @@ class Activity implements ControllerProviderInterface $activity->setHasLimit(false); $activity->setPrettyString(false); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } + $this->doReport($app, $request, $activity, $conf); - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $activity, 'activity_edited_documents'); } $report = $this->doReport($app, $request, $activity, $conf); @@ -652,13 +612,9 @@ class Activity implements ControllerProviderInterface $activity->setHasLimit(false); $activity->setPrettyString(false); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } + $this->doReport($app, $request, $activity, $conf); - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $activity, 'activity_validated_documents'); } $report = $this->doReport($app, $request, $activity, $conf); @@ -718,13 +674,9 @@ class Activity implements ControllerProviderInterface $activity->setHasLimit(false); $activity->setPrettyString(false); - try { - $csv = \format::arr_to_csv($activity->getResult(), $activity->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } + $this->doReport($app, $request, $activity, $conf); - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $activity, 'activity_send_documents'); } $report = $this->doReport($app, $request, $activity, $conf); @@ -887,4 +839,39 @@ class Activity implements ControllerProviderInterface return $reportArray; } + + /** + * Prefix the method to call with the controller class name + * + * @param string $method The method to call + * @return string + */ + private function call($method) + { + return sprintf('%s::%s', __CLASS__, $method); + } + + private function getCSVResponse(Application $app, \module_report $report, $type) + { + // set headers + $headers = array(); + foreach (array_keys($report->getDisplay()) as $k) { + $headers[$k] = $k; + } + // set headers as first row + $result = $report->getResult(); + array_unshift($result, $headers); + + $collection = new CallbackCollection($result, function($row) use ($report) { + // restrict to displayed fields + return array_map('strip_tags', array_intersect_key($row, $report->getDisplay())); + }); + + $filename = sprintf('report_export_%s_%s.csv', $type, date('Ymd')); + $response = new CSVFileResponse($filename, function() use ($app, $collection) { + $app['csv.exporter']->export('php://output', $collection); + }); + + return $response; + } } diff --git a/lib/Alchemy/Phrasea/Controller/Report/Export.php b/lib/Alchemy/Phrasea/Controller/Report/Export.php deleted file mode 100644 index a8bcbd0c04..0000000000 --- a/lib/Alchemy/Phrasea/Controller/Report/Export.php +++ /dev/null @@ -1,74 +0,0 @@ -addMandatoryAuthentication($controllers); - - $controllers->before(function () use ($app) { - $app['firewall']->requireAccessToModule('report'); - }); - - $controllers->post('/csv', 'controller.report.export:exportCSV') - ->bind('report_export_csv'); - - return $controllers; - } - - /** - * Export data to a csv file - * - * @param Application $app - * @param Request $request - * @return Response - */ - public function exportCSV(Application $app, Request $request) - { - $name = $request->request->get('name', 'export'); - - if (null === $data = $request->request->get('csv')) { - $app->abort(400); - } - - $filename = mb_strtolower('report_' . $name . '_' . date('dmY') . '.csv'); - $data = preg_replace('/[ \t\r\f]+/', '', $data); - - $response = new Response($data, 200, [ - 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT', - 'Last-Modified' => gmdate('D, d M Y H:i:s'). ' GMT', - 'Cache-Control' => 'no-store, no-cache, must-revalidate', - 'Cache-Control' => 'post-check=0, pre-check=0', - 'Pragma' => 'no-cache', - 'Content-Type' => 'text/csv', - 'Content-Length' => strlen($data), - 'Cache-Control' => 'max-age=3600, must-revalidate', - 'Content-Disposition' => 'max-age=3600, must-revalidate', - ]); - - $response->headers->set('Content-Disposition', $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename)); - - return $response; - } -} diff --git a/lib/Alchemy/Phrasea/Controller/Report/Informations.php b/lib/Alchemy/Phrasea/Controller/Report/Informations.php index cdba69d281..b593cc8fe9 100644 --- a/lib/Alchemy/Phrasea/Controller/Report/Informations.php +++ b/lib/Alchemy/Phrasea/Controller/Report/Informations.php @@ -11,6 +11,8 @@ namespace Alchemy\Phrasea\Controller\Report; +use Alchemy\Phrasea\Core\Response\CSVFileResponse; +use Goodby\CSV\Export\Standard\Collection\CallbackCollection; use Silex\Application; use Silex\ControllerProviderInterface; use Symfony\Component\HttpFoundation\Request; @@ -183,13 +185,7 @@ class Informations implements ControllerProviderInterface if ($request->request->get('printcsv') == 'on') { $report->setPrettyString(false); - try { - $csv = \format::arr_to_csv($report->getResult(), $report->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $report, 'info_user'); } $html = $app['twig']->render('report/ajax_data_content.html.twig', [ @@ -432,13 +428,7 @@ class Informations implements ControllerProviderInterface if ($request->request->get('printcsv') == 'on') { $download->setPrettyString(false); - try { - $csv = \format::arr_to_csv($download->getResult(), $download->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $download, 'info_document'); } $html .= $app['twig']->render('report/ajax_data_content.html.twig', [ @@ -481,15 +471,8 @@ class Informations implements ControllerProviderInterface $reportArray = $info->buildTabGrpInfo(false, [], $request->request->get('user'), $conf, false); if ($request->request->get('printcsv') == 'on' && isset($download)) { - $download->setPrettyString(false); - try { - $csv = \format::arr_to_csv($download->getResult(), $download->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $info, 'info_user'); } $html .= $app['twig']->render('report/ajax_data_content.html.twig', [ @@ -512,6 +495,41 @@ class Informations implements ControllerProviderInterface 'rs' => $html, 'display_nav' => false, 'title' => $title - ]); + )); + } + + /** + * Prefix the method to call with the controller class name + * + * @param string $method The method to call + * @return string + */ + private function call($method) + { + return sprintf('%s::%s', __CLASS__, $method); + } + + private function getCSVResponse(Application $app, \module_report $report, $type) + { + // set headers + $headers = array(); + foreach (array_keys($report->getDisplay()) as $k) { + $headers[$k] = $k; + } + // set headers as first row + $result = $report->getResult(); + array_unshift($result, $headers); + + $collection = new CallbackCollection($result, function($row) use ($report) { + // restrict fields to the displayed ones + return array_map('strip_tags', array_intersect_key($row, $report->getDisplay())); + }); + + $filename = sprintf('report_export_%s_%s.csv', $type, date('Ymd')); + $response = new CSVFileResponse($filename, function() use ($app, $collection) { + $app['csv.exporter']->export('php://output', $collection); + }); + + return $response; } } diff --git a/lib/Alchemy/Phrasea/Controller/Report/Root.php b/lib/Alchemy/Phrasea/Controller/Report/Root.php index db3b440fc1..5b0b84e954 100644 --- a/lib/Alchemy/Phrasea/Controller/Report/Root.php +++ b/lib/Alchemy/Phrasea/Controller/Report/Root.php @@ -11,11 +11,14 @@ namespace Alchemy\Phrasea\Controller\Report; +use Alchemy\Phrasea\Core\Response\CSVFileResponse; +use Goodby\CSV\Export\Standard\Collection\CallbackCollection; use Silex\Application; use Silex\ControllerProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; class Root implements ControllerProviderInterface { @@ -202,13 +205,7 @@ class Root implements ControllerProviderInterface $this->doReport($app, $request, $cnx, $conf); - try { - $csv = \format::arr_to_csv($cnx->getResult(), $cnx->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $cnx, 'connections'); } $report = $this->doReport($app, $request, $cnx, $conf); @@ -269,13 +266,7 @@ class Root implements ControllerProviderInterface $this->doReport($app, $request, $questions, $conf); - try { - $csv = \format::arr_to_csv($questions->getResult(), $questions->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $questions, 'questions'); } $report = $this->doReport($app, $request, $questions, $conf); @@ -345,13 +336,7 @@ class Root implements ControllerProviderInterface $this->doReport($app, $request, $download, $conf); - try { - $csv = \format::arr_to_csv($download->getResult(), $download->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $download, 'download'); } $report = $this->doReport($app, $request, $download, $conf); @@ -417,13 +402,7 @@ class Root implements ControllerProviderInterface $this->doReport($app, $request, $document, $conf, 'record_id'); - try { - $csv = \format::arr_to_csv($document->getResult(), $document->getDisplay()); - } catch (\Exception $e) { - $csv = ''; - } - - return $app->json(['rs' => $csv]); + return $this->getCSVResponse($app, $document, 'documents'); } $report = $this->doReport($app, $request, $document, $conf, 'record_id'); @@ -503,17 +482,39 @@ class Root implements ControllerProviderInterface 'combo' => $nav->buildTabCombo($conf_combo) ]; - if ($request->request->get('printcsv') == 'on') { - return $app->json([ - 'nav' => \format::arr_to_csv($report['nav']['result'], $conf_nav), - 'os' => \format::arr_to_csv($report['os']['result'], $conf_os), - 'res' => \format::arr_to_csv($report['res']['result'], $conf_res), - 'mod' => \format::arr_to_csv($report['mod']['result'], $conf_mod), - 'combo' => \format::arr_to_csv($report['combo']['result'], $conf_combo) - ]); - } + if ($request->request->get('printcsv') == 'on') { + $result = array(); - return $app->json([ + $result[] = array_keys($conf_nav); + foreach($report['nav']['result'] as $row) { + $result[] = array_values($row); + }; + $result[] = array_keys($conf_os); + foreach($report['os']['result'] as $row) { + $result[] = array_values($row); + }; + $result[] = array_keys($conf_res); + foreach($report['res']['result'] as $row) { + $result[] = array_values($row); + }; + $result[] = array_keys($conf_mod); + foreach($report['mod']['result'] as $row) { + $result[] = array_values($row); + }; + $result[] = array_keys($conf_combo); + foreach($report['combo']['result'] as $row) { + $result[] = array_values($row); + }; + + $filename = sprintf('report_export_info_%s.csv', date('Ymd')); + $response = new CSVFileResponse($filename, function() use ($app, $result) { + $app['csv.exporter']->export('php://output', $result); + }); + + return $response; + } + + return $app->json([ 'rs' => $app['twig']->render('report/ajax_data_content.html.twig', [ 'result' => isset($report['report']) ? $report['report'] : $report, 'is_infouser' => false, @@ -662,4 +663,39 @@ class Root implements ControllerProviderInterface return $reportArray; } + + /** + * Prefix the method to call with the controller class name + * + * @param string $method The method to call + * @return string + */ + private function call($method) + { + return sprintf('%s::%s', __CLASS__, $method); + } + + private function getCSVResponse(Application $app, \module_report $report, $type) + { + // set headers + $headers = array(); + foreach (array_keys($report->getDisplay()) as $k) { + $headers[$k] = $k; + } + // set headers as first row + $result = $report->getResult(); + array_unshift($result, $headers); + + $collection = new CallbackCollection($result, function($row) use ($report) { + // restrict fields to the displayed ones + return array_map('strip_tags', array_intersect_key($row, $report->getDisplay())); + }); + + $filename = sprintf('report_export_%s_%s.csv', $type, date('Ymd')); + $response = new CSVFileResponse($filename, function() use ($app, $collection) { + $app['csv.exporter']->export('php://output', $collection); + }); + + return $response; + } } diff --git a/lib/Alchemy/Phrasea/Core/Provider/CSVServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/CSVServiceProvider.php new file mode 100644 index 0000000000..be841339f3 --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Provider/CSVServiceProvider.php @@ -0,0 +1,77 @@ +share(function () { + $config = new ExporterConfig(); + return $config + ->setDelimiter(",") + ->setEnclosure('"') + ->setEscape("\\") + ->setToCharset('UTF-8') + ->setFromCharset('UTF-8'); + + }); + + $app['csv.exporter'] = $app->share(function ($app) { + return new Exporter($app['csv.exporter.config']); + }); + + $app['csv.lexer.config'] = $app->share(function ($app) { + return new LexerConfig(); + }); + + $app['csv.lexer'] = $app->share(function ($app) { + return new Lexer($app['csv.lexer.config']); + }); + + $app['csv.interpreter'] = $app->share(function ($app) { + return new Interpreter(); + }); + + $app['csv.response'] = $app->protect(function ($callback) use ($app) { + // set headers to fix ie issues + $response = new StreamedResponse($callback, 200, array( + 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT', + 'Last-Modified' => gmdate('D, d M Y H:i:s'). ' GMT', + 'Cache-Control' => 'no-store, no-cache, must-revalidate', + 'Cache-Control' => 'post-check=0, pre-check=0', + 'Pragma' => 'no-cache', + 'Content-Type' => 'text/csv', + 'Cache-Control' => 'max-age=3600, must-revalidate', + 'Content-Disposition' => 'max-age=3600, must-revalidate', + )); + + $response->headers->set('Content-Disposition', $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + 'export.csv' + )); + }); + } + + public function boot(Application $app) + { + } +} diff --git a/lib/Alchemy/Phrasea/Core/Response/CSVFileResponse.php b/lib/Alchemy/Phrasea/Core/Response/CSVFileResponse.php new file mode 100644 index 0000000000..19863dfcee --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Response/CSVFileResponse.php @@ -0,0 +1,43 @@ + 'Mon, 26 Jul 1997 05:00:00 GMT', + 'Last-Modified' => gmdate('D, d M Y H:i:s'). ' GMT', + 'Cache-Control' => 'no-store, no-cache, must-revalidate', + 'Cache-Control' => 'post-check=0, pre-check=0', + 'Pragma' => 'no-cache', + 'Cache-Control' => 'max-age=3600, must-revalidate', + 'Content-Disposition' => 'max-age=3600, must-revalidate', + ), + $headers + )); + + $this->headers->set('Content-Type', 'text/csv'); + + $this->headers->set('Content-Disposition', $this->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + $filename, + // ascii filename fallback + false === preg_match('/^[\x20-\x7e]*$/', $filename) ? '' : preg_replace('/[^(x20-x7F)]*$/', '', $filename) + )); + } +} \ No newline at end of file diff --git a/lib/classes/format.php b/lib/classes/format.php deleted file mode 100644 index eb88f5149b..0000000000 --- a/lib/classes/format.php +++ /dev/null @@ -1,97 +0,0 @@ - $value) { - foreach ($tmp as $k => $v) { - if ($key == $k) { - $line[] = '"' . str_replace('"', '""', strip_tags($v)) . '"'; - } - } - } - } - - if ($tri_column && count($tri_column) == count($line)) { - return implode(",", $line); - } elseif (count($arr) == count($line)) { - return implode(",", $line); - } else - throw new Exception('CSV failed'); - } - - public static function arr_to_csv($arr, $tri_column = false) - { - $lines = []; - - if ($tri_column) { - $title = ""; - foreach ($tri_column as $v) { - if (isset($v['title'])) - $title .= ( empty($title) ? "" : ",") . '"' . str_replace('"', '""', strip_tags($v['title'])) . '"'; - } - ! empty($title) ? $lines[] = $title : ""; - } - foreach ($arr as $v) { - $lines[] = self::arr_to_csv_line($v, $tri_column); - } - - return implode("\n", $lines); - } - - public static function csv_to_arr($filename) - { - $separateur = ","; - $array = []; - // For mac - $autoDetectLineEndings = ini_get("auto_detect_line_endings"); - - ini_set("auto_detect_line_endings", true); - - if ($file = fopen($filename, "r")) { - $test1 = fgetcsv($file, 1024, ","); - rewind($file); - $test2 = fgetcsv($file, 1024, ";"); - rewind($file); - - if (count($test1) == 1 || ( count($test2) > count($test1) && count($test2) < 20)) { - $separateur = ";"; - } - - while ($array[] = fgetcsv($file, 1024, $separateur)); - fclose($file); - array_pop($array); - } - - ini_set("auto_detect_line_endings", $autoDetectLineEndings); - - return $array; - } -} diff --git a/templates/web/report/generate_tab.html.twig b/templates/web/report/generate_tab.html.twig index 4624f8e5f7..17a9cffbcd 100644 --- a/templates/web/report/generate_tab.html.twig +++ b/templates/web/report/generate_tab.html.twig @@ -18,12 +18,9 @@ {% endif %} {% if result.csv %} -
- - - - -
+ + + {% endif %}
{{ result.periode }}
diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Admin/UsersTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Admin/UsersTest.php index 84f6b8023e..c7b2a35832 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Admin/UsersTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Admin/UsersTest.php @@ -217,7 +217,8 @@ class UsersTest extends \PhraseanetAuthenticatedWebTestCase $response = self::$DI['client']->getResponse(); $this->assertTrue($response->isOK()); $this->assertEquals("text/csv; charset=UTF-8", $response->headers->get("Content-type")); - $this->assertEquals("attachment; filename=export.csv", $response->headers->get("content-disposition")); + $date = new \DateTime(); + $this->assertEquals('attachment; filename="user_export_'.$date->format('Ymd').'.csv"', $response->headers->get("content-disposition")); } public function testRouteThSearch() @@ -319,7 +320,8 @@ class UsersTest extends \PhraseanetAuthenticatedWebTestCase $this->assertTrue($response->isOK()); $this->assertRegexp("#text/csv#", $response->headers->get("content-type")); $this->assertRegexp("#charset=UTF-8#", $response->headers->get("content-type")); - $this->assertEquals("attachment; filename=export.csv", $response->headers->get("content-disposition")); + $date = new \DateTime(); + $this->assertEquals('attachment; filename="user_export_'.$date->format('Ymd').'.csv"', $response->headers->get("content-disposition")); } public function testResetRights() diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Report/ExportTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Report/ExportTest.php deleted file mode 100644 index 9b99e6d6b2..0000000000 --- a/tests/Alchemy/Tests/Phrasea/Controller/Report/ExportTest.php +++ /dev/null @@ -1,26 +0,0 @@ -request('POST', '/report/export/csv', [ - 'csv' => $data, - 'name' => 'test', - ]); - - $response = self::$DI['client']->getResponse(); - - $this->assertTrue($response->isOk()); - - $this->assertRegexp('/attachment/', $response->headers->get('content-disposition')); - $this->assertRegexp('/report_test/', $response->headers->get('content-disposition')); - } -} diff --git a/www/skins/report/report.js b/www/skins/report/report.js index 84586dcd46..013672d965 100644 --- a/www/skins/report/report.js +++ b/www/skins/report/report.js @@ -790,68 +790,27 @@ function print() { } function csv() { - var button = $(".form_csv input[name=submit]"); + var button = $("#export_csv"); button.unbind("click").bind("click", function (e) { e.preventDefault(); var $this = $(this); - var $formm = $this.closest("form"); if ($this.closest("#dialog").length > 0) { var $form = $("#dialog").data("dataForm"); - } - else { + } else { var $form = $this.closest(".ui-tabs-panel").find(".report_form"); } - $form.find("input[name=printcsv]").val("on"); + //clone form and submit + var clone = $form.clone().appendTo($this); - if (button.data('ajaxRunning')) { - button.data('ajaxQuery').abort(); - button.data('ajaxRunning', false); - } + clone.attr("action", $form.find("input[name=action]").val()) + .attr("method",'POST') + .removeAttr("onsubmit") + .find("input[name=printcsv]").val("on"); - var query = $.ajax({ - type: "POST", - url: $form.find("input[name=action]").val(), - dataType: "json", - data: $form.serializeArray(), - beforeSend: function () { - $formm.after("
"); - $formm.next("div").addClass("onload"); - button.data('ajaxRunning', true); - }, - timeOut: function () { - button.data('ajaxRunning', false); - }, - error: function () { - button.data('ajaxRunning', false); - }, - success: function (data) { - $formm.next("div").remove(); - button.data('ajaxRunning', false); - $form.find("input[name=printcsv]").val("off"); - - if (typeof data.rs === "object") { - var $key = $this.closest("table").attr("class"); - var $csv = data.rs[$key]; - $formm.find("textarea[name=csv]").val($csv); - } - else if (data.rs === false) { - $("body").append("
Une erreur s'est produite
"); - $("#dialog").dialog({ - close: function () { - $(this).remove(); - } - }); - } - else { - $formm.find("textarea[name=csv]").val(data.rs); - } - $formm.find("input[name=doit]").trigger('click'); - } - }); - button.data('ajaxQuery', query); + $(clone).submit().remove(); }); }