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/.gitignore b/.gitignore index 402046fa..f3750b81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .idea/ config/config.php reports/ +!src/Reports/ +dashboards/ cache/ classes/local/*.php templates/local 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 deleted file mode 100644 index 58bbabf9..00000000 --- a/classes/filters/barFilter.php +++ /dev/null @@ -1,13 +0,0 @@ -getValue()/max($report->options['Values'][$value->key]))); - - $value->setValue("
"."".$value->getValue(true)."",true); - - return $value; - } -} diff --git a/classes/filters/classFilter.php b/classes/filters/classFilter.php deleted file mode 100644 index 1e5029f3..00000000 --- a/classes/filters/classFilter.php +++ /dev/null @@ -1,8 +0,0 @@ -addClass($options['class']); - - return $value; - } -} diff --git a/classes/filters/dateFilter.php b/classes/filters/dateFilter.php deleted file mode 100644 index 67db8b75..00000000 --- a/classes/filters/dateFilter.php +++ /dev/null @@ -1,26 +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 9f2918a9..00000000 --- a/classes/filters/drilldownFilter.php +++ /dev/null @@ -1,89 +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 = 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); - } -} diff --git a/classes/filters/geoipFilter.php b/classes/filters/geoipFilter.php deleted file mode 100644 index eae50187..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 2c54b80f..00000000 --- a/classes/filters/hideFilter.php +++ /dev/null @@ -1,6 +0,0 @@ -is_html = true; - return $value; - } -} diff --git a/classes/filters/imgsizeFilter.php b/classes/filters/imgsizeFilter.php deleted file mode 100644 index abe094e1..00000000 --- a/classes/filters/imgsizeFilter.php +++ /dev/null @@ -1,17 +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 da0ce690..00000000 --- a/classes/filters/linkFilter.php +++ /dev/null @@ -1,16 +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/paddingFilter.php b/classes/filters/paddingFilter.php deleted file mode 100644 index 8c5a3225..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 f82e2981..00000000 --- a/classes/filters/preFilter.php +++ /dev/null @@ -1,8 +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 e6fe44d0..00000000 --- a/classes/filters/twigFilter.php +++ /dev/null @@ -1,18 +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 e9569725..00000000 --- a/classes/headers/ChartHeader.php +++ /dev/null @@ -1,404 +0,0 @@ -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); - } -} diff --git a/classes/headers/ColumnsHeader.php b/classes/headers/ColumnsHeader.php deleted file mode 100644 index e01420f3..00000000 --- a/classes/headers/ColumnsHeader.php +++ /dev/null @@ -1,96 +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 = 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 - ); - } -} diff --git a/classes/headers/FilterHeader.php b/classes/headers/FilterHeader.php deleted file mode 100644 index f85de5c8..00000000 --- a/classes/headers/FilterHeader.php +++ /dev/null @@ -1,48 +0,0 @@ -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() - ); - } -} diff --git a/classes/headers/FormattingHeader.php b/classes/headers/FormattingHeader.php deleted file mode 100644 index 37659177..00000000 --- a/classes/headers/FormattingHeader.php +++ /dev/null @@ -1,178 +0,0 @@ -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; - } - } - } -} diff --git a/classes/headers/IncludeHeader.php b/classes/headers/IncludeHeader.php deleted file mode 100644 index 7f2ce5ab..00000000 --- a/classes/headers/IncludeHeader.php +++ /dev/null @@ -1,47 +0,0 @@ -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 - ); - } -} diff --git a/classes/headers/InfoHeader.php b/classes/headers/InfoHeader.php deleted file mode 100644 index e973e842..00000000 --- a/classes/headers/InfoHeader.php +++ /dev/null @@ -1,55 +0,0 @@ -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; - } -} diff --git a/classes/headers/OptionsHeader.php b/classes/headers/OptionsHeader.php deleted file mode 100644 index f46fb18a..00000000 --- a/classes/headers/OptionsHeader.php +++ /dev/null @@ -1,142 +0,0 @@ -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; - } -} diff --git a/classes/headers/RollupHeader.php b/classes/headers/RollupHeader.php deleted file mode 100644 index 3b89ab1c..00000000 --- a/classes/headers/RollupHeader.php +++ /dev/null @@ -1,125 +0,0 @@ -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']])); - - $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 279ab1a4..00000000 --- a/classes/headers/VariableHeader.php +++ /dev/null @@ -1,219 +0,0 @@ -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); - } - } - } -} diff --git a/classes/headers/deprecated/CacheHeader.php b/classes/headers/deprecated/CacheHeader.php deleted file mode 100644 index fdc45248..00000000 --- a/classes/headers/deprecated/CacheHeader.php +++ /dev/null @@ -1,23 +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 544427cb..00000000 --- a/classes/headers/deprecated/CautionHeader.php +++ /dev/null @@ -1,23 +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 83c7416d..00000000 --- a/classes/headers/deprecated/ColumnHeader.php +++ /dev/null @@ -1,8 +0,0 @@ -$value - ); - } -} diff --git a/classes/headers/deprecated/DatabaseHeader.php b/classes/headers/deprecated/DatabaseHeader.php deleted file mode 100644 index caf6fe61..00000000 --- a/classes/headers/deprecated/DatabaseHeader.php +++ /dev/null @@ -1,14 +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 57eea619..00000000 --- a/classes/headers/deprecated/DescriptionHeader.php +++ /dev/null @@ -1,14 +0,0 @@ -$value - ); - } -} diff --git a/classes/headers/deprecated/DetailHeader.php b/classes/headers/deprecated/DetailHeader.php deleted file mode 100644 index 817ec787..00000000 --- a/classes/headers/deprecated/DetailHeader.php +++ /dev/null @@ -1,73 +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 75ea23d7..00000000 --- a/classes/headers/deprecated/MongodatabaseHeader.php +++ /dev/null @@ -1,14 +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 b5cdc50b..00000000 --- a/classes/headers/deprecated/NameHeader.php +++ /dev/null @@ -1,14 +0,0 @@ -$value - ); - } -} diff --git a/classes/headers/deprecated/NoteHeader.php b/classes/headers/deprecated/NoteHeader.php deleted file mode 100644 index bb0452b0..00000000 --- a/classes/headers/deprecated/NoteHeader.php +++ /dev/null @@ -1,14 +0,0 @@ -$value - ); - } -} diff --git a/classes/headers/deprecated/OptionHeader.php b/classes/headers/deprecated/OptionHeader.php deleted file mode 100644 index 68ed4064..00000000 --- a/classes/headers/deprecated/OptionHeader.php +++ /dev/null @@ -1,8 +0,0 @@ -$value - ); - } -} diff --git a/classes/headers/deprecated/TotalHeader.php b/classes/headers/deprecated/TotalHeader.php deleted file mode 100644 index b2e3474e..00000000 --- a/classes/headers/deprecated/TotalHeader.php +++ /dev/null @@ -1,6 +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 ad004b18..00000000 --- a/classes/headers/deprecated/TypeHeader.php +++ /dev/null @@ -1,14 +0,0 @@ -$value - ); - } -} diff --git a/classes/headers/deprecated/ValueHeader.php b/classes/headers/deprecated/ValueHeader.php deleted file mode 100644 index a2a9b689..00000000 --- a/classes/headers/deprecated/ValueHeader.php +++ /dev/null @@ -1,42 +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 b018af47..00000000 --- a/classes/report_formats/ChartReportFormat.php +++ /dev/null @@ -1,13 +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 61976b5a..00000000 --- a/classes/report_formats/CsvReportFormat.php +++ /dev/null @@ -1,25 +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 001c51a6..00000000 --- a/classes/report_formats/DebugReportFormat.php +++ /dev/null @@ -1,22 +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 0d889d32..00000000 --- a/classes/report_formats/HtmlReportFormat.php +++ /dev/null @@ -1,39 +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 e700dfa8..00000000 --- a/classes/report_formats/JsonReportFormat.php +++ /dev/null @@ -1,65 +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 - 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']; - } - - 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 cfd9d50d..00000000 --- a/classes/report_formats/RawReportFormat.php +++ /dev/null @@ -1,17 +0,0 @@ -renderReportPage('sql/report'); - } -} diff --git a/classes/report_formats/TableReportFormat.php b/classes/report_formats/TableReportFormat.php deleted file mode 100644 index c132a983..00000000 --- a/classes/report_formats/TableReportFormat.php +++ /dev/null @@ -1,16 +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 8ce8d2ad..00000000 --- a/classes/report_formats/TextReportFormat.php +++ /dev/null @@ -1,92 +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 720e40b2..00000000 --- a/classes/report_formats/XlsReportBase.php +++ /dev/null @@ -1,68 +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 dbebf96d..00000000 --- a/classes/report_formats/XlsReportFormat.php +++ /dev/null @@ -1,26 +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 618dcf98..00000000 --- a/classes/report_formats/XlsxReportFormat.php +++ /dev/null @@ -1,26 +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 54e869ec..00000000 --- a/classes/report_formats/XmlReportFormat.php +++ /dev/null @@ -1,37 +0,0 @@ -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); - } - - 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 ed1b4589..00000000 --- a/classes/report_types/AdoPivotReportType.php +++ /dev/null @@ -1,175 +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 = 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"; - } - 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] = mysql_real_escape_string(trim($value2)); - } - $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']]; - - $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[] = array('display'=>$row[0], 'value'=>$row[1]); - } else { - $options[] = $row[0]; - } - } - - return $options; - } - - 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']]; - - $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 63bfb1f8..00000000 --- a/classes/report_types/AdoReportType.php +++ /dev/null @@ -1,159 +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 = 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) { - $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']) { - $options[] = 'ALL'; - } - - while ($row = $result->FetchRow()) { - if ($result->FieldCount() > 1) { - $options[] = array('display'=>$row[0], 'value'=>$row[1]); - } else { - $options[] = $row[0]; - } - } - - return $options; - } - - 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 diff --git a/classes/report_types/MongoReportType.php b/classes/report_types/MongoReportType.php deleted file mode 100644 index 0a30d4f5..00000000 --- a/classes/report_types/MongoReportType.php +++ /dev/null @@ -1,77 +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 c13ec39b..00000000 --- a/classes/report_types/MysqlReportType.php +++ /dev/null @@ -1,4 +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 = 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; - } - - 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; - } -} diff --git a/classes/report_types/PhpReportType.php b/classes/report_types/PhpReportType.php deleted file mode 100644 index 3674a764..00000000 --- a/classes/report_types/PhpReportType.php +++ /dev/null @@ -1,87 +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 22a3c5b9..02df6377 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,15 @@ }, "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" + "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/config/config.php.sample b/config/config.php.sample index 02c93d68..861a775d 100644 --- a/config/config.php.sample +++ b/config/config.php.sample @@ -1,123 +1,137 @@ 'sample_reports', - - //the root directory of all dashboards - 'dashboardDir' => 'sample_dashboards', - - //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 deleted file mode 100644 index 9121e59e..00000000 --- a/index.php +++ /dev/null @@ -1,108 +0,0 @@ -setApplicationName(PhpReports::$config['ga_api']['applicationName']); - $ga_client->setClientId(PhpReports::$config['ga_api']['clientId']); - $ga_client->setAccessType('offline'); - $ga_client->setClientSecret(PhpReports::$config['ga_api']['clientSecret']); - $ga_client->setRedirectUri(PhpReports::$config['ga_api']['redirectUri']); - $ga_service = new Google_Service_Analytics($ga_client); - $ga_client->addScope(Google_Service_Analytics::ANALYTICS); - if(isset($_GET['code'])) { - $ga_client->authenticate($_GET['code']); - $_SESSION['ga_token'] = $ga_client->getAccessToken(); - - if(isset($_SESSION['ga_authenticate_redirect'])) { - $url = $_SESSION['ga_authenticate_redirect']; - unset($_SESSION['ga_authenticate_redirect']); - header("Location: $url"); - exit; - } - } - if(isset($_SESSION['ga_token'])) { - $ga_client->setAccessToken($_SESSION['ga_token']); - } - elseif(isset(PhpReports::$config['ga_api']['accessToken'])) { - $ga_client->setAccessToken(PhpReports::$config['ga_api']['accessToken']); - $_SESSION['ga_token'] = $ga_client->getAccessToken(); - } - - Flight::route('/ga_authenticate',function() use($ga_client) { - $authUrl = $ga_client->createAuthUrl(); - if(isset($_GET['redirect'])) { - $_SESSION['ga_authenticate_redirect'] = $_GET['redirect']; - } - header("Location: $authUrl"); - exit; - }); -} - -Flight::route('/',function() { - PhpReports::listReports(); -}); - -Flight::route('/dashboards',function() { - PhpReports::listDashboards(); -}); - -Flight::route('/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(); -}); - -//if no report format is specified, default to html -Flight::route('/report',function() { - PhpReports::displayReport($_REQUEST['report'],'html'); -}); - -//reports in a specific format (e.g. 'html','csv','json','xml', etc.) -Flight::route('/report/@format',function($format) { - PhpReports::displayReport($_REQUEST['report'],$format); -}); - -Flight::route('/edit',function() { - PhpReports::editReport($_REQUEST['report']); -}); - -Flight::route('/set-environment',function() { - header("Content-Type: application/json"); - $_SESSION['environment'] = $_REQUEST['environment']; - - echo '{ "status": "OK" }'; -}); - -//email report -Flight::route('/email',function() { - PhpReports::emailReport(); -}); - -Flight::set('flight.handle_errors', false); -Flight::set('flight.log_errors', true); - -Flight::start(); diff --git a/lib/PhpReports/FilterBase.php b/lib/PhpReports/FilterBase.php deleted file mode 100644 index cb422fe0..00000000 --- a/lib/PhpReports/FilterBase.php +++ /dev/null @@ -1,12 +0,0 @@ -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; - } -} diff --git a/lib/PhpReports/PhpReports.php b/lib/PhpReports/PhpReports.php deleted file mode 100644 index 8a921d87..00000000 --- a/lib/PhpReports/PhpReports.php +++ /dev/null @@ -1,680 +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->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;
-	}
-}
-PhpReports::init();
diff --git a/lib/PhpReports/Report.php b/lib/PhpReports/Report.php
deleted file mode 100644
index 7164439b..00000000
--- a/lib/PhpReports/Report.php
+++ /dev/null
@@ -1,623 +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 = 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);
-	}
-}
-?>
diff --git a/lib/PhpReports/ReportFormatBase.php b/lib/PhpReports/ReportFormatBase.php
deleted file mode 100644
index 49f682b5..00000000
--- a/lib/PhpReports/ReportFormatBase.php
+++ /dev/null
@@ -1,15 +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(array('/\s+/','/[^a-zA-Z0-9_]*/'),array('_',''),$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 575c85f3..fb170eee 100644
--- a/lib/simplediff/SimpleDiff.php
+++ b/lib/simplediff/SimpleDiff.php
@@ -1,100 +1,130 @@
 
