PHRAS-2189 report v2 master (#2742)

move code to services
added "download" report
change services to factory
added excel lib
added prod/report routes (download)
cleanup api routes
add : allow anonymized (user, fonction, societe... are "-")
removed : xls support (memory eating lib) in favor of xlsx
add : report download only on "document" and "preview" subdef classes
cs : report factory
add : restored "site" filter (see todos in src)
remove debug, cs

todo : doc
This commit is contained in:
jygaulier
2018-11-12 19:43:40 +01:00
committed by GitHub
parent 9298580888
commit b8bebbce11
18 changed files with 3791 additions and 717 deletions

View File

@@ -119,7 +119,8 @@
"alchemy/worker-bundle": "^0.1.6",
"alchemy/queue-bundle": "^0.1.5",
"google/recaptcha": "^1.1",
"facebook/graph-sdk": "^5.6"
"facebook/graph-sdk": "^5.6",
"box/spout": "^2.7"
},
"require-dev": {
"mikey179/vfsStream": "~1.5",

70
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "27b7c5232802fb93dcf5dc79141df96f",
"content-hash": "253990d4c81f9ae5f78c6c1221d6cf29",
"packages": [
{
"name": "alchemy-fr/tcpdf-clone",
@@ -1024,6 +1024,74 @@
],
"time": "2015-09-28T16:26:35+00:00"
},
{
"name": "box/spout",
"version": "v2.7.3",
"source": {
"type": "git",
"url": "https://github.com/box/spout.git",
"reference": "3681a3421a868ab9a65da156c554f756541f452b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/box/spout/zipball/3681a3421a868ab9a65da156c554f756541f452b",
"reference": "3681a3421a868ab9a65da156c554f756541f452b",
"shasum": ""
},
"require": {
"ext-xmlreader": "*",
"ext-zip": "*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.0"
},
"suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
"ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8.x-dev"
}
},
"autoload": {
"psr-4": {
"Box\\Spout\\": "src/Spout"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Adrien Loison",
"email": "adrien@box.com"
}
],
"description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
"homepage": "https://www.github.com/box/spout",
"keywords": [
"OOXML",
"csv",
"excel",
"memory",
"odf",
"ods",
"office",
"open",
"php",
"read",
"scale",
"spreadsheet",
"stream",
"write",
"xlsx"
],
"time": "2017-09-25T19:44:35+00:00"
},
{
"name": "cocur/slugify",
"version": "v2.3",

View File

@@ -24,9 +24,11 @@ use Alchemy\Phrasea\Core\Event\Subscriber\ApiExceptionHandlerSubscriber;
use Alchemy\Phrasea\Core\Event\Subscriber\ApiOauth2ErrorsSubscriber;
use Alchemy\Phrasea\Core\PhraseaEvents;
use Alchemy\Phrasea\Core\Provider\JsonSchemaServiceProvider;
use Alchemy\Phrasea\Report\ControllerProvider\ApiReportControllerProvider;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiApplicationLoader extends BaseApplicationLoader
{
protected function doPrePluginServiceRegistration(Application $app)
@@ -34,6 +36,7 @@ class ApiApplicationLoader extends BaseApplicationLoader
$app->register(new OAuth2());
$app->register(new V1());
$app->register(new V2());
$app->register(new ApiReportControllerProvider());
$app->register(new JsonSchemaServiceProvider());
}
@@ -132,6 +135,7 @@ class ApiApplicationLoader extends BaseApplicationLoader
$app->mount('/datafiles/', new Datafiles());
$app->mount('/api/v1', new V1());
$app->mount('/api/v2', new V2());
$app->mount('/api/report', new ApiReportControllerProvider());
$app->mount('/permalink/', new Permalink());
$app->mount($app['controller.media_accessor.route_prefix'], new MediaAccessor());
$app->mount('/include/minify/', new Minifier());

View File

@@ -5,9 +5,11 @@ namespace Alchemy\Phrasea\Application;
use Alchemy\EmbedProvider\EmbedServiceProvider;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\ControllerProvider as Providers;
use Alchemy\Phrasea\Report\ControllerProvider\ProdReportControllerProvider;
use Assert\Assertion;
use Silex\ControllerProviderInterface;
class RouteLoader
{
@@ -53,6 +55,7 @@ class RouteLoader
'/prod/records/edit' => Providers\Prod\Edit::class,
'/prod/records/movecollection' => Providers\Prod\MoveCollection::class,
'/prod/records/property' => Providers\Prod\Property::class,
'/prod/report/' => ProdReportControllerProvider::class,
'/prod/share/' => Providers\Prod\Share::class,
'/prod/story' => Providers\Prod\Story::class,
'/prod/subdefs' => Providers\Prod\Subdefs::class,

View File

@@ -77,6 +77,7 @@ class ControllerProviderServiceProvider implements ServiceProviderInterface
Prod\Push::class => [],
Prod\Query::class => [],
Prod\Record::class => [],
\Alchemy\Phrasea\Report\ControllerProvider\ProdReportControllerProvider::class => [],
Prod\Root::class => [],
Prod\Share::class => [],
Prod\Story::class => [],

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Out\Module;
use Box\Spout\Writer;
use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type;
class Excel
{
const FORMAT_CSV = 'format_csv';
const FORMAT_ODS = 'format_ods';
const FORMAT_XLSX = 'format_xlsx';
private $format;
/** @var \Box\Spout\Writer\WriterInterface */
private $writer;
public function __construct($format, $filename)
{
$this->format = $format;
switch($format) {
case self::FORMAT_CSV:
/** @var Writer\CSV\Writer $writer */
$writer = WriterFactory::create(Type::CSV);
$writer->setFieldDelimiter(';')
->setShouldAddBOM(false);
break;
case self::FORMAT_ODS:
/** @var Writer\ODS\Writer $writer */
$writer = WriterFactory::create(Type::ODS);
break;
case self::FORMAT_XLSX:
/** @var Writer\XLSX\Writer $writer */
$writer = WriterFactory::create(Type::XLSX);
break;
default:
throw new \InvalidArgumentException(sprintf("format \"%s\" is not handled by Spout"));
break;
}
$writer->openToBrowser($filename);
$this->writer = $writer;
}
public function __destruct()
{
$this->writer->close();
}
public function getActiveSheet()
{
if($this->format == self::FORMAT_CSV) {
return "_unique_sheet_";
}
/** @var Writer\XLSX\Writer $w */
$w = $this->writer;
$sheetIndex = $w->getCurrentSheet()->getIndex();
return $sheetIndex;
}
public function addRow($row)
{
$this->writer->addRow($row);
}
public function render()
{
$this->writer->close();
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*
namespace Alchemy\Phrasea\Out\Module;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
use PhpOffice\PhpSpreadsheet\IOFactory;
class Excel
{
const FORMAT_CSV = 'format_csv';
const FORMAT_XLS = 'format_xls';
const FORMAT_XLSX = 'format_xlsx';
private $spreadsheet;
/** @var int[] * /
private $currentRowBySheet;
public function __construct()
{
$this->currentRowBySheet = [];
$this->spreadsheet = new Spreadsheet();
}
public function getActiveSheet()
{
$sheetIndex = $this->spreadsheet->getActiveSheetIndex();
if(!array_key_exists($sheetIndex, $this->currentRowBySheet)) {
$this->currentRowBySheet[$sheetIndex] = 1;
}
return $this->spreadsheet->getActiveSheet();
}
public function addRow($row)
{
$sheet = $this->getActiveSheet();
$sheetIndex = $this->spreadsheet->getActiveSheetIndex();
/** @var int $r * /
$r = $this->currentRowBySheet[$sheetIndex];
$c = 1;
foreach($row as $v) {
$sheet->setCellValueByColumnAndRow($c++, $r, $v);
}
$this->currentRowBySheet[$sheetIndex] = $r+1;
}
public function fill()
{
$sheet = $this->getActiveSheet();
$sheet->setCellValue('A1', 'Hello World !');
}
public function render($format)
{
switch($format) {
case self::FORMAT_XLS:
header('Content-Type: application/vnd.ms-excel');
$writer = IOFactory::createWriter($this->spreadsheet, 'Xls');
break;
case self::FORMAT_XLSX:
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$writer = IOFactory::createWriter($this->spreadsheet, 'Xlsx');
break;
}
header('Content-Disposition: attachment;filename="myfile.xls"');
header('Cache-Control: max-age=0');
$writer = IOFactory::createWriter($this->spreadsheet, 'Xls');
$writer->save('php://output');
}
}
*/

View File

@@ -0,0 +1,145 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report\Controller;
use Alchemy\Phrasea\Application\Helper\JsonBodyAware;
use Alchemy\Phrasea\Controller\Api\Result;
use Alchemy\Phrasea\Report\ReportConnections;
use Alchemy\Phrasea\Report\ReportDownloads;
use Alchemy\Phrasea\Report\ReportFactory;
use Alchemy\Phrasea\Report\ReportRecords;
use Alchemy\Phrasea\Report\ReportService;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\HttpFoundation\Request;
class ApiReportController
{
use JsonBodyAware;
private $reportFactory;
private $reportService;
private $anonymousReport;
private $acl;
/**
* @param ReportFactory $reportFactory
* @param ReportService $reportService
* @param Bool $anonymousReport
* @param \ACL $acl
*/
public function __construct(ReportFactory $reportFactory, ReportService $reportService, $anonymousReport, \ACL $acl)
{
$this->reportFactory = $reportFactory;
$this->reportService = $reportService;
$this->anonymousReport = $anonymousReport;
$this->acl = $acl;
}
/**
* route api/report
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function rootAction(Request $request)
{
$ret = [
'granted' => $this->reportService->getGranted()
];
$result = Result::create($request, $ret);
return $result->createResponse();
}
/**
* route api/report/connections
*
* @param Request $request
* @param $sbasId
* @return \Symfony\Component\HttpFoundation\Response
*/
public function connectionsAction(Request $request, $sbasId)
{
/** @var ReportConnections $report */
$report = $this->reportFactory->createReport(
ReportFactory::CONNECTIONS,
$sbasId,
[
'dmin' => $request->get('dmin'),
'dmax' => $request->get('dmax'),
'group' => $request->get('group'),
'anonymize' => $this->anonymousReport,
]
);
$result = Result::create($request, $report->getContent());
return $result->createResponse();
}
/**
* route api/report/downloads
*
* @param Request $request
* @param $sbasId
* @return \Symfony\Component\HttpFoundation\Response
*/
public function downloadsAction(Request $request, $sbasId)
{
/** @var ReportDownloads $report */
$report = $this->reportFactory->createReport(
ReportFactory::DOWNLOADS,
$sbasId,
[
'dmin' => $request->get('dmin'),
'dmax' => $request->get('dmax'),
'group' => $request->get('group'),
'bases' => $request->get('base'),
'anonymize' => $this->anonymousReport,
]
);
$result = Result::create($request, $report->getContent());
return $result->createResponse();
}
/**
* route api/report/records
*
* @param Request $request
* @param $sbasId
* @return \Symfony\Component\HttpFoundation\Response
*/
public function recordsAction(Request $request, $sbasId)
{
/** @var ReportRecords $report */
$report = $this->reportFactory->createReport(
ReportFactory::RECORDS,
$sbasId,
[
'dmin' => $request->get('dmin'),
'dmax' => $request->get('dmax'),
'group' => $request->get('group'),
'base' => $request->get('base'),
'meta' => $request->get('meta'),
]
);
$result = Result::create($request, $report->getContent());
return $result->createResponse();
}
}

View File

@@ -0,0 +1,195 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report\Controller;
use Alchemy\Phrasea\Report\Report;
use Alchemy\Phrasea\Report\ReportConnections;
use Alchemy\Phrasea\Report\ReportDownloads;
use Alchemy\Phrasea\Report\ReportFactory;
use Alchemy\Phrasea\Report\ReportRecords;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ProdReportController
{
private static $mapFromExtension = [
'csv' => [
'contentType' => 'text/csv',
'format' => Report::FORMAT_CSV,
],
'ods' => [
'contentType' => 'application/vnd.oasis.opendocument.spreadsheet',
'format' => Report::FORMAT_ODS,
],
'xlsx' => [
'contentType' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'format' => Report::FORMAT_XLSX,
],
];
private $reportFactory;
private $anonymousReport;
private $acl;
private $extension = null;
/**
* @param ReportFactory $reportFactory
* @param Bool $anonymousReport
* @param \ACL $acl
*/
public function __construct(ReportFactory $reportFactory, $anonymousReport, \ACL $acl)
{
$this->reportFactory = $reportFactory;
$this->anonymousReport = $anonymousReport;
$this->acl = $acl;
}
/**
* route prod/report/connections
*
* @param Request $request
* @param $sbasId
* @return StreamedResponse
*/
public function connectionsAction(Request $request, $sbasId)
{
if(!($extension = $request->get('format'))) {
$extension = 'csv';
}
if(!array_key_exists($extension, self::$mapFromExtension)) {
throw new \InvalidArgumentException(sprintf("bad format \"%s\" for report", $extension));
}
$this->extension = $extension;
/** @var ReportConnections $report */
$report = $this->reportFactory->createReport(
ReportFactory::CONNECTIONS,
$sbasId,
[
'dmin' => $request->get('dmin'),
'dmax' => $request->get('dmax'),
'group' => $request->get('group'),
'anonymize' => $this->anonymousReport,
]
);
$report->setFormat(self::$mapFromExtension[$this->extension]['format']);
$response = new StreamedResponse();
$this->setHeadersFromFormat($response, $report);
$response->setCallback(function() use($report) {
$report->render();
});
return $response;
}
/**
* route prod/report/downloads
*
* @param Request $request
* @param $sbasId
* @return StreamedResponse
*/
public function downloadsAction(Request $request, $sbasId)
{
if(!($extension = $request->get('format'))) {
$extension = 'csv';
}
if(!array_key_exists($extension, self::$mapFromExtension)) {
throw new \InvalidArgumentException(sprintf("bad format \"%s\" for report", $extension));
}
$this->extension = $extension;
/** @var ReportDownloads $report */
$report = $this->reportFactory->createReport(
ReportFactory::DOWNLOADS,
$sbasId,
[
'dmin' => $request->get('dmin'),
'dmax' => $request->get('dmax'),
'group' => $request->get('group'),
'bases' => $request->get('base'),
'anonymize' => $this->anonymousReport,
]
);
$report->setFormat(self::$mapFromExtension[$this->extension]['format']);
$response = new StreamedResponse();
$this->setHeadersFromFormat($response, $report);
$response->setCallback(function() use($report) {
$report->render();
});
return $response;
}
/**
* route prod/report/records
*
* @param Request $request
* @param $sbasId
* @return StreamedResponse
*/
public function recordsAction(Request $request, $sbasId)
{
if(!($extension = $request->get('format'))) {
$extension = 'csv';
}
if(!array_key_exists($extension, self::$mapFromExtension)) {
throw new \InvalidArgumentException(sprintf("bad format \"%s\" for report", $extension));
}
$this->extension = $extension;
/** @var ReportRecords $report */
$report = $this->reportFactory->createReport(
ReportFactory::RECORDS,
$sbasId,
[
'dmin' => $request->get('dmin'),
'dmax' => $request->get('dmax'),
'group' => $request->get('group'),
'base' => $request->get('base'),
'meta' => $request->get('meta'),
]
);
$report->setFormat(self::$mapFromExtension[$this->extension]['format']);
set_time_limit(600);
$response = new StreamedResponse();
$this->setHeadersFromFormat($response, $report);
$response->setCallback(function() use($report) {
$report->render();
});
return $response;
}
private function setHeadersFromFormat($response, Report $report)
{
$response->headers->set('Content-Type', self::$mapFromExtension[$this->extension]['contentType']);
$response->headers->set('Content-Disposition', 'attachment;filename="' . $report->getName() . '"');
$response->headers->set('Cache-Control', 'max-age=0');
}
}

View File

@@ -0,0 +1,108 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report\ControllerProvider;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Alchemy\Phrasea\ControllerProvider\Api\Api;
use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait;
use Alchemy\Phrasea\Core\Event\Listener\OAuthListener;
use Alchemy\Phrasea\Report\Controller\ApiReportController;
use Alchemy\Phrasea\Report\ReportFactory;
use Alchemy\Phrasea\Report\ReportService;
use Silex\Application;
use Silex\Controller;
use Silex\ControllerProviderInterface;
use Silex\ServiceProviderInterface;
class ApiReportControllerProvider extends Api implements ControllerProviderInterface, ServiceProviderInterface
{
use ControllerProviderTrait;
const VERSION = '2.0.0';
public function register(Application $app)
{
$app['controller.api.v2.report'] = $app->share(
function (PhraseaApplication $app) {
return (new ApiReportController(
$app['report.factory'],
$app['report.service'],
$app['conf']->get(['registry', 'modules', 'anonymous-report']),
$app->getAclForUser($app->getAuthenticatedUser())
));
}
);
$app['report.factory'] = $app->share(
function (PhraseaApplication $app) {
return (new ReportFactory(
$app['conf']->get(['main', 'key']),
$app['phraseanet.appbox'],
$app->getAclForUser($app->getAuthenticatedUser())
));
}
);
$app['report.service'] = $app->share(
function (PhraseaApplication $app) {
return (new ReportService(
$app['conf']->get(['main', 'key']),
$app['phraseanet.appbox'],
$app->getAclForUser($app->getAuthenticatedUser())
));
}
);
}
public function boot(Application $app)
{
// Intentionally left empty
}
public function connect(Application $app)
{
if (! $this->isApiEnabled($app)) {
return $app['controllers_factory'];
}
$controllers = $this->createCollection($app);
/*
$firewall = $this->getFirewall($app);
$controllers->before(function () use ($firewall) {
$firewall->requireAccessToModule('report');
});
*/
$controllers->before(new OAuthListener());
$controllers
->get('/', 'controller.api.v2.report:rootAction')
;
$controllers
->get('/connections/{sbasId}/', 'controller.api.v2.report:connectionsAction')
->assert('sbasId', '\d+')
;
$controllers
->get('/downloads/{sbasId}/', 'controller.api.v2.report:downloadsAction')
->assert('sbasId', '\d+')
;
$controllers
->get('/records/{sbasId}/', 'controller.api.v2.report:recordsAction')
->assert('sbasId', '\d+')
;
return $controllers;
}
}

View File

@@ -0,0 +1,79 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report\ControllerProvider;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait;
use Alchemy\Phrasea\Report\Controller\ProdReportController;
use Alchemy\Phrasea\Report\ReportFactory;
use Silex\Application;
use Silex\ControllerProviderInterface;
use Silex\ServiceProviderInterface;
class ProdReportControllerProvider implements ControllerProviderInterface, ServiceProviderInterface
{
use ControllerProviderTrait;
public function register(Application $app)
{
$app['controller.prod.report'] = $app->share(
function (PhraseaApplication $app) {
return (new ProdReportController(
$app['report.factory'],
$app['conf']->get(['registry', 'modules', 'anonymous-report']),
$app->getAclForUser($app->getAuthenticatedUser())
));
}
);
$app['report.factory'] = $app->share(
function (PhraseaApplication $app) {
return (new ReportFactory(
$app['conf']->get(['main', 'key']),
$app['phraseanet.appbox'],
$app->getAclForUser($app->getAuthenticatedUser())
));
}
);
}
public function boot(Application $app)
{
// no-op
}
/**
* {@inheritDoc}
*/
public function connect(Application $app)
{
$controllers = $this->createAuthenticatedCollection($app);
$controllers
->get('/connections/{sbasId}/', 'controller.prod.report:connectionsAction')
->assert('sbasId', '\d+')
;
$controllers
->get('/downloads/{sbasId}/', 'controller.prod.report:downloadsAction')
->assert('sbasId', '\d+')
;
$controllers
->get('/records/{sbasId}/', 'controller.prod.report:recordsAction')
->assert('sbasId', '\d+')
;
return $controllers;
}
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Out\Module\Excel;
abstract class Report
{
const FORMAT_CSV = 'format_csv';
const FORMAT_ODS = 'format_ods';
// const FORMAT_XLS = 'format_xls';
const FORMAT_XLSX = 'format_xlsx';
private $format = self::FORMAT_CSV;
/** @var \databox */
protected $databox;
protected $parms;
public function __construct(\databox $databox, $parms)
{
$this->databox = $databox;
$this->parms = $parms;
$this->databox->get_connection()->getWrappedConnection()->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, FALSE);
}
abstract function getName();
abstract function getColumnTitles();
abstract function getKeyName();
abstract function getAllRows($callback);
protected function getDatabox()
{
return $this->databox;
}
public function getContent()
{
$ret = [];
$this->getAllRows(
function($row) use($ret) {
$ret[] = $row;
}
);
return $ret;
}
/**
* get quoted coll id's granted for report, possibly filtered by
* baseIds : only from this list of bases
*
* @param \ACL $acl
* @param int[]|null $baseIds
* @return array
*/
protected function getCollIds(\ACL $acl, $baseIds)
{
$ret = [];
/** @var \collection $collection */
foreach($acl->get_granted_base([\ACL::CANREPORT]) as $collection) {
if($collection->get_sbas_id() != $this->databox->get_sbas_id()) {
continue;
}
if(!is_null($baseIds) && !in_array($collection->get_base_id(), $baseIds)) {
continue;
}
$ret[] = $this->databox->get_connection()->quote($collection->get_coll_id());
}
return $ret;
}
public function setFormat($format)
{
if(!in_array($format, [
//self::FORMAT_XLS,
self::FORMAT_CSV,
self::FORMAT_ODS,
self::FORMAT_XLSX,
])) {
throw new \InvalidArgumentException(sprintf("bad format \"%s\" for report", $format));
}
$this->format = $format;
return $this;
}
public function getFormat()
{
return $this->format;
}
public function render()
{
switch($this->format) {
//case self::FORMAT_XLS:
case self::FORMAT_CSV:
case self::FORMAT_ODS:
case self::FORMAT_XLSX:
$this->renderAsExcel();
break;
default:
// should not happen since format is checked before
break;
}
}
private function renderAsExcel()
{
switch($this->format) {
//case self::FORMAT_XLS:
// $excel = new Excel(Excel::FORMAT_XLS);
// header('Content-Type: application/vnd.ms-excel');
// break;
case self::FORMAT_XLSX:
$excel = new Excel(Excel::FORMAT_XLSX, $this->getName() . ".xlsx");
break;
case self::FORMAT_ODS:
$excel = new Excel(Excel::FORMAT_ODS, $this->getName() . ".ods");
break;
case self::FORMAT_CSV:
default:
$excel = new Excel(Excel::FORMAT_CSV, $this->getName() . ".csv");
break;
}
$excel->addRow($this->getColumnTitles());
$n = 0;
$this->getAllRows(
function($row) use($excel, $n) {
$excel->addRow($row);
if($n++ % 10000 === 0) {
flush();
}
}
);
$excel->render();
}
}

View File

@@ -0,0 +1,149 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
class ReportConnections extends Report
{
private $appKey;
/* those vars will be set once by computeVars() */
private $name = null;
private $sql = null;
private $columnTitles = [];
private $keyName = null;
public function getColumnTitles()
{
$this->computeVars();
return $this->columnTitles;
}
public function getKeyName()
{
$this->computeVars();
return $this->keyName;
}
public function getName()
{
$this->computeVars();
return $this->name;
}
public function setAppKey($appKey)
{
$this->appKey = $appKey;
return $this;
}
public function getAllRows($callback)
{
$this->computeVars();
$stmt = $this->databox->get_connection()->executeQuery($this->sql, []);
while (($row = $stmt->fetch())) {
$callback($row);
}
$stmt->closeCursor();
}
private function computeVars()
{
if(!is_null($this->name)) {
// vars already computed
return;
}
switch($this->parms['group']) {
case null:
$this->name = "Connections";
$this->columnTitles = ['id', 'date', 'usrid', 'user', 'fonction', 'societe', 'activite', 'pays', 'nav', 'version', 'os', 'res', 'ip', 'user_agent'];
if($this->parms['anonymize']) {
$sql = "SELECT `id`, `date`,\n"
. " `usrid`, '-' AS `user`, '-' AS `fonction`, '-' AS `societe`, '-' AS `activite`, '-' AS `pays`,\n"
. " `nav`, `version`, `os`, `res`, `ip`, `user_agent` FROM `log`\n"
. " WHERE {{GlobalFilter}}";
}
else {
$sql = "SELECT `id`, `date`,\n"
. " `usrid`, `user`, `fonction`, `societe`, `activite`, `pays`,\n"
. " `nav`, `version`, `os`, `res`, `ip`, `user_agent` FROM `log`\n"
. " WHERE {{GlobalFilter}}";
}
$this->keyName = null;
break;
case 'user':
$this->name = "Connections per user";
$this->columnTitles = ['user_id', 'user', 'fonction', 'societe', 'activite', 'pays', 'min_date', 'max_date', 'nb'];
if($this->parms['anonymize']) {
$sql = "SELECT `usrid`, '-' AS `user`, '-' AS `fonction`, '-' AS `societe`, '-' AS `activite`, '-' AS `pays`,\n"
. " MIN(`date`) AS `dmin`, MAX(`date`) AS `dmax`, SUM(1) AS `nb` FROM `log`\n"
. " WHERE {{GlobalFilter}}\n"
. " GROUP BY `usrid`\n"
. " ORDER BY `nb` DESC";
}
else {
$sql = "SELECT `usrid`, `user`, `fonction`, `societe`, `activite`, `pays`,\n"
. " MIN(`date`) AS `dmin`, MAX(`date`) AS `dmax`, SUM(1) AS `nb` FROM `log`\n"
. " WHERE {{GlobalFilter}}\n"
. " GROUP BY `usrid`\n"
. " ORDER BY `nb` DESC";
}
$this->keyName = 'usrid';
break;
case 'nav':
case 'nav,version':
case 'os':
case 'os,nav':
case 'os,nav,version':
case 'res':
$this->name = "Connections per " . $this->parms['group'];
$groups = explode(',', $this->parms['group']);
$qgroups = implode(
',',
array_map(function($g) {return '`'.$g.'`';}, $groups)
);
$this->columnTitles = $groups;
$this->columnTitles[] = 'nb';
$sql = "SELECT " . $qgroups . ", SUM(1) AS `nb` FROM `log`\n"
. " WHERE {{GlobalFilter}}\n"
. " GROUP BY " . $qgroups . "\n"
. " ORDER BY `nb` DESC"
;
$this->keyName = null;
break;
default:
throw new InvalidArgumentException('invalid "group" argument');
break;
}
$filter = "`usrid`>0";
// next line : comment to disable "site", to test on an imported dataset from another instance
$filter .= " AND `site` = " . $this->databox->get_connection()->quote($this->appKey);
if($this->parms['dmin']) {
$filter .= "\n AND `log`.`date` >= " . $this->databox->get_connection()->quote($this->parms['dmin']);
}
if($this->parms['dmax']) {
$filter .= "\n AND `log`.`date` <= " . $this->databox->get_connection()->quote($this->parms['dmax']);
}
$this->sql = str_replace('{{GlobalFilter}}', $filter, $sql);
// file_put_contents("/tmp/phraseanet-log.txt", sprintf("%s (%d) %s\n", __FILE__, __LINE__, var_export($this->sql, true)), FILE_APPEND);
}
}

View File

@@ -0,0 +1,179 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
class ReportDownloads extends Report
{
private $appKey;
/** @var \ACL */
private $acl;
/* those vars will be set once by computeVars() */
private $name = null;
private $sql = null;
private $columnTitles = [];
private $keyName = null;
public function getColumnTitles()
{
$this->computeVars();
return $this->columnTitles;
}
public function getKeyName()
{
$this->computeVars();
return $this->keyName;
}
public function getName()
{
$this->computeVars();
return $this->name;
}
public function setAppKey($appKey)
{
$this->appKey = $appKey;
return $this;
}
public function setACL($acl)
{
$this->acl = $acl;
return $this;
}
public function getAllRows($callback)
{
$this->computeVars();
$stmt = $this->databox->get_connection()->executeQuery($this->sql, []);
while (($row = $stmt->fetch())) {
$callback($row);
}
$stmt->closeCursor();
}
private function computeVars()
{
if(!is_null($this->name)) {
// vars already computed
return;
}
switch ($this->parms['group']) {
case null:
$this->name = "Downloads";
$this->columnTitles = ['id', 'usrid', 'user', 'fonction', 'societe', 'activite', 'pays', 'date', 'record_id', 'coll_id', 'subdef'];
if($this->parms['anonymize']) {
$sql = "SELECT `ld`.`id`, `l`.`usrid`, '-' AS `user`, '-' AS `fonction`, '-' AS `societe`, '-' AS `activite`, '-' AS `pays`,\n"
. " `ld`.`date`, `ld`.`record_id`, `ld`.`coll_id`, `ld`.`final`"
. " FROM `log_docs` AS `ld` INNER JOIN `log` AS `l` ON `l`.`id`=`ld`.`log_id`\n"
. " WHERE {{GlobalFilter}}";
}
else {
$sql = "SELECT `ld`.`id`, `l`.`usrid`, `l`.`user`, `l`.`fonction`, `l`.`societe`, `l`.`activite`, `l`.`pays`,\n"
. " `ld`.`date`, `ld`.`record_id`, `ld`.`coll_id`, `ld`.`final`"
. " FROM `log_docs` AS `ld` INNER JOIN `log` AS `l` ON `l`.`id`=`ld`.`log_id`\n"
. " WHERE {{GlobalFilter}}";
}
$this->keyName = 'id';
break;
case 'user':
$this->name = "Downloads by user";
$this->columnTitles = ['usrid', 'user', 'fonction', 'societe', 'activite', 'pays', 'min_date', 'max_date', 'nb'];
if($this->parms['anonymize']) {
$sql = "SELECT `l`.`usrid`, '-' AS `user`, '-' AS `fonction`, '-' AS `societe`, '-' AS `activite`, '-' AS `pays`,\n"
. " MIN(`ld`.`date`) AS `dmin`, MAX(`ld`.`date`) AS `dmax`, SUM(1) AS `nb`\n"
. " FROM `log_docs` AS `ld` INNER JOIN `log` AS `l` ON `l`.`id`=`ld`.`log_id`\n"
. " WHERE {{GlobalFilter}}\n"
. " GROUP BY `l`.`usrid`\n"
. " ORDER BY `nb` DESC";
}
else {
$sql = "SELECT `l`.`usrid`, `l`.`user`, `l`.`fonction`, `l`.`societe`, `l`.`activite`, `l`.`pays`,\n"
. " MIN(`ld`.`date`) AS `dmin`, MAX(`ld`.`date`) AS `dmax`, SUM(1) AS `nb`\n"
. " FROM `log_docs` AS `ld` INNER JOIN `log` AS `l` ON `l`.`id`=`ld`.`log_id`\n"
. " WHERE {{GlobalFilter}}\n"
. " GROUP BY `l`.`usrid`\n"
. " ORDER BY `nb` DESC";
}
$this->keyName = 'usrid';
break;
case 'record':
$this->name = "Downloads by record";
$this->columnTitles = ['record_id', 'min_date', 'max_date', 'nb'];
$sql = "SELECT `ld`.`record_id`,\n"
. " MIN(`ld`.`date`) AS `dmin`, MAX(`ld`.`date`) AS `dmax`, SUM(1) AS `nb`\n"
. " FROM `log_docs` AS `ld` INNER JOIN `log` AS `l` ON `l`.`id`=`ld`.`log_id`\n"
. " WHERE {{GlobalFilter}}\n"
. " GROUP BY `l`.`usrid`\n"
. " ORDER BY `nb` DESC"
;
$this->keyName = 'record_id';
break;
default:
throw new InvalidArgumentException('invalid "group" argument');
break;
}
// get acl-filtered coll_id(s) as already sql-quoted
$collIds = $this->getCollIds($this->acl, $this->parms['bases']);
if(!empty($collIds)) {
// filter subdefs by class
$subdefsToReport = ['document' => $this->databox->get_connection()->quote('document')];
foreach ($this->getDatabox()->get_subdef_structure() as $subGroup) {
foreach ($subGroup->getIterator() as $sub) {
if(in_array($sub->get_class(), ['document', 'preview'])) {
// keep only unique names
$subdefsToReport[$sub->get_name()] = $this->databox->get_connection()->quote($sub->get_name());
}
}
}
$subdefsToReport = join(',', $subdefsToReport);
$filter = "`action`='download' AND `ld`.`coll_id` IN(" . join(',', $collIds) . ")\n"
. " AND `l`.`usrid`>0\n"
. " AND `ld`.`final` IN(" . $subdefsToReport . ")";
// next line : comment to disable "site", to test on an imported dataset from another instance
$filter .= "\n AND `l`.`site` = " . $this->databox->get_connection()->quote($this->appKey);
if($this->parms['dmin']) {
$filter .= "\n AND `ld`.`date` >= " . $this->databox->get_connection()->quote($this->parms['dmin']);
}
if($this->parms['dmax']) {
$filter .= "\n AND `ld`.`date` <= " . $this->databox->get_connection()->quote($this->parms['dmax']);
}
}
else {
// no collections report ?
// keep the sql intact (to match placeholders/parameters), but enforce empty result
$filter = "FALSE";
}
$this->sql = str_replace('{{GlobalFilter}}', $filter, $sql);
// file_put_contents("/tmp/phraseanet-log.txt", sprintf("%s (%d) %s\n", __FILE__, __LINE__, $this->sql), FILE_APPEND);
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report;
use Alchemy\Phrasea\Application;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class ReportFactory
*
* published as service $app['report.factory']
*
* @package Alchemy\Phrasea\Report
*/
class ReportFactory
{
const CONNECTIONS = 'connections';
const DOWNLOADS = 'downloads';
const RECORDS = 'records';
protected $appKey;
protected $appbox;
protected $databox;
protected $acl;
/**
* @param string $appKey
* @param \appbox $appbox
* @param \ACL acl
*/
public function __construct($appKey, \appbox $appbox, \ACL $acl)
{
$this->appKey = $appKey;
$this->appbox = $appbox;
$this->acl = $acl;
}
/**
* @param $table
* @param null $sbasId
* @param null $parms
*
* @return ReportConnections | ReportDownloads
*/
public function createReport($table, $sbasId=null, $parms=null)
{
switch($table) {
case self::CONNECTIONS:
return (new ReportConnections(
$this->findDbOr404($sbasId),
$parms
))
->setAppKey($this->appKey)
;
break;
case self::DOWNLOADS:
return (new ReportDownloads(
$this->findDbOr404($sbasId),
$parms
))
->setAppKey($this->appKey)
->setACL($this->acl)
;
break;
case self::RECORDS:
return (new ReportRecords(
$this->findDbOr404($sbasId),
$parms
))
->setACL($this->acl)
;
break;
default:
throw new \InvalidArgumentException(sprintf("unknown table type \"%s\"", $table));
break;
}
}
/**
* @param int $sbasId
* @return \databox
*/
private function findDbOr404($sbasId)
{
$db = $this->appbox->get_databox(($sbasId));
if(!$db) {
throw new NotFoundHttpException(sprintf('Databox %s not found', $sbasId));
}
return $db;
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report;
use Alchemy\Phrasea\Application;
class ReportRecords extends Report
{
/** @var \ACL */
private $acl;
/* those vars will be set once by computeVars() */
private $name = null;
private $sqlWhere = null;
private $sqlColSelect = null;
private $columnTitles = null;
private $keyName = null;
public function getColumnTitles()
{
$this->computeVars();
return $this->columnTitles;
}
public function getKeyName()
{
$this->computeVars();
return $this->keyName;
}
public function getName()
{
$this->computeVars();
return $this->name;
}
public function setACL($acl)
{
$this->acl = $acl;
return $this;
}
public function getAllRows($callback)
{
$this->computeVars();
$lastRid = 0;
while(true) {
$sql = "SELECT MIN(record_id) AS `from`, MAX(record_id) AS `to` FROM (\n"
. "SELECT record_id FROM record AS `r`\n"
. "WHERE " . $this->sqlWhere . " AND record_id>" . $lastRid . " LIMIT 5000) AS _t";
$stmt = $this->databox->get_connection()->executeQuery($sql, []);
$row = $stmt->fetch();
$stmt->closeCursor();
if($row && !is_null($row['from']) && !is_null($row['to'])) {
$sql = "SELECT r.record_id, c.asciiname, r.moddate, r.mime, r.type, r.originalname,\n"
. $this->sqlColSelect . "\n"
. "FROM (`record` AS `r` LEFT JOIN `coll` AS `c` USING(`coll_id`)) LEFT JOIN `metadatas` AS `m` USING(`record_id`)\n"
. "WHERE " . $this->sqlWhere . "\n"
. " AND r.record_id >= " . $row['from'] . " AND r.record_id <= " . $row['to'] . "\n"
. "GROUP BY `record_id`\n";
// file_put_contents("/tmp/phraseanet-log.txt", sprintf("%s (%d) %s\n", __FILE__, __LINE__, var_export($sql, true)), FILE_APPEND);
$stmt = $this->databox->get_connection()->executeQuery($sql, []);
$rows = $stmt->fetchAll();
$stmt->closeCursor();
foreach($rows as $row) {
$callback($row);
$lastRid = $row['record_id'];
}
}
else {
break;
}
}
}
private function computeVars()
{
if(!is_null($this->name)) {
// vars already computed
return;
}
// pivot-like query on metadata fields
$this->sqlColSelect = [];
$this->columnTitles = ['record_id', 'collection', 'moddate', 'mime', 'type', 'originalname'];
foreach($this->getDatabox()->get_meta_structure() as $field) {
// skip the fields that can't be reported
if(!$field->is_report() || ($field->isBusiness() && !$this->acl->can_see_business_fields($this->getDatabox()))) {
continue;
}
// if a list of meta was provided, just keep those
if(is_array($this->parms['meta']) && !in_array($field->get_name(), $this->parms['meta'])) {
continue;
}
// column names is not important in the result, simply match the 'title' position
$this->columnTitles[] = $field->get_name();
$this->sqlColSelect[] = sprintf("GROUP_CONCAT(IF(`m`.`meta_struct_id`=%s, `m`.`value`, NULL)) AS `f%s`", $field->get_id(), $field->get_id());
}
$this->sqlColSelect = join(",\n", $this->sqlColSelect);
// get acl-filtered coll_id(s) as already sql-quoted
$collIds = $this->getCollIds($this->acl, $this->parms['base']);
if(!empty($collIds)) {
$this->sqlWhere = "`r`.`parent_record_id`=0 AND `r`.`coll_id` IN(" . join(',', $collIds) . ")";
if(!is_null($this->parms['dmin'])) {
$this->sqlWhere .= " AND r.moddate >= " . $this->databox->get_connection()->quote($this->parms['dmin']);
}
if(!is_null($this->parms['dmax'])) {
$this->sqlWhere .= " AND r.moddate <= " . $this->databox->get_connection()->quote($this->parms['dmax']);
}
}
else {
$this->sqlWhere = "FALSE";
}
$this->name = "Databox";
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Report;
use Alchemy\Phrasea\Application;
class ReportService
{
protected $appKey;
protected $appbox;
protected $acl;
/**
* @param string $appKey
* @param \appbox $appbox
* @param \ACL acl
*/
public function __construct($appKey, \appbox $appbox, \ACL $acl)
{
$this->appKey = $appKey;
$this->appbox = $appbox;
$this->acl = $acl;
}
/**
* return bases allowed for reporting, grouped by databox
* @return array
*/
public function getGranted()
{
$databoxes = [];
/** @var \collection $collection */
foreach ($this->acl->get_granted_base([\ACL::CANREPORT]) as $collection) {
$sbas_id = $collection->get_sbas_id();
if (!isset($databoxes[$sbas_id])) {
$databoxes[$sbas_id] = [
'id' => $sbas_id,
'name' => $collection->get_databox()->get_viewname(),
'collections' => []
];
}
$databoxes[$sbas_id]['collections'][$collection->get_base_id()] = [
'id' => $collection->get_base_id(),
'coll_id' => $collection->get_coll_id(),
'name' => $collection->get_name()
];
}
return ['databoxes' => $databoxes];
}
}

2939
yarn.lock

File diff suppressed because it is too large Load Diff