From ce14c5845f9efba9c6033e1c8e7588f5aa355ee8 Mon Sep 17 00:00:00 2001 From: Fede Isas Date: Sun, 8 May 2016 19:26:00 -0300 Subject: [PATCH 01/19] Add variable brand to config --- config/config.php.sample | 3 +++ templates/default/html/page.twig | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config/config.php.sample b/config/config.php.sample index 02c93d68..c36616e7 100644 --- a/config/config.php.sample +++ b/config/config.php.sample @@ -7,6 +7,9 @@ return array( //the root directory of all dashboards 'dashboardDir' => 'sample_dashboards', + // Company name on navigation + 'brand' => 'PHP Reports', + //the directory where things will be cached //this is relative to the project root by default, but can be set to an absolute path too //the cache has some relatively long lived data so don't use /tmp if you can avoid it diff --git a/templates/default/html/page.twig b/templates/default/html/page.twig index aba86a21..ae8c5617 100644 --- a/templates/default/html/page.twig +++ b/templates/default/html/page.twig @@ -53,7 +53,7 @@ --> - + "; + } + + return $ret; + } } -?> From ade3fbec422068e5acf417236f742a3418384b52 Mon Sep 17 00:00:00 2001 From: Fede Isas Date: Sun, 8 May 2016 20:51:25 -0300 Subject: [PATCH 07/19] Code styling, new dependencies, starting revamp --- .editorconfig | 16 + .php_cs | 77 + .travis.yml | 29 + classes/filters/barFilter.php | 39 +- classes/filters/classFilter.php | 15 +- classes/filters/dateFilter.php | 55 +- classes/filters/drilldownFilter.php | 174 +- classes/filters/geoipFilter.php | 50 +- classes/filters/hideFilter.php | 10 +- classes/filters/htmlFilter.php | 13 +- classes/filters/imgsizeFilter.php | 34 +- classes/filters/linkFilter.php | 30 +- classes/filters/numberFilter.php | 6 +- classes/filters/paddingFilter.php | 24 +- classes/filters/preFilter.php | 14 +- classes/filters/twigFilter.php | 34 +- classes/headers/ChartHeader.php | 839 +++++----- classes/headers/ColumnsHeader.php | 186 ++- classes/headers/FilterHeader.php | 94 +- classes/headers/FormattingHeader.php | 362 ++-- classes/headers/IncludeHeader.php | 93 +- classes/headers/InfoHeader.php | 109 +- classes/headers/OptionsHeader.php | 285 ++-- classes/headers/RollupHeader.php | 264 +-- classes/headers/VariableHeader.php | 444 ++--- classes/headers/deprecated/CacheHeader.php | 45 +- classes/headers/deprecated/CautionHeader.php | 45 +- classes/headers/deprecated/ColumnHeader.php | 14 +- classes/headers/deprecated/CreatedHeader.php | 27 +- classes/headers/deprecated/DatabaseHeader.php | 27 +- .../headers/deprecated/DescriptionHeader.php | 27 +- classes/headers/deprecated/DetailHeader.php | 141 +- .../deprecated/MongodatabaseHeader.php | 27 +- classes/headers/deprecated/NameHeader.php | 27 +- classes/headers/deprecated/NoteHeader.php | 27 +- classes/headers/deprecated/OptionHeader.php | 14 +- classes/headers/deprecated/PlotHeader.php | 14 +- classes/headers/deprecated/StatusHeader.php | 27 +- classes/headers/deprecated/TotalHeader.php | 10 +- classes/headers/deprecated/TotalsHeader.php | 37 +- classes/headers/deprecated/TypeHeader.php | 27 +- classes/headers/deprecated/ValueHeader.php | 84 +- classes/report_formats/ChartReportFormat.php | 26 +- classes/report_formats/CsvReportFormat.php | 53 +- classes/report_formats/DebugReportFormat.php | 42 +- classes/report_formats/HtmlReportFormat.php | 77 +- classes/report_formats/JsonReportFormat.php | 124 +- classes/report_formats/RawReportFormat.php | 33 +- classes/report_formats/SqlReportFormat.php | 18 +- classes/report_formats/TableReportFormat.php | 27 +- classes/report_formats/TextReportFormat.php | 187 ++- classes/report_formats/XlsReportBase.php | 144 +- classes/report_formats/XlsReportFormat.php | 42 +- classes/report_formats/XlsxReportFormat.php | 42 +- classes/report_formats/XmlReportFormat.php | 70 +- classes/report_types/AdoPivotReportType.php | 201 +-- classes/report_types/AdoReportType.php | 301 ++-- classes/report_types/MongoReportType.php | 150 +- classes/report_types/MysqlReportType.php | 6 +- classes/report_types/PdoReportType.php | 426 ++--- classes/report_types/PhpReportType.php | 181 +- composer.json | 5 +- composer.lock | 1156 ++++++++++++- lib/PhpReports/FilterBase.php | 24 +- lib/PhpReports/HeaderBase.php | 215 +-- lib/PhpReports/PhpReports.php | 1453 +++++++++-------- lib/PhpReports/Report.php | 1339 ++++++++------- lib/PhpReports/ReportFormatBase.php | 27 +- lib/PhpReports/ReportTypeBase.php | 41 +- lib/PhpReports/ReportValue.php | 203 +-- lib/adodb/pivottable.inc.php | 151 +- lib/simplediff/SimpleDiff.php | 14 +- phpunit.xml | 20 + 73 files changed, 6288 insertions(+), 4426 deletions(-) create mode 100644 .editorconfig create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 phpunit.xml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8791d9f2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.php_cs b/.php_cs new file mode 100644 index 00000000..6933329d --- /dev/null +++ b/.php_cs @@ -0,0 +1,77 @@ +finder(DefaultFinder::create()->in(__DIR__)) + ->fixers($fixers) + ->level(FixerInterface::NONE_LEVEL) + ->setUsingCache(true); \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..40ceda54 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: php + +php: + - 5.5.9 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +env: + global: + - setup=basic + +matrix: + include: + - php: 5.5.9 + env: setup=lowest + - php: 5.5.9 + env: setup=stable + +sudo: false + +before_install: + - travis_retry composer self-update + +install: + - composer install --no-interaction --prefer-dist + +script: vendor/bin/phpunit \ No newline at end of file diff --git a/classes/filters/barFilter.php b/classes/filters/barFilter.php index 58bbabf9..ee6483a6 100644 --- a/classes/filters/barFilter.php +++ b/classes/filters/barFilter.php @@ -1,13 +1,30 @@ getValue()/max($report->options['Values'][$value->key]))); - - $value->setValue("
"."".$value->getValue(true)."",true); - - return $value; - } + +class barFilter extends FilterBase +{ + public static function filter($value, $options = [], &$report, &$row) + { + if (isset($options['width'])) { + $max = $options['width']; + } else { + $max = 200; + } + + $width = round($max * ($value->getValue() / max($report->options['Values'][$value->key]))); + + $value->setValue( + join('', [ + "
", + "", + $value->getValue(true), + "", + ]), + true + ); + + return $value; + } } diff --git a/classes/filters/classFilter.php b/classes/filters/classFilter.php index 1e5029f3..1c89af4d 100644 --- a/classes/filters/classFilter.php +++ b/classes/filters/classFilter.php @@ -1,8 +1,11 @@ addClass($options['class']); - - return $value; - } + +class classFilter extends FilterBase +{ + public static function filter($value, $options = [], &$report, &$row) + { + $value->addClass($options['class']); + + return $value; + } } diff --git a/classes/filters/dateFilter.php b/classes/filters/dateFilter.php index 67db8b75..f6450e07 100644 --- a/classes/filters/dateFilter.php +++ b/classes/filters/dateFilter.php @@ -1,26 +1,33 @@ options['Database']; - - $time = strtotime($value->getValue()); - - //if the time couldn't be parsed, just return the original value - if(!$time) { - return $value; - } - - //if a timezone correction is needed for the database being selected from - $environment = $report->getEnvironment(); - if(isset($environment[$options['database']]['time_offset'])) { - $time_offset = -1*$environment[$options['database']]['time_offset']; - - $time = strtotime((($time_offset > 0)? '+' : '-').abs($time_offset).' hours',$time); - } - - $value->setValue(date($options['format'],$time)); - - return $value; - } + +class dateFilter extends FilterBase +{ + public static function filter($value, $options = [], &$report, &$row) + { + if (!isset($options['format'])) { + $options['format'] = (isset(PhpReports::$config['default_date_format']) ? PhpReports::$config['default_date_format'] : 'Y-m-d H:i:s'); + } + if (!isset($options['database'])) { + $options['database'] = $report->options['Database']; + } + + $time = strtotime($value->getValue()); + + //if the time couldn't be parsed, just return the original value + if (!$time) { + return $value; + } + + //if a timezone correction is needed for the database being selected from + $environment = $report->getEnvironment(); + if (isset($environment[$options['database']]['time_offset'])) { + $time_offset = -1*$environment[$options['database']]['time_offset']; + + $time = strtotime((($time_offset > 0) ? '+' : '-').abs($time_offset).' hours', $time); + } + + $value->setValue(date($options['format'], $time)); + + return $value; + } } diff --git a/classes/filters/drilldownFilter.php b/classes/filters/drilldownFilter.php index 9f2918a9..477f6f8c 100644 --- a/classes/filters/drilldownFilter.php +++ b/classes/filters/drilldownFilter.php @@ -1,89 +1,91 @@ report); - array_pop($temp); - $try[] = implode('/',$temp).'/'.$options['report']; - $try[] = $options['report']; - } - - //see if the file exists directly - $found = false; - $path = ''; - foreach($try as $report_name) { - if(file_exists(PhpReports::$config['reportDir'].'/'.$report_name)) { - $path = $report_name; - $found = true; - break; - } - } - - //see if the report is missing a file extension - if(!$found) { - foreach($try as $report_name) { - $possible_reports = glob(PhpReports::$config['reportDir'].'/'.$report_name.'.*'); - - if($possible_reports) { - $path = substr($possible_reports[0],strlen(PhpReports::$config['reportDir'].'/')); - $found = true; - break; - } - } - } - - if(!$found) { - return $value; - } - - $url = PhpReports::$request->base.'/report/html/?report='.$path; +class drilldownFilter extends linkFilter +{ + public static function filter($value, $options = [], &$report, &$row) + { + if (!isset($options['macros'])) { + $options['macros'] = []; + } - $macros = array(); - foreach($options['macros'] as $k=>$v) { - //if the macro needs to be replaced with the value of another column - if(isset($v['column'])) { - if(isset($row[$v['column']])) { - $v = $row[$v['column']]; - } - else $v = ""; - } - //if the macro is just a constant - elseif(isset($v['constant'])) { - $v = $v['constant']; - } - - $macros[$k] = $v; - } - - $macros = array_merge($report->macros,$macros); - unset($macros['host']); - - foreach($macros as $k=>$v) { - if(is_array($v)) { - foreach($v as $v2) { - $url .= '¯os['.$k.'][]='.$v2; - } - } - else { - $url.='¯os['.$k.']='.$v; - } - } - - $options = array( - 'url'=>$url - ); - - return parent::filter($value, $options, $report, $row); - } + //determine report + //list of reports to try + $try = []; + + //relative to reportDir + if ($options['report']{0} === '/') { + $try[] = substr($options['report'], 1); + } else { + //relative to parent report + $temp = explode('/', $report->report); + array_pop($temp); + $try[] = implode('/', $temp).'/'.$options['report']; + $try[] = $options['report']; + } + + //see if the file exists directly + $found = false; + $path = ''; + foreach ($try as $report_name) { + if (file_exists(PhpReports::$config['reportDir'].'/'.$report_name)) { + $path = $report_name; + $found = true; + break; + } + } + + //see if the report is missing a file extension + if (!$found) { + foreach ($try as $report_name) { + $possible_reports = glob(PhpReports::$config['reportDir'].'/'.$report_name.'.*'); + + if ($possible_reports) { + $path = substr($possible_reports[0], strlen(PhpReports::$config['reportDir'].'/')); + $found = true; + break; + } + } + } + + if (!$found) { + return $value; + } + + $url = PhpReports::$request->base.'/report/html/?report='.$path; + + $macros = []; + foreach ($options['macros'] as $k => $v) { + //if the macro needs to be replaced with the value of another column + if (isset($v['column'])) { + if (isset($row[$v['column']])) { + $v = $row[$v['column']]; + } else { + $v = ""; + } + } elseif (isset($v['constant'])) { + //if the macro is just a constant + $v = $v['constant']; + } + + $macros[$k] = $v; + } + + $macros = array_merge($report->macros, $macros); + unset($macros['host']); + + foreach ($macros as $k => $v) { + if (is_array($v)) { + foreach ($v as $v2) { + $url .= '¯os['.$k.'][]='.$v2; + } + } else { + $url .= '¯os['.$k.']='.$v; + } + } + + $options = array( + 'url' => $url, + ); + + return parent::filter($value, $options, $report, $row); + } } diff --git a/classes/filters/geoipFilter.php b/classes/filters/geoipFilter.php index eae50187..a92e50f9 100644 --- a/classes/filters/geoipFilter.php +++ b/classes/filters/geoipFilter.php @@ -1,27 +1,27 @@ getValue()); - - if($record) { - $display = ''; - - $display = $record['city']; - if($record['country_code'] !== 'US') { - $display .= ' '.$record['country_name']; - } - else { - $display .= ', '.$record['region']; - } - - $value->setValue($display); - - $value->chart_value = array('Latitude'=>$record['latitude'],'Longitude'=>$record['longitude'],'Location'=>$display); - } - else { - $value->chart_value = array('Latitude'=>0, 'Longitude'=>0, 'Location'=>'Unknown'); - } - - return $value; - } +class geoipFilter extends FilterBase +{ + public static function filter($value, $options = array(), &$report, &$row) + { + $record = geoip_record_by_name($value->getValue()); + + if ($record) { + $display = ''; + + $display = $record['city']; + if ($record['country_code'] !== 'US') { + $display .= ' '.$record['country_name']; + } else { + $display .= ', '.$record['region']; + } + + $value->setValue($display); + + $value->chart_value = array('Latitude' => $record['latitude'],'Longitude' => $record['longitude'],'Location' => $display); + } else { + $value->chart_value = array('Latitude' => 0, 'Longitude' => 0, 'Location' => 'Unknown'); + } + + return $value; + } } diff --git a/classes/filters/hideFilter.php b/classes/filters/hideFilter.php index 2c54b80f..cc5da5c7 100644 --- a/classes/filters/hideFilter.php +++ b/classes/filters/hideFilter.php @@ -1,6 +1,8 @@ is_html = true; - return $value; - } +class htmlFilter extends FilterBase +{ + public static function filter($value, $options = array(), &$report, &$row) + { + $value->is_html = true; + + return $value; + } } diff --git a/classes/filters/imgsizeFilter.php b/classes/filters/imgsizeFilter.php index abe094e1..daccc499 100644 --- a/classes/filters/imgsizeFilter.php +++ b/classes/filters/imgsizeFilter.php @@ -1,17 +1,21 @@ getValue(), 'rb'); - $img = new Imagick(); - $img->readImageFile($handle); - $data = $img->identifyImage(); - - if(!isset($options['format'])) $options['format'] = self::$default_format; - - $value->setValue(PhpReports::renderString($options['format'], $data)); - - return $value; - } +class imgsizeFilter extends FilterBase +{ + public static $default_format = '{{ geometry.width }}x{{ geometry.height }} {{ compression }}, {{ fileSize }}'; + + public static function filter($value, $options = array(), &$report, &$row) + { + $handle = fopen($value->getValue(), 'rb'); + $img = new Imagick(); + $img->readImageFile($handle); + $data = $img->identifyImage(); + + if (!isset($options['format'])) { + $options['format'] = self::$default_format; + } + + $value->setValue(PhpReports::renderString($options['format'], $data)); + + return $value; + } } diff --git a/classes/filters/linkFilter.php b/classes/filters/linkFilter.php index da0ce690..b248bdeb 100644 --- a/classes/filters/linkFilter.php +++ b/classes/filters/linkFilter.php @@ -1,16 +1,20 @@ getValue()) return $value; - - $url = isset($options['url'])? $options['url'] : $value->getValue(); - $attr = (isset($options['blank']) && $options['blank'])? ' target="_blank"' : ''; - $display = isset($options['display'])? $options['display'] : $value->getValue(); +class linkFilter extends FilterBase +{ + public static function filter($value, $options = array(), &$report, &$row) + { + if (!$value->getValue()) { + return $value; + } - $html = ''.$display.''; - - $value->setValue($html, true); - - return $value; - } + $url = isset($options['url']) ? $options['url'] : $value->getValue(); + $attr = (isset($options['blank']) && $options['blank']) ? ' target="_blank"' : ''; + $display = isset($options['display']) ? $options['display'] : $value->getValue(); + + $html = ''.$display.''; + + $value->setValue($html, true); + + return $value; + } } diff --git a/classes/filters/numberFilter.php b/classes/filters/numberFilter.php index ec3e4e18..5cab76a8 100644 --- a/classes/filters/numberFilter.php +++ b/classes/filters/numberFilter.php @@ -6,8 +6,10 @@ * Time: 15:37 * To change this template use File | Settings | File Templates. */ -class numberFilter extends FilterBase { - public static function filter($value, $options = array(), &$report, &$row) { +class numberFilter extends FilterBase +{ + public static function filter($value, $options = array(), &$report, &$row) + { $decimals = $options['decimals'] ? $options['decimals'] : 0; $dec_sepr = $options['decimal_sep'] ? $options['decimal_sep'] : ','; $thousand = $options['thousands_sep'] ? $options['thousands_sep'] : ' '; diff --git a/classes/filters/paddingFilter.php b/classes/filters/paddingFilter.php index 8c5a3225..7cdd17e3 100644 --- a/classes/filters/paddingFilter.php +++ b/classes/filters/paddingFilter.php @@ -1,14 +1,14 @@ addClass('right'); - } - elseif($options['direction'] === 'l') { - $value->addClass('left'); - } - - return $value; - } +class paddingFilter extends FilterBase +{ + public static function filter($value, $options = array(), &$report, &$row) + { + if ($options['direction'] === 'r') { + $value->addClass('right'); + } elseif ($options['direction'] === 'l') { + $value->addClass('left'); + } + + return $value; + } } diff --git a/classes/filters/preFilter.php b/classes/filters/preFilter.php index f82e2981..04f33744 100644 --- a/classes/filters/preFilter.php +++ b/classes/filters/preFilter.php @@ -1,8 +1,10 @@ setValue("
".$value->getValue(true)."
",true); - - return $value; - } +class preFilter extends FilterBase +{ + public static function filter($value, $options = array(), &$report, &$row) + { + $value->setValue("
".$value->getValue(true)."
", true); + + return $value; + } } diff --git a/classes/filters/twigFilter.php b/classes/filters/twigFilter.php index e6fe44d0..bbb2ce54 100644 --- a/classes/filters/twigFilter.php +++ b/classes/filters/twigFilter.php @@ -1,18 +1,20 @@ getValue(); - - $result = PhpReports::renderString($template,array( - "value"=>$value->getValue(), - "row"=>$row - )); - - $value->setValue($result, $html); - - return $value; - } +class twigFilter extends FilterBase +{ + public static function filter($value, $options = array(), &$report, &$row) + { + // If this is html + $html = isset($options['html']) ? $options['html'] : false; + + $template = isset($options['template']) ? $options['template'] : $value->getValue(); + + $result = PhpReports::renderString($template, array( + "value" => $value->getValue(), + "row" => $row, + )); + + $value->setValue($result, $html); + + return $value; + } } diff --git a/classes/headers/ChartHeader.php b/classes/headers/ChartHeader.php index e9569725..f7a8c8c7 100644 --- a/classes/headers/ChartHeader.php +++ b/classes/headers/ChartHeader.php @@ -1,404 +1,439 @@ array( - 'type'=>'array', - 'default'=>array() - ), - 'dataset'=>array( - 'default'=>0 - ), - 'type'=>array( - 'type'=>'enum', - 'values'=>array( - 'LineChart', - 'GeoChart', - 'AnnotatedTimeLine', - 'BarChart', - 'ColumnChart', - 'Timeline', - 'AreaChart', - 'Histogram', - 'ComboChart', - 'BubbleChart', - 'CandlestickChart', - 'Gauge', - 'Map', - 'PieChart', - 'Sankey', - 'ScatterChart', - 'SteppedAreaChart', - 'WordTree', - ), - 'default'=>'LineChart' - ), - 'title'=>array( - 'type'=>'string', - 'default'=>'' - ), - 'width'=>array( - 'type'=>'string', - 'default'=>'100%' - ), - 'height'=>array( - 'type'=>'string', - 'default'=>'400px' - ), - 'xhistogram'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'buckets'=>array( - 'type'=>'number', - 'default'=>0 - ), - 'omit-totals'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'omit-total'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'rotate-x-labels'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'grid'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'timefmt'=>array( - 'type'=>'string', - 'default'=>'' - ), - 'xformat'=>array( - 'type'=>'string', - 'default'=>'' - ), - 'yrange'=>array( - 'type'=>'string', - 'default'=>'' - ), - 'all'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'colors'=>array( - 'type'=>'array', - 'default'=>array() - ), - 'roles'=>array( - 'type'=>'object', - 'default'=>array() - ), - 'markers'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'omit-columns'=>array( - 'type'=>'array', - 'default'=>array() - ), - 'options'=>array( - 'type'=>'object', - 'default'=>array() - ) - ); - - public static function init($params, &$report) { - $report->exportHeader('Chart',$params); - - if(!isset($params['type'])) { - $params['type'] = 'LineChart'; - } - - if(isset($params['omit-total'])) { - $params['omit-totals'] = $params['omit-total']; - unset($params['omit-total']); - } - - if(!isset($report->options['Charts'])) $report->options['Charts'] = array(); - - if(isset($params['width'])) $params['width'] = self::fixDimension($params['width']); - if(isset($params['height'])) $params['height'] = self::fixDimension($params['height']); - - $params['num'] = count($report->options['Charts'])+1; - $params['Rows'] = array(); - - $report->options['Charts'][] = $params; - - $report->options['has_charts'] = true; - - } - protected static function fixDimension($dim) { - if(preg_match('/^[0-9]+$/',$dim)) $dim .= "px"; - return $dim; - } - - public static function parseShortcut($value) { - $params = explode(',',$value); - $value = array(); - foreach($params as $param) { - $param = trim($param); - if(strpos($param,'=') !== false) { - list($key,$val) = explode('=',$param,2); - $key = trim($key); - $val = trim($val); - - //some parameters can have multiple values separated by ":" - if(in_array($key,array('x','y','colors'),true)) { - $val = explode(':',$val); - } - } - else { - $key = $param; - $val = true; - } - - $value[$key] = $val; - } - - if(isset($value['x'])) $value['columns'] = $value['x']; - else $value['columns'] = array(1); - - if(isset($value['y'])) $value['columns'] = array_merge($value['columns'],$value['y']); - else $value['all'] = true; - - unset($value['x']); - unset($value['y']); - - return $value; - } - - protected static function getRowInfo(&$rows, $params, $num, &$report) { - $cols = array(); - - //expand columns - $chart_rows = array(); - foreach($rows as $k=>$row) { - $vals = array(); - - if($k===0) { - $i=1; - $unsorted = 1000; - foreach($row['values'] as $key=>$value) { - if (($temp = array_search($row['values'][$key]->i, $report->options['Charts'][$num]['columns']))!==false) { - $cols[$temp] = $key; - } elseif (($temp = array_search($row['values'][$key]->key, $report->options['Charts'][$num]['columns']))!==false) { - $cols[$temp] = $key; - } - //if all columns are included, add after any specifically defined ones - elseif($report->options['Charts'][$num]['all']) { - $cols[$unsorted] = $key; - $unsorted ++; - } - } - - ksort($cols); - } - - foreach($cols as $key) { - if(isset($row['values'][$key]->chart_value) && is_array($row['values'][$key]->chart_value)) { - foreach($row['values'][$key]->chart_value as $ckey=>$cval) { - $temp = new ReportValue($row['values'][$key]->i, $ckey, trim($cval,'%$ ')); - $temp->setValue($cval); - $vals[] = $temp; - } - } else { - $temp = new ReportValue($row['values'][$key]->i, $row['values'][$key]->key, $row['values'][$key]->original_value); - $temp->setValue(trim($row['values'][$key]->getValue(),'%$ ')); - $vals[] = $temp; - } - } - - $chart_rows[] = $vals; - } - - //determine column types - $types = array(); - foreach($chart_rows as $i=>$row) { - foreach($row as $k=>$v) { - $type = self::determineDataType($v->original_value); - //if the value is null, it doesn't influence the column type - if(!$type) { - $chart_rows[$i][$k]->setValue(null); - continue; - } - //if we don't know the column type yet, set it to this row's value - elseif(!isset($types[$k])) $types[$k] = $type; - //if any row has a string value for the column, the whole column is a string type - elseif($type === 'string') $types[$k] = 'string'; - //if the column is currently a date and this row is a time/datetime, set the column to datetime type - elseif($types[$k] === 'date' && in_array($type,array('timeofday','datetime'))) $types[$k] = 'datetime'; - //if the column is currently a time and this row is a date/datetime, set the column to datetime type - elseif($types[$k] === 'timeofday' && in_array($type,array('date','datetime'))) $types[$k] = 'datetime'; - //if the column is currently a date and this row is a number set the column type to number - elseif($types[$k] === 'date' && $type === 'number') $types[$k] = 'number'; - } - } - - $report->options['Charts'][$num]['datatypes'] = $types; - - //build chart rows - $report->options['Charts'][$num]['Rows'] = array(); - - foreach($chart_rows as $i=>&$row) { - $vals = array(); - foreach($row as $key=>$val) { - if(is_null($val->getValue())) { - $val->datatype = 'null'; - } - elseif($types[$key] === 'datetime') { - $val->setValue(date('m/d/Y H:i:s',strtotime($val->getValue()))); - $val->datatype = 'datetime'; - } - elseif($types[$key] === 'timeofday') { - $val->setValue(date('H:i:s',strtotime($val->getValue()))); - $val->datatype = 'timeofday'; - } - elseif($types[$key] === 'date') { - $val->setValue(date('m/d/Y',strtotime($val->getValue()))); - $val->datatype = 'date'; - } - elseif($types[$key] === 'number') { - $val->setValue(round(floatval(preg_replace('/[^-0-9\.]*/','',$val->getValue())),6)); - $val->datatype = 'number'; - } - else { - $val->datatype = 'string'; - } - - $vals[] = $val; - } - - $report->options['Charts'][$num]['Rows'][] = array( - 'values'=>$vals, - 'first'=>!$report->options['Charts'][$num]['Rows'] - ); - } - } - - protected static function generateHistogramRows($rows, $column, $num_buckets) { - $column_key = null; - - //if a name is given as the column, determine the column index - if(!is_numeric($column)) { - foreach($rows[0]['values'] as $k=>$v) { - if($v->key == $column) { - $column = $k; - $column_key = $v->key; - break; - } - } - } - //if an index is given, convert to 0-based - else { - $column --; - $column_key = $rows[0]['values'][$column]->key; - } - - //get a list of values for the histogram - $vals = array(); - foreach($rows as &$row) { - $vals[] = floatval(preg_replace('/[^0-9.]*/','',$row['values'][$column]->getValue())); - } - sort($vals); - - //determine buckets - $count = count($vals); - $buckets = array(); - $min = $vals[0]; - $max = $vals[$count-1]; - $step = ($max-$min)/$num_buckets; - $old_limit = $min; - - for($i=1;$i<$num_buckets+1;$i++) { - $limit = $old_limit + $step; - - $buckets[round($old_limit,2)." - ".round($limit,2)] = count(array_filter($vals,function($val) use($old_limit,$limit) { - return $val >= $old_limit && $val < $limit; - })); - $old_limit = $limit; - } - - //build chart rows - $chart_rows = array(); - foreach($buckets as $name=>$count) { - $chart_rows[] = array( - 'values'=>array( - new ReportValue(1,$name,$name), - new ReportValue(2,'value',$count) - ), - 'first'=>!$chart_rows - ); - } - return $chart_rows; - } - - protected static function determineDataType($value) { - if(is_null($value)) return null; - elseif($value === '') return null; - elseif(preg_match('/^([$%(\-+\s])*([0-9,]+(\.[0-9]+)?|\.[0-9]+)([$%(\-+\s])*$/',$value)) return 'number'; - elseif(preg_match('/^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$/',$value)) return 'timeofday'; - elseif(preg_match('/^[0-9]+(\/|-)[0-9]+/',$value) && strtotime($value)) { - if(date('H:i:s',strtotime($value))==='00:00:00') return 'date'; - else return 'datetime'; - } - else return 'string'; - } - - public static function beforeRender(&$report) { - // Expand out multiple datasets into their own charts - $new_charts = array(); - foreach($report->options['Charts'] as $num=>$params) { - $copy = $params; - - // If chart is for multiple datasets - if(is_array($params['dataset'])) { - foreach($params['dataset'] as $dataset) { - $copy['dataset'] = $dataset; - $copy['num'] = count($new_charts)+1; - $new_charts[] = $copy; - } - } - // If chart is for all datasets - elseif($params['dataset']===true) { - foreach($report->options['DataSets'] as $j=>$dataset) { - $copy['dataset'] = $j; - $copy['num'] = count($new_charts)+1; - $new_charts[] = $copy; - } - } - // If chart is for one dataset - else { - $copy['num'] = count($new_charts)+1; - $new_charts[] = $copy; - } - } - - $report->options['Charts'] = $new_charts; - - foreach($report->options['Charts'] as $num=>&$params) { - self::_processChart($num,$params,$params['dataset'],$report); - } - } - protected static function _processChart($num, &$params, $dataset, &$report) { - if(isset($params['xhistogram']) && $params['xhistogram']) { - $rows = self::generateHistogramRows($report->options['DataSets'][$dataset]['rows'],$params['columns'][0],$params['buckets']); - $params['columns'] = array(1,2); - } - else { - $rows = array(); - if(isset($report->options['DataSets'])) { - $rows = $report->options['DataSets'][$dataset]['rows']; - } - - if(count($rows)) { - if(!$params['columns']) $params['columns'] = range(1,count($rows[0]['values'])); - } - } - - self::getRowInfo($rows, $params, $num, $report); - } + +class ChartHeader extends HeaderBase +{ + public static $validation = [ + 'columns' => [ + 'type' => 'array', + 'default' => [], + ], + 'dataset' => [ + 'default' => 0, + ], + 'type' => [ + 'type' => 'enum', + 'values' => [ + 'LineChart', + 'GeoChart', + 'AnnotatedTimeLine', + 'BarChart', + 'ColumnChart', + 'Timeline', + 'AreaChart', + 'Histogram', + 'ComboChart', + 'BubbleChart', + 'CandlestickChart', + 'Gauge', + 'Map', + 'PieChart', + 'Sankey', + 'ScatterChart', + 'SteppedAreaChart', + 'WordTree', + ], + 'default' => 'LineChart', + ], + 'title' => [ + 'type' => 'string', + 'default' => '', + ], + 'width' => [ + 'type' => 'string', + 'default' => '100%', + ], + 'height' => [ + 'type' => 'string', + 'default' => '400px', + ], + 'xhistogram' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'buckets' => [ + 'type' => 'number', + 'default' => 0, + ], + 'omit-totals' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'omit-total' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'rotate-x-labels' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'grid' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'timefmt' => [ + 'type' => 'string', + 'default' => '', + ], + 'xformat' => [ + 'type' => 'string', + 'default' => '', + ], + 'yrange' => [ + 'type' => 'string', + 'default' => '', + ], + 'all' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'colors' => [ + 'type' => 'array', + 'default' => [], + ], + 'roles' => [ + 'type' => 'object', + 'default' => [], + ], + 'markers' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'omit-columns' => [ + 'type' => 'array', + 'default' => [], + ], + 'options' => [ + 'type' => 'object', + 'default' => [], + ], + ]; + + public static function init($params, &$report) + { + $report->exportHeader('Chart', $params); + + if (!isset($params['type'])) { + $params['type'] = 'LineChart'; + } + + if (isset($params['omit-total'])) { + $params['omit-totals'] = $params['omit-total']; + unset($params['omit-total']); + } + + if (!isset($report->options['Charts'])) { + $report->options['Charts'] = []; + } + + if (isset($params['width'])) { + $params['width'] = self::fixDimension($params['width']); + } + if (isset($params['height'])) { + $params['height'] = self::fixDimension($params['height']); + } + + $params['num'] = count($report->options['Charts'])+1; + $params['Rows'] = []; + + $report->options['Charts'][] = $params; + + $report->options['has_charts'] = true; + } + protected static function fixDimension($dim) + { + if (preg_match('/^[0-9]+$/', $dim)) { + $dim .= "px"; + } + + return $dim; + } + + public static function parseShortcut($value) + { + $params = explode(',', $value); + $value = []; + foreach ($params as $param) { + $param = trim($param); + if (strpos($param, '=') !== false) { + list($key, $val) = explode('=', $param, 2); + $key = trim($key); + $val = trim($val); + + //some parameters can have multiple values separated by ":" + if (in_array($key, array('x', 'y', 'colors'), true)) { + $val = explode(':', $val); + } + } else { + $key = $param; + $val = true; + } + + $value[$key] = $val; + } + + if (isset($value['x'])) { + $value['columns'] = $value['x']; + } else { + $value['columns'] = array(1); + } + + if (isset($value['y'])) { + $value['columns'] = array_merge($value['columns'], $value['y']); + } else { + $value['all'] = true; + } + + unset($value['x']); + unset($value['y']); + + return $value; + } + + protected static function getRowInfo(&$rows, $params, $num, &$report) + { + $cols = []; + + //expand columns + $chart_rows = []; + foreach ($rows as $k => $row) { + $vals = []; + + if ($k === 0) { + $i = 1; + $unsorted = 1000; + foreach ($row['values'] as $key => $value) { + if (($temp = array_search($row['values'][$key]->i, $report->options['Charts'][$num]['columns'])) !== false) { + $cols[$temp] = $key; + } elseif (($temp = array_search($row['values'][$key]->key, $report->options['Charts'][$num]['columns'])) !== false) { + $cols[$temp] = $key; + } elseif ($report->options['Charts'][$num]['all']) { + //if all columns are included, add after any specifically defined ones + $cols[$unsorted] = $key; + $unsorted ++; + } + } + + ksort($cols); + } + + foreach ($cols as $key) { + if (isset($row['values'][$key]->chart_value) && is_array($row['values'][$key]->chart_value)) { + foreach ($row['values'][$key]->chart_value as $ckey => $cval) { + $temp = new ReportValue($row['values'][$key]->i, $ckey, trim($cval, '%$ ')); + $temp->setValue($cval); + $vals[] = $temp; + } + } else { + $temp = new ReportValue($row['values'][$key]->i, $row['values'][$key]->key, $row['values'][$key]->original_value); + $temp->setValue(trim($row['values'][$key]->getValue(), '%$ ')); + $vals[] = $temp; + } + } + + $chart_rows[] = $vals; + } + + //determine column types + $types = []; + foreach ($chart_rows as $i => $row) { + foreach ($row as $k => $v) { + $type = self::determineDataType($v->original_value); + //if the value is null, it doesn't influence the column type + if (!$type) { + $chart_rows[$i][$k]->setValue(null); + continue; + } elseif (!isset($types[$k])) { + //if we don't know the column type yet, set it to this row's value + $types[$k] = $type; + } elseif ($type === 'string') { + //if any row has a string value for the column, the whole column is a string type + $types[$k] = 'string'; + } elseif ($types[$k] === 'date' && in_array($type, array('timeofday', 'datetime'))) { + //if the column is currently a date and this row is a time/datetime, set the column to datetime type + $types[$k] = 'datetime'; + } elseif ($types[$k] === 'timeofday' && in_array($type, array('date', 'datetime'))) { + //if the column is currently a time and this row is a date/datetime, set the column to datetime type + $types[$k] = 'datetime'; + } elseif ($types[$k] === 'date' && $type === 'number') { + //if the column is currently a date and this row is a number set the column type to number + $types[$k] = 'number'; + } + } + } + + $report->options['Charts'][$num]['datatypes'] = $types; + + //build chart rows + $report->options['Charts'][$num]['Rows'] = []; + + foreach ($chart_rows as $i => &$row) { + $vals = []; + foreach ($row as $key => $val) { + if (is_null($val->getValue())) { + $val->datatype = 'null'; + } elseif ($types[$key] === 'datetime') { + $val->setValue(date('m/d/Y H:i:s', strtotime($val->getValue()))); + $val->datatype = 'datetime'; + } elseif ($types[$key] === 'timeofday') { + $val->setValue(date('H:i:s', strtotime($val->getValue()))); + $val->datatype = 'timeofday'; + } elseif ($types[$key] === 'date') { + $val->setValue(date('m/d/Y', strtotime($val->getValue()))); + $val->datatype = 'date'; + } elseif ($types[$key] === 'number') { + $val->setValue(round(floatval(preg_replace('/[^-0-9\.]*/', '', $val->getValue())), 6)); + $val->datatype = 'number'; + } else { + $val->datatype = 'string'; + } + + $vals[] = $val; + } + + $report->options['Charts'][$num]['Rows'][] = array( + 'values' => $vals, + 'first' => !$report->options['Charts'][$num]['Rows'], + ); + } + } + + protected static function generateHistogramRows($rows, $column, $num_buckets) + { + $column_key = null; + + //if a name is given as the column, determine the column index + if (!is_numeric($column)) { + foreach ($rows[0]['values'] as $k => $v) { + if ($v->key == $column) { + $column = $k; + $column_key = $v->key; + break; + } + } + } else { + //if an index is given, convert to 0-based + $column --; + $column_key = $rows[0]['values'][$column]->key; + } + + //get a list of values for the histogram + $vals = []; + foreach ($rows as &$row) { + $vals[] = floatval(preg_replace('/[^0-9.]*/', '', $row['values'][$column]->getValue())); + } + sort($vals); + + //determine buckets + $count = count($vals); + $buckets = []; + $min = $vals[0]; + $max = $vals[$count-1]; + $step = ($max-$min)/$num_buckets; + $old_limit = $min; + + for ($i = 1; $i < $num_buckets + 1; $i++) { + $limit = $old_limit + $step; + + $buckets[round($old_limit, 2)." - ".round($limit, 2)] = count( + array_filter( + $vals, + function ($val) use ($old_limit, $limit) { + return $val >= $old_limit && $val < $limit; + } + ) + ); + $old_limit = $limit; + } + + //build chart rows + $chart_rows = []; + foreach ($buckets as $name => $count) { + $chart_rows[] = array( + 'values' => array( + new ReportValue(1, $name, $name), + new ReportValue(2, 'value', $count), + ), + 'first' => !$chart_rows, + ); + } + + return $chart_rows; + } + + protected static function determineDataType($value) + { + if (is_null($value)) { + return null; + } elseif ($value === '') { + return null; + } elseif (preg_match('/^([$%(\-+\s])*([0-9,]+(\.[0-9]+)?|\.[0-9]+)([$%(\-+\s])*$/', $value)) { + return 'number'; + } elseif (preg_match('/^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$/', $value)) { + return 'timeofday'; + } elseif (preg_match('/^[0-9]+(\/|-)[0-9]+/', $value) && strtotime($value)) { + if (date('H:i:s', strtotime($value)) === '00:00:00') { + return 'date'; + } else { + return 'datetime'; + } + } else { + return 'string'; + } + } + + public static function beforeRender(&$report) + { + // Expand out multiple datasets into their own charts + $new_charts = []; + foreach ($report->options['Charts'] as $num => $params) { + $copy = $params; + + // If chart is for multiple datasets + if (is_array($params['dataset'])) { + foreach ($params['dataset'] as $dataset) { + $copy['dataset'] = $dataset; + $copy['num'] = count($new_charts)+1; + $new_charts[] = $copy; + } + } elseif ($params['dataset'] === true) { + // If chart is for all datasets + foreach ($report->options['DataSets'] as $j => $dataset) { + $copy['dataset'] = $j; + $copy['num'] = count($new_charts)+1; + $new_charts[] = $copy; + } + } else { + // If chart is for one dataset + $copy['num'] = count($new_charts)+1; + $new_charts[] = $copy; + } + } + + $report->options['Charts'] = $new_charts; + + foreach ($report->options['Charts'] as $num => &$params) { + self::_processChart($num, $params, $params['dataset'], $report); + } + } + + protected static function _processChart($num, &$params, $dataset, &$report) + { + if (isset($params['xhistogram']) && $params['xhistogram']) { + $rows = self::generateHistogramRows($report->options['DataSets'][$dataset]['rows'], $params['columns'][0], $params['buckets']); + $params['columns'] = array(1,2); + } else { + $rows = []; + if (isset($report->options['DataSets'])) { + $rows = $report->options['DataSets'][$dataset]['rows']; + } + + if (count($rows)) { + if (!$params['columns']) { + $params['columns'] = range(1, count($rows[0]['values'])); + } + } + } + + self::getRowInfo($rows, $params, $num, $report); + } } diff --git a/classes/headers/ColumnsHeader.php b/classes/headers/ColumnsHeader.php index e01420f3..8d45c7b9 100644 --- a/classes/headers/ColumnsHeader.php +++ b/classes/headers/ColumnsHeader.php @@ -1,96 +1,94 @@ $options) { - if(!isset($options['type'])) throw new Exception("Must specify column type for column $column"); - $type = $options['type']; - unset($options['type']); - $report->addFilter($params['dataset'],$column,$type,$options); - } - } - - public static function parseShortcut($value) { - if(preg_match('/^[0-9]+\:/',$value)) { - $dataset = substr($value,0,strpos($value,':')); - $value = substr($value,strlen($dataset)+1); - } - else { - $dataset = 0; - } - - $parts = explode(',',$value); - $params = array(); - $i = 1; - foreach($parts as $part) { - $type = null; - $options = null; - - $part = trim($part); - //special cases - //'rpadN' or 'lpadN' where N is number of spaces to pad - if(substr($part,1,3)==='pad') { - $type = 'padding'; - - $options = array( - 'direction'=>$part[0], - 'spaces'=>intval(substr($part,4)) - ); - } - //link or link(display) or link_blank or link_blank(display) - elseif(substr($part,0,4)==='link') { - //link(display) or link_blank(display) - if(strpos($part,'(') !== false) { - list($type,$display) = explode('(',substr($part,0,-1),2); - } - else { - $type = $part; - $display = 'link'; - } - - $blank = ($type == 'link_blank'); - $type = 'link'; - - $options = array( - 'display'=>$display, - 'blank'=>$blank - ); - } - //synonyms for 'html' - elseif(in_array($part,array('html','raw'))) { - $type = 'html'; - } - //url synonym for link - elseif($part === 'url') { - $type = 'link'; - $options = array( - 'blank'=>false - ); - } - elseif($part === 'bar') { - $type = 'bar'; - $options = array(); - } - elseif($part === 'pre') { - $type = 'pre'; - } - //normal case - else { - $type = 'class'; - $options = array( - 'class'=>$part - ); - } - - $options['type'] = $type; - - $params[$i] = $options; - - $i++; - } - - return array( - 'dataset'=>$dataset, - 'columns'=>$params - ); - } + +class ColumnsHeader extends HeaderBase +{ + public static function init($params, &$report) + { + foreach ($params['columns'] as $column => $options) { + if (!isset($options['type'])) { + throw new Exception("Must specify column type for column $column"); + } + $type = $options['type']; + unset($options['type']); + $report->addFilter($params['dataset'], $column, $type, $options); + } + } + + public static function parseShortcut($value) + { + if (preg_match('/^[0-9]+\:/', $value)) { + $dataset = substr($value, 0, strpos($value, ':')); + $value = substr($value, strlen($dataset)+1); + } else { + $dataset = 0; + } + + $parts = explode(',', $value); + $params = []; + $i = 1; + foreach ($parts as $part) { + $type = null; + $options = null; + + $part = trim($part); + //special cases + //'rpadN' or 'lpadN' where N is number of spaces to pad + if (substr($part, 1, 3) === 'pad') { + $type = 'padding'; + + $options = [ + 'direction' => $part[0], + 'spaces' => intval(substr($part, 4)), + ]; + } elseif (substr($part, 0, 4) === 'link') { + //link or link(display) or link_blank or link_blank(display) + //link(display) or link_blank(display) + if (strpos($part, '(') !== false) { + list($type, $display) = explode('(', substr($part, 0, -1), 2); + } else { + $type = $part; + $display = 'link'; + } + + $blank = ($type == 'link_blank'); + $type = 'link'; + + $options = array( + 'display' => $display, + 'blank' => $blank, + ); + } elseif (in_array($part, array('html', 'raw'))) { + //synonyms for 'html' + $type = 'html'; + } elseif ($part === 'url') { + //url synonym for link + $type = 'link'; + $options = [ + 'blank' => false, + ]; + } elseif ($part === 'bar') { + $type = 'bar'; + $options = []; + } elseif ($part === 'pre') { + $type = 'pre'; + } else { + //normal case + $type = 'class'; + $options = [ + 'class' => $part, + ]; + } + + $options['type'] = $type; + + $params[$i] = $options; + + $i++; + } + + return [ + 'dataset' => $dataset, + 'columns' => $params, + ]; + } } diff --git a/classes/headers/FilterHeader.php b/classes/headers/FilterHeader.php index f85de5c8..f1a5e000 100644 --- a/classes/headers/FilterHeader.php +++ b/classes/headers/FilterHeader.php @@ -1,48 +1,50 @@ array( - 'required'=>true, - 'type'=>'string' - ), - 'filter'=>array( - 'required'=>true, - 'type'=>'string' - ), - 'params'=>array( - 'type'=>'object', - 'default'=>array() - ), - 'dataset'=>array( - 'default'=>0 - ) - ); - - public static function init($params, &$report) { - $report->addFilter($params['dataset'],$params['column'],$params['filter'],$params['params']); - } - - //in format: column, params - //params can be a JSON object or "filter" - //filter classes are defined in class/filters/ - //examples: - // "4,geoip" - apply a geoip filter to the 4th column - // 'Ip,{"filter":"geoip"}' - apply a geoip filter to the "Ip" column - public static function parseShortcut($value) { - if(strpos($value,',') === false) { - $col = "1"; - $filter = $value; - } - else { - list($col,$filter) = explode(',',$value,2); - $col = trim($col); - } - $filter = trim($filter); - - return array( - 'column'=>$col, - 'filter'=>$filter, - 'params'=>array() - ); - } +class FilterHeader extends HeaderBase +{ + public static $validation = [ + 'column' => [ + 'required' => true, + 'type' => 'string', + ], + 'filter' => [ + 'required' => true, + 'type' => 'string', + ], + 'params' => [ + 'type' => 'object', + 'default' => [], + ], + 'dataset' => [ + 'default' => 0, + ], + ]; + + public static function init($params, &$report) + { + $report->addFilter($params['dataset'], $params['column'], $params['filter'], $params['params']); + } + + //in format: column, params + //params can be a JSON object or "filter" + //filter classes are defined in class/filters/ + //examples: + // "4,geoip" - apply a geoip filter to the 4th column + // 'Ip,{"filter":"geoip"}' - apply a geoip filter to the "Ip" column + public static function parseShortcut($value) + { + if (strpos($value, ',') === false) { + $col = "1"; + $filter = $value; + } else { + list($col, $filter) = explode(',', $value, 2); + $col = trim($col); + } + $filter = trim($filter); + + return [ + 'column' => $col, + 'filter' => $filter, + 'params' => [], + ]; + } } diff --git a/classes/headers/FormattingHeader.php b/classes/headers/FormattingHeader.php index 37659177..ba353068 100644 --- a/classes/headers/FormattingHeader.php +++ b/classes/headers/FormattingHeader.php @@ -1,178 +1,188 @@ array( - 'type'=>'number', - 'default'=>null - ), - 'noborder'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'vertical'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'table'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'showcount'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'font'=>array( - 'type'=>'string' - ), - 'nodata'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'selectable'=>array( - 'type'=>'string' - ), - 'dataset'=>array( - 'required'=>true, - 'default'=>true - ) - ); - - public static function init($params, &$report) { - if(!isset($report->options['Formatting'])) $report->options['Formatting'] = array(); - $report->options['Formatting'][] = $params; - } - - public static function parseShortcut($value) { - $options = explode(',',$value); - - $params = array(); - - foreach($options as $v) { - if(strpos($v,'=')!==false) { - list($k,$v) = explode('=',$v,2); - $v = trim($v); - } - else { - $k = $v; - $v=true; - } - - $k = trim($k); - - $params[$k] = $v; - } - - return $params; - } - - public static function beforeRender(&$report) { - $formatting = array(); - // Expand out by dataset - foreach($report->options['Formatting'] as $params) { - $copy = $params; - unset($copy['dataset']); - - if(isset($report->options['DataSets'])) { - // Multiple datasets defined - if(is_array($params['dataset'])) { - foreach($params['dataset'] as $i) { - if(isset($report->options['DataSets'][$i])) { - if(!isset($formatting[$i])) $formatting[$i] = array(); - foreach($copy as $k=>$v) { - $formatting[$i][$k] = $v; - } - } - } - } - // All datasets - elseif($params['dataset']===true) { - foreach($report->options['DataSets'] as $i=>$dataset) { - if(!isset($formatting[$i])) $formatting[$i] = array(); - foreach($copy as $k=>$v) { - $formatting[$i][$k] = $v; - } - } - } - // Single dataset - else { - if(!isset($report->options['DataSets'][$params['dataset']])) continue; - if(!isset($formatting[$params['dataset']])) $formatting[$params['dataset']] = array(); - foreach($copy as $k=>$v) { - $formatting[$params['dataset']][$k] = $v; - } - } - } - } - - $report->options['Formatting'] = $formatting; - - // Apply formatting options for each dataset - foreach($formatting as $i=>$params) { - if(isset($params['limit']) && $params['limit']) { - $report->options['DataSets'][$i]['rows'] = array_slice($report->options['DataSets'][$i]['rows'],0,intval($params['limit'])); - } - if(isset($params['selectable']) && $params['selectable']) { - $selected = array(); - - // New style "selected_{{DATASET}}" querystring - if(isset($_GET['selected_'.$i])) { - $selected = $_GET['selected_'.$i]; - } - // Old style "selected" querystring - elseif(isset($_GET['selected'])) { - $selected = $_GET['selected']; - } - - if($selected) { - $selected_key = null; - foreach($report->options['DataSets'][$i]['rows'][0]['values'] as $key=>$value) { - if($value->key == $params['selectable']) { - $selected_key = $key; - break; - } - } - - if($selected_key !== null) { - foreach($report->options['DataSets'][$i]['rows'] as $key=>$row) { - - if(!in_array($row['values'][$selected_key]->getValue(),$selected)) { - unset($report->options['DataSets'][$i]['rows'][$key]); - } - } - $report->options['DataSets'][$i]['rows'] = array_values($report->options['DataSets'][$i]['rows']); - } - } - } - if(isset($params['vertical']) && $params['vertical']) { - $rows = array(); - foreach($report->options['DataSets'][$i]['rows'] as $row) { - foreach($row['values'] as $value) { - if(!isset($rows[$value->key])) { - $header = new ReportValue(1, 'key', $value->key); - $header->class = 'left lpad'; - $header->is_header = true; - - $rows[$value->key] = array( - 'values'=>array( - $header - ), - 'first'=>!$rows - ); - } - - $rows[$value->key]['values'][] = $value; - } - } - - $rows = array_values($rows); - - $report->options['DataSets'][$i]['vertical'] = $rows; - } - - unset($params['vertical']); - foreach($params as $k=>$v) { - $report->options['DataSets'][$i][$k] = $v; - } - } - } + +class FormattingHeader extends HeaderBase +{ + public static $validation = [ + 'limit' => [ + 'type' => 'number', + 'default' => null, + ], + 'noborder' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'vertical' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'table' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'showcount' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'font' => [ + 'type' => 'string', + ], + 'nodata' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'selectable' => [ + 'type' => 'string', + ], + 'dataset' => [ + 'required' => true, + 'default' => true, + ], + ]; + + public static function init($params, &$report) + { + if (!isset($report->options['Formatting'])) { + $report->options['Formatting'] = []; + } + $report->options['Formatting'][] = $params; + } + + public static function parseShortcut($value) + { + $options = explode(',', $value); + + $params = []; + + foreach ($options as $v) { + if (strpos($v, '=') !== false) { + list($k, $v) = explode('=', $v, 2); + $v = trim($v); + } else { + $k = $v; + $v = true; + } + + $k = trim($k); + + $params[$k] = $v; + } + + return $params; + } + + public static function beforeRender(&$report) + { + $formatting = []; + // Expand out by dataset + foreach ($report->options['Formatting'] as $params) { + $copy = $params; + unset($copy['dataset']); + + if (isset($report->options['DataSets'])) { + // Multiple datasets defined + if (is_array($params['dataset'])) { + foreach ($params['dataset'] as $i) { + if (isset($report->options['DataSets'][$i])) { + if (!isset($formatting[$i])) { + $formatting[$i] = []; + } + foreach ($copy as $k => $v) { + $formatting[$i][$k] = $v; + } + } + } + } elseif ($params['dataset'] === true) { + // All datasets + foreach ($report->options['DataSets'] as $i => $dataset) { + if (!isset($formatting[$i])) { + $formatting[$i] = []; + } + foreach ($copy as $k => $v) { + $formatting[$i][$k] = $v; + } + } + } else { + // Single dataset + if (!isset($report->options['DataSets'][$params['dataset']])) { + continue; + } + if (!isset($formatting[$params['dataset']])) { + $formatting[$params['dataset']] = []; + } + foreach ($copy as $k => $v) { + $formatting[$params['dataset']][$k] = $v; + } + } + } + } + + $report->options['Formatting'] = $formatting; + + // Apply formatting options for each dataset + foreach ($formatting as $i => $params) { + if (isset($params['limit']) && $params['limit']) { + $report->options['DataSets'][$i]['rows'] = array_slice($report->options['DataSets'][$i]['rows'], 0, intval($params['limit'])); + } + if (isset($params['selectable']) && $params['selectable']) { + $selected = []; + + // New style "selected_{{DATASET}}" querystring + if (isset($_GET['selected_'.$i])) { + $selected = $_GET['selected_'.$i]; + } elseif (isset($_GET['selected'])) { + // Old style "selected" querystring + $selected = $_GET['selected']; + } + + if ($selected) { + $selected_key = null; + foreach ($report->options['DataSets'][$i]['rows'][0]['values'] as $key => $value) { + if ($value->key == $params['selectable']) { + $selected_key = $key; + break; + } + } + + if ($selected_key !== null) { + foreach ($report->options['DataSets'][$i]['rows'] as $key => $row) { + if (!in_array($row['values'][$selected_key]->getValue(), $selected)) { + unset($report->options['DataSets'][$i]['rows'][$key]); + } + } + $report->options['DataSets'][$i]['rows'] = array_values($report->options['DataSets'][$i]['rows']); + } + } + } + if (isset($params['vertical']) && $params['vertical']) { + $rows = []; + foreach ($report->options['DataSets'][$i]['rows'] as $row) { + foreach ($row['values'] as $value) { + if (!isset($rows[$value->key])) { + $header = new ReportValue(1, 'key', $value->key); + $header->class = 'left lpad'; + $header->is_header = true; + + $rows[$value->key] = [ + 'values' => [ + $header, + ], + 'first' => !$rows, + ]; + } + + $rows[$value->key]['values'][] = $value; + } + } + + $rows = array_values($rows); + + $report->options['DataSets'][$i]['vertical'] = $rows; + } + + unset($params['vertical']); + foreach ($params as $k => $v) { + $report->options['DataSets'][$i][$k] = $v; + } + } + } } diff --git a/classes/headers/IncludeHeader.php b/classes/headers/IncludeHeader.php index 7f2ce5ab..18edfac4 100644 --- a/classes/headers/IncludeHeader.php +++ b/classes/headers/IncludeHeader.php @@ -1,47 +1,50 @@ array( - 'required'=>true, - 'type'=>'string' - ) - ); - - public static function init($params, &$report) { - if($params['report'][0] === '/') { - $report_path = substr($params['report'],1); - } - else { - $report_path = dirname($report->report).'/'.$params['report']; - } - - - if(!file_exists(PhpReports::$config['reportDir'].'/'.$report_path)) { - $possible_reports = glob(PhpReports::$config['reportDir'].'/'.$report_path.'.*'); - - if($possible_reports) { - $report_path = substr($possible_reports[0],strlen(PhpReports::$config['reportDir'].'/')); - } - else { - throw new Exception("Unknown report in INCLUDE header '$report_path'"); - } - } - - $included_report = new Report($report_path); - - //parse any exported headers from the included report - foreach($included_report->exported_headers as $header) { - $report->parseHeader($header['name'],$header['params']); - } - - if(!isset($report->options['Includes'])) $report->options['Includes'] = array(); - - $report->options['Includes'][] = $included_report; - } - - public static function parseShortcut($value) { - return array( - 'report'=>$value - ); - } + +class IncludeHeader extends HeaderBase +{ + public static $validation = [ + 'report' => [ + 'required' => true, + 'type' => 'string', + ], + ]; + + public static function init($params, &$report) + { + if ($params['report'][0] === '/') { + $report_path = substr($params['report'], 1); + } else { + $report_path = dirname($report->report).'/'.$params['report']; + } + + if (!file_exists(PhpReports::$config['reportDir'].'/'.$report_path)) { + $possible_reports = glob(PhpReports::$config['reportDir'].'/'.$report_path.'.*'); + + if ($possible_reports) { + $report_path = substr($possible_reports[0], strlen(PhpReports::$config['reportDir'].'/')); + } else { + throw new Exception("Unknown report in INCLUDE header '$report_path'"); + } + } + + $included_report = new Report($report_path); + + //parse any exported headers from the included report + foreach ($included_report->exported_headers as $header) { + $report->parseHeader($header['name'], $header['params']); + } + + if (!isset($report->options['Includes'])) { + $report->options['Includes'] = []; + } + + $report->options['Includes'][] = $included_report; + } + + public static function parseShortcut($value) + { + return [ + 'report' => $value, + ]; + } } diff --git a/classes/headers/InfoHeader.php b/classes/headers/InfoHeader.php index e973e842..6c33730a 100644 --- a/classes/headers/InfoHeader.php +++ b/classes/headers/InfoHeader.php @@ -1,55 +1,58 @@ array( - 'type'=>'string' - ), - 'description'=>array( - 'type'=>'string' - ), - 'created'=>array( - 'type'=>'string', - 'pattern'=>'/^[0-9]{4}-[0-9]{2}-[0-9]{2}/' - ), - 'note'=>array( - 'type'=>'string' - ), - 'type'=>array( - 'type'=>'string' - ), - 'status'=>array( - 'type'=>'string' - ) - ); - - public static function init($params, &$report) { - foreach($params as $key=>$value) { - $report->options[ucfirst($key)] = $value; - } - } - - // Accepts shortcut format: - // name=My Report,description=This is My Report - public static function parseShortcut($value) { - $parts = explode(',',$value); - - $params = array(); - - foreach($parts as $v) { - if(strpos($v,'=')!==false) { - list($k,$v) = explode('=',$v,2); - $v = trim($v); - } - else { - $k = $v; - $v=true; - } - - $k = trim($k); - - $params[$k] = $v; - } - - return $params; - } + +class InfoHeader extends HeaderBase +{ + public static $validation = [ + 'name' => [ + 'type' => 'string', + ], + 'description' => [ + 'type' => 'string', + ], + 'created' => [ + 'type' => 'string', + 'pattern' => '/^[0-9]{4}-[0-9]{2}-[0-9]{2}/', + ], + 'note' => [ + 'type' => 'string', + ], + 'type' => [ + 'type' => 'string', + ], + 'status' => [ + 'type' => 'string', + ], + ]; + + public static function init($params, &$report) + { + foreach ($params as $key => $value) { + $report->options[ucfirst($key)] = $value; + } + } + + // Accepts shortcut format: + // name=My Report,description=This is My Report + public static function parseShortcut($value) + { + $parts = explode(',', $value); + + $params = []; + + foreach ($parts as $v) { + if (strpos($v, '=') !== false) { + list($k, $v) = explode('=', $v, 2); + $v = trim($v); + } else { + $k = $v; + $v = true; + } + + $k = trim($k); + + $params[$k] = $v; + } + + return $params; + } } diff --git a/classes/headers/OptionsHeader.php b/classes/headers/OptionsHeader.php index f46fb18a..8409ffad 100644 --- a/classes/headers/OptionsHeader.php +++ b/classes/headers/OptionsHeader.php @@ -1,142 +1,149 @@ array( - 'type'=>'number', - 'default'=>null - ), - 'access'=>array( - 'type'=>'enum', - 'values'=>array('rw','readonly'), - 'default'=>'readonly' - ), - 'noborder'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'noreport'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'vertical'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'ignore'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'table'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'showcount'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'font'=>array( - 'type'=>'string' - ), - 'stop'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'nodata'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'version'=>array( - 'type'=>'number', - 'default'=>1 - ), - 'selectable'=>array( - 'type'=>'string' - ), - 'mongodatabase'=>array( - 'type'=>'string' - ), - 'database'=>array( - 'type'=>'string' - ), - 'cache'=>array( - 'min'=>0, - 'type'=>'number' - ), - 'ttl'=>array( - 'min'=>0, - 'type'=>'number' - ), - 'default_dataset'=>array( - 'type'=>'number', - 'default'=>0 - ), - 'has_charts'=>array( - 'type'=>'boolean' - ) - ); - - public static function init($params, &$report) { - //legacy support for the 'ttl' cache parameter - if(isset($params['ttl'])) { - $params['cache'] = $params['ttl']; - unset($params['ttl']); - } - if(isset($params['has_charts']) && $params['has_charts']) { - if(!isset($report->options['Charts'])) $report->options['Charts'] = array(); - } - - // Some parameters were moved to a 'FORMATTING' header - // We need to catch those and add the header to the report - $formatting_header = array(); - - foreach($params as $key=>$value) { - // This is a FORMATTING parameter - if(in_array($key,array('limit','noborder','vertical','table','showcount','font','nodata','selectable'))) { - $formatting_header[$key] = $value; - continue; - } - - //some of the keys need to be uppercase (for legacy reasons) - if(in_array($key,array('database','mongodatabase','cache'))) $key = ucfirst($key); - - $report->options[$key] = $value; - - //if the value is different from the default, it can be exported - if(!isset(self::$validation[$key]['default']) || ($value && $value !== self::$validation[$key]['default'])) { - //only export some of the options - if(in_array($key,array('access','Cache'),true)) { - $report->exportHeader('Options',array($key=>$value)); - } - } - } - - if($formatting_header) { - $formatting_header['dataset'] = true; - $report->parseHeader('Formatting',$formatting_header); - } - } - - public static function parseShortcut($value) { - $options = explode(',',$value); - - $params = array(); - - foreach($options as $v) { - if(strpos($v,'=')!==false) { - list($k,$v) = explode('=',$v,2); - $v = trim($v); - } - else { - $k = $v; - $v=true; - } - - $k = trim($k); - - $params[$k] = $v; - } - - return $params; - } +class OptionsHeader extends HeaderBase +{ + public static $validation = [ + 'limit' => [ + 'type' => 'number', + 'default' => null, + ], + 'access' => [ + 'type' => 'enum', + 'values' => ['rw', 'readonly'], + 'default' => 'readonly', + ], + 'noborder' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'noreport' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'vertical' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'ignore' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'table' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'showcount' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'font' => [ + 'type' => 'string', + ], + 'stop' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'nodata' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'version' => [ + 'type' => 'number', + 'default' => 1, + ], + 'selectable' => [ + 'type' => 'string', + ], + 'mongodatabase' => [ + 'type' => 'string', + ], + 'database' => [ + 'type' => 'string', + ], + 'cache' => [ + 'min' => 0, + 'type' => 'number', + ], + 'ttl' => [ + 'min' => 0, + 'type' => 'number', + ], + 'default_dataset' => [ + 'type' => 'number', + 'default' => 0, + ], + 'has_charts' => [ + 'type' => 'boolean', + ], + ]; + + public static function init($params, &$report) + { + //legacy support for the 'ttl' cache parameter + if (isset($params['ttl'])) { + $params['cache'] = $params['ttl']; + unset($params['ttl']); + } + + if (isset($params['has_charts']) && $params['has_charts']) { + if (!isset($report->options['Charts'])) { + $report->options['Charts'] = []; + } + } + + // Some parameters were moved to a 'FORMATTING' header + // We need to catch those and add the header to the report + $formatting_header = []; + + foreach ($params as $key => $value) { + // This is a FORMATTING parameter + if (in_array($key, ['limit', 'noborder', 'vertical', 'table', 'showcount', 'font', 'nodata', 'selectable'])) { + $formatting_header[$key] = $value; + continue; + } + + //some of the keys need to be uppercase (for legacy reasons) + if (in_array($key, ['database', 'mongodatabase', 'cache'])) { + $key = ucfirst($key); + } + + $report->options[$key] = $value; + + //if the value is different from the default, it can be exported + if (!isset(self::$validation[$key]['default']) || ($value && $value !== self::$validation[$key]['default'])) { + //only export some of the options + if (in_array($key, array('access', 'Cache'), true)) { + $report->exportHeader('Options', array($key => $value)); + } + } + } + + if ($formatting_header) { + $formatting_header['dataset'] = true; + $report->parseHeader('Formatting', $formatting_header); + } + } + + public static function parseShortcut($value) + { + $options = explode(',', $value); + + $params = []; + + foreach ($options as $v) { + if (strpos($v, '=') !== false) { + list($k, $v) = explode('=', $v, 2); + $v = trim($v); + } else { + $k = $v; + $v = true; + } + + $k = trim($k); + + $params[$k] = $v; + } + + return $params; + } } diff --git a/classes/headers/RollupHeader.php b/classes/headers/RollupHeader.php index 3b89ab1c..5932b904 100644 --- a/classes/headers/RollupHeader.php +++ b/classes/headers/RollupHeader.php @@ -1,125 +1,147 @@ array( - 'required'=>true, - 'type'=>'object', - 'default'=>array() - ), - 'dataset'=>array( - 'required'=>false, - 'default'=>0 - ) - ); - - public static function init($params, &$report) { - //make sure at least 1 column is defined - if(empty($params['columns'])) throw new Exception("Rollup header needs at least 1 column defined"); - - if(!isset($report->options['Rollup'])) $report->options['Rollup'] = array(); - - // If more than one dataset is defined, add the rollup header multiple times - if(is_array($params['dataset'])) { - $new_params = $params; - foreach($params['dataset'] as $dataset) { - $new_params['dataset'] = $dataset; - $report->options['Rollup'][] = $new_params; - } - } - // Otherwise, just add one rollup header - else { - $report->options['Rollup'][] = $params; - } - } - - public static function beforeRender(&$report) { - //cache for Twig parameters for each dataset/column - $twig_params = array(); - - // Now that we know how many datasets we have, expand out Rollup headers with dataset->true - $new_rollups = array(); - foreach($report->options['Rollup'] as $i=>$rollup) { - if($rollup['dataset']===true && isset($report->options['DataSets'])) { - $copy = $rollup; - foreach($report->options['DataSets'] as $i=>$dataset) { - $copy['dataset'] = $i; - $new_rollups[] = $copy; - } - } - else { - $new_rollups[] = $rollup; - } - } - $report->options['Rollup'] = $new_rollups; - - // First get all the values - foreach($report->options['Rollup'] as $rollup) { - // If we already got twig parameters for this dataset, skip it - if(isset($twig_params[$rollup['dataset']])) continue; - $twig_params[$rollup['dataset']] = array(); - if(isset($report->options['DataSets'])) { - if(isset($report->options['DataSets'][$rollup['dataset']])) { - foreach($report->options['DataSets'][$rollup['dataset']]['rows'] as $row) { - foreach($row['values'] as $value) { - if(!isset($twig_params[$rollup['dataset']][$value->key])) $twig_params[$rollup['dataset']][$value->key] = array('values'=>array()); - $twig_params[$rollup['dataset']][$value->key]['values'][] = $value->getValue(); - } - } - } - } - } - // Then, calculate other statistical properties - foreach($twig_params as $dataset=>&$tp) { - foreach($tp as $column=>&$params) { - //get non-null values and sort them - $real_values = array_filter($params['values'],function($a) {if($a === null || $a==='') return false; return true; }); - sort($real_values); - - $params['sum'] = array_sum($real_values); - $params['count'] = count($real_values); - if($params['count']) { - $params['mean'] = $params['average'] = $params['sum'] / $params['count']; - $params['median'] = ($params['count']%2)? ($real_values[$params['count']/2-1] + $real_values[$params['count']/2])/2 : $real_values[floor($params['count']/2)]; - $params['min'] = $real_values[0]; - $params['max'] = $real_values[$params['count']-1]; - } - else { - $params['mean'] = $params['average'] = $params['median'] = $params['min'] = $params['max'] = 0; - } - - $devs = array(); - if (empty($real_values)) { - $params['stdev'] = 0; - } else if (function_exists('stats_standard_deviation')) { - $params['stdev'] = stats_standard_deviation($real_values); - } else { - foreach($real_values as $v) $devs[] = pow($v - $params['mean'], 2); - $params['stdev'] = sqrt(array_sum($devs) / (count($devs))); - } - } - } - - //render each rollup row - foreach($report->options['Rollup'] as $rollup) { - if(!isset($report->options['DataSets'][$rollup['dataset']]['footer'])) $report->options['DataSets'][$rollup['dataset']]['footer'] = array(); - $columns = $rollup['columns']; - $row = array( - 'values'=>array(), - 'rollup'=>true - ); - - foreach($twig_params[$rollup['dataset']] as $column=>$p) { - if(isset($columns[$column])) { - $p = array_merge($p,array('row'=>$twig_params[$rollup['dataset']])); +class RollupHeader extends HeaderBase +{ + public static $validation = [ + 'columns' => [ + 'required' => true, + 'type' => 'object', + 'default' => [], + ], + 'dataset' => [ + 'required' => false, + 'default' => 0, + ], + ]; - $row['values'][] = new ReportValue(-1,$column,PhpReports::renderString($columns[$column],$p)); - } - else { - $row['values'][] = new ReportValue(-1,$column,null); - } - } - $report->options['DataSets'][$rollup['dataset']]['footer'][] = $row; - } - } + public static function init($params, &$report) + { + //make sure at least 1 column is defined + if (empty($params['columns'])) { + throw new Exception("Rollup header needs at least 1 column defined"); + } + + if (!isset($report->options['Rollup'])) { + $report->options['Rollup'] = []; + } + + // If more than one dataset is defined, add the rollup header multiple times + if (is_array($params['dataset'])) { + $new_params = $params; + foreach ($params['dataset'] as $dataset) { + $new_params['dataset'] = $dataset; + $report->options['Rollup'][] = $new_params; + } + } else { + // Otherwise, just add one rollup header + $report->options['Rollup'][] = $params; + } + } + + public static function beforeRender(&$report) + { + //cache for Twig parameters for each dataset/column + $twig_params = []; + + // Now that we know how many datasets we have, expand out Rollup headers with dataset->true + $new_rollups = []; + foreach ($report->options['Rollup'] as $i => $rollup) { + if ($rollup['dataset'] === true && isset($report->options['DataSets'])) { + $copy = $rollup; + foreach ($report->options['DataSets'] as $i => $dataset) { + $copy['dataset'] = $i; + $new_rollups[] = $copy; + } + } else { + $new_rollups[] = $rollup; + } + } + $report->options['Rollup'] = $new_rollups; + + // First get all the values + foreach ($report->options['Rollup'] as $rollup) { + // If we already got twig parameters for this dataset, skip it + if (isset($twig_params[$rollup['dataset']])) { + continue; + } + $twig_params[$rollup['dataset']] = []; + if (isset($report->options['DataSets'])) { + if (isset($report->options['DataSets'][$rollup['dataset']])) { + foreach ($report->options['DataSets'][$rollup['dataset']]['rows'] as $row) { + foreach ($row['values'] as $value) { + if (!isset($twig_params[$rollup['dataset']][$value->key])) { + $twig_params[$rollup['dataset']][$value->key] = ['values' => []]; + } + $twig_params[$rollup['dataset']][$value->key]['values'][] = $value->getValue(); + } + } + } + } + } + + // Then, calculate other statistical properties + foreach ($twig_params as $dataset => &$tp) { + foreach ($tp as $column => &$params) { + //get non-null values and sort them + $real_values = array_filter( + $params['values'], + function ($a) { + if ($a === null || $a === '') { + return false; + } + + return true; + } + ); + + sort($real_values); + + $params['sum'] = array_sum($real_values); + $params['count'] = count($real_values); + if ($params['count']) { + $params['mean'] = $params['average'] = $params['sum'] / $params['count']; + $params['median'] = ($params['count']%2) ? ($real_values[$params['count']/2-1] + $real_values[$params['count']/2])/2 : $real_values[floor($params['count']/2)]; + $params['min'] = $real_values[0]; + $params['max'] = $real_values[$params['count']-1]; + } else { + $params['mean'] = $params['average'] = $params['median'] = $params['min'] = $params['max'] = 0; + } + + $devs = []; + if (empty($real_values)) { + $params['stdev'] = 0; + } elseif (function_exists('stats_standard_deviation')) { + $params['stdev'] = stats_standard_deviation($real_values); + } else { + foreach ($real_values as $v) { + $devs[] = pow($v - $params['mean'], 2); + } + $params['stdev'] = sqrt(array_sum($devs) / (count($devs))); + } + } + } + + //render each rollup row + foreach ($report->options['Rollup'] as $rollup) { + if (!isset($report->options['DataSets'][$rollup['dataset']]['footer'])) { + $report->options['DataSets'][$rollup['dataset']]['footer'] = []; + } + $columns = $rollup['columns']; + $row = [ + 'values' => [], + 'rollup' => true, + ]; + + foreach ($twig_params[$rollup['dataset']] as $column => $p) { + if (isset($columns[$column])) { + $p = array_merge($p, ['row' => $twig_params[$rollup['dataset']]]); + + $row['values'][] = new ReportValue(-1, $column, PhpReports::renderString($columns[$column], $p)); + } else { + $row['values'][] = new ReportValue(-1, $column, null); + } + } + $report->options['DataSets'][$rollup['dataset']]['footer'][] = $row; + } + } } diff --git a/classes/headers/VariableHeader.php b/classes/headers/VariableHeader.php index 279ab1a4..2eba8953 100644 --- a/classes/headers/VariableHeader.php +++ b/classes/headers/VariableHeader.php @@ -1,219 +1,229 @@ array( - 'required'=>true, - 'type'=>'string' - ), - 'display'=>array( - 'type'=>'string' - ), - 'type'=>array( - 'type'=>'enum', - 'values'=>array('text','select','textarea','date','daterange'), - 'default'=>'text' - ), - 'options'=>array( - 'type'=>'array' - ), - 'default'=>array( - - ), - 'empty'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'multiple'=>array( - 'type'=>'boolean', - 'default'=>false - ), - 'database_options'=>array( - 'type'=>'object' - ), - 'description'=>array( - 'type'=>'string' - ), - 'format'=>array( - 'type'=>'string', - 'default'=>'Y-m-d H:i:s' - ), - 'modifier_options'=>array( - 'type'=>'array' - ), - 'time_offset'=>array( - 'type'=>'number' - ), - ); - - public static function init($params, &$report) { - if(!isset($params['display']) || !$params['display']) $params['display'] = $params['name']; - - if(!preg_match('/^[a-zA-Z][a-zA-Z0-9_\-]*$/',$params['name'])) throw new Exception("Invalid variable name: $params[name]"); - - //add to options - if(!isset($report->options['Variables'])) $report->options['Variables'] = array(); - $report->options['Variables'][$params['name']] = $params; - - //add to macros - if(!isset($report->macros[$params['name']]) && isset($params['default'])) { - $report->addMacro($params['name'],$params['default']); - - $report->macros[$params['name']] = $params['default']; - - if(!isset($params['empty']) || !$params['empty']) { - $report->is_ready = false; - } - } - elseif(!isset($report->macros[$params['name']])) { - $report->addMacro($params['name'],''); - - if(!isset($params['empty']) || !$params['empty']) { - $report->is_ready = false; - } - } - - //convert newline separated strings to array for vars that support multiple values - if($params['multiple'] && !is_array($report->macros[$params['name']])) $report->addMacro($params['name'],explode("\n",$report->macros[$params['name']])); - - $report->exportHeader('Variable',$params); - } - - public static function parseShortcut($value) { - list($var,$params) = explode(',',$value,2); - $var = trim($var); - $params = trim($params); - - $parts = explode(',',$params); - $params = array( - 'name'=>$var, - 'display'=>trim($parts[0]) - ); - - unset($parts[0]); - - $extra = implode(',',$parts); - - //just "name, label" - if(!$extra) return $params; - - //if the 3rd item is "LIST", use multi-select - if(preg_match('/^\s*LIST\s*\b/',$extra)) { - $params['multiple'] = true; - $extra = array_pop(explode(',',$extra,2)); - } - - //table.column, where clause, ALL - if(preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,[^,]+,\s*ALL\s*$/', $extra)) { - list($table_column, $where, $all) = explode(',',$extra, 3); - list($table,$column) = explode('.',$table_column,2); - - $params['type'] = 'select'; - - $var_params = array( - 'table'=>$table, - 'column'=>$column, - 'all'=>true, - 'where'=>$where - ); - - $params['database_options'] = $var_params; - } - - //table.column, ALL - elseif(preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,\s*ALL\s*$/', $extra)) { - list($table_column, $all) = explode(',',$extra, 2); - list($table,$column) = explode('.',$table_column,2); - - $params['type'] = 'select'; - - $var_params = array( - 'table'=>$table, - 'column'=>$column, - 'all'=>true - ); - - $params['database_options'] = $var_params; - } - - //table.column, where clause - elseif(preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,[^,]+$/', $extra)) { - list($table_column, $where) = explode(',',$extra, 2); - list($table,$column) = explode('.',$table_column,2); - - $params['type'] = 'select'; - - $var_params = array( - 'table'=>$table, - 'column'=>$column, - 'where'=>$where - ); - - $params['database_options'] = $var_params; - } - - //table.column - elseif(preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*$/', $extra)) { - list($table,$column) = explode('.',$extra,2); - - $params['type'] = 'select'; - - $var_params = array( - 'table'=>$table, - 'column'=>$column - ); - - $params['database_options'] = $var_params; - } - - //option1|option2 - elseif(preg_match('/^\s*([a-zA-Z0-9_\- ]+\|)+[a-zA-Z0-9_\- ]+$/',$extra)) { - $options = explode('|',$extra); - - $params['type'] = 'select'; - $params['options'] = $options; - } - - return $params; - } - - public static function afterParse(&$report) { - $classname = $report->options['Type'].'ReportType'; - - foreach($report->options['Variables'] as $var=>$params) { - //if it's a select variable and the options are pulled from a database - if(isset($params['database_options'])) { - $classname::openConnection($report); - $params['options'] = $classname::getVariableOptions($params['database_options'],$report); - - $report->options['Variables'][$var] = $params; - } - - //if the type is daterange, parse start and end with strtotime - if($params['type'] === 'daterange' && !empty($report->macros[$params['name']][0]) && !empty($report->macros[$params['name']][1])) { - $start = date_create($report->macros[$params['name']][0]); - if(!$start) throw new Exception($params['display']." must have a valid start date."); - date_time_set($start,0,0,0); - $report->macros[$params['name']]['start'] = date_format($start,$params['format']); - - $end = date_create($report->macros[$params['name']][1]); - if(!$end) throw new Exception($params['display']." must have a valid end date."); - date_time_set($end,23,59,59); - $report->macros[$params['name']]['end'] = date_format($end,$params['format']); - } - } - } - - public static function beforeRun(&$report) { - foreach($report->options['Variables'] as $var=>$params) { - //if the type is date, parse with strtotime - if($params['type'] === 'date' && $report->macros[$params['name']]) { - - $time = strtotime($report->macros[$params['name']]); - if(!$time) throw new Exception($params['display']." must be a valid datetime value."); - - $report->macros[$params['name']] = date($params['format'],$time); - } - } - } + +class VariableHeader extends HeaderBase +{ + public static $validation = [ + 'name' => [ + 'required' => true, + 'type' => 'string', + ], + 'display' => [ + 'type' => 'string', + ], + 'type' => [ + 'type' => 'enum', + 'values' => ['text', 'select', 'textarea', 'date', 'daterange'], + 'default' => 'text', + ], + 'options' => [ + 'type' => 'array', + ], + 'default' => [], + 'empty' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'multiple' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'database_options' => [ + 'type' => 'object', + ], + 'description' => [ + 'type' => 'string', + ], + 'format' => [ + 'type' => 'string', + 'default' => 'Y-m-d H:i:s', + ], + 'modifier_options' => [ + 'type' => 'array', + ], + 'time_offset' => [ + 'type' => 'number', + ], + ]; + + public static function init($params, &$report) + { + if (!isset($params['display']) || !$params['display']) { + $params['display'] = $params['name']; + } + + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_\-]*$/', $params['name'])) { + throw new Exception("Invalid variable name: $params[name]"); + } + + //add to options + if (!isset($report->options['Variables'])) { + $report->options['Variables'] = []; + } + $report->options['Variables'][$params['name']] = $params; + + //add to macros + if (!isset($report->macros[$params['name']]) && isset($params['default'])) { + $report->addMacro($params['name'], $params['default']); + + $report->macros[$params['name']] = $params['default']; + + if (!isset($params['empty']) || !$params['empty']) { + $report->is_ready = false; + } + } elseif (!isset($report->macros[$params['name']])) { + $report->addMacro($params['name'], ''); + + if (!isset($params['empty']) || !$params['empty']) { + $report->is_ready = false; + } + } + + //convert newline separated strings to array for vars that support multiple values + if ($params['multiple'] && !is_array($report->macros[$params['name']])) { + $report->addMacro($params['name'], explode("\n", $report->macros[$params['name']])); + } + + $report->exportHeader('Variable', $params); + } + + public static function parseShortcut($value) + { + list($var, $params) = explode(',', $value, 2); + $var = trim($var); + $params = trim($params); + + $parts = explode(',', $params); + $params = [ + 'name' => $var, + 'display' => trim($parts[0]), + ]; + + unset($parts[0]); + + $extra = implode(',', $parts); + + //just "name, label" + if (!$extra) { + return $params; + } + + //if the 3rd item is "LIST", use multi-select + if (preg_match('/^\s*LIST\s*\b/', $extra)) { + $params['multiple'] = true; + $extraexplode = explode(',', $extra, 2); + $extra = array_pop($extraexplode); + } + + //table.column, where clause, ALL + if (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,[^,]+,\s*ALL\s*$/', $extra)) { + list($table_column, $where, $all) = explode(',', $extra, 3); + list($table, $column) = explode('.', $table_column, 2); + + $params['type'] = 'select'; + + $var_params = [ + 'table' => $table, + 'column' => $column, + 'all' => true, + 'where' => $where, + ]; + + $params['database_options'] = $var_params; + } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,\s*ALL\s*$/', $extra)) { + //table.column, ALL + list($table_column, $all) = explode(',', $extra, 2); + list($table, $column) = explode('.', $table_column, 2); + + $params['type'] = 'select'; + + $var_params = [ + 'table' => $table, + 'column' => $column, + 'all' => true, + ]; + + $params['database_options'] = $var_params; + } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,[^,]+$/', $extra)) { + //table.column, where clause + list($table_column, $where) = explode(',', $extra, 2); + list($table, $column) = explode('.', $table_column, 2); + + $params['type'] = 'select'; + + $var_params = [ + 'table' => $table, + 'column' => $column, + 'where' => $where, + ]; + + $params['database_options'] = $var_params; + } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*$/', $extra)) { + //table.column + list($table, $column) = explode('.', $extra, 2); + + $params['type'] = 'select'; + + $var_params = [ + 'table' => $table, + 'column' => $column, + ]; + + $params['database_options'] = $var_params; + } elseif (preg_match('/^\s*([a-zA-Z0-9_\- ]+\|)+[a-zA-Z0-9_\- ]+$/', $extra)) { + //option1|option2 + $options = explode('|', $extra); + + $params['type'] = 'select'; + $params['options'] = $options; + } + + return $params; + } + + public static function afterParse(&$report) + { + $classname = $report->options['Type'].'ReportType'; + + foreach ($report->options['Variables'] as $var => $params) { + //if it's a select variable and the options are pulled from a database + if (isset($params['database_options'])) { + $classname::openConnection($report); + $params['options'] = $classname::getVariableOptions($params['database_options'], $report); + + $report->options['Variables'][$var] = $params; + } + + //if the type is daterange, parse start and end with strtotime + if ($params['type'] === 'daterange' && !empty($report->macros[$params['name']][0]) && !empty($report->macros[$params['name']][1])) { + $start = date_create($report->macros[$params['name']][0]); + if (!$start) { + throw new Exception($params['display']." must have a valid start date."); + } + date_time_set($start, 0, 0, 0); + $report->macros[$params['name']]['start'] = date_format($start, $params['format']); + + $end = date_create($report->macros[$params['name']][1]); + if (!$end) { + throw new Exception($params['display']." must have a valid end date."); + } + date_time_set($end, 23, 59, 59); + $report->macros[$params['name']]['end'] = date_format($end, $params['format']); + } + } + } + + public static function beforeRun(&$report) + { + foreach ($report->options['Variables'] as $var => $params) { + //if the type is date, parse with strtotime + if ($params['type'] === 'date' && $report->macros[$params['name']]) { + $time = strtotime($report->macros[$params['name']]); + if (!$time) { + throw new Exception($params['display']." must be a valid datetime value."); + } + + $report->macros[$params['name']] = date($params['format'], $time); + } + } + } } diff --git a/classes/headers/deprecated/CacheHeader.php b/classes/headers/deprecated/CacheHeader.php index fdc45248..9747f466 100644 --- a/classes/headers/deprecated/CacheHeader.php +++ b/classes/headers/deprecated/CacheHeader.php @@ -1,23 +1,26 @@ intval($value) - ); - } - //if cache is being turned off - else { - return array( - 'cache'=>0 - ); - } - } +class CacheHeader extends OptionsHeader +{ + public static function init($params, &$report) + { + trigger_error("CACHE header is deprecated. Use the OPTIONS header with the 'cache' parameter instead.", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + //if a cache ttl is being set + if (is_numeric($value)) { + return array( + 'cache' => intval($value), + ); + } + //if cache is being turned off + else { + return array( + 'cache' => 0, + ); + } + } } diff --git a/classes/headers/deprecated/CautionHeader.php b/classes/headers/deprecated/CautionHeader.php index 544427cb..79be1f1b 100644 --- a/classes/headers/deprecated/CautionHeader.php +++ b/classes/headers/deprecated/CautionHeader.php @@ -1,23 +1,26 @@ array( - 'required'=>true, - 'type'=>'string' - ) - ); - - public static function init($params, &$report) { - trigger_error("CAUTION header is deprecated.",E_USER_DEPRECATED); - - $report->options['Caution'] = $params['value']; - - $report->exportHeader('Caution',$params); - } - - public static function parseShortcut($value) { - return array( - 'value'=>$value - ); - } +class CautionHeader extends HeaderBase +{ + static $validation = array( + 'value' => array( + 'required' => true, + 'type' => 'string', + ), + ); + + public static function init($params, &$report) + { + trigger_error("CAUTION header is deprecated.", E_USER_DEPRECATED); + + $report->options['Caution'] = $params['value']; + + $report->exportHeader('Caution', $params); + } + + public static function parseShortcut($value) + { + return array( + 'value' => $value, + ); + } } diff --git a/classes/headers/deprecated/ColumnHeader.php b/classes/headers/deprecated/ColumnHeader.php index 83c7416d..d896df1a 100644 --- a/classes/headers/deprecated/ColumnHeader.php +++ b/classes/headers/deprecated/ColumnHeader.php @@ -1,8 +1,10 @@ $value - ); - } +class CreatedHeader extends InfoHeader +{ + public static function init($params, &$report) + { + trigger_error("CREATED header is deprecated. Use the INFO header with the 'created' parameter instead.", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + return array( + 'created' => $value, + ); + } } diff --git a/classes/headers/deprecated/DatabaseHeader.php b/classes/headers/deprecated/DatabaseHeader.php index caf6fe61..7fc846b6 100644 --- a/classes/headers/deprecated/DatabaseHeader.php +++ b/classes/headers/deprecated/DatabaseHeader.php @@ -1,14 +1,17 @@ report.")",E_USER_DEPRECATED); - - return parent::init($params, $report); - } - - public static function parseShortcut($value) { - return array( - 'database'=>trim($value) - ); - } +class DatabaseHeader extends OptionsHeader +{ + public static function init($params, &$report) + { + trigger_error("DATABASE header is deprecated. Use the OPTIONS header with the 'database' parameter instead. (".$report->report.")", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + return array( + 'database' => trim($value), + ); + } } diff --git a/classes/headers/deprecated/DescriptionHeader.php b/classes/headers/deprecated/DescriptionHeader.php index 57eea619..30c25643 100644 --- a/classes/headers/deprecated/DescriptionHeader.php +++ b/classes/headers/deprecated/DescriptionHeader.php @@ -1,14 +1,17 @@ $value - ); - } +class DescriptionHeader extends InfoHeader +{ + public static function init($params, &$report) + { + trigger_error("DESCRIPTION header is deprecated. Use the INFO header with the 'description' parameter instead.", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + return array( + 'description' => $value, + ); + } } diff --git a/classes/headers/deprecated/DetailHeader.php b/classes/headers/deprecated/DetailHeader.php index 817ec787..f0b36a22 100644 --- a/classes/headers/deprecated/DetailHeader.php +++ b/classes/headers/deprecated/DetailHeader.php @@ -1,73 +1,72 @@ array( - 'required'=>true, - 'type'=>'string' - ), - 'column'=>array( - 'required'=>true, - 'type'=>'string' - ), - 'macros'=>array( - 'type'=>'object' - ) - ); - - public static function init($params, &$report) { - trigger_error("DETAIL header is deprecated. Use the FILTER header with the 'drilldown' filter instead.",E_USER_DEPRECATED); - - $report->addFilter($params['column'],'drilldown',$params); - } - - public static function parseShortcut($value) { - $parts = explode(',',$value,3); - - if(count($parts) < 2) { - throw new Exception("Cannot parse DETAIL header '$value'"); - } - - $col = trim($parts[0]); - $report_name = trim($parts[1]); - - if(isset($parts[2])) { - $parts[2] = trim($parts[2]); - $macros = array(); - $temp = explode(',',$parts[2]); - foreach($temp as $macro) { - $macro = trim($macro); - if(strpos($macro,'=') !== false) { - list($key,$val) = explode('=',$macro,2); - $key = trim($key); - $val = trim($val); - - if(in_array($val[0],array('"',"'"))) { - $val = array( - 'constant'=>trim($val,'\'"') - ); - } - else { - $val = array( - 'column'=>$val - ); - } - - $macros[$key] = $val; - } - else { - $macros[$macro] = $macro; - } - } - - } - else { - $macros = array(); - } - - return array( - 'report'=>$report_name, - 'column'=>$col, - 'macros'=>$macros - ); - } +class DetailHeader extends HeaderBase +{ + static $validation = array( + 'report' => array( + 'required' => true, + 'type' => 'string', + ), + 'column' => array( + 'required' => true, + 'type' => 'string', + ), + 'macros' => array( + 'type' => 'object', + ), + ); + + public static function init($params, &$report) + { + trigger_error("DETAIL header is deprecated. Use the FILTER header with the 'drilldown' filter instead.", E_USER_DEPRECATED); + + $report->addFilter($params['column'], 'drilldown', $params); + } + + public static function parseShortcut($value) + { + $parts = explode(',', $value, 3); + + if (count($parts) < 2) { + throw new Exception("Cannot parse DETAIL header '$value'"); + } + + $col = trim($parts[0]); + $report_name = trim($parts[1]); + + if (isset($parts[2])) { + $parts[2] = trim($parts[2]); + $macros = array(); + $temp = explode(',', $parts[2]); + foreach ($temp as $macro) { + $macro = trim($macro); + if (strpos($macro, '=') !== false) { + list($key, $val) = explode('=', $macro, 2); + $key = trim($key); + $val = trim($val); + + if (in_array($val[0], array('"', "'"))) { + $val = array( + 'constant' => trim($val, '\'"'), + ); + } else { + $val = array( + 'column' => $val, + ); + } + + $macros[$key] = $val; + } else { + $macros[$macro] = $macro; + } + } + } else { + $macros = array(); + } + + return array( + 'report' => $report_name, + 'column' => $col, + 'macros' => $macros, + ); + } } diff --git a/classes/headers/deprecated/MongodatabaseHeader.php b/classes/headers/deprecated/MongodatabaseHeader.php index 75ea23d7..093e3502 100644 --- a/classes/headers/deprecated/MongodatabaseHeader.php +++ b/classes/headers/deprecated/MongodatabaseHeader.php @@ -1,14 +1,17 @@ report.")",E_USER_DEPRECATED); - - return parent::init($params, $report); - } - - public static function parseShortcut($value) { - return array( - 'mongodatabase'=>$value - ); - } +class MongodatabaseHeader extends OptionsHeader +{ + public static function init($params, &$report) + { + trigger_error("MONGODATABASE header is deprecated. Use the OPTIONS header with the 'mongodatabase' parameter instead. (".$report->report.")", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + return array( + 'mongodatabase' => $value, + ); + } } diff --git a/classes/headers/deprecated/NameHeader.php b/classes/headers/deprecated/NameHeader.php index b5cdc50b..5d65ebe2 100644 --- a/classes/headers/deprecated/NameHeader.php +++ b/classes/headers/deprecated/NameHeader.php @@ -1,14 +1,17 @@ $value - ); - } +class NameHeader extends InfoHeader +{ + public static function init($params, &$report) + { + trigger_error("NAME header is deprecated. Use the INFO header with the 'name' parameter instead.", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + return array( + 'name' => $value, + ); + } } diff --git a/classes/headers/deprecated/NoteHeader.php b/classes/headers/deprecated/NoteHeader.php index bb0452b0..fcc8da33 100644 --- a/classes/headers/deprecated/NoteHeader.php +++ b/classes/headers/deprecated/NoteHeader.php @@ -1,14 +1,17 @@ $value - ); - } +class NoteHeader extends InfoHeader +{ + public static function init($params, &$report) + { + trigger_error("NOTE header is deprecated. Use the INFO header with the 'note' parameter instead.", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + return array( + 'note' => $value, + ); + } } diff --git a/classes/headers/deprecated/OptionHeader.php b/classes/headers/deprecated/OptionHeader.php index 68ed4064..c8ea4bc5 100644 --- a/classes/headers/deprecated/OptionHeader.php +++ b/classes/headers/deprecated/OptionHeader.php @@ -1,8 +1,10 @@ $value - ); - } +class StatusHeader extends InfoHeader +{ + public static function init($params, &$report) + { + trigger_error("STATUS header is deprecated. Use the INFO header with the 'status' parameter instead.", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + return array( + 'status' => $value, + ); + } } diff --git a/classes/headers/deprecated/TotalHeader.php b/classes/headers/deprecated/TotalHeader.php index b2e3474e..42fe2936 100644 --- a/classes/headers/deprecated/TotalHeader.php +++ b/classes/headers/deprecated/TotalHeader.php @@ -1,6 +1,8 @@ array( - 'required'=>true, - 'type'=>'string' - ) - ); - - public static function init($params, &$report) { - trigger_error("TOTALS header is deprecated. Use the ROLLUP header instead.",E_USER_DEPRECATED); - } - - public static function parseShortcut($value) { - return array( - 'value'=>$value - ); - } +class TotalsHeader extends HeaderBase +{ + static $validation = array( + 'value' => array( + 'required' => true, + 'type' => 'string', + ), + ); + + public static function init($params, &$report) + { + trigger_error("TOTALS header is deprecated. Use the ROLLUP header instead.", E_USER_DEPRECATED); + } + + public static function parseShortcut($value) + { + return array( + 'value' => $value, + ); + } } diff --git a/classes/headers/deprecated/TypeHeader.php b/classes/headers/deprecated/TypeHeader.php index ad004b18..24dd4787 100644 --- a/classes/headers/deprecated/TypeHeader.php +++ b/classes/headers/deprecated/TypeHeader.php @@ -1,14 +1,17 @@ $value - ); - } +class TypeHeader extends InfoHeader +{ + public static function init($params, &$report) + { + trigger_error("TYPE header is deprecated. Use the INFO header with the 'type' parameter instead.", E_USER_DEPRECATED); + + return parent::init($params, $report); + } + + public static function parseShortcut($value) + { + return array( + 'type' => $value, + ); + } } diff --git a/classes/headers/deprecated/ValueHeader.php b/classes/headers/deprecated/ValueHeader.php index a2a9b689..6f5af655 100644 --- a/classes/headers/deprecated/ValueHeader.php +++ b/classes/headers/deprecated/ValueHeader.php @@ -1,42 +1,46 @@ array( - 'required'=>true, - 'type'=>'string' - ), - 'value'=>array( - 'required'=>true - ) - ); - - public static function init($params, &$report) { - trigger_error("VALUE header is deprecated. Use the VARIABLE header with a 'default' parameter instead.",E_USER_DEPRECATED); - - if(isset($report->options['Variables'][$params['name']])) { - if($report->macros[$params['name']]) return; - - $report->options['Variables'][$params['name']]['default'] = $params['value']; - $report->macros[$params['name']] = $params['value']; - - $report->exportHeader('Value',$params); - } - else { - throw new Exception("Providing value for unknown variable $params[name]"); - } - } - - public static function parseShortcut($value) { - if(strpos($value,',') === false) { - throw new Exception("Invalid value '$value'"); - } - list($name,$value) = explode(',',$value); - $var = trim($name); - $default = trim($value); - - return array( - 'name'=>$var, - 'value'=>$default - ); - } +class ValueHeader extends HeaderBase +{ + static $validation = array( + 'name' => array( + 'required' => true, + 'type' => 'string', + ), + 'value' => array( + 'required' => true, + ), + ); + + public static function init($params, &$report) + { + trigger_error("VALUE header is deprecated. Use the VARIABLE header with a 'default' parameter instead.", E_USER_DEPRECATED); + + if (isset($report->options['Variables'][$params['name']])) { + if ($report->macros[$params['name']]) { + return; + } + + $report->options['Variables'][$params['name']]['default'] = $params['value']; + $report->macros[$params['name']] = $params['value']; + + $report->exportHeader('Value', $params); + } else { + throw new Exception("Providing value for unknown variable $params[name]"); + } + } + + public static function parseShortcut($value) + { + if (strpos($value, ',') === false) { + throw new Exception("Invalid value '$value'"); + } + list($name, $value) = explode(',', $value); + $var = trim($name); + $default = trim($value); + + return array( + 'name' => $var, + 'value' => $default, + ); + } } diff --git a/classes/report_formats/ChartReportFormat.php b/classes/report_formats/ChartReportFormat.php index b018af47..06bab396 100644 --- a/classes/report_formats/ChartReportFormat.php +++ b/classes/report_formats/ChartReportFormat.php @@ -1,13 +1,17 @@ options['has_charts']) return; - - //always use cache for chart reports - //$report->use_cache = true; - - $result = $report->renderReportPage('html/chart_report'); - - echo $result; - } +class ChartReportFormat extends ReportFormatBase +{ + public static function display(&$report, &$request) + { + if (!$report->options['has_charts']) { + return; + } + + //always use cache for chart reports + //$report->use_cache = true; + + $result = $report->renderReportPage('html/chart_report'); + + echo $result; + } } diff --git a/classes/report_formats/CsvReportFormat.php b/classes/report_formats/CsvReportFormat.php index 61976b5a..975ef5ed 100644 --- a/classes/report_formats/CsvReportFormat.php +++ b/classes/report_formats/CsvReportFormat.php @@ -1,25 +1,32 @@ use_cache = true; - - $file_name = preg_replace(array('/[\s]+/','/[^0-9a-zA-Z\-_\.]/'),array('_',''),$report->options['Name']); - - header("Content-type: application/csv"); - header("Content-Disposition: attachment; filename=".$file_name.".csv"); - header("Pragma: no-cache"); - header("Expires: 0"); - - $i=0; - if(isset($_GET['dataset'])) $i = $_GET['dataset']; - elseif(isset($report->options['default_dataset'])) $i = $report->options['default_dataset']; - $i = intval($i); - - $data = $report->renderReportPage('csv/report',array( - 'dataset'=>$i - )); - - if(trim($data)) echo $data; - } +class CsvReportFormat extends ReportFormatBase +{ + public static function display(&$report, &$request) + { + //always use cache for CSV reports + $report->use_cache = true; + + $file_name = preg_replace(array('/[\s]+/', '/[^0-9a-zA-Z\-_\.]/'), array('_', ''), $report->options['Name']); + + header("Content-type: application/csv"); + header("Content-Disposition: attachment; filename=".$file_name.".csv"); + header("Pragma: no-cache"); + header("Expires: 0"); + + $i = 0; + if (isset($_GET['dataset'])) { + $i = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $i = $report->options['default_dataset']; + } + $i = intval($i); + + $data = $report->renderReportPage('csv/report', array( + 'dataset' => $i, + )); + + if (trim($data)) { + echo $data; + } + } } diff --git a/classes/report_formats/DebugReportFormat.php b/classes/report_formats/DebugReportFormat.php index 001c51a6..c75237fa 100644 --- a/classes/report_formats/DebugReportFormat.php +++ b/classes/report_formats/DebugReportFormat.php @@ -1,22 +1,24 @@ getRaw()."\n\n\n"; - $content .= "****************** Macros ******************\n\n".print_r($report->macros,true)."\n\n\n"; - $content .= "****************** All Report Options ******************\n\n".print_r($report->options,true)."\n\n\n"; - - if($report->is_ready) { - $report->run(); - - $content .= "****************** Generated Query ******************\n\n".print_r($report->options['Query'],true)."\n\n\n"; - - $content .= "****************** Report Rows ******************\n\n".print_r($report->options['DataSets'],true)."\n\n\n"; - } - - echo $content; - } +class DebugReportFormat extends ReportFormatBase +{ + public static function display(&$report, &$request) + { + header("Content-type: text/plain"); + header("Pragma: no-cache"); + header("Expires: 0"); + + $content = "****************** Raw Report File ******************\n\n".$report->getRaw()."\n\n\n"; + $content .= "****************** Macros ******************\n\n".print_r($report->macros, true)."\n\n\n"; + $content .= "****************** All Report Options ******************\n\n".print_r($report->options, true)."\n\n\n"; + + if ($report->is_ready) { + $report->run(); + + $content .= "****************** Generated Query ******************\n\n".print_r($report->options['Query'], true)."\n\n\n"; + + $content .= "****************** Report Rows ******************\n\n".print_r($report->options['DataSets'], true)."\n\n\n"; + } + + echo $content; + } } diff --git a/classes/report_formats/HtmlReportFormat.php b/classes/report_formats/HtmlReportFormat.php index 0d889d32..df8813a7 100644 --- a/classes/report_formats/HtmlReportFormat.php +++ b/classes/report_formats/HtmlReportFormat.php @@ -1,39 +1,42 @@ async = !isset($request->query['content_only']); - if(isset($request->query['no_async'])) $report->async = false; - - //if we're only getting the report content - if(isset($request->query['content_only'])) { - $template = 'html/content_only'; - } - else { - $template = 'html/report'; - } - - try { - $additional_vars = array(); - if(isset($request->query['no_charts'])) $additional_vars['no_charts'] = true; - - $html = $report->renderReportPage($template,$additional_vars); - echo $html; - } - catch(Exception $e) { - if(isset($request->query['content_only'])) { - $template = 'html/blank_page'; - } - - $vars = array( - 'title'=>$report->report, - 'header'=>'

There was an error running your report

', - 'error'=>$e->getMessage(), - 'content'=>"

Report Query

".$report->options['Query_Formatted'], - ); - - echo PhpReports::render($template, $vars); - } - } +class HtmlReportFormat extends ReportFormatBase +{ + public static function display(&$report, &$request) + { + //determine if this is an asyncronous report or not + $report->async = !isset($request->query['content_only']); + if (isset($request->query['no_async'])) { + $report->async = false; + } + + //if we're only getting the report content + if (isset($request->query['content_only'])) { + $template = 'html/content_only'; + } else { + $template = 'html/report'; + } + + try { + $additional_vars = array(); + if (isset($request->query['no_charts'])) { + $additional_vars['no_charts'] = true; + } + + $html = $report->renderReportPage($template, $additional_vars); + echo $html; + } catch (Exception $e) { + if (isset($request->query['content_only'])) { + $template = 'html/blank_page'; + } + + $vars = array( + 'title' => $report->report, + 'header' => '

There was an error running your report

', + 'error' => $e->getMessage(), + 'content' => "

Report Query

".$report->options['Query_Formatted'], + ); + + echo PhpReports::render($template, $vars); + } + } } diff --git a/classes/report_formats/JsonReportFormat.php b/classes/report_formats/JsonReportFormat.php index e700dfa8..f572359b 100644 --- a/classes/report_formats/JsonReportFormat.php +++ b/classes/report_formats/JsonReportFormat.php @@ -1,65 +1,71 @@ run(); - - if(!$report->options['DataSets']) return; +class JsonReportFormat extends ReportFormatBase +{ + public static function display(&$report, &$request) + { + header("Content-type: application/json"); + header("Pragma: no-cache"); + header("Expires: 0"); - $result = array(); - if(isset($_GET['datasets'])) { - $datasets = $_GET['datasets']; - // If all the datasets should be included - if($datasets === 'all') { - $datasets = array_keys($report->options['DataSets']); - } - // If just a single dataset was specified, make it an array - else if(!is_array($datasets)) { - $datasets = explode(',',$datasets); - } - - foreach($datasets as $i) { - $result[] = self::getDataSet($i, $report); - } - } - else { - $i=0; - if(isset($_GET['dataset'])) $i = $_GET['dataset']; - elseif(isset($report->options['default_dataset'])) $i = $report->options['default_dataset']; - $i = intval($i); - - $dataset = self::getDataSet($i, $report); - $result = $dataset['rows']; - } + //run the report + $report->run(); - if(defined('JSON_PRETTY_PRINT')) { - echo json_encode($result,JSON_PRETTY_PRINT); - } - else { - echo json_encode($result); - } - } - - public static function getDataSet($i, &$report) { - $dataset = array(); - foreach($report->options['DataSets'][$i] as $k=>$v) { - $dataset[$k] = $v; - } + if (!$report->options['DataSets']) { + return; + } - $rows = array(); - foreach($dataset['rows'] as $i=>$row) { - $tmp = array(); - foreach($row['values'] as $key=>$value){ - $tmp[$value->key] = $value->getValue(); - } - $rows[] = $tmp; - } - $dataset['rows'] = $rows; + $result = array(); + if (isset($_GET['datasets'])) { + $datasets = $_GET['datasets']; + // If all the datasets should be included + if ($datasets === 'all') { + $datasets = array_keys($report->options['DataSets']); + } + // If just a single dataset was specified, make it an array + elseif (!is_array($datasets)) { + $datasets = explode(',', $datasets); + } - return $dataset; - } + foreach ($datasets as $i) { + $result[] = self::getDataSet($i, $report); + } + } else { + $i = 0; + if (isset($_GET['dataset'])) { + $i = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $i = $report->options['default_dataset']; + } + $i = intval($i); + + $dataset = self::getDataSet($i, $report); + $result = $dataset['rows']; + } + + if (defined('JSON_PRETTY_PRINT')) { + echo json_encode($result, JSON_PRETTY_PRINT); + } else { + echo json_encode($result); + } + } + + public static function getDataSet($i, &$report) + { + $dataset = array(); + foreach ($report->options['DataSets'][$i] as $k => $v) { + $dataset[$k] = $v; + } + + $rows = array(); + foreach ($dataset['rows'] as $i => $row) { + $tmp = array(); + foreach ($row['values'] as $key => $value) { + $tmp[$value->key] = $value->getValue(); + } + $rows[] = $tmp; + } + $dataset['rows'] = $rows; + + return $dataset; + } } diff --git a/classes/report_formats/RawReportFormat.php b/classes/report_formats/RawReportFormat.php index cfd9d50d..fc9f69c7 100644 --- a/classes/report_formats/RawReportFormat.php +++ b/classes/report_formats/RawReportFormat.php @@ -1,17 +1,20 @@ renderReportPage('sql/report'); - } +class SqlReportFormat extends ReportFormatBase +{ + public static function display(&$report, &$request) + { + header("Content-type: text/plain"); + header("Pragma: no-cache"); + header("Expires: 0"); + + echo $report->renderReportPage('sql/report'); + } } diff --git a/classes/report_formats/TableReportFormat.php b/classes/report_formats/TableReportFormat.php index c132a983..a38fa4d5 100644 --- a/classes/report_formats/TableReportFormat.php +++ b/classes/report_formats/TableReportFormat.php @@ -1,16 +1,15 @@ options['inline_email'] = true; - $report->use_cache = true; - - try { - $html = $report->renderReportPage('html/table'); - echo $html; - } - catch(Exception $e) { - - } - } +class TableReportFormat extends ReportFormatBase +{ + public static function display(&$report, &$request) + { + $report->options['inline_email'] = true; + $report->use_cache = true; + + try { + $html = $report->renderReportPage('html/table'); + echo $html; + } catch (Exception $e) { + } + } } diff --git a/classes/report_formats/TextReportFormat.php b/classes/report_formats/TextReportFormat.php index 8ce8d2ad..8f5754df 100644 --- a/classes/report_formats/TextReportFormat.php +++ b/classes/report_formats/TextReportFormat.php @@ -1,92 +1,101 @@ use_cache = true; - - //run the report - $report->run(); - - if(!$report->options['DataSets']) return; - - foreach($report->options['DataSets'] as $i=>$dataset) { - if(isset($dataset['title'])) echo $dataset['title']."\n"; - TextReportFormat::displayDataSet($dataset); - - // If this isn't the last dataset, add some spacing - if($i < count($report->options['DataSets'])-1) { - echo "\n\n"; - } - } +class TextReportFormat extends ReportFormatBase +{ + public static function display(&$report, &$request) + { + header("Content-type: text/plain"); + header("Pragma: no-cache"); + header("Expires: 0"); + + $report->use_cache = true; + + //run the report + $report->run(); + + if (!$report->options['DataSets']) { + return; + } + + foreach ($report->options['DataSets'] as $i => $dataset) { + if (isset($dataset['title'])) { + echo $dataset['title']."\n"; + } + TextReportFormat::displayDataSet($dataset); + + // If this isn't the last dataset, add some spacing + if ($i < count($report->options['DataSets'])-1) { + echo "\n\n"; + } + } + } + + protected static function displayDataSet($dataset) + { + /** + * This code taken from Stack Overflow answer by ehudokai + * http://stackoverflow.com/a/4597190 + */ + + //first get your sizes + $sizes = array(); + $first_row = $dataset['rows'][0]; + foreach ($first_row['values'] as $key => $value) { + $key = $value->key; + $value = $value->getValue(); + + //initialize to the size of the column name + $sizes[$key] = strlen($key); + } + foreach ($dataset['rows'] as $row) { + foreach ($row['values'] as $key => $value) { + $key = $value->key; + $value = $value->getValue(); + + $length = strlen($value); + if ($length > $sizes[$key]) { + $sizes[$key] = $length; + } // get largest result size + } + } + + //top of output + foreach ($sizes as $length) { + echo "+".str_pad("", $length+2, "-"); + } + echo "+\n"; + + // column names + foreach ($first_row['values'] as $key => $value) { + $key = $value->key; + $value = $value->getValue(); + + echo "| "; + echo str_pad($key, $sizes[$key]+1); + } + echo "|\n"; + + //line under column names + foreach ($sizes as $length) { + echo "+".str_pad("", $length+2, "-"); + } + echo "+\n"; + + //output data + foreach ($dataset['rows'] as $row) { + foreach ($row['values'] as $key => $value) { + $key = $value->key; + $value = $value->getValue(); + + echo "| "; + echo str_pad($value, $sizes[$key]+1); + } + echo "|\n"; + } + + //bottom of output + foreach ($sizes as $length) { + echo "+".str_pad("", $length+2, "-"); + } + echo "+\n"; } - - protected static function displayDataSet($dataset) { - /** - * This code taken from Stack Overflow answer by ehudokai - * http://stackoverflow.com/a/4597190 - */ - - //first get your sizes - $sizes = array(); - $first_row = $dataset['rows'][0]; - foreach($first_row['values'] as $key=>$value){ - $key = $value->key; - $value = $value->getValue(); - - //initialize to the size of the column name - $sizes[$key] = strlen($key); - } - foreach($dataset['rows'] as $row) { - foreach($row['values'] as $key=>$value){ - $key = $value->key; - $value = $value->getValue(); - - $length = strlen($value); - if($length > $sizes[$key]) $sizes[$key] = $length; // get largest result size - } - } - - //top of output - foreach($sizes as $length){ - echo "+".str_pad("",$length+2,"-"); - } - echo "+\n"; - - // column names - foreach($first_row['values'] as $key=>$value){ - $key = $value->key; - $value = $value->getValue(); - - echo "| "; - echo str_pad($key,$sizes[$key]+1); - } - echo "|\n"; - - //line under column names - foreach($sizes as $length){ - echo "+".str_pad("",$length+2,"-"); - } - echo "+\n"; - - //output data - foreach($dataset['rows'] as $row) { - foreach($row['values'] as $key=>$value){ - $key = $value->key; - $value = $value->getValue(); - - echo "| "; - echo str_pad($value,$sizes[$key]+1); - } - echo "|\n"; - } - - //bottom of output - foreach($sizes as $length){ - echo "+".str_pad("",$length+2,"-"); - } - echo "+\n"; - } } diff --git a/classes/report_formats/XlsReportBase.php b/classes/report_formats/XlsReportBase.php index 720e40b2..3cf56292 100644 --- a/classes/report_formats/XlsReportBase.php +++ b/classes/report_formats/XlsReportBase.php @@ -1,68 +1,76 @@ -getProperties()->setCreator("PHP-Reports") - ->setLastModifiedBy("PHP-Reports") - ->setTitle("") - ->setSubject("") - ->setDescription(""); - - foreach($report->options['DataSets'] as $i=>$dataset) { - $objPHPExcel->createSheet($i); - self::addSheet($objPHPExcel,$dataset,$i); - } - - // Set the active sheet to the first one - $objPHPExcel->setActiveSheetIndex(0); - - return $objPHPExcel; - } - public static function addSheet($objPHPExcel,$dataset, $i) { - $rows = array(); - $row = array(); - $cols = 0; - $first_row = $dataset['rows'][0]; - foreach($first_row['values'] as $key=>$value){ - array_push($row, $value->key); - $cols++; - } - array_push($rows, $row); - $row = array(); - - foreach($dataset['rows'] as $r) { - foreach($r['values'] as $key=>$value){ - array_push($row, $value->getValue()); - } - array_push($rows, $row); - $row = array(); - } - - $objPHPExcel->setActiveSheetIndex($i)->fromArray($rows, NULL, 'A1'); - $objPHPExcel->getActiveSheet()->setAutoFilter('A1:'.self::columnLetter($cols).count($rows)); - for ($a = 1; $a <= $cols; $a++) { - $objPHPExcel->getActiveSheet()->getColumnDimension(self::columnLetter($a))->setAutoSize(true); - } - - if(isset($dataset['title'])) $objPHPExcel->getActiveSheet()->setTitle($dataset['title']); - - return $objPHPExcel; - } -} +getProperties()->setCreator("PHP-Reports") + ->setLastModifiedBy("PHP-Reports") + ->setTitle("") + ->setSubject("") + ->setDescription(""); + + foreach ($report->options['DataSets'] as $i => $dataset) { + $objPHPExcel->createSheet($i); + self::addSheet($objPHPExcel, $dataset, $i); + } + + // Set the active sheet to the first one + $objPHPExcel->setActiveSheetIndex(0); + + return $objPHPExcel; + } + public static function addSheet($objPHPExcel, $dataset, $i) + { + $rows = array(); + $row = array(); + $cols = 0; + $first_row = $dataset['rows'][0]; + foreach ($first_row['values'] as $key => $value) { + array_push($row, $value->key); + $cols++; + } + array_push($rows, $row); + $row = array(); + + foreach ($dataset['rows'] as $r) { + foreach ($r['values'] as $key => $value) { + array_push($row, $value->getValue()); + } + array_push($rows, $row); + $row = array(); + } + + $objPHPExcel->setActiveSheetIndex($i)->fromArray($rows, null, 'A1'); + $objPHPExcel->getActiveSheet()->setAutoFilter('A1:'.self::columnLetter($cols).count($rows)); + for ($a = 1; $a <= $cols; $a++) { + $objPHPExcel->getActiveSheet()->getColumnDimension(self::columnLetter($a))->setAutoSize(true); + } + + if (isset($dataset['title'])) { + $objPHPExcel->getActiveSheet()->setTitle($dataset['title']); + } + + return $objPHPExcel; + } +} diff --git a/classes/report_formats/XlsReportFormat.php b/classes/report_formats/XlsReportFormat.php index dbebf96d..828e1a53 100644 --- a/classes/report_formats/XlsReportFormat.php +++ b/classes/report_formats/XlsReportFormat.php @@ -1,26 +1,30 @@ options['Name']); +class XlsReportFormat extends XlsReportBase +{ + public static function display(&$report, &$request) + { + // First let set up some headers + $file_name = preg_replace(array('/[\s]+/', '/[^0-9a-zA-Z\-_\.]/'), array('_', ''), $report->options['Name']); - //always use cache for Excel reports - $report->use_cache = true; + //always use cache for Excel reports + $report->use_cache = true; - //run the report - $report->run(); + //run the report + $report->run(); - if(!$report->options['DataSets']) return; + if (!$report->options['DataSets']) { + return; + } - $objPHPExcel = parent::getExcelRepresantation($report); + $objPHPExcel = parent::getExcelRepresantation($report); - $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5'); - - header('Content-Type: application/vnd.ms-excel'); - header('Content-Disposition: attachment;filename="'.$file_name.'.xls"'); - header('Pragma: no-cache'); - header('Expires: 0'); - - $objWriter->save('php://output'); - } + $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5'); + + header('Content-Type: application/vnd.ms-excel'); + header('Content-Disposition: attachment;filename="'.$file_name.'.xls"'); + header('Pragma: no-cache'); + header('Expires: 0'); + + $objWriter->save('php://output'); + } } diff --git a/classes/report_formats/XlsxReportFormat.php b/classes/report_formats/XlsxReportFormat.php index 618dcf98..295b1d31 100644 --- a/classes/report_formats/XlsxReportFormat.php +++ b/classes/report_formats/XlsxReportFormat.php @@ -1,26 +1,30 @@ options['Name']); +class XlsxReportFormat extends XlsReportBase +{ + public static function display(&$report, &$request) + { + // First let set up some headers + $file_name = preg_replace(array('/[\s]+/', '/[^0-9a-zA-Z\-_\.]/'), array('_', ''), $report->options['Name']); - //always use cache for Excel reports - $report->use_cache = true; + //always use cache for Excel reports + $report->use_cache = true; - //run the report - $report->run(); + //run the report + $report->run(); - if(!$report->options['DataSets']) return; + if (!$report->options['DataSets']) { + return; + } - $objPHPExcel = parent::getExcelRepresantation($report); + $objPHPExcel = parent::getExcelRepresantation($report); - $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); - - header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - header('Content-Disposition: attachment;filename="'.$file_name.'.xlsx"'); - header('Pragma: no-cache'); - header('Expires: 0'); - - $objWriter->save('php://output'); - } + $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); + + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="'.$file_name.'.xlsx"'); + header('Pragma: no-cache'); + header('Expires: 0'); + + $objWriter->save('php://output'); + } } diff --git a/classes/report_formats/XmlReportFormat.php b/classes/report_formats/XmlReportFormat.php index 54e869ec..17e652e4 100644 --- a/classes/report_formats/XmlReportFormat.php +++ b/classes/report_formats/XmlReportFormat.php @@ -1,37 +1,41 @@ options['DataSets']); - } - // If just a single dataset was specified, make it an array - else if(!is_array($datasets)) { - $datasets = explode(',',$datasets); - } - } - else { - $i=0; - if(isset($_GET['dataset'])) $i = $_GET['dataset']; - elseif(isset($report->options['default_dataset'])) $i = $report->options['default_dataset']; - $i = intval($i); - - $datasets = array($i); - } + $datasets = array(); + $dataset_format = false; - echo $report->renderReportPage('xml/report',array( - 'datasets'=>$datasets, - 'dataset_format'=>$dataset_format - )); - } + if (isset($_GET['datasets'])) { + $dataset_format = true; + $datasets = $_GET['datasets']; + // If all the datasets should be included + if ($datasets === 'all') { + $datasets = array_keys($report->options['DataSets']); + } + // If just a single dataset was specified, make it an array + elseif (!is_array($datasets)) { + $datasets = explode(',', $datasets); + } + } else { + $i = 0; + if (isset($_GET['dataset'])) { + $i = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $i = $report->options['default_dataset']; + } + $i = intval($i); + + $datasets = array($i); + } + + echo $report->renderReportPage('xml/report', array( + 'datasets' => $datasets, + 'dataset_format' => $dataset_format, + )); + } } diff --git a/classes/report_types/AdoPivotReportType.php b/classes/report_types/AdoPivotReportType.php index ed1b4589..258a6ba2 100644 --- a/classes/report_types/AdoPivotReportType.php +++ b/classes/report_types/AdoPivotReportType.php @@ -1,133 +1,146 @@ options['Environment']][$report->options['Database']])) { - throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); - } - //make sure the syntax highlighting is using the proper class - SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; +class AdoPivotReportType extends ReportTypeBase +{ + public static function init(&$report) + { + $environments = PhpReports::$config['environments']; + + if (!isset($environments[$report->options['Environment']][$report->options['Database']])) { + throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); + } + + //make sure the syntax highlighting is using the proper class + SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; //set a formatted query here for debugging. It will be overwritten below after macros are substituted. $report->options['Query_Formatted'] = "
".$report->raw_query."
"; $object = spyc_load($report->raw_query); - $report->raw_query = array(); - //if there are any included reports, add the report sql to the top - if(isset($report->options['Includes'])) { - $included_sql = ''; - foreach($report->options['Includes'] as &$included_report) { - $included_sql .= trim($included_report->raw_query)."\n"; - } + $report->raw_query = []; + //if there are any included reports, add the report sql to the top + if (isset($report->options['Includes'])) { + $included_sql = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_sql .= trim($included_report->raw_query)."\n"; + } if (strlen($included_sql) > 0) { - $report->raw_query[] = $included_sql; + $report->raw_query[] = $included_sql; } - } + } $report->raw_query[] = $object; - } - - public static function openConnection(&$report) { - if(isset($report->conn)) return; - - $environments = PhpReports::$config['environments']; - $config = $environments[$report->options['Environment']][$report->options['Database']]; - - if(!($report->conn = ADONewConnection($config['uri']))) { - throw new Exception('Could not connect to the database'); - } - } - - public static function closeConnection(&$report) { - if (!isset($report->conn)) return; - if ($report->conn->IsConnected()) { - $report->conn->Close(); - } - unset($report->conn); - } - - public static function getVariableOptions($params, &$report) { + } + + public static function openConnection(&$report) + { + if (isset($report->conn)) { + return; + } + + $environments = PhpReports::$config['environments']; + $config = $environments[$report->options['Environment']][$report->options['Database']]; + + if (!($report->conn = ADONewConnection($config['uri']))) { + throw new Exception('Could not connect to the database'); + } + } + + public static function closeConnection(&$report) + { + if (!isset($report->conn)) { + return; + } + if ($report->conn->IsConnected()) { + $report->conn->Close(); + } + unset($report->conn); + } + + public static function getVariableOptions($params, &$report) + { $report->conn->SetFetchMode(ADODB_FETCH_NUM); $query = 'SELECT DISTINCT '.$params['column'].' FROM '.$params['table']; - - if(isset($params['where'])) { - $query .= ' WHERE '.$params['where']; - } + + if (isset($params['where'])) { + $query .= ' WHERE '.$params['where']; + } $macros = $report->macros; - foreach($macros as $key=>$value) { - if(is_array($value)) { - foreach($value as $key2=>$value2) { - $value[$key2] = mysql_real_escape_string(trim($value2)); + foreach ($macros as $key => $value) { + if (is_array($value)) { + foreach ($value as $key2 => $value2) { + $value[$key2] = trim($value2); } $macros[$key] = $value; - } - else { - $macros[$key] = mysql_real_escape_string($value); + } else { + $macros[$key] = $value; } - if($value === 'ALL') $macros[$key.'_all'] = true; + if ($value === 'ALL') { + $macros[$key.'_all'] = true; + } } //add the config and environment settings as macros $macros['config'] = PhpReports::$config; $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; - $result = $report->conn->Execute(PhpReports::renderString($query, $macros)); - - if (!$result) { - throw new Exception("Unable to get variable options: ".$report->conn->ErrorMsg()); - } + $result = $report->conn->Execute(PhpReports::renderString($query, $macros)); + + if (!$result) { + throw new Exception("Unable to get variable options: ".$report->conn->ErrorMsg()); + } + + $options = array(); - $options = array(); - - if(isset($params['all']) && $params['all']) { + if (isset($params['all']) && $params['all']) { $options[] = 'ALL'; } while ($row = $result->FetchRow()) { if ($result->FieldCount() > 1) { - $options[] = array('display'=>$row[0], 'value'=>$row[1]); + $options[] = ['display' => $row[0], 'value' => $row[1]]; } else { $options[] = $row[0]; } } return $options; - } - - public static function run(&$report) { + } + + public static function run(&$report) + { $report->conn->SetFetchMode(ADODB_FETCH_ASSOC); - $rows = array(); - - $macros = $report->macros; - foreach($macros as $key=>$value) { - if(is_array($value)) { - $first = true; - foreach($value as $key2=>$value2) { - $value[$key2] = mysql_real_escape_string(trim($value2)); - $first = false; - } - $macros[$key] = $value; - } - else { - $macros[$key] = mysql_real_escape_string($value); - } - - if($value === 'ALL') $macros[$key.'_all'] = true; - } - - //add the config and environment settings as macros - $macros['config'] = PhpReports::$config; - $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; + $rows = []; + + $macros = $report->macros; + foreach ($macros as $key => $value) { + if (is_array($value)) { + $first = true; + foreach ($value as $key2 => $value2) { + $value[$key2] = mysql_real_escape_string(trim($value2)); + $first = false; + } + $macros[$key] = $value; + } else { + $macros[$key] = mysql_real_escape_string($value); + } + + if ($value === 'ALL') { + $macros[$key.'_all'] = true; + } + } + + //add the config and environment settings as macros + $macros['config'] = PhpReports::$config; + $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; $raw_sql = ""; foreach ($report->raw_query as $qry) { if (is_array($qry)) { - foreach ($qry as $key=>$value) { + foreach ($qry as $key => $value) { // TODO handle arrays better if (!is_bool($value) && !is_array($value)) { $qry[$key] = PhpReports::renderString($value, $macros); @@ -150,20 +163,22 @@ public static function run(&$report) { //split into individual queries and run each one, saving the last result $queries = SqlFormatter::splitQuery($sql); - foreach($queries as $query) { + foreach ($queries as $query) { if (!is_array($query)) { //skip empty queries $query = trim($query); - if(!$query) continue; + if (!$query) { + continue; + } $result = $report->conn->Execute($query); - if(!$result) { + if (!$result) { throw new Exception("Query failed: ".$report->conn->ErrorMsg()); } //if this query had an assert=empty flag and returned results, throw error - if(preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/',$query)) { - if($result->GetAssoc()) { + if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { + if ($result->GetAssoc()) { throw new Exception("Assert failed. Query did not return empty results."); } } @@ -171,5 +186,5 @@ public static function run(&$report) { } return $result->GetArray(); - } + } } diff --git a/classes/report_types/AdoReportType.php b/classes/report_types/AdoReportType.php index 63bfb1f8..e0390565 100644 --- a/classes/report_types/AdoReportType.php +++ b/classes/report_types/AdoReportType.php @@ -1,159 +1,172 @@ options['Environment']][$report->options['Database']])) { - throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); - } - - //make sure the syntax highlighting is using the proper class - SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; - - //default host macro to mysql's host if it isn't defined elsewhere - //if(!isset($report->macros['host'])) $report->macros['host'] = $mysql['host']; - - //replace legacy shorthand macro format - foreach($report->macros as $key=>$value) { - $params = array(); - if(isset($report->options['Variables'][$key])) { - $params = $report->options['Variables'][$key]; - } - - //macros shortcuts for arrays - if(isset($params['multiple']) && $params['multiple']) { - //allow {macro} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} - //this is shorthand for comma separated list - $report->raw_query = preg_replace('/([^\{])\{'.$key.'\}([^\}])/','$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}\'{{ item }}\'{% endfor %}$2',$report->raw_query); - - //allow {(macro)} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} - //this is shorthand for quoted, comma separated list - $report->raw_query = preg_replace('/([^\{])\{\('.$key.'\)\}([^\}])/','$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}(\'{{ item }}\'){% endfor %}$2',$report->raw_query); - } - //macros sortcuts for non-arrays - else { - //allow {macro} instead of {{macro}} for legacy support - $report->raw_query = preg_replace('/([^\{])(\{'.$key.'+\})([^\}])/','$1{$2}$3',$report->raw_query); - } - } - - //if there are any included reports, add the report sql to the top - if(isset($report->options['Includes'])) { - $included_sql = ''; - foreach($report->options['Includes'] as &$included_report) { - $included_sql .= trim($included_report->raw_query)."\n"; - } - - $report->raw_query = $included_sql . $report->raw_query; - } - - //set a formatted query here for debugging. It will be overwritten below after macros are substituted. - $report->options['Query_Formatted'] = SqlFormatter::format($report->raw_query); - } - - public static function openConnection(&$report) { - if(isset($report->conn)) return; - - $environments = PhpReports::$config['environments']; - $config = $environments[$report->options['Environment']][$report->options['Database']]; - - if(!($report->conn = ADONewConnection($config['uri']))) { - throw new Exception('Could not connect to the database'); - } - } - - public static function closeConnection(&$report) { - if (!isset($report->conn)) return; - if ($report->conn->IsConnected()) { - $report->conn->Close(); - } - unset($report->conn); - } - - public static function getVariableOptions($params, &$report) { + +class AdoReportType extends ReportTypeBase +{ + public static function init(&$report) + { + $environments = PhpReports::$config['environments']; + + if (!isset($environments[$report->options['Environment']][$report->options['Database']])) { + throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); + } + + //make sure the syntax highlighting is using the proper class + SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; + + //default host macro to mysql's host if it isn't defined elsewhere + //if(!isset($report->macros['host'])) $report->macros['host'] = $mysql['host']; + + //replace legacy shorthand macro format + foreach ($report->macros as $key => $value) { + $params = []; + if (isset($report->options['Variables'][$key])) { + $params = $report->options['Variables'][$key]; + } + + //macros shortcuts for arrays + if (isset($params['multiple']) && $params['multiple']) { + //allow {macro} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} + //this is shorthand for comma separated list + $report->raw_query = preg_replace('/([^\{])\{'.$key.'\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}\'{{ item }}\'{% endfor %}$2', $report->raw_query); + + //allow {(macro)} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} + //this is shorthand for quoted, comma separated list + $report->raw_query = preg_replace('/([^\{])\{\('.$key.'\)\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}(\'{{ item }}\'){% endfor %}$2', $report->raw_query); + } else { + //macros sortcuts for non-arrays + //allow {macro} instead of {{macro}} for legacy support + $report->raw_query = preg_replace('/([^\{])(\{'.$key.'+\})([^\}])/', '$1{$2}$3', $report->raw_query); + } + } + + //if there are any included reports, add the report sql to the top + if (isset($report->options['Includes'])) { + $included_sql = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_sql .= trim($included_report->raw_query)."\n"; + } + + $report->raw_query = $included_sql.$report->raw_query; + } + + //set a formatted query here for debugging. It will be overwritten below after macros are substituted. + $report->options['Query_Formatted'] = SqlFormatter::format($report->raw_query); + } + + public static function openConnection(&$report) + { + if (isset($report->conn)) { + return; + } + + $environments = PhpReports::$config['environments']; + $config = $environments[$report->options['Environment']][$report->options['Database']]; + + if (!($report->conn = ADONewConnection($config['uri']))) { + throw new Exception('Could not connect to the database'); + } + } + + public static function closeConnection(&$report) + { + if (!isset($report->conn)) { + return; + } + if ($report->conn->IsConnected()) { + $report->conn->Close(); + } + unset($report->conn); + } + + public static function getVariableOptions($params, &$report) + { $report->conn->SetFetchMode(ADODB_FETCH_NUM); $query = 'SELECT DISTINCT '.$params['column'].' FROM '.$params['table']; - - if(isset($params['where'])) { - $query .= ' WHERE '.$params['where']; - } - - $result = $report->conn->Execute($query); - - if (!$result) { - throw new Exception("Unable to get variable options: ".$report->conn->ErrorMsg()); - } - - $options = array(); - - if(isset($params['all']) && $params['all']) { + + if (isset($params['where'])) { + $query .= ' WHERE '.$params['where']; + } + + $result = $report->conn->Execute($query); + + if (!$result) { + throw new Exception("Unable to get variable options: ".$report->conn->ErrorMsg()); + } + + $options = []; + + if (isset($params['all']) && $params['all']) { $options[] = 'ALL'; } while ($row = $result->FetchRow()) { if ($result->FieldCount() > 1) { - $options[] = array('display'=>$row[0], 'value'=>$row[1]); + $options[] = ['display' => $row[0], 'value' => $row[1]]; } else { $options[] = $row[0]; } } return $options; - } - - public static function run(&$report) { + } + + public static function run(&$report) + { $report->conn->SetFetchMode(ADODB_FETCH_ASSOC); - $rows = array(); - - $macros = $report->macros; - foreach($macros as $key=>$value) { - if(is_array($value)) { - $first = true; - foreach($value as $key2=>$value2) { - $value[$key2] = mysql_real_escape_string(trim($value2)); - $first = false; - } - $macros[$key] = $value; - } - else { - $macros[$key] = mysql_real_escape_string($value); - } - - if($value === 'ALL') $macros[$key.'_all'] = true; - } - - //add the config and environment settings as macros - $macros['config'] = PhpReports::$config; - $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; - - //expand macros in query - $sql = PhpReports::render($report->raw_query,$macros); - - $report->options['Query'] = $sql; - - $report->options['Query_Formatted'] = SqlFormatter::format($sql); - - //split into individual queries and run each one, saving the last result - $queries = SqlFormatter::splitQuery($sql); - - foreach($queries as $query) { - //skip empty queries - $query = trim($query); - if(!$query) continue; - - $result = $report->conn->Execute($query); - if(!$result) { - throw new Exception("Query failed: ".$report->conn->ErrorMsg()); - } - - //if this query had an assert=empty flag and returned results, throw error - if(preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/',$query)) { - if($result->GetAssoc()) { - throw new Exception("Assert failed. Query did not return empty results."); - } - } - } - - return $result->GetArray(); - } -} \ No newline at end of file + $rows = []; + + $macros = $report->macros; + foreach ($macros as $key => $value) { + if (is_array($value)) { + $first = true; + foreach ($value as $key2 => $value2) { + $value[$key2] = mysql_real_escape_string(trim($value2)); + $first = false; + } + $macros[$key] = $value; + } else { + $macros[$key] = mysql_real_escape_string($value); + } + + if ($value === 'ALL') { + $macros[$key.'_all'] = true; + } + } + + //add the config and environment settings as macros + $macros['config'] = PhpReports::$config; + $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; + + //expand macros in query + $sql = PhpReports::render($report->raw_query, $macros); + + $report->options['Query'] = $sql; + + $report->options['Query_Formatted'] = SqlFormatter::format($sql); + + //split into individual queries and run each one, saving the last result + $queries = SqlFormatter::splitQuery($sql); + + foreach ($queries as $query) { + //skip empty queries + $query = trim($query); + if (!$query) { + continue; + } + + $result = $report->conn->Execute($query); + if (!$result) { + throw new Exception("Query failed: ".$report->conn->ErrorMsg()); + } + + //if this query had an assert=empty flag and returned results, throw error + if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { + if ($result->GetAssoc()) { + throw new Exception("Assert failed. Query did not return empty results."); + } + } + } + + return $result->GetArray(); + } +} diff --git a/classes/report_types/MongoReportType.php b/classes/report_types/MongoReportType.php index 0a30d4f5..3e10bfac 100644 --- a/classes/report_types/MongoReportType.php +++ b/classes/report_types/MongoReportType.php @@ -1,77 +1,83 @@ options['Environment']][$report->options['Database']])) { - throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); - } - - $mongo = $environments[$report->options['Environment']][$report->options['Database']]; - - //default host macro to mysql's host if it isn't defined elsewhere - if(!isset($report->macros['host'])) $report->macros['host'] = $mongo['host']; - - //if there are any included reports, add it to the top of the raw query - if(isset($report->options['Includes'])) { - $included_code = ''; - foreach($report->options['Includes'] as &$included_report) { - $included_code .= trim($included_report->raw_query)."\n"; - } - - $report->raw_query = $included_code . $report->raw_query; - } - } - - public static function openConnection(&$report) { - - } - - public static function closeConnection(&$report) { - - } - - public static function run(&$report) { - $eval = ''; - foreach($report->macros as $key=>$value) { - if(is_array($value)) { - $value = json_encode($value); - } - else { - $value = '"'.addslashes($value).'"'; - } - - $eval .= 'var '.$key.' = '.$value.';'."\n"; - } - $eval .= $report->raw_query; - - $environments = PhpReports::$config['environments']; - $config = $environments[$report->options['Environment']][$report->options['Database']]; - - $mongo_database = isset($report->options['Mongodatabase'])? $report->options['Mongodatabase'] : ''; - - //command without eval string - $command = 'mongo '.$config['host'].':'.$config['port'].'/'.$mongo_database.' --quiet --eval '; - - //easy to read formatted query - $report->options['Query_Formatted'] = '
+class MongoReportType extends ReportTypeBase +{ + public static function init(&$report) + { + $environments = PhpReports::$config['environments']; + + if (!isset($environments[$report->options['Environment']][$report->options['Database']])) { + throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); + } + + $mongo = $environments[$report->options['Environment']][$report->options['Database']]; + + //default host macro to mysql's host if it isn't defined elsewhere + if (!isset($report->macros['host'])) { + $report->macros['host'] = $mongo['host']; + } + + //if there are any included reports, add it to the top of the raw query + if (isset($report->options['Includes'])) { + $included_code = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_code .= trim($included_report->raw_query)."\n"; + } + + $report->raw_query = $included_code.$report->raw_query; + } + } + + public static function openConnection(&$report) + { + } + + public static function closeConnection(&$report) + { + } + + public static function run(&$report) + { + $eval = ''; + foreach ($report->macros as $key => $value) { + if (is_array($value)) { + $value = json_encode($value); + } else { + $value = '"'.addslashes($value).'"'; + } + + $eval .= 'var '.$key.' = '.$value.';'."\n"; + } + $eval .= $report->raw_query; + + $environments = PhpReports::$config['environments']; + $config = $environments[$report->options['Environment']][$report->options['Database']]; + + $mongo_database = isset($report->options['Mongodatabase']) ? $report->options['Mongodatabase'] : ''; + + //command without eval string + $command = 'mongo '.$config['host'].':'.$config['port'].'/'.$mongo_database.' --quiet --eval '; + + //easy to read formatted query + $report->options['Query_Formatted'] = '
$ '.$command.'"..."
'. - 'Eval String:'. - '
'.htmlentities($eval).'
+ 'Eval String:'. + '
'.htmlentities($eval).'
'; - //escape the eval string and add it to the command - $command .= escapeshellarg($eval); - $report->options['Query'] = '$ '.$command; - - //include stderr so we can capture shell errors (like "command mongo not found") - $result = shell_exec($command.' 2>&1'); - - $result = trim($result); - - $json = json_decode($result, true); - if($json === NULL) throw new Exception($result); - - return $json; - } + //escape the eval string and add it to the command + $command .= escapeshellarg($eval); + $report->options['Query'] = '$ '.$command; + + //include stderr so we can capture shell errors (like "command mongo not found") + $result = shell_exec($command.' 2>&1'); + + $result = trim($result); + + $json = json_decode($result, true); + if ($json === NULL) { + throw new Exception($result); + } + + return $json; + } } diff --git a/classes/report_types/MysqlReportType.php b/classes/report_types/MysqlReportType.php index c13ec39b..32f8c5c9 100644 --- a/classes/report_types/MysqlReportType.php +++ b/classes/report_types/MysqlReportType.php @@ -1,4 +1,6 @@ options['Environment']][$report->options['Database']])) { - throw new Exception("No ".$report->options['Database']." info defined for environment '".$report->options['Environment']."'"); - } - - //make sure the syntax highlighting is using the proper class - SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; - - //replace legacy shorthand macro format - foreach($report->macros as $key=>$value) { - if(isset($report->options['Variables'][$key])) { - $params = $report->options['Variables'][$key]; - } - else { - $params = array(); - } - - //macros shortcuts for arrays - if(isset($params['multiple']) && $params['multiple']) { - //allow {macro} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} - //this is shorthand for comma separated list - $report->raw_query = preg_replace('/([^\{])\{'.$key.'\}([^\}])/','$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}\'{{ item }}\'{% endfor %}$2',$report->raw_query); - - //allow {(macro)} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} - //this is shorthand for quoted, comma separated list - $report->raw_query = preg_replace('/([^\{])\{\('.$key.'\)\}([^\}])/','$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}(\'{{ item }}\'){% endfor %}$2',$report->raw_query); - } - //macros sortcuts for non-arrays - else { - //allow {macro} instead of {{macro}} for legacy support - $report->raw_query = preg_replace('/([^\{])(\{'.$key.'+\})([^\}])/','$1{$2}$3',$report->raw_query); - } - } - - //if there are any included reports, add the report sql to the top - if(isset($report->options['Includes'])) { - $included_sql = ''; - foreach($report->options['Includes'] as &$included_report) { - $included_sql .= trim($included_report->raw_query)."\n"; - } - - $report->raw_query = $included_sql . $report->raw_query; - } - - //set a formatted query here for debugging. It will be overwritten below after macros are substituted. - $report->options['Query_Formatted'] = SqlFormatter::format($report->raw_query); - } - - public static function openConnection(&$report) { - if(isset($report->conn)) return; - - - $environments = PhpReports::$config['environments']; - $config = $environments[$report->options['Environment']][$report->options['Database']]; - - if(isset($config['dsn'])) { - $dsn = $config['dsn']; - } - else { - $host = $config['host']; - if(isset($report->options['access']) && $report->options['access']==='rw') { - if(isset($config['host_rw'])) $host = $config['host_rw']; - } - - $driver = isset($config['driver'])? $config['driver'] : static::$default_driver; - - if(!$driver) { - throw new Exception("Must specify database `driver` (e.g. 'mysql')"); - } - - $dsn = $driver.':host='.$host; - - if(isset($config['database'])) { - $dsn .= ';dbname='.$config['database']; - } - } - - //the default is to use a user with read only privileges - $username = $config['user']; - $password = $config['pass']; - - //if the report requires read/write privileges - if(isset($report->options['access']) && $report->options['access']==='rw') { - if(isset($config['user_rw'])) $username = $config['user_rw']; - if(isset($config['pass_rw'])) $password = $config['pass_rw']; - } - - $report->conn = new PDO($dsn,$username,$password); - - $report->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - } - - public static function closeConnection(&$report) { - if(!isset($report->conn)) return; - $report->conn = null; - unset($report->conn); - } - - public static function getVariableOptions($params, &$report) { - $displayColumn = $params['column']; - if(isset($params['display'])) $displayColumn = $params['display']; - - $query = 'SELECT DISTINCT `'.$params['column'].'` as val, `'.$displayColumn.'` as disp FROM '.$params['table']; - - if(isset($params['where'])) { - $query .= ' WHERE '.$params['where']; - } - - if(isset($params['order']) && in_array($params['order'], array('ASC', 'DESC')) ) { - $query .= ' ORDER BY '.$params['column'].' '.$params['order']; - } - - $result = $report->conn->query($query); - - $options = array(); - - if(isset($params['all'])) $options[] = 'ALL'; - - while($row = $result->fetch(PDO::FETCH_ASSOC)) { - $options[] = array( - 'value'=>$row['val'], - 'display'=>$row['disp'] - ); - } - return $options; - } +class PdoReportType extends ReportTypeBase +{ + public static $default_driver = null; + + public static function init(&$report) + { + $environments = PhpReports::$config['environments']; + + if (!isset($environments[$report->options['Environment']][$report->options['Database']])) { + throw new Exception("No ".$report->options['Database']." info defined for environment '".$report->options['Environment']."'"); + } + + //make sure the syntax highlighting is using the proper class + SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; + + //replace legacy shorthand macro format + foreach ($report->macros as $key => $value) { + if (isset($report->options['Variables'][$key])) { + $params = $report->options['Variables'][$key]; + } else { + $params = []; + } + + //macros shortcuts for arrays + if (isset($params['multiple']) && $params['multiple']) { + //allow {macro} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} + //this is shorthand for comma separated list + $report->raw_query = preg_replace('/([^\{])\{'.$key.'\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}\'{{ item }}\'{% endfor %}$2', $report->raw_query); + + //allow {(macro)} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} + //this is shorthand for quoted, comma separated list + $report->raw_query = preg_replace('/([^\{])\{\('.$key.'\)\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}(\'{{ item }}\'){% endfor %}$2', $report->raw_query); + } else { + //macros sortcuts for non-arrays + //allow {macro} instead of {{macro}} for legacy support + $report->raw_query = preg_replace('/([^\{])(\{'.$key.'+\})([^\}])/', '$1{$2}$3', $report->raw_query); + } + } + + //if there are any included reports, add the report sql to the top + if (isset($report->options['Includes'])) { + $included_sql = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_sql .= trim($included_report->raw_query)."\n"; + } + + $report->raw_query = $included_sql.$report->raw_query; + } + + //set a formatted query here for debugging. It will be overwritten below after macros are substituted. + $report->options['Query_Formatted'] = SqlFormatter::format($report->raw_query); + } + + public static function openConnection(&$report) + { + if (isset($report->conn)) { + return; + } + + $environments = PhpReports::$config['environments']; + $config = $environments[$report->options['Environment']][$report->options['Database']]; + + if (isset($config['dsn'])) { + $dsn = $config['dsn']; + } else { + $host = $config['host']; + if (isset($report->options['access']) && $report->options['access'] === 'rw') { + if (isset($config['host_rw'])) { + $host = $config['host_rw']; + } + } + + $driver = isset($config['driver']) ? $config['driver'] : static::$default_driver; + + if (!$driver) { + throw new Exception("Must specify database `driver` (e.g. 'mysql')"); + } + + $dsn = $driver.':host='.$host; + + if (isset($config['database'])) { + $dsn .= ';dbname='.$config['database']; + } + } + + //the default is to use a user with read only privileges + $username = $config['user']; + $password = $config['pass']; + + //if the report requires read/write privileges + if (isset($report->options['access']) && $report->options['access'] === 'rw') { + if (isset($config['user_rw'])) { + $username = $config['user_rw']; + } + if (isset($config['pass_rw'])) { + $password = $config['pass_rw']; + } + } + + $report->conn = new PDO($dsn, $username, $password); + + $report->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } + + public static function closeConnection(&$report) + { + if (!isset($report->conn)) { + return; + } + $report->conn = null; + unset($report->conn); + } + + public static function getVariableOptions($params, &$report) + { + $displayColumn = $params['column']; + if (isset($params['display'])) { + $displayColumn = $params['display']; + } + + $query = 'SELECT DISTINCT `'.$params['column'].'` as val, `'.$displayColumn.'` as disp FROM '.$params['table']; + + if (isset($params['where'])) { + $query .= ' WHERE '.$params['where']; + } + + if (isset($params['order']) && in_array($params['order'], ['ASC', 'DESC'])) { + $query .= ' ORDER BY '.$params['column'].' '.$params['order']; + } + + $result = $report->conn->query($query); + + $options = []; + + if (isset($params['all'])) { + $options[] = 'ALL'; + } + + while ($row = $result->fetch(PDO::FETCH_ASSOC)) { + $options[] = [ + 'value' => $row['val'], + 'display' => $row['disp'], + ]; + } + + return $options; + } + + public static function run(&$report) + { + $macros = $report->macros; + foreach ($macros as $key => $value) { + if (is_array($value)) { + $first = true; + foreach ($value as $key2 => $value2) { + $value[$key2] = $report->conn->quote(trim($value2)); + $value[$key2] = preg_replace("/(^'|'$)/", '', $value[$key2]); + $first = false; + } + $macros[$key] = $value; + } else { + $macros[$key] = $report->conn->quote($value); + $macros[$key] = preg_replace("/(^'|'$)/", '', $macros[$key]); + } + + if ($value === 'ALL') { + $macros[$key.'_all'] = true; + } + } + + //add the config and environment settings as macros + $macros['config'] = PhpReports::$config; + $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; + + //expand macros in query + $sql = PhpReports::render($report->raw_query, $macros); + + $report->options['Query'] = $sql; + + $report->options['Query_Formatted'] = SqlFormatter::format($sql); + + //split into individual queries and run each one, saving the last result + $queries = SqlFormatter::splitQuery($sql); + + $datasets = []; + + $explicit_datasets = preg_match('/--\s+@dataset(\s*=\s*|\s+)true/', $sql); + + foreach ($queries as $i => $query) { + $is_last = $i === count($queries)-1; + + //skip empty queries + $query = trim($query); + if (!$query) { + continue; + } + + $result = $report->conn->query($query); + + //if this query had an assert=empty flag and returned results, throw error + if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { + if ($result->fetch(PDO::FETCH_ASSOC)) { + throw new Exception("Assert failed. Query did not return empty results."); + } + } + + // If this query should be included as a dataset + if ((!$explicit_datasets && $is_last) || preg_match('/--\s+@dataset(\s*=\s*|\s+)true/', $query)) { + $dataset = ['rows' => []]; + + while ($row = $result->fetch(PDO::FETCH_ASSOC)) { + $dataset['rows'][] = $row; + } + + // Get dataset title if it has one + if (preg_match('/--\s+@title(\s*=\s*|\s+)(.*)/', $query, $matches)) { + $dataset['title'] = $matches[2]; + } - public static function run(&$report) { - $macros = $report->macros; - foreach($macros as $key=>$value) { - if(is_array($value)) { - $first = true; - foreach($value as $key2=>$value2) { - $value[$key2] = $report->conn->quote(trim($value2)); - $value[$key2] = preg_replace("/(^'|'$)/",'',$value[$key2]); - $first = false; - } - $macros[$key] = $value; - } - else { - $macros[$key] = $report->conn->quote($value); - $macros[$key] = preg_replace("/(^'|'$)/",'',$macros[$key]); - } - - if($value === 'ALL') $macros[$key.'_all'] = true; - } - - //add the config and environment settings as macros - $macros['config'] = PhpReports::$config; - $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; - - //expand macros in query - $sql = PhpReports::render($report->raw_query,$macros); - - $report->options['Query'] = $sql; - - $report->options['Query_Formatted'] = SqlFormatter::format($sql); - - //split into individual queries and run each one, saving the last result - $queries = SqlFormatter::splitQuery($sql); - - $datasets = array(); - - $explicit_datasets = preg_match('/--\s+@dataset(\s*=\s*|\s+)true/',$sql); - - foreach($queries as $i=>$query) { - $is_last = $i === count($queries)-1; - - //skip empty queries - $query = trim($query); - if(!$query) continue; - - $result = $report->conn->query($query); - - //if this query had an assert=empty flag and returned results, throw error - if(preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/',$query)) { - if($result->fetch(PDO::FETCH_ASSOC)) throw new Exception("Assert failed. Query did not return empty results."); - } - - // If this query should be included as a dataset - if((!$explicit_datasets && $is_last) || preg_match('/--\s+@dataset(\s*=\s*|\s+)true/',$query)) { - $dataset = array('rows'=>array()); - - while($row = $result->fetch(PDO::FETCH_ASSOC)) { - $dataset['rows'][] = $row; - } - - // Get dataset title if it has one - if(preg_match('/--\s+@title(\s*=\s*|\s+)(.*)/',$query,$matches)) { - $dataset['title'] = $matches[2]; - } - - $datasets[] = $dataset; - } - } - - return $datasets; - } + $datasets[] = $dataset; + } + } + + return $datasets; + } } diff --git a/classes/report_types/PhpReportType.php b/classes/report_types/PhpReportType.php index 3674a764..76328e51 100644 --- a/classes/report_types/PhpReportType.php +++ b/classes/report_types/PhpReportType.php @@ -1,87 +1,98 @@ raw_query = "report."\n".trim($report->raw_query); - - //if there are any included reports, add it to the top of the raw query - if(isset($report->options['Includes'])) { - $included_code = ''; - foreach($report->options['Includes'] as &$included_report) { - $included_code .= "\n".trim($included_report->raw_query).""; - } - - if($included_code) $included_code.= "\n"; - - $report->raw_query = $included_code . $report->raw_query; - - //make sure the raw query has a closing PHP tag at the end - //this makes sure it will play nice as an included report - if(!preg_match('/\?>\s*$/',$report->raw_query)) $report->raw_query .= "\n?>"; - } - } - - public static function openConnection(&$report) { - - } - - public static function closeConnection(&$report) { - - } - - public static function run(&$report) { - $eval = "macros as $key=>$value) { - $value = var_export($value,true); - - $eval .= "\n".'$'.$key.' = '.$value.';'; - } - $eval .= "\n?>".$report->raw_query; - - $config = PhpReports::$config; - - //store in both $database and $environment for backwards compatibility - $database = PhpReports::$config['environments'][$report->options['Environment']]; - $environment = $database; - - $report->options['Query'] = $report->raw_query; - - $parts = preg_split('/<\?php \/\*(BEGIN|END) (INCLUDED REPORT|REPORT MACROS)\*\/ \?>/',$eval); - $report->options['Query_Formatted'] = ''; - $code = htmlentities(trim(array_pop($parts))); - $linenum = 1; - foreach($parts as $part) { - if(!trim($part)) continue; - - //get name of report - $name = preg_match("|//REPORT: ([^\n]+)\n|",$part,$matches); - - if(!$matches) { - $name = "Variables"; - } - else { - $name = $matches[1]; - } - - $report->options['Query_Formatted'] .= '
'; - $report->options['Query_Formatted'] .= "
".htmlentities(trim($part))."
"; - $report->options['Query_Formatted'] .= "
"; - $linenum += count(explode("\n",trim($part))); - } - - $report->options['Query_Formatted'] .= '
'.$code.'
'; - - ob_start(); - ini_set('display_errors','Off'); - eval('?>'.$eval); - $result = ob_get_contents(); - ob_end_clean(); - ini_set('display_errors','On'); - - $result = trim($result); - - $json = json_decode($result, true); - if($json === NULL) throw new Exception($result); - - return $json; - } + +class PhpReportType extends ReportTypeBase +{ + public static function init(&$report) + { + $report->raw_query = "report."\n".trim($report->raw_query); + + //if there are any included reports, add it to the top of the raw query + if (isset($report->options['Includes'])) { + $included_code = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_code .= "\n".trim($included_report->raw_query).""; + } + + if ($included_code) { + $included_code .= "\n"; + } + + $report->raw_query = $included_code.$report->raw_query; + + //make sure the raw query has a closing PHP tag at the end + //this makes sure it will play nice as an included report + if (!preg_match('/\?>\s*$/', $report->raw_query)) { + $report->raw_query .= "\n?>"; + } + } + } + + public static function openConnection(&$report) + { + } + + public static function closeConnection(&$report) + { + } + + public static function run(&$report) + { + $eval = "macros as $key => $value) { + $value = var_export($value, true); + + $eval .= "\n".'$'.$key.' = '.$value.';'; + } + $eval .= "\n?>".$report->raw_query; + + $config = PhpReports::$config; + + //store in both $database and $environment for backwards compatibility + $database = PhpReports::$config['environments'][$report->options['Environment']]; + $environment = $database; + + $report->options['Query'] = $report->raw_query; + + $parts = preg_split('/<\?php \/\*(BEGIN|END) (INCLUDED REPORT|REPORT MACROS)\*\/ \?>/', $eval); + $report->options['Query_Formatted'] = ''; + $code = htmlentities(trim(array_pop($parts))); + $linenum = 1; + foreach ($parts as $part) { + if (!trim($part)) { + continue; + } + + //get name of report + $name = preg_match("|//REPORT: ([^\n]+)\n|", $part, $matches); + + if (!$matches) { + $name = "Variables"; + } else { + $name = $matches[1]; + } + + $report->options['Query_Formatted'] .= '
'; + $report->options['Query_Formatted'] .= "
".htmlentities(trim($part))."
"; + $report->options['Query_Formatted'] .= "
"; + $linenum += count(explode("\n", trim($part))); + } + + $report->options['Query_Formatted'] .= '
'.$code.'
'; + + ob_start(); + ini_set('display_errors', 'Off'); + eval('?>'.$eval); + $result = ob_get_contents(); + ob_end_clean(); + ini_set('display_errors', 'On'); + + $result = trim($result); + + $json = json_decode($result, true); + if ($json === null) { + throw new Exception($result); + } + + return $json; + } } diff --git a/composer.json b/composer.json index 22a3c5b9..36ffc821 100644 --- a/composer.json +++ b/composer.json @@ -31,5 +31,8 @@ "vendor/jdorn/file-system-cache/" ] }, - "minimum-stability": "dev" + "minimum-stability": "dev", + "require-dev": { + "phpunit/phpunit": "^5.3" + } } diff --git a/composer.lock b/composer.lock index e6df5bbf..20217ee5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "f95ea5ffe884cc7b98c3637b05df0644", - "content-hash": "5dd8eccbc6699fee138e5031fa79c285", + "hash": "5c2e1ebbc198a1cb68093448b5c1f999", + "content-hash": "14bccedea3e35332c93d6a6d8da730a4", "packages": [ { "name": "adodb/adodb-php", @@ -17,7 +17,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ADOdb/ADOdb/zipball/7d941967aa1b81d636f5b6e1f3549b2fc95248b9", + "url": "https://api.github.com/repos/ADOdb/ADOdb/zipball/b0c70859b8784a6e7740f3c149c0daf72b49c2cc", "reference": "7d941967aa1b81d636f5b6e1f3549b2fc95248b9", "shasum": "" }, @@ -302,7 +302,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPExcel/zipball/7fa160905bec24ae5fdf7c98db7a4e1925a4acfa", + "url": "https://api.github.com/repos/PHPOffice/PHPExcel/zipball/8af620f97b8b1c8a677d90b3d7203fa562050db1", "reference": "7fa160905bec24ae5fdf7c98db7a4e1925a4acfa", "shasum": "" }, @@ -363,7 +363,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/fffbc0e2a7e376dbb0a4b5f2ff6847330f20ccf9", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/68f50da0fdd6cb0146d17d6bde20b76e0e57e7fb", "reference": "fffbc0e2a7e376dbb0a4b5f2ff6847330f20ccf9", "shasum": "" }, @@ -416,7 +416,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/3d0afc03892719b7eaa5f2b8d93260a79f8c578e", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/f167cc0a65ab0a2a53558c9385b4a324c1a36b02", "reference": "3d0afc03892719b7eaa5f2b8d93260a79f8c578e", "shasum": "" }, @@ -468,7 +468,1149 @@ "time": "2016-04-01 06:54:57" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "416fb8ad1d095a87f1d21bc40711843cd122fd4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/416fb8ad1d095a87f1d21bc40711843cd122fd4a", + "reference": "416fb8ad1d095a87f1d21bc40711843cd122fd4a", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2016-03-31 10:24:22" + }, + { + "name": "myclabs/deep-copy", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "a8773992b362b58498eed24bf85005f363c34771" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/a8773992b362b58498eed24bf85005f363c34771", + "reference": "a8773992b362b58498eed24bf85005f363c34771", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2015-11-20 12:04:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2015-02-03 12:10:50" + }, + { + "name": "phpspec/prophecy", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "80138299aadb590ce3fd0b75d30343fff9f8d0a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/80138299aadb590ce3fd0b75d30343fff9f8d0a8", + "reference": "80138299aadb590ce3fd0b75d30343fff9f8d0a8", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "~2.0", + "sebastian/comparator": "~1.1", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpspec/phpspec": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-04-29 12:18:42" + }, + { + "name": "phpunit/php-code-coverage", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "9eb699e97e0097f05bcf1cd9bbb91c582a48bf9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9eb699e97e0097f05bcf1cd9bbb91c582a48bf9e", + "reference": "9eb699e97e0097f05bcf1cd9bbb91c582a48bf9e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "^1.4.2", + "sebastian/code-unit-reverse-lookup": "~1.0", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0|~2.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.4.0", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2016-05-04 12:34:12" + }, + { + "name": "phpunit/php-file-iterator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2015-06-21 13:08:43" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2015-06-21 08:01:12" + }, + { + "name": "phpunit/php-token-stream", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "cab6c6fefee93d7b7c3a01292a0fe0884ea66644" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cab6c6fefee93d7b7c3a01292a0fe0884ea66644", + "reference": "cab6c6fefee93d7b7c3a01292a0fe0884ea66644", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2015-09-23 14:46:55" + }, + { + "name": "phpunit/phpunit", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "82a3aafd187033fd5572ad28dfff7c1989cbf68d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/82a3aafd187033fd5572ad28dfff7c1989cbf68d", + "reference": "82a3aafd187033fd5572ad28dfff7c1989cbf68d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "^4.0", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/object-enumerator": "~1.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0|~2.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2016-05-05 07:36:17" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "5d8c2a839d2c77757b7499eb135f34f9f5f07e6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5d8c2a839d2c77757b7499eb135f34f9f5f07e6f", + "reference": "5d8c2a839d2c77757b7499eb135f34f9f5f07e6f", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": ">=5.6", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2016-04-20 14:39:30" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2016-02-13 06:45:14" + }, + { + "name": "sebastian/comparator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-07-26 15:48:44" + }, + { + "name": "sebastian/diff", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08 07:14:41" + }, + { + "name": "sebastian/environment", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "2292b116f43c272ff4328083096114f84ea46a56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/2292b116f43c272ff4328083096114f84ea46a56", + "reference": "2292b116f43c272ff4328083096114f84ea46a56", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-05-04 07:59:13" + }, + { + "name": "sebastian/exporter", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "f88f8936517d54ae6d589166810877fb2015d0a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/f88f8936517d54ae6d589166810877fb2015d0a2", + "reference": "f88f8936517d54ae6d589166810877fb2015d0a2", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2015-08-09 04:23:41" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/object-enumerator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2016-01-28 13:25:10" + }, + { + "name": "sebastian/recursion-context", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "7ff5b1b3dcc55b8ab8ae61ef99d4730940856ee7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/7ff5b1b3dcc55b8ab8ae61ef99d4730940856ee7", + "reference": "7ff5b1b3dcc55b8ab8ae61ef99d4730940856ee7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2016-01-28 05:39:29" + }, + { + "name": "sebastian/resource-operations", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28 20:34:47" + }, + { + "name": "sebastian/version", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-02-04 12:56:52" + }, + { + "name": "symfony/yaml", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "407e31ad9742ace5c3d01642f02a3b2e6062bae5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/407e31ad9742ace5c3d01642f02a3b2e6062bae5", + "reference": "407e31ad9742ace5c3d01642f02a3b2e6062bae5", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-03-30 14:44:34" + } + ], "aliases": [], "minimum-stability": "dev", "stability-flags": { diff --git a/lib/PhpReports/FilterBase.php b/lib/PhpReports/FilterBase.php index cb422fe0..db4ee626 100644 --- a/lib/PhpReports/FilterBase.php +++ b/lib/PhpReports/FilterBase.php @@ -1,12 +1,16 @@ getMessage()); - } - - static::init($params, $report); - } - - public static function init($params, &$report) { - - } - - public static function parseShortcut($value) { - return array(); - } - - public static function beforeRender(&$report) { - - } - - public static function afterParse(&$report) { - - } - - public static function beforeRun(&$report) { - - } - - protected static function validate($params) { - if(!static::$validation) return $params; - - $errors = array(); - - foreach(static::$validation as $key=>$rules) { - //fill in default params - if(isset($rules['default']) && !isset($params[$key])) { - $params[$key] = $rules['default']; - continue; - } - - //if the param isn't required and it's defined, we can skip validation - if((!isset($rules['required']) || !$rules['required']) && !isset($params[$key])) continue; - - //if the param must be a specific datatype - if(isset($rules['type'])) { - if($rules['type'] === 'number' && !is_numeric($params[$key])) $errors[] = "$key must be a number (".gettype($params[$key])." given)"; - elseif($rules['type'] === 'array' && !is_array($params[$key])) $errors[] = "$key must be an array (".gettype($params[$key])." given)"; - elseif($rules['type'] === 'boolean' && !is_bool($params[$key])) $errors[] = "$key must be true or false (".gettype($params[$key])." given)"; - elseif($rules['type'] === 'string' && !is_string($params[$key])) $errors[] = "$key must be a string (".gettype($params[$key])." given)"; - elseif($rules['type'] === 'enum' && !in_array($params[$key],$rules['values'])) $errors[] = "$key must be one of: [".implode(', ',$rules['values'])."]"; - elseif($rules['type'] === 'object' && !is_array($params[$key])) $errors[] = "$key must be an object (".gettype($params[$key])." given)"; - } - - //other validation rules - if(isset($rules['min']) && $params[$key] < $rules['min']) $errors[] = "$key must be at least $rules[min]"; - if(isset($rules['max']) && $params[$key] > $rules['max']) $errors[] = "$key must be at most $rules[min]"; - - if(isset($rules['pattern']) && !preg_match($rules['pattern'],$params[$key])) $errors[] = "$key does not match required pattern"; - } - - //every possible param must be defined in the validation rules - foreach($params as $k=>$v) { - if(!isset(static::$validation[$k])) $errors[] = "Unknown parameter '$k'"; - } - - if($errors) { - throw new Exception(implode(". ",$errors)); - } - else return $params; - } + +class HeaderBase +{ + protected static $validation = []; + + public static function parse($key, $value, &$report) + { + $params = null; + + if (is_array($value)) { + $params = $value; + } elseif ($value[0] === '{') { + //try to json_decode value + //this is a wrapper json_decode function that supports non-strict JSON + //for example, {key:"value",'key2':'value2',} + $params = PhpReports::json_decode($value, true); + } + + //if it couldn't be parsed as json, try parsing it as a shortcut form + if (!$params) { + $params = static::parseShortcut($value); + } + + if (!$params) { + throw new Exception("Could not parse header '$key'"); + } + + //run defined validation rules and fill in default params + try { + $params = static::validate($params); + } catch (Exception $e) { + throw new Exception($key." Header: ".$e->getMessage()); + } + + static::init($params, $report); + } + + public static function init($params, &$report) + { + } + + public static function parseShortcut($value) + { + return []; + } + + public static function beforeRender(&$report) + { + } + + public static function afterParse(&$report) + { + } + + public static function beforeRun(&$report) + { + } + + protected static function validate($params) + { + if (!static::$validation) { + return $params; + } + + $errors = []; + + foreach (static::$validation as $key => $rules) { + //fill in default params + if (isset($rules['default']) && !isset($params[$key])) { + $params[$key] = $rules['default']; + continue; + } + + //if the param isn't required and it's defined, we can skip validation + if ((!isset($rules['required']) || !$rules['required']) && !isset($params[$key])) { + continue; + } + + //if the param must be a specific datatype + if (isset($rules['type'])) { + if ($rules['type'] === 'number' && !is_numeric($params[$key])) { + $errors[] = "$key must be a number (".gettype($params[$key])." given)"; + } elseif ($rules['type'] === 'array' && !is_array($params[$key])) { + $errors[] = "$key must be an array (".gettype($params[$key])." given)"; + } elseif ($rules['type'] === 'boolean' && !is_bool($params[$key])) { + $errors[] = "$key must be true or false (".gettype($params[$key])." given)"; + } elseif ($rules['type'] === 'string' && !is_string($params[$key])) { + $errors[] = "$key must be a string (".gettype($params[$key])." given)"; + } elseif ($rules['type'] === 'enum' && !in_array($params[$key], $rules['values'])) { + $errors[] = "$key must be one of: [".implode(', ', $rules['values'])."]"; + } elseif ($rules['type'] === 'object' && !is_array($params[$key])) { + $errors[] = "$key must be an object (".gettype($params[$key])." given)"; + } + } + + //other validation rules + if (isset($rules['min']) && $params[$key] < $rules['min']) { + $errors[] = "$key must be at least $rules[min]"; + } + if (isset($rules['max']) && $params[$key] > $rules['max']) { + $errors[] = "$key must be at most $rules[min]"; + } + + if (isset($rules['pattern']) && !preg_match($rules['pattern'], $params[$key])) { + $errors[] = "$key does not match required pattern"; + } + } + + //every possible param must be defined in the validation rules + foreach ($params as $k => $v) { + if (!isset(static::$validation[$k])) { + $errors[] = "Unknown parameter '$k'"; + } + } + + if ($errors) { + throw new Exception(implode(". ", $errors)); + } else { + return $params; + } + } } diff --git a/lib/PhpReports/PhpReports.php b/lib/PhpReports/PhpReports.php index 8a921d87..96259e74 100644 --- a/lib/PhpReports/PhpReports.php +++ b/lib/PhpReports/PhpReports.php @@ -1,680 +1,779 @@ base; - - if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') { - $protocol = 'https://'; - } else { - $protocol = 'http://'; - } - self::$request->base = $protocol.rtrim($_SERVER['HTTP_HOST'].self::$request->base,'/'); - - //the load order for templates is: "templates/local", "templates/default", "templates" - //this means loading the template "html/report.twig" will load the local first and then the default - //if you want to extend a default template from within a local template, you can do {% extends "default/html/report.twig" %} and it will fall back to the last loader - $template_dirs = array('templates/default','templates'); - if(file_exists('templates/local')) array_unshift($template_dirs, 'templates/local'); - - $loader = new Twig_Loader_Chain(array( - new Twig_Loader_Filesystem($template_dirs), - new Twig_Loader_String() - )); - self::$twig = new Twig_Environment($loader); - self::$twig->addFunction(new Twig_SimpleFunction('dbdate', 'PhpReports::dbdate')); - self::$twig->addFunction(new Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN')); - - if(isset($_COOKIE['reports-theme']) && $_COOKIE['reports-theme']) { - $theme = $_COOKIE['reports-theme']; - } - else { - $theme = self::$config['bootstrap_theme']; - } - self::$twig->addGlobal('theme', $theme); - self::$twig->addGlobal('path', $path); - - self::$twig->addFilter('var_dump', new Twig_Filter_Function('var_dump')); - - self::$twig_string = new Twig_Environment(new Twig_Loader_String(), array('autoescape'=>false)); - self::$twig_string->addFunction(new Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN')); - - FileSystemCache::$cacheDir = self::$config['cacheDir']; - - if(!isset($_SESSION['environment']) || !isset(self::$config['environments'][$_SESSION['environment']])) { - $_SESSION['environment'] = array_shift(array_keys(self::$config['environments'])); - } - - // Extend twig. - if (isset($config['twig_init_function']) && is_callable($config['twig_init_function'])) { - $config['twig_init_function'](self::$twig); - $config['twig_init_function'](self::$twig_string); - } - } - - public static function setVar($key,$value) { - if(!self::$vars) self::$vars = array(); - - self::$vars[$key] = $value; - } - public static function getVar($key, $default=null) { - if(isset(self::$vars[$key])) return self::$vars[$key]; - else return $default; - } - - public static function dbdate($time, $database=null, $format=null) { - $report = self::getVar('Report',null); - if(!$report) return strtotime('Y-m-d H:i:s',strtotime($time)); - - //if a variable name was passed in - $var = null; - if(isset($report->options['Variables'][$time])) { - $var = $report->options['Variables'][$time]; - $time = $report->macros[$time]; - } - - $time = strtotime($time); - - $environment = $report->getEnvironment(); - - //determine time offset - $offset = 0; - - if($database) { - if(isset($environment[$database]['time_offset'])) $offset = $environment[$database]['time_offset']; - } - else { - $database = $report->getDatabase(); - if(isset($database['time_offset'])) $offset = $database['time_offset']; - } - - //if the time needs to be adjusted - if($offset) { - $time = strtotime((($offset > 0)? '+' : '-').abs($offset).' hours',$time); - } - - //determine output format - if($format) { - $time = date($format,$time); - } - elseif($var && isset($var['format'])) { - $time = date($var['format'],$time); - } - //default to Y-m-d H:i:s - else { - $time = date('Y-m-d H:i:s',$time); - } - - return $time; - } - - public static function generateSqlIN($column, $values, $or_null = false) { - $sql = "$column IN ("; - foreach ($values as $value) { - $sql .= is_numeric($value) ? $value : "'$value'"; - if ($value !== end($values)) { - $sql .= ', '; - } - } - $sql .= ")"; - if ($or_null) { - $sql.= " OR $column IS NULL"; - } - return $sql; - } - - public static function render($template, $macros) { - $default = array( - 'base'=>self::$request->base, - 'report_list_url'=>self::$request->base.'/', - 'request'=>self::$request, - 'querystring'=>$_SERVER['QUERY_STRING'], - 'config'=>self::$config, - 'environment'=>$_SESSION['environment'], - 'recent_reports'=>self::getRecentReports(), - 'session'=>$_SESSION - ); - $macros = array_merge($default,$macros); - - //if a template path like 'html/report' is given, add the twig file extension - if(preg_match('/^[a-zA-Z_\-0-9\/]+$/',$template)) $template .= '.twig'; - return self::$twig->render($template,$macros); - } - - public static function renderString($template, $macros) { - return self::$twig_string->render($template,$macros); - } - - public static function displayReport($report,$type) { - $classname = ucfirst(strtolower($type)).'ReportFormat'; - - $error_header = 'An error occurred while running your report'; - $content = ''; - - try { - if(!class_exists($classname)) { - $error_header = 'Unknown report format'; - throw new Exception("Unknown report format '$type'"); - } - - try { - $report = $classname::prepareReport($report); - } - catch(Exception $e) { - $error_header = 'An error occurred while preparing your report'; - throw $e; - } - - $classname::display($report,self::$request); - - if(isset($report->options['Query_Formatted'])) { - $content = $report->options['Query_Formatted']; - } - } - catch(Exception $e) { - echo self::render('html/page',array( - 'title'=>$report->report, - 'header'=>'

'.$error_header.'

', - 'error'=>$e->getMessage(), - 'content'=>$content, - 'breadcrumb'=>array('Report List'=>'', $report->report => true) - )); - } - } - - public static function editReport($report) { - $template_vars = array(); - - try { - $report = ReportFormatBase::prepareReport($report); - - $template_vars = array( - 'report'=>$report->report, - 'options'=>$report->options, - 'contents'=>$report->getRaw(), - 'extension'=>array_pop(explode('.',$report->report)) - ); - } - //if there is an error parsing the report - catch(Exception $e) { - $template_vars = array( - 'report'=>$report, - 'contents'=>Report::getReportFileContents($report), - 'options'=>array(), - 'extension'=>array_pop(explode('.',$report)), - 'error'=>$e - ); - } - - if(isset($_POST['preview'])) { - echo "
".SimpleDiff::htmlDiffSummary($template_vars['contents'],$_POST['contents'])."
"; - } - elseif(isset($_POST['save'])) { - Report::setReportFileContents($template_vars['report'],$_POST['contents']); - } - else { - echo self::render('html/report_editor',$template_vars); - } - } - - public static function listReports() { - $errors = array(); - - $reports = self::getReports(self::$config['reportDir'].'/',$errors); - - $template_vars['reports'] = $reports; - $template_vars['report_errors'] = $errors; - - $start = microtime(true); - echo self::render('html/report_list',$template_vars); - } - - public static function listDashboards() { - $dashboards = self::getDashboards(); - - uasort($dashboards,function($a,$b) { - return strcmp($a['title'],$b['title']); - }); - - echo self::render('html/dashboard_list',array( - 'dashboards'=>$dashboards - )); - } - - public static function displayDashboard($dashboard) { - $content = self::getDashboard($dashboard); - - echo self::render('html/dashboard',array( - 'dashboard'=>$content - )); - } - - public static function getDashboards() { - $dashboards = glob(PhpReports::$config['dashboardDir'].'/*.json'); - - $ret = array(); - foreach($dashboards as $key=>$value) { - $name = basename($value,'.json'); - $ret[$name] = self::getDashboard($name); - } - - return $ret; - } - - public static function getDashboard($dashboard) { - $file = PhpReports::$config['dashboardDir'].'/'.$dashboard.'.json'; - if(!file_exists($file)) { - throw new Exception("Unknown dashboard - ".$dashboard); - } - - return json_decode(file_get_contents($file),true); - } - - public static function getRecentReports() { - $recently_run = FileSystemCache::retrieve(FileSystemCache::generateCacheKey('recently_run')); - $recent = array(); - if($recently_run !== false) { - $i = 0; - foreach($recently_run as $report) { - if($i > 10) break; - - $headers = self::getReportHeaders($report); - - if(!$headers) continue; - if(isset($recent[$headers['url']])) continue; - - $recent[$headers['url']] = $headers; - $i++; - } - } - - return array_values($recent); - } - public static function getReportListJSON($reports=null) { - if($reports === null) { - $errors = array(); - $reports = self::getReports(self::$config['reportDir'].'/',$errors); - } - - //weight by popular reports - $recently_run = FileSystemCache::retrieve(FileSystemCache::generateCacheKey('recently_run')); - $popular = array(); - if($recently_run !== false) { - foreach($recently_run as $report) { - if(!isset($popular[$report])) $popular[$report] = 1; - else $popular[$report]++; - } - } - $parts = array(); - - foreach($reports as $report) { - if($report['is_dir'] && $report['children']) { - //skip if the directory doesn't have a title - if(!isset($report['Title']) || !$report['Title']) continue; - - $part = trim(self::getReportListJSON($report['children']),'[],'); - if($part) $parts[] = $part; - } - else { - //skip if report is marked as dangerous - if((isset($report['stop'])&&$report['stop']) || isset($report['Caution']) || isset($report['warning'])) continue; - if(!isset($report['url'])) continue; - if(!isset($report['report'])) continue; - - //skip if report is marked as ignore - if(isset($report['ignore']) && $report['ignore']) continue; - - if(isset($popular[$report['report']])) { - $popularity = $popular[$report['report']]; - } - else $popularity = 0; - - $parts[] = json_encode(array( - 'name'=>$report['Name'], - 'url'=>$report['url'], - 'popularity'=>$popularity - )); - } - } - - return '['.trim(implode(',',$parts),',').']'; - } - - protected static function getReportHeaders($report) { - $cacheKey = FileSystemCache::generateCacheKey(array(self::$request->base, $report),'report_headers'); - - //check if report data is cached and newer than when the report file was created - //the url parameter ?nocache will bypass this and not use cache - $data =false; - - $loc = Report::getFileLocation($report); - if(!file_exists($loc)) { - return false; - } - if(!isset($_REQUEST['nocache'])) { - $data = FileSystemCache::retrieve($cacheKey, filemtime($loc)); - } - - //report data not cached, need to parse it - if($data === false) { - $temp = new Report($report); - - $data = $temp->options; - - $data['report'] = $report; - $data['url'] = self::$request->base.'/report/html/?report='.$report; - $data['is_dir'] = false; - $data['Id'] = str_replace(array('_','-','/',' ','.'),array('','','_','-','_'),trim($report,'/')); - if(!isset($data['Name'])) $data['Name'] = ucwords(str_replace(array('_','-'),' ',basename($report))); - - //store parsed report in cache - FileSystemCache::store($cacheKey, $data); - } - - return $data; - } - - protected static function getReports($dir, &$errors = null) { - $base = self::$config['reportDir'].'/'; - - $reports = glob($dir.'*',GLOB_NOSORT); - $return = array(); - foreach($reports as $key=>$report) { - $title = $description = false; - - if(is_dir($report)) { - if(file_exists($report.'/TITLE.txt')) $title = file_get_contents($report.'/TITLE.txt'); - if(file_exists($report.'/README.txt')) $description = file_get_contents($report.'/README.txt'); - - $id = str_replace(array('_','-','/',' '),array('','','_','-'),trim(substr($report,strlen($base)),'/')); - - $children = self::getReports($report.'/', $errors); - - $count = 0; - foreach($children as $child) { - if(isset($child['count'])) $count += $child['count']; - else $count++; - } - - $return[] = array( - 'Name'=>ucwords(str_replace(array('_','-'),' ',basename($report))), - 'Title'=>$title, - 'Id'=> $id, - 'Description'=>$description, - 'is_dir'=>true, - 'children'=>$children, - 'count'=>$count - ); - } - else { - //files to skip - if(strpos(basename($report),'.') === false) continue; - $ext = array_pop(explode('.',$report)); - if(!isset(self::$config['default_file_extension_mapping'][$ext])) continue; - - $name = substr($report,strlen($base)); - - try { - $data = self::getReportHeaders($name,$base); - $return[] = $data; - } - catch(Exception $e) { - if(!$errors) $errors = array(); - $errors[] = array( - 'report'=>$name, - 'exception'=>$e - ); - } - } - } - - usort($return,function(&$a,&$b) { - if($a['is_dir'] && !$b['is_dir']) return 1; - elseif($b['is_dir'] && !$a['is_dir']) return -1; - - if(empty($a['Title']) && empty($b['Title'])) return strcmp($a['Name'],$b['Name']); - elseif(empty($a['Title'])) return 1; - elseif(empty($b['Title'])) return -1; - - return strcmp($a['Title'], $b['Title']); - }); - - return $return; - } - - /** - * Emails a report given a TO address, a subject, and a message - */ - public static function emailReport() { - if(!isset($_REQUEST['email']) || !filter_var($_REQUEST['email'], FILTER_VALIDATE_EMAIL)) { - echo json_encode(array('error'=>'Valid email address required')); - return; - } - if(!isset($_REQUEST['url'])) { - echo json_encode(array('error'=>'Report url required')); - return; - } - if(!isset(PhpReports::$config['mail_settings']['enabled']) || !PhpReports::$config['mail_settings']['enabled']) { - echo json_encode(array('error'=>'Email is disabled on this server')); - return; - } - if(!isset(PhpReports::$config['mail_settings']['from'])) { - echo json_encode(array('error'=>'Email settings have not been properly configured on this server')); - return; - } - - $from = PhpReports::$config['mail_settings']['from']; - $subject = $_REQUEST['subject']? $_REQUEST['subject'] : 'Database Report'; - $body = $_REQUEST['message']? $_REQUEST['message'] : "You've been sent a database report!"; - $email = $_REQUEST['email']; - $link = $_REQUEST['url']; - $csv_link = str_replace('report/html/?','report/csv/?',$link); - $table_link = str_replace('report/html/?','report/table/?',$link); - $text_link = str_replace('report/html/?','report/text/?',$link); - - // Get the CSV file attachment and the inline HTML table - $csv = self::urlDownload($csv_link); - $table = self::urlDownload($table_link); - $text = self::urlDownload($text_link); - - $email_text = $body."\n\n".$text."\n\nView the report online at $link"; - $email_html = "

$body

$table

View the report online at ".htmlentities($link)."

"; - - // Create the message - $message = Swift_Message::newInstance() - ->setSubject($subject) - ->setFrom($from) - ->setTo($email) - //text body - ->setBody($email_text) - //html body - ->addPart($email_html, 'text/html') - ; - - $attachment = Swift_Attachment::newInstance() - ->setFilename('report.csv') - ->setContentType('text/csv') - ->setBody($csv) - ; - - $message->attach($attachment); - - // Create the Transport - $transport = self::getMailTransport(); - $mailer = Swift_Mailer::newInstance($transport); - - try { - // Send the message - $result = $mailer->send($message); - } - catch(Exception $e) { - echo json_encode(array( - 'error'=>$e->getMessage() - )); - return; - } - - if($result) { - echo json_encode(array( - 'success'=>true - )); - } - else { - echo json_encode(array( - 'error'=>'Failed to send email to requested recipient' - )); - } - } - - /** - * Determines the email transport to use based on the configuration settings - */ - protected static function getMailTransport() { - if(!isset(PhpReports::$config['mail_settings'])) PhpReports::$config['mail_settings'] = array(); - if(!isset(PhpReports::$config['mail_settings']['method'])) PhpReports::$config['mail_settings']['method'] = 'mail'; - - switch(PhpReports::$config['mail_settings']['method']) { - case 'mail': - return Swift_MailTransport::newInstance(); - case 'sendmail': - return Swift_MailTransport::newInstance( - isset(PhpReports::$config['mail_settings']['command'])? PhpReports::$config['mail_settings']['command'] : '/usr/sbin/sendmail -bs' - ); - case 'smtp': - if(!isset(PhpReports::$config['mail_settings']['server'])) throw new Exception("SMTP server must be configured"); - $transport = Swift_SmtpTransport::newInstance( - PhpReports::$config['mail_settings']['server'], - isset(PhpReports::$config['mail_settings']['port'])? PhpReports::$config['mail_settings']['port'] : 25 - ); - - //if username/password - if(isset(PhpReports::$config['mail_settings']['username'])) { - $transport->setUsername(PhpReports::$config['mail_settings']['username']); - $transport->setPassword(PhpReports::$config['mail_settings']['password']); - } - - //if using encryption - if(isset(PhpReports::$config['mail_settings']['encryption'])) { - $transport->setEncryption(PhpReports::$config['mail_settings']['encryption']); - } - - return $transport; - default: - throw new Exception("Mail method must be either 'mail', 'sendmail', or 'smtp'"); - } - } - - /** - * Autoloader methods - */ - public static function loader($className) { - if(!isset(self::$loader_cache)) { - self::buildLoaderCache(); - } - - if(isset(self::$loader_cache[$className])) { - require_once(self::$loader_cache[$className]); - return true; - } - else { - return false; - } - } - public static function buildLoaderCache() { - self::load('classes/local'); - self::load('classes',array('classes/local')); - self::load('lib'); - } - public static function load($dir, $skip=array()) { - $files = glob($dir.'/*.php'); - $dirs = glob($dir.'/*',GLOB_ONLYDIR); - - - foreach($files as $file) { - //for file names same as class name - $className = basename($file,'.php'); - if(!isset(self::$loader_cache[$className])) self::$loader_cache[$className] = $file; - - //for PEAR style: Path_To_Class.php - $parts = explode('/',substr($file,0,-4)); - array_shift($parts); - $className = implode('_',$parts); - //if any of the directories in the path are lowercase, it isn't in PEAR format - if(preg_match('/(^|_)[a-z]/',$className)) continue; - if(!isset(self::$loader_cache[$className])) self::$loader_cache[$className] = $file; - } - - foreach($dirs as $dir2) { - //directories to skip - if($dir2[0]==='.') continue; - if(in_array($dir2,$skip)) continue; - if(in_array(basename($dir2),array('tests','test','example','examples','bin'))) continue; - - self::load($dir2,$skip); - } - } - - /** - * A more lenient json_decode than the built-in PHP one. - * It supports strict JSON as well as javascript syntax (i.e. unquoted/single quoted keys, single quoted values, trailing commmas) - */ - public static function json_decode($json, $assoc=false) { - //replace single quoted values - $json = preg_replace('/:\s*\'(([^\']|\\\\\')*)\'\s*([},])/e', "':'.json_encode(stripslashes('$1')).'$3'", $json); - - //replace single quoted keys - $json = preg_replace('/\'(([^\']|\\\\\')*)\'\s*:/e', "json_encode(stripslashes('$1')).':'", $json); - - //remove any line breaks in the code - $json = str_replace(array("\n","\r"),"",$json); - - //replace non-quoted keys with double quoted keys - $json = preg_replace('#(?
\{|\[|,)\s*(?(?:\w|_)+)\s*:#im', '$1"$2":', $json);
-
-		//remove trailing comma
-		$json = preg_replace('/,\s*\}/','}',$json);
-
-		return json_decode($json, $assoc);
-	}
-
-	protected static function urlDownload($url) {
-		$ch = curl_init();
-		curl_setopt($ch, CURLOPT_URL, $url);
-		curl_setopt($ch, CURLOPT_HEADER, 0);
-		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-
-		$output = curl_exec($ch);
-		curl_close($ch);
-
-		return $output;
-	}
+
+class PhpReports
+{
+    public static $config;
+    public static $request;
+    public static $twig;
+    public static $twig_string;
+
+    public static $vars;
+
+    private static $loader_cache;
+
+    public static function init($config = 'config/config.php')
+    {
+        //set up our autoloader
+        spl_autoload_register(array('PhpReports', 'loader'), true, true);
+
+        if (!file_exists($config)) {
+            throw new Exception("Cannot find config file");
+        }
+
+        // The config.php.sample is used to populate default values should the config.php be incomplete.
+        // As a result, we require it be there.
+        if (!file_exists('config/config.php.sample')) {
+            throw new Exception("Cannot find sample config. Please leave config/config.php.sample in place for default values.");
+        }
+
+        $default_config = include 'config/config.php.sample';
+        $config = include $config;
+
+        self::$config = array_merge($default_config, $config);
+
+        self::$request = Flight::request();
+
+        $path = self::$request->base;
+
+        if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
+            $protocol = 'https://';
+        } else {
+            $protocol = 'http://';
+        }
+        self::$request->base = $protocol.rtrim($_SERVER['HTTP_HOST'].self::$request->base, '/');
+
+        //the load order for templates is: "templates/local", "templates/default", "templates"
+        //this means loading the template "html/report.twig" will load the local first and then the default
+        //if you want to extend a default template from within a local template, you can do {% extends "default/html/report.twig" %} and it will fall back to the last loader
+        $template_dirs = array('templates/default','templates');
+        if (file_exists('templates/local')) {
+            array_unshift($template_dirs, 'templates/local');
+        }
+
+        $loader = new Twig_Loader_Chain(array(
+            new Twig_Loader_Filesystem($template_dirs),
+            new Twig_Loader_String(),
+        ));
+        self::$twig = new Twig_Environment($loader);
+        self::$twig->addFunction(new Twig_SimpleFunction('dbdate', 'PhpReports::dbdate'));
+        self::$twig->addFunction(new Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN'));
+
+        if (isset($_COOKIE['reports-theme']) && $_COOKIE['reports-theme']) {
+            $theme = $_COOKIE['reports-theme'];
+        } else {
+            $theme = self::$config['bootstrap_theme'];
+        }
+        self::$twig->addGlobal('theme', $theme);
+        self::$twig->addGlobal('path', $path);
+        self::$twig->addGlobal('brand', self::$config['brand']);
+
+        self::$twig->addFilter('var_dump', new Twig_Filter_Function('var_dump'));
+
+        self::$twig_string = new Twig_Environment(new Twig_Loader_String(), array('autoescape' => false));
+        self::$twig_string->addFunction(new Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN'));
+
+        FileSystemCache::$cacheDir = self::$config['cacheDir'];
+
+        if (!isset($_SESSION['environment']) || !isset(self::$config['environments'][$_SESSION['environment']])) {
+            $environments = array_keys(self::$config['environments']);
+            $_SESSION['environment'] = array_shift($environments);
+        }
+
+        // Extend twig.
+        if (isset($config['twig_init_function']) && is_callable($config['twig_init_function'])) {
+            $config['twig_init_function'](self::$twig);
+            $config['twig_init_function'](self::$twig_string);
+        }
+    }
+
+    public static function setVar($key, $value)
+    {
+        if (!self::$vars) {
+            self::$vars = array();
+        }
+
+        self::$vars[$key] = $value;
+    }
+    public static function getVar($key, $default = null)
+    {
+        if (isset(self::$vars[$key])) {
+            return self::$vars[$key];
+        } else {
+            return $default;
+        }
+    }
+
+    public static function dbdate($time, $database = null, $format = null)
+    {
+        $report = self::getVar('Report', null);
+        if (!$report) {
+            return strtotime('Y-m-d H:i:s', strtotime($time));
+        }
+
+        //if a variable name was passed in
+        $var = null;
+        if (isset($report->options['Variables'][$time])) {
+            $var = $report->options['Variables'][$time];
+            $time = $report->macros[$time];
+        }
+
+        $time = strtotime($time);
+
+        $environment = $report->getEnvironment();
+
+        //determine time offset
+        $offset = 0;
+
+        if ($database) {
+            if (isset($environment[$database]['time_offset'])) {
+                $offset = $environment[$database]['time_offset'];
+            }
+        } else {
+            $database = $report->getDatabase();
+            if (isset($database['time_offset'])) {
+                $offset = $database['time_offset'];
+            }
+        }
+
+        //if the time needs to be adjusted
+        if ($offset) {
+            $time = strtotime((($offset > 0) ? '+' : '-').abs($offset).' hours', $time);
+        }
+
+        //determine output format
+        if ($format) {
+            $time = date($format, $time);
+        } elseif ($var && isset($var['format'])) {
+            $time = date($var['format'], $time);
+        } else {
+            //default to Y-m-d H:i:s
+            $time = date('Y-m-d H:i:s', $time);
+        }
+
+        return $time;
+    }
+
+    public static function generateSqlIN($column, $values, $or_null = false)
+    {
+        $sql = "$column IN (";
+        foreach ($values as $value) {
+            $sql .= is_numeric($value) ? $value : "'$value'";
+            if ($value !== end($values)) {
+                $sql .= ', ';
+            }
+        }
+        $sql .= ")";
+        if ($or_null) {
+            $sql .= " OR $column IS NULL";
+        }
+
+        return $sql;
+    }
+
+    public static function render($template, $macros)
+    {
+        $default = array(
+            'base' => self::$request->base,
+            'report_list_url' => self::$request->base.'/',
+            'request' => self::$request,
+            'querystring' => (array_key_exists('QUERY_STRING', $_SERVER) ? $_SERVER['QUERY_STRING'] : null),
+            'config' => self::$config,
+            'environment' => $_SESSION['environment'],
+            'recent_reports' => self::getRecentReports(),
+            'session' => $_SESSION,
+        );
+        $macros = array_merge($default, $macros);
+
+        //if a template path like 'html/report' is given, add the twig file extension
+        if (preg_match('/^[a-zA-Z_\-0-9\/]+$/', $template)) {
+            $template .= '.twig';
+        }
+
+        return self::$twig->render($template, $macros);
+    }
+
+    public static function renderString($template, $macros)
+    {
+        return self::$twig_string->render($template, $macros);
+    }
+
+    public static function displayReport($report, $type)
+    {
+        $classname = ucfirst(strtolower($type)).'ReportFormat';
+
+        $error_header = 'An error occurred while running your report';
+        $content = '';
+
+        try {
+            if (!class_exists($classname)) {
+                $error_header = 'Unknown report format';
+                throw new Exception("Unknown report format '$type'");
+            }
+
+            try {
+                $report = $classname::prepareReport($report);
+            } catch (Exception $e) {
+                $error_header = 'An error occurred while preparing your report';
+                throw $e;
+            }
+
+            $classname::display($report, self::$request);
+
+            if (isset($report->options['Query_Formatted'])) {
+                $content = $report->options['Query_Formatted'];
+            }
+        } catch (Exception $e) {
+            echo self::render('html/page', array(
+                'title' => $report->report,
+                'header' => '

'.$error_header.'

', + 'error' => $e->getMessage(), + 'content' => $content, + 'breadcrumb' => array('Report List' => '', $report->report => true), + )); + } + } + + public static function editReport($report) + { + $template_vars = array(); + + try { + $report = ReportFormatBase::prepareReport($report); + + $template_vars = array( + 'report' => $report->report, + 'options' => $report->options, + 'contents' => $report->getRaw(), + 'extension' => array_pop(explode('.', $report->report)), + ); + } catch (Exception $e) { + //if there is an error parsing the report + $template_vars = array( + 'report' => $report, + 'contents' => Report::getReportFileContents($report), + 'options' => array(), + 'extension' => array_pop(explode('.', $report)), + 'error' => $e, + ); + } + + if (isset($_POST['preview'])) { + echo "
".SimpleDiff::htmlDiffSummary($template_vars['contents'], $_POST['contents'])."
"; + } elseif (isset($_POST['save'])) { + Report::setReportFileContents($template_vars['report'], $_POST['contents']); + } else { + echo self::render('html/report_editor', $template_vars); + } + } + + public static function listReports() + { + $errors = array(); + + $reports = self::getReports(self::$config['reportDir'].'/', $errors); + + $template_vars['reports'] = $reports; + $template_vars['report_errors'] = $errors; + + $start = microtime(true); + echo self::render('html/report_list', $template_vars); + } + + public static function listDashboards() + { + $dashboards = self::getDashboards(); + + uasort($dashboards, function ($a, $b) { + return strcmp($a['title'], $b['title']); + }); + + echo self::render('html/dashboard_list', array( + 'dashboards' => $dashboards, + )); + } + + public static function displayDashboard($dashboard) + { + $content = self::getDashboard($dashboard); + + echo self::render('html/dashboard', array( + 'dashboard' => $content, + )); + } + + public static function getDashboards() + { + $dashboards = glob(PhpReports::$config['dashboardDir'].'/*.json'); + + $ret = array(); + foreach ($dashboards as $key => $value) { + $name = basename($value, '.json'); + $ret[$name] = self::getDashboard($name); + } + + return $ret; + } + + public static function getDashboard($dashboard) + { + $file = PhpReports::$config['dashboardDir'].'/'.$dashboard.'.json'; + if (!file_exists($file)) { + throw new Exception("Unknown dashboard - ".$dashboard); + } + + return json_decode(file_get_contents($file), true); + } + + public static function getRecentReports() + { + $recently_run = FileSystemCache::retrieve(FileSystemCache::generateCacheKey('recently_run')); + $recent = array(); + if ($recently_run !== false) { + $i = 0; + foreach ($recently_run as $report) { + if ($i > 10) { + break; + } + + $headers = self::getReportHeaders($report); + + if (!$headers) { + continue; + } + if (isset($recent[$headers['url']])) { + continue; + } + + $recent[$headers['url']] = $headers; + $i++; + } + } + + return array_values($recent); + } + public static function getReportListJSON($reports = null) + { + if ($reports === null) { + $errors = array(); + $reports = self::getReports(self::$config['reportDir'].'/', $errors); + } + + //weight by popular reports + $recently_run = FileSystemCache::retrieve(FileSystemCache::generateCacheKey('recently_run')); + $popular = array(); + if ($recently_run !== false) { + foreach ($recently_run as $report) { + if (!isset($popular[$report])) { + $popular[$report] = 1; + } else { + $popular[$report]++; + } + } + } + $parts = array(); + + foreach ($reports as $report) { + if ($report['is_dir'] && $report['children']) { + //skip if the directory doesn't have a title + if (!isset($report['Title']) || !$report['Title']) { + continue; + } + + $part = trim(self::getReportListJSON($report['children']), '[],'); + if ($part) { + $parts[] = $part; + } + } else { + //skip if report is marked as dangerous + if ((isset($report['stop']) && $report['stop']) || isset($report['Caution']) || isset($report['warning'])) { + continue; + } + if (!isset($report['url'])) { + continue; + } + if (!isset($report['report'])) { + continue; + } + + //skip if report is marked as ignore + if (isset($report['ignore']) && $report['ignore']) { + continue; + } + + if (isset($popular[$report['report']])) { + $popularity = $popular[$report['report']]; + } else { + $popularity = 0; + } + + $parts[] = json_encode(array( + 'name' => $report['Name'], + 'url' => $report['url'], + 'popularity' => $popularity, + )); + } + } + + return '['.trim(implode(',', $parts), ',').']'; + } + + protected static function getReportHeaders($report) + { + $cacheKey = FileSystemCache::generateCacheKey(array(self::$request->base, $report), 'report_headers'); + + //check if report data is cached and newer than when the report file was created + //the url parameter ?nocache will bypass this and not use cache + $data = false; + + $loc = Report::getFileLocation($report); + if (!file_exists($loc)) { + return false; + } + if (!isset($_REQUEST['nocache'])) { + $data = FileSystemCache::retrieve($cacheKey, filemtime($loc)); + } + + //report data not cached, need to parse it + if ($data === false) { + $temp = new Report($report); + + $data = $temp->options; + + $data['report'] = $report; + $data['url'] = self::$request->base.'/report/html/?report='.$report; + $data['is_dir'] = false; + $data['Id'] = str_replace(array('_', '-', '/', ' ', '.'), array('', '', '_', '-', '_'), trim($report, '/')); + if (!isset($data['Name'])) { + $data['Name'] = ucwords(str_replace(array('_', '-'), ' ', basename($report))); + } + + //store parsed report in cache + FileSystemCache::store($cacheKey, $data); + } + + return $data; + } + + protected static function getReports($dir, &$errors = null) + { + $base = self::$config['reportDir'].'/'; + + $reports = glob($dir.'*', GLOB_NOSORT); + $return = array(); + foreach ($reports as $key => $report) { + $title = $description = false; + + if (is_dir($report)) { + if (file_exists($report.'/TITLE.txt')) { + $title = file_get_contents($report.'/TITLE.txt'); + } + if (file_exists($report.'/README.txt')) { + $description = file_get_contents($report.'/README.txt'); + } + + $id = str_replace(array('_', '-', '/', ' '), array('', '', '_', '-'), trim(substr($report, strlen($base)), '/')); + + $children = self::getReports($report.'/', $errors); + + $count = 0; + foreach ($children as $child) { + if (isset($child['count'])) { + $count += $child['count']; + } else { + $count++; + } + } + + $return[] = array( + 'Name' => ucwords(str_replace(array('_', '-'), ' ', basename($report))), + 'Title' => $title, + 'Id' => $id, + 'Description' => $description, + 'is_dir' => true, + 'children' => $children, + 'count' => $count, + ); + } else { + //files to skip + if (strpos(basename($report), '.') === false) { + continue; + } + $reportExploded = explode('.', $report); + $ext = array_pop($reportExploded); + if (!isset(self::$config['default_file_extension_mapping'][$ext])) { + continue; + } + + $name = substr($report, strlen($base)); + + try { + $data = self::getReportHeaders($name, $base); + $return[] = $data; + } catch (Exception $e) { + if (!$errors) { + $errors = array(); + } + $errors[] = array( + 'report' => $name, + 'exception' => $e, + ); + } + } + } + + usort($return, function (&$a, &$b) { + if ($a['is_dir'] && !$b['is_dir']) { + return 1; + } elseif ($b['is_dir'] && !$a['is_dir']) { + return -1; + } + + if (empty($a['Title']) && empty($b['Title'])) { + return strcmp($a['Name'], $b['Name']); + } elseif (empty($a['Title'])) { + return 1; + } elseif (empty($b['Title'])) { + return -1; + } + + return strcmp($a['Title'], $b['Title']); + }); + + return $return; + } + + /** + * Emails a report given a TO address, a subject, and a message + */ + public static function emailReport() + { + if (!isset($_REQUEST['email']) || !filter_var($_REQUEST['email'], FILTER_VALIDATE_EMAIL)) { + echo json_encode(array('error' => 'Valid email address required')); + + return; + } + if (!isset($_REQUEST['url'])) { + echo json_encode(array('error' => 'Report url required')); + + return; + } + if (!isset(PhpReports::$config['mail_settings']['enabled']) || !PhpReports::$config['mail_settings']['enabled']) { + echo json_encode(array('error' => 'Email is disabled on this server')); + + return; + } + if (!isset(PhpReports::$config['mail_settings']['from'])) { + echo json_encode(array('error' => 'Email settings have not been properly configured on this server')); + + return; + } + + $from = PhpReports::$config['mail_settings']['from']; + $subject = $_REQUEST['subject'] ? $_REQUEST['subject'] : 'Database Report'; + $body = $_REQUEST['message'] ? $_REQUEST['message'] : "You've been sent a database report!"; + $email = $_REQUEST['email']; + $link = $_REQUEST['url']; + $csv_link = str_replace('report/html/?', 'report/csv/?', $link); + $table_link = str_replace('report/html/?', 'report/table/?', $link); + $text_link = str_replace('report/html/?', 'report/text/?', $link); + + // Get the CSV file attachment and the inline HTML table + $csv = self::urlDownload($csv_link); + $table = self::urlDownload($table_link); + $text = self::urlDownload($text_link); + + $email_text = $body."\n\n".$text."\n\nView the report online at $link"; + $email_html = "

$body

$table

View the report online at ".htmlentities($link)."

"; + + // Create the message + $message = Swift_Message::newInstance() + ->setSubject($subject) + ->setFrom($from) + ->setTo($email) + //text body + ->setBody($email_text) + //html body + ->addPart($email_html, 'text/html') + ; + + $attachment = Swift_Attachment::newInstance() + ->setFilename('report.csv') + ->setContentType('text/csv') + ->setBody($csv) + ; + + $message->attach($attachment); + + // Create the Transport + $transport = self::getMailTransport(); + $mailer = Swift_Mailer::newInstance($transport); + + try { + // Send the message + $result = $mailer->send($message); + } catch (Exception $e) { + echo json_encode(array( + 'error' => $e->getMessage(), + )); + + return; + } + + if ($result) { + echo json_encode(array( + 'success' => true, + )); + } else { + echo json_encode(array( + 'error' => 'Failed to send email to requested recipient', + )); + } + } + + /** + * Determines the email transport to use based on the configuration settings + */ + protected static function getMailTransport() + { + if (!isset(PhpReports::$config['mail_settings'])) { + PhpReports::$config['mail_settings'] = array(); + } + if (!isset(PhpReports::$config['mail_settings']['method'])) { + PhpReports::$config['mail_settings']['method'] = 'mail'; + } + + switch (PhpReports::$config['mail_settings']['method']) { + case 'mail': + return Swift_MailTransport::newInstance(); + case 'sendmail': + return Swift_MailTransport::newInstance( + isset(PhpReports::$config['mail_settings']['command']) ? PhpReports::$config['mail_settings']['command'] : '/usr/sbin/sendmail -bs' + ); + case 'smtp': + if (!isset(PhpReports::$config['mail_settings']['server'])) { + throw new Exception("SMTP server must be configured"); + } + $transport = Swift_SmtpTransport::newInstance( + PhpReports::$config['mail_settings']['server'], + isset(PhpReports::$config['mail_settings']['port']) ? PhpReports::$config['mail_settings']['port'] : 25 + ); + + //if username/password + if (isset(PhpReports::$config['mail_settings']['username'])) { + $transport->setUsername(PhpReports::$config['mail_settings']['username']); + $transport->setPassword(PhpReports::$config['mail_settings']['password']); + } + + //if using encryption + if (isset(PhpReports::$config['mail_settings']['encryption'])) { + $transport->setEncryption(PhpReports::$config['mail_settings']['encryption']); + } + + return $transport; + default: + throw new Exception("Mail method must be either 'mail', 'sendmail', or 'smtp'"); + } + } + + /** + * Autoloader methods + */ + public static function loader($className) + { + if (!isset(self::$loader_cache)) { + self::buildLoaderCache(); + } + + if (isset(self::$loader_cache[$className])) { + require_once self::$loader_cache[$className]; + + return true; + } else { + return false; + } + } + public static function buildLoaderCache() + { + self::load('classes/local'); + self::load('classes', array('classes/local')); + self::load('lib'); + } + public static function load($dir, $skip = array()) + { + $files = glob($dir.'/*.php'); + $dirs = glob($dir.'/*', GLOB_ONLYDIR); + + foreach ($files as $file) { + //for file names same as class name + $className = basename($file, '.php'); + if (!isset(self::$loader_cache[$className])) { + self::$loader_cache[$className] = $file; + } + + //for PEAR style: Path_To_Class.php + $parts = explode('/', substr($file, 0, -4)); + array_shift($parts); + $className = implode('_', $parts); + //if any of the directories in the path are lowercase, it isn't in PEAR format + if (preg_match('/(^|_)[a-z]/', $className)) { + continue; + } + if (!isset(self::$loader_cache[$className])) { + self::$loader_cache[$className] = $file; + } + } + + foreach ($dirs as $dir2) { + //directories to skip + if ($dir2[0] === '.') { + continue; + } + if (in_array($dir2, $skip)) { + continue; + } + if (in_array(basename($dir2), array('tests', 'test', 'example', 'examples', 'bin'))) { + continue; + } + + self::load($dir2, $skip); + } + } + + /** + * A more lenient json_decode than the built-in PHP one. + * It supports strict JSON as well as javascript syntax (i.e. unquoted/single quoted keys, single quoted values, trailing commmas) + */ + public static function json_decode($json, $assoc = false) + { + //replace single quoted values + $json = preg_replace_callback('/:\s*\'(([^\']|\\\\\')*)\'\s*([},])/', create_function('$matches', 'return "\':\'.json_encode(stripslashes(\'$matches[1]\')).\'$matches[3]\'";'), $json); + + //replace single quoted keys + $json = preg_replace_callback('/\'(([^\']|\\\\\')*)\'\s*:/', create_function('$matches', 'return "json_encode(stripslashes(\'$matches[1]\')).\':\'";'), $json); + + //remove any line breaks in the code + $json = str_replace(array("\n", "\r"), "", $json); + + //replace non-quoted keys with double quoted keys + $json = preg_replace('#(?
\{|\[|,)\s*(?(?:\w|_)+)\s*:#im', '$1"$2":', $json);
+
+        //remove trailing comma
+        $json = preg_replace('/,\s*\}/', '}', $json);
+
+        return json_decode($json, $assoc);
+    }
+
+    protected static function urlDownload($url)
+    {
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_HEADER, 0);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+        $output = curl_exec($ch);
+        curl_close($ch);
+
+        return $output;
+    }
 }
 PhpReports::init();
diff --git a/lib/PhpReports/Report.php b/lib/PhpReports/Report.php
index 7164439b..4328a900 100644
--- a/lib/PhpReports/Report.php
+++ b/lib/PhpReports/Report.php
@@ -1,623 +1,720 @@
 report = $report;
-		
-		if(!file_exists(self::getFileLocation($report))) {
-			throw new Exception('Report not found - '.$report);
-		}
-		
-		$this->filemtime = filemtime(self::getFileLocation($report));
-		
-		$this->use_cache = $use_cache;
-		
-		//get the raw report file
-		$this->raw = self::getReportFileContents($report);
-		
-		//if there are no headers in this report
-		if(strpos($this->raw,"\n\n") === false) {
-			throw new Exception('Report missing headers - '.$report);
-		}
-		
-		//split the raw report into headers and code
-		list($this->raw_headers, $this->raw_query) = explode("\n\n",$this->raw,2);
-		
-		$this->macros = array();
-		foreach($macros as $key=>$value) {
-			$this->addMacro($key,$value);
-		}
-		
-		$this->parseHeaders();
-		
-		$this->options['Environment'] = $environment;
-		
-		$this->initDb();
-		
-		$this->getTimeEstimate();
-	}
-	
-	public static function getFileLocation($report) {
-		//make sure the report path doesn't go up a level for security reasons
-		if(strpos($report,"..")!==false) {
-			$reportdir = realpath(PhpReports::$config['reportDir']).'/';
-			$reportpath = substr(realpath(PhpReports::$config['reportDir'].'/'.$report),0,strlen($reportdir));
-			
-			if($reportpath !== $reportdir) throw new Exception('Invalid report - '.$report);
-		}
-		
-		$reportDir = PhpReports::$config['reportDir'];
-		return $reportDir.'/'.$report;
-	}
-	
-	public static function setReportFileContents($report, $new_contents) {
-		echo "SAVING CONTENTS TO ".self::getFileLocation($report);
-		
-		if(!file_put_contents(self::getFileLocation($report),$new_contents)) {
-			throw new Exception("Failed to set report contents");
-		}
-		
-		echo "\n".$new_contents;
-	}
-	public static function getReportFileContents($report) {
-		$contents = file_get_contents(self::getFileLocation($report));
-		
-		//convert EOL to unix format
-		return str_replace(array("\r\n","\r"),"\n",$contents);
-	}
-	
-	public function getDatabase() {
-		if(isset($this->options['Database']) && $this->options['Database']) {
-			$environment = $this->getEnvironment();
-			
-			if(isset($environment[$this->options['Database']])) {
-				return $environment[$this->options['Database']];
-			}
-		}
-		
-		return array();
-	}
-	public function getEnvironment() {
-		return PhpReports::$config['environments'][$this->options['Environment']];
-	}
-	
-	public function addMacro($name, $value) {
-		$this->macros[$name] = $value;
-	}
-	public function exportHeader($name,$params) {
-		$this->exported_headers[] = array('name'=>$name,'params'=>$params);
-	}
-	
-	public function getCacheKey() {
-		return FileSystemCache::generateCacheKey(array(
-			'report'=>$this->report,
-			'macros'=>$this->macros,
-			'database'=>$this->options['Environment']
-		),'report_results');
-	}
-	public function getReportTimesCacheKey() {
-		return FileSystemCache::generateCacheKey($this->report,'report_times');
-	}
-
-	protected function retrieveFromCache() {
-		if(!$this->use_cache) {
-			return false;
-		}
-		
-		return FileSystemCache::retrieve($this->getCacheKey(),'results', $this->filemtime);
-	}
-	
-	protected function storeInCache() {
-		if(isset($this->options['Cache']) && is_numeric($this->options['Cache'])) {
-			$ttl = intval($this->options['Cache']);
-		}
-		else {
-			//default to caching things for 10 minutes
-			$ttl = 600;
-		}
-		
-		FileSystemCache::store($this->getCacheKey(), $this->options, 'results', $ttl);
-	}
-	
-	protected function parseHeaders() {
-		//default the report to being ready
-		//if undefined variables are found in the headers, set to false
-		$this->is_ready = true;
-		
-		$this->options = array(
-			'Filters'=>array(),
-			'Variables'=>array(),
-			'Includes'=>array(),
-		);
-		$this->headers = array();
-		
-		$lines = explode("\n",$this->raw_headers);
-		
-		//remove empty headers and remove comment characters
-		$fixed_lines = array();
-		foreach($lines as $line) {
-			if(empty($line)) continue;
-			
-			//if the line doesn't start with a comment character, skip
-			if(!in_array(substr($line,0,2),array('--','/*','//',' *')) && $line[0] !== '#') continue;
-			
-			//remove comment from start of line and skip if empty
-			$line = trim(ltrim($line,"-*/# \t"));
-			if(!$line) continue;
-			
-			$fixed_lines[] = $line;
-		}
-		$lines = $fixed_lines;
-		
-		$name = null;
-		$value = '';
-		foreach($lines as $line) {
-			$has_name_value = preg_match('/^\s*[A-Z0-9_\-]+\s*\:/',$line);
-		
-			//if this is the first header and not in the format name:value, assume it is the report name
-			if(!$has_name_value && $name === null && (!isset($this->options['Name']) || !$this->options['Name'])) {
-				$this->parseHeader('Info',array('name'=>$line));
-			}
-			else {
-				//if this is a continuation of another header
-				if(!$has_name_value) {
-					$value .= "\n".trim($line);
-				}
-				//if this is a new header
-				else {
-					//if the previous header didn't have a name, assume it is the description
-					if($value && $name === null) {
-						$this->parseHeader('Info',array('description'=>$value));
-					}
-					//otherwise, parse the previous header
-					elseif($value) {
-						$this->parseHeader($name,$value);
-					}
-					
-					list($name,$value) = explode(':',$line,2);
-					$name = trim($name);
-					$value = trim($value);
-				
-					if(strtoupper($name) === $name) $name = ucfirst(strtolower($name));
-				}
-			}
-		}
-		//parse the last header
-		if($value && $name) {
-			$this->parseHeader($name,$value);
-		}
-		
-		//try to infer report type from file extension
-		if(!isset($this->options['Type'])) {
-			$file_type = array_pop(explode('.',$this->report));
-			
-			if(!isset(PhpReports::$config['default_file_extension_mapping'][$file_type])) {
-				throw new Exception("Unknown report type - ".$this->report);
-			}
-			else {
-				$this->options['Type'] = PhpReports::$config['default_file_extension_mapping'][$file_type];
-			}
-		}
-		
-		if(!isset($this->options['Database'])) $this->options['Database'] = strtolower($this->options['Type']);
-		
-		if(!isset($this->options['Name'])) $this->options['Name'] = $this->report;
-	}
-	
-	public function parseHeader($name,$value,$dataset=null) {
-		$classname = $name.'Header';
-		if(class_exists($classname)) {
-			if($dataset !== null && isset($classname::$validation) && isset($classname::$validation['dataset'])) $value['dataset'] = $dataset;
-			$classname::parse($name,$value,$this);
-			if(!in_array($name,$this->headers)) $this->headers[] = $name;
-		}
-		else {
-			throw new Exception("Unknown header '$name' - ".$this->report);
-		}
-	}
-	
-	public function addFilter($dataset, $column, $type, $options) {
-		// If adding for multiple datasets
-		if(is_array($dataset)) {
-			foreach($dataset as $d) {
-				$this->addFilter($d,$column,$type,$options);
-			}
-		}
-		// If adding for all datasets
-		else if($dataset === true) {
-			$this->addFilter('all',$column,$type,$options);
-		}
-		// If adding for a single dataset
-		else {
-			if(!isset($this->filters[$dataset])) $this->filters[$dataset] = array();
-			if(!isset($this->filters[$dataset][$column])) $this->filters[$dataset][$column] = array();
-			
-			$this->filters[$dataset][$column][$type] = $options;
-		}
-		
-	}
-	protected function applyFilters($dataset, $column, $value, $row) {
-		// First, apply filters for all datasets
-		if(isset($this->filters['all']) && isset($this->filters['all'][$column])) {
-			foreach($this->filters['all'][$column] as $type=>$options) {
-				$classname = $type.'Filter';
-				$value = $classname::filter($value, $options, $this, $row);
-				
-				//if the column should not be displayed
-				if($value === false) return false;
-			}
-		}
-		
-		// Then apply filters for this specific dataset
-		if(isset($this->filters[$dataset]) && isset($this->filters[$dataset][$column])) { 
-			foreach($this->filters[$dataset][$column] as $type=>$options) {
-				$classname = $type.'Filter';
-				$value = $classname::filter($value, $options, $this, $row);
-				
-				//if the column should not be displayed
-				if($value === false) return false;
-			}
-		}
-		
-		return $value;
-	}
-	
-	protected function initDb() {
-		//if the database isn't set, use the first defined one from config
-		$environments = PhpReports::$config['environments'];
-		if(!$this->options['Environment']) {
-			$this->options['Environment'] = current(array_keys($environments));
-		}
-		
-		//set database options
-		$environment_options = array();
-		foreach($environments as $key=>$params) {
-			$environment_options[] = array(
-				'name'=>$key,
-				'selected'=>$key===$this->options['Environment']
-			);
-		}
-		$this->options['Environments'] = $environment_options;
-		
-		//add a host macro
-		if(isset($environments[$this->options['Environment']]['host'])) {
-			$this->macros['host'] = $environments[$this->options['Environment']]['host'];
-		}
-		
-		$classname = $this->options['Type'].'ReportType';
-		
-		if(!class_exists($classname)) {
-			throw new exception("Unknown report type '".$this->options['Type']."'");
-		}
-		
-		$classname::init($this);
-	}
-	
-	public function getRaw() {
-		return $this->raw;
-	}
-	public function getUrl() {
-		return 'report/html/?report='.urlencode($this->report);
-	}
-	
-	public function prepareVariableForm() {
-		$vars = array();
-		
-		if($this->options['Variables']) {
-			foreach($this->options['Variables'] as $var => $params) {
-				if(!isset($params['name'])) $params['name'] = ucwords(str_replace(array('_','-'),' ',$var));
-				if(!isset($params['type'])) $params['type'] = 'string';
-				if(!isset($params['options'])) $params['options'] = false;
-				$params['value'] = $this->macros[$var];
-				$params['key'] = $var;
-				
-				if($params['type'] === 'select') {
-					$params['is_select'] = true;
-					
-					foreach($params['options'] as $key=>$option) {
-						if(!is_array($option)) {
-							$params['options'][$key] = array(
-								'display'=>$option,
-								'value'=>$option
-							);
-						}
-						if($params['options'][$key]['value'] == $params['value']) $params['options'][$key]['selected'] = true;
-						elseif(is_array($params['value']) && in_array($params['options'][$key]['value'],$params['value'])) $params['options'][$key]['selected'] = true;
-						else $params['options'][$key]['selected'] = false;
-						
-						if($params['multiple']) {
-							$params['is_multiselect'] = true;
-							$params['choices'] = count($params['options']);
-						}
-					}
-				}
-				else {
-					if($params['multiple']) {
-						$params['is_textarea'] = true;
-					}
-				}
-				
-				if(isset($params['modifier_options'])) {
-					$modifier_value = isset($this->macros[$var.'_modifier'])? $this->macros[$var.'_modifier'] : null;
-					
-					foreach($params['modifier_options'] as $key=>$option) {
-						if(!is_array($option)) {
-							$params['modifier_options'][$key] = array(
-								'display'=>$option,
-								'value'=>$option
-							);
-						}
-						
-						if($params['modifier_options'][$key]['value'] == $modifier_value) $params['modifier_options'][$key]['selected'] = true;
-						else $params['modifier_options'][$key]['selected'] = false;
-					}
-					
-				}
-				
-				$vars[] = $params;
-			}	
-		}
-		
-		return $vars;
-	}
-	
-	protected function _runReport() {
-		if(!$this->is_ready) {
-			throw new Exception("Report is not ready.  Missing variables");
-		}
-		
-		PhpReports::setVar('Report',$this);
-		
-		//release the write lock on the session file
-		//so the session isn't locked while the report is running
-		session_write_close();
-		
-		$classname = $this->options['Type'].'ReportType';
-		
-		if(!class_exists($classname)) {
-			throw new exception("Unknown report type '".$this->options['Type']."'");
-		}
-		
-		foreach($this->headers as $header) {
-			$headerclass = $header.'Header';
-			$headerclass::beforeRun($this);
-		}
-		
-		$classname::openConnection($this);
-		$datasets = $classname::run($this);		
-		$classname::closeConnection($this);
-		
-		// Convert old single dataset format to multi-dataset format
-		if(!isset($datasets[0]['rows']) || !is_array($datasets[0]['rows'])) {
-			$datasets = array(
-				array(
-					'rows'=>$datasets
-				)
-			);
-		}
-
-		// Only include a subset of datasets
-		$include = array_keys($datasets);
-		if(isset($_GET['dataset'])) {
-			$include = array($_GET['dataset']);
-		}
-		elseif(isset($_GET['datasets'])) {
-			// If just a single dataset was specified, make it an array
-			if(!is_array($_GET['datasets'])) {
-				$include = explode(',',$_GET['datasets']);
-			}
-			else {
-				$include = $_GET['datasets'];
-			}
-		}
-
-		$this->options['DataSets'] = array();
-		foreach($include as $i) {
-			if(!isset($datasets[$i])) continue;
-			$this->options['DataSets'][$i] = $datasets[$i];
-		}
-
-		$this->parseDynamicHeaders();
-	}
-
-	protected function parseDynamicHeaders() {
-		foreach($this->options['DataSets'] as $i=>&$dataset) {
-			if(isset($dataset['headers'])) {
-				foreach($dataset['headers'] as $j=>$header) {
-					if(isset($header['header']) && isset($header['value'])) {
-						$this->parseHeader($header['header'],$header['value'],$i);
-					}
-				}
-			}
-		}
-	}
-	
-	protected function getTimeEstimate() {
-		$report_times = FileSystemCache::retrieve($this->getReportTimesCacheKey());
-		if(!$report_times) return;
-		
-		sort($report_times);
-		
-		$sum = array_sum($report_times);
-		$count = count($report_times);
-		$average = $sum/$count;
-		$quartile1 = $report_times[round(($count-1)/4)];
-		$median = $report_times[round(($count-1)/2)];
-		$quartile3 = $report_times[round(($count-1)*3/4)];
-		$min = min($report_times);
-		$max = max($report_times);
-		$iqr = $quartile3-$quartile1;
-		$range = (1.5)*$iqr;
-		
-		$sample_square = 0;
-		for($i = 0; $i < $count; $i++) {
-			$sample_square += pow($report_times[$i], 2);
-		}
-		$standard_deviation = sqrt($sample_square / $count - pow(($average), 2));
-		
-		$this->options['time_estimate'] = array(
-			'times'=>$report_times,
-			'count'=>$count,
-			'min'=>round($min,2),
-			'max'=>round($max,2),
-			'median'=>round($median,2),
-			'average'=>round($average,2),
-			'q1'=>round($quartile1,2),
-			'q3'=>round($quartile3,2),
-			'iqr'=>round($range,2),
-			'sum'=>round($sum,2),
-			'stdev'=>round($standard_deviation,2)
-		);
-	}
-	protected function prepareDataSets() {
-		foreach($this->options['DataSets'] as $i=>$dataset) {
-			$this->prepareRows($i);
-		}
-		if(isset($this->options['DataSets'][0])) {
-			$this->options['Rows'] = $this->options['DataSets'][0]['rows'];
-			$this->options['Count'] = $this->options['DataSets'][0]['count'];
-		}
-	}
-	protected function prepareRows($dataset) {
-		$rows = array();
-		
-		//generate list of all values for each numeric column
-		//this is used to calculate percentiles/averages/etc.
-		$vals = array();
-		foreach($this->options['DataSets'][$dataset]['rows'] as $row) {
-			foreach($row as $key=>$value) {
-				if(!isset($vals[$key])) $vals[$key] = array();
-				
-				if(is_numeric($value)) $vals[$key][] = $value;
-			}
-		}
-		$this->options['DataSets'][$dataset]['values'] = $vals;
-		
-		foreach($this->options['DataSets'][$dataset]['rows'] as $row) {
-			$rowval = array();
-			
-			$i=1;
-			foreach($row as $key=>$value) {
-				$val = new ReportValue($i, $key, $value);
-				
-				//apply filters for the column key
-				$val = $this->applyFilters($dataset,$key,$val,$row);
-				//apply filters for the column position
-				if($val) $val = $this->applyFilters($dataset,$i,$val,$row);
-				
-				if($val) {
-					$rowval[] = $val;
-				}
-				
-				$i++;
-			}
-			
-			$first = !$rows;
-			
-			$rows[] = array(
-				'values'=>$rowval,
-				'first'=>$first
-			);
-		}
-		
-		$this->options['DataSets'][$dataset]['rows'] = $rows;
-		$this->options['DataSets'][$dataset]['count'] = count($rows);
-	}
-	
-	public function run() {
-		if($this->has_run) return true;
-		
-		//at this point, all the headers are parsed and we haven't run the report yet
-		foreach($this->headers as $header) {
-			$classname = $header.'Header';
-			$classname::afterParse($this);
-		}
-		
-		//record how long it takes to run the report
-		$start = microtime(true);
-		
-		if($this->is_ready && !$this->async) {
-			//if the report is cached
-			if($options = $this->retrieveFromCache()) {				
-				$this->options = $options;
-				$this->options['FromCache'] = true;
-			}
-			else {
-				$this->_runReport();
-				$this->prepareDataSets();
-				$this->storeInCache();
-			}
-
-			//add this to the list of recently run reports
-			$recently_run_key = FileSystemCache::generateCacheKey('recently_run');
-			$recently_run = FileSystemCache::retrieve($recently_run_key);
-			if($recently_run === false) {
-				$recently_run = array();
-			}
-			array_unshift($recently_run,$this->report);
-			if(count($recently_run) > 200) $recently_run = array_slice($recently_run,0,200);
-			FileSystemCache::store($recently_run_key,$recently_run);
-		}
-		
-		//call the beforeRender callback for each header
-		foreach($this->headers as $header) {
-			$classname = $header.'Header';
-			$classname::beforeRender($this);
-		}
-		
-		$this->options['Time'] = round(microtime(true) - $start,5);
-		
-		if($this->is_ready && !$this->async && !isset($this->options['FromCache'])) {
-			//get current report times for this report
-			$report_times = FileSystemCache::retrieve($this->getReportTimesCacheKey());
-			if(!$report_times) $report_times = array();
-			//only keep the last 10 times for each report
-			//this keeps the timing data up to date and relevant
-			if(count($report_times) > 10) array_shift($report_times);
-			
-			//store report times
-			$report_times[] = $this->options['Time'];
-			FileSystemCache::store($this->getReportTimesCacheKey(), $report_times);
-		}
-		
-		$this->has_run = true;
-	}
-	
-	public function renderReportPage($template='html/report', $additional_vars = array()) {
-		$this->run();
-		
-		$template_vars = array(
-			'is_ready'=>$this->is_ready,
-			'async'=>$this->async,
-			'report_url'=>PhpReports::$request->base.'/report/?'.$_SERVER['QUERY_STRING'],
-			'report_querystring'=>$_SERVER['QUERY_STRING'],
-			'base'=>PhpReports::$request->base,
-			'report'=>$this->report,
-			'vars'=>$this->prepareVariableForm(),
-			'macros'=>$this->macros,
-		);
-		
-		$template_vars = array_merge($template_vars,$additional_vars);
-		
-		$template_vars = array_merge($template_vars,$this->options);
-		
-		return PhpReports::render($template, $template_vars);
-	}
+class Report
+{
+    public $report;
+    public $macros = [];
+    public $exported_headers = [];
+    public $options = [];
+    public $is_ready = false;
+    public $async = false;
+    public $headers = [];
+    public $header_lines = [];
+    public $raw_query;
+    public $use_cache;
+
+    protected $raw;
+    protected $raw_headers;
+    protected $filters = [];
+    protected $filemtime;
+    protected $has_run = false;
+
+    public function __construct($report, $macros = [], $environment = null, $use_cache = null)
+    {
+        $this->report = $report;
+
+        if (!file_exists(self::getFileLocation($report))) {
+            throw new Exception('Report not found - '.$report);
+        }
+
+        $this->filemtime = filemtime(self::getFileLocation($report));
+
+        $this->use_cache = $use_cache;
+
+        //get the raw report file
+        $this->raw = self::getReportFileContents($report);
+
+        //if there are no headers in this report
+        if (strpos($this->raw, "\n\n") === false) {
+            throw new Exception('Report missing headers - '.$report);
+        }
+
+        //split the raw report into headers and code
+        list($this->raw_headers, $this->raw_query) = explode("\n\n", $this->raw, 2);
+
+        $this->macros = [];
+        foreach ($macros as $key => $value) {
+            $this->addMacro($key, $value);
+        }
+
+        $this->parseHeaders();
+
+        $this->options['Environment'] = $environment;
+
+        $this->initDb();
+
+        $this->getTimeEstimate();
+    }
+
+    public static function getFileLocation($report)
+    {
+        //make sure the report path doesn't go up a level for security reasons
+        if (strpos($report, "..") !== false) {
+            $reportdir = realpath(PhpReports::$config['reportDir']).'/';
+            $reportpath = substr(realpath(PhpReports::$config['reportDir'].'/'.$report), 0, strlen($reportdir));
+
+            if ($reportpath !== $reportdir) {
+                throw new Exception('Invalid report - '.$report);
+            }
+        }
+
+        $reportDir = PhpReports::$config['reportDir'];
+
+        return $reportDir.'/'.$report;
+    }
+
+    public static function setReportFileContents($report, $new_contents)
+    {
+        echo "SAVING CONTENTS TO ".self::getFileLocation($report);
+
+        if (!file_put_contents(self::getFileLocation($report), $new_contents)) {
+            throw new Exception("Failed to set report contents");
+        }
+
+        echo "\n".$new_contents;
+    }
+
+    public static function getReportFileContents($report)
+    {
+        $contents = file_get_contents(self::getFileLocation($report));
+
+        //convert EOL to unix format
+        return str_replace(["\r\n", "\r"], "\n", $contents);
+    }
+
+    public function getDatabase()
+    {
+        if (isset($this->options['Database']) && $this->options['Database']) {
+            $environment = $this->getEnvironment();
+
+            if (isset($environment[$this->options['Database']])) {
+                return $environment[$this->options['Database']];
+            }
+        }
+
+        return array();
+    }
+
+    public function getEnvironment()
+    {
+        return PhpReports::$config['environments'][$this->options['Environment']];
+    }
+
+    public function addMacro($name, $value)
+    {
+        $this->macros[$name] = $value;
+    }
+
+    public function exportHeader($name, $params)
+    {
+        $this->exported_headers[] = ['name' => $name, 'params' => $params];
+    }
+
+    public function getCacheKey()
+    {
+        return FileSystemCache::generateCacheKey(
+            [
+                'report' => $this->report,
+                'macros' => $this->macros,
+                'database' => $this->options['Environment'],
+            ],
+            'report_results'
+        );
+    }
+
+    public function getReportTimesCacheKey()
+    {
+        return FileSystemCache::generateCacheKey($this->report, 'report_times');
+    }
+
+    protected function retrieveFromCache()
+    {
+        if (!$this->use_cache) {
+            return false;
+        }
+
+        return FileSystemCache::retrieve($this->getCacheKey(), 'results', $this->filemtime);
+    }
+
+    protected function storeInCache()
+    {
+        if (isset($this->options['Cache']) && is_numeric($this->options['Cache'])) {
+            $ttl = intval($this->options['Cache']);
+        } else {
+            $ttl = 600; //default to caching things for 10 minutes
+        }
+
+        FileSystemCache::store($this->getCacheKey(), $this->options, 'results', $ttl);
+    }
+
+    protected function parseHeaders()
+    {
+        //default the report to being ready
+        //if undefined variables are found in the headers, set to false
+        $this->is_ready = true;
+
+        $this->options = [
+            'Filters' => [],
+            'Variables' => [],
+            'Includes' => [],
+        ];
+
+        $this->headers = [];
+
+        $lines = explode("\n", $this->raw_headers);
+
+        //remove empty headers and remove comment characters
+        $fixed_lines = [];
+
+        foreach ($lines as $line) {
+            if (empty($line)) {
+                continue;
+            }
+
+            //if the line doesn't start with a comment character, skip
+            if (!in_array(substr($line, 0, 2), ['--', '/*', '//', ' *']) && $line[0] !== '#') {
+                continue;
+            }
+
+            //remove comment from start of line and skip if empty
+            $line = trim(ltrim($line, "-*/# \t"));
+            if (!$line) {
+                continue;
+            }
+
+            $fixed_lines[] = $line;
+        }
+
+        $lines = $fixed_lines;
+
+        $name = null;
+        $value = '';
+        foreach ($lines as $line) {
+            $has_name_value = preg_match('/^\s*[A-Z0-9_\-]+\s*\:/', $line);
+
+            //if this is the first header and not in the format name:value, assume it is the report name
+            if (!$has_name_value && $name === null && (!isset($this->options['Name']) || !$this->options['Name'])) {
+                $this->parseHeader('Info', ['name' => $line]);
+            } else {
+                //if this is a continuation of another header
+                if (!$has_name_value) {
+                    $value .= "\n".trim($line);
+                } else {
+                    //if this is a new header
+                    //if the previous header didn't have a name, assume it is the description
+                    if ($value && $name === null) {
+                        $this->parseHeader('Info', ['description' => $value]);
+                    } elseif ($value) {
+                        //otherwise, parse the previous header
+                        $this->parseHeader($name, $value);
+                    }
+
+                    list($name, $value) = explode(':', $line, 2);
+                    $name = trim($name);
+                    $value = trim($value);
+
+                    if (strtoupper($name) === $name) {
+                        $name = ucfirst(strtolower($name));
+                    };
+                }
+            }
+        }
+        //parse the last header
+        if ($value && $name) {
+            $this->parseHeader($name, $value);
+        }
+
+        //try to infer report type from file extension
+        if (!isset($this->options['Type'])) {
+            $explodedReport = explode('.', $this->report);
+            $file_type = array_pop($explodedReport);
+
+            if (!isset(PhpReports::$config['default_file_extension_mapping'][$file_type])) {
+                throw new Exception("Unknown report type - ".$this->report);
+            } else {
+                $this->options['Type'] = PhpReports::$config['default_file_extension_mapping'][$file_type];
+            }
+        }
+
+        if (!isset($this->options['Database'])) {
+            $this->options['Database'] = strtolower($this->options['Type']);
+        }
+
+        if (!isset($this->options['Name'])) {
+            $this->options['Name'] = $this->report;
+        }
+    }
+
+    public function parseHeader($name, $value, $dataset = null)
+    {
+        $classname = $name.'Header';
+
+        if (class_exists($classname)) {
+            if ($dataset !== null && isset($classname::$validation) && isset($classname::$validation['dataset'])) {
+                $value['dataset'] = $dataset;
+            }
+
+            $classname::parse($name, $value, $this);
+
+            if (!in_array($name, $this->headers)) {
+                $this->headers[] = $name;
+            }
+        } else {
+            throw new Exception("Unknown header '$name' - ".$this->report);
+        }
+    }
+
+    public function addFilter($dataset, $column, $type, $options)
+    {
+        // If adding for multiple datasets
+        if (is_array($dataset)) {
+            foreach ($dataset as $d) {
+                $this->addFilter($d, $column, $type, $options);
+            }
+        } elseif ($dataset === true) {
+            // If adding for all datasets
+            $this->addFilter('all', $column, $type, $options);
+        } else {
+            // If adding for a single dataset
+            if (!isset($this->filters[$dataset])) {
+                $this->filters[$dataset] = [];
+            }
+
+            if (!isset($this->filters[$dataset][$column])) {
+                $this->filters[$dataset][$column] = [];
+            }
+
+            $this->filters[$dataset][$column][$type] = $options;
+        }
+    }
+
+    protected function applyFilters($dataset, $column, $value, $row)
+    {
+        // First, apply filters for all datasets
+        if (isset($this->filters['all']) && isset($this->filters['all'][$column])) {
+            foreach ($this->filters['all'][$column] as $type => $options) {
+                $classname = $type.'Filter';
+                $value = $classname::filter($value, $options, $this, $row);
+
+                //if the column should not be displayed
+                if ($value === false) {
+                    return false;
+                }
+            }
+        }
+
+        // Then apply filters for this specific dataset
+        if (isset($this->filters[$dataset]) && isset($this->filters[$dataset][$column])) {
+            foreach ($this->filters[$dataset][$column] as $type => $options) {
+                $classname = $type.'Filter';
+                $value = $classname::filter($value, $options, $this, $row);
+
+                //if the column should not be displayed
+                if ($value === false) {
+                    return false;
+                }
+            }
+        }
+
+        return $value;
+    }
+
+    protected function initDb()
+    {
+        //if the database isn't set, use the first defined one from config
+        $environments = PhpReports::$config['environments'];
+        if (!$this->options['Environment']) {
+            $this->options['Environment'] = current(array_keys($environments));
+        }
+
+        //set database options
+        $environment_options = [];
+        foreach ($environments as $key => $params) {
+            $environment_options[] = [
+                'name' => $key,
+                'selected' => ($key === $this->options['Environment']),
+            ];
+        }
+
+        $this->options['Environments'] = $environment_options;
+
+        //add a host macro
+        if (isset($environments[$this->options['Environment']]['host'])) {
+            $this->macros['host'] = $environments[$this->options['Environment']]['host'];
+        }
+
+        $classname = $this->options['Type'].'ReportType';
+
+        if (!class_exists($classname)) {
+            throw new Exception("Unknown report type '".$this->options['Type']."'");
+        }
+
+        $classname::init($this);
+    }
+
+    public function getRaw()
+    {
+        return $this->raw;
+    }
+
+    public function getUrl()
+    {
+        return 'report/html/?report='.urlencode($this->report);
+    }
+
+    public function prepareVariableForm()
+    {
+        $vars = [];
+
+        if ($this->options['Variables']) {
+            foreach ($this->options['Variables'] as $var => $params) {
+                if (!isset($params['name'])) {
+                    $params['name'] = ucwords(str_replace(['_', '-'], ' ', $var));
+                }
+                if (!isset($params['type'])) {
+                    $params['type'] = 'string';
+                }
+                if (!isset($params['options'])) {
+                    $params['options'] = false;
+                }
+                $params['value'] = $this->macros[$var];
+                $params['key'] = $var;
+
+                if ($params['type'] === 'select') {
+                    $params['is_select'] = true;
+
+                    foreach ($params['options'] as $key => $option) {
+                        if (!is_array($option)) {
+                            $params['options'][$key] = [
+                                'display' => $option,
+                                'value' => $option,
+                            ];
+                        }
+
+                        if ($params['options'][$key]['value'] == $params['value']) {
+                            $params['options'][$key]['selected'] = true;
+                        } elseif (is_array($params['value']) && in_array($params['options'][$key]['value'], $params['value'])) {
+                            $params['options'][$key]['selected'] = true;
+                        } else {
+                            $params['options'][$key]['selected'] = false;
+                        }
+
+                        if ($params['multiple']) {
+                            $params['is_multiselect'] = true;
+                            $params['choices'] = count($params['options']);
+                        }
+                    }
+                } else {
+                    if ($params['multiple']) {
+                        $params['is_textarea'] = true;
+                    }
+                }
+
+                if (isset($params['modifier_options'])) {
+                    $modifier_value = isset($this->macros[$var.'_modifier']) ? $this->macros[$var.'_modifier'] : null;
+
+                    foreach ($params['modifier_options'] as $key => $option) {
+                        if (!is_array($option)) {
+                            $params['modifier_options'][$key] = [
+                                'display' => $option,
+                                'value' => $option,
+                            ];
+                        }
+
+                        if ($params['modifier_options'][$key]['value'] == $modifier_value) {
+                            $params['modifier_options'][$key]['selected'] = true;
+                        } else {
+                            $params['modifier_options'][$key]['selected'] = false;
+                        }
+                    }
+                }
+
+                $vars[] = $params;
+            }
+        }
+
+        return $vars;
+    }
+
+    protected function _runReport()
+    {
+        if (!$this->is_ready) {
+            throw new Exception("Report is not ready.  Missing variables");
+        }
+
+        PhpReports::setVar('Report', $this);
+
+        //release the write lock on the session file
+        //so the session isn't locked while the report is running
+        session_write_close();
+
+        $classname = $this->options['Type'].'ReportType';
+
+        if (!class_exists($classname)) {
+            throw new Exception("Unknown report type '".$this->options['Type']."'");
+        }
+
+        foreach ($this->headers as $header) {
+            $headerclass = $header.'Header';
+            $headerclass::beforeRun($this);
+        }
+
+        $classname::openConnection($this);
+        $datasets = $classname::run($this);
+        $classname::closeConnection($this);
+
+        // Convert old single dataset format to multi-dataset format
+        if (!isset($datasets[0]['rows']) || !is_array($datasets[0]['rows'])) {
+            $datasets = [
+                [
+                    'rows' => $datasets,
+                ],
+            ];
+        }
+
+        // Only include a subset of datasets
+        $include = array_keys($datasets);
+        if (isset($_GET['dataset'])) {
+            $include = [$_GET['dataset']];
+        } elseif (isset($_GET['datasets'])) {
+            // If just a single dataset was specified, make it an array
+            if (!is_array($_GET['datasets'])) {
+                $include = explode(',', $_GET['datasets']);
+            } else {
+                $include = $_GET['datasets'];
+            }
+        }
+
+        $this->options['DataSets'] = [];
+        foreach ($include as $i) {
+            if (!isset($datasets[$i])) {
+                continue;
+            }
+            $this->options['DataSets'][$i] = $datasets[$i];
+        }
+
+        $this->parseDynamicHeaders();
+    }
+
+    protected function parseDynamicHeaders()
+    {
+        foreach ($this->options['DataSets'] as $i => &$dataset) {
+            if (isset($dataset['headers'])) {
+                foreach ($dataset['headers'] as $j => $header) {
+                    if (isset($header['header']) && isset($header['value'])) {
+                        $this->parseHeader($header['header'], $header['value'], $i);
+                    }
+                }
+            }
+        }
+    }
+
+    protected function getTimeEstimate()
+    {
+        $report_times = FileSystemCache::retrieve($this->getReportTimesCacheKey());
+        if (!$report_times) {
+            return;
+        }
+
+        sort($report_times);
+
+        $sum = array_sum($report_times);
+        $count = count($report_times);
+        $average = $sum/$count;
+        $quartile1 = $report_times[round(($count-1)/4)];
+        $median = $report_times[round(($count-1)/2)];
+        $quartile3 = $report_times[round(($count-1)*3/4)];
+        $min = min($report_times);
+        $max = max($report_times);
+        $iqr = $quartile3-$quartile1;
+        $range = (1.5)*$iqr;
+
+        $sample_square = 0;
+        for ($i = 0; $i < $count; $i++) {
+            $sample_square += pow($report_times[$i], 2);
+        }
+        $standard_deviation = sqrt($sample_square / $count - pow(($average), 2));
+
+        $this->options['time_estimate'] = [
+            'times' => $report_times,
+            'count' => $count,
+            'min' => round($min, 2),
+            'max' => round($max, 2),
+            'median' => round($median, 2),
+            'average' => round($average, 2),
+            'q1' => round($quartile1, 2),
+            'q3' => round($quartile3, 2),
+            'iqr' => round($range, 2),
+            'sum' => round($sum, 2),
+            'stdev' => round($standard_deviation, 2),
+        ];
+    }
+
+    protected function prepareDataSets()
+    {
+        foreach ($this->options['DataSets'] as $i => $dataset) {
+            $this->prepareRows($i);
+        }
+
+        if (isset($this->options['DataSets'][0])) {
+            $this->options['Rows'] = $this->options['DataSets'][0]['rows'];
+            $this->options['Count'] = $this->options['DataSets'][0]['count'];
+        }
+    }
+
+    protected function prepareRows($dataset)
+    {
+        $rows = [];
+
+        //generate list of all values for each numeric column
+        //this is used to calculate percentiles/averages/etc.
+        $vals = [];
+        foreach ($this->options['DataSets'][$dataset]['rows'] as $row) {
+            foreach ($row as $key => $value) {
+                if (!isset($vals[$key])) {
+                    $vals[$key] = [];
+                }
+
+                if (is_numeric($value)) {
+                    $vals[$key][] = $value;
+                }
+            }
+        }
+
+        $this->options['DataSets'][$dataset]['values'] = $vals;
+
+        foreach ($this->options['DataSets'][$dataset]['rows'] as $row) {
+            $rowval = [];
+
+            $i = 1;
+            foreach ($row as $key => $value) {
+                $val = new ReportValue($i, $key, $value);
+
+                //apply filters for the column key
+                $val = $this->applyFilters($dataset, $key, $val, $row);
+                //apply filters for the column position
+                if ($val) {
+                    $val = $this->applyFilters($dataset, $i, $val, $row);
+                }
+
+                if ($val) {
+                    $rowval[] = $val;
+                }
+
+                $i++;
+            }
+
+            $first = !$rows;
+
+            $rows[] = array(
+                'values' => $rowval,
+                'first' => $first,
+            );
+        }
+
+        $this->options['DataSets'][$dataset]['rows'] = $rows;
+        $this->options['DataSets'][$dataset]['count'] = count($rows);
+    }
+
+    public function run()
+    {
+        if ($this->has_run) {
+            return true;
+        }
+
+        //at this point, all the headers are parsed and we haven't run the report yet
+        foreach ($this->headers as $header) {
+            $classname = $header.'Header';
+            $classname::afterParse($this);
+        }
+
+        //record how long it takes to run the report
+        $start = microtime(true);
+
+        if ($this->is_ready && !$this->async) {
+            //if the report is cached
+            if ($options = $this->retrieveFromCache()) {
+                $this->options = $options;
+                $this->options['FromCache'] = true;
+            } else {
+                $this->_runReport();
+                $this->prepareDataSets();
+                $this->storeInCache();
+            }
+
+            //add this to the list of recently run reports
+            $recently_run_key = FileSystemCache::generateCacheKey('recently_run');
+            $recently_run = FileSystemCache::retrieve($recently_run_key);
+
+            if ($recently_run === false) {
+                $recently_run = [];
+            }
+
+            array_unshift($recently_run, $this->report);
+
+            if (count($recently_run) > 200) {
+                $recently_run = array_slice($recently_run, 0, 200);
+            }
+
+            FileSystemCache::store($recently_run_key, $recently_run);
+        }
+
+        //call the beforeRender callback for each header
+        foreach ($this->headers as $header) {
+            $classname = $header.'Header';
+            $classname::beforeRender($this);
+        }
+
+        $this->options['Time'] = round(microtime(true) - $start, 5);
+
+        if ($this->is_ready && !$this->async && !isset($this->options['FromCache'])) {
+            //get current report times for this report
+            $report_times = FileSystemCache::retrieve($this->getReportTimesCacheKey());
+            if (!$report_times) {
+                $report_times = [];
+            }
+            //only keep the last 10 times for each report
+            //this keeps the timing data up to date and relevant
+            if (count($report_times) > 10) {
+                array_shift($report_times);
+            }
+
+            //store report times
+            $report_times[] = $this->options['Time'];
+            FileSystemCache::store($this->getReportTimesCacheKey(), $report_times);
+        }
+
+        $this->has_run = true;
+    }
+
+    public function renderReportPage($template = 'html/report', $additional_vars = [])
+    {
+        $this->run();
+
+        $template_vars = [
+            'is_ready' => $this->is_ready,
+            'async' => $this->async,
+            'report_url' => PhpReports::$request->base.'/report/?'.$_SERVER['QUERY_STRING'],
+            'report_querystring' => $_SERVER['QUERY_STRING'],
+            'base' => PhpReports::$request->base,
+            'report' => $this->report,
+            'vars' => $this->prepareVariableForm(),
+            'macros' => $this->macros,
+        ];
+
+        $template_vars = array_merge($template_vars, $additional_vars);
+
+        $template_vars = array_merge($template_vars, $this->options);
+
+        return PhpReports::render($template, $template_vars);
+    }
 }
-?>
diff --git a/lib/PhpReports/ReportFormatBase.php b/lib/PhpReports/ReportFormatBase.php
index 49f682b5..cea90884 100644
--- a/lib/PhpReports/ReportFormatBase.php
+++ b/lib/PhpReports/ReportFormatBase.php
@@ -1,15 +1,22 @@
 i = $i;
-		$this->key = $key;
-		$this->original_value = $value;
-		$this->filtered_value = is_string($value)? strip_tags($value) : $value;
-		$this->html_value = $value;
-		$this->chart_value = $value;
-		
-		$this->is_html = false;
-		$this->class = '';
-		
-		$this->type = $this->_getType();
-	}
-	
-	public function addClass($class) {
-		$this->class = trim($this->class . ' ' .$class);
-	}
-	
-	public function setValue($value, $html = false) {		
-		if(is_string($value)) $value = trim($value);
-		
-		if($html) {
-			$this->is_html = true;
-			$this->html_value = $value;
-		}
-		else {
-			$this->is_html = false;
-			$this->filtered_value = is_string($value)? htmlentities($value) : $value;
-			$this->html_value = $value;
-		}
-		
-		$this->type = $this->_getType();
-	}
-	
-	protected function _getType($value=null) {
-		if(is_null($value)) return null;
-		elseif(trim($value) === '') return null;
-		elseif(preg_match('/^([$%(\-+\s])*([0-9,]+(\.[0-9]+)?|\.[0-9]+)([$%(\-+\s])*$/',$value)) return 'number';
-		elseif(strtotime($value)) return 'date';
-		else return 'string';
-	}
-	protected function _getDisplayValue($value, $html=false, $date=false) {
-		$type = $this->_getType($value);
-		
-		if($type === null) {
-			if($html && $this->is_html) return ' ';
-			else return null;
-		}
-		elseif($type === 'number') {
-			return $value;
-		}
-		elseif($type === 'date') {
-			if($date) return date($date,strtotime($value));
-			else return $value;
-		}
-		elseif($type === 'string') {
-			return utf8_encode($value);
-		}
-	}
-	
-	public function getValue($html = false, $date = false) {
-		if($html) {
-			$return = $this->_getDisplayValue($this->html_value, true, $date);
-
-			if($this->is_html) {
-				return $return;
-			}
-			else {
-				return htmlentities($return);
-			}
-		}
-		else {
-			return $this->_getDisplayValue($this->filtered_value, false, $date);
-		}
-	}
-	
-	public function getKeyCollapsed() {
-		return trim(preg_replace(array('/\s+/','/[^a-zA-Z0-9_]*/'),array('_',''),$this->key),'_');
-	}
+
+class ReportValue
+{
+    public $key;
+    public $i;
+
+    public $original_value;
+    public $filtered_value;
+    public $html_value;
+    public $chart_value;
+
+    public $is_html;
+    public $type;
+
+    public $class;
+
+    public function __construct($i, $key, $value)
+    {
+        $this->i = $i;
+        $this->key = $key;
+        $this->original_value = $value;
+        $this->filtered_value = is_string($value) ? strip_tags($value) : $value;
+        $this->html_value = $value;
+        $this->chart_value = $value;
+
+        $this->is_html = false;
+        $this->class = '';
+
+        $this->type = $this->_getType();
+    }
+
+    public function addClass($class)
+    {
+        $this->class = trim($this->class.' '.$class);
+    }
+
+    public function setValue($value, $html = false)
+    {
+        if (is_string($value)) {
+            $value = trim($value);
+        }
+
+        if ($html) {
+            $this->is_html = true;
+            $this->html_value = $value;
+        } else {
+            $this->is_html = false;
+            $this->filtered_value = is_string($value) ? htmlentities($value) : $value;
+            $this->html_value = $value;
+        }
+
+        $this->type = $this->_getType();
+    }
+
+    protected function _getType($value = null)
+    {
+        if (is_null($value)) {
+            return null;
+        } elseif (trim($value) === '') {
+            return null;
+        } elseif (preg_match('/^([$%(\-+\s])*([0-9,]+(\.[0-9]+)?|\.[0-9]+)([$%(\-+\s])*$/', $value)) {
+            return 'number';
+        } elseif (strtotime($value)) {
+            return 'date';
+        } else {
+            return 'string';
+        }
+    }
+    protected function _getDisplayValue($value, $html = false, $date = false)
+    {
+        $type = $this->_getType($value);
+
+        if ($type === null) {
+            if ($html && $this->is_html) {
+                return ' ';
+            } else {
+                return null;
+            }
+        } elseif ($type === 'number') {
+            return $value;
+        } elseif ($type === 'date') {
+            if ($date) {
+                return date($date, strtotime($value));
+            } else {
+                return $value;
+            }
+        } elseif ($type === 'string') {
+            return utf8_encode($value);
+        }
+    }
+
+    public function getValue($html = false, $date = false)
+    {
+        if ($html) {
+            $return = $this->_getDisplayValue($this->html_value, true, $date);
+
+            if ($this->is_html) {
+                return $return;
+            } else {
+                return htmlentities($return);
+            }
+        } else {
+            return $this->_getDisplayValue($this->filtered_value, false, $date);
+        }
+    }
+
+    public function getKeyCollapsed()
+    {
+        return trim(preg_replace(['/\s+/', '/[^a-zA-Z0-9_]*/'], ['_', ''], $this->key), '_');
+    }
 }
diff --git a/lib/adodb/pivottable.inc.php b/lib/adodb/pivottable.inc.php
index ff828ca5..3dfeaf62 100644
--- a/lib/adodb/pivottable.inc.php
+++ b/lib/adodb/pivottable.inc.php
@@ -1,23 +1,23 @@
 databaseType,'access') !== false; 
-	// note - vfp 6 still doesn' work even with IIF enabled || $db->databaseType == 'vfp';
-	
- 	if ($where) $where = "\nWHERE $where";
-	if (!is_array($colfield)) $colarr = $db->GetCol("select distinct $colfield from $tables $where order by 1");
-	$hidecnt = $aggfield ? true : false;
-	
-	$sel = "$rowfields, ";
-	if (is_array($colfield)) {
-		foreach ($colfield as $k => $v) {
-			$k = trim($k);
-			if (!$hidecnt) {
-				$sel .= $iif ? 
-					"\n\t$aggfn(IIF($v,1,0)) AS \"$k\", "
-					:
-					"\n\t$aggfn(CASE WHEN $v THEN 1 ELSE 0 END) AS \"$k\", ";
-			}
-			if ($aggfield) {
-				$sel .= $iif ?
-					"\n\t$aggfn(IIF($v,$aggfield,0)) AS \"$sumlabel$k\", "
-					:
-					"\n\t$aggfn(CASE WHEN $v THEN $aggfield ELSE 0 END) AS \"$sumlabel$k\", ";
-			}
-		} 
-	} else {
-		foreach ($colarr as $v) {
-			if (!is_numeric($v)) $vq = $db->qstr($v);
-			else $vq = $v;
-			$v = trim($v);
-			if (strlen($v) == 0	) $v = 'null';
-			if (!$hidecnt) {
-				$sel .= $iif ?
-					"\n\t$aggfn(IIF($colfield=$vq,1,0)) AS \"$v\", "
-					:
-					"\n\t$aggfn(CASE WHEN $colfield=$vq THEN 1 ELSE 0 END) AS \"$v\", ";
-			}
-			if ($aggfield) {
-				if ($hidecnt) $label = $v;
-				else $label = "{$v}_$aggfield";
-				$sel .= $iif ?
-					"\n\t$aggfn(IIF($colfield=$vq,$aggfield,0)) AS \"$label\", "
-					:
-					"\n\t$aggfn(CASE WHEN $colfield=$vq THEN $aggfield ELSE 0 END) AS \"$label\", ";
-			}
-		}
-	}
-	if ($includeaggfield && ($aggfield && $aggfield != '1')) {
-		$agg = "$aggfn($aggfield)";
+
+    $iif = strpos($db->databaseType, 'access') !== false;
+    // note - vfp 6 still doesn' work even with IIF enabled || $db->databaseType == 'vfp';
+
+    if ($where) {
+        $where = "\nWHERE $where";
+    }
+    if (!is_array($colfield)) {
+        $colarr = $db->GetCol("select distinct $colfield from $tables $where order by 1");
+    }
+    $hidecnt = $aggfield ? true : false;
+
+    $sel = "$rowfields, ";
+    if (is_array($colfield)) {
+        foreach ($colfield as $k => $v) {
+            $k = trim($k);
+            if (!$hidecnt) {
+                $sel .= $iif ?
+                    "\n\t$aggfn(IIF($v,1,0)) AS \"$k\", "
+                    :
+                    "\n\t$aggfn(CASE WHEN $v THEN 1 ELSE 0 END) AS \"$k\", ";
+            }
+            if ($aggfield) {
+                $sel .= $iif ?
+                    "\n\t$aggfn(IIF($v,$aggfield,0)) AS \"$sumlabel$k\", "
+                    :
+                    "\n\t$aggfn(CASE WHEN $v THEN $aggfield ELSE 0 END) AS \"$sumlabel$k\", ";
+            }
+        }
+    } else {
+        foreach ($colarr as $v) {
+            if (!is_numeric($v)) {
+                $vq = $db->qstr($v);
+            } else {
+                $vq = $v;
+            }
+            $v = trim($v);
+            if (strlen($v) == 0) {
+                $v = 'null';
+            }
+            if (!$hidecnt) {
+                $sel .= $iif ?
+                    "\n\t$aggfn(IIF($colfield=$vq,1,0)) AS \"$v\", "
+                    :
+                    "\n\t$aggfn(CASE WHEN $colfield=$vq THEN 1 ELSE 0 END) AS \"$v\", ";
+            }
+            if ($aggfield) {
+                if ($hidecnt) {
+                    $label = $v;
+                } else {
+                    $label = "{$v}_$aggfield";
+                }
+                $sel .= $iif ?
+                    "\n\t$aggfn(IIF($colfield=$vq,$aggfield,0)) AS \"$label\", "
+                    :
+                    "\n\t$aggfn(CASE WHEN $colfield=$vq THEN $aggfield ELSE 0 END) AS \"$label\", ";
+            }
+        }
+    }
+    if ($includeaggfield && ($aggfield && $aggfield != '1')) {
+        $agg = "$aggfn($aggfield)";
         if (strstr($sumlabel, '{}')) {
             $sumlabel = trim($sumlabel, ' \t\n\r\0\x0B{}').' '.trim($aggfield);
         }
         $sel .= "\n\t$agg AS \"$sumlabel\", ";
-	}
-	
-	if ($showcount) {
-		$sel .= "\n\tSUM(1) as Total";
+    }
+
+    if ($showcount) {
+        $sel .= "\n\tSUM(1) as Total";
     } else {
-		$sel = substr($sel,0,strlen($sel)-2);
+        $sel = substr($sel, 0, strlen($sel)-2);
     }
 
     if ($orderBy) {
@@ -107,10 +120,10 @@ function PivotTableSQL(&$db, $tables, $rowfields, $colfield, $where = false, $or
         }
     }
 
-	// Strip aliases
-	$rowfields = preg_replace('/\s+AS\s+[\'\"]?[\w\s]+[\'\"]?/i', '', $rowfields);
-	
-	$sql = "SELECT $sel \nFROM $tables $where \nGROUP BY $rowfields $orderSql $limitSql";
-	
-	return $sql;
+    // Strip aliases
+    $rowfields = preg_replace('/\s+AS\s+[\'\"]?[\w\s]+[\'\"]?/i', '', $rowfields);
+
+    $sql = "SELECT $sel \nFROM $tables $where \nGROUP BY $rowfields $orderSql $limitSql";
+
+    return $sql;
 }
diff --git a/lib/simplediff/SimpleDiff.php b/lib/simplediff/SimpleDiff.php
index 771eff72..fb170eee 100644
--- a/lib/simplediff/SimpleDiff.php
+++ b/lib/simplediff/SimpleDiff.php
@@ -53,12 +53,13 @@ protected function htmlDiff($old, $new)
         $diff = self::diff(explode(" ", $old), explode(" ", $new));
         foreach ($diff as $k) {
             if (is_array($k)) {
-                $ret .= (!empty($k['d']) ? "" . implode(" ", array_map('htmlentities', $k['d'])) . " ":'') .
-                    (!empty($k['i']) ? "" . implode(" ", array_map('htmlentities', $k['i'])) . " " : '');
+                $ret .= (!empty($k['d']) ? "".implode(" ", array_map('htmlentities', $k['d']))." " : '').
+                    (!empty($k['i']) ? "".implode(" ", array_map('htmlentities', $k['i']))." " : '');
             } else {
-                $ret .= htmlentities($k) . " ";
+                $ret .= htmlentities($k)." ";
             }
         }
+
         return $ret;
     }
 
@@ -76,7 +77,6 @@ protected function hasChange($diff, $i, $before = 0, $after = 0)
             }
         }
 
-
         if (!isset($diff[$i])) {
             return false;
         }
@@ -114,10 +114,10 @@ public function htmlDiffSummary($old, $new)
             }
 
             if (is_array($k)) {
-                $ret .= (!empty($k['d']) ? "" . implode("\n", array_map('htmlentities', $k['d'])) . "\n" : '') .
-                    (!empty($k['i']) ? "" . implode("\n", array_map('htmlentities', $k['i'])) . "\n" : '');
+                $ret .= (!empty($k['d']) ? "".implode("\n", array_map('htmlentities', $k['d']))."\n" : '').
+                    (!empty($k['i']) ? "".implode("\n", array_map('htmlentities', $k['i']))."\n" : '');
             } elseif ($diff_section) {
-                $ret .= htmlentities($k) . "\n";
+                $ret .= htmlentities($k)."\n";
             }
         }
 
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 00000000..f7f4ae3d
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,20 @@
+
+
+    
+        
+            ./tests
+        
+    
+
\ No newline at end of file

From c885a0bb7deea5f518a81b5b41e605235c547b32 Mon Sep 17 00:00:00 2001
From: Fede Isas 
Date: Sun, 8 May 2016 20:55:46 -0300
Subject: [PATCH 08/19] Cleanup

---
 config/config.php.sample | 261 ++++++++++++++++++++-------------------
 index.php                |   2 +-
 2 files changed, 137 insertions(+), 126 deletions(-)

diff --git a/config/config.php.sample b/config/config.php.sample
index c36616e7..861a775d 100644
--- a/config/config.php.sample
+++ b/config/config.php.sample
@@ -1,126 +1,137 @@
  'sample_reports',
-
-	//the root directory of all dashboards
-	'dashboardDir' => 'sample_dashboards',
-
-	// Company name on navigation
-	'brand' => 'PHP Reports',
-
-	//the directory where things will be cached
-	//this is relative to the project root by default, but can be set to an absolute path too
-	//the cache has some relatively long lived data so don't use /tmp if you can avoid it
-	//(for example historical report timing data is stored here)
-	'cacheDir' => 'cache',
-
-	//this maps file extensions to report types
-	//to override this for a specific report, simply add a TYPE header
-	//any file extension not in this array will be ignored when pulling the report list
-	'default_file_extension_mapping' => array(
-		'sql'=>'Pdo',
-		'php'=>'Php',
-		'js'=>'Mongo',
-		'ado'=>'Ado',
-		'pivot'=>'AdoPivot',
-	),
-
-	//this enables listing different types of download formats on the report page
-	//to change that one can add or remove any format from the list below
-	//in order to create a divider a list entry have to be added with any key name and
-	//a value of 'divider'
-	'report_formats' => array(
-		'csv'=>'CSV',
-		'xlsx'=>'Download Excel 2007',
-		'xls'=>'Download Excel 97-2003',
-		'text'=>'Text',
-		'table'=>'Simple table',
-		'raw data'=>'divider',
-		'json'=>'JSON',
-		'xml'=>'XML',
-		'sql'=>'SQL INSERT command',
-		'technical'=>'divider',
-		'debug'=>'Debug information',
-		'raw'=>'Raw report dump',
-	),
-
-	//this enebales one to change the default bootstrap theme
-	'bootstrap_theme' => 'default',
-
-	//this list all the available themes for a user to switch and use the one he or she likes
-	//once removed the theme will not appear in the dropdown
-	//if all to be removed - no dropdown will be visible for the user and the default (above) will be used
-	'bootstrap_themelist' => array(
-	    'default',
-	    'amelia', 'cerulean', 'cosmo', 'cyborg', 'flatly', 'journal', 'readable', 'simplex', 'slate', 'spacelab', 'united'
-	),
-
-	//email settings
-	'mail_settings' => array(
-		//set 'enabled' to true to enable the 'email this report' functionality
-		'enabled'=>false,
-
-		'from'=>'reports@yourdomain.com',
-
-		//php's mail function
-		'method'=>'mail'
-
-		//sendmail
-		/*
-		'method'=>'sendmail',
-		'command'=>'/usr/sbin/sendmail -bs' //optional
-		*/
-
-		//smtp
-		/*
-		'method'=>'smtp',
-		'server'=>'smtp.yourdomain.com',
-		'port'=>'25', 						//optional (default 25)
-		'username'=>'youremailusername', 	//optional
-		'password'=>'yoursmtppassword', 	//optional
-		'encryption'=>'ssl' 				//optional (either 'ssl' or 'tls')
-		*/
-	),
-
-	// Google Analytics API Integration
-	/*
-	'ga_api'=>array(
-		'applicationName'=>'PHP Reports',
-		'clientId'=>'abcdef123456',
-		'clientSecret'=>'1234abcd',
-		'redirectUri'=>'http://example.com/'
-	),
-	*/
-
-	//this defines the database environments
-	//the keys are the environment names (e.g. "dev", "production")
-	//the values are arrays that contain connection info
-	'environments' => array(
-		'main'=>array(
-			// Supports AdoDB connections
-			'ado'=>array(
-				'uri'=>'mysql://username:password@localhost/database'
-			),
-
-			// Supports and PDO database
-			'pdo'=>array(
-				'dsn'=>'mysql:host=localhost;dbname=test',
-				'user'=>'readonly',
-				'pass'=>'password',
-			),
-
-			// Supports MongoDB
-			'mongo'=>array(
-				'host'=>'localhost',
-				'port'=>'27017'
-			),
-		),
-	),
-	// This is called twice, once for each Twig_Environment that is used
-	'twig_init_function' => function (Twig_Environment $twig) {
-
-	}
-);
-?>
+
+return [
+    //the root directory of all your reports
+    //reports can be organized in subdirectories
+    'reportDir' => 'sample_reports',
+
+    //the root directory of all dashboards
+    'dashboardDir' => 'sample_dashboards',
+
+    // Company name on navigation
+    'brand' => 'PHP Reports',
+
+    //the directory where things will be cached
+    //this is relative to the project root by default, but can be set to an absolute path too
+    //the cache has some relatively long lived data so don't use /tmp if you can avoid it
+    //(for example historical report timing data is stored here)
+    'cacheDir' => 'cache',
+
+    //this maps file extensions to report types
+    //to override this for a specific report, simply add a TYPE header
+    //any file extension not in this array will be ignored when pulling the report list
+    'default_file_extension_mapping' => [
+        'sql' => 'Pdo',
+        'php' => 'Php',
+        'js' => 'Mongo',
+        'ado' => 'Ado',
+        'pivot' => 'AdoPivot',
+    ],
+
+    //this enables listing different types of download formats on the report page
+    //to change that one can add or remove any format from the list below
+    //in order to create a divider a list entry have to be added with any key name and
+    //a value of 'divider'
+    'report_formats' => [
+        'csv' => 'CSV',
+        'xlsx' => 'Download Excel 2007',
+        'xls' => 'Download Excel 97-2003',
+        'text' => 'Text',
+        'table' => 'Simple table',
+        'raw  da ta'=>'divider',
+        'json' => 'JSON',
+        'xml' => 'XML',
+        'sql' => 'SQL INSERT command',
+        'technical' => 'divider',
+        'debug' => 'Debug information',
+        'raw' => 'Raw report dump',
+    ],
+
+    //this enebales one to change the default bootstrap theme
+    'bootstrap_theme' => 'default',
+
+    //this list all the available themes for a user to switch and use the one he or she likes
+    //once removed the theme will not appear in the dropdown
+    //if all to be removed - no dropdown will be visible for the user and the default (above) will be used
+    'bootstrap_themelist' => [
+        'default',
+        'amelia',
+        'cerulean',
+        'cosmo',
+        'cyborg',
+        'flatly',
+        'journal',
+        'readable',
+        'simplex',
+        'slate',
+        'spacelab',
+        'united',
+    ],
+
+    //email settings
+    'mail_settings' => [
+        //set 'enabled' to true to enable the 'email this report' functionality
+        'enabled' => false,
+
+        'from' => 'reports@yourdomain.com',
+
+        //php's mail function
+        'method' => 'mail',
+
+        //sendmail
+        /*
+        'method' => 'sendmail',
+        'command' => '/usr/sbin/sendmail -bs', //optional
+        */
+
+        //smtp
+        /*
+        'method' => 'smtp',
+        'server' => 'smtp.yourdomain.com',
+        'port' => '25',                       //optional (default 25)
+        'username' => 'youremailusername',    //optional
+        'password' => 'yoursmtppassword',     //optional
+        'encryption' => 'ssl',                 //optional (either 'ssl' or 'tls')
+        */
+    ],
+
+    // Google Analytics API Integration
+    /*
+    'ga_api' => [
+        'applicationName' => 'PHP Reports',
+        'clientId' => 'abcdef123456',
+        'clientSecret' => '1234abcd',
+        'redirectUri' => 'http://example.com/',
+    ],
+    */
+
+    //this defines the database environments
+    //the keys are the environment names (e.g. "dev", "production")
+    //the values are arrays that contain connection info
+    'environments' => [
+        'main' => [
+            // Supports AdoDB connections
+            'ado' => [
+                'uri' => 'mysql://username:password@localhost/database',
+            ],
+
+            // Supports and PDO database
+            'pdo' => [
+                'dsn' => 'mysql:host=localhost;dbname=test',
+                'user' => 'readonly',
+                'pass' => 'password',
+            ],
+
+            // Supports MongoDB
+            'mongo' => [
+                'host' => 'localhost',
+                'port' => '27017',
+            ],
+        ],
+    ],
+
+    // This is called twice, once for each Twig_Environment that is used
+    'twig_init_function' => function (Twig_Environment $twig) {
+
+    }
+];
diff --git a/index.php b/index.php
index 5b98ef6b..19eb7636 100644
--- a/index.php
+++ b/index.php
@@ -93,7 +93,7 @@
     header("Content-Type: application/json");
     $_SESSION['environment'] = $_REQUEST['environment'];
 
-    echo '{ "status": "OK" }';
+    echo json_encode(['status' => 'OK']);
 });
 
 //email report

From bfcc0f5cb4afb4d5098269de238fdb09e855b28a Mon Sep 17 00:00:00 2001
From: Fede Isas 
Date: Mon, 9 May 2016 12:06:45 -0300
Subject: [PATCH 09/19] WIP

---
 .gitignore                                    |   1 +
 classes/filters/barFilter.php                 |  30 -
 classes/filters/classFilter.php               |  11 -
 classes/filters/dateFilter.php                |  33 -
 classes/filters/drilldownFilter.php           |  91 --
 classes/filters/geoipFilter.php               |  27 -
 classes/filters/hideFilter.php                |   8 -
 classes/filters/htmlFilter.php                |  10 -
 classes/filters/imgsizeFilter.php             |  21 -
 classes/filters/linkFilter.php                |  20 -
 classes/filters/numberFilter.php              |  22 -
 classes/filters/paddingFilter.php             |  14 -
 classes/filters/preFilter.php                 |  10 -
 classes/filters/twigFilter.php                |  20 -
 classes/headers/ChartHeader.php               | 439 ----------
 classes/headers/ColumnsHeader.php             |  94 ---
 classes/headers/FilterHeader.php              |  50 --
 classes/headers/FormattingHeader.php          | 188 -----
 classes/headers/IncludeHeader.php             |  50 --
 classes/headers/InfoHeader.php                |  58 --
 classes/headers/OptionsHeader.php             | 149 ----
 classes/headers/RollupHeader.php              | 147 ----
 classes/headers/VariableHeader.php            | 229 -----
 classes/headers/deprecated/CacheHeader.php    |  26 -
 classes/headers/deprecated/CautionHeader.php  |  26 -
 classes/headers/deprecated/ColumnHeader.php   |  10 -
 classes/headers/deprecated/CreatedHeader.php  |  17 -
 classes/headers/deprecated/DatabaseHeader.php |  17 -
 .../headers/deprecated/DescriptionHeader.php  |  17 -
 classes/headers/deprecated/DetailHeader.php   |  72 --
 .../deprecated/MongodatabaseHeader.php        |  17 -
 classes/headers/deprecated/NameHeader.php     |  17 -
 classes/headers/deprecated/NoteHeader.php     |  17 -
 classes/headers/deprecated/OptionHeader.php   |  10 -
 classes/headers/deprecated/PlotHeader.php     |  12 -
 classes/headers/deprecated/StatusHeader.php   |  17 -
 classes/headers/deprecated/TotalHeader.php    |   8 -
 classes/headers/deprecated/TotalsHeader.php   |  22 -
 classes/headers/deprecated/TypeHeader.php     |  17 -
 classes/headers/deprecated/ValueHeader.php    |  46 --
 classes/report_formats/ChartReportFormat.php  |  17 -
 classes/report_formats/CsvReportFormat.php    |  32 -
 classes/report_formats/DebugReportFormat.php  |  24 -
 classes/report_formats/HtmlReportFormat.php   |  42 -
 classes/report_formats/JsonReportFormat.php   |  71 --
 classes/report_formats/RawReportFormat.php    |  20 -
 classes/report_formats/SqlReportFormat.php    |  12 -
 classes/report_formats/TableReportFormat.php  |  15 -
 classes/report_formats/TextReportFormat.php   | 101 ---
 classes/report_formats/XlsReportBase.php      |  76 --
 classes/report_formats/XlsReportFormat.php    |  30 -
 classes/report_formats/XlsxReportFormat.php   |  30 -
 classes/report_formats/XmlReportFormat.php    |  41 -
 classes/report_types/AdoPivotReportType.php   | 190 -----
 classes/report_types/AdoReportType.php        | 172 ----
 classes/report_types/MongoReportType.php      |  83 --
 classes/report_types/MysqlReportType.php      |   6 -
 classes/report_types/PdoReportType.php        | 228 -----
 classes/report_types/PhpReportType.php        |  98 ---
 composer.json                                 |   9 +-
 lib/PhpReports/FilterBase.php                 |  16 -
 lib/PhpReports/HeaderBase.php                 | 123 ---
 lib/PhpReports/PhpReports.php                 | 779 ------------------
 lib/PhpReports/Report.php                     | 720 ----------------
 lib/PhpReports/ReportFormatBase.php           |  22 -
 lib/PhpReports/ReportTypeBase.php             |  25 -
 lib/PhpReports/ReportValue.php                | 112 ---
 index.php => public/index.php                 |   7 +-
 templates/default/html/chart_page.twig        |   6 +-
 templates/default/html/dashboard.twig         |  48 +-
 templates/default/html/page.twig              |  22 +-
 templates/default/html/report.twig            |  38 +-
 templates/default/html/report_editor.twig     |  20 +-
 templates/default/html/report_list.twig       |   4 +-
 templates/default/html/report_list_item.twig  |  20 +-
 75 files changed, 88 insertions(+), 5261 deletions(-)
 delete mode 100644 classes/filters/barFilter.php
 delete mode 100644 classes/filters/classFilter.php
 delete mode 100644 classes/filters/dateFilter.php
 delete mode 100644 classes/filters/drilldownFilter.php
 delete mode 100644 classes/filters/geoipFilter.php
 delete mode 100644 classes/filters/hideFilter.php
 delete mode 100644 classes/filters/htmlFilter.php
 delete mode 100644 classes/filters/imgsizeFilter.php
 delete mode 100644 classes/filters/linkFilter.php
 delete mode 100644 classes/filters/numberFilter.php
 delete mode 100644 classes/filters/paddingFilter.php
 delete mode 100644 classes/filters/preFilter.php
 delete mode 100644 classes/filters/twigFilter.php
 delete mode 100644 classes/headers/ChartHeader.php
 delete mode 100644 classes/headers/ColumnsHeader.php
 delete mode 100644 classes/headers/FilterHeader.php
 delete mode 100644 classes/headers/FormattingHeader.php
 delete mode 100644 classes/headers/IncludeHeader.php
 delete mode 100644 classes/headers/InfoHeader.php
 delete mode 100644 classes/headers/OptionsHeader.php
 delete mode 100644 classes/headers/RollupHeader.php
 delete mode 100644 classes/headers/VariableHeader.php
 delete mode 100644 classes/headers/deprecated/CacheHeader.php
 delete mode 100644 classes/headers/deprecated/CautionHeader.php
 delete mode 100644 classes/headers/deprecated/ColumnHeader.php
 delete mode 100644 classes/headers/deprecated/CreatedHeader.php
 delete mode 100644 classes/headers/deprecated/DatabaseHeader.php
 delete mode 100644 classes/headers/deprecated/DescriptionHeader.php
 delete mode 100644 classes/headers/deprecated/DetailHeader.php
 delete mode 100644 classes/headers/deprecated/MongodatabaseHeader.php
 delete mode 100644 classes/headers/deprecated/NameHeader.php
 delete mode 100644 classes/headers/deprecated/NoteHeader.php
 delete mode 100644 classes/headers/deprecated/OptionHeader.php
 delete mode 100644 classes/headers/deprecated/PlotHeader.php
 delete mode 100644 classes/headers/deprecated/StatusHeader.php
 delete mode 100644 classes/headers/deprecated/TotalHeader.php
 delete mode 100644 classes/headers/deprecated/TotalsHeader.php
 delete mode 100644 classes/headers/deprecated/TypeHeader.php
 delete mode 100644 classes/headers/deprecated/ValueHeader.php
 delete mode 100644 classes/report_formats/ChartReportFormat.php
 delete mode 100644 classes/report_formats/CsvReportFormat.php
 delete mode 100644 classes/report_formats/DebugReportFormat.php
 delete mode 100644 classes/report_formats/HtmlReportFormat.php
 delete mode 100644 classes/report_formats/JsonReportFormat.php
 delete mode 100644 classes/report_formats/RawReportFormat.php
 delete mode 100644 classes/report_formats/SqlReportFormat.php
 delete mode 100644 classes/report_formats/TableReportFormat.php
 delete mode 100644 classes/report_formats/TextReportFormat.php
 delete mode 100644 classes/report_formats/XlsReportBase.php
 delete mode 100644 classes/report_formats/XlsReportFormat.php
 delete mode 100644 classes/report_formats/XlsxReportFormat.php
 delete mode 100644 classes/report_formats/XmlReportFormat.php
 delete mode 100644 classes/report_types/AdoPivotReportType.php
 delete mode 100644 classes/report_types/AdoReportType.php
 delete mode 100644 classes/report_types/MongoReportType.php
 delete mode 100644 classes/report_types/MysqlReportType.php
 delete mode 100644 classes/report_types/PdoReportType.php
 delete mode 100644 classes/report_types/PhpReportType.php
 delete mode 100644 lib/PhpReports/FilterBase.php
 delete mode 100644 lib/PhpReports/HeaderBase.php
 delete mode 100644 lib/PhpReports/PhpReports.php
 delete mode 100644 lib/PhpReports/Report.php
 delete mode 100644 lib/PhpReports/ReportFormatBase.php
 delete mode 100644 lib/PhpReports/ReportTypeBase.php
 delete mode 100644 lib/PhpReports/ReportValue.php
 rename index.php => public/index.php (94%)

diff --git a/.gitignore b/.gitignore
index cd2a2cd9..6224f900 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ dashboards/
 cache/
 classes/local/*.php
 templates/local
+old/
 vendor/
\ No newline at end of file
diff --git a/classes/filters/barFilter.php b/classes/filters/barFilter.php
deleted file mode 100644
index ee6483a6..00000000
--- a/classes/filters/barFilter.php
+++ /dev/null
@@ -1,30 +0,0 @@
-getValue() / max($report->options['Values'][$value->key])));
-
-        $value->setValue(
-            join('', [
-                "
", - "", - $value->getValue(true), - "", - ]), - true - ); - - return $value; - } -} diff --git a/classes/filters/classFilter.php b/classes/filters/classFilter.php deleted file mode 100644 index 1c89af4d..00000000 --- a/classes/filters/classFilter.php +++ /dev/null @@ -1,11 +0,0 @@ -addClass($options['class']); - - return $value; - } -} diff --git a/classes/filters/dateFilter.php b/classes/filters/dateFilter.php deleted file mode 100644 index f6450e07..00000000 --- a/classes/filters/dateFilter.php +++ /dev/null @@ -1,33 +0,0 @@ -options['Database']; - } - - $time = strtotime($value->getValue()); - - //if the time couldn't be parsed, just return the original value - if (!$time) { - return $value; - } - - //if a timezone correction is needed for the database being selected from - $environment = $report->getEnvironment(); - if (isset($environment[$options['database']]['time_offset'])) { - $time_offset = -1*$environment[$options['database']]['time_offset']; - - $time = strtotime((($time_offset > 0) ? '+' : '-').abs($time_offset).' hours', $time); - } - - $value->setValue(date($options['format'], $time)); - - return $value; - } -} diff --git a/classes/filters/drilldownFilter.php b/classes/filters/drilldownFilter.php deleted file mode 100644 index 477f6f8c..00000000 --- a/classes/filters/drilldownFilter.php +++ /dev/null @@ -1,91 +0,0 @@ -report); - array_pop($temp); - $try[] = implode('/', $temp).'/'.$options['report']; - $try[] = $options['report']; - } - - //see if the file exists directly - $found = false; - $path = ''; - foreach ($try as $report_name) { - if (file_exists(PhpReports::$config['reportDir'].'/'.$report_name)) { - $path = $report_name; - $found = true; - break; - } - } - - //see if the report is missing a file extension - if (!$found) { - foreach ($try as $report_name) { - $possible_reports = glob(PhpReports::$config['reportDir'].'/'.$report_name.'.*'); - - if ($possible_reports) { - $path = substr($possible_reports[0], strlen(PhpReports::$config['reportDir'].'/')); - $found = true; - break; - } - } - } - - if (!$found) { - return $value; - } - - $url = PhpReports::$request->base.'/report/html/?report='.$path; - - $macros = []; - foreach ($options['macros'] as $k => $v) { - //if the macro needs to be replaced with the value of another column - if (isset($v['column'])) { - if (isset($row[$v['column']])) { - $v = $row[$v['column']]; - } else { - $v = ""; - } - } elseif (isset($v['constant'])) { - //if the macro is just a constant - $v = $v['constant']; - } - - $macros[$k] = $v; - } - - $macros = array_merge($report->macros, $macros); - unset($macros['host']); - - foreach ($macros as $k => $v) { - if (is_array($v)) { - foreach ($v as $v2) { - $url .= '¯os['.$k.'][]='.$v2; - } - } else { - $url .= '¯os['.$k.']='.$v; - } - } - - $options = array( - 'url' => $url, - ); - - return parent::filter($value, $options, $report, $row); - } -} diff --git a/classes/filters/geoipFilter.php b/classes/filters/geoipFilter.php deleted file mode 100644 index a92e50f9..00000000 --- a/classes/filters/geoipFilter.php +++ /dev/null @@ -1,27 +0,0 @@ -getValue()); - - if ($record) { - $display = ''; - - $display = $record['city']; - if ($record['country_code'] !== 'US') { - $display .= ' '.$record['country_name']; - } else { - $display .= ', '.$record['region']; - } - - $value->setValue($display); - - $value->chart_value = array('Latitude' => $record['latitude'],'Longitude' => $record['longitude'],'Location' => $display); - } else { - $value->chart_value = array('Latitude' => 0, 'Longitude' => 0, 'Location' => 'Unknown'); - } - - return $value; - } -} diff --git a/classes/filters/hideFilter.php b/classes/filters/hideFilter.php deleted file mode 100644 index cc5da5c7..00000000 --- a/classes/filters/hideFilter.php +++ /dev/null @@ -1,8 +0,0 @@ -is_html = true; - - return $value; - } -} diff --git a/classes/filters/imgsizeFilter.php b/classes/filters/imgsizeFilter.php deleted file mode 100644 index daccc499..00000000 --- a/classes/filters/imgsizeFilter.php +++ /dev/null @@ -1,21 +0,0 @@ -getValue(), 'rb'); - $img = new Imagick(); - $img->readImageFile($handle); - $data = $img->identifyImage(); - - if (!isset($options['format'])) { - $options['format'] = self::$default_format; - } - - $value->setValue(PhpReports::renderString($options['format'], $data)); - - return $value; - } -} diff --git a/classes/filters/linkFilter.php b/classes/filters/linkFilter.php deleted file mode 100644 index b248bdeb..00000000 --- a/classes/filters/linkFilter.php +++ /dev/null @@ -1,20 +0,0 @@ -getValue()) { - return $value; - } - - $url = isset($options['url']) ? $options['url'] : $value->getValue(); - $attr = (isset($options['blank']) && $options['blank']) ? ' target="_blank"' : ''; - $display = isset($options['display']) ? $options['display'] : $value->getValue(); - - $html = ''.$display.''; - - $value->setValue($html, true); - - return $value; - } -} diff --git a/classes/filters/numberFilter.php b/classes/filters/numberFilter.php deleted file mode 100644 index 5cab76a8..00000000 --- a/classes/filters/numberFilter.php +++ /dev/null @@ -1,22 +0,0 @@ -getValue())) { - $value->setValue(number_format($value->getValue(), $decimals, $dec_sepr, $thousand), true); - } - - return $value; - } -} diff --git a/classes/filters/paddingFilter.php b/classes/filters/paddingFilter.php deleted file mode 100644 index 7cdd17e3..00000000 --- a/classes/filters/paddingFilter.php +++ /dev/null @@ -1,14 +0,0 @@ -addClass('right'); - } elseif ($options['direction'] === 'l') { - $value->addClass('left'); - } - - return $value; - } -} diff --git a/classes/filters/preFilter.php b/classes/filters/preFilter.php deleted file mode 100644 index 04f33744..00000000 --- a/classes/filters/preFilter.php +++ /dev/null @@ -1,10 +0,0 @@ -setValue("
".$value->getValue(true)."
", true); - - return $value; - } -} diff --git a/classes/filters/twigFilter.php b/classes/filters/twigFilter.php deleted file mode 100644 index bbb2ce54..00000000 --- a/classes/filters/twigFilter.php +++ /dev/null @@ -1,20 +0,0 @@ -getValue(); - - $result = PhpReports::renderString($template, array( - "value" => $value->getValue(), - "row" => $row, - )); - - $value->setValue($result, $html); - - return $value; - } -} diff --git a/classes/headers/ChartHeader.php b/classes/headers/ChartHeader.php deleted file mode 100644 index f7a8c8c7..00000000 --- a/classes/headers/ChartHeader.php +++ /dev/null @@ -1,439 +0,0 @@ - [ - 'type' => 'array', - 'default' => [], - ], - 'dataset' => [ - 'default' => 0, - ], - 'type' => [ - 'type' => 'enum', - 'values' => [ - 'LineChart', - 'GeoChart', - 'AnnotatedTimeLine', - 'BarChart', - 'ColumnChart', - 'Timeline', - 'AreaChart', - 'Histogram', - 'ComboChart', - 'BubbleChart', - 'CandlestickChart', - 'Gauge', - 'Map', - 'PieChart', - 'Sankey', - 'ScatterChart', - 'SteppedAreaChart', - 'WordTree', - ], - 'default' => 'LineChart', - ], - 'title' => [ - 'type' => 'string', - 'default' => '', - ], - 'width' => [ - 'type' => 'string', - 'default' => '100%', - ], - 'height' => [ - 'type' => 'string', - 'default' => '400px', - ], - 'xhistogram' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'buckets' => [ - 'type' => 'number', - 'default' => 0, - ], - 'omit-totals' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'omit-total' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'rotate-x-labels' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'grid' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'timefmt' => [ - 'type' => 'string', - 'default' => '', - ], - 'xformat' => [ - 'type' => 'string', - 'default' => '', - ], - 'yrange' => [ - 'type' => 'string', - 'default' => '', - ], - 'all' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'colors' => [ - 'type' => 'array', - 'default' => [], - ], - 'roles' => [ - 'type' => 'object', - 'default' => [], - ], - 'markers' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'omit-columns' => [ - 'type' => 'array', - 'default' => [], - ], - 'options' => [ - 'type' => 'object', - 'default' => [], - ], - ]; - - public static function init($params, &$report) - { - $report->exportHeader('Chart', $params); - - if (!isset($params['type'])) { - $params['type'] = 'LineChart'; - } - - if (isset($params['omit-total'])) { - $params['omit-totals'] = $params['omit-total']; - unset($params['omit-total']); - } - - if (!isset($report->options['Charts'])) { - $report->options['Charts'] = []; - } - - if (isset($params['width'])) { - $params['width'] = self::fixDimension($params['width']); - } - if (isset($params['height'])) { - $params['height'] = self::fixDimension($params['height']); - } - - $params['num'] = count($report->options['Charts'])+1; - $params['Rows'] = []; - - $report->options['Charts'][] = $params; - - $report->options['has_charts'] = true; - } - protected static function fixDimension($dim) - { - if (preg_match('/^[0-9]+$/', $dim)) { - $dim .= "px"; - } - - return $dim; - } - - public static function parseShortcut($value) - { - $params = explode(',', $value); - $value = []; - foreach ($params as $param) { - $param = trim($param); - if (strpos($param, '=') !== false) { - list($key, $val) = explode('=', $param, 2); - $key = trim($key); - $val = trim($val); - - //some parameters can have multiple values separated by ":" - if (in_array($key, array('x', 'y', 'colors'), true)) { - $val = explode(':', $val); - } - } else { - $key = $param; - $val = true; - } - - $value[$key] = $val; - } - - if (isset($value['x'])) { - $value['columns'] = $value['x']; - } else { - $value['columns'] = array(1); - } - - if (isset($value['y'])) { - $value['columns'] = array_merge($value['columns'], $value['y']); - } else { - $value['all'] = true; - } - - unset($value['x']); - unset($value['y']); - - return $value; - } - - protected static function getRowInfo(&$rows, $params, $num, &$report) - { - $cols = []; - - //expand columns - $chart_rows = []; - foreach ($rows as $k => $row) { - $vals = []; - - if ($k === 0) { - $i = 1; - $unsorted = 1000; - foreach ($row['values'] as $key => $value) { - if (($temp = array_search($row['values'][$key]->i, $report->options['Charts'][$num]['columns'])) !== false) { - $cols[$temp] = $key; - } elseif (($temp = array_search($row['values'][$key]->key, $report->options['Charts'][$num]['columns'])) !== false) { - $cols[$temp] = $key; - } elseif ($report->options['Charts'][$num]['all']) { - //if all columns are included, add after any specifically defined ones - $cols[$unsorted] = $key; - $unsorted ++; - } - } - - ksort($cols); - } - - foreach ($cols as $key) { - if (isset($row['values'][$key]->chart_value) && is_array($row['values'][$key]->chart_value)) { - foreach ($row['values'][$key]->chart_value as $ckey => $cval) { - $temp = new ReportValue($row['values'][$key]->i, $ckey, trim($cval, '%$ ')); - $temp->setValue($cval); - $vals[] = $temp; - } - } else { - $temp = new ReportValue($row['values'][$key]->i, $row['values'][$key]->key, $row['values'][$key]->original_value); - $temp->setValue(trim($row['values'][$key]->getValue(), '%$ ')); - $vals[] = $temp; - } - } - - $chart_rows[] = $vals; - } - - //determine column types - $types = []; - foreach ($chart_rows as $i => $row) { - foreach ($row as $k => $v) { - $type = self::determineDataType($v->original_value); - //if the value is null, it doesn't influence the column type - if (!$type) { - $chart_rows[$i][$k]->setValue(null); - continue; - } elseif (!isset($types[$k])) { - //if we don't know the column type yet, set it to this row's value - $types[$k] = $type; - } elseif ($type === 'string') { - //if any row has a string value for the column, the whole column is a string type - $types[$k] = 'string'; - } elseif ($types[$k] === 'date' && in_array($type, array('timeofday', 'datetime'))) { - //if the column is currently a date and this row is a time/datetime, set the column to datetime type - $types[$k] = 'datetime'; - } elseif ($types[$k] === 'timeofday' && in_array($type, array('date', 'datetime'))) { - //if the column is currently a time and this row is a date/datetime, set the column to datetime type - $types[$k] = 'datetime'; - } elseif ($types[$k] === 'date' && $type === 'number') { - //if the column is currently a date and this row is a number set the column type to number - $types[$k] = 'number'; - } - } - } - - $report->options['Charts'][$num]['datatypes'] = $types; - - //build chart rows - $report->options['Charts'][$num]['Rows'] = []; - - foreach ($chart_rows as $i => &$row) { - $vals = []; - foreach ($row as $key => $val) { - if (is_null($val->getValue())) { - $val->datatype = 'null'; - } elseif ($types[$key] === 'datetime') { - $val->setValue(date('m/d/Y H:i:s', strtotime($val->getValue()))); - $val->datatype = 'datetime'; - } elseif ($types[$key] === 'timeofday') { - $val->setValue(date('H:i:s', strtotime($val->getValue()))); - $val->datatype = 'timeofday'; - } elseif ($types[$key] === 'date') { - $val->setValue(date('m/d/Y', strtotime($val->getValue()))); - $val->datatype = 'date'; - } elseif ($types[$key] === 'number') { - $val->setValue(round(floatval(preg_replace('/[^-0-9\.]*/', '', $val->getValue())), 6)); - $val->datatype = 'number'; - } else { - $val->datatype = 'string'; - } - - $vals[] = $val; - } - - $report->options['Charts'][$num]['Rows'][] = array( - 'values' => $vals, - 'first' => !$report->options['Charts'][$num]['Rows'], - ); - } - } - - protected static function generateHistogramRows($rows, $column, $num_buckets) - { - $column_key = null; - - //if a name is given as the column, determine the column index - if (!is_numeric($column)) { - foreach ($rows[0]['values'] as $k => $v) { - if ($v->key == $column) { - $column = $k; - $column_key = $v->key; - break; - } - } - } else { - //if an index is given, convert to 0-based - $column --; - $column_key = $rows[0]['values'][$column]->key; - } - - //get a list of values for the histogram - $vals = []; - foreach ($rows as &$row) { - $vals[] = floatval(preg_replace('/[^0-9.]*/', '', $row['values'][$column]->getValue())); - } - sort($vals); - - //determine buckets - $count = count($vals); - $buckets = []; - $min = $vals[0]; - $max = $vals[$count-1]; - $step = ($max-$min)/$num_buckets; - $old_limit = $min; - - for ($i = 1; $i < $num_buckets + 1; $i++) { - $limit = $old_limit + $step; - - $buckets[round($old_limit, 2)." - ".round($limit, 2)] = count( - array_filter( - $vals, - function ($val) use ($old_limit, $limit) { - return $val >= $old_limit && $val < $limit; - } - ) - ); - $old_limit = $limit; - } - - //build chart rows - $chart_rows = []; - foreach ($buckets as $name => $count) { - $chart_rows[] = array( - 'values' => array( - new ReportValue(1, $name, $name), - new ReportValue(2, 'value', $count), - ), - 'first' => !$chart_rows, - ); - } - - return $chart_rows; - } - - protected static function determineDataType($value) - { - if (is_null($value)) { - return null; - } elseif ($value === '') { - return null; - } elseif (preg_match('/^([$%(\-+\s])*([0-9,]+(\.[0-9]+)?|\.[0-9]+)([$%(\-+\s])*$/', $value)) { - return 'number'; - } elseif (preg_match('/^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$/', $value)) { - return 'timeofday'; - } elseif (preg_match('/^[0-9]+(\/|-)[0-9]+/', $value) && strtotime($value)) { - if (date('H:i:s', strtotime($value)) === '00:00:00') { - return 'date'; - } else { - return 'datetime'; - } - } else { - return 'string'; - } - } - - public static function beforeRender(&$report) - { - // Expand out multiple datasets into their own charts - $new_charts = []; - foreach ($report->options['Charts'] as $num => $params) { - $copy = $params; - - // If chart is for multiple datasets - if (is_array($params['dataset'])) { - foreach ($params['dataset'] as $dataset) { - $copy['dataset'] = $dataset; - $copy['num'] = count($new_charts)+1; - $new_charts[] = $copy; - } - } elseif ($params['dataset'] === true) { - // If chart is for all datasets - foreach ($report->options['DataSets'] as $j => $dataset) { - $copy['dataset'] = $j; - $copy['num'] = count($new_charts)+1; - $new_charts[] = $copy; - } - } else { - // If chart is for one dataset - $copy['num'] = count($new_charts)+1; - $new_charts[] = $copy; - } - } - - $report->options['Charts'] = $new_charts; - - foreach ($report->options['Charts'] as $num => &$params) { - self::_processChart($num, $params, $params['dataset'], $report); - } - } - - protected static function _processChart($num, &$params, $dataset, &$report) - { - if (isset($params['xhistogram']) && $params['xhistogram']) { - $rows = self::generateHistogramRows($report->options['DataSets'][$dataset]['rows'], $params['columns'][0], $params['buckets']); - $params['columns'] = array(1,2); - } else { - $rows = []; - if (isset($report->options['DataSets'])) { - $rows = $report->options['DataSets'][$dataset]['rows']; - } - - if (count($rows)) { - if (!$params['columns']) { - $params['columns'] = range(1, count($rows[0]['values'])); - } - } - } - - self::getRowInfo($rows, $params, $num, $report); - } -} diff --git a/classes/headers/ColumnsHeader.php b/classes/headers/ColumnsHeader.php deleted file mode 100644 index 8d45c7b9..00000000 --- a/classes/headers/ColumnsHeader.php +++ /dev/null @@ -1,94 +0,0 @@ - $options) { - if (!isset($options['type'])) { - throw new Exception("Must specify column type for column $column"); - } - $type = $options['type']; - unset($options['type']); - $report->addFilter($params['dataset'], $column, $type, $options); - } - } - - public static function parseShortcut($value) - { - if (preg_match('/^[0-9]+\:/', $value)) { - $dataset = substr($value, 0, strpos($value, ':')); - $value = substr($value, strlen($dataset)+1); - } else { - $dataset = 0; - } - - $parts = explode(',', $value); - $params = []; - $i = 1; - foreach ($parts as $part) { - $type = null; - $options = null; - - $part = trim($part); - //special cases - //'rpadN' or 'lpadN' where N is number of spaces to pad - if (substr($part, 1, 3) === 'pad') { - $type = 'padding'; - - $options = [ - 'direction' => $part[0], - 'spaces' => intval(substr($part, 4)), - ]; - } elseif (substr($part, 0, 4) === 'link') { - //link or link(display) or link_blank or link_blank(display) - //link(display) or link_blank(display) - if (strpos($part, '(') !== false) { - list($type, $display) = explode('(', substr($part, 0, -1), 2); - } else { - $type = $part; - $display = 'link'; - } - - $blank = ($type == 'link_blank'); - $type = 'link'; - - $options = array( - 'display' => $display, - 'blank' => $blank, - ); - } elseif (in_array($part, array('html', 'raw'))) { - //synonyms for 'html' - $type = 'html'; - } elseif ($part === 'url') { - //url synonym for link - $type = 'link'; - $options = [ - 'blank' => false, - ]; - } elseif ($part === 'bar') { - $type = 'bar'; - $options = []; - } elseif ($part === 'pre') { - $type = 'pre'; - } else { - //normal case - $type = 'class'; - $options = [ - 'class' => $part, - ]; - } - - $options['type'] = $type; - - $params[$i] = $options; - - $i++; - } - - return [ - 'dataset' => $dataset, - 'columns' => $params, - ]; - } -} diff --git a/classes/headers/FilterHeader.php b/classes/headers/FilterHeader.php deleted file mode 100644 index f1a5e000..00000000 --- a/classes/headers/FilterHeader.php +++ /dev/null @@ -1,50 +0,0 @@ - [ - 'required' => true, - 'type' => 'string', - ], - 'filter' => [ - 'required' => true, - 'type' => 'string', - ], - 'params' => [ - 'type' => 'object', - 'default' => [], - ], - 'dataset' => [ - 'default' => 0, - ], - ]; - - public static function init($params, &$report) - { - $report->addFilter($params['dataset'], $params['column'], $params['filter'], $params['params']); - } - - //in format: column, params - //params can be a JSON object or "filter" - //filter classes are defined in class/filters/ - //examples: - // "4,geoip" - apply a geoip filter to the 4th column - // 'Ip,{"filter":"geoip"}' - apply a geoip filter to the "Ip" column - public static function parseShortcut($value) - { - if (strpos($value, ',') === false) { - $col = "1"; - $filter = $value; - } else { - list($col, $filter) = explode(',', $value, 2); - $col = trim($col); - } - $filter = trim($filter); - - return [ - 'column' => $col, - 'filter' => $filter, - 'params' => [], - ]; - } -} diff --git a/classes/headers/FormattingHeader.php b/classes/headers/FormattingHeader.php deleted file mode 100644 index ba353068..00000000 --- a/classes/headers/FormattingHeader.php +++ /dev/null @@ -1,188 +0,0 @@ - [ - 'type' => 'number', - 'default' => null, - ], - 'noborder' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'vertical' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'table' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'showcount' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'font' => [ - 'type' => 'string', - ], - 'nodata' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'selectable' => [ - 'type' => 'string', - ], - 'dataset' => [ - 'required' => true, - 'default' => true, - ], - ]; - - public static function init($params, &$report) - { - if (!isset($report->options['Formatting'])) { - $report->options['Formatting'] = []; - } - $report->options['Formatting'][] = $params; - } - - public static function parseShortcut($value) - { - $options = explode(',', $value); - - $params = []; - - foreach ($options as $v) { - if (strpos($v, '=') !== false) { - list($k, $v) = explode('=', $v, 2); - $v = trim($v); - } else { - $k = $v; - $v = true; - } - - $k = trim($k); - - $params[$k] = $v; - } - - return $params; - } - - public static function beforeRender(&$report) - { - $formatting = []; - // Expand out by dataset - foreach ($report->options['Formatting'] as $params) { - $copy = $params; - unset($copy['dataset']); - - if (isset($report->options['DataSets'])) { - // Multiple datasets defined - if (is_array($params['dataset'])) { - foreach ($params['dataset'] as $i) { - if (isset($report->options['DataSets'][$i])) { - if (!isset($formatting[$i])) { - $formatting[$i] = []; - } - foreach ($copy as $k => $v) { - $formatting[$i][$k] = $v; - } - } - } - } elseif ($params['dataset'] === true) { - // All datasets - foreach ($report->options['DataSets'] as $i => $dataset) { - if (!isset($formatting[$i])) { - $formatting[$i] = []; - } - foreach ($copy as $k => $v) { - $formatting[$i][$k] = $v; - } - } - } else { - // Single dataset - if (!isset($report->options['DataSets'][$params['dataset']])) { - continue; - } - if (!isset($formatting[$params['dataset']])) { - $formatting[$params['dataset']] = []; - } - foreach ($copy as $k => $v) { - $formatting[$params['dataset']][$k] = $v; - } - } - } - } - - $report->options['Formatting'] = $formatting; - - // Apply formatting options for each dataset - foreach ($formatting as $i => $params) { - if (isset($params['limit']) && $params['limit']) { - $report->options['DataSets'][$i]['rows'] = array_slice($report->options['DataSets'][$i]['rows'], 0, intval($params['limit'])); - } - if (isset($params['selectable']) && $params['selectable']) { - $selected = []; - - // New style "selected_{{DATASET}}" querystring - if (isset($_GET['selected_'.$i])) { - $selected = $_GET['selected_'.$i]; - } elseif (isset($_GET['selected'])) { - // Old style "selected" querystring - $selected = $_GET['selected']; - } - - if ($selected) { - $selected_key = null; - foreach ($report->options['DataSets'][$i]['rows'][0]['values'] as $key => $value) { - if ($value->key == $params['selectable']) { - $selected_key = $key; - break; - } - } - - if ($selected_key !== null) { - foreach ($report->options['DataSets'][$i]['rows'] as $key => $row) { - if (!in_array($row['values'][$selected_key]->getValue(), $selected)) { - unset($report->options['DataSets'][$i]['rows'][$key]); - } - } - $report->options['DataSets'][$i]['rows'] = array_values($report->options['DataSets'][$i]['rows']); - } - } - } - if (isset($params['vertical']) && $params['vertical']) { - $rows = []; - foreach ($report->options['DataSets'][$i]['rows'] as $row) { - foreach ($row['values'] as $value) { - if (!isset($rows[$value->key])) { - $header = new ReportValue(1, 'key', $value->key); - $header->class = 'left lpad'; - $header->is_header = true; - - $rows[$value->key] = [ - 'values' => [ - $header, - ], - 'first' => !$rows, - ]; - } - - $rows[$value->key]['values'][] = $value; - } - } - - $rows = array_values($rows); - - $report->options['DataSets'][$i]['vertical'] = $rows; - } - - unset($params['vertical']); - foreach ($params as $k => $v) { - $report->options['DataSets'][$i][$k] = $v; - } - } - } -} diff --git a/classes/headers/IncludeHeader.php b/classes/headers/IncludeHeader.php deleted file mode 100644 index 18edfac4..00000000 --- a/classes/headers/IncludeHeader.php +++ /dev/null @@ -1,50 +0,0 @@ - [ - 'required' => true, - 'type' => 'string', - ], - ]; - - public static function init($params, &$report) - { - if ($params['report'][0] === '/') { - $report_path = substr($params['report'], 1); - } else { - $report_path = dirname($report->report).'/'.$params['report']; - } - - if (!file_exists(PhpReports::$config['reportDir'].'/'.$report_path)) { - $possible_reports = glob(PhpReports::$config['reportDir'].'/'.$report_path.'.*'); - - if ($possible_reports) { - $report_path = substr($possible_reports[0], strlen(PhpReports::$config['reportDir'].'/')); - } else { - throw new Exception("Unknown report in INCLUDE header '$report_path'"); - } - } - - $included_report = new Report($report_path); - - //parse any exported headers from the included report - foreach ($included_report->exported_headers as $header) { - $report->parseHeader($header['name'], $header['params']); - } - - if (!isset($report->options['Includes'])) { - $report->options['Includes'] = []; - } - - $report->options['Includes'][] = $included_report; - } - - public static function parseShortcut($value) - { - return [ - 'report' => $value, - ]; - } -} diff --git a/classes/headers/InfoHeader.php b/classes/headers/InfoHeader.php deleted file mode 100644 index 6c33730a..00000000 --- a/classes/headers/InfoHeader.php +++ /dev/null @@ -1,58 +0,0 @@ - [ - 'type' => 'string', - ], - 'description' => [ - 'type' => 'string', - ], - 'created' => [ - 'type' => 'string', - 'pattern' => '/^[0-9]{4}-[0-9]{2}-[0-9]{2}/', - ], - 'note' => [ - 'type' => 'string', - ], - 'type' => [ - 'type' => 'string', - ], - 'status' => [ - 'type' => 'string', - ], - ]; - - public static function init($params, &$report) - { - foreach ($params as $key => $value) { - $report->options[ucfirst($key)] = $value; - } - } - - // Accepts shortcut format: - // name=My Report,description=This is My Report - public static function parseShortcut($value) - { - $parts = explode(',', $value); - - $params = []; - - foreach ($parts as $v) { - if (strpos($v, '=') !== false) { - list($k, $v) = explode('=', $v, 2); - $v = trim($v); - } else { - $k = $v; - $v = true; - } - - $k = trim($k); - - $params[$k] = $v; - } - - return $params; - } -} diff --git a/classes/headers/OptionsHeader.php b/classes/headers/OptionsHeader.php deleted file mode 100644 index 8409ffad..00000000 --- a/classes/headers/OptionsHeader.php +++ /dev/null @@ -1,149 +0,0 @@ - [ - 'type' => 'number', - 'default' => null, - ], - 'access' => [ - 'type' => 'enum', - 'values' => ['rw', 'readonly'], - 'default' => 'readonly', - ], - 'noborder' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'noreport' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'vertical' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'ignore' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'table' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'showcount' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'font' => [ - 'type' => 'string', - ], - 'stop' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'nodata' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'version' => [ - 'type' => 'number', - 'default' => 1, - ], - 'selectable' => [ - 'type' => 'string', - ], - 'mongodatabase' => [ - 'type' => 'string', - ], - 'database' => [ - 'type' => 'string', - ], - 'cache' => [ - 'min' => 0, - 'type' => 'number', - ], - 'ttl' => [ - 'min' => 0, - 'type' => 'number', - ], - 'default_dataset' => [ - 'type' => 'number', - 'default' => 0, - ], - 'has_charts' => [ - 'type' => 'boolean', - ], - ]; - - public static function init($params, &$report) - { - //legacy support for the 'ttl' cache parameter - if (isset($params['ttl'])) { - $params['cache'] = $params['ttl']; - unset($params['ttl']); - } - - if (isset($params['has_charts']) && $params['has_charts']) { - if (!isset($report->options['Charts'])) { - $report->options['Charts'] = []; - } - } - - // Some parameters were moved to a 'FORMATTING' header - // We need to catch those and add the header to the report - $formatting_header = []; - - foreach ($params as $key => $value) { - // This is a FORMATTING parameter - if (in_array($key, ['limit', 'noborder', 'vertical', 'table', 'showcount', 'font', 'nodata', 'selectable'])) { - $formatting_header[$key] = $value; - continue; - } - - //some of the keys need to be uppercase (for legacy reasons) - if (in_array($key, ['database', 'mongodatabase', 'cache'])) { - $key = ucfirst($key); - } - - $report->options[$key] = $value; - - //if the value is different from the default, it can be exported - if (!isset(self::$validation[$key]['default']) || ($value && $value !== self::$validation[$key]['default'])) { - //only export some of the options - if (in_array($key, array('access', 'Cache'), true)) { - $report->exportHeader('Options', array($key => $value)); - } - } - } - - if ($formatting_header) { - $formatting_header['dataset'] = true; - $report->parseHeader('Formatting', $formatting_header); - } - } - - public static function parseShortcut($value) - { - $options = explode(',', $value); - - $params = []; - - foreach ($options as $v) { - if (strpos($v, '=') !== false) { - list($k, $v) = explode('=', $v, 2); - $v = trim($v); - } else { - $k = $v; - $v = true; - } - - $k = trim($k); - - $params[$k] = $v; - } - - return $params; - } -} diff --git a/classes/headers/RollupHeader.php b/classes/headers/RollupHeader.php deleted file mode 100644 index 5932b904..00000000 --- a/classes/headers/RollupHeader.php +++ /dev/null @@ -1,147 +0,0 @@ - [ - 'required' => true, - 'type' => 'object', - 'default' => [], - ], - 'dataset' => [ - 'required' => false, - 'default' => 0, - ], - ]; - - public static function init($params, &$report) - { - //make sure at least 1 column is defined - if (empty($params['columns'])) { - throw new Exception("Rollup header needs at least 1 column defined"); - } - - if (!isset($report->options['Rollup'])) { - $report->options['Rollup'] = []; - } - - // If more than one dataset is defined, add the rollup header multiple times - if (is_array($params['dataset'])) { - $new_params = $params; - foreach ($params['dataset'] as $dataset) { - $new_params['dataset'] = $dataset; - $report->options['Rollup'][] = $new_params; - } - } else { - // Otherwise, just add one rollup header - $report->options['Rollup'][] = $params; - } - } - - public static function beforeRender(&$report) - { - //cache for Twig parameters for each dataset/column - $twig_params = []; - - // Now that we know how many datasets we have, expand out Rollup headers with dataset->true - $new_rollups = []; - foreach ($report->options['Rollup'] as $i => $rollup) { - if ($rollup['dataset'] === true && isset($report->options['DataSets'])) { - $copy = $rollup; - foreach ($report->options['DataSets'] as $i => $dataset) { - $copy['dataset'] = $i; - $new_rollups[] = $copy; - } - } else { - $new_rollups[] = $rollup; - } - } - $report->options['Rollup'] = $new_rollups; - - // First get all the values - foreach ($report->options['Rollup'] as $rollup) { - // If we already got twig parameters for this dataset, skip it - if (isset($twig_params[$rollup['dataset']])) { - continue; - } - $twig_params[$rollup['dataset']] = []; - if (isset($report->options['DataSets'])) { - if (isset($report->options['DataSets'][$rollup['dataset']])) { - foreach ($report->options['DataSets'][$rollup['dataset']]['rows'] as $row) { - foreach ($row['values'] as $value) { - if (!isset($twig_params[$rollup['dataset']][$value->key])) { - $twig_params[$rollup['dataset']][$value->key] = ['values' => []]; - } - $twig_params[$rollup['dataset']][$value->key]['values'][] = $value->getValue(); - } - } - } - } - } - - // Then, calculate other statistical properties - foreach ($twig_params as $dataset => &$tp) { - foreach ($tp as $column => &$params) { - //get non-null values and sort them - $real_values = array_filter( - $params['values'], - function ($a) { - if ($a === null || $a === '') { - return false; - } - - return true; - } - ); - - sort($real_values); - - $params['sum'] = array_sum($real_values); - $params['count'] = count($real_values); - if ($params['count']) { - $params['mean'] = $params['average'] = $params['sum'] / $params['count']; - $params['median'] = ($params['count']%2) ? ($real_values[$params['count']/2-1] + $real_values[$params['count']/2])/2 : $real_values[floor($params['count']/2)]; - $params['min'] = $real_values[0]; - $params['max'] = $real_values[$params['count']-1]; - } else { - $params['mean'] = $params['average'] = $params['median'] = $params['min'] = $params['max'] = 0; - } - - $devs = []; - if (empty($real_values)) { - $params['stdev'] = 0; - } elseif (function_exists('stats_standard_deviation')) { - $params['stdev'] = stats_standard_deviation($real_values); - } else { - foreach ($real_values as $v) { - $devs[] = pow($v - $params['mean'], 2); - } - $params['stdev'] = sqrt(array_sum($devs) / (count($devs))); - } - } - } - - //render each rollup row - foreach ($report->options['Rollup'] as $rollup) { - if (!isset($report->options['DataSets'][$rollup['dataset']]['footer'])) { - $report->options['DataSets'][$rollup['dataset']]['footer'] = []; - } - $columns = $rollup['columns']; - $row = [ - 'values' => [], - 'rollup' => true, - ]; - - foreach ($twig_params[$rollup['dataset']] as $column => $p) { - if (isset($columns[$column])) { - $p = array_merge($p, ['row' => $twig_params[$rollup['dataset']]]); - - $row['values'][] = new ReportValue(-1, $column, PhpReports::renderString($columns[$column], $p)); - } else { - $row['values'][] = new ReportValue(-1, $column, null); - } - } - $report->options['DataSets'][$rollup['dataset']]['footer'][] = $row; - } - } -} diff --git a/classes/headers/VariableHeader.php b/classes/headers/VariableHeader.php deleted file mode 100644 index 2eba8953..00000000 --- a/classes/headers/VariableHeader.php +++ /dev/null @@ -1,229 +0,0 @@ - [ - 'required' => true, - 'type' => 'string', - ], - 'display' => [ - 'type' => 'string', - ], - 'type' => [ - 'type' => 'enum', - 'values' => ['text', 'select', 'textarea', 'date', 'daterange'], - 'default' => 'text', - ], - 'options' => [ - 'type' => 'array', - ], - 'default' => [], - 'empty' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'multiple' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'database_options' => [ - 'type' => 'object', - ], - 'description' => [ - 'type' => 'string', - ], - 'format' => [ - 'type' => 'string', - 'default' => 'Y-m-d H:i:s', - ], - 'modifier_options' => [ - 'type' => 'array', - ], - 'time_offset' => [ - 'type' => 'number', - ], - ]; - - public static function init($params, &$report) - { - if (!isset($params['display']) || !$params['display']) { - $params['display'] = $params['name']; - } - - if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_\-]*$/', $params['name'])) { - throw new Exception("Invalid variable name: $params[name]"); - } - - //add to options - if (!isset($report->options['Variables'])) { - $report->options['Variables'] = []; - } - $report->options['Variables'][$params['name']] = $params; - - //add to macros - if (!isset($report->macros[$params['name']]) && isset($params['default'])) { - $report->addMacro($params['name'], $params['default']); - - $report->macros[$params['name']] = $params['default']; - - if (!isset($params['empty']) || !$params['empty']) { - $report->is_ready = false; - } - } elseif (!isset($report->macros[$params['name']])) { - $report->addMacro($params['name'], ''); - - if (!isset($params['empty']) || !$params['empty']) { - $report->is_ready = false; - } - } - - //convert newline separated strings to array for vars that support multiple values - if ($params['multiple'] && !is_array($report->macros[$params['name']])) { - $report->addMacro($params['name'], explode("\n", $report->macros[$params['name']])); - } - - $report->exportHeader('Variable', $params); - } - - public static function parseShortcut($value) - { - list($var, $params) = explode(',', $value, 2); - $var = trim($var); - $params = trim($params); - - $parts = explode(',', $params); - $params = [ - 'name' => $var, - 'display' => trim($parts[0]), - ]; - - unset($parts[0]); - - $extra = implode(',', $parts); - - //just "name, label" - if (!$extra) { - return $params; - } - - //if the 3rd item is "LIST", use multi-select - if (preg_match('/^\s*LIST\s*\b/', $extra)) { - $params['multiple'] = true; - $extraexplode = explode(',', $extra, 2); - $extra = array_pop($extraexplode); - } - - //table.column, where clause, ALL - if (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,[^,]+,\s*ALL\s*$/', $extra)) { - list($table_column, $where, $all) = explode(',', $extra, 3); - list($table, $column) = explode('.', $table_column, 2); - - $params['type'] = 'select'; - - $var_params = [ - 'table' => $table, - 'column' => $column, - 'all' => true, - 'where' => $where, - ]; - - $params['database_options'] = $var_params; - } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,\s*ALL\s*$/', $extra)) { - //table.column, ALL - list($table_column, $all) = explode(',', $extra, 2); - list($table, $column) = explode('.', $table_column, 2); - - $params['type'] = 'select'; - - $var_params = [ - 'table' => $table, - 'column' => $column, - 'all' => true, - ]; - - $params['database_options'] = $var_params; - } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,[^,]+$/', $extra)) { - //table.column, where clause - list($table_column, $where) = explode(',', $extra, 2); - list($table, $column) = explode('.', $table_column, 2); - - $params['type'] = 'select'; - - $var_params = [ - 'table' => $table, - 'column' => $column, - 'where' => $where, - ]; - - $params['database_options'] = $var_params; - } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*$/', $extra)) { - //table.column - list($table, $column) = explode('.', $extra, 2); - - $params['type'] = 'select'; - - $var_params = [ - 'table' => $table, - 'column' => $column, - ]; - - $params['database_options'] = $var_params; - } elseif (preg_match('/^\s*([a-zA-Z0-9_\- ]+\|)+[a-zA-Z0-9_\- ]+$/', $extra)) { - //option1|option2 - $options = explode('|', $extra); - - $params['type'] = 'select'; - $params['options'] = $options; - } - - return $params; - } - - public static function afterParse(&$report) - { - $classname = $report->options['Type'].'ReportType'; - - foreach ($report->options['Variables'] as $var => $params) { - //if it's a select variable and the options are pulled from a database - if (isset($params['database_options'])) { - $classname::openConnection($report); - $params['options'] = $classname::getVariableOptions($params['database_options'], $report); - - $report->options['Variables'][$var] = $params; - } - - //if the type is daterange, parse start and end with strtotime - if ($params['type'] === 'daterange' && !empty($report->macros[$params['name']][0]) && !empty($report->macros[$params['name']][1])) { - $start = date_create($report->macros[$params['name']][0]); - if (!$start) { - throw new Exception($params['display']." must have a valid start date."); - } - date_time_set($start, 0, 0, 0); - $report->macros[$params['name']]['start'] = date_format($start, $params['format']); - - $end = date_create($report->macros[$params['name']][1]); - if (!$end) { - throw new Exception($params['display']." must have a valid end date."); - } - date_time_set($end, 23, 59, 59); - $report->macros[$params['name']]['end'] = date_format($end, $params['format']); - } - } - } - - public static function beforeRun(&$report) - { - foreach ($report->options['Variables'] as $var => $params) { - //if the type is date, parse with strtotime - if ($params['type'] === 'date' && $report->macros[$params['name']]) { - $time = strtotime($report->macros[$params['name']]); - if (!$time) { - throw new Exception($params['display']." must be a valid datetime value."); - } - - $report->macros[$params['name']] = date($params['format'], $time); - } - } - } -} diff --git a/classes/headers/deprecated/CacheHeader.php b/classes/headers/deprecated/CacheHeader.php deleted file mode 100644 index 9747f466..00000000 --- a/classes/headers/deprecated/CacheHeader.php +++ /dev/null @@ -1,26 +0,0 @@ - intval($value), - ); - } - //if cache is being turned off - else { - return array( - 'cache' => 0, - ); - } - } -} diff --git a/classes/headers/deprecated/CautionHeader.php b/classes/headers/deprecated/CautionHeader.php deleted file mode 100644 index 79be1f1b..00000000 --- a/classes/headers/deprecated/CautionHeader.php +++ /dev/null @@ -1,26 +0,0 @@ - array( - 'required' => true, - 'type' => 'string', - ), - ); - - public static function init($params, &$report) - { - trigger_error("CAUTION header is deprecated.", E_USER_DEPRECATED); - - $report->options['Caution'] = $params['value']; - - $report->exportHeader('Caution', $params); - } - - public static function parseShortcut($value) - { - return array( - 'value' => $value, - ); - } -} diff --git a/classes/headers/deprecated/ColumnHeader.php b/classes/headers/deprecated/ColumnHeader.php deleted file mode 100644 index d896df1a..00000000 --- a/classes/headers/deprecated/ColumnHeader.php +++ /dev/null @@ -1,10 +0,0 @@ - $value, - ); - } -} diff --git a/classes/headers/deprecated/DatabaseHeader.php b/classes/headers/deprecated/DatabaseHeader.php deleted file mode 100644 index 7fc846b6..00000000 --- a/classes/headers/deprecated/DatabaseHeader.php +++ /dev/null @@ -1,17 +0,0 @@ -report.")", E_USER_DEPRECATED); - - return parent::init($params, $report); - } - - public static function parseShortcut($value) - { - return array( - 'database' => trim($value), - ); - } -} diff --git a/classes/headers/deprecated/DescriptionHeader.php b/classes/headers/deprecated/DescriptionHeader.php deleted file mode 100644 index 30c25643..00000000 --- a/classes/headers/deprecated/DescriptionHeader.php +++ /dev/null @@ -1,17 +0,0 @@ - $value, - ); - } -} diff --git a/classes/headers/deprecated/DetailHeader.php b/classes/headers/deprecated/DetailHeader.php deleted file mode 100644 index f0b36a22..00000000 --- a/classes/headers/deprecated/DetailHeader.php +++ /dev/null @@ -1,72 +0,0 @@ - array( - 'required' => true, - 'type' => 'string', - ), - 'column' => array( - 'required' => true, - 'type' => 'string', - ), - 'macros' => array( - 'type' => 'object', - ), - ); - - public static function init($params, &$report) - { - trigger_error("DETAIL header is deprecated. Use the FILTER header with the 'drilldown' filter instead.", E_USER_DEPRECATED); - - $report->addFilter($params['column'], 'drilldown', $params); - } - - public static function parseShortcut($value) - { - $parts = explode(',', $value, 3); - - if (count($parts) < 2) { - throw new Exception("Cannot parse DETAIL header '$value'"); - } - - $col = trim($parts[0]); - $report_name = trim($parts[1]); - - if (isset($parts[2])) { - $parts[2] = trim($parts[2]); - $macros = array(); - $temp = explode(',', $parts[2]); - foreach ($temp as $macro) { - $macro = trim($macro); - if (strpos($macro, '=') !== false) { - list($key, $val) = explode('=', $macro, 2); - $key = trim($key); - $val = trim($val); - - if (in_array($val[0], array('"', "'"))) { - $val = array( - 'constant' => trim($val, '\'"'), - ); - } else { - $val = array( - 'column' => $val, - ); - } - - $macros[$key] = $val; - } else { - $macros[$macro] = $macro; - } - } - } else { - $macros = array(); - } - - return array( - 'report' => $report_name, - 'column' => $col, - 'macros' => $macros, - ); - } -} diff --git a/classes/headers/deprecated/MongodatabaseHeader.php b/classes/headers/deprecated/MongodatabaseHeader.php deleted file mode 100644 index 093e3502..00000000 --- a/classes/headers/deprecated/MongodatabaseHeader.php +++ /dev/null @@ -1,17 +0,0 @@ -report.")", E_USER_DEPRECATED); - - return parent::init($params, $report); - } - - public static function parseShortcut($value) - { - return array( - 'mongodatabase' => $value, - ); - } -} diff --git a/classes/headers/deprecated/NameHeader.php b/classes/headers/deprecated/NameHeader.php deleted file mode 100644 index 5d65ebe2..00000000 --- a/classes/headers/deprecated/NameHeader.php +++ /dev/null @@ -1,17 +0,0 @@ - $value, - ); - } -} diff --git a/classes/headers/deprecated/NoteHeader.php b/classes/headers/deprecated/NoteHeader.php deleted file mode 100644 index fcc8da33..00000000 --- a/classes/headers/deprecated/NoteHeader.php +++ /dev/null @@ -1,17 +0,0 @@ - $value, - ); - } -} diff --git a/classes/headers/deprecated/OptionHeader.php b/classes/headers/deprecated/OptionHeader.php deleted file mode 100644 index c8ea4bc5..00000000 --- a/classes/headers/deprecated/OptionHeader.php +++ /dev/null @@ -1,10 +0,0 @@ - $value, - ); - } -} diff --git a/classes/headers/deprecated/TotalHeader.php b/classes/headers/deprecated/TotalHeader.php deleted file mode 100644 index 42fe2936..00000000 --- a/classes/headers/deprecated/TotalHeader.php +++ /dev/null @@ -1,8 +0,0 @@ - array( - 'required' => true, - 'type' => 'string', - ), - ); - - public static function init($params, &$report) - { - trigger_error("TOTALS header is deprecated. Use the ROLLUP header instead.", E_USER_DEPRECATED); - } - - public static function parseShortcut($value) - { - return array( - 'value' => $value, - ); - } -} diff --git a/classes/headers/deprecated/TypeHeader.php b/classes/headers/deprecated/TypeHeader.php deleted file mode 100644 index 24dd4787..00000000 --- a/classes/headers/deprecated/TypeHeader.php +++ /dev/null @@ -1,17 +0,0 @@ - $value, - ); - } -} diff --git a/classes/headers/deprecated/ValueHeader.php b/classes/headers/deprecated/ValueHeader.php deleted file mode 100644 index 6f5af655..00000000 --- a/classes/headers/deprecated/ValueHeader.php +++ /dev/null @@ -1,46 +0,0 @@ - array( - 'required' => true, - 'type' => 'string', - ), - 'value' => array( - 'required' => true, - ), - ); - - public static function init($params, &$report) - { - trigger_error("VALUE header is deprecated. Use the VARIABLE header with a 'default' parameter instead.", E_USER_DEPRECATED); - - if (isset($report->options['Variables'][$params['name']])) { - if ($report->macros[$params['name']]) { - return; - } - - $report->options['Variables'][$params['name']]['default'] = $params['value']; - $report->macros[$params['name']] = $params['value']; - - $report->exportHeader('Value', $params); - } else { - throw new Exception("Providing value for unknown variable $params[name]"); - } - } - - public static function parseShortcut($value) - { - if (strpos($value, ',') === false) { - throw new Exception("Invalid value '$value'"); - } - list($name, $value) = explode(',', $value); - $var = trim($name); - $default = trim($value); - - return array( - 'name' => $var, - 'value' => $default, - ); - } -} diff --git a/classes/report_formats/ChartReportFormat.php b/classes/report_formats/ChartReportFormat.php deleted file mode 100644 index 06bab396..00000000 --- a/classes/report_formats/ChartReportFormat.php +++ /dev/null @@ -1,17 +0,0 @@ -options['has_charts']) { - return; - } - - //always use cache for chart reports - //$report->use_cache = true; - - $result = $report->renderReportPage('html/chart_report'); - - echo $result; - } -} diff --git a/classes/report_formats/CsvReportFormat.php b/classes/report_formats/CsvReportFormat.php deleted file mode 100644 index 975ef5ed..00000000 --- a/classes/report_formats/CsvReportFormat.php +++ /dev/null @@ -1,32 +0,0 @@ -use_cache = true; - - $file_name = preg_replace(array('/[\s]+/', '/[^0-9a-zA-Z\-_\.]/'), array('_', ''), $report->options['Name']); - - header("Content-type: application/csv"); - header("Content-Disposition: attachment; filename=".$file_name.".csv"); - header("Pragma: no-cache"); - header("Expires: 0"); - - $i = 0; - if (isset($_GET['dataset'])) { - $i = $_GET['dataset']; - } elseif (isset($report->options['default_dataset'])) { - $i = $report->options['default_dataset']; - } - $i = intval($i); - - $data = $report->renderReportPage('csv/report', array( - 'dataset' => $i, - )); - - if (trim($data)) { - echo $data; - } - } -} diff --git a/classes/report_formats/DebugReportFormat.php b/classes/report_formats/DebugReportFormat.php deleted file mode 100644 index c75237fa..00000000 --- a/classes/report_formats/DebugReportFormat.php +++ /dev/null @@ -1,24 +0,0 @@ -getRaw()."\n\n\n"; - $content .= "****************** Macros ******************\n\n".print_r($report->macros, true)."\n\n\n"; - $content .= "****************** All Report Options ******************\n\n".print_r($report->options, true)."\n\n\n"; - - if ($report->is_ready) { - $report->run(); - - $content .= "****************** Generated Query ******************\n\n".print_r($report->options['Query'], true)."\n\n\n"; - - $content .= "****************** Report Rows ******************\n\n".print_r($report->options['DataSets'], true)."\n\n\n"; - } - - echo $content; - } -} diff --git a/classes/report_formats/HtmlReportFormat.php b/classes/report_formats/HtmlReportFormat.php deleted file mode 100644 index df8813a7..00000000 --- a/classes/report_formats/HtmlReportFormat.php +++ /dev/null @@ -1,42 +0,0 @@ -async = !isset($request->query['content_only']); - if (isset($request->query['no_async'])) { - $report->async = false; - } - - //if we're only getting the report content - if (isset($request->query['content_only'])) { - $template = 'html/content_only'; - } else { - $template = 'html/report'; - } - - try { - $additional_vars = array(); - if (isset($request->query['no_charts'])) { - $additional_vars['no_charts'] = true; - } - - $html = $report->renderReportPage($template, $additional_vars); - echo $html; - } catch (Exception $e) { - if (isset($request->query['content_only'])) { - $template = 'html/blank_page'; - } - - $vars = array( - 'title' => $report->report, - 'header' => '

There was an error running your report

', - 'error' => $e->getMessage(), - 'content' => "

Report Query

".$report->options['Query_Formatted'], - ); - - echo PhpReports::render($template, $vars); - } - } -} diff --git a/classes/report_formats/JsonReportFormat.php b/classes/report_formats/JsonReportFormat.php deleted file mode 100644 index f572359b..00000000 --- a/classes/report_formats/JsonReportFormat.php +++ /dev/null @@ -1,71 +0,0 @@ -run(); - - if (!$report->options['DataSets']) { - return; - } - - $result = array(); - if (isset($_GET['datasets'])) { - $datasets = $_GET['datasets']; - // If all the datasets should be included - if ($datasets === 'all') { - $datasets = array_keys($report->options['DataSets']); - } - // If just a single dataset was specified, make it an array - elseif (!is_array($datasets)) { - $datasets = explode(',', $datasets); - } - - foreach ($datasets as $i) { - $result[] = self::getDataSet($i, $report); - } - } else { - $i = 0; - if (isset($_GET['dataset'])) { - $i = $_GET['dataset']; - } elseif (isset($report->options['default_dataset'])) { - $i = $report->options['default_dataset']; - } - $i = intval($i); - - $dataset = self::getDataSet($i, $report); - $result = $dataset['rows']; - } - - if (defined('JSON_PRETTY_PRINT')) { - echo json_encode($result, JSON_PRETTY_PRINT); - } else { - echo json_encode($result); - } - } - - public static function getDataSet($i, &$report) - { - $dataset = array(); - foreach ($report->options['DataSets'][$i] as $k => $v) { - $dataset[$k] = $v; - } - - $rows = array(); - foreach ($dataset['rows'] as $i => $row) { - $tmp = array(); - foreach ($row['values'] as $key => $value) { - $tmp[$value->key] = $value->getValue(); - } - $rows[] = $tmp; - } - $dataset['rows'] = $rows; - - return $dataset; - } -} diff --git a/classes/report_formats/RawReportFormat.php b/classes/report_formats/RawReportFormat.php deleted file mode 100644 index fc9f69c7..00000000 --- a/classes/report_formats/RawReportFormat.php +++ /dev/null @@ -1,20 +0,0 @@ -renderReportPage('sql/report'); - } -} diff --git a/classes/report_formats/TableReportFormat.php b/classes/report_formats/TableReportFormat.php deleted file mode 100644 index a38fa4d5..00000000 --- a/classes/report_formats/TableReportFormat.php +++ /dev/null @@ -1,15 +0,0 @@ -options['inline_email'] = true; - $report->use_cache = true; - - try { - $html = $report->renderReportPage('html/table'); - echo $html; - } catch (Exception $e) { - } - } -} diff --git a/classes/report_formats/TextReportFormat.php b/classes/report_formats/TextReportFormat.php deleted file mode 100644 index 8f5754df..00000000 --- a/classes/report_formats/TextReportFormat.php +++ /dev/null @@ -1,101 +0,0 @@ -use_cache = true; - - //run the report - $report->run(); - - if (!$report->options['DataSets']) { - return; - } - - foreach ($report->options['DataSets'] as $i => $dataset) { - if (isset($dataset['title'])) { - echo $dataset['title']."\n"; - } - TextReportFormat::displayDataSet($dataset); - - // If this isn't the last dataset, add some spacing - if ($i < count($report->options['DataSets'])-1) { - echo "\n\n"; - } - } - } - - protected static function displayDataSet($dataset) - { - /** - * This code taken from Stack Overflow answer by ehudokai - * http://stackoverflow.com/a/4597190 - */ - - //first get your sizes - $sizes = array(); - $first_row = $dataset['rows'][0]; - foreach ($first_row['values'] as $key => $value) { - $key = $value->key; - $value = $value->getValue(); - - //initialize to the size of the column name - $sizes[$key] = strlen($key); - } - foreach ($dataset['rows'] as $row) { - foreach ($row['values'] as $key => $value) { - $key = $value->key; - $value = $value->getValue(); - - $length = strlen($value); - if ($length > $sizes[$key]) { - $sizes[$key] = $length; - } // get largest result size - } - } - - //top of output - foreach ($sizes as $length) { - echo "+".str_pad("", $length+2, "-"); - } - echo "+\n"; - - // column names - foreach ($first_row['values'] as $key => $value) { - $key = $value->key; - $value = $value->getValue(); - - echo "| "; - echo str_pad($key, $sizes[$key]+1); - } - echo "|\n"; - - //line under column names - foreach ($sizes as $length) { - echo "+".str_pad("", $length+2, "-"); - } - echo "+\n"; - - //output data - foreach ($dataset['rows'] as $row) { - foreach ($row['values'] as $key => $value) { - $key = $value->key; - $value = $value->getValue(); - - echo "| "; - echo str_pad($value, $sizes[$key]+1); - } - echo "|\n"; - } - - //bottom of output - foreach ($sizes as $length) { - echo "+".str_pad("", $length+2, "-"); - } - echo "+\n"; - } -} diff --git a/classes/report_formats/XlsReportBase.php b/classes/report_formats/XlsReportBase.php deleted file mode 100644 index 3cf56292..00000000 --- a/classes/report_formats/XlsReportBase.php +++ /dev/null @@ -1,76 +0,0 @@ -getProperties()->setCreator("PHP-Reports") - ->setLastModifiedBy("PHP-Reports") - ->setTitle("") - ->setSubject("") - ->setDescription(""); - - foreach ($report->options['DataSets'] as $i => $dataset) { - $objPHPExcel->createSheet($i); - self::addSheet($objPHPExcel, $dataset, $i); - } - - // Set the active sheet to the first one - $objPHPExcel->setActiveSheetIndex(0); - - return $objPHPExcel; - } - public static function addSheet($objPHPExcel, $dataset, $i) - { - $rows = array(); - $row = array(); - $cols = 0; - $first_row = $dataset['rows'][0]; - foreach ($first_row['values'] as $key => $value) { - array_push($row, $value->key); - $cols++; - } - array_push($rows, $row); - $row = array(); - - foreach ($dataset['rows'] as $r) { - foreach ($r['values'] as $key => $value) { - array_push($row, $value->getValue()); - } - array_push($rows, $row); - $row = array(); - } - - $objPHPExcel->setActiveSheetIndex($i)->fromArray($rows, null, 'A1'); - $objPHPExcel->getActiveSheet()->setAutoFilter('A1:'.self::columnLetter($cols).count($rows)); - for ($a = 1; $a <= $cols; $a++) { - $objPHPExcel->getActiveSheet()->getColumnDimension(self::columnLetter($a))->setAutoSize(true); - } - - if (isset($dataset['title'])) { - $objPHPExcel->getActiveSheet()->setTitle($dataset['title']); - } - - return $objPHPExcel; - } -} diff --git a/classes/report_formats/XlsReportFormat.php b/classes/report_formats/XlsReportFormat.php deleted file mode 100644 index 828e1a53..00000000 --- a/classes/report_formats/XlsReportFormat.php +++ /dev/null @@ -1,30 +0,0 @@ -options['Name']); - - //always use cache for Excel reports - $report->use_cache = true; - - //run the report - $report->run(); - - if (!$report->options['DataSets']) { - return; - } - - $objPHPExcel = parent::getExcelRepresantation($report); - - $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5'); - - header('Content-Type: application/vnd.ms-excel'); - header('Content-Disposition: attachment;filename="'.$file_name.'.xls"'); - header('Pragma: no-cache'); - header('Expires: 0'); - - $objWriter->save('php://output'); - } -} diff --git a/classes/report_formats/XlsxReportFormat.php b/classes/report_formats/XlsxReportFormat.php deleted file mode 100644 index 295b1d31..00000000 --- a/classes/report_formats/XlsxReportFormat.php +++ /dev/null @@ -1,30 +0,0 @@ -options['Name']); - - //always use cache for Excel reports - $report->use_cache = true; - - //run the report - $report->run(); - - if (!$report->options['DataSets']) { - return; - } - - $objPHPExcel = parent::getExcelRepresantation($report); - - $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); - - header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - header('Content-Disposition: attachment;filename="'.$file_name.'.xlsx"'); - header('Pragma: no-cache'); - header('Expires: 0'); - - $objWriter->save('php://output'); - } -} diff --git a/classes/report_formats/XmlReportFormat.php b/classes/report_formats/XmlReportFormat.php deleted file mode 100644 index 17e652e4..00000000 --- a/classes/report_formats/XmlReportFormat.php +++ /dev/null @@ -1,41 +0,0 @@ -options['DataSets']); - } - // If just a single dataset was specified, make it an array - elseif (!is_array($datasets)) { - $datasets = explode(',', $datasets); - } - } else { - $i = 0; - if (isset($_GET['dataset'])) { - $i = $_GET['dataset']; - } elseif (isset($report->options['default_dataset'])) { - $i = $report->options['default_dataset']; - } - $i = intval($i); - - $datasets = array($i); - } - - echo $report->renderReportPage('xml/report', array( - 'datasets' => $datasets, - 'dataset_format' => $dataset_format, - )); - } -} diff --git a/classes/report_types/AdoPivotReportType.php b/classes/report_types/AdoPivotReportType.php deleted file mode 100644 index 258a6ba2..00000000 --- a/classes/report_types/AdoPivotReportType.php +++ /dev/null @@ -1,190 +0,0 @@ -options['Environment']][$report->options['Database']])) { - throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); - } - - //make sure the syntax highlighting is using the proper class - SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; - - //set a formatted query here for debugging. It will be overwritten below after macros are substituted. - $report->options['Query_Formatted'] = "
".$report->raw_query."
"; - - $object = spyc_load($report->raw_query); - - $report->raw_query = []; - //if there are any included reports, add the report sql to the top - if (isset($report->options['Includes'])) { - $included_sql = ''; - foreach ($report->options['Includes'] as &$included_report) { - $included_sql .= trim($included_report->raw_query)."\n"; - } - if (strlen($included_sql) > 0) { - $report->raw_query[] = $included_sql; - } - } - - $report->raw_query[] = $object; - } - - public static function openConnection(&$report) - { - if (isset($report->conn)) { - return; - } - - $environments = PhpReports::$config['environments']; - $config = $environments[$report->options['Environment']][$report->options['Database']]; - - if (!($report->conn = ADONewConnection($config['uri']))) { - throw new Exception('Could not connect to the database'); - } - } - - public static function closeConnection(&$report) - { - if (!isset($report->conn)) { - return; - } - if ($report->conn->IsConnected()) { - $report->conn->Close(); - } - unset($report->conn); - } - - public static function getVariableOptions($params, &$report) - { - $report->conn->SetFetchMode(ADODB_FETCH_NUM); - $query = 'SELECT DISTINCT '.$params['column'].' FROM '.$params['table']; - - if (isset($params['where'])) { - $query .= ' WHERE '.$params['where']; - } - - $macros = $report->macros; - foreach ($macros as $key => $value) { - if (is_array($value)) { - foreach ($value as $key2 => $value2) { - $value[$key2] = trim($value2); - } - $macros[$key] = $value; - } else { - $macros[$key] = $value; - } - - if ($value === 'ALL') { - $macros[$key.'_all'] = true; - } - } - - //add the config and environment settings as macros - $macros['config'] = PhpReports::$config; - $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; - - $result = $report->conn->Execute(PhpReports::renderString($query, $macros)); - - if (!$result) { - throw new Exception("Unable to get variable options: ".$report->conn->ErrorMsg()); - } - - $options = array(); - - if (isset($params['all']) && $params['all']) { - $options[] = 'ALL'; - } - - while ($row = $result->FetchRow()) { - if ($result->FieldCount() > 1) { - $options[] = ['display' => $row[0], 'value' => $row[1]]; - } else { - $options[] = $row[0]; - } - } - - return $options; - } - - public static function run(&$report) - { - $report->conn->SetFetchMode(ADODB_FETCH_ASSOC); - $rows = []; - - $macros = $report->macros; - foreach ($macros as $key => $value) { - if (is_array($value)) { - $first = true; - foreach ($value as $key2 => $value2) { - $value[$key2] = mysql_real_escape_string(trim($value2)); - $first = false; - } - $macros[$key] = $value; - } else { - $macros[$key] = mysql_real_escape_string($value); - } - - if ($value === 'ALL') { - $macros[$key.'_all'] = true; - } - } - - //add the config and environment settings as macros - $macros['config'] = PhpReports::$config; - $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; - - $raw_sql = ""; - foreach ($report->raw_query as $qry) { - if (is_array($qry)) { - foreach ($qry as $key => $value) { - // TODO handle arrays better - if (!is_bool($value) && !is_array($value)) { - $qry[$key] = PhpReports::renderString($value, $macros); - } - } - //TODO This sux - need a class or something :-) - $raw_sql .= PivotTableSQL($report->conn, $qry['tables'], $qry['rows'], $qry['columns'], $qry['where'], $qry['orderBy'], $qry['limit'], $qry['agg_field'], $qry['agg_label'], $qry['agg_fun'], $qry['include_agg_field'], $qry['show_count']); - } else { - $raw_sql .= $qry; - } - } - - //expand macros in query - $sql = PhpReports::render($raw_sql, $macros); - - $report->options['Query'] = $sql; - - $report->options['Query_Formatted'] = SqlFormatter::format($sql); - - //split into individual queries and run each one, saving the last result - $queries = SqlFormatter::splitQuery($sql); - - foreach ($queries as $query) { - if (!is_array($query)) { - //skip empty queries - $query = trim($query); - if (!$query) { - continue; - } - - $result = $report->conn->Execute($query); - if (!$result) { - throw new Exception("Query failed: ".$report->conn->ErrorMsg()); - } - - //if this query had an assert=empty flag and returned results, throw error - if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { - if ($result->GetAssoc()) { - throw new Exception("Assert failed. Query did not return empty results."); - } - } - } - } - - return $result->GetArray(); - } -} diff --git a/classes/report_types/AdoReportType.php b/classes/report_types/AdoReportType.php deleted file mode 100644 index e0390565..00000000 --- a/classes/report_types/AdoReportType.php +++ /dev/null @@ -1,172 +0,0 @@ -options['Environment']][$report->options['Database']])) { - throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); - } - - //make sure the syntax highlighting is using the proper class - SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; - - //default host macro to mysql's host if it isn't defined elsewhere - //if(!isset($report->macros['host'])) $report->macros['host'] = $mysql['host']; - - //replace legacy shorthand macro format - foreach ($report->macros as $key => $value) { - $params = []; - if (isset($report->options['Variables'][$key])) { - $params = $report->options['Variables'][$key]; - } - - //macros shortcuts for arrays - if (isset($params['multiple']) && $params['multiple']) { - //allow {macro} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} - //this is shorthand for comma separated list - $report->raw_query = preg_replace('/([^\{])\{'.$key.'\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}\'{{ item }}\'{% endfor %}$2', $report->raw_query); - - //allow {(macro)} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} - //this is shorthand for quoted, comma separated list - $report->raw_query = preg_replace('/([^\{])\{\('.$key.'\)\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}(\'{{ item }}\'){% endfor %}$2', $report->raw_query); - } else { - //macros sortcuts for non-arrays - //allow {macro} instead of {{macro}} for legacy support - $report->raw_query = preg_replace('/([^\{])(\{'.$key.'+\})([^\}])/', '$1{$2}$3', $report->raw_query); - } - } - - //if there are any included reports, add the report sql to the top - if (isset($report->options['Includes'])) { - $included_sql = ''; - foreach ($report->options['Includes'] as &$included_report) { - $included_sql .= trim($included_report->raw_query)."\n"; - } - - $report->raw_query = $included_sql.$report->raw_query; - } - - //set a formatted query here for debugging. It will be overwritten below after macros are substituted. - $report->options['Query_Formatted'] = SqlFormatter::format($report->raw_query); - } - - public static function openConnection(&$report) - { - if (isset($report->conn)) { - return; - } - - $environments = PhpReports::$config['environments']; - $config = $environments[$report->options['Environment']][$report->options['Database']]; - - if (!($report->conn = ADONewConnection($config['uri']))) { - throw new Exception('Could not connect to the database'); - } - } - - public static function closeConnection(&$report) - { - if (!isset($report->conn)) { - return; - } - if ($report->conn->IsConnected()) { - $report->conn->Close(); - } - unset($report->conn); - } - - public static function getVariableOptions($params, &$report) - { - $report->conn->SetFetchMode(ADODB_FETCH_NUM); - $query = 'SELECT DISTINCT '.$params['column'].' FROM '.$params['table']; - - if (isset($params['where'])) { - $query .= ' WHERE '.$params['where']; - } - - $result = $report->conn->Execute($query); - - if (!$result) { - throw new Exception("Unable to get variable options: ".$report->conn->ErrorMsg()); - } - - $options = []; - - if (isset($params['all']) && $params['all']) { - $options[] = 'ALL'; - } - - while ($row = $result->FetchRow()) { - if ($result->FieldCount() > 1) { - $options[] = ['display' => $row[0], 'value' => $row[1]]; - } else { - $options[] = $row[0]; - } - } - - return $options; - } - - public static function run(&$report) - { - $report->conn->SetFetchMode(ADODB_FETCH_ASSOC); - $rows = []; - - $macros = $report->macros; - foreach ($macros as $key => $value) { - if (is_array($value)) { - $first = true; - foreach ($value as $key2 => $value2) { - $value[$key2] = mysql_real_escape_string(trim($value2)); - $first = false; - } - $macros[$key] = $value; - } else { - $macros[$key] = mysql_real_escape_string($value); - } - - if ($value === 'ALL') { - $macros[$key.'_all'] = true; - } - } - - //add the config and environment settings as macros - $macros['config'] = PhpReports::$config; - $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; - - //expand macros in query - $sql = PhpReports::render($report->raw_query, $macros); - - $report->options['Query'] = $sql; - - $report->options['Query_Formatted'] = SqlFormatter::format($sql); - - //split into individual queries and run each one, saving the last result - $queries = SqlFormatter::splitQuery($sql); - - foreach ($queries as $query) { - //skip empty queries - $query = trim($query); - if (!$query) { - continue; - } - - $result = $report->conn->Execute($query); - if (!$result) { - throw new Exception("Query failed: ".$report->conn->ErrorMsg()); - } - - //if this query had an assert=empty flag and returned results, throw error - if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { - if ($result->GetAssoc()) { - throw new Exception("Assert failed. Query did not return empty results."); - } - } - } - - return $result->GetArray(); - } -} diff --git a/classes/report_types/MongoReportType.php b/classes/report_types/MongoReportType.php deleted file mode 100644 index 3e10bfac..00000000 --- a/classes/report_types/MongoReportType.php +++ /dev/null @@ -1,83 +0,0 @@ -options['Environment']][$report->options['Database']])) { - throw new Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); - } - - $mongo = $environments[$report->options['Environment']][$report->options['Database']]; - - //default host macro to mysql's host if it isn't defined elsewhere - if (!isset($report->macros['host'])) { - $report->macros['host'] = $mongo['host']; - } - - //if there are any included reports, add it to the top of the raw query - if (isset($report->options['Includes'])) { - $included_code = ''; - foreach ($report->options['Includes'] as &$included_report) { - $included_code .= trim($included_report->raw_query)."\n"; - } - - $report->raw_query = $included_code.$report->raw_query; - } - } - - public static function openConnection(&$report) - { - } - - public static function closeConnection(&$report) - { - } - - public static function run(&$report) - { - $eval = ''; - foreach ($report->macros as $key => $value) { - if (is_array($value)) { - $value = json_encode($value); - } else { - $value = '"'.addslashes($value).'"'; - } - - $eval .= 'var '.$key.' = '.$value.';'."\n"; - } - $eval .= $report->raw_query; - - $environments = PhpReports::$config['environments']; - $config = $environments[$report->options['Environment']][$report->options['Database']]; - - $mongo_database = isset($report->options['Mongodatabase']) ? $report->options['Mongodatabase'] : ''; - - //command without eval string - $command = 'mongo '.$config['host'].':'.$config['port'].'/'.$mongo_database.' --quiet --eval '; - - //easy to read formatted query - $report->options['Query_Formatted'] = '
-
$ '.$command.'"..."
'. - 'Eval String:'. - '
'.htmlentities($eval).'
-
'; - - //escape the eval string and add it to the command - $command .= escapeshellarg($eval); - $report->options['Query'] = '$ '.$command; - - //include stderr so we can capture shell errors (like "command mongo not found") - $result = shell_exec($command.' 2>&1'); - - $result = trim($result); - - $json = json_decode($result, true); - if ($json === NULL) { - throw new Exception($result); - } - - return $json; - } -} diff --git a/classes/report_types/MysqlReportType.php b/classes/report_types/MysqlReportType.php deleted file mode 100644 index 32f8c5c9..00000000 --- a/classes/report_types/MysqlReportType.php +++ /dev/null @@ -1,6 +0,0 @@ -options['Environment']][$report->options['Database']])) { - throw new Exception("No ".$report->options['Database']." info defined for environment '".$report->options['Environment']."'"); - } - - //make sure the syntax highlighting is using the proper class - SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; - - //replace legacy shorthand macro format - foreach ($report->macros as $key => $value) { - if (isset($report->options['Variables'][$key])) { - $params = $report->options['Variables'][$key]; - } else { - $params = []; - } - - //macros shortcuts for arrays - if (isset($params['multiple']) && $params['multiple']) { - //allow {macro} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} - //this is shorthand for comma separated list - $report->raw_query = preg_replace('/([^\{])\{'.$key.'\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}\'{{ item }}\'{% endfor %}$2', $report->raw_query); - - //allow {(macro)} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} - //this is shorthand for quoted, comma separated list - $report->raw_query = preg_replace('/([^\{])\{\('.$key.'\)\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}(\'{{ item }}\'){% endfor %}$2', $report->raw_query); - } else { - //macros sortcuts for non-arrays - //allow {macro} instead of {{macro}} for legacy support - $report->raw_query = preg_replace('/([^\{])(\{'.$key.'+\})([^\}])/', '$1{$2}$3', $report->raw_query); - } - } - - //if there are any included reports, add the report sql to the top - if (isset($report->options['Includes'])) { - $included_sql = ''; - foreach ($report->options['Includes'] as &$included_report) { - $included_sql .= trim($included_report->raw_query)."\n"; - } - - $report->raw_query = $included_sql.$report->raw_query; - } - - //set a formatted query here for debugging. It will be overwritten below after macros are substituted. - $report->options['Query_Formatted'] = SqlFormatter::format($report->raw_query); - } - - public static function openConnection(&$report) - { - if (isset($report->conn)) { - return; - } - - $environments = PhpReports::$config['environments']; - $config = $environments[$report->options['Environment']][$report->options['Database']]; - - if (isset($config['dsn'])) { - $dsn = $config['dsn']; - } else { - $host = $config['host']; - if (isset($report->options['access']) && $report->options['access'] === 'rw') { - if (isset($config['host_rw'])) { - $host = $config['host_rw']; - } - } - - $driver = isset($config['driver']) ? $config['driver'] : static::$default_driver; - - if (!$driver) { - throw new Exception("Must specify database `driver` (e.g. 'mysql')"); - } - - $dsn = $driver.':host='.$host; - - if (isset($config['database'])) { - $dsn .= ';dbname='.$config['database']; - } - } - - //the default is to use a user with read only privileges - $username = $config['user']; - $password = $config['pass']; - - //if the report requires read/write privileges - if (isset($report->options['access']) && $report->options['access'] === 'rw') { - if (isset($config['user_rw'])) { - $username = $config['user_rw']; - } - if (isset($config['pass_rw'])) { - $password = $config['pass_rw']; - } - } - - $report->conn = new PDO($dsn, $username, $password); - - $report->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - } - - public static function closeConnection(&$report) - { - if (!isset($report->conn)) { - return; - } - $report->conn = null; - unset($report->conn); - } - - public static function getVariableOptions($params, &$report) - { - $displayColumn = $params['column']; - if (isset($params['display'])) { - $displayColumn = $params['display']; - } - - $query = 'SELECT DISTINCT `'.$params['column'].'` as val, `'.$displayColumn.'` as disp FROM '.$params['table']; - - if (isset($params['where'])) { - $query .= ' WHERE '.$params['where']; - } - - if (isset($params['order']) && in_array($params['order'], ['ASC', 'DESC'])) { - $query .= ' ORDER BY '.$params['column'].' '.$params['order']; - } - - $result = $report->conn->query($query); - - $options = []; - - if (isset($params['all'])) { - $options[] = 'ALL'; - } - - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $options[] = [ - 'value' => $row['val'], - 'display' => $row['disp'], - ]; - } - - return $options; - } - - public static function run(&$report) - { - $macros = $report->macros; - foreach ($macros as $key => $value) { - if (is_array($value)) { - $first = true; - foreach ($value as $key2 => $value2) { - $value[$key2] = $report->conn->quote(trim($value2)); - $value[$key2] = preg_replace("/(^'|'$)/", '', $value[$key2]); - $first = false; - } - $macros[$key] = $value; - } else { - $macros[$key] = $report->conn->quote($value); - $macros[$key] = preg_replace("/(^'|'$)/", '', $macros[$key]); - } - - if ($value === 'ALL') { - $macros[$key.'_all'] = true; - } - } - - //add the config and environment settings as macros - $macros['config'] = PhpReports::$config; - $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; - - //expand macros in query - $sql = PhpReports::render($report->raw_query, $macros); - - $report->options['Query'] = $sql; - - $report->options['Query_Formatted'] = SqlFormatter::format($sql); - - //split into individual queries and run each one, saving the last result - $queries = SqlFormatter::splitQuery($sql); - - $datasets = []; - - $explicit_datasets = preg_match('/--\s+@dataset(\s*=\s*|\s+)true/', $sql); - - foreach ($queries as $i => $query) { - $is_last = $i === count($queries)-1; - - //skip empty queries - $query = trim($query); - if (!$query) { - continue; - } - - $result = $report->conn->query($query); - - //if this query had an assert=empty flag and returned results, throw error - if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { - if ($result->fetch(PDO::FETCH_ASSOC)) { - throw new Exception("Assert failed. Query did not return empty results."); - } - } - - // If this query should be included as a dataset - if ((!$explicit_datasets && $is_last) || preg_match('/--\s+@dataset(\s*=\s*|\s+)true/', $query)) { - $dataset = ['rows' => []]; - - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $dataset['rows'][] = $row; - } - - // Get dataset title if it has one - if (preg_match('/--\s+@title(\s*=\s*|\s+)(.*)/', $query, $matches)) { - $dataset['title'] = $matches[2]; - } - - $datasets[] = $dataset; - } - } - - return $datasets; - } -} diff --git a/classes/report_types/PhpReportType.php b/classes/report_types/PhpReportType.php deleted file mode 100644 index 76328e51..00000000 --- a/classes/report_types/PhpReportType.php +++ /dev/null @@ -1,98 +0,0 @@ -raw_query = "report."\n".trim($report->raw_query); - - //if there are any included reports, add it to the top of the raw query - if (isset($report->options['Includes'])) { - $included_code = ''; - foreach ($report->options['Includes'] as &$included_report) { - $included_code .= "\n".trim($included_report->raw_query).""; - } - - if ($included_code) { - $included_code .= "\n"; - } - - $report->raw_query = $included_code.$report->raw_query; - - //make sure the raw query has a closing PHP tag at the end - //this makes sure it will play nice as an included report - if (!preg_match('/\?>\s*$/', $report->raw_query)) { - $report->raw_query .= "\n?>"; - } - } - } - - public static function openConnection(&$report) - { - } - - public static function closeConnection(&$report) - { - } - - public static function run(&$report) - { - $eval = "macros as $key => $value) { - $value = var_export($value, true); - - $eval .= "\n".'$'.$key.' = '.$value.';'; - } - $eval .= "\n?>".$report->raw_query; - - $config = PhpReports::$config; - - //store in both $database and $environment for backwards compatibility - $database = PhpReports::$config['environments'][$report->options['Environment']]; - $environment = $database; - - $report->options['Query'] = $report->raw_query; - - $parts = preg_split('/<\?php \/\*(BEGIN|END) (INCLUDED REPORT|REPORT MACROS)\*\/ \?>/', $eval); - $report->options['Query_Formatted'] = ''; - $code = htmlentities(trim(array_pop($parts))); - $linenum = 1; - foreach ($parts as $part) { - if (!trim($part)) { - continue; - } - - //get name of report - $name = preg_match("|//REPORT: ([^\n]+)\n|", $part, $matches); - - if (!$matches) { - $name = "Variables"; - } else { - $name = $matches[1]; - } - - $report->options['Query_Formatted'] .= '
'; - $report->options['Query_Formatted'] .= "
".htmlentities(trim($part))."
"; - $report->options['Query_Formatted'] .= "
"; - $linenum += count(explode("\n", trim($part))); - } - - $report->options['Query_Formatted'] .= '
'.$code.'
'; - - ob_start(); - ini_set('display_errors', 'Off'); - eval('?>'.$eval); - $result = ob_get_contents(); - ob_end_clean(); - ini_set('display_errors', 'On'); - - $result = trim($result); - - $json = json_decode($result, true); - if ($json === null) { - throw new Exception($result); - } - - return $json; - } -} diff --git a/composer.json b/composer.json index 36ffc821..02df6377 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,12 @@ }, "autoload": { "files": [ - "lib/adodb/pivottable.inc.php" + "lib/adodb/pivottable.inc.php", + "lib/simplediff/SimpleDiff.php" ], - "classmap": [ - "vendor/jdorn/file-system-cache/" - ] + "psr-4": { + "PhpReports\\": "src/" + } }, "minimum-stability": "dev", "require-dev": { diff --git a/lib/PhpReports/FilterBase.php b/lib/PhpReports/FilterBase.php deleted file mode 100644 index db4ee626..00000000 --- a/lib/PhpReports/FilterBase.php +++ /dev/null @@ -1,16 +0,0 @@ -getMessage()); - } - - static::init($params, $report); - } - - public static function init($params, &$report) - { - } - - public static function parseShortcut($value) - { - return []; - } - - public static function beforeRender(&$report) - { - } - - public static function afterParse(&$report) - { - } - - public static function beforeRun(&$report) - { - } - - protected static function validate($params) - { - if (!static::$validation) { - return $params; - } - - $errors = []; - - foreach (static::$validation as $key => $rules) { - //fill in default params - if (isset($rules['default']) && !isset($params[$key])) { - $params[$key] = $rules['default']; - continue; - } - - //if the param isn't required and it's defined, we can skip validation - if ((!isset($rules['required']) || !$rules['required']) && !isset($params[$key])) { - continue; - } - - //if the param must be a specific datatype - if (isset($rules['type'])) { - if ($rules['type'] === 'number' && !is_numeric($params[$key])) { - $errors[] = "$key must be a number (".gettype($params[$key])." given)"; - } elseif ($rules['type'] === 'array' && !is_array($params[$key])) { - $errors[] = "$key must be an array (".gettype($params[$key])." given)"; - } elseif ($rules['type'] === 'boolean' && !is_bool($params[$key])) { - $errors[] = "$key must be true or false (".gettype($params[$key])." given)"; - } elseif ($rules['type'] === 'string' && !is_string($params[$key])) { - $errors[] = "$key must be a string (".gettype($params[$key])." given)"; - } elseif ($rules['type'] === 'enum' && !in_array($params[$key], $rules['values'])) { - $errors[] = "$key must be one of: [".implode(', ', $rules['values'])."]"; - } elseif ($rules['type'] === 'object' && !is_array($params[$key])) { - $errors[] = "$key must be an object (".gettype($params[$key])." given)"; - } - } - - //other validation rules - if (isset($rules['min']) && $params[$key] < $rules['min']) { - $errors[] = "$key must be at least $rules[min]"; - } - if (isset($rules['max']) && $params[$key] > $rules['max']) { - $errors[] = "$key must be at most $rules[min]"; - } - - if (isset($rules['pattern']) && !preg_match($rules['pattern'], $params[$key])) { - $errors[] = "$key does not match required pattern"; - } - } - - //every possible param must be defined in the validation rules - foreach ($params as $k => $v) { - if (!isset(static::$validation[$k])) { - $errors[] = "Unknown parameter '$k'"; - } - } - - if ($errors) { - throw new Exception(implode(". ", $errors)); - } else { - return $params; - } - } -} diff --git a/lib/PhpReports/PhpReports.php b/lib/PhpReports/PhpReports.php deleted file mode 100644 index 96259e74..00000000 --- a/lib/PhpReports/PhpReports.php +++ /dev/null @@ -1,779 +0,0 @@ -base; - - if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') { - $protocol = 'https://'; - } else { - $protocol = 'http://'; - } - self::$request->base = $protocol.rtrim($_SERVER['HTTP_HOST'].self::$request->base, '/'); - - //the load order for templates is: "templates/local", "templates/default", "templates" - //this means loading the template "html/report.twig" will load the local first and then the default - //if you want to extend a default template from within a local template, you can do {% extends "default/html/report.twig" %} and it will fall back to the last loader - $template_dirs = array('templates/default','templates'); - if (file_exists('templates/local')) { - array_unshift($template_dirs, 'templates/local'); - } - - $loader = new Twig_Loader_Chain(array( - new Twig_Loader_Filesystem($template_dirs), - new Twig_Loader_String(), - )); - self::$twig = new Twig_Environment($loader); - self::$twig->addFunction(new Twig_SimpleFunction('dbdate', 'PhpReports::dbdate')); - self::$twig->addFunction(new Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN')); - - if (isset($_COOKIE['reports-theme']) && $_COOKIE['reports-theme']) { - $theme = $_COOKIE['reports-theme']; - } else { - $theme = self::$config['bootstrap_theme']; - } - self::$twig->addGlobal('theme', $theme); - self::$twig->addGlobal('path', $path); - self::$twig->addGlobal('brand', self::$config['brand']); - - self::$twig->addFilter('var_dump', new Twig_Filter_Function('var_dump')); - - self::$twig_string = new Twig_Environment(new Twig_Loader_String(), array('autoescape' => false)); - self::$twig_string->addFunction(new Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN')); - - FileSystemCache::$cacheDir = self::$config['cacheDir']; - - if (!isset($_SESSION['environment']) || !isset(self::$config['environments'][$_SESSION['environment']])) { - $environments = array_keys(self::$config['environments']); - $_SESSION['environment'] = array_shift($environments); - } - - // Extend twig. - if (isset($config['twig_init_function']) && is_callable($config['twig_init_function'])) { - $config['twig_init_function'](self::$twig); - $config['twig_init_function'](self::$twig_string); - } - } - - public static function setVar($key, $value) - { - if (!self::$vars) { - self::$vars = array(); - } - - self::$vars[$key] = $value; - } - public static function getVar($key, $default = null) - { - if (isset(self::$vars[$key])) { - return self::$vars[$key]; - } else { - return $default; - } - } - - public static function dbdate($time, $database = null, $format = null) - { - $report = self::getVar('Report', null); - if (!$report) { - return strtotime('Y-m-d H:i:s', strtotime($time)); - } - - //if a variable name was passed in - $var = null; - if (isset($report->options['Variables'][$time])) { - $var = $report->options['Variables'][$time]; - $time = $report->macros[$time]; - } - - $time = strtotime($time); - - $environment = $report->getEnvironment(); - - //determine time offset - $offset = 0; - - if ($database) { - if (isset($environment[$database]['time_offset'])) { - $offset = $environment[$database]['time_offset']; - } - } else { - $database = $report->getDatabase(); - if (isset($database['time_offset'])) { - $offset = $database['time_offset']; - } - } - - //if the time needs to be adjusted - if ($offset) { - $time = strtotime((($offset > 0) ? '+' : '-').abs($offset).' hours', $time); - } - - //determine output format - if ($format) { - $time = date($format, $time); - } elseif ($var && isset($var['format'])) { - $time = date($var['format'], $time); - } else { - //default to Y-m-d H:i:s - $time = date('Y-m-d H:i:s', $time); - } - - return $time; - } - - public static function generateSqlIN($column, $values, $or_null = false) - { - $sql = "$column IN ("; - foreach ($values as $value) { - $sql .= is_numeric($value) ? $value : "'$value'"; - if ($value !== end($values)) { - $sql .= ', '; - } - } - $sql .= ")"; - if ($or_null) { - $sql .= " OR $column IS NULL"; - } - - return $sql; - } - - public static function render($template, $macros) - { - $default = array( - 'base' => self::$request->base, - 'report_list_url' => self::$request->base.'/', - 'request' => self::$request, - 'querystring' => (array_key_exists('QUERY_STRING', $_SERVER) ? $_SERVER['QUERY_STRING'] : null), - 'config' => self::$config, - 'environment' => $_SESSION['environment'], - 'recent_reports' => self::getRecentReports(), - 'session' => $_SESSION, - ); - $macros = array_merge($default, $macros); - - //if a template path like 'html/report' is given, add the twig file extension - if (preg_match('/^[a-zA-Z_\-0-9\/]+$/', $template)) { - $template .= '.twig'; - } - - return self::$twig->render($template, $macros); - } - - public static function renderString($template, $macros) - { - return self::$twig_string->render($template, $macros); - } - - public static function displayReport($report, $type) - { - $classname = ucfirst(strtolower($type)).'ReportFormat'; - - $error_header = 'An error occurred while running your report'; - $content = ''; - - try { - if (!class_exists($classname)) { - $error_header = 'Unknown report format'; - throw new Exception("Unknown report format '$type'"); - } - - try { - $report = $classname::prepareReport($report); - } catch (Exception $e) { - $error_header = 'An error occurred while preparing your report'; - throw $e; - } - - $classname::display($report, self::$request); - - if (isset($report->options['Query_Formatted'])) { - $content = $report->options['Query_Formatted']; - } - } catch (Exception $e) { - echo self::render('html/page', array( - 'title' => $report->report, - 'header' => '

'.$error_header.'

', - 'error' => $e->getMessage(), - 'content' => $content, - 'breadcrumb' => array('Report List' => '', $report->report => true), - )); - } - } - - public static function editReport($report) - { - $template_vars = array(); - - try { - $report = ReportFormatBase::prepareReport($report); - - $template_vars = array( - 'report' => $report->report, - 'options' => $report->options, - 'contents' => $report->getRaw(), - 'extension' => array_pop(explode('.', $report->report)), - ); - } catch (Exception $e) { - //if there is an error parsing the report - $template_vars = array( - 'report' => $report, - 'contents' => Report::getReportFileContents($report), - 'options' => array(), - 'extension' => array_pop(explode('.', $report)), - 'error' => $e, - ); - } - - if (isset($_POST['preview'])) { - echo "
".SimpleDiff::htmlDiffSummary($template_vars['contents'], $_POST['contents'])."
"; - } elseif (isset($_POST['save'])) { - Report::setReportFileContents($template_vars['report'], $_POST['contents']); - } else { - echo self::render('html/report_editor', $template_vars); - } - } - - public static function listReports() - { - $errors = array(); - - $reports = self::getReports(self::$config['reportDir'].'/', $errors); - - $template_vars['reports'] = $reports; - $template_vars['report_errors'] = $errors; - - $start = microtime(true); - echo self::render('html/report_list', $template_vars); - } - - public static function listDashboards() - { - $dashboards = self::getDashboards(); - - uasort($dashboards, function ($a, $b) { - return strcmp($a['title'], $b['title']); - }); - - echo self::render('html/dashboard_list', array( - 'dashboards' => $dashboards, - )); - } - - public static function displayDashboard($dashboard) - { - $content = self::getDashboard($dashboard); - - echo self::render('html/dashboard', array( - 'dashboard' => $content, - )); - } - - public static function getDashboards() - { - $dashboards = glob(PhpReports::$config['dashboardDir'].'/*.json'); - - $ret = array(); - foreach ($dashboards as $key => $value) { - $name = basename($value, '.json'); - $ret[$name] = self::getDashboard($name); - } - - return $ret; - } - - public static function getDashboard($dashboard) - { - $file = PhpReports::$config['dashboardDir'].'/'.$dashboard.'.json'; - if (!file_exists($file)) { - throw new Exception("Unknown dashboard - ".$dashboard); - } - - return json_decode(file_get_contents($file), true); - } - - public static function getRecentReports() - { - $recently_run = FileSystemCache::retrieve(FileSystemCache::generateCacheKey('recently_run')); - $recent = array(); - if ($recently_run !== false) { - $i = 0; - foreach ($recently_run as $report) { - if ($i > 10) { - break; - } - - $headers = self::getReportHeaders($report); - - if (!$headers) { - continue; - } - if (isset($recent[$headers['url']])) { - continue; - } - - $recent[$headers['url']] = $headers; - $i++; - } - } - - return array_values($recent); - } - public static function getReportListJSON($reports = null) - { - if ($reports === null) { - $errors = array(); - $reports = self::getReports(self::$config['reportDir'].'/', $errors); - } - - //weight by popular reports - $recently_run = FileSystemCache::retrieve(FileSystemCache::generateCacheKey('recently_run')); - $popular = array(); - if ($recently_run !== false) { - foreach ($recently_run as $report) { - if (!isset($popular[$report])) { - $popular[$report] = 1; - } else { - $popular[$report]++; - } - } - } - $parts = array(); - - foreach ($reports as $report) { - if ($report['is_dir'] && $report['children']) { - //skip if the directory doesn't have a title - if (!isset($report['Title']) || !$report['Title']) { - continue; - } - - $part = trim(self::getReportListJSON($report['children']), '[],'); - if ($part) { - $parts[] = $part; - } - } else { - //skip if report is marked as dangerous - if ((isset($report['stop']) && $report['stop']) || isset($report['Caution']) || isset($report['warning'])) { - continue; - } - if (!isset($report['url'])) { - continue; - } - if (!isset($report['report'])) { - continue; - } - - //skip if report is marked as ignore - if (isset($report['ignore']) && $report['ignore']) { - continue; - } - - if (isset($popular[$report['report']])) { - $popularity = $popular[$report['report']]; - } else { - $popularity = 0; - } - - $parts[] = json_encode(array( - 'name' => $report['Name'], - 'url' => $report['url'], - 'popularity' => $popularity, - )); - } - } - - return '['.trim(implode(',', $parts), ',').']'; - } - - protected static function getReportHeaders($report) - { - $cacheKey = FileSystemCache::generateCacheKey(array(self::$request->base, $report), 'report_headers'); - - //check if report data is cached and newer than when the report file was created - //the url parameter ?nocache will bypass this and not use cache - $data = false; - - $loc = Report::getFileLocation($report); - if (!file_exists($loc)) { - return false; - } - if (!isset($_REQUEST['nocache'])) { - $data = FileSystemCache::retrieve($cacheKey, filemtime($loc)); - } - - //report data not cached, need to parse it - if ($data === false) { - $temp = new Report($report); - - $data = $temp->options; - - $data['report'] = $report; - $data['url'] = self::$request->base.'/report/html/?report='.$report; - $data['is_dir'] = false; - $data['Id'] = str_replace(array('_', '-', '/', ' ', '.'), array('', '', '_', '-', '_'), trim($report, '/')); - if (!isset($data['Name'])) { - $data['Name'] = ucwords(str_replace(array('_', '-'), ' ', basename($report))); - } - - //store parsed report in cache - FileSystemCache::store($cacheKey, $data); - } - - return $data; - } - - protected static function getReports($dir, &$errors = null) - { - $base = self::$config['reportDir'].'/'; - - $reports = glob($dir.'*', GLOB_NOSORT); - $return = array(); - foreach ($reports as $key => $report) { - $title = $description = false; - - if (is_dir($report)) { - if (file_exists($report.'/TITLE.txt')) { - $title = file_get_contents($report.'/TITLE.txt'); - } - if (file_exists($report.'/README.txt')) { - $description = file_get_contents($report.'/README.txt'); - } - - $id = str_replace(array('_', '-', '/', ' '), array('', '', '_', '-'), trim(substr($report, strlen($base)), '/')); - - $children = self::getReports($report.'/', $errors); - - $count = 0; - foreach ($children as $child) { - if (isset($child['count'])) { - $count += $child['count']; - } else { - $count++; - } - } - - $return[] = array( - 'Name' => ucwords(str_replace(array('_', '-'), ' ', basename($report))), - 'Title' => $title, - 'Id' => $id, - 'Description' => $description, - 'is_dir' => true, - 'children' => $children, - 'count' => $count, - ); - } else { - //files to skip - if (strpos(basename($report), '.') === false) { - continue; - } - $reportExploded = explode('.', $report); - $ext = array_pop($reportExploded); - if (!isset(self::$config['default_file_extension_mapping'][$ext])) { - continue; - } - - $name = substr($report, strlen($base)); - - try { - $data = self::getReportHeaders($name, $base); - $return[] = $data; - } catch (Exception $e) { - if (!$errors) { - $errors = array(); - } - $errors[] = array( - 'report' => $name, - 'exception' => $e, - ); - } - } - } - - usort($return, function (&$a, &$b) { - if ($a['is_dir'] && !$b['is_dir']) { - return 1; - } elseif ($b['is_dir'] && !$a['is_dir']) { - return -1; - } - - if (empty($a['Title']) && empty($b['Title'])) { - return strcmp($a['Name'], $b['Name']); - } elseif (empty($a['Title'])) { - return 1; - } elseif (empty($b['Title'])) { - return -1; - } - - return strcmp($a['Title'], $b['Title']); - }); - - return $return; - } - - /** - * Emails a report given a TO address, a subject, and a message - */ - public static function emailReport() - { - if (!isset($_REQUEST['email']) || !filter_var($_REQUEST['email'], FILTER_VALIDATE_EMAIL)) { - echo json_encode(array('error' => 'Valid email address required')); - - return; - } - if (!isset($_REQUEST['url'])) { - echo json_encode(array('error' => 'Report url required')); - - return; - } - if (!isset(PhpReports::$config['mail_settings']['enabled']) || !PhpReports::$config['mail_settings']['enabled']) { - echo json_encode(array('error' => 'Email is disabled on this server')); - - return; - } - if (!isset(PhpReports::$config['mail_settings']['from'])) { - echo json_encode(array('error' => 'Email settings have not been properly configured on this server')); - - return; - } - - $from = PhpReports::$config['mail_settings']['from']; - $subject = $_REQUEST['subject'] ? $_REQUEST['subject'] : 'Database Report'; - $body = $_REQUEST['message'] ? $_REQUEST['message'] : "You've been sent a database report!"; - $email = $_REQUEST['email']; - $link = $_REQUEST['url']; - $csv_link = str_replace('report/html/?', 'report/csv/?', $link); - $table_link = str_replace('report/html/?', 'report/table/?', $link); - $text_link = str_replace('report/html/?', 'report/text/?', $link); - - // Get the CSV file attachment and the inline HTML table - $csv = self::urlDownload($csv_link); - $table = self::urlDownload($table_link); - $text = self::urlDownload($text_link); - - $email_text = $body."\n\n".$text."\n\nView the report online at $link"; - $email_html = "

$body

$table

View the report online at ".htmlentities($link)."

"; - - // Create the message - $message = Swift_Message::newInstance() - ->setSubject($subject) - ->setFrom($from) - ->setTo($email) - //text body - ->setBody($email_text) - //html body - ->addPart($email_html, 'text/html') - ; - - $attachment = Swift_Attachment::newInstance() - ->setFilename('report.csv') - ->setContentType('text/csv') - ->setBody($csv) - ; - - $message->attach($attachment); - - // Create the Transport - $transport = self::getMailTransport(); - $mailer = Swift_Mailer::newInstance($transport); - - try { - // Send the message - $result = $mailer->send($message); - } catch (Exception $e) { - echo json_encode(array( - 'error' => $e->getMessage(), - )); - - return; - } - - if ($result) { - echo json_encode(array( - 'success' => true, - )); - } else { - echo json_encode(array( - 'error' => 'Failed to send email to requested recipient', - )); - } - } - - /** - * Determines the email transport to use based on the configuration settings - */ - protected static function getMailTransport() - { - if (!isset(PhpReports::$config['mail_settings'])) { - PhpReports::$config['mail_settings'] = array(); - } - if (!isset(PhpReports::$config['mail_settings']['method'])) { - PhpReports::$config['mail_settings']['method'] = 'mail'; - } - - switch (PhpReports::$config['mail_settings']['method']) { - case 'mail': - return Swift_MailTransport::newInstance(); - case 'sendmail': - return Swift_MailTransport::newInstance( - isset(PhpReports::$config['mail_settings']['command']) ? PhpReports::$config['mail_settings']['command'] : '/usr/sbin/sendmail -bs' - ); - case 'smtp': - if (!isset(PhpReports::$config['mail_settings']['server'])) { - throw new Exception("SMTP server must be configured"); - } - $transport = Swift_SmtpTransport::newInstance( - PhpReports::$config['mail_settings']['server'], - isset(PhpReports::$config['mail_settings']['port']) ? PhpReports::$config['mail_settings']['port'] : 25 - ); - - //if username/password - if (isset(PhpReports::$config['mail_settings']['username'])) { - $transport->setUsername(PhpReports::$config['mail_settings']['username']); - $transport->setPassword(PhpReports::$config['mail_settings']['password']); - } - - //if using encryption - if (isset(PhpReports::$config['mail_settings']['encryption'])) { - $transport->setEncryption(PhpReports::$config['mail_settings']['encryption']); - } - - return $transport; - default: - throw new Exception("Mail method must be either 'mail', 'sendmail', or 'smtp'"); - } - } - - /** - * Autoloader methods - */ - public static function loader($className) - { - if (!isset(self::$loader_cache)) { - self::buildLoaderCache(); - } - - if (isset(self::$loader_cache[$className])) { - require_once self::$loader_cache[$className]; - - return true; - } else { - return false; - } - } - public static function buildLoaderCache() - { - self::load('classes/local'); - self::load('classes', array('classes/local')); - self::load('lib'); - } - public static function load($dir, $skip = array()) - { - $files = glob($dir.'/*.php'); - $dirs = glob($dir.'/*', GLOB_ONLYDIR); - - foreach ($files as $file) { - //for file names same as class name - $className = basename($file, '.php'); - if (!isset(self::$loader_cache[$className])) { - self::$loader_cache[$className] = $file; - } - - //for PEAR style: Path_To_Class.php - $parts = explode('/', substr($file, 0, -4)); - array_shift($parts); - $className = implode('_', $parts); - //if any of the directories in the path are lowercase, it isn't in PEAR format - if (preg_match('/(^|_)[a-z]/', $className)) { - continue; - } - if (!isset(self::$loader_cache[$className])) { - self::$loader_cache[$className] = $file; - } - } - - foreach ($dirs as $dir2) { - //directories to skip - if ($dir2[0] === '.') { - continue; - } - if (in_array($dir2, $skip)) { - continue; - } - if (in_array(basename($dir2), array('tests', 'test', 'example', 'examples', 'bin'))) { - continue; - } - - self::load($dir2, $skip); - } - } - - /** - * A more lenient json_decode than the built-in PHP one. - * It supports strict JSON as well as javascript syntax (i.e. unquoted/single quoted keys, single quoted values, trailing commmas) - */ - public static function json_decode($json, $assoc = false) - { - //replace single quoted values - $json = preg_replace_callback('/:\s*\'(([^\']|\\\\\')*)\'\s*([},])/', create_function('$matches', 'return "\':\'.json_encode(stripslashes(\'$matches[1]\')).\'$matches[3]\'";'), $json); - - //replace single quoted keys - $json = preg_replace_callback('/\'(([^\']|\\\\\')*)\'\s*:/', create_function('$matches', 'return "json_encode(stripslashes(\'$matches[1]\')).\':\'";'), $json); - - //remove any line breaks in the code - $json = str_replace(array("\n", "\r"), "", $json); - - //replace non-quoted keys with double quoted keys - $json = preg_replace('#(?
\{|\[|,)\s*(?(?:\w|_)+)\s*:#im', '$1"$2":', $json);
-
-        //remove trailing comma
-        $json = preg_replace('/,\s*\}/', '}', $json);
-
-        return json_decode($json, $assoc);
-    }
-
-    protected static function urlDownload($url)
-    {
-        $ch = curl_init();
-        curl_setopt($ch, CURLOPT_URL, $url);
-        curl_setopt($ch, CURLOPT_HEADER, 0);
-        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-
-        $output = curl_exec($ch);
-        curl_close($ch);
-
-        return $output;
-    }
-}
-PhpReports::init();
diff --git a/lib/PhpReports/Report.php b/lib/PhpReports/Report.php
deleted file mode 100644
index 4328a900..00000000
--- a/lib/PhpReports/Report.php
+++ /dev/null
@@ -1,720 +0,0 @@
-report = $report;
-
-        if (!file_exists(self::getFileLocation($report))) {
-            throw new Exception('Report not found - '.$report);
-        }
-
-        $this->filemtime = filemtime(self::getFileLocation($report));
-
-        $this->use_cache = $use_cache;
-
-        //get the raw report file
-        $this->raw = self::getReportFileContents($report);
-
-        //if there are no headers in this report
-        if (strpos($this->raw, "\n\n") === false) {
-            throw new Exception('Report missing headers - '.$report);
-        }
-
-        //split the raw report into headers and code
-        list($this->raw_headers, $this->raw_query) = explode("\n\n", $this->raw, 2);
-
-        $this->macros = [];
-        foreach ($macros as $key => $value) {
-            $this->addMacro($key, $value);
-        }
-
-        $this->parseHeaders();
-
-        $this->options['Environment'] = $environment;
-
-        $this->initDb();
-
-        $this->getTimeEstimate();
-    }
-
-    public static function getFileLocation($report)
-    {
-        //make sure the report path doesn't go up a level for security reasons
-        if (strpos($report, "..") !== false) {
-            $reportdir = realpath(PhpReports::$config['reportDir']).'/';
-            $reportpath = substr(realpath(PhpReports::$config['reportDir'].'/'.$report), 0, strlen($reportdir));
-
-            if ($reportpath !== $reportdir) {
-                throw new Exception('Invalid report - '.$report);
-            }
-        }
-
-        $reportDir = PhpReports::$config['reportDir'];
-
-        return $reportDir.'/'.$report;
-    }
-
-    public static function setReportFileContents($report, $new_contents)
-    {
-        echo "SAVING CONTENTS TO ".self::getFileLocation($report);
-
-        if (!file_put_contents(self::getFileLocation($report), $new_contents)) {
-            throw new Exception("Failed to set report contents");
-        }
-
-        echo "\n".$new_contents;
-    }
-
-    public static function getReportFileContents($report)
-    {
-        $contents = file_get_contents(self::getFileLocation($report));
-
-        //convert EOL to unix format
-        return str_replace(["\r\n", "\r"], "\n", $contents);
-    }
-
-    public function getDatabase()
-    {
-        if (isset($this->options['Database']) && $this->options['Database']) {
-            $environment = $this->getEnvironment();
-
-            if (isset($environment[$this->options['Database']])) {
-                return $environment[$this->options['Database']];
-            }
-        }
-
-        return array();
-    }
-
-    public function getEnvironment()
-    {
-        return PhpReports::$config['environments'][$this->options['Environment']];
-    }
-
-    public function addMacro($name, $value)
-    {
-        $this->macros[$name] = $value;
-    }
-
-    public function exportHeader($name, $params)
-    {
-        $this->exported_headers[] = ['name' => $name, 'params' => $params];
-    }
-
-    public function getCacheKey()
-    {
-        return FileSystemCache::generateCacheKey(
-            [
-                'report' => $this->report,
-                'macros' => $this->macros,
-                'database' => $this->options['Environment'],
-            ],
-            'report_results'
-        );
-    }
-
-    public function getReportTimesCacheKey()
-    {
-        return FileSystemCache::generateCacheKey($this->report, 'report_times');
-    }
-
-    protected function retrieveFromCache()
-    {
-        if (!$this->use_cache) {
-            return false;
-        }
-
-        return FileSystemCache::retrieve($this->getCacheKey(), 'results', $this->filemtime);
-    }
-
-    protected function storeInCache()
-    {
-        if (isset($this->options['Cache']) && is_numeric($this->options['Cache'])) {
-            $ttl = intval($this->options['Cache']);
-        } else {
-            $ttl = 600; //default to caching things for 10 minutes
-        }
-
-        FileSystemCache::store($this->getCacheKey(), $this->options, 'results', $ttl);
-    }
-
-    protected function parseHeaders()
-    {
-        //default the report to being ready
-        //if undefined variables are found in the headers, set to false
-        $this->is_ready = true;
-
-        $this->options = [
-            'Filters' => [],
-            'Variables' => [],
-            'Includes' => [],
-        ];
-
-        $this->headers = [];
-
-        $lines = explode("\n", $this->raw_headers);
-
-        //remove empty headers and remove comment characters
-        $fixed_lines = [];
-
-        foreach ($lines as $line) {
-            if (empty($line)) {
-                continue;
-            }
-
-            //if the line doesn't start with a comment character, skip
-            if (!in_array(substr($line, 0, 2), ['--', '/*', '//', ' *']) && $line[0] !== '#') {
-                continue;
-            }
-
-            //remove comment from start of line and skip if empty
-            $line = trim(ltrim($line, "-*/# \t"));
-            if (!$line) {
-                continue;
-            }
-
-            $fixed_lines[] = $line;
-        }
-
-        $lines = $fixed_lines;
-
-        $name = null;
-        $value = '';
-        foreach ($lines as $line) {
-            $has_name_value = preg_match('/^\s*[A-Z0-9_\-]+\s*\:/', $line);
-
-            //if this is the first header and not in the format name:value, assume it is the report name
-            if (!$has_name_value && $name === null && (!isset($this->options['Name']) || !$this->options['Name'])) {
-                $this->parseHeader('Info', ['name' => $line]);
-            } else {
-                //if this is a continuation of another header
-                if (!$has_name_value) {
-                    $value .= "\n".trim($line);
-                } else {
-                    //if this is a new header
-                    //if the previous header didn't have a name, assume it is the description
-                    if ($value && $name === null) {
-                        $this->parseHeader('Info', ['description' => $value]);
-                    } elseif ($value) {
-                        //otherwise, parse the previous header
-                        $this->parseHeader($name, $value);
-                    }
-
-                    list($name, $value) = explode(':', $line, 2);
-                    $name = trim($name);
-                    $value = trim($value);
-
-                    if (strtoupper($name) === $name) {
-                        $name = ucfirst(strtolower($name));
-                    };
-                }
-            }
-        }
-        //parse the last header
-        if ($value && $name) {
-            $this->parseHeader($name, $value);
-        }
-
-        //try to infer report type from file extension
-        if (!isset($this->options['Type'])) {
-            $explodedReport = explode('.', $this->report);
-            $file_type = array_pop($explodedReport);
-
-            if (!isset(PhpReports::$config['default_file_extension_mapping'][$file_type])) {
-                throw new Exception("Unknown report type - ".$this->report);
-            } else {
-                $this->options['Type'] = PhpReports::$config['default_file_extension_mapping'][$file_type];
-            }
-        }
-
-        if (!isset($this->options['Database'])) {
-            $this->options['Database'] = strtolower($this->options['Type']);
-        }
-
-        if (!isset($this->options['Name'])) {
-            $this->options['Name'] = $this->report;
-        }
-    }
-
-    public function parseHeader($name, $value, $dataset = null)
-    {
-        $classname = $name.'Header';
-
-        if (class_exists($classname)) {
-            if ($dataset !== null && isset($classname::$validation) && isset($classname::$validation['dataset'])) {
-                $value['dataset'] = $dataset;
-            }
-
-            $classname::parse($name, $value, $this);
-
-            if (!in_array($name, $this->headers)) {
-                $this->headers[] = $name;
-            }
-        } else {
-            throw new Exception("Unknown header '$name' - ".$this->report);
-        }
-    }
-
-    public function addFilter($dataset, $column, $type, $options)
-    {
-        // If adding for multiple datasets
-        if (is_array($dataset)) {
-            foreach ($dataset as $d) {
-                $this->addFilter($d, $column, $type, $options);
-            }
-        } elseif ($dataset === true) {
-            // If adding for all datasets
-            $this->addFilter('all', $column, $type, $options);
-        } else {
-            // If adding for a single dataset
-            if (!isset($this->filters[$dataset])) {
-                $this->filters[$dataset] = [];
-            }
-
-            if (!isset($this->filters[$dataset][$column])) {
-                $this->filters[$dataset][$column] = [];
-            }
-
-            $this->filters[$dataset][$column][$type] = $options;
-        }
-    }
-
-    protected function applyFilters($dataset, $column, $value, $row)
-    {
-        // First, apply filters for all datasets
-        if (isset($this->filters['all']) && isset($this->filters['all'][$column])) {
-            foreach ($this->filters['all'][$column] as $type => $options) {
-                $classname = $type.'Filter';
-                $value = $classname::filter($value, $options, $this, $row);
-
-                //if the column should not be displayed
-                if ($value === false) {
-                    return false;
-                }
-            }
-        }
-
-        // Then apply filters for this specific dataset
-        if (isset($this->filters[$dataset]) && isset($this->filters[$dataset][$column])) {
-            foreach ($this->filters[$dataset][$column] as $type => $options) {
-                $classname = $type.'Filter';
-                $value = $classname::filter($value, $options, $this, $row);
-
-                //if the column should not be displayed
-                if ($value === false) {
-                    return false;
-                }
-            }
-        }
-
-        return $value;
-    }
-
-    protected function initDb()
-    {
-        //if the database isn't set, use the first defined one from config
-        $environments = PhpReports::$config['environments'];
-        if (!$this->options['Environment']) {
-            $this->options['Environment'] = current(array_keys($environments));
-        }
-
-        //set database options
-        $environment_options = [];
-        foreach ($environments as $key => $params) {
-            $environment_options[] = [
-                'name' => $key,
-                'selected' => ($key === $this->options['Environment']),
-            ];
-        }
-
-        $this->options['Environments'] = $environment_options;
-
-        //add a host macro
-        if (isset($environments[$this->options['Environment']]['host'])) {
-            $this->macros['host'] = $environments[$this->options['Environment']]['host'];
-        }
-
-        $classname = $this->options['Type'].'ReportType';
-
-        if (!class_exists($classname)) {
-            throw new Exception("Unknown report type '".$this->options['Type']."'");
-        }
-
-        $classname::init($this);
-    }
-
-    public function getRaw()
-    {
-        return $this->raw;
-    }
-
-    public function getUrl()
-    {
-        return 'report/html/?report='.urlencode($this->report);
-    }
-
-    public function prepareVariableForm()
-    {
-        $vars = [];
-
-        if ($this->options['Variables']) {
-            foreach ($this->options['Variables'] as $var => $params) {
-                if (!isset($params['name'])) {
-                    $params['name'] = ucwords(str_replace(['_', '-'], ' ', $var));
-                }
-                if (!isset($params['type'])) {
-                    $params['type'] = 'string';
-                }
-                if (!isset($params['options'])) {
-                    $params['options'] = false;
-                }
-                $params['value'] = $this->macros[$var];
-                $params['key'] = $var;
-
-                if ($params['type'] === 'select') {
-                    $params['is_select'] = true;
-
-                    foreach ($params['options'] as $key => $option) {
-                        if (!is_array($option)) {
-                            $params['options'][$key] = [
-                                'display' => $option,
-                                'value' => $option,
-                            ];
-                        }
-
-                        if ($params['options'][$key]['value'] == $params['value']) {
-                            $params['options'][$key]['selected'] = true;
-                        } elseif (is_array($params['value']) && in_array($params['options'][$key]['value'], $params['value'])) {
-                            $params['options'][$key]['selected'] = true;
-                        } else {
-                            $params['options'][$key]['selected'] = false;
-                        }
-
-                        if ($params['multiple']) {
-                            $params['is_multiselect'] = true;
-                            $params['choices'] = count($params['options']);
-                        }
-                    }
-                } else {
-                    if ($params['multiple']) {
-                        $params['is_textarea'] = true;
-                    }
-                }
-
-                if (isset($params['modifier_options'])) {
-                    $modifier_value = isset($this->macros[$var.'_modifier']) ? $this->macros[$var.'_modifier'] : null;
-
-                    foreach ($params['modifier_options'] as $key => $option) {
-                        if (!is_array($option)) {
-                            $params['modifier_options'][$key] = [
-                                'display' => $option,
-                                'value' => $option,
-                            ];
-                        }
-
-                        if ($params['modifier_options'][$key]['value'] == $modifier_value) {
-                            $params['modifier_options'][$key]['selected'] = true;
-                        } else {
-                            $params['modifier_options'][$key]['selected'] = false;
-                        }
-                    }
-                }
-
-                $vars[] = $params;
-            }
-        }
-
-        return $vars;
-    }
-
-    protected function _runReport()
-    {
-        if (!$this->is_ready) {
-            throw new Exception("Report is not ready.  Missing variables");
-        }
-
-        PhpReports::setVar('Report', $this);
-
-        //release the write lock on the session file
-        //so the session isn't locked while the report is running
-        session_write_close();
-
-        $classname = $this->options['Type'].'ReportType';
-
-        if (!class_exists($classname)) {
-            throw new Exception("Unknown report type '".$this->options['Type']."'");
-        }
-
-        foreach ($this->headers as $header) {
-            $headerclass = $header.'Header';
-            $headerclass::beforeRun($this);
-        }
-
-        $classname::openConnection($this);
-        $datasets = $classname::run($this);
-        $classname::closeConnection($this);
-
-        // Convert old single dataset format to multi-dataset format
-        if (!isset($datasets[0]['rows']) || !is_array($datasets[0]['rows'])) {
-            $datasets = [
-                [
-                    'rows' => $datasets,
-                ],
-            ];
-        }
-
-        // Only include a subset of datasets
-        $include = array_keys($datasets);
-        if (isset($_GET['dataset'])) {
-            $include = [$_GET['dataset']];
-        } elseif (isset($_GET['datasets'])) {
-            // If just a single dataset was specified, make it an array
-            if (!is_array($_GET['datasets'])) {
-                $include = explode(',', $_GET['datasets']);
-            } else {
-                $include = $_GET['datasets'];
-            }
-        }
-
-        $this->options['DataSets'] = [];
-        foreach ($include as $i) {
-            if (!isset($datasets[$i])) {
-                continue;
-            }
-            $this->options['DataSets'][$i] = $datasets[$i];
-        }
-
-        $this->parseDynamicHeaders();
-    }
-
-    protected function parseDynamicHeaders()
-    {
-        foreach ($this->options['DataSets'] as $i => &$dataset) {
-            if (isset($dataset['headers'])) {
-                foreach ($dataset['headers'] as $j => $header) {
-                    if (isset($header['header']) && isset($header['value'])) {
-                        $this->parseHeader($header['header'], $header['value'], $i);
-                    }
-                }
-            }
-        }
-    }
-
-    protected function getTimeEstimate()
-    {
-        $report_times = FileSystemCache::retrieve($this->getReportTimesCacheKey());
-        if (!$report_times) {
-            return;
-        }
-
-        sort($report_times);
-
-        $sum = array_sum($report_times);
-        $count = count($report_times);
-        $average = $sum/$count;
-        $quartile1 = $report_times[round(($count-1)/4)];
-        $median = $report_times[round(($count-1)/2)];
-        $quartile3 = $report_times[round(($count-1)*3/4)];
-        $min = min($report_times);
-        $max = max($report_times);
-        $iqr = $quartile3-$quartile1;
-        $range = (1.5)*$iqr;
-
-        $sample_square = 0;
-        for ($i = 0; $i < $count; $i++) {
-            $sample_square += pow($report_times[$i], 2);
-        }
-        $standard_deviation = sqrt($sample_square / $count - pow(($average), 2));
-
-        $this->options['time_estimate'] = [
-            'times' => $report_times,
-            'count' => $count,
-            'min' => round($min, 2),
-            'max' => round($max, 2),
-            'median' => round($median, 2),
-            'average' => round($average, 2),
-            'q1' => round($quartile1, 2),
-            'q3' => round($quartile3, 2),
-            'iqr' => round($range, 2),
-            'sum' => round($sum, 2),
-            'stdev' => round($standard_deviation, 2),
-        ];
-    }
-
-    protected function prepareDataSets()
-    {
-        foreach ($this->options['DataSets'] as $i => $dataset) {
-            $this->prepareRows($i);
-        }
-
-        if (isset($this->options['DataSets'][0])) {
-            $this->options['Rows'] = $this->options['DataSets'][0]['rows'];
-            $this->options['Count'] = $this->options['DataSets'][0]['count'];
-        }
-    }
-
-    protected function prepareRows($dataset)
-    {
-        $rows = [];
-
-        //generate list of all values for each numeric column
-        //this is used to calculate percentiles/averages/etc.
-        $vals = [];
-        foreach ($this->options['DataSets'][$dataset]['rows'] as $row) {
-            foreach ($row as $key => $value) {
-                if (!isset($vals[$key])) {
-                    $vals[$key] = [];
-                }
-
-                if (is_numeric($value)) {
-                    $vals[$key][] = $value;
-                }
-            }
-        }
-
-        $this->options['DataSets'][$dataset]['values'] = $vals;
-
-        foreach ($this->options['DataSets'][$dataset]['rows'] as $row) {
-            $rowval = [];
-
-            $i = 1;
-            foreach ($row as $key => $value) {
-                $val = new ReportValue($i, $key, $value);
-
-                //apply filters for the column key
-                $val = $this->applyFilters($dataset, $key, $val, $row);
-                //apply filters for the column position
-                if ($val) {
-                    $val = $this->applyFilters($dataset, $i, $val, $row);
-                }
-
-                if ($val) {
-                    $rowval[] = $val;
-                }
-
-                $i++;
-            }
-
-            $first = !$rows;
-
-            $rows[] = array(
-                'values' => $rowval,
-                'first' => $first,
-            );
-        }
-
-        $this->options['DataSets'][$dataset]['rows'] = $rows;
-        $this->options['DataSets'][$dataset]['count'] = count($rows);
-    }
-
-    public function run()
-    {
-        if ($this->has_run) {
-            return true;
-        }
-
-        //at this point, all the headers are parsed and we haven't run the report yet
-        foreach ($this->headers as $header) {
-            $classname = $header.'Header';
-            $classname::afterParse($this);
-        }
-
-        //record how long it takes to run the report
-        $start = microtime(true);
-
-        if ($this->is_ready && !$this->async) {
-            //if the report is cached
-            if ($options = $this->retrieveFromCache()) {
-                $this->options = $options;
-                $this->options['FromCache'] = true;
-            } else {
-                $this->_runReport();
-                $this->prepareDataSets();
-                $this->storeInCache();
-            }
-
-            //add this to the list of recently run reports
-            $recently_run_key = FileSystemCache::generateCacheKey('recently_run');
-            $recently_run = FileSystemCache::retrieve($recently_run_key);
-
-            if ($recently_run === false) {
-                $recently_run = [];
-            }
-
-            array_unshift($recently_run, $this->report);
-
-            if (count($recently_run) > 200) {
-                $recently_run = array_slice($recently_run, 0, 200);
-            }
-
-            FileSystemCache::store($recently_run_key, $recently_run);
-        }
-
-        //call the beforeRender callback for each header
-        foreach ($this->headers as $header) {
-            $classname = $header.'Header';
-            $classname::beforeRender($this);
-        }
-
-        $this->options['Time'] = round(microtime(true) - $start, 5);
-
-        if ($this->is_ready && !$this->async && !isset($this->options['FromCache'])) {
-            //get current report times for this report
-            $report_times = FileSystemCache::retrieve($this->getReportTimesCacheKey());
-            if (!$report_times) {
-                $report_times = [];
-            }
-            //only keep the last 10 times for each report
-            //this keeps the timing data up to date and relevant
-            if (count($report_times) > 10) {
-                array_shift($report_times);
-            }
-
-            //store report times
-            $report_times[] = $this->options['Time'];
-            FileSystemCache::store($this->getReportTimesCacheKey(), $report_times);
-        }
-
-        $this->has_run = true;
-    }
-
-    public function renderReportPage($template = 'html/report', $additional_vars = [])
-    {
-        $this->run();
-
-        $template_vars = [
-            'is_ready' => $this->is_ready,
-            'async' => $this->async,
-            'report_url' => PhpReports::$request->base.'/report/?'.$_SERVER['QUERY_STRING'],
-            'report_querystring' => $_SERVER['QUERY_STRING'],
-            'base' => PhpReports::$request->base,
-            'report' => $this->report,
-            'vars' => $this->prepareVariableForm(),
-            'macros' => $this->macros,
-        ];
-
-        $template_vars = array_merge($template_vars, $additional_vars);
-
-        $template_vars = array_merge($template_vars, $this->options);
-
-        return PhpReports::render($template, $template_vars);
-    }
-}
diff --git a/lib/PhpReports/ReportFormatBase.php b/lib/PhpReports/ReportFormatBase.php
deleted file mode 100644
index cea90884..00000000
--- a/lib/PhpReports/ReportFormatBase.php
+++ /dev/null
@@ -1,22 +0,0 @@
-i = $i;
-        $this->key = $key;
-        $this->original_value = $value;
-        $this->filtered_value = is_string($value) ? strip_tags($value) : $value;
-        $this->html_value = $value;
-        $this->chart_value = $value;
-
-        $this->is_html = false;
-        $this->class = '';
-
-        $this->type = $this->_getType();
-    }
-
-    public function addClass($class)
-    {
-        $this->class = trim($this->class.' '.$class);
-    }
-
-    public function setValue($value, $html = false)
-    {
-        if (is_string($value)) {
-            $value = trim($value);
-        }
-
-        if ($html) {
-            $this->is_html = true;
-            $this->html_value = $value;
-        } else {
-            $this->is_html = false;
-            $this->filtered_value = is_string($value) ? htmlentities($value) : $value;
-            $this->html_value = $value;
-        }
-
-        $this->type = $this->_getType();
-    }
-
-    protected function _getType($value = null)
-    {
-        if (is_null($value)) {
-            return null;
-        } elseif (trim($value) === '') {
-            return null;
-        } elseif (preg_match('/^([$%(\-+\s])*([0-9,]+(\.[0-9]+)?|\.[0-9]+)([$%(\-+\s])*$/', $value)) {
-            return 'number';
-        } elseif (strtotime($value)) {
-            return 'date';
-        } else {
-            return 'string';
-        }
-    }
-    protected function _getDisplayValue($value, $html = false, $date = false)
-    {
-        $type = $this->_getType($value);
-
-        if ($type === null) {
-            if ($html && $this->is_html) {
-                return ' ';
-            } else {
-                return null;
-            }
-        } elseif ($type === 'number') {
-            return $value;
-        } elseif ($type === 'date') {
-            if ($date) {
-                return date($date, strtotime($value));
-            } else {
-                return $value;
-            }
-        } elseif ($type === 'string') {
-            return utf8_encode($value);
-        }
-    }
-
-    public function getValue($html = false, $date = false)
-    {
-        if ($html) {
-            $return = $this->_getDisplayValue($this->html_value, true, $date);
-
-            if ($this->is_html) {
-                return $return;
-            } else {
-                return htmlentities($return);
-            }
-        } else {
-            return $this->_getDisplayValue($this->filtered_value, false, $date);
-        }
-    }
-
-    public function getKeyCollapsed()
-    {
-        return trim(preg_replace(['/\s+/', '/[^a-zA-Z0-9_]*/'], ['_', ''], $this->key), '_');
-    }
-}
diff --git a/index.php b/public/index.php
similarity index 94%
rename from index.php
rename to public/index.php
index 19eb7636..016075b5 100644
--- a/index.php
+++ b/public/index.php
@@ -10,10 +10,9 @@
 ini_set('max_execution_time', 300);
 
 //sets up autoloading of composer dependencies
-include 'vendor/autoload.php';
+include '../vendor/autoload.php';
 
-//sets up autoload (looks in classes/local/, classes/, and lib/ in that order)
-require 'lib/PhpReports/PhpReports.php';
+use PhpReports\PhpReports;
 
 header("Access-Control-Allow-Origin: *");
 
@@ -101,7 +100,7 @@
     PhpReports::emailReport();
 });
 
-Flight::set('flight.handle_errors', false);
+// Flight::set('flight.handle_errors', false);
 Flight::set('flight.log_errors', true);
 
 Flight::start();
diff --git a/templates/default/html/chart_page.twig b/templates/default/html/chart_page.twig
index 64b17f13..d123ed24 100644
--- a/templates/default/html/chart_page.twig
+++ b/templates/default/html/chart_page.twig
@@ -1,12 +1,12 @@
 {% extends "html/blank_page.twig" %}
 {% block javascripts %}
 	
-    
+    
 	
-    
-    
+    
+    
 {% endblock %}
 {% block stylesheets %}
 {{ parent() }}
diff --git a/templates/default/html/dashboard.twig b/templates/default/html/dashboard.twig
index 65b46130..a4f63f68 100644
--- a/templates/default/html/dashboard.twig
+++ b/templates/default/html/dashboard.twig
@@ -40,15 +40,15 @@
 
 {% block javascripts %}
 {{ parent() }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
 
+    
     
-    
-    
-    
-	
+    
+    
+    
+	
     
-        
+        
+        
     {% endif %}
     {% if not nodata %}
-        
+        
     {% endif %}
-    
-    
-    
-    
-    
-    
-    
+    
+    
+    
+    
+    
+    
+    
     
+
 
-    
-	
-    
-    
+  
+  
+  
+  
+  
 {% endblock %}
 {% block stylesheets %}
 {{ parent() }}
 
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/default/html/chart_report.twig b/templates/default/html/chart_report.twig
index 48c74dd4..3d1ea9cb 100644
--- a/templates/default/html/chart_report.twig
+++ b/templates/default/html/chart_report.twig
@@ -1,46 +1,46 @@
 {% extends "html/chart_page.twig" %}
 
 {% block content %}
-	{% for chart in Charts %}
-	
- {% endfor %} - - + google.setOnLoadCallback(drawCharts); + {% endblock %} diff --git a/templates/default/html/page.twig b/templates/default/html/page.twig index 70372a9a..a699f004 100644 --- a/templates/default/html/page.twig +++ b/templates/default/html/page.twig @@ -47,10 +47,10 @@ + + + + --> diff --git a/templates/default/html/table.twig b/templates/default/html/table.twig index aa2182fd..4850d5a0 100644 --- a/templates/default/html/table.twig +++ b/templates/default/html/table.twig @@ -54,7 +54,7 @@ {% for value in row.values %} {% if value.is_header is defined and value.is_header %}{% set tag = 'th' %}{% else %}{% set tag = 'td' %}{% endif %} <{{ tag }} class="{{ value.class }}"> - {{ value.getValue(true)|raw }} + {{ value.getValue(true)|raw }} {% endfor %} @@ -98,11 +98,11 @@
{% endif %} - {% for value in row.values %} - - {{ value.getValue(true)|raw }} - - {% endfor %} + {% for value in row.values %} + + {{ value.getValue(true)|raw }} + + {% endfor %} {% endfor %} From 0b7afee47eb123ece5bce51f7186d0b8fa71858c Mon Sep 17 00:00:00 2001 From: Fede Isas Date: Mon, 9 May 2016 22:59:29 -0300 Subject: [PATCH 11/19] Cleanup --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6224f900..cd2a2cd9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ dashboards/ cache/ classes/local/*.php templates/local -old/ vendor/ \ No newline at end of file From 6929857d2ad1d46b84e564e1dc69392c4d1f2983 Mon Sep 17 00:00:00 2001 From: Fede Isas Date: Tue, 10 May 2016 20:19:42 -0300 Subject: [PATCH 12/19] Commit --- .gitignore | 3 +- src/Reports/Filters/BarFilter.php | 34 + src/Reports/Filters/ClassFilter.php | 15 + src/Reports/Filters/DateFilter.php | 37 + src/Reports/Filters/DrilldownFilter.php | 96 +++ src/Reports/Filters/Filter.php | 15 + src/Reports/Filters/GeoipFilter.php | 32 + src/Reports/Filters/HideFilter.php | 13 + src/Reports/Filters/HtmlFilter.php | 15 + src/Reports/Filters/ImgsizeFilter.php | 31 + src/Reports/Filters/LinkFilter.php | 34 + src/Reports/Filters/NumberFilter.php | 21 + src/Reports/Filters/PaddingFilter.php | 19 + src/Reports/Filters/PreFilter.php | 15 + src/Reports/Filters/TwigFilter.php | 25 + src/Reports/Formats/ChartReportFormat.php | 19 + src/Reports/Formats/CsvReportFormat.php | 37 + src/Reports/Formats/DebugReportFormat.php | 29 + src/Reports/Formats/Format.php | 24 + src/Reports/Formats/FormatInterface.php | 10 + src/Reports/Formats/HtmlReportFormat.php | 49 ++ src/Reports/Formats/JsonReportFormat.php | 71 ++ src/Reports/Formats/RawReportFormat.php | 27 + src/Reports/Formats/ReportFormatBase.php | 20 + src/Reports/Formats/SqlReportFormat.php | 17 + src/Reports/Formats/TableReportFormat.php | 20 + src/Reports/Formats/TextReportFormat.php | 108 +++ src/Reports/Formats/XlsReportBase.php | 81 +++ src/Reports/Formats/XlsReportFormat.php | 37 + src/Reports/Formats/XlsxReportFormat.php | 37 + src/Reports/Formats/XmlReportFormat.php | 45 ++ src/Reports/Headers/ChartHeader.php | 442 ++++++++++++ src/Reports/Headers/ColumnsHeader.php | 95 +++ src/Reports/Headers/FilterHeader.php | 52 ++ src/Reports/Headers/FormattingHeader.php | 189 ++++++ src/Reports/Headers/HeaderBase.php | 126 ++++ src/Reports/Headers/IncludeHeader.php | 54 ++ src/Reports/Headers/InfoHeader.php | 59 ++ src/Reports/Headers/OptionsHeader.php | 150 +++++ src/Reports/Headers/RollupHeader.php | 151 +++++ src/Reports/Headers/VariableHeader.php | 230 +++++++ src/Reports/PhpReports.php | 784 ++++++++++++++++++++++ src/Reports/Report.php | 723 ++++++++++++++++++++ src/Reports/ReportValue.php | 118 ++++ src/Reports/Types/AdoPivotReportType.php | 191 ++++++ src/Reports/Types/AdoReportType.php | 173 +++++ src/Reports/Types/MongoReportType.php | 85 +++ src/Reports/Types/MysqlReportType.php | 7 + src/Reports/Types/PdoReportType.php | 231 +++++++ src/Reports/Types/PhpReportType.php | 101 +++ src/Reports/Types/Type.php | 26 + 51 files changed, 5022 insertions(+), 1 deletion(-) create mode 100644 src/Reports/Filters/BarFilter.php create mode 100644 src/Reports/Filters/ClassFilter.php create mode 100644 src/Reports/Filters/DateFilter.php create mode 100644 src/Reports/Filters/DrilldownFilter.php create mode 100644 src/Reports/Filters/Filter.php create mode 100644 src/Reports/Filters/GeoipFilter.php create mode 100644 src/Reports/Filters/HideFilter.php create mode 100644 src/Reports/Filters/HtmlFilter.php create mode 100644 src/Reports/Filters/ImgsizeFilter.php create mode 100644 src/Reports/Filters/LinkFilter.php create mode 100644 src/Reports/Filters/NumberFilter.php create mode 100644 src/Reports/Filters/PaddingFilter.php create mode 100644 src/Reports/Filters/PreFilter.php create mode 100644 src/Reports/Filters/TwigFilter.php create mode 100644 src/Reports/Formats/ChartReportFormat.php create mode 100644 src/Reports/Formats/CsvReportFormat.php create mode 100644 src/Reports/Formats/DebugReportFormat.php create mode 100644 src/Reports/Formats/Format.php create mode 100644 src/Reports/Formats/FormatInterface.php create mode 100644 src/Reports/Formats/HtmlReportFormat.php create mode 100644 src/Reports/Formats/JsonReportFormat.php create mode 100644 src/Reports/Formats/RawReportFormat.php create mode 100644 src/Reports/Formats/ReportFormatBase.php create mode 100644 src/Reports/Formats/SqlReportFormat.php create mode 100644 src/Reports/Formats/TableReportFormat.php create mode 100644 src/Reports/Formats/TextReportFormat.php create mode 100644 src/Reports/Formats/XlsReportBase.php create mode 100644 src/Reports/Formats/XlsReportFormat.php create mode 100644 src/Reports/Formats/XlsxReportFormat.php create mode 100644 src/Reports/Formats/XmlReportFormat.php create mode 100644 src/Reports/Headers/ChartHeader.php create mode 100644 src/Reports/Headers/ColumnsHeader.php create mode 100644 src/Reports/Headers/FilterHeader.php create mode 100644 src/Reports/Headers/FormattingHeader.php create mode 100644 src/Reports/Headers/HeaderBase.php create mode 100644 src/Reports/Headers/IncludeHeader.php create mode 100644 src/Reports/Headers/InfoHeader.php create mode 100644 src/Reports/Headers/OptionsHeader.php create mode 100644 src/Reports/Headers/RollupHeader.php create mode 100644 src/Reports/Headers/VariableHeader.php create mode 100644 src/Reports/PhpReports.php create mode 100644 src/Reports/Report.php create mode 100644 src/Reports/ReportValue.php create mode 100644 src/Reports/Types/AdoPivotReportType.php create mode 100644 src/Reports/Types/AdoReportType.php create mode 100644 src/Reports/Types/MongoReportType.php create mode 100644 src/Reports/Types/MysqlReportType.php create mode 100644 src/Reports/Types/PdoReportType.php create mode 100644 src/Reports/Types/PhpReportType.php create mode 100644 src/Reports/Types/Type.php diff --git a/.gitignore b/.gitignore index cd2a2cd9..f3750b81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ .idea/ config/config.php reports/ +!src/Reports/ dashboards/ cache/ classes/local/*.php templates/local -vendor/ \ No newline at end of file +vendor/ diff --git a/src/Reports/Filters/BarFilter.php b/src/Reports/Filters/BarFilter.php new file mode 100644 index 00000000..042ecca4 --- /dev/null +++ b/src/Reports/Filters/BarFilter.php @@ -0,0 +1,34 @@ +getValue() / max($report->options['Values'][$value->key]))); + + $value->setValue( + join('', [ + "
", + "", + $value->getValue(true), + "", + ]), + true + ); + + return $value; + } +} diff --git a/src/Reports/Filters/ClassFilter.php b/src/Reports/Filters/ClassFilter.php new file mode 100644 index 00000000..57f6cab3 --- /dev/null +++ b/src/Reports/Filters/ClassFilter.php @@ -0,0 +1,15 @@ +addClass($options['class']); + + return $value; + } +} diff --git a/src/Reports/Filters/DateFilter.php b/src/Reports/Filters/DateFilter.php new file mode 100644 index 00000000..6509fb64 --- /dev/null +++ b/src/Reports/Filters/DateFilter.php @@ -0,0 +1,37 @@ +options['Database']; + } + + $time = strtotime($value->getValue()); + + //if the time couldn't be parsed, just return the original value + if (!$time) { + return $value; + } + + //if a timezone correction is needed for the database being selected from + $environment = $report->getEnvironment(); + if (isset($environment[$options['database']]['time_offset'])) { + $time_offset = -1*$environment[$options['database']]['time_offset']; + + $time = strtotime((($time_offset > 0) ? '+' : '-').abs($time_offset).' hours', $time); + } + + $value->setValue(date($options['format'], $time)); + + return $value; + } +} diff --git a/src/Reports/Filters/DrilldownFilter.php b/src/Reports/Filters/DrilldownFilter.php new file mode 100644 index 00000000..f8e3d50b --- /dev/null +++ b/src/Reports/Filters/DrilldownFilter.php @@ -0,0 +1,96 @@ +report); + array_pop($temp); + $try[] = implode('/', $temp).'/'.$options['report']; + $try[] = $options['report']; + } + + //see if the file exists directly + $found = false; + $path = ''; + foreach ($try as $report_name) { + if (file_exists(PhpReports::$config['reportDir'].'/'.$report_name)) { + $path = $report_name; + $found = true; + break; + } + } + + //see if the report is missing a file extension + if (!$found) { + foreach ($try as $report_name) { + $possible_reports = glob(PhpReports::$config['reportDir'].'/'.$report_name.'.*'); + + if ($possible_reports) { + $path = substr($possible_reports[0], strlen(PhpReports::$config['reportDir'].'/')); + $found = true; + break; + } + } + } + + if (!$found) { + return $value; + } + + $url = PhpReports::$request->base.'/report/html/?report='.$path; + + $macros = []; + foreach ($options['macros'] as $k => $v) { + //if the macro needs to be replaced with the value of another column + if (isset($v['column'])) { + if (isset($row[$v['column']])) { + $v = $row[$v['column']]; + } else { + $v = ""; + } + } elseif (isset($v['constant'])) { + //if the macro is just a constant + $v = $v['constant']; + } + + $macros[$k] = $v; + } + + $macros = array_merge($report->macros, $macros); + unset($macros['host']); + + foreach ($macros as $k => $v) { + if (is_array($v)) { + foreach ($v as $v2) { + $url .= '¯os['.$k.'][]='.$v2; + } + } else { + $url .= '¯os['.$k.']='.$v; + } + } + + $options = [ + 'url' => $url, + ]; + + return parent::filter($value, $options, $report, $row); + } +} diff --git a/src/Reports/Filters/Filter.php b/src/Reports/Filters/Filter.php new file mode 100644 index 00000000..9a6b5704 --- /dev/null +++ b/src/Reports/Filters/Filter.php @@ -0,0 +1,15 @@ +getValue()); + + if ($record) { + $display = ''; + + $display = $record['city']; + if ($record['country_code'] !== 'US') { + $display .= ' '.$record['country_name']; + } else { + $display .= ', '.$record['region']; + } + + $value->setValue($display); + + $value->chart_value = ['Latitude' => $record['latitude'], 'Longitude' => $record['longitude'], 'Location' => $display]; + } else { + $value->chart_value = ['Latitude' => 0, 'Longitude' => 0, 'Location' => 'Unknown']; + } + + return $value; + } +} diff --git a/src/Reports/Filters/HideFilter.php b/src/Reports/Filters/HideFilter.php new file mode 100644 index 00000000..add27cda --- /dev/null +++ b/src/Reports/Filters/HideFilter.php @@ -0,0 +1,13 @@ +is_html = true; + + return $value; + } +} diff --git a/src/Reports/Filters/ImgsizeFilter.php b/src/Reports/Filters/ImgsizeFilter.php new file mode 100644 index 00000000..5b653199 --- /dev/null +++ b/src/Reports/Filters/ImgsizeFilter.php @@ -0,0 +1,31 @@ +getValue(), 'rb'); + $img = new Imagick(); + $img->readImageFile($handle); + $data = $img->identifyImage(); + + if (!isset($options['format'])) { + $options['format'] = self::$default_format; + } + + $value->setValue(PhpReports::renderString($options['format'], $data)); + + return $value; + } +} diff --git a/src/Reports/Filters/LinkFilter.php b/src/Reports/Filters/LinkFilter.php new file mode 100644 index 00000000..00f65198 --- /dev/null +++ b/src/Reports/Filters/LinkFilter.php @@ -0,0 +1,34 @@ +getValue()) { + return $value; + } + + $url = isset($options['url']) ? $options['url'] : $value->getValue(); + $attr = (isset($options['blank']) && $options['blank']) ? ' target="_blank"' : ''; + $display = isset($options['display']) ? $options['display'] : $value->getValue(); + + $value->setValue( + join('', [ + '', + $display, + '', + ]), + true + ); + + return $value; + } +} diff --git a/src/Reports/Filters/NumberFilter.php b/src/Reports/Filters/NumberFilter.php new file mode 100644 index 00000000..11e8ae22 --- /dev/null +++ b/src/Reports/Filters/NumberFilter.php @@ -0,0 +1,21 @@ +getValue())) { + $value->setValue(number_format($value->getValue(), $decimals, $dec_sepr, $thousand), true); + } + + return $value; + } +} diff --git a/src/Reports/Filters/PaddingFilter.php b/src/Reports/Filters/PaddingFilter.php new file mode 100644 index 00000000..9b74face --- /dev/null +++ b/src/Reports/Filters/PaddingFilter.php @@ -0,0 +1,19 @@ +addClass('right'); + } elseif ($options['direction'] === 'l') { + $value->addClass('left'); + } + + return $value; + } +} diff --git a/src/Reports/Filters/PreFilter.php b/src/Reports/Filters/PreFilter.php new file mode 100644 index 00000000..4ce99605 --- /dev/null +++ b/src/Reports/Filters/PreFilter.php @@ -0,0 +1,15 @@ +setValue('
'.$value->getValue(true).'
', true); + + return $value; + } +} diff --git a/src/Reports/Filters/TwigFilter.php b/src/Reports/Filters/TwigFilter.php new file mode 100644 index 00000000..a436b6e1 --- /dev/null +++ b/src/Reports/Filters/TwigFilter.php @@ -0,0 +1,25 @@ +getValue(); + + $result = PhpReports::renderString($template, [ + 'value' => $value->getValue(), + 'row' => $row, + ]); + + $value->setValue($result, $html); + + return $value; + } +} diff --git a/src/Reports/Formats/ChartReportFormat.php b/src/Reports/Formats/ChartReportFormat.php new file mode 100644 index 00000000..c58c2529 --- /dev/null +++ b/src/Reports/Formats/ChartReportFormat.php @@ -0,0 +1,19 @@ +options['has_charts']) { + return; + } + + //always use cache for chart reports + $report->use_cache = true; + + $result = $report->renderReportPage('html/chart_report'); + + echo $result; + } +} diff --git a/src/Reports/Formats/CsvReportFormat.php b/src/Reports/Formats/CsvReportFormat.php new file mode 100644 index 00000000..35224ec7 --- /dev/null +++ b/src/Reports/Formats/CsvReportFormat.php @@ -0,0 +1,37 @@ +use_cache = true; + + $file_name = preg_replace(array('/[\s]+/', '/[^0-9a-zA-Z\-_\.]/'), ['_', ''], $report->options['Name']); + + header("Content-type: application/csv"); + header("Content-Disposition: attachment; filename=".$file_name.".csv"); + header("Pragma: no-cache"); + header("Expires: 0"); + + $i = 0; + if (isset($_GET['dataset'])) { + $i = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $i = $report->options['default_dataset']; + } + $i = intval($i); + + $data = $report->renderReportPage('csv/report', [ + 'dataset' => $i, + ]); + + if (trim($data)) { + echo $data; + } + } +} diff --git a/src/Reports/Formats/DebugReportFormat.php b/src/Reports/Formats/DebugReportFormat.php new file mode 100644 index 00000000..23b36b9d --- /dev/null +++ b/src/Reports/Formats/DebugReportFormat.php @@ -0,0 +1,29 @@ +getRaw()."\n\n\n"; + $content .= "****************** Macros ******************\n\n".print_r($report->macros, true)."\n\n\n"; + $content .= "****************** All Report Options ******************\n\n".print_r($report->options, true)."\n\n\n"; + + if ($report->is_ready) { + $report->run(); + + $content .= "****************** Generated Query ******************\n\n".print_r($report->options['Query'], true)."\n\n\n"; + + $content .= "****************** Report Rows ******************\n\n".print_r($report->options['DataSets'], true)."\n\n\n"; + } + + echo $content; + } +} diff --git a/src/Reports/Formats/Format.php b/src/Reports/Formats/Format.php new file mode 100644 index 00000000..e44db740 --- /dev/null +++ b/src/Reports/Formats/Format.php @@ -0,0 +1,24 @@ +async = !isset($request->query['content_only']); + if (isset($request->query['no_async'])) { + $report->async = false; + } + + //if we're only getting the report content + if (isset($request->query['content_only'])) { + $template = 'html/content_only'; + } else { + $template = 'html/report'; + } + + try { + $additional_vars = array(); + if (isset($request->query['no_charts'])) { + $additional_vars['no_charts'] = true; + } + + $html = $report->renderReportPage($template, $additional_vars); + echo $html; + } catch (\Exception $e) { + if (isset($request->query['content_only'])) { + $template = 'html/blank_page'; + } + + $vars = array( + 'title' => $report->report, + 'header' => '

There was an error running your report

', + 'error' => $e->getMessage(), + 'content' => "

Report Query

".$report->options['Query_Formatted'], + ); + + echo PhpReports::render($template, $vars); + } + } +} diff --git a/src/Reports/Formats/JsonReportFormat.php b/src/Reports/Formats/JsonReportFormat.php new file mode 100644 index 00000000..f572359b --- /dev/null +++ b/src/Reports/Formats/JsonReportFormat.php @@ -0,0 +1,71 @@ +run(); + + if (!$report->options['DataSets']) { + return; + } + + $result = array(); + if (isset($_GET['datasets'])) { + $datasets = $_GET['datasets']; + // If all the datasets should be included + if ($datasets === 'all') { + $datasets = array_keys($report->options['DataSets']); + } + // If just a single dataset was specified, make it an array + elseif (!is_array($datasets)) { + $datasets = explode(',', $datasets); + } + + foreach ($datasets as $i) { + $result[] = self::getDataSet($i, $report); + } + } else { + $i = 0; + if (isset($_GET['dataset'])) { + $i = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $i = $report->options['default_dataset']; + } + $i = intval($i); + + $dataset = self::getDataSet($i, $report); + $result = $dataset['rows']; + } + + if (defined('JSON_PRETTY_PRINT')) { + echo json_encode($result, JSON_PRETTY_PRINT); + } else { + echo json_encode($result); + } + } + + public static function getDataSet($i, &$report) + { + $dataset = array(); + foreach ($report->options['DataSets'][$i] as $k => $v) { + $dataset[$k] = $v; + } + + $rows = array(); + foreach ($dataset['rows'] as $i => $row) { + $tmp = array(); + foreach ($row['values'] as $key => $value) { + $tmp[$value->key] = $value->getValue(); + } + $rows[] = $tmp; + } + $dataset['rows'] = $rows; + + return $dataset; + } +} diff --git a/src/Reports/Formats/RawReportFormat.php b/src/Reports/Formats/RawReportFormat.php new file mode 100644 index 00000000..6b286eb7 --- /dev/null +++ b/src/Reports/Formats/RawReportFormat.php @@ -0,0 +1,27 @@ +renderReportPage('sql/report'); + } +} diff --git a/src/Reports/Formats/TableReportFormat.php b/src/Reports/Formats/TableReportFormat.php new file mode 100644 index 00000000..257f584b --- /dev/null +++ b/src/Reports/Formats/TableReportFormat.php @@ -0,0 +1,20 @@ +options['inline_email'] = true; + $report->use_cache = true; + + try { + $html = $report->renderReportPage('html/table'); + echo $html; + } catch (\Exception $e) { + } + } +} diff --git a/src/Reports/Formats/TextReportFormat.php b/src/Reports/Formats/TextReportFormat.php new file mode 100644 index 00000000..220ac288 --- /dev/null +++ b/src/Reports/Formats/TextReportFormat.php @@ -0,0 +1,108 @@ +use_cache = true; + + //run the report + $report->run(); + + if (!$report->options['DataSets']) { + return; + } + + foreach ($report->options['DataSets'] as $i => $dataset) { + if (isset($dataset['title'])) { + echo $dataset['title']."\n"; + } + + TextReportFormat::displayDataSet($dataset); + + // If this isn't the last dataset, add some spacing + if ($i < count($report->options['DataSets'])-1) { + echo "\n\n"; + } + } + } + + protected static function displayDataSet($dataset) + { + /** + * This code taken from Stack Overflow answer by ehudokai + * http://stackoverflow.com/a/4597190 + */ + + //first get your sizes + $sizes = []; + $first_row = $dataset['rows'][0]; + foreach ($first_row['values'] as $key => $value) { + $key = $value->key; + $value = $value->getValue(); + + //initialize to the size of the column name + $sizes[$key] = strlen($key); + } + + foreach ($dataset['rows'] as $row) { + foreach ($row['values'] as $key => $value) { + $key = $value->key; + $value = $value->getValue(); + + $length = strlen($value); + if ($length > $sizes[$key]) { + $sizes[$key] = $length; + } // get largest result size + } + } + + //top of output + foreach ($sizes as $length) { + echo "+".str_pad("", $length + 2, "-"); + } + echo "+\n"; + + // column names + foreach ($first_row['values'] as $key => $value) { + $key = $value->key; + $value = $value->getValue(); + + echo "| "; + echo str_pad($key, $sizes[$key]+1); + } + echo "|\n"; + + //line under column names + foreach ($sizes as $length) { + echo "+".str_pad("", $length + 2, "-"); + } + echo "+\n"; + + //output data + foreach ($dataset['rows'] as $row) { + foreach ($row['values'] as $key => $value) { + $key = $value->key; + $value = $value->getValue(); + + echo "| "; + echo str_pad($value, $sizes[$key]+1); + } + echo "|\n"; + } + + //bottom of output + foreach ($sizes as $length) { + echo "+".str_pad("", $length + 2, "-"); + } + echo "+\n"; + } +} diff --git a/src/Reports/Formats/XlsReportBase.php b/src/Reports/Formats/XlsReportBase.php new file mode 100644 index 00000000..b8de3eaf --- /dev/null +++ b/src/Reports/Formats/XlsReportBase.php @@ -0,0 +1,81 @@ +getProperties()->setCreator("PHP-Reports") + ->setLastModifiedBy("PHP-Reports") + ->setTitle("") + ->setSubject("") + ->setDescription(""); + + foreach ($report->options['DataSets'] as $i => $dataset) { + $objPHPExcel->createSheet($i); + self::addSheet($objPHPExcel, $dataset, $i); + } + + // Set the active sheet to the first one + $objPHPExcel->setActiveSheetIndex(0); + + return $objPHPExcel; + } + + public static function addSheet($objPHPExcel, $dataset, $i) + { + $rows = []; + $row = []; + $cols = 0; + $first_row = $dataset['rows'][0]; + foreach ($first_row['values'] as $key => $value) { + array_push($row, $value->key); + $cols++; + } + array_push($rows, $row); + $row = []; + + foreach ($dataset['rows'] as $r) { + foreach ($r['values'] as $key => $value) { + array_push($row, $value->getValue()); + } + array_push($rows, $row); + $row = []; + } + + $objPHPExcel->setActiveSheetIndex($i)->fromArray($rows, null, 'A1'); + $objPHPExcel->getActiveSheet()->setAutoFilter('A1:'.self::columnLetter($cols).count($rows)); + for ($a = 1; $a <= $cols; $a++) { + $objPHPExcel->getActiveSheet()->getColumnDimension(self::columnLetter($a))->setAutoSize(true); + } + + if (isset($dataset['title'])) { + $objPHPExcel->getActiveSheet()->setTitle($dataset['title']); + } + + return $objPHPExcel; + } +} diff --git a/src/Reports/Formats/XlsReportFormat.php b/src/Reports/Formats/XlsReportFormat.php new file mode 100644 index 00000000..622352c3 --- /dev/null +++ b/src/Reports/Formats/XlsReportFormat.php @@ -0,0 +1,37 @@ +options['Name']); + + //always use cache for Excel reports + $report->use_cache = true; + + //run the report + $report->run(); + + if (!$report->options['DataSets']) { + return; + } + + $objPHPExcel = parent::getExcelRepresantation($report); + + $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5'); + + header('Content-Type: application/vnd.ms-excel'); + header('Content-Disposition: attachment;filename="'.$file_name.'.xls"'); + header('Pragma: no-cache'); + header('Expires: 0'); + + $objWriter->save('php://output'); + } +} diff --git a/src/Reports/Formats/XlsxReportFormat.php b/src/Reports/Formats/XlsxReportFormat.php new file mode 100644 index 00000000..793f4ff8 --- /dev/null +++ b/src/Reports/Formats/XlsxReportFormat.php @@ -0,0 +1,37 @@ +options['Name']); + + //always use cache for Excel reports + $report->use_cache = true; + + //run the report + $report->run(); + + if (!$report->options['DataSets']) { + return; + } + + $objPHPExcel = parent::getExcelRepresantation($report); + + $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); + + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="'.$file_name.'.xlsx"'); + header('Pragma: no-cache'); + header('Expires: 0'); + + $objWriter->save('php://output'); + } +} diff --git a/src/Reports/Formats/XmlReportFormat.php b/src/Reports/Formats/XmlReportFormat.php new file mode 100644 index 00000000..11ca8298 --- /dev/null +++ b/src/Reports/Formats/XmlReportFormat.php @@ -0,0 +1,45 @@ +options['DataSets']); + } elseif (!is_array($datasets)) { + // If just a single dataset was specified, make it an array + $datasets = explode(',', $datasets); + } + } else { + $i = 0; + if (isset($_GET['dataset'])) { + $i = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $i = $report->options['default_dataset']; + } + $i = intval($i); + + $datasets = [$i]; + } + + echo $report->renderReportPage('xml/report', [ + 'datasets' => $datasets, + 'dataset_format' => $dataset_format, + ]); + } +} diff --git a/src/Reports/Headers/ChartHeader.php b/src/Reports/Headers/ChartHeader.php new file mode 100644 index 00000000..7a0d18dc --- /dev/null +++ b/src/Reports/Headers/ChartHeader.php @@ -0,0 +1,442 @@ + [ + 'type' => 'array', + 'default' => [], + ], + 'dataset' => [ + 'default' => 0, + ], + 'type' => [ + 'type' => 'enum', + 'values' => [ + 'LineChart', + 'GeoChart', + 'AnnotatedTimeLine', + 'BarChart', + 'ColumnChart', + 'Timeline', + 'AreaChart', + 'Histogram', + 'ComboChart', + 'BubbleChart', + 'CandlestickChart', + 'Gauge', + 'Map', + 'PieChart', + 'Sankey', + 'ScatterChart', + 'SteppedAreaChart', + 'WordTree', + ], + 'default' => 'LineChart', + ], + 'title' => [ + 'type' => 'string', + 'default' => '', + ], + 'width' => [ + 'type' => 'string', + 'default' => '100%', + ], + 'height' => [ + 'type' => 'string', + 'default' => '400px', + ], + 'xhistogram' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'buckets' => [ + 'type' => 'number', + 'default' => 0, + ], + 'omit-totals' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'omit-total' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'rotate-x-labels' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'grid' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'timefmt' => [ + 'type' => 'string', + 'default' => '', + ], + 'xformat' => [ + 'type' => 'string', + 'default' => '', + ], + 'yrange' => [ + 'type' => 'string', + 'default' => '', + ], + 'all' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'colors' => [ + 'type' => 'array', + 'default' => [], + ], + 'roles' => [ + 'type' => 'object', + 'default' => [], + ], + 'markers' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'omit-columns' => [ + 'type' => 'array', + 'default' => [], + ], + 'options' => [ + 'type' => 'object', + 'default' => [], + ], + ]; + + public static function init($params, &$report) + { + $report->exportHeader('Chart', $params); + + if (!isset($params['type'])) { + $params['type'] = 'LineChart'; + } + + if (isset($params['omit-total'])) { + $params['omit-totals'] = $params['omit-total']; + unset($params['omit-total']); + } + + if (!isset($report->options['Charts'])) { + $report->options['Charts'] = []; + } + + if (isset($params['width'])) { + $params['width'] = self::fixDimension($params['width']); + } + if (isset($params['height'])) { + $params['height'] = self::fixDimension($params['height']); + } + + $params['num'] = count($report->options['Charts'])+1; + $params['Rows'] = []; + + $report->options['Charts'][] = $params; + + $report->options['has_charts'] = true; + } + protected static function fixDimension($dim) + { + if (preg_match('/^[0-9]+$/', $dim)) { + $dim .= "px"; + } + + return $dim; + } + + public static function parseShortcut($value) + { + $params = explode(',', $value); + $value = []; + foreach ($params as $param) { + $param = trim($param); + if (strpos($param, '=') !== false) { + list($key, $val) = explode('=', $param, 2); + $key = trim($key); + $val = trim($val); + + //some parameters can have multiple values separated by ":" + if (in_array($key, ['x', 'y', 'colors'], true)) { + $val = explode(':', $val); + } + } else { + $key = $param; + $val = true; + } + + $value[$key] = $val; + } + + if (isset($value['x'])) { + $value['columns'] = $value['x']; + } else { + $value['columns'] = [1]; + } + + if (isset($value['y'])) { + $value['columns'] = array_merge($value['columns'], $value['y']); + } else { + $value['all'] = true; + } + + unset($value['x']); + unset($value['y']); + + return $value; + } + + protected static function getRowInfo(&$rows, $params, $num, &$report) + { + $cols = []; + + //expand columns + $chart_rows = []; + foreach ($rows as $k => $row) { + $vals = []; + + if ($k === 0) { + $i = 1; + $unsorted = 1000; + foreach ($row['values'] as $key => $value) { + if (($temp = array_search($row['values'][$key]->i, $report->options['Charts'][$num]['columns'])) !== false) { + $cols[$temp] = $key; + } elseif (($temp = array_search($row['values'][$key]->key, $report->options['Charts'][$num]['columns'])) !== false) { + $cols[$temp] = $key; + } elseif ($report->options['Charts'][$num]['all']) { + //if all columns are included, add after any specifically defined ones + $cols[$unsorted] = $key; + $unsorted ++; + } + } + + ksort($cols); + } + + foreach ($cols as $key) { + if (isset($row['values'][$key]->chart_value) && is_array($row['values'][$key]->chart_value)) { + foreach ($row['values'][$key]->chart_value as $ckey => $cval) { + $temp = new ReportValue($row['values'][$key]->i, $ckey, trim($cval, '%$ ')); + $temp->setValue($cval); + $vals[] = $temp; + } + } else { + $temp = new ReportValue($row['values'][$key]->i, $row['values'][$key]->key, $row['values'][$key]->original_value); + $temp->setValue(trim($row['values'][$key]->getValue(), '%$ ')); + $vals[] = $temp; + } + } + + $chart_rows[] = $vals; + } + + //determine column types + $types = []; + foreach ($chart_rows as $i => $row) { + foreach ($row as $k => $v) { + $type = self::determineDataType($v->original_value); + //if the value is null, it doesn't influence the column type + if (!$type) { + $chart_rows[$i][$k]->setValue(null); + continue; + } elseif (!isset($types[$k])) { + //if we don't know the column type yet, set it to this row's value + $types[$k] = $type; + } elseif ($type === 'string') { + //if any row has a string value for the column, the whole column is a string type + $types[$k] = 'string'; + } elseif ($types[$k] === 'date' && in_array($type, ['timeofday', 'datetime'])) { + //if the column is currently a date and this row is a time/datetime, set the column to datetime type + $types[$k] = 'datetime'; + } elseif ($types[$k] === 'timeofday' && in_array($type, ['date', 'datetime'])) { + //if the column is currently a time and this row is a date/datetime, set the column to datetime type + $types[$k] = 'datetime'; + } elseif ($types[$k] === 'date' && $type === 'number') { + //if the column is currently a date and this row is a number set the column type to number + $types[$k] = 'number'; + } + } + } + + $report->options['Charts'][$num]['datatypes'] = $types; + + //build chart rows + $report->options['Charts'][$num]['Rows'] = []; + + foreach ($chart_rows as $i => &$row) { + $vals = []; + foreach ($row as $key => $val) { + if (is_null($val->getValue())) { + $val->datatype = 'null'; + } elseif ($types[$key] === 'datetime') { + $val->setValue(date('m/d/Y H:i:s', strtotime($val->getValue()))); + $val->datatype = 'datetime'; + } elseif ($types[$key] === 'timeofday') { + $val->setValue(date('H:i:s', strtotime($val->getValue()))); + $val->datatype = 'timeofday'; + } elseif ($types[$key] === 'date') { + $val->setValue(date('m/d/Y', strtotime($val->getValue()))); + $val->datatype = 'date'; + } elseif ($types[$key] === 'number') { + $val->setValue(round(floatval(preg_replace('/[^-0-9\.]*/', '', $val->getValue())), 6)); + $val->datatype = 'number'; + } else { + $val->datatype = 'string'; + } + + $vals[] = $val; + } + + $report->options['Charts'][$num]['Rows'][] = [ + 'values' => $vals, + 'first' => !$report->options['Charts'][$num]['Rows'], + ]; + } + } + + protected static function generateHistogramRows($rows, $column, $num_buckets) + { + $column_key = null; + + //if a name is given as the column, determine the column index + if (!is_numeric($column)) { + foreach ($rows[0]['values'] as $k => $v) { + if ($v->key == $column) { + $column = $k; + $column_key = $v->key; + break; + } + } + } else { + //if an index is given, convert to 0-based + $column --; + $column_key = $rows[0]['values'][$column]->key; + } + + //get a list of values for the histogram + $vals = []; + foreach ($rows as &$row) { + $vals[] = floatval(preg_replace('/[^0-9.]*/', '', $row['values'][$column]->getValue())); + } + sort($vals); + + //determine buckets + $count = count($vals); + $buckets = []; + $min = $vals[0]; + $max = $vals[$count-1]; + $step = ($max-$min)/$num_buckets; + $old_limit = $min; + + for ($i = 1; $i < $num_buckets + 1; $i++) { + $limit = $old_limit + $step; + + $buckets[round($old_limit, 2)." - ".round($limit, 2)] = count( + array_filter( + $vals, + function ($val) use ($old_limit, $limit) { + return $val >= $old_limit && $val < $limit; + } + ) + ); + $old_limit = $limit; + } + + //build chart rows + $chart_rows = []; + foreach ($buckets as $name => $count) { + $chart_rows[] = [ + 'values' => [ + new ReportValue(1, $name, $name), + new ReportValue(2, 'value', $count), + ], + 'first' => !$chart_rows, + ]; + } + + return $chart_rows; + } + + protected static function determineDataType($value) + { + if (is_null($value)) { + return null; + } elseif ($value === '') { + return null; + } elseif (preg_match('/^([$%(\-+\s])*([0-9,]+(\.[0-9]+)?|\.[0-9]+)([$%(\-+\s])*$/', $value)) { + return 'number'; + } elseif (preg_match('/^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$/', $value)) { + return 'timeofday'; + } elseif (preg_match('/^[0-9]+(\/|-)[0-9]+/', $value) && strtotime($value)) { + if (date('H:i:s', strtotime($value)) === '00:00:00') { + return 'date'; + } else { + return 'datetime'; + } + } else { + return 'string'; + } + } + + public static function beforeRender(&$report) + { + // Expand out multiple datasets into their own charts + $new_charts = []; + foreach ($report->options['Charts'] as $num => $params) { + $copy = $params; + + // If chart is for multiple datasets + if (is_array($params['dataset'])) { + foreach ($params['dataset'] as $dataset) { + $copy['dataset'] = $dataset; + $copy['num'] = count($new_charts)+1; + $new_charts[] = $copy; + } + } elseif ($params['dataset'] === true) { + // If chart is for all datasets + foreach ($report->options['DataSets'] as $j => $dataset) { + $copy['dataset'] = $j; + $copy['num'] = count($new_charts)+1; + $new_charts[] = $copy; + } + } else { + // If chart is for one dataset + $copy['num'] = count($new_charts)+1; + $new_charts[] = $copy; + } + } + + $report->options['Charts'] = $new_charts; + + foreach ($report->options['Charts'] as $num => &$params) { + self::_processChart($num, $params, $params['dataset'], $report); + } + } + + protected static function _processChart($num, &$params, $dataset, &$report) + { + if (isset($params['xhistogram']) && $params['xhistogram']) { + $rows = self::generateHistogramRows($report->options['DataSets'][$dataset]['rows'], $params['columns'][0], $params['buckets']); + $params['columns'] = [1, 2]; + } else { + $rows = []; + if (isset($report->options['DataSets'])) { + $rows = $report->options['DataSets'][$dataset]['rows']; + } + + if (count($rows)) { + if (!$params['columns']) { + $params['columns'] = range(1, count($rows[0]['values'])); + } + } + } + + self::getRowInfo($rows, $params, $num, $report); + } +} diff --git a/src/Reports/Headers/ColumnsHeader.php b/src/Reports/Headers/ColumnsHeader.php new file mode 100644 index 00000000..11cbdb99 --- /dev/null +++ b/src/Reports/Headers/ColumnsHeader.php @@ -0,0 +1,95 @@ + $options) { + if (!isset($options['type'])) { + throw new \Exception("Must specify column type for column $column"); + } + $type = $options['type']; + unset($options['type']); + $report->addFilter($params['dataset'], $column, $type, $options); + } + } + + public static function parseShortcut($value) + { + if (preg_match('/^[0-9]+\:/', $value)) { + $dataset = substr($value, 0, strpos($value, ':')); + $value = substr($value, strlen($dataset)+1); + } else { + $dataset = 0; + } + + $parts = explode(',', $value); + $params = []; + $i = 1; + foreach ($parts as $part) { + $type = null; + $options = null; + + $part = trim($part); + //special cases + //'rpadN' or 'lpadN' where N is number of spaces to pad + if (substr($part, 1, 3) === 'pad') { + $type = 'padding'; + + $options = [ + 'direction' => $part[0], + 'spaces' => intval(substr($part, 4)), + ]; + } elseif (substr($part, 0, 4) === 'link') { + //link or link(display) or link_blank or link_blank(display) + //link(display) or link_blank(display) + if (strpos($part, '(') !== false) { + list($type, $display) = explode('(', substr($part, 0, -1), 2); + } else { + $type = $part; + $display = 'link'; + } + + $blank = ($type == 'link_blank'); + $type = 'link'; + + $options = [ + 'display' => $display, + 'blank' => $blank, + ]; + } elseif (in_array($part, ['html', 'raw'])) { + //synonyms for 'html' + $type = 'html'; + } elseif ($part === 'url') { + //url synonym for link + $type = 'link'; + $options = [ + 'blank' => false, + ]; + } elseif ($part === 'bar') { + $type = 'bar'; + $options = []; + } elseif ($part === 'pre') { + $type = 'pre'; + } else { + //normal case + $type = 'class'; + $options = [ + 'class' => $part, + ]; + } + + $options['type'] = $type; + + $params[$i] = $options; + + $i++; + } + + return [ + 'dataset' => $dataset, + 'columns' => $params, + ]; + } +} diff --git a/src/Reports/Headers/FilterHeader.php b/src/Reports/Headers/FilterHeader.php new file mode 100644 index 00000000..a8259b09 --- /dev/null +++ b/src/Reports/Headers/FilterHeader.php @@ -0,0 +1,52 @@ + [ + 'required' => true, + 'type' => 'string', + ], + 'filter' => [ + 'required' => true, + 'type' => 'string', + ], + 'params' => [ + 'type' => 'object', + 'default' => [], + ], + 'dataset' => [ + 'default' => 0, + ], + ]; + + public static function init($params, &$report) + { + $report->addFilter($params['dataset'], $params['column'], $params['filter'], $params['params']); + } + + //in format: column, params + //params can be a JSON object or "filter" + //filter classes are defined in class/filters/ + //examples: + // "4,geoip" - apply a geoip filter to the 4th column + // 'Ip,{"filter":"geoip"}' - apply a geoip filter to the "Ip" column + public static function parseShortcut($value) + { + if (strpos($value, ',') === false) { + $col = "1"; + $filter = $value; + } else { + list($col, $filter) = explode(',', $value, 2); + $col = trim($col); + } + $filter = trim($filter); + + return [ + 'column' => $col, + 'filter' => $filter, + 'params' => [], + ]; + } +} diff --git a/src/Reports/Headers/FormattingHeader.php b/src/Reports/Headers/FormattingHeader.php new file mode 100644 index 00000000..abde3d99 --- /dev/null +++ b/src/Reports/Headers/FormattingHeader.php @@ -0,0 +1,189 @@ + [ + 'type' => 'number', + 'default' => null, + ], + 'noborder' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'vertical' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'table' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'showcount' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'font' => [ + 'type' => 'string', + ], + 'nodata' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'selectable' => [ + 'type' => 'string', + ], + 'dataset' => [ + 'required' => true, + 'default' => true, + ], + ]; + + public static function init($params, &$report) + { + if (!isset($report->options['Formatting'])) { + $report->options['Formatting'] = []; + } + $report->options['Formatting'][] = $params; + } + + public static function parseShortcut($value) + { + $options = explode(',', $value); + + $params = []; + + foreach ($options as $v) { + if (strpos($v, '=') !== false) { + list($k, $v) = explode('=', $v, 2); + $v = trim($v); + } else { + $k = $v; + $v = true; + } + + $k = trim($k); + + $params[$k] = $v; + } + + return $params; + } + + public static function beforeRender(&$report) + { + $formatting = []; + // Expand out by dataset + foreach ($report->options['Formatting'] as $params) { + $copy = $params; + unset($copy['dataset']); + + if (isset($report->options['DataSets'])) { + // Multiple datasets defined + if (is_array($params['dataset'])) { + foreach ($params['dataset'] as $i) { + if (isset($report->options['DataSets'][$i])) { + if (!isset($formatting[$i])) { + $formatting[$i] = []; + } + foreach ($copy as $k => $v) { + $formatting[$i][$k] = $v; + } + } + } + } elseif ($params['dataset'] === true) { + // All datasets + foreach ($report->options['DataSets'] as $i => $dataset) { + if (!isset($formatting[$i])) { + $formatting[$i] = []; + } + foreach ($copy as $k => $v) { + $formatting[$i][$k] = $v; + } + } + } else { + // Single dataset + if (!isset($report->options['DataSets'][$params['dataset']])) { + continue; + } + if (!isset($formatting[$params['dataset']])) { + $formatting[$params['dataset']] = []; + } + foreach ($copy as $k => $v) { + $formatting[$params['dataset']][$k] = $v; + } + } + } + } + + $report->options['Formatting'] = $formatting; + + // Apply formatting options for each dataset + foreach ($formatting as $i => $params) { + if (isset($params['limit']) && $params['limit']) { + $report->options['DataSets'][$i]['rows'] = array_slice($report->options['DataSets'][$i]['rows'], 0, intval($params['limit'])); + } + if (isset($params['selectable']) && $params['selectable']) { + $selected = []; + + // New style "selected_{{DATASET}}" querystring + if (isset($_GET['selected_'.$i])) { + $selected = $_GET['selected_'.$i]; + } elseif (isset($_GET['selected'])) { + // Old style "selected" querystring + $selected = $_GET['selected']; + } + + if ($selected) { + $selected_key = null; + foreach ($report->options['DataSets'][$i]['rows'][0]['values'] as $key => $value) { + if ($value->key == $params['selectable']) { + $selected_key = $key; + break; + } + } + + if ($selected_key !== null) { + foreach ($report->options['DataSets'][$i]['rows'] as $key => $row) { + if (!in_array($row['values'][$selected_key]->getValue(), $selected)) { + unset($report->options['DataSets'][$i]['rows'][$key]); + } + } + $report->options['DataSets'][$i]['rows'] = array_values($report->options['DataSets'][$i]['rows']); + } + } + } + if (isset($params['vertical']) && $params['vertical']) { + $rows = []; + foreach ($report->options['DataSets'][$i]['rows'] as $row) { + foreach ($row['values'] as $value) { + if (!isset($rows[$value->key])) { + $header = new ReportValue(1, 'key', $value->key); + $header->class = 'left lpad'; + $header->is_header = true; + + $rows[$value->key] = [ + 'values' => [ + $header, + ], + 'first' => !$rows, + ]; + } + + $rows[$value->key]['values'][] = $value; + } + } + + $rows = array_values($rows); + + $report->options['DataSets'][$i]['vertical'] = $rows; + } + + unset($params['vertical']); + foreach ($params as $k => $v) { + $report->options['DataSets'][$i][$k] = $v; + } + } + } +} diff --git a/src/Reports/Headers/HeaderBase.php b/src/Reports/Headers/HeaderBase.php new file mode 100644 index 00000000..1f17dc1d --- /dev/null +++ b/src/Reports/Headers/HeaderBase.php @@ -0,0 +1,126 @@ +getMessage()); + } + + static::init($params, $report); + } + + public static function init($params, &$report) + { + } + + public static function parseShortcut($value) + { + return []; + } + + public static function beforeRender(&$report) + { + } + + public static function afterParse(&$report) + { + } + + public static function beforeRun(&$report) + { + } + + protected static function validate($params) + { + if (!static::$validation) { + return $params; + } + + $errors = []; + + foreach (static::$validation as $key => $rules) { + //fill in default params + if (isset($rules['default']) && !isset($params[$key])) { + $params[$key] = $rules['default']; + continue; + } + + //if the param isn't required and it's defined, we can skip validation + if ((!isset($rules['required']) || !$rules['required']) && !isset($params[$key])) { + continue; + } + + //if the param must be a specific datatype + if (isset($rules['type'])) { + if ($rules['type'] === 'number' && !is_numeric($params[$key])) { + $errors[] = "$key must be a number (".gettype($params[$key])." given)"; + } elseif ($rules['type'] === 'array' && !is_array($params[$key])) { + $errors[] = "$key must be an array (".gettype($params[$key])." given)"; + } elseif ($rules['type'] === 'boolean' && !is_bool($params[$key])) { + $errors[] = "$key must be true or false (".gettype($params[$key])." given)"; + } elseif ($rules['type'] === 'string' && !is_string($params[$key])) { + $errors[] = "$key must be a string (".gettype($params[$key])." given)"; + } elseif ($rules['type'] === 'enum' && !in_array($params[$key], $rules['values'])) { + $errors[] = "$key must be one of: [".implode(', ', $rules['values'])."]"; + } elseif ($rules['type'] === 'object' && !is_array($params[$key])) { + $errors[] = "$key must be an object (".gettype($params[$key])." given)"; + } + } + + //other validation rules + if (isset($rules['min']) && $params[$key] < $rules['min']) { + $errors[] = "$key must be at least $rules[min]"; + } + if (isset($rules['max']) && $params[$key] > $rules['max']) { + $errors[] = "$key must be at most $rules[min]"; + } + + if (isset($rules['pattern']) && !preg_match($rules['pattern'], $params[$key])) { + $errors[] = "$key does not match required pattern"; + } + } + + //every possible param must be defined in the validation rules + foreach ($params as $k => $v) { + if (!isset(static::$validation[$k])) { + $errors[] = "Unknown parameter '$k'"; + } + } + + if ($errors) { + throw new \Exception(implode(". ", $errors)); + } else { + return $params; + } + } +} diff --git a/src/Reports/Headers/IncludeHeader.php b/src/Reports/Headers/IncludeHeader.php new file mode 100644 index 00000000..45ccc69b --- /dev/null +++ b/src/Reports/Headers/IncludeHeader.php @@ -0,0 +1,54 @@ + [ + 'required' => true, + 'type' => 'string', + ], + ]; + + public static function init($params, &$report) + { + if ($params['report'][0] === '/') { + $report_path = substr($params['report'], 1); + } else { + $report_path = dirname($report->report).'/'.$params['report']; + } + + if (!file_exists(PhpReports::$config['reportDir'].'/'.$report_path)) { + $possible_reports = glob(PhpReports::$config['reportDir'].'/'.$report_path.'.*'); + + if ($possible_reports) { + $report_path = substr($possible_reports[0], strlen(PhpReports::$config['reportDir'].'/')); + } else { + throw new \Exception("Unknown report in INCLUDE header '$report_path'"); + } + } + + $included_report = new Report($report_path); + + //parse any exported headers from the included report + foreach ($included_report->exported_headers as $header) { + $report->parseHeader($header['name'], $header['params']); + } + + if (!isset($report->options['Includes'])) { + $report->options['Includes'] = []; + } + + $report->options['Includes'][] = $included_report; + } + + public static function parseShortcut($value) + { + return [ + 'report' => $value, + ]; + } +} diff --git a/src/Reports/Headers/InfoHeader.php b/src/Reports/Headers/InfoHeader.php new file mode 100644 index 00000000..26b25825 --- /dev/null +++ b/src/Reports/Headers/InfoHeader.php @@ -0,0 +1,59 @@ + [ + 'type' => 'string', + ], + 'description' => [ + 'type' => 'string', + ], + 'created' => [ + 'type' => 'string', + 'pattern' => '/^[0-9]{4}-[0-9]{2}-[0-9]{2}/', + ], + 'note' => [ + 'type' => 'string', + ], + 'type' => [ + 'type' => 'string', + ], + 'status' => [ + 'type' => 'string', + ], + ]; + + public static function init($params, &$report) + { + foreach ($params as $key => $value) { + $report->options[ucfirst($key)] = $value; + } + } + + // Accepts shortcut format: + // name=My Report,description=This is My Report + public static function parseShortcut($value) + { + $parts = explode(',', $value); + + $params = []; + + foreach ($parts as $v) { + if (strpos($v, '=') !== false) { + list($k, $v) = explode('=', $v, 2); + $v = trim($v); + } else { + $k = $v; + $v = true; + } + + $k = trim($k); + + $params[$k] = $v; + } + + return $params; + } +} diff --git a/src/Reports/Headers/OptionsHeader.php b/src/Reports/Headers/OptionsHeader.php new file mode 100644 index 00000000..007a711e --- /dev/null +++ b/src/Reports/Headers/OptionsHeader.php @@ -0,0 +1,150 @@ + [ + 'type' => 'number', + 'default' => null, + ], + 'access' => [ + 'type' => 'enum', + 'values' => ['rw', 'readonly'], + 'default' => 'readonly', + ], + 'noborder' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'noreport' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'vertical' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'ignore' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'table' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'showcount' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'font' => [ + 'type' => 'string', + ], + 'stop' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'nodata' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'version' => [ + 'type' => 'number', + 'default' => 1, + ], + 'selectable' => [ + 'type' => 'string', + ], + 'mongodatabase' => [ + 'type' => 'string', + ], + 'database' => [ + 'type' => 'string', + ], + 'cache' => [ + 'min' => 0, + 'type' => 'number', + ], + 'ttl' => [ + 'min' => 0, + 'type' => 'number', + ], + 'default_dataset' => [ + 'type' => 'number', + 'default' => 0, + ], + 'has_charts' => [ + 'type' => 'boolean', + ], + ]; + + public static function init($params, &$report) + { + //legacy support for the 'ttl' cache parameter + if (isset($params['ttl'])) { + $params['cache'] = $params['ttl']; + unset($params['ttl']); + } + + if (isset($params['has_charts']) && $params['has_charts']) { + if (!isset($report->options['Charts'])) { + $report->options['Charts'] = []; + } + } + + // Some parameters were moved to a 'FORMATTING' header + // We need to catch those and add the header to the report + $formatting_header = []; + + foreach ($params as $key => $value) { + // This is a FORMATTING parameter + if (in_array($key, ['limit', 'noborder', 'vertical', 'table', 'showcount', 'font', 'nodata', 'selectable'])) { + $formatting_header[$key] = $value; + continue; + } + + //some of the keys need to be uppercase (for legacy reasons) + if (in_array($key, ['database', 'mongodatabase', 'cache'])) { + $key = ucfirst($key); + } + + $report->options[$key] = $value; + + //if the value is different from the default, it can be exported + if (!isset(self::$validation[$key]['default']) || ($value && $value !== self::$validation[$key]['default'])) { + //only export some of the options + if (in_array($key, array('access', 'Cache'), true)) { + $report->exportHeader('Options', array($key => $value)); + } + } + } + + if ($formatting_header) { + $formatting_header['dataset'] = true; + $report->parseHeader('Formatting', $formatting_header); + } + } + + public static function parseShortcut($value) + { + $options = explode(',', $value); + + $params = []; + + foreach ($options as $v) { + if (strpos($v, '=') !== false) { + list($k, $v) = explode('=', $v, 2); + $v = trim($v); + } else { + $k = $v; + $v = true; + } + + $k = trim($k); + + $params[$k] = $v; + } + + return $params; + } +} diff --git a/src/Reports/Headers/RollupHeader.php b/src/Reports/Headers/RollupHeader.php new file mode 100644 index 00000000..c315195f --- /dev/null +++ b/src/Reports/Headers/RollupHeader.php @@ -0,0 +1,151 @@ + [ + 'required' => true, + 'type' => 'object', + 'default' => [], + ], + 'dataset' => [ + 'required' => false, + 'default' => 0, + ], + ]; + + public static function init($params, &$report) + { + //make sure at least 1 column is defined + if (empty($params['columns'])) { + throw new \Exception("Rollup header needs at least 1 column defined"); + } + + if (!isset($report->options['Rollup'])) { + $report->options['Rollup'] = []; + } + + // If more than one dataset is defined, add the rollup header multiple times + if (is_array($params['dataset'])) { + $new_params = $params; + foreach ($params['dataset'] as $dataset) { + $new_params['dataset'] = $dataset; + $report->options['Rollup'][] = $new_params; + } + } else { + // Otherwise, just add one rollup header + $report->options['Rollup'][] = $params; + } + } + + public static function beforeRender(&$report) + { + //cache for Twig parameters for each dataset/column + $twig_params = []; + + // Now that we know how many datasets we have, expand out Rollup headers with dataset->true + $new_rollups = []; + foreach ($report->options['Rollup'] as $i => $rollup) { + if ($rollup['dataset'] === true && isset($report->options['DataSets'])) { + $copy = $rollup; + foreach ($report->options['DataSets'] as $i => $dataset) { + $copy['dataset'] = $i; + $new_rollups[] = $copy; + } + } else { + $new_rollups[] = $rollup; + } + } + $report->options['Rollup'] = $new_rollups; + + // First get all the values + foreach ($report->options['Rollup'] as $rollup) { + // If we already got twig parameters for this dataset, skip it + if (isset($twig_params[$rollup['dataset']])) { + continue; + } + $twig_params[$rollup['dataset']] = []; + if (isset($report->options['DataSets'])) { + if (isset($report->options['DataSets'][$rollup['dataset']])) { + foreach ($report->options['DataSets'][$rollup['dataset']]['rows'] as $row) { + foreach ($row['values'] as $value) { + if (!isset($twig_params[$rollup['dataset']][$value->key])) { + $twig_params[$rollup['dataset']][$value->key] = ['values' => []]; + } + $twig_params[$rollup['dataset']][$value->key]['values'][] = $value->getValue(); + } + } + } + } + } + + // Then, calculate other statistical properties + foreach ($twig_params as $dataset => &$tp) { + foreach ($tp as $column => &$params) { + //get non-null values and sort them + $real_values = array_filter( + $params['values'], + function ($a) { + if ($a === null || $a === '') { + return false; + } + + return true; + } + ); + + sort($real_values); + + $params['sum'] = array_sum($real_values); + $params['count'] = count($real_values); + if ($params['count']) { + $params['mean'] = $params['average'] = $params['sum'] / $params['count']; + $params['median'] = ($params['count']%2) ? ($real_values[$params['count']/2-1] + $real_values[$params['count']/2])/2 : $real_values[floor($params['count']/2)]; + $params['min'] = $real_values[0]; + $params['max'] = $real_values[$params['count']-1]; + } else { + $params['mean'] = $params['average'] = $params['median'] = $params['min'] = $params['max'] = 0; + } + + $devs = []; + if (empty($real_values)) { + $params['stdev'] = 0; + } elseif (function_exists('stats_standard_deviation')) { + $params['stdev'] = stats_standard_deviation($real_values); + } else { + foreach ($real_values as $v) { + $devs[] = pow($v - $params['mean'], 2); + } + $params['stdev'] = sqrt(array_sum($devs) / (count($devs))); + } + } + } + + //render each rollup row + foreach ($report->options['Rollup'] as $rollup) { + if (!isset($report->options['DataSets'][$rollup['dataset']]['footer'])) { + $report->options['DataSets'][$rollup['dataset']]['footer'] = []; + } + $columns = $rollup['columns']; + $row = [ + 'values' => [], + 'rollup' => true, + ]; + + foreach ($twig_params[$rollup['dataset']] as $column => $p) { + if (isset($columns[$column])) { + $p = array_merge($p, ['row' => $twig_params[$rollup['dataset']]]); + + $row['values'][] = new ReportValue(-1, $column, PhpReports::renderString($columns[$column], $p)); + } else { + $row['values'][] = new ReportValue(-1, $column, null); + } + } + $report->options['DataSets'][$rollup['dataset']]['footer'][] = $row; + } + } +} diff --git a/src/Reports/Headers/VariableHeader.php b/src/Reports/Headers/VariableHeader.php new file mode 100644 index 00000000..6e6165d9 --- /dev/null +++ b/src/Reports/Headers/VariableHeader.php @@ -0,0 +1,230 @@ + [ + 'required' => true, + 'type' => 'string', + ], + 'display' => [ + 'type' => 'string', + ], + 'type' => [ + 'type' => 'enum', + 'values' => ['text', 'select', 'textarea', 'date', 'daterange'], + 'default' => 'text', + ], + 'options' => [ + 'type' => 'array', + ], + 'default' => [], + 'empty' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'multiple' => [ + 'type' => 'boolean', + 'default' => false, + ], + 'database_options' => [ + 'type' => 'object', + ], + 'description' => [ + 'type' => 'string', + ], + 'format' => [ + 'type' => 'string', + 'default' => 'Y-m-d H:i:s', + ], + 'modifier_options' => [ + 'type' => 'array', + ], + 'time_offset' => [ + 'type' => 'number', + ], + ]; + + public static function init($params, &$report) + { + if (!isset($params['display']) || !$params['display']) { + $params['display'] = $params['name']; + } + + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_\-]*$/', $params['name'])) { + throw new \Exception("Invalid variable name: $params[name]"); + } + + //add to options + if (!isset($report->options['Variables'])) { + $report->options['Variables'] = []; + } + $report->options['Variables'][$params['name']] = $params; + + //add to macros + if (!isset($report->macros[$params['name']]) && isset($params['default'])) { + $report->addMacro($params['name'], $params['default']); + + $report->macros[$params['name']] = $params['default']; + + if (!isset($params['empty']) || !$params['empty']) { + $report->is_ready = false; + } + } elseif (!isset($report->macros[$params['name']])) { + $report->addMacro($params['name'], ''); + + if (!isset($params['empty']) || !$params['empty']) { + $report->is_ready = false; + } + } + + //convert newline separated strings to array for vars that support multiple values + if ($params['multiple'] && !is_array($report->macros[$params['name']])) { + $report->addMacro($params['name'], explode("\n", $report->macros[$params['name']])); + } + + $report->exportHeader('Variable', $params); + } + + public static function parseShortcut($value) + { + list($var, $params) = explode(',', $value, 2); + $var = trim($var); + $params = trim($params); + + $parts = explode(',', $params); + $params = [ + 'name' => $var, + 'display' => trim($parts[0]), + ]; + + unset($parts[0]); + + $extra = implode(',', $parts); + + //just "name, label" + if (!$extra) { + return $params; + } + + //if the 3rd item is "LIST", use multi-select + if (preg_match('/^\s*LIST\s*\b/', $extra)) { + $params['multiple'] = true; + $extraexplode = explode(',', $extra, 2); + $extra = array_pop($extraexplode); + } + + //table.column, where clause, ALL + if (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,[^,]+,\s*ALL\s*$/', $extra)) { + list($table_column, $where, $all) = explode(',', $extra, 3); + list($table, $column) = explode('.', $table_column, 2); + + $params['type'] = 'select'; + + $var_params = [ + 'table' => $table, + 'column' => $column, + 'all' => true, + 'where' => $where, + ]; + + $params['database_options'] = $var_params; + } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,\s*ALL\s*$/', $extra)) { + //table.column, ALL + list($table_column, $all) = explode(',', $extra, 2); + list($table, $column) = explode('.', $table_column, 2); + + $params['type'] = 'select'; + + $var_params = [ + 'table' => $table, + 'column' => $column, + 'all' => true, + ]; + + $params['database_options'] = $var_params; + } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*,[^,]+$/', $extra)) { + //table.column, where clause + list($table_column, $where) = explode(',', $extra, 2); + list($table, $column) = explode('.', $table_column, 2); + + $params['type'] = 'select'; + + $var_params = [ + 'table' => $table, + 'column' => $column, + 'where' => $where, + ]; + + $params['database_options'] = $var_params; + } elseif (preg_match('/^\s*[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\s*$/', $extra)) { + //table.column + list($table, $column) = explode('.', $extra, 2); + + $params['type'] = 'select'; + + $var_params = [ + 'table' => $table, + 'column' => $column, + ]; + + $params['database_options'] = $var_params; + } elseif (preg_match('/^\s*([a-zA-Z0-9_\- ]+\|)+[a-zA-Z0-9_\- ]+$/', $extra)) { + //option1|option2 + $options = explode('|', $extra); + + $params['type'] = 'select'; + $params['options'] = $options; + } + + return $params; + } + + public static function afterParse(&$report) + { + $classname = $report->options['Type'].'ReportType'; + + foreach ($report->options['Variables'] as $var => $params) { + //if it's a select variable and the options are pulled from a database + if (isset($params['database_options'])) { + $classname::openConnection($report); + $params['options'] = $classname::getVariableOptions($params['database_options'], $report); + + $report->options['Variables'][$var] = $params; + } + + //if the type is daterange, parse start and end with strtotime + if ($params['type'] === 'daterange' && !empty($report->macros[$params['name']][0]) && !empty($report->macros[$params['name']][1])) { + $start = date_create($report->macros[$params['name']][0]); + if (!$start) { + throw new \Exception($params['display']." must have a valid start date."); + } + date_time_set($start, 0, 0, 0); + $report->macros[$params['name']]['start'] = date_format($start, $params['format']); + + $end = date_create($report->macros[$params['name']][1]); + if (!$end) { + throw new \Exception($params['display']." must have a valid end date."); + } + date_time_set($end, 23, 59, 59); + $report->macros[$params['name']]['end'] = date_format($end, $params['format']); + } + } + } + + public static function beforeRun(&$report) + { + foreach ($report->options['Variables'] as $var => $params) { + //if the type is date, parse with strtotime + if ($params['type'] === 'date' && $report->macros[$params['name']]) { + $time = strtotime($report->macros[$params['name']]); + if (!$time) { + throw new \Exception($params['display']." must be a valid datetime value."); + } + + $report->macros[$params['name']] = date($params['format'], $time); + } + } + } +} diff --git a/src/Reports/PhpReports.php b/src/Reports/PhpReports.php new file mode 100644 index 00000000..0b47391d --- /dev/null +++ b/src/Reports/PhpReports.php @@ -0,0 +1,784 @@ +base; + + if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') { + $protocol = 'https://'; + } else { + $protocol = 'http://'; + } + + self::$request->base = $protocol.rtrim($_SERVER['HTTP_HOST'].self::$request->base, '/'); + + //the load order for templates is: "templates/local", "templates/default", "templates" + //this means loading the template "html/report.twig" will load the local first and then the default + //if you want to extend a default template from within a local template, you can do {% extends "default/html/report.twig" %} and it will fall back to the last loader + $template_dirs = array('../templates/default','../templates'); + if (file_exists('../templates/local')) { + array_unshift($template_dirs, '../templates/local'); + } + + $loader = new \Twig_Loader_Chain(array( + new \Twig_Loader_Filesystem($template_dirs), + new \Twig_Loader_String(), + )); + + self::$twig = new \Twig_Environment($loader); + self::$twig->addFunction(new \Twig_SimpleFunction('dbdate', 'PhpReports::dbdate')); + self::$twig->addFunction(new \Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN')); + + if (isset($_COOKIE['reports-theme']) && $_COOKIE['reports-theme']) { + $theme = $_COOKIE['reports-theme']; + } else { + $theme = self::$config['bootstrap_theme']; + } + self::$twig->addGlobal('theme', $theme); + self::$twig->addGlobal('path', $path); + self::$twig->addGlobal('brand', self::$config['brand']); + + self::$twig->addFilter('var_dump', new \Twig_Filter_Function('var_dump')); + + self::$twig_string = new \Twig_Environment(new \Twig_Loader_String(), array('autoescape' => false)); + self::$twig_string->addFunction(new \Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN')); + + \FileSystemCache::$cacheDir = self::$config['cacheDir']; + + if (!isset($_SESSION['environment']) || !isset(self::$config['environments'][$_SESSION['environment']])) { + $environments = array_keys(self::$config['environments']); + $_SESSION['environment'] = array_shift($environments); + } + + // Extend twig. + if (isset($config['twig_init_function']) && is_callable($config['twig_init_function'])) { + $config['twig_init_function'](self::$twig); + $config['twig_init_function'](self::$twig_string); + } + } + + public static function setVar($key, $value) + { + if (!self::$vars) { + self::$vars = []; + } + + self::$vars[$key] = $value; + } + + public static function getVar($key, $default = null) + { + if (isset(self::$vars[$key])) { + return self::$vars[$key]; + } else { + return $default; + } + } + + public static function dbdate($time, $database = null, $format = null) + { + $report = self::getVar('Report', null); + if (!$report) { + return strtotime('Y-m-d H:i:s', strtotime($time)); + } + + //if a variable name was passed in + $var = null; + if (isset($report->options['Variables'][$time])) { + $var = $report->options['Variables'][$time]; + $time = $report->macros[$time]; + } + + $time = strtotime($time); + + $environment = $report->getEnvironment(); + + //determine time offset + $offset = 0; + + if ($database) { + if (isset($environment[$database]['time_offset'])) { + $offset = $environment[$database]['time_offset']; + } + } else { + $database = $report->getDatabase(); + if (isset($database['time_offset'])) { + $offset = $database['time_offset']; + } + } + + //if the time needs to be adjusted + if ($offset) { + $time = strtotime((($offset > 0) ? '+' : '-').abs($offset).' hours', $time); + } + + //determine output format + if ($format) { + $time = date($format, $time); + } elseif ($var && isset($var['format'])) { + $time = date($var['format'], $time); + } else { + //default to Y-m-d H:i:s + $time = date('Y-m-d H:i:s', $time); + } + + return $time; + } + + public static function generateSqlIN($column, $values, $or_null = false) + { + $sql = "$column IN ("; + foreach ($values as $value) { + $sql .= is_numeric($value) ? $value : "'$value'"; + if ($value !== end($values)) { + $sql .= ', '; + } + } + $sql .= ")"; + if ($or_null) { + $sql .= " OR $column IS NULL"; + } + + return $sql; + } + + public static function render($template, $macros) + { + $default = [ + 'base' => self::$request->base, + 'report_list_url' => self::$request->base.'/', + 'request' => self::$request, + 'querystring' => (array_key_exists('QUERY_STRING', $_SERVER) ? $_SERVER['QUERY_STRING'] : null), + 'config' => self::$config, + 'environment' => $_SESSION['environment'], + 'recent_reports' => self::getRecentReports(), + 'session' => $_SESSION, + ]; + $macros = array_merge($default, $macros); + + //if a template path like 'html/report' is given, add the twig file extension + if (preg_match('/^[a-zA-Z_\-0-9\/]+$/', $template)) { + $template .= '.twig'; + } + + return self::$twig->render($template, $macros); + } + + public static function renderString($template, $macros) + { + return self::$twig_string->render($template, $macros); + } + + public static function displayReport($report, $type) + { + $classname = '\\PhpReports\\Formats\\'.ucfirst(strtolower($type)).'ReportFormat'; + + $error_header = 'An error occurred while running your report'; + $content = ''; + + try { + if (!class_exists($classname)) { + $error_header = 'Unknown report format'; + throw new \Exception("Unknown report format '$type'"); + } + + try { + $report = $classname::prepareReport($report); + } catch (\Exception $e) { + $error_header = 'An error occurred while preparing your report'; + throw $e; + } + + $classname::display($report, self::$request); + + if (isset($report->options['Query_Formatted'])) { + $content = $report->options['Query_Formatted']; + } + } catch (\Exception $e) { + echo '
';
+            var_dump($e);
+            die();
+            echo self::render('html/page', [
+                'title' => $report->report,
+                'header' => '

'.$error_header.'

', + 'error' => $e->getMessage(), + 'content' => $content, + 'breadcrumb' => ['Report List' => '', $report->report => true], + ]); + } + } + + public static function editReport($report) + { + $template_vars = []; + + try { + $report = new Report($report, [], $_SESSION['environment']); + // $report = ReportFormatBase::prepareReport($report); + + $template_vars = [ + 'report' => $report->report, + 'options' => $report->options, + 'contents' => $report->getRaw(), + 'extension' => array_pop(explode('.', $report->report)), + ]; + } catch (\Exception $e) { + //if there is an error parsing the report + $template_vars = [ + 'report' => $report, + 'contents' => Report::getReportFileContents($report), + 'options' => [], + 'extension' => array_pop(explode('.', $report)), + 'error' => $e, + ]; + } + + if (isset($_POST['preview'])) { + echo "
".SimpleDiff::htmlDiffSummary($template_vars['contents'], $_POST['contents'])."
"; + } elseif (isset($_POST['save'])) { + Report::setReportFileContents($template_vars['report'], $_POST['contents']); + } else { + echo self::render('html/report_editor', $template_vars); + } + } + + public static function listReports() + { + $errors = []; + + $reports = self::getReports(self::$config['reportDir'].'/', $errors); + + $template_vars['reports'] = $reports; + $template_vars['report_errors'] = $errors; + + $start = microtime(true); + + echo self::render('html/report_list', $template_vars); + } + + public static function listDashboards() + { + $dashboards = self::getDashboards(); + + uasort($dashboards, function ($a, $b) { + return strcmp($a['title'], $b['title']); + }); + + echo self::render('html/dashboard_list', array( + 'dashboards' => $dashboards, + )); + } + + public static function displayDashboard($dashboard) + { + $content = self::getDashboard($dashboard); + + echo self::render('html/dashboard', array( + 'dashboard' => $content, + )); + } + + public static function getDashboards() + { + $dashboards = glob(PhpReports::$config['dashboardDir'].'/*.json'); + + $ret = []; + foreach ($dashboards as $key => $value) { + $name = basename($value, '.json'); + $ret[$name] = self::getDashboard($name); + } + + return $ret; + } + + public static function getDashboard($dashboard) + { + $file = PhpReports::$config['dashboardDir'].'/'.$dashboard.'.json'; + if (!file_exists($file)) { + throw new \Exception("Unknown dashboard - ".$dashboard); + } + + return json_decode(file_get_contents($file), true); + } + + public static function getRecentReports() + { + $recently_run = \FileSystemCache::retrieve(\FileSystemCache::generateCacheKey('recently_run')); + $recent = array(); + if ($recently_run !== false) { + $i = 0; + foreach ($recently_run as $report) { + if ($i > 10) { + break; + } + + $headers = self::getReportHeaders($report); + + if (!$headers) { + continue; + } + if (isset($recent[$headers['url']])) { + continue; + } + + $recent[$headers['url']] = $headers; + $i++; + } + } + + return array_values($recent); + } + public static function getReportListJSON($reports = null) + { + if ($reports === null) { + $errors = array(); + $reports = self::getReports(self::$config['reportDir'].'/', $errors); + } + + //weight by popular reports + $recently_run = \FileSystemCache::retrieve(\FileSystemCache::generateCacheKey('recently_run')); + $popular = array(); + if ($recently_run !== false) { + foreach ($recently_run as $report) { + if (!isset($popular[$report])) { + $popular[$report] = 1; + } else { + $popular[$report]++; + } + } + } + $parts = array(); + + foreach ($reports as $report) { + if ($report['is_dir'] && $report['children']) { + //skip if the directory doesn't have a title + if (!isset($report['Title']) || !$report['Title']) { + continue; + } + + $part = trim(self::getReportListJSON($report['children']), '[],'); + if ($part) { + $parts[] = $part; + } + } else { + //skip if report is marked as dangerous + if ((isset($report['stop']) && $report['stop']) || isset($report['Caution']) || isset($report['warning'])) { + continue; + } + if (!isset($report['url'])) { + continue; + } + if (!isset($report['report'])) { + continue; + } + + //skip if report is marked as ignore + if (isset($report['ignore']) && $report['ignore']) { + continue; + } + + if (isset($popular[$report['report']])) { + $popularity = $popular[$report['report']]; + } else { + $popularity = 0; + } + + $parts[] = json_encode(array( + 'name' => $report['Name'], + 'url' => $report['url'], + 'popularity' => $popularity, + )); + } + } + + return '['.trim(implode(',', $parts), ',').']'; + } + + protected static function getReportHeaders($report) + { + $cacheKey = \FileSystemCache::generateCacheKey(array(self::$request->base, $report), 'report_headers'); + + //check if report data is cached and newer than when the report file was created + //the url parameter ?nocache will bypass this and not use cache + $data = false; + + $loc = Report::getFileLocation($report); + if (!file_exists($loc)) { + return false; + } + if (!isset($_REQUEST['nocache'])) { + $data = \FileSystemCache::retrieve($cacheKey, filemtime($loc)); + } + + //report data not cached, need to parse it + if ($data === false) { + $temp = new Report($report); + + $data = $temp->options; + + $data['report'] = $report; + $data['url'] = self::$request->base.'/report/html/?report='.$report; + $data['is_dir'] = false; + $data['Id'] = str_replace(array('_', '-', '/', ' ', '.'), array('', '', '_', '-', '_'), trim($report, '/')); + if (!isset($data['Name'])) { + $data['Name'] = ucwords(str_replace(array('_', '-'), ' ', basename($report))); + } + + //store parsed report in cache + \FileSystemCache::store($cacheKey, $data); + } + + return $data; + } + + protected static function getReports($dir, &$errors = null) + { + $base = self::$config['reportDir'].'/'; + + $reports = glob($dir.'*', GLOB_NOSORT); + $return = array(); + foreach ($reports as $key => $report) { + $title = $description = false; + + if (is_dir($report)) { + if (file_exists($report.'/TITLE.txt')) { + $title = file_get_contents($report.'/TITLE.txt'); + } + if (file_exists($report.'/README.txt')) { + $description = file_get_contents($report.'/README.txt'); + } + + $id = str_replace(array('_', '-', '/', ' '), array('', '', '_', '-'), trim(substr($report, strlen($base)), '/')); + + $children = self::getReports($report.'/', $errors); + + $count = 0; + foreach ($children as $child) { + if (isset($child['count'])) { + $count += $child['count']; + } else { + $count++; + } + } + + $return[] = array( + 'Name' => ucwords(str_replace(array('_', '-'), ' ', basename($report))), + 'Title' => $title, + 'Id' => $id, + 'Description' => $description, + 'is_dir' => true, + 'children' => $children, + 'count' => $count, + ); + } else { + //files to skip + if (strpos(basename($report), '.') === false) { + continue; + } + $reportExploded = explode('.', $report); + $ext = array_pop($reportExploded); + if (!isset(self::$config['default_file_extension_mapping'][$ext])) { + continue; + } + + $name = substr($report, strlen($base)); + + try { + $data = self::getReportHeaders($name, $base); + $return[] = $data; + } catch (\Exception $e) { + if (!$errors) { + $errors = array(); + } + $errors[] = array( + 'report' => $name, + 'exception' => $e, + ); + } + } + } + + usort($return, function (&$a, &$b) { + if ($a['is_dir'] && !$b['is_dir']) { + return 1; + } elseif ($b['is_dir'] && !$a['is_dir']) { + return -1; + } + + if (empty($a['Title']) && empty($b['Title'])) { + return strcmp($a['Name'], $b['Name']); + } elseif (empty($a['Title'])) { + return 1; + } elseif (empty($b['Title'])) { + return -1; + } + + return strcmp($a['Title'], $b['Title']); + }); + + return $return; + } + + /** + * Emails a report given a TO address, a subject, and a message + */ + public static function emailReport() + { + if (!isset($_REQUEST['email']) || !filter_var($_REQUEST['email'], FILTER_VALIDATE_EMAIL)) { + echo json_encode(array('error' => 'Valid email address required')); + + return; + } + if (!isset($_REQUEST['url'])) { + echo json_encode(array('error' => 'Report url required')); + + return; + } + if (!isset(PhpReports::$config['mail_settings']['enabled']) || !PhpReports::$config['mail_settings']['enabled']) { + echo json_encode(array('error' => 'Email is disabled on this server')); + + return; + } + if (!isset(PhpReports::$config['mail_settings']['from'])) { + echo json_encode(array('error' => 'Email settings have not been properly configured on this server')); + + return; + } + + $from = PhpReports::$config['mail_settings']['from']; + $subject = $_REQUEST['subject'] ? $_REQUEST['subject'] : 'Database Report'; + $body = $_REQUEST['message'] ? $_REQUEST['message'] : "You've been sent a database report!"; + $email = $_REQUEST['email']; + $link = $_REQUEST['url']; + $csv_link = str_replace('report/html/?', 'report/csv/?', $link); + $table_link = str_replace('report/html/?', 'report/table/?', $link); + $text_link = str_replace('report/html/?', 'report/text/?', $link); + + // Get the CSV file attachment and the inline HTML table + $csv = self::urlDownload($csv_link); + $table = self::urlDownload($table_link); + $text = self::urlDownload($text_link); + + $email_text = $body."\n\n".$text."\n\nView the report online at $link"; + $email_html = "

$body

$table

View the report online at ".htmlentities($link)."

"; + + // Create the message + $message = Swift_Message::newInstance() + ->setSubject($subject) + ->setFrom($from) + ->setTo($email) + //text body + ->setBody($email_text) + //html body + ->addPart($email_html, 'text/html') + ; + + $attachment = Swift_Attachment::newInstance() + ->setFilename('report.csv') + ->setContentType('text/csv') + ->setBody($csv) + ; + + $message->attach($attachment); + + // Create the Transport + $transport = self::getMailTransport(); + $mailer = Swift_Mailer::newInstance($transport); + + try { + // Send the message + $result = $mailer->send($message); + } catch (\Exception $e) { + echo json_encode(array( + 'error' => $e->getMessage(), + )); + + return; + } + + if ($result) { + echo json_encode(array( + 'success' => true, + )); + } else { + echo json_encode(array( + 'error' => 'Failed to send email to requested recipient', + )); + } + } + + /** + * Determines the email transport to use based on the configuration settings + */ + protected static function getMailTransport() + { + if (!isset(PhpReports::$config['mail_settings'])) { + PhpReports::$config['mail_settings'] = array(); + } + if (!isset(PhpReports::$config['mail_settings']['method'])) { + PhpReports::$config['mail_settings']['method'] = 'mail'; + } + + switch (PhpReports::$config['mail_settings']['method']) { + case 'mail': + return Swift_MailTransport::newInstance(); + case 'sendmail': + return Swift_MailTransport::newInstance( + isset(PhpReports::$config['mail_settings']['command']) ? PhpReports::$config['mail_settings']['command'] : '/usr/sbin/sendmail -bs' + ); + case 'smtp': + if (!isset(PhpReports::$config['mail_settings']['server'])) { + throw new \Exception("SMTP server must be configured"); + } + $transport = Swift_SmtpTransport::newInstance( + PhpReports::$config['mail_settings']['server'], + isset(PhpReports::$config['mail_settings']['port']) ? PhpReports::$config['mail_settings']['port'] : 25 + ); + + //if username/password + if (isset(PhpReports::$config['mail_settings']['username'])) { + $transport->setUsername(PhpReports::$config['mail_settings']['username']); + $transport->setPassword(PhpReports::$config['mail_settings']['password']); + } + + //if using encryption + if (isset(PhpReports::$config['mail_settings']['encryption'])) { + $transport->setEncryption(PhpReports::$config['mail_settings']['encryption']); + } + + return $transport; + default: + throw new \Exception("Mail method must be either 'mail', 'sendmail', or 'smtp'"); + } + } + + /** + * Autoloader methods + */ + // public static function loader($className) + // { + // if (!isset(self::$loader_cache)) { + // self::buildLoaderCache(); + // } + + // if (isset(self::$loader_cache[$className])) { + // require_once self::$loader_cache[$className]; + + // return true; + // } else { + // return false; + // } + // } + // public static function buildLoaderCache() + // { + // self::load('classes/local'); + // self::load('classes', array('classes/local')); + // self::load('lib'); + // } + // public static function load($dir, $skip = array()) + // { + // $files = glob($dir.'/*.php'); + // $dirs = glob($dir.'/*', GLOB_ONLYDIR); + + // foreach ($files as $file) { + // //for file names same as class name + // $className = basename($file, '.php'); + // if (!isset(self::$loader_cache[$className])) { + // self::$loader_cache[$className] = $file; + // } + + // //for PEAR style: Path_To_Class.php + // $parts = explode('/', substr($file, 0, -4)); + // array_shift($parts); + // $className = implode('_', $parts); + // //if any of the directories in the path are lowercase, it isn't in PEAR format + // if (preg_match('/(^|_)[a-z]/', $className)) { + // continue; + // } + // if (!isset(self::$loader_cache[$className])) { + // self::$loader_cache[$className] = $file; + // } + // } + + // foreach ($dirs as $dir2) { + // //directories to skip + // if ($dir2[0] === '.') { + // continue; + // } + // if (in_array($dir2, $skip)) { + // continue; + // } + // if (in_array(basename($dir2), array('tests', 'test', 'example', 'examples', 'bin'))) { + // continue; + // } + + // self::load($dir2, $skip); + // } + // } + + /** + * A more lenient json_decode than the built-in PHP one. + * It supports strict JSON as well as javascript syntax (i.e. unquoted/single quoted keys, single quoted values, trailing commmas) + */ + public static function json_decode($json, $assoc = false) + { + //replace single quoted values + $json = preg_replace_callback('/:\s*\'(([^\']|\\\\\')*)\'\s*([},])/', create_function('$matches', 'return "\':\'.json_encode(stripslashes(\'$matches[1]\')).\'$matches[3]\'";'), $json); + + //replace single quoted keys + $json = preg_replace_callback('/\'(([^\']|\\\\\')*)\'\s*:/', create_function('$matches', 'return "json_encode(stripslashes(\'$matches[1]\')).\':\'";'), $json); + + //remove any line breaks in the code + $json = str_replace(array("\n", "\r"), "", $json); + + //replace non-quoted keys with double quoted keys + $json = preg_replace('#(?
\{|\[|,)\s*(?(?:\w|_)+)\s*:#im', '$1"$2":', $json);
+
+        //remove trailing comma
+        $json = preg_replace('/,\s*\}/', '}', $json);
+
+        return json_decode($json, $assoc);
+    }
+
+    protected static function urlDownload($url)
+    {
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_HEADER, 0);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+        $output = curl_exec($ch);
+        curl_close($ch);
+
+        return $output;
+    }
+}
+
+PhpReports::init();
diff --git a/src/Reports/Report.php b/src/Reports/Report.php
new file mode 100644
index 00000000..4de1715d
--- /dev/null
+++ b/src/Reports/Report.php
@@ -0,0 +1,723 @@
+report = $report;
+
+        if (!file_exists(self::getFileLocation($report))) {
+            throw new \Exception('Report not found - '.$report);
+        }
+
+        $this->filemtime = filemtime(self::getFileLocation($report));
+
+        $this->use_cache = $use_cache;
+
+        //get the raw report file
+        $this->raw = self::getReportFileContents($report);
+
+        //if there are no headers in this report
+        if (strpos($this->raw, "\n\n") === false) {
+            throw new \Exception('Report missing headers - '.$report);
+        }
+
+        //split the raw report into headers and code
+        list($this->raw_headers, $this->raw_query) = explode("\n\n", $this->raw, 2);
+
+        $this->macros = [];
+        foreach ($macros as $key => $value) {
+            $this->addMacro($key, $value);
+        }
+
+        $this->parseHeaders();
+
+        $this->options['Environment'] = $environment;
+
+        $this->initDb();
+
+        $this->getTimeEstimate();
+    }
+
+    public static function getFileLocation($report)
+    {
+        //make sure the report path doesn't go up a level for security reasons
+        if (strpos($report, "..") !== false) {
+            $reportdir = realpath(PhpReports::$config['reportDir']).'/';
+            $reportpath = substr(realpath(PhpReports::$config['reportDir'].'/'.$report), 0, strlen($reportdir));
+
+            if ($reportpath !== $reportdir) {
+                throw new \Exception('Invalid report - '.$report);
+            }
+        }
+
+        $reportDir = PhpReports::$config['reportDir'];
+
+        return $reportDir.'/'.$report;
+    }
+
+    public static function setReportFileContents($report, $new_contents)
+    {
+        echo "SAVING CONTENTS TO ".self::getFileLocation($report);
+
+        if (!file_put_contents(self::getFileLocation($report), $new_contents)) {
+            throw new \Exception("Failed to set report contents");
+        }
+
+        echo "\n".$new_contents;
+    }
+
+    public static function getReportFileContents($report)
+    {
+        $contents = file_get_contents(self::getFileLocation($report));
+
+        //convert EOL to unix format
+        return str_replace(["\r\n", "\r"], "\n", $contents);
+    }
+
+    public function getDatabase()
+    {
+        if (isset($this->options['Database']) && $this->options['Database']) {
+            $environment = $this->getEnvironment();
+
+            if (isset($environment[$this->options['Database']])) {
+                return $environment[$this->options['Database']];
+            }
+        }
+
+        return array();
+    }
+
+    public function getEnvironment()
+    {
+        return PhpReports::$config['environments'][$this->options['Environment']];
+    }
+
+    public function addMacro($name, $value)
+    {
+        $this->macros[$name] = $value;
+    }
+
+    public function exportHeader($name, $params)
+    {
+        $this->exported_headers[] = ['name' => $name, 'params' => $params];
+    }
+
+    public function getCacheKey()
+    {
+        return \FileSystemCache::generateCacheKey(
+            [
+                'report' => $this->report,
+                'macros' => $this->macros,
+                'database' => $this->options['Environment'],
+            ],
+            'report_results'
+        );
+    }
+
+    public function getReportTimesCacheKey()
+    {
+        return \FileSystemCache::generateCacheKey($this->report, 'report_times');
+    }
+
+    protected function retrieveFromCache()
+    {
+        if (!$this->use_cache) {
+            return false;
+        }
+
+        return \FileSystemCache::retrieve($this->getCacheKey(), 'results', $this->filemtime);
+    }
+
+    protected function storeInCache()
+    {
+        if (isset($this->options['Cache']) && is_numeric($this->options['Cache'])) {
+            $ttl = intval($this->options['Cache']);
+        } else {
+            $ttl = 600; //default to caching things for 10 minutes
+        }
+
+        \FileSystemCache::store($this->getCacheKey(), $this->options, 'results', $ttl);
+    }
+
+    protected function parseHeaders()
+    {
+        //default the report to being ready
+        //if undefined variables are found in the headers, set to false
+        $this->is_ready = true;
+
+        $this->options = [
+            'Filters' => [],
+            'Variables' => [],
+            'Includes' => [],
+        ];
+
+        $this->headers = [];
+
+        $lines = explode("\n", $this->raw_headers);
+
+        //remove empty headers and remove comment characters
+        $fixed_lines = [];
+
+        foreach ($lines as $line) {
+            if (empty($line)) {
+                continue;
+            }
+
+            //if the line doesn't start with a comment character, skip
+            if (!in_array(substr($line, 0, 2), ['--', '/*', '//', ' *']) && $line[0] !== '#') {
+                continue;
+            }
+
+            //remove comment from start of line and skip if empty
+            $line = trim(ltrim($line, "-*/# \t"));
+            if (!$line) {
+                continue;
+            }
+
+            $fixed_lines[] = $line;
+        }
+
+        $lines = $fixed_lines;
+
+        $name = null;
+        $value = '';
+
+        foreach ($lines as $line) {
+            $has_name_value = preg_match('/^\s*[A-Z0-9_\-]+\s*\:/', $line);
+
+            //if this is the first header and not in the format name:value, assume it is the report name
+            if (!$has_name_value && $name === null && (!isset($this->options['Name']) || !$this->options['Name'])) {
+                $this->parseHeader('Info', ['name' => $line]);
+            } else {
+                //if this is a continuation of another header
+                if (!$has_name_value) {
+                    $value .= "\n".trim($line);
+                } else {
+                    //if this is a new header
+                    //if the previous header didn't have a name, assume it is the description
+                    if ($value && $name === null) {
+                        $this->parseHeader('Info', ['description' => $value]);
+                    } elseif ($value) {
+                        //otherwise, parse the previous header
+                        $this->parseHeader($name, $value);
+                    }
+
+                    list($name, $value) = explode(':', $line, 2);
+                    $name = trim($name);
+                    $value = trim($value);
+
+                    if (strtoupper($name) === $name) {
+                        $name = ucfirst(strtolower($name));
+                    };
+                }
+            }
+        }
+        //parse the last header
+        if ($value && $name) {
+            $this->parseHeader($name, $value);
+        }
+
+        //try to infer report type from file extension
+        if (!isset($this->options['Type'])) {
+            $explodedReport = explode('.', $this->report);
+            $file_type = array_pop($explodedReport);
+
+            if (!isset(PhpReports::$config['default_file_extension_mapping'][$file_type])) {
+                throw new \Exception("Unknown report type - ".$this->report);
+            } else {
+                $this->options['Type'] = PhpReports::$config['default_file_extension_mapping'][$file_type];
+            }
+        }
+
+        if (!isset($this->options['Database'])) {
+            $this->options['Database'] = strtolower($this->options['Type']);
+        }
+
+        if (!isset($this->options['Name'])) {
+            $this->options['Name'] = $this->report;
+        }
+    }
+
+    public function parseHeader($name, $value, $dataset = null)
+    {
+        $classname = '\\PhpReports\\Headers\\'.$name.'Header';
+
+        if (class_exists($classname)) {
+            if ($dataset !== null && isset($classname::$validation) && isset($classname::$validation['dataset'])) {
+                $value['dataset'] = $dataset;
+            }
+
+            $classname::parse($name, $value, $this);
+
+            if (!in_array($name, $this->headers)) {
+                $this->headers[] = $name;
+            }
+        } else {
+            throw new \Exception("Unknown header '$name' - ".$this->report);
+        }
+    }
+
+    public function addFilter($dataset, $column, $type, $options)
+    {
+        // If adding for multiple datasets
+        if (is_array($dataset)) {
+            foreach ($dataset as $d) {
+                $this->addFilter($d, $column, $type, $options);
+            }
+        } elseif ($dataset === true) {
+            // If adding for all datasets
+            $this->addFilter('all', $column, $type, $options);
+        } else {
+            // If adding for a single dataset
+            if (!isset($this->filters[$dataset])) {
+                $this->filters[$dataset] = [];
+            }
+
+            if (!isset($this->filters[$dataset][$column])) {
+                $this->filters[$dataset][$column] = [];
+            }
+
+            $this->filters[$dataset][$column][$type] = $options;
+        }
+    }
+
+    protected function applyFilters($dataset, $column, $value, $row)
+    {
+        // First, apply filters for all datasets
+        if (isset($this->filters['all']) && isset($this->filters['all'][$column])) {
+            foreach ($this->filters['all'][$column] as $type => $options) {
+                $classname = '\\PhpReports\\Filters\\'.$type.'Filter';
+                $value = $classname::filter($value, $options, $this, $row);
+
+                //if the column should not be displayed
+                if ($value === false) {
+                    return false;
+                }
+            }
+        }
+
+        // Then apply filters for this specific dataset
+        if (isset($this->filters[$dataset]) && isset($this->filters[$dataset][$column])) {
+            foreach ($this->filters[$dataset][$column] as $type => $options) {
+                $classname = '\\PhpReports\\Filters\\'.$type.'Filter';
+                $value = $classname::filter($value, $options, $this, $row);
+
+                //if the column should not be displayed
+                if ($value === false) {
+                    return false;
+                }
+            }
+        }
+
+        return $value;
+    }
+
+    protected function initDb()
+    {
+        //if the database isn't set, use the first defined one from config
+        $environments = PhpReports::$config['environments'];
+        if (!$this->options['Environment']) {
+            $this->options['Environment'] = current(array_keys($environments));
+        }
+
+        //set database options
+        $environment_options = [];
+        foreach ($environments as $key => $params) {
+            $environment_options[] = [
+                'name' => $key,
+                'selected' => ($key === $this->options['Environment']),
+            ];
+        }
+
+        $this->options['Environments'] = $environment_options;
+
+        //add a host macro
+        if (isset($environments[$this->options['Environment']]['host'])) {
+            $this->macros['host'] = $environments[$this->options['Environment']]['host'];
+        }
+
+        $classname = '\\PhpReports\\Types\\'.$this->options['Type'].'ReportType';
+
+        if (!class_exists($classname)) {
+            throw new \Exception("Unknown report type '".$this->options['Type']."'");
+        }
+
+        $classname::init($this);
+    }
+
+    public function getRaw()
+    {
+        return $this->raw;
+    }
+
+    public function getUrl()
+    {
+        return 'report/html/?report='.urlencode($this->report);
+    }
+
+    public function prepareVariableForm()
+    {
+        $vars = [];
+
+        if ($this->options['Variables']) {
+            foreach ($this->options['Variables'] as $var => $params) {
+                if (!isset($params['name'])) {
+                    $params['name'] = ucwords(str_replace(['_', '-'], ' ', $var));
+                }
+                if (!isset($params['type'])) {
+                    $params['type'] = 'string';
+                }
+                if (!isset($params['options'])) {
+                    $params['options'] = false;
+                }
+                $params['value'] = $this->macros[$var];
+                $params['key'] = $var;
+
+                if ($params['type'] === 'select') {
+                    $params['is_select'] = true;
+
+                    foreach ($params['options'] as $key => $option) {
+                        if (!is_array($option)) {
+                            $params['options'][$key] = [
+                                'display' => $option,
+                                'value' => $option,
+                            ];
+                        }
+
+                        if ($params['options'][$key]['value'] == $params['value']) {
+                            $params['options'][$key]['selected'] = true;
+                        } elseif (is_array($params['value']) && in_array($params['options'][$key]['value'], $params['value'])) {
+                            $params['options'][$key]['selected'] = true;
+                        } else {
+                            $params['options'][$key]['selected'] = false;
+                        }
+
+                        if ($params['multiple']) {
+                            $params['is_multiselect'] = true;
+                            $params['choices'] = count($params['options']);
+                        }
+                    }
+                } else {
+                    if ($params['multiple']) {
+                        $params['is_textarea'] = true;
+                    }
+                }
+
+                if (isset($params['modifier_options'])) {
+                    $modifier_value = isset($this->macros[$var.'_modifier']) ? $this->macros[$var.'_modifier'] : null;
+
+                    foreach ($params['modifier_options'] as $key => $option) {
+                        if (!is_array($option)) {
+                            $params['modifier_options'][$key] = [
+                                'display' => $option,
+                                'value' => $option,
+                            ];
+                        }
+
+                        if ($params['modifier_options'][$key]['value'] == $modifier_value) {
+                            $params['modifier_options'][$key]['selected'] = true;
+                        } else {
+                            $params['modifier_options'][$key]['selected'] = false;
+                        }
+                    }
+                }
+
+                $vars[] = $params;
+            }
+        }
+
+        return $vars;
+    }
+
+    protected function _runReport()
+    {
+        if (!$this->is_ready) {
+            throw new \Exception("Report is not ready. Missing variables");
+        }
+
+        PhpReports::setVar('Report', $this);
+
+        //release the write lock on the session file
+        //so the session isn't locked while the report is running
+        session_write_close();
+
+        $classname = '\\PhpReports\\Types\\'.$this->options['Type'].'ReportType';
+
+        if (!class_exists($classname)) {
+            throw new \Exception("Unknown report type '".$this->options['Type']."'");
+        }
+
+        foreach ($this->headers as $header) {
+            $headerclass = '\\PhpReports\\Headers\\'.$header.'Header';
+            $headerclass::beforeRun($this);
+        }
+
+        $classname::openConnection($this);
+        $datasets = $classname::run($this);
+        $classname::closeConnection($this);
+
+        // Convert old single dataset format to multi-dataset format
+        if (!isset($datasets[0]['rows']) || !is_array($datasets[0]['rows'])) {
+            $datasets = [
+                [
+                    'rows' => $datasets,
+                ],
+            ];
+        }
+
+        // Only include a subset of datasets
+        $include = array_keys($datasets);
+        if (isset($_GET['dataset'])) {
+            $include = [$_GET['dataset']];
+        } elseif (isset($_GET['datasets'])) {
+            // If just a single dataset was specified, make it an array
+            if (!is_array($_GET['datasets'])) {
+                $include = explode(',', $_GET['datasets']);
+            } else {
+                $include = $_GET['datasets'];
+            }
+        }
+
+        $this->options['DataSets'] = [];
+        foreach ($include as $i) {
+            if (!isset($datasets[$i])) {
+                continue;
+            }
+            $this->options['DataSets'][$i] = $datasets[$i];
+        }
+
+        $this->parseDynamicHeaders();
+    }
+
+    protected function parseDynamicHeaders()
+    {
+        foreach ($this->options['DataSets'] as $i => &$dataset) {
+            if (isset($dataset['headers'])) {
+                foreach ($dataset['headers'] as $j => $header) {
+                    if (isset($header['header']) && isset($header['value'])) {
+                        $this->parseHeader($header['header'], $header['value'], $i);
+                    }
+                }
+            }
+        }
+    }
+
+    protected function getTimeEstimate()
+    {
+        $report_times = \FileSystemCache::retrieve($this->getReportTimesCacheKey());
+        if (!$report_times) {
+            return;
+        }
+
+        sort($report_times);
+
+        $sum = array_sum($report_times);
+        $count = count($report_times);
+        $average = $sum/$count;
+        $quartile1 = $report_times[round(($count-1)/4)];
+        $median = $report_times[round(($count-1)/2)];
+        $quartile3 = $report_times[round(($count-1)*3/4)];
+        $min = min($report_times);
+        $max = max($report_times);
+        $iqr = $quartile3-$quartile1;
+        $range = (1.5)*$iqr;
+
+        $sample_square = 0;
+        for ($i = 0; $i < $count; $i++) {
+            $sample_square += pow($report_times[$i], 2);
+        }
+        $standard_deviation = sqrt($sample_square / $count - pow(($average), 2));
+
+        $this->options['time_estimate'] = [
+            'times' => $report_times,
+            'count' => $count,
+            'min' => round($min, 2),
+            'max' => round($max, 2),
+            'median' => round($median, 2),
+            'average' => round($average, 2),
+            'q1' => round($quartile1, 2),
+            'q3' => round($quartile3, 2),
+            'iqr' => round($range, 2),
+            'sum' => round($sum, 2),
+            'stdev' => round($standard_deviation, 2),
+        ];
+    }
+
+    protected function prepareDataSets()
+    {
+        foreach ($this->options['DataSets'] as $i => $dataset) {
+            $this->prepareRows($i);
+        }
+
+        if (isset($this->options['DataSets'][0])) {
+            $this->options['Rows'] = $this->options['DataSets'][0]['rows'];
+            $this->options['Count'] = $this->options['DataSets'][0]['count'];
+        }
+    }
+
+    protected function prepareRows($dataset)
+    {
+        $rows = [];
+
+        //generate list of all values for each numeric column
+        //this is used to calculate percentiles/averages/etc.
+        $vals = [];
+        foreach ($this->options['DataSets'][$dataset]['rows'] as $row) {
+            foreach ($row as $key => $value) {
+                if (!isset($vals[$key])) {
+                    $vals[$key] = [];
+                }
+
+                if (is_numeric($value)) {
+                    $vals[$key][] = $value;
+                }
+            }
+        }
+
+        $this->options['DataSets'][$dataset]['values'] = $vals;
+
+        foreach ($this->options['DataSets'][$dataset]['rows'] as $row) {
+            $rowval = [];
+
+            $i = 1;
+            foreach ($row as $key => $value) {
+                $val = new ReportValue($i, $key, $value);
+
+                //apply filters for the column key
+                $val = $this->applyFilters($dataset, $key, $val, $row);
+                //apply filters for the column position
+                if ($val) {
+                    $val = $this->applyFilters($dataset, $i, $val, $row);
+                }
+
+                if ($val) {
+                    $rowval[] = $val;
+                }
+
+                $i++;
+            }
+
+            $first = !$rows;
+
+            $rows[] = array(
+                'values' => $rowval,
+                'first' => $first,
+            );
+        }
+
+        $this->options['DataSets'][$dataset]['rows'] = $rows;
+        $this->options['DataSets'][$dataset]['count'] = count($rows);
+    }
+
+    public function run()
+    {
+        if ($this->has_run) {
+            return true;
+        }
+
+        //at this point, all the headers are parsed and we haven't run the report yet
+        foreach ($this->headers as $header) {
+            $classname = '\\PhpReports\\Headers\\'.$header.'Header';
+            $classname::afterParse($this);
+        }
+
+        //record how long it takes to run the report
+        $start = microtime(true);
+
+        if ($this->is_ready && !$this->async) {
+            //if the report is cached
+            if ($options = $this->retrieveFromCache()) {
+                $this->options = $options;
+                $this->options['FromCache'] = true;
+            } else {
+                $this->_runReport();
+                $this->prepareDataSets();
+                $this->storeInCache();
+            }
+
+            //add this to the list of recently run reports
+            $recently_run_key = \FileSystemCache::generateCacheKey('recently_run');
+            $recently_run = \FileSystemCache::retrieve($recently_run_key);
+
+            if ($recently_run === false) {
+                $recently_run = [];
+            }
+
+            array_unshift($recently_run, $this->report);
+
+            if (count($recently_run) > 200) {
+                $recently_run = array_slice($recently_run, 0, 200);
+            }
+
+            \FileSystemCache::store($recently_run_key, $recently_run);
+        }
+
+        //call the beforeRender callback for each header
+        foreach ($this->headers as $header) {
+            $classname = '\\PhpReports\\Headers\\'.$header.'Header';
+            $classname::beforeRender($this);
+        }
+
+        $this->options['Time'] = round(microtime(true) - $start, 5);
+
+        if ($this->is_ready && !$this->async && !isset($this->options['FromCache'])) {
+            //get current report times for this report
+            $report_times = \FileSystemCache::retrieve($this->getReportTimesCacheKey());
+            if (!$report_times) {
+                $report_times = [];
+            }
+            //only keep the last 10 times for each report
+            //this keeps the timing data up to date and relevant
+            if (count($report_times) > 10) {
+                array_shift($report_times);
+            }
+
+            //store report times
+            $report_times[] = $this->options['Time'];
+            \FileSystemCache::store($this->getReportTimesCacheKey(), $report_times);
+        }
+
+        $this->has_run = true;
+    }
+
+    public function renderReportPage($template = 'html/report', $additional_vars = [])
+    {
+        $this->run();
+
+        $template_vars = [
+            'is_ready' => $this->is_ready,
+            'async' => $this->async,
+            'report_url' => PhpReports::$request->base.'/report/?'.$_SERVER['QUERY_STRING'],
+            'report_querystring' => $_SERVER['QUERY_STRING'],
+            'base' => PhpReports::$request->base,
+            'report' => $this->report,
+            'vars' => $this->prepareVariableForm(),
+            'macros' => $this->macros,
+        ];
+
+        $template_vars = array_merge($template_vars, $additional_vars);
+
+        $template_vars = array_merge($template_vars, $this->options);
+
+        return PhpReports::render($template, $template_vars);
+    }
+}
diff --git a/src/Reports/ReportValue.php b/src/Reports/ReportValue.php
new file mode 100644
index 00000000..910f6542
--- /dev/null
+++ b/src/Reports/ReportValue.php
@@ -0,0 +1,118 @@
+i = $i;
+        $this->key = $key;
+        $this->original_value = $value;
+        $this->filtered_value = is_string($value) ? strip_tags($value) : $value;
+        $this->html_value = $value;
+        $this->chart_value = $value;
+
+        $this->is_html = false;
+        $this->class = '';
+
+        $this->type = $this->_getType();
+    }
+
+    public function addClass($class)
+    {
+        $this->class = trim($this->class.' '.$class);
+    }
+
+    public function setValue($value, $html = false)
+    {
+        if (is_string($value)) {
+            $value = trim($value);
+        }
+
+        if ($html) {
+            $this->is_html = true;
+            $this->html_value = $value;
+        } else {
+            $this->is_html = false;
+            $this->filtered_value = is_string($value) ? htmlentities($value) : $value;
+            $this->html_value = $value;
+        }
+
+        $this->type = $this->_getType();
+    }
+
+    protected function _getType($value = null)
+    {
+        if (is_null($value)) {
+            return null;
+        } elseif (trim($value) === '') {
+            return null;
+        } elseif (preg_match('/^([$%(\-+\s])*([0-9,]+(\.[0-9]+)?|\.[0-9]+)([$%(\-+\s])*$/', $value)) {
+            return 'number';
+        } elseif (strtotime($value)) {
+            return 'date';
+        } else {
+            return 'string';
+        }
+    }
+    protected function _getDisplayValue($value, $html = false, $date = false)
+    {
+        $type = $this->_getType($value);
+
+        if ($type === null) {
+            if ($html && $this->is_html) {
+                return ' ';
+            } else {
+                return null;
+            }
+        } elseif ($type === 'number') {
+            return $value;
+        } elseif ($type === 'date') {
+            if ($date) {
+                return date($date, strtotime($value));
+            } else {
+                return $value;
+            }
+        } elseif ($type === 'string') {
+            $decoded = utf8_decode($value);
+            if (mb_detect_encoding($decoded, 'UTF-8', true) === false) {
+                return $value;
+            }
+
+            return $decoded;
+        }
+    }
+
+    public function getValue($html = false, $date = false)
+    {
+        if ($html) {
+            $return = $this->_getDisplayValue($this->html_value, true, $date);
+
+            if ($this->is_html) {
+                return $return;
+            } else {
+                return htmlentities($return);
+            }
+        } else {
+            return $this->_getDisplayValue($this->filtered_value, false, $date);
+        }
+    }
+
+    public function getKeyCollapsed()
+    {
+        return trim(preg_replace(['/\s+/', '/[^a-zA-Z0-9_]*/'], ['_', ''], $this->key), '_');
+    }
+}
diff --git a/src/Reports/Types/AdoPivotReportType.php b/src/Reports/Types/AdoPivotReportType.php
new file mode 100644
index 00000000..c6b858c5
--- /dev/null
+++ b/src/Reports/Types/AdoPivotReportType.php
@@ -0,0 +1,191 @@
+options['Environment']][$report->options['Database']])) {
+            throw new \Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'");
+        }
+
+        //make sure the syntax highlighting is using the proper class
+        SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'";
+
+        //set a formatted query here for debugging.  It will be overwritten below after macros are substituted.
+        $report->options['Query_Formatted'] = "
".$report->raw_query."
"; + + $object = spyc_load($report->raw_query); + + $report->raw_query = []; + //if there are any included reports, add the report sql to the top + if (isset($report->options['Includes'])) { + $included_sql = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_sql .= trim($included_report->raw_query)."\n"; + } + if (strlen($included_sql) > 0) { + $report->raw_query[] = $included_sql; + } + } + + $report->raw_query[] = $object; + } + + public static function openConnection(&$report) + { + if (isset($report->conn)) { + return; + } + + $environments = PhpReports::$config['environments']; + $config = $environments[$report->options['Environment']][$report->options['Database']]; + + if (!($report->conn = ADONewConnection($config['uri']))) { + throw new \Exception('Could not connect to the database'); + } + } + + public static function closeConnection(&$report) + { + if (!isset($report->conn)) { + return; + } + if ($report->conn->IsConnected()) { + $report->conn->Close(); + } + unset($report->conn); + } + + public static function getVariableOptions($params, &$report) + { + $report->conn->SetFetchMode(ADODB_FETCH_NUM); + $query = 'SELECT DISTINCT '.$params['column'].' FROM '.$params['table']; + + if (isset($params['where'])) { + $query .= ' WHERE '.$params['where']; + } + + $macros = $report->macros; + foreach ($macros as $key => $value) { + if (is_array($value)) { + foreach ($value as $key2 => $value2) { + $value[$key2] = trim($value2); + } + $macros[$key] = $value; + } else { + $macros[$key] = $value; + } + + if ($value === 'ALL') { + $macros[$key.'_all'] = true; + } + } + + //add the config and environment settings as macros + $macros['config'] = PhpReports::$config; + $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; + + $result = $report->conn->Execute(PhpReports::renderString($query, $macros)); + + if (!$result) { + throw new \Exception("Unable to get variable options: ".$report->conn->ErrorMsg()); + } + + $options = array(); + + if (isset($params['all']) && $params['all']) { + $options[] = 'ALL'; + } + + while ($row = $result->FetchRow()) { + if ($result->FieldCount() > 1) { + $options[] = ['display' => $row[0], 'value' => $row[1]]; + } else { + $options[] = $row[0]; + } + } + + return $options; + } + + public static function run(&$report) + { + $report->conn->SetFetchMode(ADODB_FETCH_ASSOC); + $rows = []; + + $macros = $report->macros; + foreach ($macros as $key => $value) { + if (is_array($value)) { + $first = true; + foreach ($value as $key2 => $value2) { + $value[$key2] = mysql_real_escape_string(trim($value2)); + $first = false; + } + $macros[$key] = $value; + } else { + $macros[$key] = mysql_real_escape_string($value); + } + + if ($value === 'ALL') { + $macros[$key.'_all'] = true; + } + } + + //add the config and environment settings as macros + $macros['config'] = PhpReports::$config; + $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; + + $raw_sql = ""; + foreach ($report->raw_query as $qry) { + if (is_array($qry)) { + foreach ($qry as $key => $value) { + // TODO handle arrays better + if (!is_bool($value) && !is_array($value)) { + $qry[$key] = PhpReports::renderString($value, $macros); + } + } + //TODO This sux - need a class or something :-) + $raw_sql .= PivotTableSQL($report->conn, $qry['tables'], $qry['rows'], $qry['columns'], $qry['where'], $qry['orderBy'], $qry['limit'], $qry['agg_field'], $qry['agg_label'], $qry['agg_fun'], $qry['include_agg_field'], $qry['show_count']); + } else { + $raw_sql .= $qry; + } + } + + //expand macros in query + $sql = PhpReports::render($raw_sql, $macros); + + $report->options['Query'] = $sql; + + $report->options['Query_Formatted'] = SqlFormatter::format($sql); + + //split into individual queries and run each one, saving the last result + $queries = SqlFormatter::splitQuery($sql); + + foreach ($queries as $query) { + if (!is_array($query)) { + //skip empty queries + $query = trim($query); + if (!$query) { + continue; + } + + $result = $report->conn->Execute($query); + if (!$result) { + throw new \Exception("Query failed: ".$report->conn->ErrorMsg()); + } + + //if this query had an assert=empty flag and returned results, throw error + if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { + if ($result->GetAssoc()) { + throw new \Exception("Assert failed. Query did not return empty results."); + } + } + } + } + + return $result->GetArray(); + } +} diff --git a/src/Reports/Types/AdoReportType.php b/src/Reports/Types/AdoReportType.php new file mode 100644 index 00000000..1f082b60 --- /dev/null +++ b/src/Reports/Types/AdoReportType.php @@ -0,0 +1,173 @@ +options['Environment']][$report->options['Database']])) { + throw new \Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); + } + + //make sure the syntax highlighting is using the proper class + SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; + + //default host macro to mysql's host if it isn't defined elsewhere + //if(!isset($report->macros['host'])) $report->macros['host'] = $mysql['host']; + + //replace legacy shorthand macro format + foreach ($report->macros as $key => $value) { + $params = []; + if (isset($report->options['Variables'][$key])) { + $params = $report->options['Variables'][$key]; + } + + //macros shortcuts for arrays + if (isset($params['multiple']) && $params['multiple']) { + //allow {macro} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} + //this is shorthand for comma separated list + $report->raw_query = preg_replace('/([^\{])\{'.$key.'\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}\'{{ item }}\'{% endfor %}$2', $report->raw_query); + + //allow {(macro)} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} + //this is shorthand for quoted, comma separated list + $report->raw_query = preg_replace('/([^\{])\{\('.$key.'\)\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}(\'{{ item }}\'){% endfor %}$2', $report->raw_query); + } else { + //macros sortcuts for non-arrays + //allow {macro} instead of {{macro}} for legacy support + $report->raw_query = preg_replace('/([^\{])(\{'.$key.'+\})([^\}])/', '$1{$2}$3', $report->raw_query); + } + } + + //if there are any included reports, add the report sql to the top + if (isset($report->options['Includes'])) { + $included_sql = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_sql .= trim($included_report->raw_query)."\n"; + } + + $report->raw_query = $included_sql.$report->raw_query; + } + + //set a formatted query here for debugging. It will be overwritten below after macros are substituted. + $report->options['Query_Formatted'] = SqlFormatter::format($report->raw_query); + } + + public static function openConnection(&$report) + { + if (isset($report->conn)) { + return; + } + + $environments = PhpReports::$config['environments']; + $config = $environments[$report->options['Environment']][$report->options['Database']]; + + if (!($report->conn = ADONewConnection($config['uri']))) { + throw new \Exception('Could not connect to the database'); + } + } + + public static function closeConnection(&$report) + { + if (!isset($report->conn)) { + return; + } + if ($report->conn->IsConnected()) { + $report->conn->Close(); + } + unset($report->conn); + } + + public static function getVariableOptions($params, &$report) + { + $report->conn->SetFetchMode(ADODB_FETCH_NUM); + $query = 'SELECT DISTINCT '.$params['column'].' FROM '.$params['table']; + + if (isset($params['where'])) { + $query .= ' WHERE '.$params['where']; + } + + $result = $report->conn->Execute($query); + + if (!$result) { + throw new \Exception("Unable to get variable options: ".$report->conn->ErrorMsg()); + } + + $options = []; + + if (isset($params['all']) && $params['all']) { + $options[] = 'ALL'; + } + + while ($row = $result->FetchRow()) { + if ($result->FieldCount() > 1) { + $options[] = ['display' => $row[0], 'value' => $row[1]]; + } else { + $options[] = $row[0]; + } + } + + return $options; + } + + public static function run(&$report) + { + $report->conn->SetFetchMode(ADODB_FETCH_ASSOC); + $rows = []; + + $macros = $report->macros; + foreach ($macros as $key => $value) { + if (is_array($value)) { + $first = true; + foreach ($value as $key2 => $value2) { + $value[$key2] = mysql_real_escape_string(trim($value2)); + $first = false; + } + $macros[$key] = $value; + } else { + $macros[$key] = mysql_real_escape_string($value); + } + + if ($value === 'ALL') { + $macros[$key.'_all'] = true; + } + } + + //add the config and environment settings as macros + $macros['config'] = PhpReports::$config; + $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; + + //expand macros in query + $sql = PhpReports::render($report->raw_query, $macros); + + $report->options['Query'] = $sql; + + $report->options['Query_Formatted'] = SqlFormatter::format($sql); + + //split into individual queries and run each one, saving the last result + $queries = SqlFormatter::splitQuery($sql); + + foreach ($queries as $query) { + //skip empty queries + $query = trim($query); + if (!$query) { + continue; + } + + $result = $report->conn->Execute($query); + if (!$result) { + throw new \Exception("Query failed: ".$report->conn->ErrorMsg()); + } + + //if this query had an assert=empty flag and returned results, throw error + if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { + if ($result->GetAssoc()) { + throw new \Exception("Assert failed. Query did not return empty results."); + } + } + } + + return $result->GetArray(); + } +} diff --git a/src/Reports/Types/MongoReportType.php b/src/Reports/Types/MongoReportType.php new file mode 100644 index 00000000..bc47a8ad --- /dev/null +++ b/src/Reports/Types/MongoReportType.php @@ -0,0 +1,85 @@ +options['Environment']][$report->options['Database']])) { + throw new \Exception("No ".$report->options['Database']." database defined for environment '".$report->options['Environment']."'"); + } + + $mongo = $environments[$report->options['Environment']][$report->options['Database']]; + + //default host macro to mysql's host if it isn't defined elsewhere + if (!isset($report->macros['host'])) { + $report->macros['host'] = $mongo['host']; + } + + //if there are any included reports, add it to the top of the raw query + if (isset($report->options['Includes'])) { + $included_code = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_code .= trim($included_report->raw_query)."\n"; + } + + $report->raw_query = $included_code.$report->raw_query; + } + } + + public static function openConnection(&$report) + { + } + + public static function closeConnection(&$report) + { + } + + public static function run(&$report) + { + $eval = ''; + foreach ($report->macros as $key => $value) { + if (is_array($value)) { + $value = json_encode($value); + } else { + $value = '"'.addslashes($value).'"'; + } + + $eval .= 'var '.$key.' = '.$value.';'."\n"; + } + $eval .= $report->raw_query; + + $environments = PhpReports::$config['environments']; + $config = $environments[$report->options['Environment']][$report->options['Database']]; + + $mongo_database = isset($report->options['Mongodatabase']) ? $report->options['Mongodatabase'] : ''; + + //command without eval string + $command = 'mongo '.$config['host'].':'.$config['port'].'/'.$mongo_database.' --quiet --eval '; + + //easy to read formatted query + $report->options['Query_Formatted'] = '
+
$ '.$command.'"..."
'. + 'Eval String:'. + '
'.htmlentities($eval).'
+
'; + + //escape the eval string and add it to the command + $command .= escapeshellarg($eval); + $report->options['Query'] = '$ '.$command; + + //include stderr so we can capture shell errors (like "command mongo not found") + $result = shell_exec($command.' 2>&1'); + + $result = trim($result); + + $json = json_decode($result, true); + if ($json === NULL) { + throw new \Exception($result); + } + + return $json; + } +} diff --git a/src/Reports/Types/MysqlReportType.php b/src/Reports/Types/MysqlReportType.php new file mode 100644 index 00000000..44338100 --- /dev/null +++ b/src/Reports/Types/MysqlReportType.php @@ -0,0 +1,7 @@ +options['Environment']][$report->options['Database']])) { + throw new \Exception("No ".$report->options['Database']." info defined for environment '".$report->options['Environment']."'"); + } + + //make sure the syntax highlighting is using the proper class + \SqlFormatter::$pre_attributes = "class='prettyprint linenums lang-sql'"; + + //replace legacy shorthand macro format + foreach ($report->macros as $key => $value) { + if (isset($report->options['Variables'][$key])) { + $params = $report->options['Variables'][$key]; + } else { + $params = []; + } + + //macros shortcuts for arrays + if (isset($params['multiple']) && $params['multiple']) { + //allow {macro} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} + //this is shorthand for comma separated list + $report->raw_query = preg_replace('/([^\{])\{'.$key.'\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}\'{{ item }}\'{% endfor %}$2', $report->raw_query); + + //allow {(macro)} instead of {% for item in macro %}{% if not item.first %},{% endif %}{{ item.value }}{% endfor %} + //this is shorthand for quoted, comma separated list + $report->raw_query = preg_replace('/([^\{])\{\('.$key.'\)\}([^\}])/', '$1{% for item in '.$key.' %}{% if not loop.first %},{% endif %}(\'{{ item }}\'){% endfor %}$2', $report->raw_query); + } else { + //macros sortcuts for non-arrays + //allow {macro} instead of {{macro}} for legacy support + $report->raw_query = preg_replace('/([^\{])(\{'.$key.'+\})([^\}])/', '$1{$2}$3', $report->raw_query); + } + } + + //if there are any included reports, add the report sql to the top + if (isset($report->options['Includes'])) { + $included_sql = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_sql .= trim($included_report->raw_query)."\n"; + } + + $report->raw_query = $included_sql.$report->raw_query; + } + + //set a formatted query here for debugging. It will be overwritten below after macros are substituted. + $report->options['Query_Formatted'] = \SqlFormatter::format($report->raw_query); + } + + public static function openConnection(&$report) + { + if (isset($report->conn)) { + return; + } + + $environments = PhpReports::$config['environments']; + $config = $environments[$report->options['Environment']][$report->options['Database']]; + + if (isset($config['dsn'])) { + $dsn = $config['dsn']; + } else { + $host = $config['host']; + if (isset($report->options['access']) && $report->options['access'] === 'rw') { + if (isset($config['host_rw'])) { + $host = $config['host_rw']; + } + } + + $driver = isset($config['driver']) ? $config['driver'] : static::$default_driver; + + if (!$driver) { + throw new \Exception("Must specify database `driver` (e.g. 'mysql')"); + } + + $dsn = $driver.':host='.$host; + + if (isset($config['database'])) { + $dsn .= ';dbname='.$config['database']; + } + } + + //the default is to use a user with read only privileges + $username = $config['user']; + $password = $config['pass']; + + //if the report requires read/write privileges + if (isset($report->options['access']) && $report->options['access'] === 'rw') { + if (isset($config['user_rw'])) { + $username = $config['user_rw']; + } + if (isset($config['pass_rw'])) { + $password = $config['pass_rw']; + } + } + + $report->conn = new \PDO($dsn, $username, $password); + + $report->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + + public static function closeConnection(&$report) + { + if (!isset($report->conn)) { + return; + } + $report->conn = null; + unset($report->conn); + } + + public static function getVariableOptions($params, &$report) + { + $displayColumn = $params['column']; + if (isset($params['display'])) { + $displayColumn = $params['display']; + } + + $query = 'SELECT DISTINCT `'.$params['column'].'` as val, `'.$displayColumn.'` as disp FROM '.$params['table']; + + if (isset($params['where'])) { + $query .= ' WHERE '.$params['where']; + } + + if (isset($params['order']) && in_array($params['order'], ['ASC', 'DESC'])) { + $query .= ' ORDER BY '.$params['column'].' '.$params['order']; + } + + $result = $report->conn->query($query); + + $options = []; + + if (isset($params['all'])) { + $options[] = 'ALL'; + } + + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + $options[] = [ + 'value' => $row['val'], + 'display' => $row['disp'], + ]; + } + + return $options; + } + + public static function run(&$report) + { + $macros = $report->macros; + foreach ($macros as $key => $value) { + if (is_array($value)) { + $first = true; + foreach ($value as $key2 => $value2) { + $value[$key2] = $report->conn->quote(trim($value2)); + $value[$key2] = preg_replace("/(^'|'$)/", '', $value[$key2]); + $first = false; + } + $macros[$key] = $value; + } else { + $macros[$key] = $report->conn->quote($value); + $macros[$key] = preg_replace("/(^'|'$)/", '', $macros[$key]); + } + + if ($value === 'ALL') { + $macros[$key.'_all'] = true; + } + } + + //add the config and environment settings as macros + $macros['config'] = PhpReports::$config; + $macros['environment'] = PhpReports::$config['environments'][$report->options['Environment']]; + + //expand macros in query + $sql = PhpReports::render($report->raw_query, $macros); + + $report->options['Query'] = $sql; + + $report->options['Query_Formatted'] = \SqlFormatter::format($sql); + + //split into individual queries and run each one, saving the last result + $queries = \SqlFormatter::splitQuery($sql); + + $datasets = []; + + $explicit_datasets = preg_match('/--\s+@dataset(\s*=\s*|\s+)true/', $sql); + + foreach ($queries as $i => $query) { + $is_last = $i === count($queries)-1; + + //skip empty queries + $query = trim($query); + if (!$query) { + continue; + } + + $result = $report->conn->query($query); + + //if this query had an assert=empty flag and returned results, throw error + if (preg_match('/^--[\s+]assert[\s]*=[\s]*empty[\s]*\n/', $query)) { + if ($result->fetch(\PDO::FETCH_ASSOC)) { + throw new \Exception("Assert failed. Query did not return empty results."); + } + } + + // If this query should be included as a dataset + if ((!$explicit_datasets && $is_last) || preg_match('/--\s+@dataset(\s*=\s*|\s+)true/', $query)) { + $dataset = ['rows' => []]; + + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + $dataset['rows'][] = $row; + } + + // Get dataset title if it has one + if (preg_match('/--\s+@title(\s*=\s*|\s+)(.*)/', $query, $matches)) { + $dataset['title'] = $matches[2]; + } + + $datasets[] = $dataset; + } + } + + return $datasets; + } +} diff --git a/src/Reports/Types/PhpReportType.php b/src/Reports/Types/PhpReportType.php new file mode 100644 index 00000000..96e3ae0c --- /dev/null +++ b/src/Reports/Types/PhpReportType.php @@ -0,0 +1,101 @@ +raw_query = "report."\n".trim($report->raw_query); + + //if there are any included reports, add it to the top of the raw query + if (isset($report->options['Includes'])) { + $included_code = ''; + foreach ($report->options['Includes'] as &$included_report) { + $included_code .= "\n".trim($included_report->raw_query).""; + } + + if ($included_code) { + $included_code .= "\n"; + } + + $report->raw_query = $included_code.$report->raw_query; + + //make sure the raw query has a closing PHP tag at the end + //this makes sure it will play nice as an included report + if (!preg_match('/\?>\s*$/', $report->raw_query)) { + $report->raw_query .= "\n?>"; + } + } + } + + public static function openConnection(&$report) + { + } + + public static function closeConnection(&$report) + { + } + + public static function run(&$report) + { + $eval = "macros as $key => $value) { + $value = var_export($value, true); + + $eval .= "\n".'$'.$key.' = '.$value.';'; + } + $eval .= "\n?>".$report->raw_query; + + $config = PhpReports::$config; + + //store in both $database and $environment for backwards compatibility + $database = PhpReports::$config['environments'][$report->options['Environment']]; + $environment = $database; + + $report->options['Query'] = $report->raw_query; + + $parts = preg_split('/<\?php \/\*(BEGIN|END) (INCLUDED REPORT|REPORT MACROS)\*\/ \?>/', $eval); + $report->options['Query_Formatted'] = ''; + $code = htmlentities(trim(array_pop($parts))); + $linenum = 1; + foreach ($parts as $part) { + if (!trim($part)) { + continue; + } + + //get name of report + $name = preg_match("|//REPORT: ([^\n]+)\n|", $part, $matches); + + if (!$matches) { + $name = "Variables"; + } else { + $name = $matches[1]; + } + + $report->options['Query_Formatted'] .= '
'; + $report->options['Query_Formatted'] .= "
".htmlentities(trim($part))."
"; + $report->options['Query_Formatted'] .= "
"; + $linenum += count(explode("\n", trim($part))); + } + + $report->options['Query_Formatted'] .= '
'.$code.'
'; + + ob_start(); + ini_set('display_errors', 'Off'); + eval('?>'.$eval); + $result = ob_get_contents(); + ob_end_clean(); + ini_set('display_errors', 'On'); + + $result = trim($result); + + $json = json_decode($result, true); + if ($json === null) { + throw new \Exception($result); + } + + return $json; + } +} diff --git a/src/Reports/Types/Type.php b/src/Reports/Types/Type.php new file mode 100644 index 00000000..44d52d49 --- /dev/null +++ b/src/Reports/Types/Type.php @@ -0,0 +1,26 @@ + Date: Tue, 10 May 2016 21:40:45 -0300 Subject: [PATCH 13/19] Styling --- src/Reports/Formats/CsvReportFormat.php | 2 +- src/Reports/Formats/HtmlReportFormat.php | 8 +- src/Reports/Formats/JsonReportFormat.php | 16 +-- src/Reports/Formats/XmlReportFormat.php | 2 +- src/Reports/PhpReports.php | 138 ++++++----------------- src/Reports/Report.php | 6 +- src/Reports/Types/AdoPivotReportType.php | 2 +- 7 files changed, 56 insertions(+), 118 deletions(-) diff --git a/src/Reports/Formats/CsvReportFormat.php b/src/Reports/Formats/CsvReportFormat.php index 35224ec7..90a1c8c4 100644 --- a/src/Reports/Formats/CsvReportFormat.php +++ b/src/Reports/Formats/CsvReportFormat.php @@ -11,7 +11,7 @@ public static function display(&$report, &$request) //always use cache for CSV reports $report->use_cache = true; - $file_name = preg_replace(array('/[\s]+/', '/[^0-9a-zA-Z\-_\.]/'), ['_', ''], $report->options['Name']); + $file_name = preg_replace(['/[\s]+/', '/[^0-9a-zA-Z\-_\.]/'], ['_', ''], $report->options['Name']); header("Content-type: application/csv"); header("Content-Disposition: attachment; filename=".$file_name.".csv"); diff --git a/src/Reports/Formats/HtmlReportFormat.php b/src/Reports/Formats/HtmlReportFormat.php index e3202d28..61d37464 100644 --- a/src/Reports/Formats/HtmlReportFormat.php +++ b/src/Reports/Formats/HtmlReportFormat.php @@ -24,7 +24,7 @@ public static function display(&$report, &$request) } try { - $additional_vars = array(); + $additional_vars = []; if (isset($request->query['no_charts'])) { $additional_vars['no_charts'] = true; } @@ -36,12 +36,12 @@ public static function display(&$report, &$request) $template = 'html/blank_page'; } - $vars = array( + $vars = [ 'title' => $report->report, 'header' => '

There was an error running your report

', 'error' => $e->getMessage(), - 'content' => "

Report Query

".$report->options['Query_Formatted'], - ); + 'content' => "

Report Query

" . $report->options['Query_Formatted'], + ]; echo PhpReports::render($template, $vars); } diff --git a/src/Reports/Formats/JsonReportFormat.php b/src/Reports/Formats/JsonReportFormat.php index f572359b..cebe2604 100644 --- a/src/Reports/Formats/JsonReportFormat.php +++ b/src/Reports/Formats/JsonReportFormat.php @@ -1,6 +1,9 @@ options['DataSets']); - } - // If just a single dataset was specified, make it an array - elseif (!is_array($datasets)) { + } elseif (!is_array($datasets)) { + // If just a single dataset was specified, make it an array $datasets = explode(',', $datasets); } @@ -51,14 +53,14 @@ public static function display(&$report, &$request) public static function getDataSet($i, &$report) { - $dataset = array(); + $dataset = []; foreach ($report->options['DataSets'][$i] as $k => $v) { $dataset[$k] = $v; } - $rows = array(); + $rows = []; foreach ($dataset['rows'] as $i => $row) { - $tmp = array(); + $tmp = []; foreach ($row['values'] as $key => $value) { $tmp[$value->key] = $value->getValue(); } diff --git a/src/Reports/Formats/XmlReportFormat.php b/src/Reports/Formats/XmlReportFormat.php index 11ca8298..4f84e6ac 100644 --- a/src/Reports/Formats/XmlReportFormat.php +++ b/src/Reports/Formats/XmlReportFormat.php @@ -12,7 +12,7 @@ public static function display(&$report, &$request) header("Pragma: no-cache"); header("Expires: 0"); - $datasets = array(); + $datasets = []; $dataset_format = false; if (isset($_GET['datasets'])) { diff --git a/src/Reports/PhpReports.php b/src/Reports/PhpReports.php index 0b47391d..c8e3be12 100644 --- a/src/Reports/PhpReports.php +++ b/src/Reports/PhpReports.php @@ -42,15 +42,15 @@ public static function init($config = '../config/config.php') //the load order for templates is: "templates/local", "templates/default", "templates" //this means loading the template "html/report.twig" will load the local first and then the default //if you want to extend a default template from within a local template, you can do {% extends "default/html/report.twig" %} and it will fall back to the last loader - $template_dirs = array('../templates/default','../templates'); + $template_dirs = ['../templates/default', '../templates']; if (file_exists('../templates/local')) { array_unshift($template_dirs, '../templates/local'); } - $loader = new \Twig_Loader_Chain(array( + $loader = new \Twig_Loader_Chain([ new \Twig_Loader_Filesystem($template_dirs), new \Twig_Loader_String(), - )); + ]); self::$twig = new \Twig_Environment($loader); self::$twig->addFunction(new \Twig_SimpleFunction('dbdate', 'PhpReports::dbdate')); @@ -67,7 +67,7 @@ public static function init($config = '../config/config.php') self::$twig->addFilter('var_dump', new \Twig_Filter_Function('var_dump')); - self::$twig_string = new \Twig_Environment(new \Twig_Loader_String(), array('autoescape' => false)); + self::$twig_string = new \Twig_Environment(new \Twig_Loader_String(), ['autoescape' => false]); self::$twig_string->addFunction(new \Twig_SimpleFunction('sqlin', 'PhpReports::generateSqlIN')); \FileSystemCache::$cacheDir = self::$config['cacheDir']; @@ -291,18 +291,18 @@ public static function listDashboards() return strcmp($a['title'], $b['title']); }); - echo self::render('html/dashboard_list', array( + echo self::render('html/dashboard_list', [ 'dashboards' => $dashboards, - )); + ]); } public static function displayDashboard($dashboard) { $content = self::getDashboard($dashboard); - echo self::render('html/dashboard', array( + echo self::render('html/dashboard', [ 'dashboard' => $content, - )); + ]); } public static function getDashboards() @@ -331,7 +331,7 @@ public static function getDashboard($dashboard) public static function getRecentReports() { $recently_run = \FileSystemCache::retrieve(\FileSystemCache::generateCacheKey('recently_run')); - $recent = array(); + $recent = []; if ($recently_run !== false) { $i = 0; foreach ($recently_run as $report) { @@ -358,13 +358,13 @@ public static function getRecentReports() public static function getReportListJSON($reports = null) { if ($reports === null) { - $errors = array(); + $errors = []; $reports = self::getReports(self::$config['reportDir'].'/', $errors); } //weight by popular reports $recently_run = \FileSystemCache::retrieve(\FileSystemCache::generateCacheKey('recently_run')); - $popular = array(); + $popular = []; if ($recently_run !== false) { foreach ($recently_run as $report) { if (!isset($popular[$report])) { @@ -374,7 +374,7 @@ public static function getReportListJSON($reports = null) } } } - $parts = array(); + $parts = []; foreach ($reports as $report) { if ($report['is_dir'] && $report['children']) { @@ -410,11 +410,11 @@ public static function getReportListJSON($reports = null) $popularity = 0; } - $parts[] = json_encode(array( + $parts[] = json_encode([ 'name' => $report['Name'], 'url' => $report['url'], 'popularity' => $popularity, - )); + ]); } } @@ -423,7 +423,7 @@ public static function getReportListJSON($reports = null) protected static function getReportHeaders($report) { - $cacheKey = \FileSystemCache::generateCacheKey(array(self::$request->base, $report), 'report_headers'); + $cacheKey = \FileSystemCache::generateCacheKey([self::$request->base, $report], 'report_headers'); //check if report data is cached and newer than when the report file was created //the url parameter ?nocache will bypass this and not use cache @@ -446,9 +446,9 @@ protected static function getReportHeaders($report) $data['report'] = $report; $data['url'] = self::$request->base.'/report/html/?report='.$report; $data['is_dir'] = false; - $data['Id'] = str_replace(array('_', '-', '/', ' ', '.'), array('', '', '_', '-', '_'), trim($report, '/')); + $data['Id'] = str_replace(['_', '-', '/', ' ', '.'], ['', '', '_', '-', '_'], trim($report, '/')); if (!isset($data['Name'])) { - $data['Name'] = ucwords(str_replace(array('_', '-'), ' ', basename($report))); + $data['Name'] = ucwords(str_replace(['_', '-'], ' ', basename($report))); } //store parsed report in cache @@ -463,7 +463,7 @@ protected static function getReports($dir, &$errors = null) $base = self::$config['reportDir'].'/'; $reports = glob($dir.'*', GLOB_NOSORT); - $return = array(); + $return = []; foreach ($reports as $key => $report) { $title = $description = false; @@ -475,7 +475,7 @@ protected static function getReports($dir, &$errors = null) $description = file_get_contents($report.'/README.txt'); } - $id = str_replace(array('_', '-', '/', ' '), array('', '', '_', '-'), trim(substr($report, strlen($base)), '/')); + $id = str_replace(['_', '-', '/', ' '], ['', '', '_', '-'], trim(substr($report, strlen($base)), '/')); $children = self::getReports($report.'/', $errors); @@ -488,15 +488,15 @@ protected static function getReports($dir, &$errors = null) } } - $return[] = array( - 'Name' => ucwords(str_replace(array('_', '-'), ' ', basename($report))), + $return[] = [ + 'Name' => ucwords(str_replace(['_', '-'], ' ', basename($report))), 'Title' => $title, 'Id' => $id, 'Description' => $description, 'is_dir' => true, 'children' => $children, 'count' => $count, - ); + ]; } else { //files to skip if (strpos(basename($report), '.') === false) { @@ -515,12 +515,12 @@ protected static function getReports($dir, &$errors = null) $return[] = $data; } catch (\Exception $e) { if (!$errors) { - $errors = array(); + $errors = []; } - $errors[] = array( + $errors[] = [ 'report' => $name, 'exception' => $e, - ); + ]; } } } @@ -552,22 +552,22 @@ protected static function getReports($dir, &$errors = null) public static function emailReport() { if (!isset($_REQUEST['email']) || !filter_var($_REQUEST['email'], FILTER_VALIDATE_EMAIL)) { - echo json_encode(array('error' => 'Valid email address required')); + echo json_encode(['error' => 'Valid email address required']); return; } if (!isset($_REQUEST['url'])) { - echo json_encode(array('error' => 'Report url required')); + echo json_encode(['error' => 'Report url required']); return; } if (!isset(PhpReports::$config['mail_settings']['enabled']) || !PhpReports::$config['mail_settings']['enabled']) { - echo json_encode(array('error' => 'Email is disabled on this server')); + echo json_encode(['error' => 'Email is disabled on this server']); return; } if (!isset(PhpReports::$config['mail_settings']['from'])) { - echo json_encode(array('error' => 'Email settings have not been properly configured on this server')); + echo json_encode(['error' => 'Email settings have not been properly configured on this server']); return; } @@ -616,21 +616,21 @@ public static function emailReport() // Send the message $result = $mailer->send($message); } catch (\Exception $e) { - echo json_encode(array( + echo json_encode([ 'error' => $e->getMessage(), - )); + ]); return; } if ($result) { - echo json_encode(array( + echo json_encode([ 'success' => true, - )); + ]); } else { - echo json_encode(array( + echo json_encode([ 'error' => 'Failed to send email to requested recipient', - )); + ]); } } @@ -640,7 +640,7 @@ public static function emailReport() protected static function getMailTransport() { if (!isset(PhpReports::$config['mail_settings'])) { - PhpReports::$config['mail_settings'] = array(); + PhpReports::$config['mail_settings'] = []; } if (!isset(PhpReports::$config['mail_settings']['method'])) { PhpReports::$config['mail_settings']['method'] = 'mail'; @@ -679,70 +679,6 @@ protected static function getMailTransport() } } - /** - * Autoloader methods - */ - // public static function loader($className) - // { - // if (!isset(self::$loader_cache)) { - // self::buildLoaderCache(); - // } - - // if (isset(self::$loader_cache[$className])) { - // require_once self::$loader_cache[$className]; - - // return true; - // } else { - // return false; - // } - // } - // public static function buildLoaderCache() - // { - // self::load('classes/local'); - // self::load('classes', array('classes/local')); - // self::load('lib'); - // } - // public static function load($dir, $skip = array()) - // { - // $files = glob($dir.'/*.php'); - // $dirs = glob($dir.'/*', GLOB_ONLYDIR); - - // foreach ($files as $file) { - // //for file names same as class name - // $className = basename($file, '.php'); - // if (!isset(self::$loader_cache[$className])) { - // self::$loader_cache[$className] = $file; - // } - - // //for PEAR style: Path_To_Class.php - // $parts = explode('/', substr($file, 0, -4)); - // array_shift($parts); - // $className = implode('_', $parts); - // //if any of the directories in the path are lowercase, it isn't in PEAR format - // if (preg_match('/(^|_)[a-z]/', $className)) { - // continue; - // } - // if (!isset(self::$loader_cache[$className])) { - // self::$loader_cache[$className] = $file; - // } - // } - - // foreach ($dirs as $dir2) { - // //directories to skip - // if ($dir2[0] === '.') { - // continue; - // } - // if (in_array($dir2, $skip)) { - // continue; - // } - // if (in_array(basename($dir2), array('tests', 'test', 'example', 'examples', 'bin'))) { - // continue; - // } - - // self::load($dir2, $skip); - // } - // } - /** * A more lenient json_decode than the built-in PHP one. * It supports strict JSON as well as javascript syntax (i.e. unquoted/single quoted keys, single quoted values, trailing commmas) @@ -756,7 +692,7 @@ public static function json_decode($json, $assoc = false) $json = preg_replace_callback('/\'(([^\']|\\\\\')*)\'\s*:/', create_function('$matches', 'return "json_encode(stripslashes(\'$matches[1]\')).\':\'";'), $json); //remove any line breaks in the code - $json = str_replace(array("\n", "\r"), "", $json); + $json = str_replace(["\n", "\r"], "", $json); //replace non-quoted keys with double quoted keys $json = preg_replace('#(?
\{|\[|,)\s*(?(?:\w|_)+)\s*:#im', '$1"$2":', $json);
diff --git a/src/Reports/Report.php b/src/Reports/Report.php
index 4de1715d..11d577f7 100644
--- a/src/Reports/Report.php
+++ b/src/Reports/Report.php
@@ -103,7 +103,7 @@ public function getDatabase()
             }
         }
 
-        return array();
+        return [];
     }
 
     public function getEnvironment()
@@ -618,10 +618,10 @@ protected function prepareRows($dataset)
 
             $first = !$rows;
 
-            $rows[] = array(
+            $rows[] = [
                 'values' => $rowval,
                 'first' => $first,
-            );
+            ];
         }
 
         $this->options['DataSets'][$dataset]['rows'] = $rows;
diff --git a/src/Reports/Types/AdoPivotReportType.php b/src/Reports/Types/AdoPivotReportType.php
index c6b858c5..d2cde21f 100644
--- a/src/Reports/Types/AdoPivotReportType.php
+++ b/src/Reports/Types/AdoPivotReportType.php
@@ -94,7 +94,7 @@ public static function getVariableOptions($params, &$report)
             throw new \Exception("Unable to get variable options: ".$report->conn->ErrorMsg());
         }
 
-        $options = array();
+        $options = [];
 
         if (isset($params['all']) && $params['all']) {
             $options[] = 'ALL';

From 07a977cc7cbfd88a8fac4e0e08d53d6a75dc1016 Mon Sep 17 00:00:00 2001
From: Fede Isas 
Date: Tue, 10 May 2016 22:06:31 -0300
Subject: [PATCH 14/19] Dont run init() method in class file

---
 public/index.php           | 2 ++
 src/Reports/PhpReports.php | 2 --
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/index.php b/public/index.php
index 27d63df9..0fd774d0 100644
--- a/public/index.php
+++ b/public/index.php
@@ -103,4 +103,6 @@
 Flight::set('flight.handle_errors', false);
 Flight::set('flight.log_errors', true);
 
+PhpReports::init();
+
 Flight::start();
diff --git a/src/Reports/PhpReports.php b/src/Reports/PhpReports.php
index c8e3be12..cbdce859 100644
--- a/src/Reports/PhpReports.php
+++ b/src/Reports/PhpReports.php
@@ -716,5 +716,3 @@ protected static function urlDownload($url)
         return $output;
     }
 }
-
-PhpReports::init();

From ae41effa53c187ffa9669eb9deede76fbc2f8fe5 Mon Sep 17 00:00:00 2001
From: Fede Isas 
Date: Tue, 10 May 2016 23:24:51 -0300
Subject: [PATCH 15/19] Cleaner routes

---
 public/index.php           | 34 +++++++++++++++++++++-------------
 src/Reports/PhpReports.php | 15 ++++++++-------
 2 files changed, 29 insertions(+), 20 deletions(-)

diff --git a/public/index.php b/public/index.php
index 0fd774d0..6e79cefa 100644
--- a/public/index.php
+++ b/public/index.php
@@ -54,24 +54,25 @@
     });
 }
 
-Flight::route('/', function () {
+Flight::route('GET /', function () {
     PhpReports::listReports();
 });
 
-Flight::route('/dashboards', function () {
+Flight::route('GET /dashboards', function () {
     PhpReports::listDashboards();
 });
 
-Flight::route('/dashboard/@name', function ($name) {
+Flight::route('GET /dashboard/@name', function ($name) {
     PhpReports::displayDashboard($name);
 });
 
 //JSON list of reports (used for typeahead search)
-Flight::route('/report-list-json', function () {
-    header("Content-Type: application/json");
-    header("Cache-Control: max-age=3600");
-
-    echo PhpReports::getReportListJSON();
+Flight::route('GET /report-list-json', function () {
+    $reports = PhpReports::getReportList();
+    Flight::response()->header('Cache-Control', 'max-age=86400, public');
+    Flight::response()->header('Pragma', '');
+    Flight::etag(substr(md5(serialize($reports)), 0, 15));
+    Flight::json($reports);
 });
 
 //if no report format is specified, default to html
@@ -88,12 +89,19 @@
     PhpReports::editReport($_REQUEST['report']);
 });
 
-Flight::route('/set-environment', function () {
-    header("Content-Type: application/json");
-    $_SESSION['environment'] = $_REQUEST['environment'];
+Flight::route('GET|POST /set-environment', function () {
+    $request = Flight::request();
+    $environment = array_filter([
+        array_key_exists('environment', $request->query->getData()) ? $request->query['environment'] : null,
+        array_key_exists('environment', $request->data->getData()) ? $request->data['environment'] : null
+    ]);
 
-    echo json_encode(['status' => 'OK']);
-});
+    $environment = array_pop($environment);
+
+    $_SESSION['environment'] = $environment;
+
+    Flight::json(['status' => 'OK']);
+}, true);
 
 //email report
 Flight::route('/email', function () {
diff --git a/src/Reports/PhpReports.php b/src/Reports/PhpReports.php
index cbdce859..dc17b8d0 100644
--- a/src/Reports/PhpReports.php
+++ b/src/Reports/PhpReports.php
@@ -355,11 +355,12 @@ public static function getRecentReports()
 
         return array_values($recent);
     }
-    public static function getReportListJSON($reports = null)
+
+    public static function getReportList($reports = null)
     {
         if ($reports === null) {
             $errors = [];
-            $reports = self::getReports(self::$config['reportDir'].'/', $errors);
+            $reports = self::getReports(self::$config['reportDir'] . '/', $errors);
         }
 
         //weight by popular reports
@@ -383,8 +384,8 @@ public static function getReportListJSON($reports = null)
                     continue;
                 }
 
-                $part = trim(self::getReportListJSON($report['children']), '[],');
-                if ($part) {
+                $part = self::getReportList($report['children']);
+                if (!empty($part)) {
                     $parts[] = $part;
                 }
             } else {
@@ -410,15 +411,15 @@ public static function getReportListJSON($reports = null)
                     $popularity = 0;
                 }
 
-                $parts[] = json_encode([
+                $parts[] = [
                     'name' => $report['Name'],
                     'url' => $report['url'],
                     'popularity' => $popularity,
-                ]);
+                ];
             }
         }
 
-        return '['.trim(implode(',', $parts), ',').']';
+        return $parts;
     }
 
     protected static function getReportHeaders($report)

From b53ca6af3403191d482293ec6ab4d7973e4ff506 Mon Sep 17 00:00:00 2001
From: Fede Isas 
Date: Tue, 10 May 2016 23:56:35 -0300
Subject: [PATCH 16/19] Typehinting, docblocks

---
 src/Reports/Formats/ChartReportFormat.php |  8 +++++-
 src/Reports/Formats/CsvReportFormat.php   | 17 ++++++++-----
 src/Reports/Formats/DebugReportFormat.php |  5 +++-
 src/Reports/Formats/FormatInterface.php   |  5 +++-
 src/Reports/Formats/HtmlReportFormat.php  |  4 ++-
 src/Reports/Formats/JsonReportFormat.php  | 29 ++++++++++++---------
 src/Reports/Formats/RawReportFormat.php   |  3 ++-
 src/Reports/Formats/SqlReportFormat.php   |  5 +++-
 src/Reports/Formats/TableReportFormat.php |  5 +++-
 src/Reports/Formats/TextReportFormat.php  | 14 ++++++----
 src/Reports/Formats/XlsReportBase.php     | 31 +++++++++++++----------
 src/Reports/Formats/XlsReportFormat.php   |  4 ++-
 src/Reports/Formats/XlsxReportFormat.php  |  4 ++-
 src/Reports/Formats/XmlReportFormat.php   | 15 ++++++-----
 src/Reports/Headers/HeaderBase.php        |  6 ++++-
 src/Reports/Headers/OptionsHeader.php     |  6 +++++
 src/Reports/Headers/RollupHeader.php      |  3 +++
 src/Reports/Headers/VariableHeader.php    |  3 +++
 18 files changed, 114 insertions(+), 53 deletions(-)

diff --git a/src/Reports/Formats/ChartReportFormat.php b/src/Reports/Formats/ChartReportFormat.php
index c58c2529..ec90ba5d 100644
--- a/src/Reports/Formats/ChartReportFormat.php
+++ b/src/Reports/Formats/ChartReportFormat.php
@@ -1,9 +1,15 @@
 options['has_charts']) {
             return;
diff --git a/src/Reports/Formats/CsvReportFormat.php b/src/Reports/Formats/CsvReportFormat.php
index 90a1c8c4..4fe6bd86 100644
--- a/src/Reports/Formats/CsvReportFormat.php
+++ b/src/Reports/Formats/CsvReportFormat.php
@@ -1,12 +1,15 @@
 use_cache = true;
@@ -14,20 +17,20 @@ public static function display(&$report, &$request)
         $file_name = preg_replace(['/[\s]+/', '/[^0-9a-zA-Z\-_\.]/'], ['_', ''], $report->options['Name']);
 
         header("Content-type: application/csv");
-        header("Content-Disposition: attachment; filename=".$file_name.".csv");
+        header("Content-Disposition: attachment; filename=" . $file_name . ".csv");
         header("Pragma: no-cache");
         header("Expires: 0");
 
-        $i = 0;
+        $datasetIndex = 0;
         if (isset($_GET['dataset'])) {
-            $i = $_GET['dataset'];
+            $datasetIndex = $_GET['dataset'];
         } elseif (isset($report->options['default_dataset'])) {
-            $i = $report->options['default_dataset'];
+            $datasetIndex = $report->options['default_dataset'];
         }
-        $i = intval($i);
+        $datasetIndex = intval($datasetIndex);
 
         $data = $report->renderReportPage('csv/report', [
-            'dataset' => $i,
+            'dataset' => $datasetIndex,
         ]);
 
         if (trim($data)) {
diff --git a/src/Reports/Formats/DebugReportFormat.php b/src/Reports/Formats/DebugReportFormat.php
index 23b36b9d..90988f95 100644
--- a/src/Reports/Formats/DebugReportFormat.php
+++ b/src/Reports/Formats/DebugReportFormat.php
@@ -1,12 +1,15 @@
 async = !isset($request->query['content_only']);
diff --git a/src/Reports/Formats/JsonReportFormat.php b/src/Reports/Formats/JsonReportFormat.php
index cebe2604..33c876b7 100644
--- a/src/Reports/Formats/JsonReportFormat.php
+++ b/src/Reports/Formats/JsonReportFormat.php
@@ -1,10 +1,15 @@
 options['default_dataset'])) {
-                $i = $report->options['default_dataset'];
+                $datasetIndex = $report->options['default_dataset'];
             }
-            $i = intval($i);
+            $datasetIndex = intval($datasetIndex);
 
-            $dataset = self::getDataSet($i, $report);
+            $dataset = self::getDataSet($datasetIndex, $report);
             $result = $dataset['rows'];
         }
 
@@ -51,17 +56,17 @@ public static function display(&$report, &$request)
         }
     }
 
-    public static function getDataSet($i, &$report)
+    public static function getDataSet($datasetIndex, &$report)
     {
         $dataset = [];
-        foreach ($report->options['DataSets'][$i] as $k => $v) {
+        foreach ($report->options['DataSets'][$datasetIndex] as $k => $v) {
             $dataset[$k] = $v;
         }
 
         $rows = [];
-        foreach ($dataset['rows'] as $i => $row) {
+        foreach ($dataset['rows'] as $datasetIndex => $row) {
             $tmp = [];
-            foreach ($row['values'] as $key => $value) {
+            foreach ($row['values'] as $value) {
                 $tmp[$value->key] = $value->getValue();
             }
             $rows[] = $tmp;
diff --git a/src/Reports/Formats/RawReportFormat.php b/src/Reports/Formats/RawReportFormat.php
index 6b286eb7..eb030f3b 100644
--- a/src/Reports/Formats/RawReportFormat.php
+++ b/src/Reports/Formats/RawReportFormat.php
@@ -2,13 +2,14 @@
 namespace PhpReports\Formats;
 
 use PhpReports\Report;
+use flight\net\Request;
 
 class RawReportFormat extends Format implements FormatInterface
 {
     /**
      * @{inheritDoc}
      */
-    public static function display(&$report, &$request)
+    public static function display(Report &$report, Request &$request = null)
     {
         header("Content-type: text/plain");
         header("Pragma: no-cache");
diff --git a/src/Reports/Formats/SqlReportFormat.php b/src/Reports/Formats/SqlReportFormat.php
index c2850244..5a7315fb 100644
--- a/src/Reports/Formats/SqlReportFormat.php
+++ b/src/Reports/Formats/SqlReportFormat.php
@@ -1,12 +1,15 @@
 options['inline_email'] = true;
         $report->use_cache = true;
diff --git a/src/Reports/Formats/TextReportFormat.php b/src/Reports/Formats/TextReportFormat.php
index 220ac288..845d7da4 100644
--- a/src/Reports/Formats/TextReportFormat.php
+++ b/src/Reports/Formats/TextReportFormat.php
@@ -1,12 +1,15 @@
 getValue();
 
                 $length = strlen($value);
+                // get largest result size
                 if ($length > $sizes[$key]) {
                     $sizes[$key] = $length;
-                } // get largest result size
+                }
             }
         }
 
         //top of output
         foreach ($sizes as $length) {
-            echo "+".str_pad("", $length + 2, "-");
+            echo "+" .str_pad("", $length + 2, "-");
         }
         echo "+\n";
 
@@ -83,7 +87,7 @@ protected static function displayDataSet($dataset)
 
         //line under column names
         foreach ($sizes as $length) {
-            echo "+".str_pad("", $length + 2, "-");
+            echo "+" . str_pad("", $length + 2, "-");
         }
         echo "+\n";
 
@@ -101,7 +105,7 @@ protected static function displayDataSet($dataset)
 
         //bottom of output
         foreach ($sizes as $length) {
-            echo "+".str_pad("", $length + 2, "-");
+            echo "+" . str_pad("", $length + 2, "-");
         }
         echo "+\n";
     }
diff --git a/src/Reports/Formats/XlsReportBase.php b/src/Reports/Formats/XlsReportBase.php
index b8de3eaf..1b02ce4f 100644
--- a/src/Reports/Formats/XlsReportBase.php
+++ b/src/Reports/Formats/XlsReportBase.php
@@ -2,27 +2,32 @@
 namespace PhpReports\Formats;
 
 use PHPExcel;
+use PhpReports\Report;
 
 abstract class XlsReportBase extends Format implements FormatInterface
 {
-    private static function columnLetter($c)
+    /**
+     * @param int $columnLetter
+     * @return string $letter
+     */
+    private static function columnLetter($columnLetter)
     {
-        $c = intval($c);
-        if ($c <= 0) {
+        $columnLetter = intval($columnLetter);
+        if ($columnLetter <= 0) {
             return '';
         }
         $letter = '';
 
-        while ($c != 0) {
-            $p = ($c - 1) % 26;
-            $c = intval(($c - $p) / 26);
-            $letter = chr(65 + $p).$letter;
+        while ($columnLetter != 0) {
+            $page = ($columnLetter - 1) % 26;
+            $columnLetter = intval(($columnLetter - $page) / 26);
+            $letter = chr(65 + $page) . $letter;
         }
 
         return $letter;
     }
 
-    public static function getExcelRepresantation(&$report)
+    public static function getExcelRepresantation(Report &$report)
     {
         // Create new PHPExcel object
         $objPHPExcel = new PHPExcel();
@@ -34,9 +39,9 @@ public static function getExcelRepresantation(&$report)
                                      ->setSubject("")
                                      ->setDescription("");
 
-        foreach ($report->options['DataSets'] as $i => $dataset) {
-            $objPHPExcel->createSheet($i);
-            self::addSheet($objPHPExcel, $dataset, $i);
+        foreach ($report->options['DataSets'] as $datasetIndex => $dataset) {
+            $objPHPExcel->createSheet($datasetIndex);
+            self::addSheet($objPHPExcel, $dataset, $datasetIndex);
         }
 
         // Set the active sheet to the first one
@@ -68,8 +73,8 @@ public static function addSheet($objPHPExcel, $dataset, $i)
 
         $objPHPExcel->setActiveSheetIndex($i)->fromArray($rows, null, 'A1');
         $objPHPExcel->getActiveSheet()->setAutoFilter('A1:'.self::columnLetter($cols).count($rows));
-        for ($a = 1; $a <= $cols; $a++) {
-            $objPHPExcel->getActiveSheet()->getColumnDimension(self::columnLetter($a))->setAutoSize(true);
+        for ($columnLeter = 1; $columnLeter <= $cols; $columnLeter++) {
+            $objPHPExcel->getActiveSheet()->getColumnDimension(self::columnLetter($columnLeter))->setAutoSize(true);
         }
 
         if (isset($dataset['title'])) {
diff --git a/src/Reports/Formats/XlsReportFormat.php b/src/Reports/Formats/XlsReportFormat.php
index 622352c3..8d8ff162 100644
--- a/src/Reports/Formats/XlsReportFormat.php
+++ b/src/Reports/Formats/XlsReportFormat.php
@@ -1,6 +1,8 @@
 options['Name']);
diff --git a/src/Reports/Formats/XlsxReportFormat.php b/src/Reports/Formats/XlsxReportFormat.php
index 793f4ff8..d276b95c 100644
--- a/src/Reports/Formats/XlsxReportFormat.php
+++ b/src/Reports/Formats/XlsxReportFormat.php
@@ -1,6 +1,8 @@
 options['Name']);
diff --git a/src/Reports/Formats/XmlReportFormat.php b/src/Reports/Formats/XmlReportFormat.php
index 4f84e6ac..de13f578 100644
--- a/src/Reports/Formats/XmlReportFormat.php
+++ b/src/Reports/Formats/XmlReportFormat.php
@@ -1,12 +1,15 @@
 options['default_dataset'])) {
-                $i = $report->options['default_dataset'];
+                $datasetIndex = $report->options['default_dataset'];
             }
-            $i = intval($i);
+            $datasetIndex = intval($datasetIndex);
 
-            $datasets = [$i];
+            $datasets = [$datasetIndex];
         }
 
         echo $report->renderReportPage('xml/report', [
diff --git a/src/Reports/Headers/HeaderBase.php b/src/Reports/Headers/HeaderBase.php
index 1f17dc1d..09a1c243 100644
--- a/src/Reports/Headers/HeaderBase.php
+++ b/src/Reports/Headers/HeaderBase.php
@@ -2,12 +2,16 @@
 namespace PhpReports\Headers;
 
 use PhpReports\PhpReports;
+use PhpReports\Report;
 
 class HeaderBase
 {
+    /**
+     * @var array
+     */
     protected static $validation = [];
 
-    public static function parse($key, $value, &$report)
+    public static function parse($key, $value, Report &$report)
     {
         $params = null;
 
diff --git a/src/Reports/Headers/OptionsHeader.php b/src/Reports/Headers/OptionsHeader.php
index 007a711e..5d1002f6 100644
--- a/src/Reports/Headers/OptionsHeader.php
+++ b/src/Reports/Headers/OptionsHeader.php
@@ -3,6 +3,9 @@
 
 class OptionsHeader extends HeaderBase
 {
+    /**
+     * @var array
+     */
     public static $validation = [
         'limit' => [
             'type' => 'number',
@@ -78,6 +81,9 @@ class OptionsHeader extends HeaderBase
         ],
     ];
 
+    /**
+     * @{inheritDoc}
+     */
     public static function init($params, &$report)
     {
         //legacy support for the 'ttl' cache parameter
diff --git a/src/Reports/Headers/RollupHeader.php b/src/Reports/Headers/RollupHeader.php
index c315195f..ec2b46a5 100644
--- a/src/Reports/Headers/RollupHeader.php
+++ b/src/Reports/Headers/RollupHeader.php
@@ -6,6 +6,9 @@
 
 class RollupHeader extends HeaderBase
 {
+    /**
+     * @var array
+     */
     public static $validation = [
         'columns' => [
             'required' => true,
diff --git a/src/Reports/Headers/VariableHeader.php b/src/Reports/Headers/VariableHeader.php
index 6e6165d9..e46a90b1 100644
--- a/src/Reports/Headers/VariableHeader.php
+++ b/src/Reports/Headers/VariableHeader.php
@@ -3,6 +3,9 @@
 
 class VariableHeader extends HeaderBase
 {
+    /**
+     * @var array
+     */
     public static $validation = [
         'name' => [
             'required' => true,

From 305de01113420957c286b4e777d9785d4c30dc00 Mon Sep 17 00:00:00 2001
From: Fede Isas 
Date: Wed, 11 May 2016 00:01:42 -0300
Subject: [PATCH 17/19] Fix

---
 src/Reports/PhpReports.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Reports/PhpReports.php b/src/Reports/PhpReports.php
index dc17b8d0..e091fb3a 100644
--- a/src/Reports/PhpReports.php
+++ b/src/Reports/PhpReports.php
@@ -173,7 +173,7 @@ public static function render($template, $macros)
     {
         $default = [
             'base' => self::$request->base,
-            'report_list_url' => self::$request->base.'/',
+            'report_list_url' => self::$request->base . '/',
             'request' => self::$request,
             'querystring' => (array_key_exists('QUERY_STRING', $_SERVER) ? $_SERVER['QUERY_STRING'] : null),
             'config' => self::$config,

From 69c549190c04583ddf452d7e1d8b86fef9a0296b Mon Sep 17 00:00:00 2001
From: Fede Isas 
Date: Wed, 11 May 2016 00:07:04 -0300
Subject: [PATCH 18/19] Doc

---
 src/Reports/PhpReports.php | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/Reports/PhpReports.php b/src/Reports/PhpReports.php
index e091fb3a..7ad49029 100644
--- a/src/Reports/PhpReports.php
+++ b/src/Reports/PhpReports.php
@@ -683,6 +683,9 @@ protected static function getMailTransport()
     /**
      * A more lenient json_decode than the built-in PHP one.
      * It supports strict JSON as well as javascript syntax (i.e. unquoted/single quoted keys, single quoted values, trailing commmas)
+     * @param string $json
+     * @param boolean $assoc
+     * @return array
      */
     public static function json_decode($json, $assoc = false)
     {
@@ -704,6 +707,10 @@ public static function json_decode($json, $assoc = false)
         return json_decode($json, $assoc);
     }
 
+    /**
+     * @param string $url
+     * @return string $output
+     */
     protected static function urlDownload($url)
     {
         $ch = curl_init();

From 5ad7aa967d9c87d3970cf1573fb624095ba4a9b5 Mon Sep 17 00:00:00 2001
From: Fede Isas 
Date: Wed, 11 May 2016 00:18:10 -0300
Subject: [PATCH 19/19] Docblock

---
 src/Reports/ReportValue.php | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/Reports/ReportValue.php b/src/Reports/ReportValue.php
index 910f6542..6e7b5e3c 100644
--- a/src/Reports/ReportValue.php
+++ b/src/Reports/ReportValue.php
@@ -111,6 +111,9 @@ public function getValue($html = false, $date = false)
         }
     }
 
+    /**
+     * @return string
+     */
     public function getKeyCollapsed()
     {
         return trim(preg_replace(['/\s+/', '/[^a-zA-Z0-9_]*/'], ['_', ''], $this->key), '_');