-	May be used and distributed under the zlib/libpng license.
-	
-	This code is intended for learning purposes; it was written with short
-	code taking priority over performance. It could be used in a practical
-	application, but there are a few ways it could be optimized.
-	
-	Given two arrays, the function diff will return an array of the changes.
-	I won't describe the format of the array, but it will be obvious
-	if you use print_r() on the result of a diff on some test data.
-	
-	htmlDiff is a wrapper for the diff command, it takes two strings and
-	returns the differences in HTML. The tags used are  and ,
-	which can easily be styled with CSS.  
-*/
-
-class SimpleDiff {
-	function diff($old, $new){
-		$maxlen = 0;
-		foreach($old as $oindex => $ovalue){
-			$nkeys = array_keys($new, $ovalue);
-			foreach($nkeys as $nindex){
-				$matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ?
-					$matrix[$oindex - 1][$nindex - 1] + 1 : 1;
-				if($matrix[$oindex][$nindex] > $maxlen){
-					$maxlen = $matrix[$oindex][$nindex];
-					$omax = $oindex + 1 - $maxlen;
-					$nmax = $nindex + 1 - $maxlen;
-				}
-			}	
-		}
-		if($maxlen == 0) return array(array('d'=>$old, 'i'=>$new));
-		return array_merge(
-			self::diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)),
-			array_slice($new, $nmax, $maxlen),
-			self::diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen))
-		);
-	}
-
-	function htmlDiff($old, $new){
-		$ret = '';
-		$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']))." ":'');
-			else $ret .= htmlentities($k) . " ";
-		}
-		return $ret;
-	}
-	
-	protected function hasChange($diff, $i, $before=0, $after=0) {
-		if($before) if(self::hasChange($diff, $i-1, $before -1, 0)) return true;
-		if($after) if(self::hasChange($diff, $i+1, 0, $after -1)) return true;
-		
-		if(!isset($diff[$i])) return false;
-		if(!is_array($diff[$i])) return false;
-		if($diff[$i]['i'] || $diff[$i]['d']) return true;
-		else return false;
-	}
-	
-	function htmlDiffSummary($old, $new){
-		$ret = '';
-		$diff = self::diff(explode("\n", $old), explode("\n", $new));
-		
-		$diff_section = false;
-		
-		foreach($diff as $i=>$k){
-			//if we are within 1 lines of a change
-			if(self::hasChange($diff,$i,1,1)) {
-				//if we aren't already in a diff section, start it
-				if(!$diff_section) {
-					$diff_section = true;
-					$ret .= "
Line $i
"; - } - } - else { - //close the diff section - $diff_section = false; - $ret .= "
"; - } - - 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":''); - elseif($diff_section) { - $ret .= htmlentities($k) . "\n"; - } - } - - if($diff_section) $ret .= ""; - - return $ret; - } +/** + * + * Paul's Simple Diff Algorithm v 0.1 + * (C) Paul Butler 2007 + * May be used and distributed under the zlib/libpng license. + * + * This code is intended for learning purposes; it was written with short + * code taking priority over performance. It could be used in a practical + * application, but there are a few ways it could be optimized. + * + * Given two arrays, the function diff will return an array of the changes. + * I won't describe the format of the array, but it will be obvious + * if you use print_r() on the result of a diff on some test data. + * + * htmlDiff is a wrapper for the diff command, it takes two strings and + * returns the differences in HTML. The tags used are and , + * which can easily be styled with CSS. + */ +class SimpleDiff +{ + protected function diff($old, $new) + { + $maxlen = 0; + foreach ($old as $oindex => $ovalue) { + $nkeys = array_keys($new, $ovalue); + foreach ($nkeys as $nindex) { + $matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ? + $matrix[$oindex - 1][$nindex - 1] + 1 : 1; + if ($matrix[$oindex][$nindex] > $maxlen) { + $maxlen = $matrix[$oindex][$nindex]; + $omax = $oindex + 1 - $maxlen; + $nmax = $nindex + 1 - $maxlen; + } + } + } + + if ($maxlen == 0) { + return [['d' => $old, 'i' => $new]]; + } + + return array_merge( + self::diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)), + array_slice($new, $nmax, $maxlen), + self::diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen)) + ); + } + + protected function htmlDiff($old, $new) + { + $ret = ''; + $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']))." " : ''); + } else { + $ret .= htmlentities($k)." "; + } + } + + return $ret; + } + + protected function hasChange($diff, $i, $before = 0, $after = 0) + { + if ($before) { + if (self::hasChange($diff, $i-1, $before -1, 0)) { + return true; + } + } + + if ($after) { + if (self::hasChange($diff, $i+1, 0, $after -1)) { + return true; + } + } + + if (!isset($diff[$i])) { + return false; + } + + if (!is_array($diff[$i])) { + return false; + } + + if ($diff[$i]['i'] || $diff[$i]['d']) { + return true; + } + + return false; + } + + public function htmlDiffSummary($old, $new) + { + $ret = ''; + $diff = self::diff(explode("\n", $old), explode("\n", $new)); + + $diff_section = false; + + foreach ($diff as $i => $k) { + //if we are within 1 lines of a change + if (self::hasChange($diff, $i, 1, 1)) { + //if we aren't already in a diff section, start it + if (!$diff_section) { + $diff_section = true; + $ret .= "
Line $i
"; + } + } else { + //close the diff section + $diff_section = false; + $ret .= "
"; + } + + 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" : ''); + } elseif ($diff_section) { + $ret .= htmlentities($k)."\n"; + } + } + + if ($diff_section) { + $ret .= ""; + } + + return $ret; + } } -?> 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 diff --git a/public/css/report_list.css b/public/css/report_list.css index d8f3722c..8d0495d7 100644 --- a/public/css/report_list.css +++ b/public/css/report_list.css @@ -16,7 +16,7 @@ } #table_of_contents { bottom: 0; - top: 50px; + top: 100px; position: fixed; overflow-y: auto; } diff --git a/public/index.php b/public/index.php new file mode 100644 index 00000000..6e79cefa --- /dev/null +++ b/public/index.php @@ -0,0 +1,116 @@ +setApplicationName(PhpReports::$config['ga_api']['applicationName']); + $ga_client->setClientId(PhpReports::$config['ga_api']['clientId']); + $ga_client->setAccessType('offline'); + $ga_client->setClientSecret(PhpReports::$config['ga_api']['clientSecret']); + $ga_client->setRedirectUri(PhpReports::$config['ga_api']['redirectUri']); + $ga_service = new Google_Service_Analytics($ga_client); + $ga_client->addScope(Google_Service_Analytics::ANALYTICS); + if (isset($_GET['code'])) { + $ga_client->authenticate($_GET['code']); + $_SESSION['ga_token'] = $ga_client->getAccessToken(); + + if (isset($_SESSION['ga_authenticate_redirect'])) { + $url = $_SESSION['ga_authenticate_redirect']; + unset($_SESSION['ga_authenticate_redirect']); + header("Location: $url"); + exit; + } + } + if (isset($_SESSION['ga_token'])) { + $ga_client->setAccessToken($_SESSION['ga_token']); + } elseif (isset(PhpReports::$config['ga_api']['accessToken'])) { + $ga_client->setAccessToken(PhpReports::$config['ga_api']['accessToken']); + $_SESSION['ga_token'] = $ga_client->getAccessToken(); + } + + Flight::route('/ga_authenticate', function () use ($ga_client) { + $authUrl = $ga_client->createAuthUrl(); + if (isset($_GET['redirect'])) { + $_SESSION['ga_authenticate_redirect'] = $_GET['redirect']; + } + header("Location: $authUrl"); + exit; + }); +} + +Flight::route('GET /', function () { + PhpReports::listReports(); +}); + +Flight::route('GET /dashboards', function () { + PhpReports::listDashboards(); +}); + +Flight::route('GET /dashboard/@name', function ($name) { + PhpReports::displayDashboard($name); +}); + +//JSON list of reports (used for typeahead search) +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 +Flight::route('/report', function () { + PhpReports::displayReport($_REQUEST['report'], 'html'); +}); + +//reports in a specific format (e.g. 'html','csv','json','xml', etc.) +Flight::route('/report/@format', function ($format) { + PhpReports::displayReport($_REQUEST['report'], $format); +}); + +Flight::route('/edit', function () { + PhpReports::editReport($_REQUEST['report']); +}); + +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 + ]); + + $environment = array_pop($environment); + + $_SESSION['environment'] = $environment; + + Flight::json(['status' => 'OK']); +}, true); + +//email report +Flight::route('/email', function () { + PhpReports::emailReport(); +}); + +Flight::set('flight.handle_errors', false); +Flight::set('flight.log_errors', true); + +PhpReports::init(); + +Flight::start(); 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/classes/filters/numberFilter.php b/src/Reports/Filters/NumberFilter.php similarity index 60% rename from classes/filters/numberFilter.php rename to src/Reports/Filters/NumberFilter.php index ec3e4e18..11e8ae22 100644 --- a/classes/filters/numberFilter.php +++ b/src/Reports/Filters/NumberFilter.php @@ -1,16 +1,17 @@ getValue())) { $value->setValue(number_format($value->getValue(), $decimals, $dec_sepr, $thousand), true); } 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..ec90ba5d --- /dev/null +++ b/src/Reports/Formats/ChartReportFormat.php @@ -0,0 +1,25 @@ +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..4fe6bd86 --- /dev/null +++ b/src/Reports/Formats/CsvReportFormat.php @@ -0,0 +1,40 @@ +use_cache = true; + + $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("Pragma: no-cache"); + header("Expires: 0"); + + $datasetIndex = 0; + if (isset($_GET['dataset'])) { + $datasetIndex = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $datasetIndex = $report->options['default_dataset']; + } + $datasetIndex = intval($datasetIndex); + + $data = $report->renderReportPage('csv/report', [ + 'dataset' => $datasetIndex, + ]); + + 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..90988f95 --- /dev/null +++ b/src/Reports/Formats/DebugReportFormat.php @@ -0,0 +1,32 @@ +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 = []; + 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 = [ + '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..33c876b7 --- /dev/null +++ b/src/Reports/Formats/JsonReportFormat.php @@ -0,0 +1,78 @@ +run(); + + if (!$report->options['DataSets']) { + return; + } + + $result = []; + if (isset($_GET['datasets'])) { + $datasets = $_GET['datasets']; + // If all the datasets should be included + if ($datasets === 'all') { + $datasets = array_keys($report->options['DataSets']); + } elseif (!is_array($datasets)) { + // If just a single dataset was specified, make it an array + $datasets = explode(',', $datasets); + } + + foreach ($datasets as $datasetIndex) { + $result[] = self::getDataSet($datasetIndex, $report); + } + } else { + $datasetIndex = 0; + if (isset($_GET['dataset'])) { + $datasetIndex = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $datasetIndex = $report->options['default_dataset']; + } + $datasetIndex = intval($datasetIndex); + + $dataset = self::getDataSet($datasetIndex, $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($datasetIndex, &$report) + { + $dataset = []; + foreach ($report->options['DataSets'][$datasetIndex] as $k => $v) { + $dataset[$k] = $v; + } + + $rows = []; + foreach ($dataset['rows'] as $datasetIndex => $row) { + $tmp = []; + foreach ($row['values'] as $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..eb030f3b --- /dev/null +++ b/src/Reports/Formats/RawReportFormat.php @@ -0,0 +1,28 @@ +renderReportPage('sql/report'); + } +} diff --git a/src/Reports/Formats/TableReportFormat.php b/src/Reports/Formats/TableReportFormat.php new file mode 100644 index 00000000..56293e34 --- /dev/null +++ b/src/Reports/Formats/TableReportFormat.php @@ -0,0 +1,23 @@ +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..845d7da4 --- /dev/null +++ b/src/Reports/Formats/TextReportFormat.php @@ -0,0 +1,112 @@ +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); + // get largest result size + if ($length > $sizes[$key]) { + $sizes[$key] = $length; + } + } + } + + //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..1b02ce4f --- /dev/null +++ b/src/Reports/Formats/XlsReportBase.php @@ -0,0 +1,86 @@ +getProperties()->setCreator("PHP-Reports") + ->setLastModifiedBy("PHP-Reports") + ->setTitle("") + ->setSubject("") + ->setDescription(""); + + foreach ($report->options['DataSets'] as $datasetIndex => $dataset) { + $objPHPExcel->createSheet($datasetIndex); + self::addSheet($objPHPExcel, $dataset, $datasetIndex); + } + + // 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 ($columnLeter = 1; $columnLeter <= $cols; $columnLeter++) { + $objPHPExcel->getActiveSheet()->getColumnDimension(self::columnLetter($columnLeter))->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..8d8ff162 --- /dev/null +++ b/src/Reports/Formats/XlsReportFormat.php @@ -0,0 +1,39 @@ +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..d276b95c --- /dev/null +++ b/src/Reports/Formats/XlsxReportFormat.php @@ -0,0 +1,39 @@ +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..de13f578 --- /dev/null +++ b/src/Reports/Formats/XmlReportFormat.php @@ -0,0 +1,48 @@ +options['DataSets']); + } elseif (!is_array($datasets)) { + // If just a single dataset was specified, make it an array + $datasets = explode(',', $datasets); + } + } else { + $datasetIndex = 0; + if (isset($_GET['dataset'])) { + $datasetIndex = $_GET['dataset']; + } elseif (isset($report->options['default_dataset'])) { + $datasetIndex = $report->options['default_dataset']; + } + $datasetIndex = intval($datasetIndex); + + $datasets = [$datasetIndex]; + } + + 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..09a1c243 --- /dev/null +++ b/src/Reports/Headers/HeaderBase.php @@ -0,0 +1,130 @@ +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..5d1002f6 --- /dev/null +++ b/src/Reports/Headers/OptionsHeader.php @@ -0,0 +1,156 @@ + [ + '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', + ], + ]; + + /** + * @{inheritDoc} + */ + 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..ec2b46a5 --- /dev/null +++ b/src/Reports/Headers/RollupHeader.php @@ -0,0 +1,154 @@ + [ + '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..e46a90b1 --- /dev/null +++ b/src/Reports/Headers/VariableHeader.php @@ -0,0 +1,233 @@ + [ + '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..7ad49029 --- /dev/null +++ b/src/Reports/PhpReports.php @@ -0,0 +1,726 @@ +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 = ['../templates/default', '../templates']; + if (file_exists('../templates/local')) { + array_unshift($template_dirs, '../templates/local'); + } + + $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')); + 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(), ['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', [ + 'dashboards' => $dashboards, + ]); + } + + public static function displayDashboard($dashboard) + { + $content = self::getDashboard($dashboard); + + echo self::render('html/dashboard', [ + '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 = []; + 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 getReportList($reports = null) + { + if ($reports === null) { + $errors = []; + $reports = self::getReports(self::$config['reportDir'] . '/', $errors); + } + + //weight by popular reports + $recently_run = \FileSystemCache::retrieve(\FileSystemCache::generateCacheKey('recently_run')); + $popular = []; + if ($recently_run !== false) { + foreach ($recently_run as $report) { + if (!isset($popular[$report])) { + $popular[$report] = 1; + } else { + $popular[$report]++; + } + } + } + $parts = []; + + 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 = self::getReportList($report['children']); + if (!empty($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[] = [ + 'name' => $report['Name'], + 'url' => $report['url'], + 'popularity' => $popularity, + ]; + } + } + + return $parts; + } + + protected static function getReportHeaders($report) + { + $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 + $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(['_', '-', '/', ' ', '.'], ['', '', '_', '-', '_'], trim($report, '/')); + if (!isset($data['Name'])) { + $data['Name'] = ucwords(str_replace(['_', '-'], ' ', 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 = []; + 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(['_', '-', '/', ' '], ['', '', '_', '-'], 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[] = [ + '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) { + 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 = []; + } + $errors[] = [ + '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(['error' => 'Valid email address required']); + + return; + } + if (!isset($_REQUEST['url'])) { + echo json_encode(['error' => 'Report url required']); + + return; + } + if (!isset(PhpReports::$config['mail_settings']['enabled']) || !PhpReports::$config['mail_settings']['enabled']) { + echo json_encode(['error' => 'Email is disabled on this server']); + + return; + } + if (!isset(PhpReports::$config['mail_settings']['from'])) { + echo json_encode(['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([ + 'error' => $e->getMessage(), + ]); + + return; + } + + if ($result) { + echo json_encode([ + 'success' => true, + ]); + } else { + echo json_encode([ + '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'] = []; + } + 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'"); + } + } + + /** + * 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) + { + //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(["\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);
+    }
+
+    /**
+     * @param string $url
+     * @return string $output
+     */
+    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;
+    }
+}
diff --git a/src/Reports/Report.php b/src/Reports/Report.php
new file mode 100644
index 00000000..11d577f7
--- /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 [];
+    }
+
+    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[] = [
+                '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..6e7b5e3c
--- /dev/null
+++ b/src/Reports/ReportValue.php
@@ -0,0 +1,121 @@
+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);
+        }
+    }
+
+    /**
+     * @return string
+     */
+    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..d2cde21f
--- /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 = []; + + 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 @@ + - - - - + + + + + {% 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/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 %} - - - - - - - + + + + + + + +