Commit cd5b0065 authored by metzlerd's avatar metzlerd
Browse files

Working report engine with support for drupal database reports and other PDO database reports.

parent a9eb002e
......@@ -15,11 +15,11 @@ class FrxDataEngine{
* @return unknown
*/
public function access($arg) {
$f = $conf['access callback'];
if ($F && is_callable($f)) {
$f = $this->conf['access callback'];
if ($f && is_callable($f)) {
return $f($arg);
} else {
return FALSE;
return user_access('access content');
}
}
......
......@@ -3,20 +3,44 @@ require_once('FrxSyntaxEngine.inc');
class FrxReport {
private $rpt_xml;
private $cur_xml;
private $cur_data;
private $output;
private $data_context;
private $teng;
public $fields;
public $categories;
public $access;
public function __construct($xhtml, $data=array()) {
$this->teng = new FrxSyntaxEngine(FRX_TOKEN_EXP,'{}');
$this->teng = new FrxSyntaxEngine(FRX_TOKEN_EXP,'{}',$this);
if (!is_object($xhtml)) {
$this->rpt_xml = new SimpleXMLElement($xhtml);
} else {
$this->rpt_xml = $xhtml;
}
$this->cur_xml = $data;
$this->cur_data = $data;
// Load header data
$rpt_xml = $this->rpt_xml;
if ($rpt_xml->head) {
$this->title = (string)$rpt_xml->head->title;
foreach($rpt_xml->head->children('urn:FrxReports') as $name=>$node) {
switch ($name) {
case 'fields':
$this->fields = $node;
break;
case 'category':
$this->categories[] = (string)$node;
break;
case 'access':
$this->access[] = (string)$node;
break;
}
}
}
}
/**
* Get the data block
......@@ -25,7 +49,7 @@ class FrxReport {
*/
private function get_data($block) {
//@TODO: Merge xml data parameters into the report paramters
$this->cur_xml = forena_invoke_data_engine($block, $this->cur_xml);
$this->cur_data = forena_invoke_data_engine($block, $this->cur_data);
}
private function process_frx_attributes(SimpleXMLElement $node) {
......@@ -40,17 +64,17 @@ class FrxReport {
}
/**
* Recursive report renderer
* Walks the nodes renering the report.
*/
private function render_section(SimpleXMLElement $node) {
/**
* Recursive report renderer
* Walks the nodes renering the report.
*/
public function render_section(SimpleXMLElement $node) {
$elements = count($node->xpath('*'));
$frx = $node->attributes('urn:FrxReports');
// Test to see if we have any nodes that are contains data url
if ($node->xpath('*//@frx:*') || $frx) {
$text_nodes = count($node->xpath('child::text()'));
$child_nodes = count($node->xpath('.//*'));
$elements = count($node->xpath('*'));
// Test to see if we have any nodes that are contains data url
if ($node->xpath('*//@frx:*')||$node->xpath('./@frx:*')) {
$attrs = $node->attributes();
$tag = $node->getName();
$this->process_frx_attributes($node);
......@@ -62,36 +86,37 @@ class FrxReport {
$frx = $node->attributes('urn:FrxReports');
if ((string)$frx['foreach'] ){
// Save xml
$path = (string)$frx['foreach'];
$data = $this->cur_xml;
$data = $this->cur_data;
if($data) $nodes = $data->xpath($path);
if ($nodes) foreach ($nodes as $x) {
$this->cur_xml = $x;
$o .= '<'. $tag. $attr_text . '>';
foreach($node->xpath('child::node()') as $child) {
$this->cur_data = $x;
$o .= $this->teng->replace('<'. $tag. $attr_text . '>',$this->cur_data);
foreach($node->children() as $child) {
$o .= $this->render_section($child);
}
$o .= '</'. $tag .'>';
}
$this->cur_xml = $data;
$this->cur_data = $data;
} else {
$o .= '<'. $tag. $attr_text . '>';
foreach ($node as $child) {
$o .= $this->teng->replace('<'. $tag. $attr_text . '>',$this->cur_data);
foreach ($node->children() as $child) {
$o.= $this->render_section($child);
}
$o .= '</'. $tag .'>';
}
} else {
$tag = $node->getName();
// We can render so lets do it.
$text = $node->asXML(); ;
$o.=$this->teng->replace($text,$this->cur_xml);
$o.=$this->teng->replace($text,$this->cur_data);
}
return $o;
}
......@@ -102,6 +127,48 @@ class FrxReport {
*/
public function render($data= array()) {
$rpt_xml = $this->rpt_xml;
return $this->render_section($rpt_xml);
drupal_set_title($this->title);
if ($rpt_xml->body) $rpt_xml = $rpt_xml->body;
$body_xml = $rpt_xml;
foreach ($body_xml->children() as $node) {
$o .= $this->render_section($node);
}
return $o ;
}
/*
* Formatter used by the syntax engine to alter data that gets extracted.
* This invokes the field translation engine
*/
public function format($value, $key, $data) {
// Determine if there is a field overide entry
if ($this->fields) {
$path = 'frx:field[@id="'. $key .'"]';
$formatters = $this->fields->xpath($path);
if ($formatters) foreach ($formatters as $formatter) {
if (((string)$formatter['block'] == $this->block) || (!(string)$formatter['block'])) {
//@TODO: Replace the default extraction with something that will get sub elements of the string
$default = (string)$formatter;
$link = (string) $formatter['link'];
$format = (string) $formatter['format'];
$format_str = (string) $formatter['format-string'];
}
}
}
// Default if specified
if (!$value && $default) {
$value = $default;
}
if ($link) {
$link = $this->teng->replace($link, $data, TRUE);
list($url,$query) = explode('?',$link);
$value = l(htmlspecialchars_decode($value),$url, array('query' => $query));
}
return $value;
}
}
\ No newline at end of file
......@@ -59,7 +59,8 @@ class FrxSyntaxEngine {
* @param $data
* @return unknown_type
*/
public function replace($text, $data) {
public function replace($text, $data, $raw=FALSE) {
$match=array();
$o_text = $text;
if (preg_match_all($this->tpattern,$o_text,$match))
......@@ -70,7 +71,7 @@ class FrxSyntaxEngine {
foreach($match[0] as $match_num=>$token)
{
$path = trim($token,$this->trim_chars);
$value = $this->get_value($data, $path);
$value = $this->get_value($data, $path, $raw);
$text = str_replace($token, $value, $text);
}
......
<?php
require_once('forena.common.inc');
/**
* Save the report file to disk
*
* @param string $name File name to save report to
* @param unknown_type $data
*/
function forena_save_report($name, $data) {
$report_path = forena_report_path();
//@TODO: Clean up filename to make sure
$filepath = $report_path.'/'.$name. 'frx';
if (is_object($data)) {
$data = $data->asXML();
}
try {
file_put_contents($filepath, $data);
} catch (Exception $e) {
fornea_error('Error Saving Report', $e->getMessage());
}
}
/**
* Forena admin settings form
*
*/
function forena_settings() {
$report_path = forena_report_path();
$path = variable_get('forena_path','reports');
$form['forena_path'] = array(
'#type' => 'textfield',
'#title' => t('URL path settings'),
'#description' => t('Specify the url by which reports are accessed. (e.g. "reports"). Use a relative path '.
'and don\'t add a trailing slash or the URL won\'t work'),
'#default_value' => $path,
);
$form['forena_report_repos'] = array(
'#type' => 'textfield',
'#title' => t('Report Repository'),
'#description' => t('Indicate the directory that you want to use for your reports. In order for you to '.
'to be able to save reports, this directory should be writable by the web user. Relative'.
'paths should be entered relative to the base path of your drupal installation.'),
'#default_value' => $report_path,
);
$form['instructions'] = array(
'#type' => 'item',
'#title' => t('Data Sources'),
'#value' => '<p>'. t('Database connections and data block repositories are configured directly in the file system for '.
'security reasons. See the Forena Reports README.txt file for more information.') .'</p>',
);
$form = system_settings_form($form);
$form['#submit'][] = 'forena_settings_submit';
return $form;
}
/**
* Added submit handler to create directories and clear menu cacth
*
* @param unknown_type $form
* @param unknown_type $form_state
*/
function forena_settings_submit($form, &$form_state) {
$values = $form_state['values'];
$path = $values['forena_report_repos'];
$src_dir = drupal_get_path('module','forena') . '/repos/reports';
if (!file_exists($path)) {
try {
if (file_exists($path)) {
drupal_set_message (t('Created directory %s', array($path))) ;
}
mkdir($path);
} catch (Exception $e) {
forena_error('Unable to create report directory', $e->getMessage());
}
} elseif ($path != $src_dir) {
// Copy the reports from the
$d = dir($src_dir);
$dest_dir = $d->path;
$i=0;
while (false !== ($rpt_file = $d->read())) {
echo $entry."\n";
$src_file = $d->path .'/'. $rpt_file;
$dest_file = $path .'/'. $rpt_file;
if (is_file($src_file)) {
file_put_contents($dest_file,file_get_contents($src_file));
$i++;
}
}
$d->close();
drupal_set_message($i .' delivered reports copied from '. $src_dir. ' to '. $path);
}
menu_cache_clear();
}
......@@ -26,6 +26,7 @@ function __forena_load_repository(&$repo) {
*/
function __forena_load_engine($conf, $repo_path) {
$name = $conf['data_engine'];
$path = drupal_get_path( 'module', 'forena'). '/plugins/' . $name;
// Make sure that we don't override predefined classes
if (!class_exists($name) && file_exists($path. '.inc')) {
......@@ -83,10 +84,12 @@ function forena_repository($name='') {
function forena_invoke_data_engine($data_block, $parameters=array(), $subquery='') {
list($provider,$block) = explode('/',$data_block, 2);
// Get the data
$repos = forena_repository($provider);
$repos = forena_repository($provider);
if ($repos['data']) {
$engine = $repos['data'];
if (method_exists($engine,'data')) {
$xml = $engine->data($block, $parameters, $subquery);
}
return $xml;
......@@ -125,6 +128,12 @@ function forena_load_block_file($filepath, $comment='--') {
}
/**
* General wrapper procedure for reporting erros
*
* @param string $short_message Message that will be displayed to the users
* @param string $log Message that will be recorded in the logs.
*/
function forena_error($short_message, $log) {
if ($short_message) {
drupal_set_message($short_message,'error');
......@@ -134,4 +143,17 @@ function forena_error($short_message, $log) {
}
}
/**
* Load the report repository path
*
* @return unknown
*/
function forena_report_path() {
$report_path = variable_get('forena_report_repos','');
if (!$report_path) {
$report_path = drupal_get_path('module','forena'). '/repos/reports';
}
return $report_path;
}
......@@ -14,6 +14,16 @@ function forena_menu() {
'type' => MENU_CALLBACK,
);
$items['admin/settings/forena'] = array(
'page callback' => 'drupal_get_form',
'page arguments' => array('forena_settings'),
'title' => 'Forena Reports',
'description' => t('Tell Forena where to store report files and how users should access them.'),
'access arguments' => array('administer forena reports'),
'type' => MENU_NORMAL_ITEM,
'file' => 'forena.admin.inc',
);
$path = variable_get('forena_path','reports');
$items[$path] = array(
'page callback' => 'forena_report',
......@@ -26,39 +36,49 @@ function forena_menu() {
return $items;
}
/**
* Implementation of hook_perm
*
* @return unknown
*/
function forena_perm() {
$perms = array(
'create any report',
'design any report',
'administer forena reports'
);
//@TODO: Add the ability to create subrepositories with different permissions.
return $perms;
}
/**
* Test function for white box testing.
*
* @return unknown
*/
function forena_test() {
$output .= 'Report start';
$output .= 'Forena test page';
$r = '<body xmlns:frx="urn:FrxReports"><div frx:block="banner/test" ><div class="repeater" frx:foreach="row"><p>this is a test report for {name}</p></div></div></body>';
$output .= forena_render_report($r, null, array('uid' => 3));
return $output;
}
function forena_report($name='') {
require_once('forena.common.inc');
$report_path = variable_get('forena_report_repos','');
if (!$report_path) {
$report_path = drupal_get_path('module','forena'). '/repos/reports';
}
$report_path = forena_report_path();
if ($name) {
$filename = $report_path . '/'. $name . '.frx';
$output .= 'Starting report '. $filename;
if (file_exists($filename)) {
$output .= 'Found report';
$r_text = file_get_contents($filename);
try {
$r = new SimpleXMLElement($r_text);
} catch (Error $e) {
} catch (Exception $e) {
forena_error('Unable to read report', $e->getMessage());
}
$parms = $_GET;
unset($parms['q']);
$output .= forena_render_report($r, $parms);
} else {
drupal_not_found();
}
} else {
// @TODO: List reports
......@@ -81,3 +101,8 @@ function forena_render_report ($report, $format='', $data='') {
$output = $o->render($report, $format, $data);
return $output;
}
......@@ -48,17 +48,19 @@ class FrxDBEngine extends FrxDataEngine {
$filename = $this->block_path .'/'. $block_name . '.sql';
$block = forena_load_block_file($filename);
$xml ='';
if ($block['source'] && $this->access($block['access'])) {
if ($block['source'] && $this->access($block['access']) && $db) {
$sql = $block['source'];
$sql = $this->te->replace($sql,$params);
$rs = $db->query($sql);
$xml = new SimpleXMLElement('<table/>');
$e = $db->errorCode();
if ($e!='00000') {
if ($e!='00000') {
$i = $db->errorInfo();
forena_error($i[2].':'.$sql, $i[2]);
} else {
//$rs->debugDumpParams();
$data = $rs->fetchAll(PDO::FETCH_ASSOC);
foreach ($data as $row) {
......
<?php
class frxDrupalEngine {
class frxDrupal extends FrxDataEngine {
/**
* Implements hooks into the drupal applications
*/
private $te;
private $db;
private $block_path;
/**
* Get data from the block
* Object constructor
*
* @param unknown_type $blockname
* @param unknown_type $uri Database connection string.
* @param string $repos_path Path to location of data block definitions
*/
public function data($blockname) {
public function __construct($conf, $repos_path) {
parent::__construct($conf, $repos_path);
// Set up the stuff required to translate.
$this->block_path = $repos_path;
$this->te = new FrxSyntaxEngine(FRX_SQL_TOKEN,':',$this);
}
/**
* Get data based on file data block in the repository.
*
* @param String $block_name
* @param Array $parm_data
* @param Query $subQuery
*/
public function data($block_name, $params=array(), $subQuery='') {
// Load the block from the file
$filename = $this->block_path .'/'. $block_name . '.sql';
$block = forena_load_block_file($filename);
$xml ='';
if ($block['source'] && $this->access($block['access'])) {
$sql = $block['source'];
$sql = $this->te->replace($sql,$params);
$rs = db_query($sql);
$xml = new SimpleXMLElement('<table/>');
while ($data = db_fetch_object($rs)) {
$row_node = $xml->addChild('row');
foreach ($data as $key=>$value) {
$row_node->addChild($key,$value);
}
}
}
return $xml;
}
/**
* Implement custom SQL formatter to make sure that strings are properly escaped.
* Ideally we'd replace this with something that handles prepared statements, but it
* wouldn't work for
*
* @param unknown_type $value
* @param unknown_type $key
* @param unknown_type $data
*/
public function format($value, $key, $data) {
$value = $db_escape_string($value);
return $value;
}
}
\ No newline at end of file
......@@ -3,6 +3,6 @@
* Implements a method for importing xml feeds available on the web
*
*/
class FrxFeedEngine {
class FrxFeedEngine extends FrxDataEngine {
}
\ No newline at end of file
<?php
/*
* Sample Repository configuration file
*/
/*
* Security provider: Specify the class name that is used to provide security
*/
$conf['access callback'] = 'user_access';
/*
* Data provider:
* Specify the class name that will be used to interpret data block files.
* Note that data blocks in a repository
*
*/
$conf['data_engine'] = 'FrxDrupal';
/*
* URI:
* The format of the uri depends on the type of data engine that's being used.
* In database engines it might be the connection string to the db. In the file
* engine it would be the path to the directory containting the files
*/
// Not applicable to drupal installs
--ACCESS=access content
select uid,name,mail from users
\ No newline at end of file
<html xmlns:frx="urn:FrxReports">
<head>
<title>Users</title>
</head>
<body>
<div frx:block="drupal/users">
<table class="datagrid" >
<thead>
<tr><th>Name</th><th>Email</th></tr>
</thead>
<tbody>
<tr frx:foreach="*">
<td width="1000">{name}</td>
<td>{mail}</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
\ No newline at end of file
<html xmlns:frx="urn:FrxReports">
<head>
<title>A Sample Report</title>
<frx:fields>
<frx:field id="first_name" link="dave/foo?id={first_name}" format="" format-string=""/>
</frx:fields>