WebDB updated

Yesterday I added a little fix to WebDB for a bug that prevented filtering on column names that occur in both the table being filtered and any of the tables referred to by foreign keys. This most often applied to id columns.

[Update:] And today I’ve (hopefully) fixed the problem with foreign-key fields in edit forms not being able to be emptied. How did that live this long?! Sorry.

Saving query string parameters between requests

In which I try to implement column sorting in WebDB, but end up building a general system for persisting query string variables between HTTP requests. Sort of.

In the old, Zend-based, WebDB, column sort order was passed as a $_GET parameter and then stored in per-table variables in $_SESSION. In the index controller, both were checked when a table was loaded:

/* application/controllers/IndexController.php */
$orderby = ($this->_hasParam('orderby')) ? $this->_getParam('orderby') : $_SESSION['ordering'][$this->tableName]['orderby'];
$orderdir = ($this->_hasParam('orderdir')) ? $this->_getParam('orderdir') : $_SESSION['ordering'][$this->tableName]['orderdir'];

The advantage, of course, being that a user could sort by a column, go off somewhere else (into a record to edit it, say), and come back to the table view and have the previous sort order be remembered. Good. But annoying, too, because a table could be sorted without a sort directive being present in the URI. Not good, because the URI doesn’t reflect exactly the resource that one is looking at; one can’t bookmark that page, for instance.

We need the best of both:

  1. remembering the users’ sort preferences between requests; and
  2. ensuring that the current URI is an accurate identifier for the current resource.

The obvious answer seems to be to store sort directives in the session, and redirect requests to a proper, complete, URI if the current one doesn’t already contain those directives. Sounds good.

However, before jumping in and coding that: the same ideas apply to the filter variables, which are currently passed as $_GET parameters. So could we generalize this idea of storing parameters in $_SESSION but only ever using them from $_GET? (Oh, and I should point out that the validation of these parameters happens after this, and we’re not worried about that right now.) Can this be done in a way so that the rest of the application doesn’t need to know anything about the session stuff, and can just use $_GET['foo'] and href="?foo=bar" in whatever way it wants?

A first draft:

// To be called from Controller_WeBDB::before()
if (count($_GET)>0)
{
	$_SESSION['qs'] = $_GET;
}
elseif (isset($_SESSION['qs']) && count($_SESSION['qs'])>0)
{
	$query = URL::query($_SESSION['qs']);
	$_SESSION['qs'] = array();
	$uri = URL::base(FALSE, TRUE).$this->request->uri.$query;
	$this->request->redirect($uri);
}

Which sort of works, except that it’s not possible to specify only some of the parameters (i.e. either load them all from $_GET or all from $_SESSION, which isn’t what we want).

A final draft (a new method in Controller_WebDB, called from Controller_WebDB::before()):

/**
 * Save and load query string (i.e. `$_GET`) variables from the `$_SESSION`.
 * The idea is to carry query string variables between requests, even
 * when those variables have been omitted in the URI.
 *
 * 1. If a request has query string parameters, they are saved to
 *    `$_SESSION['qs']`, merging with whatever is already there.
 * 2. If there are parameters saved in `$_SESSION['qs']`, and if they're
 *    not already in the query string, add them and redirect the request to
 *    the resulting URI.
 *
 * @return void
 */
private function _query_string_session()
{
	// Save the query string, adding to what's already saved.
	if (count($_GET)>0)
	{
		$existing_saved = (isset($_SESSION['qs'])) ? $_SESSION['qs'] : array();
		$_SESSION['qs'] = array_merge($existing_saved, $_GET);
	}

	// Load query string variables, unless they're already present.
	if (isset($_SESSION['qs']) && count($_SESSION['qs'])>0)
	{
		$has_new = FALSE; // Whether there's anything in SESSION that's not in GET
		foreach ($_SESSION['qs'] as $key=>$val)
		{
			if (!isset($_GET[$key]))
			{
				$_GET[$key] = $val;
				$has_new = TRUE;
			}
		}
		if ($has_new)
		{
			$query = URL::query($_SESSION['qs']);
			$_SESSION['qs'] = array();
			$uri = URL::base(FALSE, TRUE).$this->request->uri.$query;
			$this->request->redirect($uri);
		}
	}
}

To clear a parameter, it obviously has to actually be set, and not just omitted from the URI. Is this going to be a problem?

The other glaring issue with the above code seems to be the insecurity in storing unvalidated content in $_SESSION, but as this content is only ever being fed back into the URI, I don’t think there’s too much of a problem there. Nowhere else in WebDB will use $_SESSION['qs'] — everything will only use $_GET, and be responsible for validation.

Well, I seem to have got somewhere with the issue of saving $_GET parameters between requests, but nowhere yet with what I started out with: sorting the columns! However, this is now going to be a pretty straight-forward matter of adding links to column headers that toggle ?orderby=asc etc. I’ll get to it next first thing next week.