[interchange] * The [jsonq] tag generates a record in a table (by default C<qc>)

Mike Heins interchange-cvs at icdevgroup.org
Thu Nov 19 12:22:49 UTC 2015


commit 75fbcbed1f51953e8c0fcca0515aa93b9638d267
Author: Mike Heins <mike at perusion.com>
Date:   Thu Nov 19 07:15:40 2015 -0500

    * The [jsonq] tag generates a record in a table (by default C<qc>)
      that allows users to access JSON records created by a query. The
      query associated with the record will be run with any parameters
      that are specified being taken either from 1) CGI variables or 2)
      the path info.
    
    * Adds QueryCache directive that speficies the "introducer", the URL
      fragment which calls the query cache short circuit, the table to be
      used, the content-type,
    
    * Adds hook early in the cycle to vet queries based on Session ID
      cookie and IP address, allowing the query to happen before sessions
      are attached or deep page initialization completes. This improves
      performance dramatically.
    
    * Adds access routine in Vend::Data to actually process the query
      and access the tables in question.
    
    * Hooks to allow an external program (eg will be added soon) to
      totally bypass IC for even lower overhead for queries.

 WHATSNEW-5.8              |    3 +
 code/UI_Tag/jsonq.coretag |  293 +++++++++++++++++++++++++++++++++++++++++++++
 lib/Vend/Config.pm        |   12 ++
 lib/Vend/Data.pm          |  268 +++++++++++++++++++++++++++++++++++++++++
 lib/Vend/Dispatch.pm      |   11 ++-
 5 files changed, 586 insertions(+), 1 deletions(-)
---
diff --git a/WHATSNEW-5.8 b/WHATSNEW-5.8
index e82a369..6a59809 100644
--- a/WHATSNEW-5.8
+++ b/WHATSNEW-5.8
@@ -280,4 +280,7 @@ External tool support
 
 * Added jEdit support for [PREFIX-calc] blocks, now syntax highlights contents as perl code.
 
+* Add secure JSON query generation for interface with AJAX widgets. Allows generation of
+  queries vetted by login, with either public access or access gated by session.
+
 (end)
