Sunday, 12 June 2011

Vocabulary by machine name, taxonomy_get_tree() and $user

(Amended on 17 Jul 2011 to replace reloading entire entities with using field_attach_load().)

Getting the ID of a vocabulary has long been a problem in Drupal but Drupal 7 goes a long way to resolving that:

$voc = taxonomy_vocabulary_machine_name_load('vocab_machine_name');

And you've got it.

However if you want to load a taxonomy_tree you still have to use the vocabulary ID so you run the line above first, and then:

$tree = taxonomy_get_tree($voc->vid, [parent], [depth], [internal value]);

The last two parameters on taxonomy_get_tree() have been swapped so if you want to specify the depth you don't have to do (..., -1, $depth) just (..., $depth). Which is handy.

Note that taxonomy_get_tree() does not return a full entity, which is unfortunate if you want any fields that have been attached to the terms. You have to fetch the fields separately but it's not hard:

$tree = taxonomy_get_tree($voc->vid);
foreach ($tree as $key => $term) {
  $terms[$key] = $term->tid;
}
field_attach_load('taxonomy_term', $terms);


The same applies to the global $user variable. This is normally the user without attached fields, so if you need a field that has been attached...


global $user;
field_attach_load('user', array($user->uid => $user));

Hope that helps.

Thursday, 9 June 2011

Contextual links

Today wasn't the first time I've had a fight with contextual links, but I decided that this time I would get to the bottom of why they are such a pain to implement.

What's a contextual link? The little on-screen tools that let you jump to editing/deleting nodes, views, configure blocks and so on. (But not taxonomy - however I'll get to that later.)

The idea behind a contextual link is as simple as it is useful. If you've viewing a node teaser somewhere on the page then, if you have the permissions, a little dropdown menu will be available that lets you edit that node. Or if it's a block you can configure the block. If it's a Views block you can either edit the view or configure the block.

And so on.

But what if you want to add your own contextual links?

If you are filling a custom block then it's not very hard. I had a situation where I displayed a block which contains some text pulled from a node - but the node can vary depending on the current setting of site taxonomy.

I wanted to add a contextual link that would allow an administrator to edit the text. So I needed to create a contextual link that would link to the currently displayed node:

$build = array(
  '#contextual_links' => array(
    'node' => array('node', array($node->nid)),
  ),
 ... rest of the content
);

The first 'node' is the module that provides the links we want, the second 'node' is the first part of the link URL while the array contains any further parts to add to the link URL.

So what we get here is node/nid/edit and node/nid/delete - the contextual links module figures out what URLs the current user can access from the base supplied URL (in this case node/nid). If the user can edit the node, but cannot delete it he'll only get the 'edit' link.

What this means is that you can't just add any old links, you have to work with the system.

Why is the second parameter an array? Because you might have more than one variable value - these are arguments for a URL, so for a block's contextual links you have:

'block' => array('admin/structure/block/manage', array($block->module, $block->delta)),


So far so good.

For my current project I needed to create a list of taxonomy subjects and I wanted to allow the name of the term to be editable. This is where things started to get hard. It was hard because I did not know three things:

The first thing to understand is that contextual links only work for template themes - not for function-based themes. The reason has nothing to do with the contextual module - it's because generic preprocess theme hooks are only called for templates. And the contextual module uses a generic preprocess function to find the elements that require links.

The second thing to understand is that your template must contain "render($title_suffix)" because that is where the contextual links are contained and they are not rendered until you specifically render them. You can use block.tpl.php as an example.

This was all fine except for the third thing: my taxonomy link would not display even though I thoroughly checked it's format and tried other types of contextual link in the same location which all worked fine. Just not the taxonomy/term/[tid]/edit.

Luckily I'd been reading up on everything to do with contextual links and remembered that someone somewhere had mentioned the 'context' setting in hook_menu(). This is the third thing that has to be in place for a link to appear, so I added a hook_menu_alter() function:

function mymodule_menu_alter(&$items) {
  $items['taxonomy/term/%taxonomy_term/edit']['context'] = MENU_CONTEXT_INLINE;
}

And suddenly my link appeared.

Finally there is the fact that if you want to put in a node create link more hacking is needed - although it's all legal Drupal. I picked this up from this blog. Essentially you have to make the URL appear to be a child item like this (in hook_menu_alter):


$items['node/add/page']['_tab'] = TRUE;
$items['node/add/page']['tab_parent'] = 'node/add';
$items['node/add/page']['context'] = MENU_CONTEXT_INLINE;


As it says on Marcus's site, it's not clear whether this will actually break the menu system, but it doesn't seem to.

I hope that's some help.

Wednesday, 4 May 2011

Search Query bug

I've just issued a bug report and patch for a problem with the Search Query class. It's not a huge bug but it prevented me from doing something I needed to do: add some restrictions to a Search Query.

