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'=>'
".$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).'-
".htmlentities(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'=>'
".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
$tableView 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
$tableView 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() }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ -->
-
+