diff --git a/code/UI_Tag/jsonq.coretag b/code/UI_Tag/jsonq.coretag
new file mode 100644
index 0000000..2bd2d4e
--- /dev/null
+++ b/code/UI_Tag/jsonq.coretag
@@ -0,0 +1,293 @@
+UserTag jsonq Order params public query
+UserTag jsonq addAttr
+UserTag jsonq Routine <<EOR
+sub {
+	my ($params, $public, $query, $opt) = @_;
+	my $qc = $Vend::Cfg->{QueryCache} or return undef;
+	my $tab = $qc->{table};
+	my $db = dbref($tab)
+		or do {
+			::logError("%s: missing table %s", 'query-cache', $tab);
+			return;
+		};
+	my $intro = $qc->{intro};
+
+	## Need to undef it if wrong because of vendURL
+	my $external = $qc->{external_program} || $opt->{external_program};
+	undef $external unless $external =~ m{^\w+:};
+
+#::logDebug("External=$external");
+# QC Table fields
+# code	qid	session	qtext	meta	params	public	secure	update_date	expire_date	results
+
+	my $exp_sess = '0';
+	my $exp_addr = '0';
+	my $exp_secure = '0';
+	my $exp_hash = '0';
+	my $exp_meta = '0';
+	my $exp_view = '0';
+	my $exp_term = '0';
+
+	$opt->{expire} ||= $public ? $qc->{default_public_expire} : $qc->{default_expire};
+
+	$public			or  $exp_sess = $Vend::SessionID;
+	$opt->{ip}		and $exp_addr = $CGI::remote_addr;
+	$opt->{secure}	and $exp_secure = 1;
+	$opt->{params}	and $exp_term = $opt->{params};
+	$opt->{hash}	and $exp_hash = $opt->{hash};
+	$opt->{meta}	and $exp_meta = $opt->{meta};
+	$opt->{meta_view}	and $exp_view = $opt->{meta_view};
+
+#::logDebug("hash=$opt->{hash}");
+	my $qid = Vend::Util::generate_key(join '|', $query, $exp_sess, $exp_addr, $exp_term, $exp_secure, $exp_hash,$exp_meta,$exp_view);
+
+	CHECKEXIST:  {
+		if(my $exist = $db->row_hash('qid',$qid) ) {
+			if(my $ed = $exist->{expire_date}) {
+				$ed =~ s/\D+//g;
+				last CHECKEXIST if $ed lt POSIX::strftime('%Y%m%d%H%M%S', localtime());
+			}
+			return Vend::Util::vendUrl("$intro/$qid", undef, $external, { secure => $exist->{secure}, add_dot_html => 0 });
+		}
+	}
+
+	my $rec = { 
+		qtext => $query,
+		qid => $qid,
+		public => $public,
+		secure => $opt->{secure},
+		hash => $opt->{hash},
+		params => $params,
+		meta_view => $opt->{meta_view},
+		meta => $opt->{meta},
+		content_type => $opt->{content_type},
+		template => $opt->{template},
+	};
+
+	if($opt->{expire} =~ /\D/ or length($opt->{expire}) < 7) {
+		my $add = $opt->{expire} =~ /[a-z]/ ? Vend::Config::time_to_seconds($opt->{expire}) : $opt->{expire} ;
+		$rec->{expire_date} = POSIX::strftime('%Y%m%d%H%M%S', localtime( time() + $add ));
+	}
+	else {
+		$rec->{expire_date} = $opt->{expire};
+	}
+
+	$rec->{session} = $Vend::SessionID unless $public;
+	$rec->{ipaddr} = $CGI::remote_addr if $opt->{ip};
+	$db->set_slice($qid, $rec);
+	return Vend::Util::vendUrl("$intro/$qid", undef, $external, { secure => $rec->{secure}, no_session => 1, add_dot_html => 0 });
+}
+EOR
+
+
+UserTag jsonq Documentation <<EOD
+=head2 NAME
+
+[jsonq] - Ajax query generation with security
+
+=head2 SYNOPSIS
+
+	[jsonq
+		query="select field1,field2,field3 ..."
+		expire="30min|3 days|86400|20170511"
+		public="0|1"
+		hash="0|1|field"
+		meta="option=value"
+		meta-view="metaview"
+		ip="0|1"
+	 ]
+
+NOTE: only the query is required
+
+=head2 CONFIGURATION
+
+	QueryCache  enabled 1
+	QueryCache  table  qc
+	QueryCache  intro  qc
+	QueryCache  default_expire 30min
+	QueryCache  default_public_expire 48hours
+	QueryCache  default_return {}
+
+=head2 PREREQUISITES
+
+	Module	JSON
+	Module	Digest::MD5
+	Module	SQL::Statement
+	Module	SQL::Parser
+
+=head2 DESCRIPTION
+
+The [jsonq] tag generates a record in a table (by default C<qc>) that allows users to access JSON records
+created by a query. The query associated with the record will be run with any parameters that are specified
+being taken either from 1) CGI variables or 2) the path info.
+
+The return value of [jsonq] is a URL to access the query.
+
+The URL used short circuits the usual Interchange session and catalog configuration mechanisms in Dispatch.pm,
+allowing fast (up to 3 times faster) access to JSON records. Alternatively, there can be an external handler
+for requests that could increase speed dramatically.
+
+The tag is standard, and is in the UserTag code area. It is enabled by specifying any setting for the
+QueryCache directive, by default "enabled 1".
+
+=head2 The table
+
+The C<qc> table has the following structure (in MySQL, other databases could be used):
+
+	+--------------+--------------+------+-----+-------------------+
+	| Field        | Type         | Null | Key | Default           |
+	+--------------+--------------+------+-----+-------------------+
+	| qid          | varchar(32)  | NO   | PRI | NULL              |
+	| session      | varchar(64)  | YES  |     | NULL              |
+	| ipaddr       | varchar(16)  | YES  |     | NULL              |
+	| qtext        | text         | NO   |     |                   |
+	| verbatim     | tinyint(1)   | YES  |     |                   |
+	| meta_view    | varchar(255) | YES  |     | NULL              |
+	| meta         | text         | YES  |     | NULL              |
+	| cols         | varchar(255) | YES  |     | NULL              |
+	| content_type | varchar(128) | YES  |     | NULL              |
+	| params       | text         | YES  |     | NULL              |
+	| template     | text         | YES  |     | NULL              |
+	| public       | char(1)      | YES  |     |                   |
+	| secure       | char(1)      | YES  |     |                   |
+	| hash         | varchar(32)  | YES  |     |                   |
+	| update_date  | timestamp    | NO   |     | CURRENT_TIMESTAMP |
+	| expire_date  | datetime     | YES  |     | NULL              |
+	| results      | text         | YES  |     | NULL              |
+	+--------------+--------------+------+-----+-------------------+
+
+When the [jsonq] tag is run, the parameters act on the table in this way:
+
+=over 4
+
+=item query	
+
+Enters the table as C<qtext>. This is the actual query that will run, and
+is possibly affected by CGI paramers C<mv_matchlimit> and C<mv_first_match>.
+
+=item public
+
+Enters table as C<public> field. If this is set, query is accessible to anyone.
+Do not use on private data sets.
+
+=item params
+
+The name of the CGI variables that will be inserted in place of any placeholders
+in the query. This uses DBI methodology, so it is secure and will not allow 
+SQL injection. If you wish to use the parameter in a C<LIKE> query, then append
+a C<%> character to the parameter, i.e.
+
+	[jsonq params="q%" query="select * from products where description like ?"]
+
+This causes the value of C<$CGI->{q} / [cgi q]> to be inserted surrounded by
+the percent signs, causing LIKE to work with partial strings.
+
+If you wish to use the parameter in a C<LIKE> query but only match the beginning
+of the string, then I<prepend> a C<^> character to the parameter, i.e.
+
+	[jsonq params="^q" query="select * from products where description like ?"]
+
+This causes the value of C<$CGI->{q} / [cgi q]> to be inserted followed by
+the percent signs, causing LIKE to work with the first part of the string
+anchored.
+
+By default, searches are rejected (returning C<default_return>) until the
+search parameter is 3 characters long. This prevents large query returns
+early in parameter typing, possibly overloading the database server.
+If you wish to start searching at a lower threshold (or a higher one)
+then append a colon followed by a digit:
+
+	[jsonq params="^q:1" query="select * from products where description like ?"]
+
+This causes the query to be done the moment the C<q> parameter has a single
+character. A C<4> would delay return until four characters are reached,
+etc.
+
+=item hash
+
+Enters table as C<hash> field. If this is blank, the query when run returns an "array of
+arrays" in JSON.  If it is set to digits only, normally 1, then the query will return
+an array of hashes. If it is set to a field name, this is the field that will be used
+to create a hash of hashes. Normally you would only use a unique key for that.
+
+=item meta_view
+
+Selects the I<meta view> which will operate on the JSON query output. This allows
+you, typically, to run Interchange filters on the output which will transform the
+output data from the query. 
+
+NOTE: If you are using the external CGI delivery mechanism, this will be ignored.
+
+=item meta
+
+Metadata options which will operate on the JSON query output. This allows
+setting other values (such as jui_datagrid to sculpt response).
+
+NOTE: If you are using the external CGI delivery mechanism, this will be ignored.
+
+=item template
+
+If you don't want JSON out, you can iterate over any array that you produce
+and output text or HTML based on the Interchange I<attr_list> format. The
+special areas
+
+	{PRE_TEMPLATE} Pre text {/PRE_TEMPLATE}
+	{POST_TEMPLATE} Post text {/POST_TEMPLATE}
+
+allow you to add text to the template that will not be iterted over.
+This invocation:
+
+  [tmpn tpl]
+  {PRE_TEMPLATE}<ul>{/PRE_TEMPLATE}
+  <li>{SKU} - {DESCRIPTION}</li>
+  {POST_TEMPLATE}</ul>{/POST_TEMPLATE}
+  [/tmpn]
+  [jsonq
+     query="select sku,description from products where description like '%Nails%'"
+  		template="[scratch tpl]" hash=1 content-type="text/html"
+		]
+
+Will produce something like this when the query is run:
+
+	<ul>
+	<li>os28057a - 16 Penny Nails</li>
+	<li>os28057b - 10 Penny Nails</li>
+	<li>os28057c - 8 Penny Nails</li>
+	</ul>
+
+This will work no matter the state of the C<hash> parameter, as the fields are
+determined. (It is probably best to use hash=1 for this query.)
+
+=item content-type
+
+This parameter will allow you to change the MIME type of the output from
+the default of C<application/json>.
+
+=back
+
+=head2 URL
+
+Here is a typical URL generated (for a catalog with a VendURL of http://www.perusion.com/c/strap):
+
+	http://www.perusion.com/c/strap/qc/059aba1aaee1debb4ecd3c67dd039e80
+
+You can specify the URL intro with the C<intro> configuration parameter.
+When it is set to C<qc>, it disables any URLs in the catalog that
+begin with /qc/ and short circuits their delivery to the routine which
+generates JSON.
+
+You can manage the presentation of the query with the C<mv_matchlimit>
+CGI parameter. If you specify C<mv_first_match> in addition, you can 
+set up paging. (Note those are remapped to C<ml> and C<fm> in most
+standard Interchange catalogs. You should take account of this if
+using the external CGI method.)
+
+NOTE: mv_first_match will not work without mv_matchlimit.
+
+=head2 AUTHOR
+
+Mike Heins, <mikeh at perusion.com>
+
+=cut
+
+EOD
diff --git a/lib/Vend/Config.pm b/lib/Vend/Config.pm
index 30d2500..8577460 100644
--- a/lib/Vend/Config.pm
+++ b/lib/Vend/Config.pm
@@ -171,6 +171,7 @@ my %HashDefaultBlank = (qw(
 					Mail			1
 					Accounting		1
 					Levy			1
+					QueryCache		1
 				));
 
 my %DumpSource = (qw(
@@ -722,6 +723,7 @@ sub catalog_directives {
 	['BounceReferralsRobot', 'yesno',        'no'],
 	['BounceRobotSessionURL',		 'yesno', 'no'],
 	['OrderCleanup',     'routine_array',    ''],
+	['QueryCache',        'hash',			''],
 	['SessionCookieSecure', 'yesno',         'no'],
 	['SessionHashLength', 'integer',         1],
 	['SessionHashLevels', 'integer',         2],
@@ -3598,6 +3600,16 @@ sub set_default_search {
 					push @Dispatches, 'DiscountSpaces';
 					return 1;
 		},
+		QueryCache => sub { 
+					my $qc; 
+					return 1 unless $qc = $C->{QueryCache}; 
+					$qc->{table} ||= 'qc'; 
+					$qc->{intro} ||= 'qc'; 
+					$qc->{default_expire} ||= '30min'; 
+					$qc->{default_public_expire} ||= '48hours'; 
+					$qc->{default_return} ||= '{}'; 
+					return 1; 
+		},
 		CookieLogin => sub {
 					return 1 unless $C->{CookieLogin};
 					push @Dispatches, 'CookieLogin';
diff --git a/lib/Vend/Data.pm b/lib/Vend/Data.pm
index 9922ca3..1df484b 100644
--- a/lib/Vend/Data.pm
+++ b/lib/Vend/Data.pm
@@ -2408,6 +2408,274 @@ sub update_data {
 	return;
 }
 
+my $JSON_present;
+BEGIN: {
+	eval {
+		require JSON;
+		require SQL::Parser;
+		require SQL::Statement;
+	};
+	$JSON_present = 1;
+	import JSON;
+}
+
+sub run_query_cache {
+	my $qc = shift; ## Query config
+	my $sessionid = shift; ## Session ID from dispatch
+	my $path = $CGI::path_info;
+
+	$Vend::ContentType = $qc->{content_type} || "application/json";
+
+	my $default = $qc->{default_return} || '{}'; ## Default string to return
+
+	if(! $Vend::allow_qc or ! $JSON_present ) {
+#::logDebug("Returning $default due to allow=$Vend::allow_qc or JSON_present=$JSON_present");
+		return $default;
+	}
+	$path =~ s{^/}{};
+	my (undef, $qid, @passed) = split m{/}, $path;
+	my $ctab = $qc->{table} || 'qc'; ## Default table for query cache
+	my $mtab = $qc->{meta_table} || 'mv_metadata'; ## Default table for metadata
+
+	my $limit;
+	if($CGI::values{mv_matchlimit} =~ /^\d+$/) {
+		$limit = ' LIMIT ';
+		if($CGI::values{mv_first_match} =~ /^\d+$/) {
+			$limit .= "$CGI::values{mv_first_match},";
+		}
+		$limit .= $CGI::values{mv_matchlimit};
+	}
+		
+#::logDebug("Running dummy database on $ctab, $mtab");
+	#dummy_database($ctab,$mtab);
+#::logDebug("Ready to eval, ctab=$ctab");
+	my $qrec;
+	my (@cols);
+	my $tab;
+	my $places;
+	my $limit_from_statement;
+	eval {
+		my $qdb = dbref($ctab);
+#::logDebug("Received qdb=$qdb");
+		$qrec = $qdb->row_hash($qid);
+#::logDebug("Received qrec=" . ::uneval($qrec));
+		if(! $qrec->{public}) {
+			$qrec->{session} eq $sessionid
+				or die ::errmsg("%s unauthorized for %s of %s", $sessionid, 'query_cache', $qid);
+		}
+		if(my $func = $qrec->{library}) {
+			if($qrec->{library_type} eq 'tag') {
+				my $Tag = new Vend::Tags;
+				my $return = $Tag->$func->(@passed);
+				if(ref($return eq 'ARRAY')) {
+					return encode_json($return);
+				}
+			}
+			elsif($qrec->{library_type} eq 'filter') {
+				
+			}
+		}
+
+## Query cache table
+#+--------------+--------------+------+-----+-------------------+-------+
+#| Field        | Type         | Null | Key | Default           | Extra |
+#+--------------+--------------+------+-----+-------------------+-------+
+#| qid          | varchar(32)  | NO   | PRI | NULL              |       | 
+#| session      | varchar(64)  | YES  |     | NULL              |       | 
+#| ipaddr       | varchar(16)  | YES  |     | NULL              |       | 
+#| qtext        | text         | YES  |     | NULL              |       | 
+#| verbatim     | tinyint(1)   | YES  |     | NULL              |       | 
+#| meta         | text         | YES  |     | NULL              |       | 
+#| cols         | text         | YES  |     | NULL              |       | 
+#| params       | text         | YES  |     | NULL              |       | 
+#| template     | text         | YES  |     | NULL              |       | 
+#| content_type | varchar(128) | YES  |     |                   |       | 
+#| public       | char(1)      | YES  |     |                   |       | 
+#| secure       | char(1)      | YES  |     |                   |       | 
+#| hash         | varchar(32)  | YES  |     |                   |       | 
+#| update_date  | timestamp    | NO   |     | CURRENT_TIMESTAMP |       | 
+#| expire_date  | datetime     | YES  |     | NULL              |       | 
+#| results      | text         | YES  |     | NULL              |       | 
+#+--------------+--------------+------+-----+-------------------+-------+
+
+		my $parser = new SQL::Parser;
+		$parser->{RaiseError} = 1;
+		$parser->{PrintError} = 0;
+
+		my $query = $qrec->{qtext};
+		my $stmt = SQL::Statement->new($query, $parser);
+
+		my $cmd = $stmt->command();
+		## only want SELECT, use library function for more
+		$cmd =~ /^select$/i or die sprintf("%s unauthorized for %s of %s", $sessionid, $cmd, $query);
+		$places = scalar($stmt->params);
+
+#::logDebug("After SQL::parser run, cmd=" . ::uneval($cmd));
+
+		($tab) = $stmt->tables();
+		$tab = $tab->{name};
+		my $c = $stmt->column_defs();
+		for my $col (@$c) {
+			$col->{type} eq 'function' and $col = $col->{value}->[0];
+			push @cols, $col->{alias} || $col->{value};
+		}
+		if( $limit and $stmt->limit() ) {
+#::logDebug("removing any external limit due to limit in statement");
+			$limit = '';
+		}
+		for(@cols) {
+			## strip leading table name
+			s/.*\.//;
+		}
+	};
+	if($@) {
+		::logDebug("Died in eval with $@, qrec->session=$qrec->{session} id=$Vend::SessionID");
+		return $default;
+	}
+
+	my $view = $qc->{meta};
+	my $qtab;
+	if($view =~ m/^\w+:+(.*)/) {
+		$qtab = $1;
+	}
+	else {
+		$qtab = $view;
+	}
+	
+	my @params;
+	my @param_names = grep /\w/, split /[\s,\0]+/, $qrec->{params};
+	my %like;
+	my %begin;
+	my %min;
+	my %refilter;
+	my @refilter;
+	for(@param_names) {
+		my $ref; my $min; my $like; my $begin;
+		s/\#//g and $ref = 1;
+		s/:(\d+)// and $min = $1;
+		s/\%$// and $like = 1;
+		s/^\^// and $begin = 1;
+#::logDebug("Final parm=$_");
+		$ref and $refilter{$_} = 1;
+		$min and $min{$_} = $min;
+		$like and $like{$_} = 1;
+		$begin and $begin{$_} = 1;
+	}
+	if(@passed) {
+		@params = @passed;
+	}
+	else {
+		for(@param_names) {
+			my $min = defined $min{$_} ? $min{$_} : 3;
+			my $val = $CGI::values{$_};
+#::logDebug("Doing $_, like=$like{$_}, val=$val refilter=$refilter{$_}");
+			if($refilter{$_}) {
+				my @do;
+				($val, @do) = split /\s+/, $val;	
+				## We only want to dothis once
+				push @refilter, @do if $refilter{$_}++ == 1;
+			}
+
+			## Here we weed out early queries
+			$min and
+				length($val) < $min and
+					return $default;
+#::logDebug("Doing $_, like=$like{$_}, val=$val");
+			if($begin{$_}) {
+				$val .= '%';
+			}
+			elsif($like{$_}) {
+				$val = '%' . $val . '%';
+			}
+			push @params, $val;
+		}
+	}
+
+	while(scalar(@params) > $places) {
+		my $killed = pop @params;
+		::logDebug( ::errmsg("Killed extra param: %s", $killed) );
+	}
+
+	my $col = {};
+	if($view) {
+		for(@cols) {
+			$col->{$_} = Vend::Table::Editor::meta_record($_,$view,$mtab);
+		}
+	}
+#::logDebug("ready for $qrec->{qtext}, param_names:" . ::uneval(\@param_names));
+#::logDebug("ready for $qrec->{qtext}, params:" . ::uneval(\@params));
+#::logDebug("ready for $qrec->{qtext}, columns:" . ::uneval(\@cols));
+
+	my $db = dbref($tab) || dbref($qtab)
+		or return $default;
+	my $dbh = $db->dbh();
+	my $sth = $dbh->prepare($qrec->{qtext} . $limit);
+	my $rc = $sth->execute(@params);
+#::logDebug("Returned $rc rows from $qrec->{qtext} (" . ::uneval(\@params) . ")");
+	return $default if $rc < 1;
+	my $ary;
+	if(! $qrec->{hash}) {
+		$ary = $sth->fetchall_arrayref();
+	}
+	elsif($qrec->{hash} =~ /jquery/i) {
+		my @out;
+		while(my $ary = $sth->fetchrow_arrayref) {
+			push @out, { value => $ary->[0], label => $ary->[1] };
+		}
+		for(@refilter) {
+#::logDebug("Refilter with $_");
+			my $re = qr($_)i;
+			@out = grep $_->{label} =~ $re, @out;
+		}
+		$ary = \@out;
+	}
+	elsif($qrec->{hash} =~ /[a-z]/) {
+		$ary = $sth->fetchall_hashref($qrec->{hash});
+	}
+	else {
+		my @out;
+		while(my $rec = $sth->fetchrow_hashref()) {
+			push @out, $rec;
+		}
+		$ary = \@out;
+	}
+	undef $sth;
+	if(my $tpl = $qrec->{template}) {
+		my @out;
+		my $pre = '';
+		my $post = '';
+		$tpl =~ s!\{PRE_TEMPLATE\}(.*)\{/PRE_TEMPLATE\}!!s
+			and $pre = $1;
+		$tpl =~ s!\{POST_TEMPLATE\}(.*)\{/POST_TEMPLATE\}!!s
+			and $post = $1;
+		$qrec->{content_type} and $Vend::ContentType = $qrec->{content_type};
+		if(ref $ary eq 'HASH') {
+			while(my ($k,$v) = each %$ary) {
+				push @out, Vend::Interpolate::tag_attr_list($tpl, $v, 1);
+			}
+		}
+		elsif( ref($ary->[0]) eq 'ARRAY') {
+			for(@$ary) {
+				my %f;
+				@f{@cols} = @$_;
+				push @out, Vend::Interpolate::tag_attr_list($tpl, \%f, 1);
+			}
+		}
+		else {
+			for(@$ary) {
+				push @out, Vend::Interpolate::tag_attr_list($tpl, $_, 1);
+			}
+		}
+#::logDebug("Getting ready to return " . ::uneval(\@out));
+		return join "", $pre, @out, $post;
+	}
+	else {
+#::logDebug("Getting ready to return " . ::uneval($ary));
+		return encode_json($ary);
+	}
+}
+
+
 1;
 
 __END__
diff --git a/lib/Vend/Dispatch.pm b/lib/Vend/Dispatch.pm
index a2453ce..68a5942 100644
--- a/lib/Vend/Dispatch.pm
+++ b/lib/Vend/Dispatch.pm
@@ -1301,6 +1301,7 @@ sub dispatch {
 		# do nothing
 	}
 	elsif ($::Instance->{CookieName} and ! $Vend::Cfg->{InternalCookie} and $CGI::cookie) {
+		$Vend::allow_qc = 1;  ## Allow the QueryCache AJAX access
 		$CGI::cookie =~ m{$::Instance->{CookieName}=($Vend::Cfg->{CookiePattern})};
 		$seed = $sessionid = $1;
 		if($Vend::Cfg->{InternalCookie}) {
@@ -1314,7 +1315,9 @@ sub dispatch {
 	}
 	elsif ( $CGI::cookie
 			and $::Instance->{CookieName} ||= 'MV_SESSION_ID'
-			and $CGI::cookie =~ /\b$::Instance->{CookieName}=(\w{8,32})(?:[:_]|%3[aA])([-\@.:A-Za-z0-9]+)/ ) {
+			and $CGI::cookie =~ /\b$::Instance->{CookieName}=(\w{8,32})(?:[:_]|%3[aA])([-\@.:A-Za-z0-9]+)/ )
+	{
+		  $Vend::allow_qc = 1;  ## Allow the QueryCache AJAX access
 	  SESSION_COOKIE: {
 	      my $id = $1;
 	      my $host = $2;
@@ -1336,6 +1339,12 @@ sub dispatch {
 
 	Vend::Server::set_process_name("$Vend::Cat $CGI::host $sessionid");
 
+    my $qc;
+    if($qc = $Vend::Cfg->{QueryCache} and $CGI::path_info =~ m{^/$qc->{intro}/} ) {
+        ## Received cached query. Will gate $Vend::allow_qc in target (allows public queries)
+        return response(Vend::Data::run_query_cache($qc,$sessionid));
+	}
+
 	$::Instance->{CookieName} = 'MV_SESSION_ID' if ! $::Instance->{CookieName};
 
 	$CGI::host = 'nobody' if $Vend::Cfg->{WideOpen};



More information about the interchange-cvs mailing list