There are two ways of modifying queries in Drupal 7, you can either use a generic "hook_query_alter()" or use "hook_query_TAG_alter()". Tags are a way of telling other modules what a query is about. For example you can have a tag "node_access" which allows modules to add additional restrictions on the query, in this case the Node module connects the query to node_grants.

The Search Query adds a "search_node" or "search_user" tag, unfortunately it adds the tag after the hooks have been called - which is as useful as a chocolate teapot. So I added the patch here: http://drupal.org/node/1146564 which simply moves the place where the tags are added.

Another thing to watch out for: the generic hook_query_alter() is not called on queries that have no tags. This is an efficiency thing as it was discovered early on that this hook was called a lot for no good reason during bootstrap.

Tuesday, 3 May 2011

Field encryption

In the next couple of weeks, along with my field_extract module (which allows developers to extract values from fields more easily), I'll also be uploading the initial version of the field_encrypt module (it works, but big sites would be problematic).

An "encrypted field" module already exists for Drupal 7 which allows you to add a special text field, that is encrypted, to an entity, but that's not what I needed. What I needed was a way of encrypting any field even if it already exists (install the module and choose which fields you'd like to encrypt).

There are a three handy hooks which make this possible: 'hook_field_storage_pre_insert', 'hook_field_storage_pre_update', and 'hook_field_storage_pre_load'; they allow other modules to intercept those actions and save or load a field - replacing the field module actions. So, knowing what fields I want to encrypt, I can intercept all load and save actions with those fields and replace them with my own save/load.

This module is only a first attempt and stores all encrypted data in one table. It would be more efficient to create separate tables for separate fields the way Drupal does. But that's for the future. It also doesn't handle deletion but I don't see that as a major issue for small sites. (There are some other areas where it would, in this initial version, have problems with big sites.)

Having said all that there is still a huge hole in security: In Drupal 7 loaded fields are stored in the "cache_field" table - completely unencrypted. I'll have to solve that too for the current client.

Sorry it's taking me so long after promising these modules but I've been rather busy...

EDIT: As I hate leaving too many loose ends the field_encryption module now checks for the presence of the mcrypt library before letting itself be installed; prevents itself being uninstalled if it is currently encrypting a field (to prevent data loss); and now overrides the standard "cache_field" functionality and encrypts cached field data as well. So it's about as secure as it can get.

Thursday, 28 April 2011

Chrome image bug and file headers

There is a bug in Chrome which means that, under some circumstances, an image can be displayed on the page for a moment and then disappear.

http://www.google.com/support/forum/p/Chrome/thread?tid=1d825178248ab136&hl=en

The fix is to remove the Content-Length header. Unfortunately that is virtually impossible to do in Drupal without hacking core.

Going into this problem revealed an interesting omission: the file_download() function collects headers from interested modules, but if two modules offer the same header the earlier one gets overwritten. Heavier modules can overwrite the headers of lighter modules.

So making my module heavy would mean that I could overwrite Content-Length with NULL.

But this is not a good solution. What's missing is that, after collecting the headers, file_download() does not use drupal_alter() to allow modules to change the headers.

So the correct solution is a patch which inserts drupal_alter('file_download_headers', $headers, $uri) into the file_download() function at the correct location. And you can find the patch here:

http://drupal.org/node/1137534

All I had to do then was create a function that implemented hook_file_download_headers() which took a look at the $uri and, if it's a graphic, remove the Content-Length header, which is perfectly safe because it's not a critical header.

Tuesday, 19 April 2011

Conditions for using where

Just to help you avoid spending as many hours as I have trying to force a nasty little query with a subquery to do what I wanted...

If you want to have a WHERE clause that contains something like "fss.entity_id = n.nid", you can't use the condition function. In other words, you can't do this:

$query->condition('fss.entity_id', 'n.nid');

You have to use this:

$query->where('fss.entity_id = n.nid');

Because the first version gets translated into fss.entity_id = 'n.nid' in the SQL, which will never be true.

In debugging this it slowly became obvious that something fundamental was wrong - because when I output the $query string and then replaced the placeholders with the right arguments, and shoved it straight into the MySQL server - it worked.

Eventually it dawned on me to use the Devel query output to see what was actually being called and found the error - though by that time I was looking for it, having exhausted every other option.

Here's a quick tip for outputting the entire contents of an object which contains public and private properties:

dpm(print_r($object, TRUE));

I could have done without that today.

Tuesday, 12 April 2011

CSS and Overlays

This might seem obvious but if you need to modify the CSS for your admin pages which appear in overlays, you will be needing to create a subtheme.

Assuming you use Seven for your admin theme, create a subtheme based on Seven, let's say you call it SevenUp, and add your own CSS overrides, then change the default admin theme to SevenUp.

You'll get all the Seven settings and your overrides.

However when you enable your theme you'll have to go to the Block admin page and remove all the blocks which have now added themselves.