Thursday 15 December 2011

MD5s as IDs

While there is the chance of duplication it can be handy to use MD5s for creating a "unique" ID for strings. I had this in my current project but I've done it before and there is a potential problem which is more likely than duplicate IDs.

There is a chance that you'll get an MD5 that begins with a zero and, potentially even worse, one that begins with a zero and is all decimal numeric (does not contain a-e digits).

In this instance, with the loose typing of PHP, you might find your MD5 gets converted to a number and loses its leading zero (or zeroes). In which case it's useless as an ID and it will take you a very long time to track it down. I know the first time it happened to me it took me a couple of days.

But there is a very simple solution, when you create your MD5 do an immediate search and replace to change all zeroes into 'g'.

$id = str_replace('0', 'g', md5($source));

Now you can be sure you will never lose your leading zero, because there isn't one.

(By the way, after 12 weeks my field_extract module has received no complaints or bug reports so I shall be promoting it to a full version.)

Wednesday 16 November 2011

Ssssh

I've not posted recently because my current job involves Drupal 6 so I've done virtually no D7 work for a while. However I do have a new set of modules for developers coming soon which will be in both D6 and D7 varieties.

Essentially it's a system that does a similar job to CTools plugins but is lightweight and standalone. It's very good for de-coupling dependent custom modules, essentially very simple but very versatile.

There's a core module that provides the API (a very small module indeed) and then some other modules that demonstrate how to use it and provide handy facilities at the same time.

Hopefully I'll have that out by Christmas. I'm also happy to say that my field_extract module is doing very nicely  in the module usage charts currently standing at 99 sites.

Monday 26 September 2011

Automatic file download

I have a personal project I'm working on which requires an automatic download link. You know the sort of thing - like you get on SourceForge: "Your file will download in x seconds". And then the file download dialog pops up.

All the solutions for this are JavaScript - and if you want a countdown then JS is the way to go. But I didn't want JavaScript.

The basic part of the trick isn't limited to Drupal at all, you just HTML:

<meta http-equiv="Refresh" content="1; URL=http://www.yoursite.com/system/files/downloadfile.ext" />

So here, after one second, the page refreshes - except it's a file to be downloaded, so your browser does the right thing and gives you a download dialog box instead.

Easy.



Sunday 11 September 2011

field_extract module in beta

EDIT: Now out of beta. Fully implemented.

EDIT: You can now get the module from http://drupal.org/project/field_extract and it works properly with Drush.

My incredibly useful field_extract module is now available on drupal.org. I am slightly embarrassed because something went wrong in the project creation process so the actual URL is:

http://drupal.org/project/1158878

And when you download it the module comes inside another folder "1158878". In fact this will work just as it comes, you'll just get the folder 1158878 in your modules folder with field_extract inside that.

If you're competent you can extract the inner "field_extract" module and get rid of the outer 1158878 folder but, as I say, it should work out of the box anyway. (I may get around to re-uploading it properly at some point but I can't wait any longer to get it out to you.)

So what does it do? For a start, this is only a developer's module. It does nothing by itself and should only be downloaded if requested by another module. But what it does do is provide a couple of functions that make it much easier to extract field data from entities.

The project page provides clear instructions on how to use it.

Enjoy.

Wednesday 7 September 2011

Reducing the footprint

Sorry I haven't blogged in a while, I have been working very hard on commercial D7 sites in my day job but haven't really come across anything spectacularly D7 I wanted to talk about.

So here's something unspectacular but quite important, and applies almost as much to D6 as D7.

Every module eats memory. Every x.module file (for enabled modules) gets loaded for every access (well, let's ignore caching). So anything that can be done to reduce the files loaded (size in this case) can only be a good thing. (Back in the day I wrote code for machines with 128K of memory of which 20K might be used for the screen, and another 32K for the OS - in those days you really had to think about keeping code as compact as possible.)

Drupal uses hooks to gather information from modules, and then it stores this information in the database caches (or other tables), after which it no longer calls these hooks - unless the caches get cleared and it has to build the information again.

Hooks like hook_menu, hook_theme, hook_permission and so on. But these hooks need to be available in the x.module file - and yet they are only called infrequently.

Solution? Put a minimal hook in the x.module file which loads another file (say x.registry.inc) and then calls the actual hook code which is in there. Like this:

In x.module:

function x_menu() {
  module_load_include('registry.inc', 'x');
  return _x_menu();
}

And in x.registry.inc:

function _x_menu() {
  $items = array();


  $items['my/path'] = array(
    // my/path data  );


  return $items;
}

If a hook is called on pretty much every page there is no advantage in doing this. But if it's a hook that's called infrequently it's definitely worth it.

This is something I'd played with back in my D6 days but then stopped using. But then I saw another contributed module that was using it and thought "yep, I should be doing that". So I am and here it is.

hook_hook_info()

There is another solution using hook_hook_info() which allows you to define a file in which hooks can be found. This does not work for the majority of system modules because they use customised hook-calling methods. Only one system module - Tokens - has it defined so token code can be located in x.tokens.inc.

However the Block module only uses standard hook-calling functions and you could specify a file for block management using hook_hook_info_alter(); I wrote something about this here.

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.

Friday 1 April 2011

drupal_get_destination and overlays

There may be a better way of doing this, but...

Had the situation where I had an admin page (set up to appear in an overlay) on which there were other links to forms. The problem was that when I clicked "Submit" on one of these forms it went back to the original page and not the previous overlay page.

If there is a destination in drupal_get_destination() this takes precedence over any form setting. So I needed to somehow force this to be what I wanted. The solution is relatively easy though might be considered a bit hacky.

What you need to do is this:

$_GET['destination'] = $_GET['q'];

which forces the current page to be the one you go back to. But this won;t always work because if drupal_get_destination() has already been called the destination will now be held in drupal_static(). So what we actually need is this:

drupal_static('drupal_get_destination', NULL, TRUE);
$_GET['destination'] = $_GET['q'];

This wipes out any older version and ensures you get what you want.

Thursday 31 March 2011

Being too clever with forms

The situation I ran into was with a content type called "document", it's pretty simple: title, description, taxonomy term and a single file upload.

The tricky bit was the taxonomy: each document could belong to a different part of the site (represented by the top level of the taxonomy) let's say SectionA and SectionB, within each of those there'd be subsections: SubA1, SubA2... and SubB1, SubB2 etc.

The first level was selected by the place the document was created - so someone might be in Section A, they create a document - I add the taxonomy term to the URL, like this: node/add/document/X and, in a hook_form_alter(), I use that added value to modify the allowed options in the taxonomy selector to only include the subsections for the section we came from.

Which is fine.

Except when you factor in the file upload. What a file upload does is rebuild the form - which meant my code was getting called again and this was bad. The real code was a bit more complex than I've described here and reloading was very bad - basically it completely wiped out the taxonomy options on the second run through. Which then meant I got validation errors.

The solution is that you have to have some way of noting that you've been here before and not do the changes again. One thing know is that $form_state does not get rewritten so you can store a variable in there marking that you've done the changes already, it should look something like this (using hook_form_FORMID_alter()):

function mymodule_form_myform_alter(&$form, &$form_state) {
  if (isset($form_state['mymodule_myform_processed'])) {
    return;
  }
  $form_state['mymodule_myform_processed'] = TRUE;


  ...the rest of the code...
}

And that does the trick.

Wednesday 30 March 2011

Using stream wrappers

Sorry I haven't uploaded my two new modules to d.o yet - just not had time.

In my current contract the client wanted the option of either downloading a PDF or opening it in the browser (assuming the browser could handle it - but that's not my problem). These PDFs being stored in the Private file space.

The first question they had was: can it be done? They'd been told it couldn't. But I assured them with naive certainty that it probably could. I had already sorted out the ability to auto-create and download archives and I was pretty sure it was merely a matter of the correct HTTP headers.

But how to do it? What I needed was a different path to the same file, one path would download it, the other path would display it in the browser.

If you're using Private files the URL you get is 'system/files/filename.pdf' which goes through various checks to ensure the download is permitted, and gets the HTTP headers at the same time, using hook_file_download(). The path fragment "system/files" is what invokes the file_download() function. If you create a different stream wrapper, such as my archive stream, this is replaced with "system/archive" - or whatever you want really.

So I thought, okay, how about I create a new stream wrapper based on a "system/views" path which invokes file_download() but extracts the Content-Disposition header before sending it.

The trick here is to make the new stream wrapper point to the same location as the Private stream - so it can access the same files - and then, when I'm building the download link, I get the file's URL (e.g. "system/files/filename.pdf") and simply do a string replace, changing "files" to "views".

So the user clicks on my modified URL, it goes through my routines which get the headers but strip out the Content-Disposition header and, as a result, instead of downloading the file, it opens in the browser. I have two links to the same file, each one does something different with the file.

In retrospect using the word "Views" may be a little confusing, obviously it's nothing to do with the Views module - sorry about that.

You need the stream wrapper class...


class ViewsStreamWrapper extends DrupalLocalStreamWrapper {
  /**
   * Implements abstract public function getDirectoryPath()
   *
   * This is identical to the private stream wrapper which means
   * we can just replace 'files' with 'views' the path and get the same file
   */
  public function getDirectoryPath() {
    return variable_get('file_private_path', '');
  }


  /**
   * Overrides getExternalUrl().
   */
  public function getExternalUrl() {
    $path = str_replace('\\', '/', $this->getTarget());
    return url('system/views/' . $path, array('absolute' => TRUE));
  }
}

You need to tell Drupal about the class...


/**
 * Implements hook_stream_wrappers().
 */
function views_stream_stream_wrappers() {
  return array(
    'views' => array(
      'name' => t('Views'),
      'class' => 'ViewsStreamWrapper',
      'description' => t('Stream wrapper for viewing files in a browser'),
      'type' => STREAM_WRAPPERS_READ,
    ),
  );
}



And then the hook_menu() and hook_file_download() support, note this code assumes a PDF.


/**
 * Implements hook_menu().
 */
function views_stream_menu() {
  $items = array();


  $items['system/views'] = array(
    'title' => 'View files',
    'page callback' => 'file_download',
    'page arguments' => array('views'),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );


  return $items;
}


/**
 * Implements hook_file_download().
 *
 * Construct the headers to ensure the file gets downloaded
 */
function views_stream_file_download($uri) {
  list($scheme, $path) = explode('://', $uri, 2);
  if ($scheme=='views') {
    $file = (object) array(
      'filename' => basename($path),
      'filemime' => 'application/pdf',
      'filesize' => filesize(drupal_realpath($uri)),
    );
    $headers = file_get_content_headers($file);
    unset($headers['Content-Disposition']);
    return $headers;
  }
}


And then as an example of modifying a private file. The $uri variable contains the file's internal path which you can get from a file entity, or file field.


$href = file_create_url($uri);
$href = str_replace('/files/', '/views/', $href);


$link = array(
  '#type' => 'link',
  '#title' => t('View'),
  '#href' => $href,
  '#attributes' => array('target' => '_blank'),
  '#weight' => 0,
);


And that's all there is to it.

Monday 21 March 2011

Coming to a website near you

Now I don't want you to get too excited but I have recently completed two little Drupal 7 modules designed to make my life easier - so may well help other developers:

field_extract
This takes the little function I built to extract values from fields to the extreme by attaching "extractors" to the cached field type data, and then using the appropriate extractor to extract the data from a field. So there's one extractor that gets nodes, another that gets terms, another for fields, for body, for text, for integers and so on. It follows proper Drupal guidelines and is completely extensible.

archive_stream
Makes it easy to build a downloadable Zip archive using stream_wrappers. You give it a temporary filename and an array of file info arrays (or file entities) and it gives you a file path you can use for downloading. The module also handles the downloading part when a user accesses the link.

Simples.

It may take a few days to get them on to drupal.org but I'll let you know.

Thursday 17 March 2011

Getting field data out of entities

EDIT: download the module that encapsulates this behaviour and does a lot more besides: read this blog.

The new field structure in Drupal 7 is very clever but also quite irritating when you want to get data out of a field in an entity, so I have written this handy routine which extracts field data:


/**
 * Returns field values as actual entities where possible,
 * also allows selection of individual items to be returned
 */
function field_fetch_field_values($entity_type, $entity, $field_name, $get_delta = NULL, $get_key = NULL) {
  $values = array();
  if (isset($entity->$field_name) && !empty($entity->$field_name)) {
    foreach (field_get_items($entity_type, $entity, $field_name) as $delta => $item) {
      $value = $item;
      $keys = array_keys($item);
      if (count($keys)==1) {
        $key = $keys[0];
        switch ($key) {
          case 'nid':
            $value = array_shift(entity_load('node', array($item[$key])));
            break;
          case 'uid':
            $value = array_shift(entity_load('user', array($item[$key])));
            break;
          case 'tid':
            $value = array_shift(entity_load('taxonomy_term', array($item[$key])));
            break;
          case 'vid':
            $value = array_shift(entity_load('taxonomy_vocabulary', array($item[$key])));
            break;
          case 'value':
            $value = $item['value'];
            break;
        }
      }
      else {
        if ($get_key && isset($item[$get_key])) {
          $value = $item[$get_key];
        }
        elseif (array_key_exists('value', $item)) {
          $value = isset($item['safe_value']) ? $item['safe_value'] : $item['value'];
        }
      }
      $values[$delta] = $value;
    }
  }
  if (is_numeric($get_delta)) {
    return isset($values[$get_delta]) ? $values[$get_delta] : NULL;
  }
  return $values;
}


Use it like this:

$terms = field_fetch_field_values('node', $node, 'field_myterms');

Extracts all the tids from the field and returns the actual terms, not the tids.

$term = field_fetch_field_values('node', $node, 'field_myterms', 0);

Returns just the first term.

$summary = field_fetch_field_values('node', $node, 'body', 0, 'safe_summary');


Returns only the summary from the body field, but does not create a summary if there isn't one.


$info = field_fetch_field_values('node', $node, 'field_myfiles', 0);


If field_myfiles contains uploaded file data you get the full file info array back, in this example just the first one.


There are probably cleverer ways of doing this (and you could do multiple entity_loads to make it more efficient) but I find this works well enough for normal day-to-day use.

Tuesday 8 March 2011

Using Views custom text

So here's the thing: for the current project I had to produce an administrative list of node types, meetings in this case, and alongside have a bunch of links to edit, delete and one for other management stuff.

Views is your friend. Creating a table output of a list of nodes ordered by meeting date is easy-peasy but the challenge here is adding links.

It crossed my mind that I could intercept Views output in half a dozen places, programmatically or by creating replacement templates but that sounds far too much like hard work. I was sure that I'd seen an option to add custom text to a View somewhere, and I knew from a project last year sometime that Views very kindly allows you to use tokens built from fields being displayed in the row it's building.

And so it turns out ... with some caveats:

I had configured the view to output three columns - Name, Date/Time and Location - now I needed a column with the links.

So I added a field, selected "Global" and chose Custom text.

Then added the text: <a href="node/[nid]/edit">Edit</a>

I'll keep it simple and just use one link.

Now this doesn't work, why? Because the tokens can only be for fields that have been used and appear <em>before</em> the field you need them. So I had to add a "Node: Nid" field and put it at the top of the list - remembering to check the "Exclude from display" box because I don't want a column of nids in the table.

And now - I get garbage in the link. The nid numbers are going in okay, but the HTML is all screwed up.

This is the caveat: I was pretty sure, with Drupal 6, that you get the raw output of the field. So if I ask for the nid all I get is the nid (could be wrong on that, I'm not going to check). But it turns out that with Drupal 7 Views what you get in the token is the processed output of the Node: Nid field.

I recommend unchecking every option, but especially uncheck the "Link this field to its node" box, because that's what I was getting, where I have [nid] in the URL it was embedding a complete link.

Having said that, once you take out all the extra options you just get the raw nid. And the link now works.

Monday 28 February 2011

The trouble with block_view_MODULE_DELTA_alter

If you want to modify a specific menu block using hook_block_view_MODULE_DELTA_alter() you are out of luck. Let's say you have a menu called "Footer menu" and you want to change its styling so you set up a function like this:

function mymodule_block_view_menu_menu_footer_menu_alter() {}

That is not a mistake: the first "menu" is the module name, and the menu module adds "menu" to the front of the menu name converted to lower case, so the delta for my "Footer menu" is "menu-footer-menu".

But this function will never get called (as of 7-rc4). Why? Because the menu module uses hyphens in the delta name, and the block module does not convert these hyphens to underscores so it tries to call: mymodule_block_view_menu_menu-footer-menu_alter() - which will fail, of course.

You have two choices: Either use only hook_block_view_alter() and check you've got the right block before changing it, or patch block.module until it gets fixed in core.

I've already issued a bug report here http://drupal.org/node/1076132 with the fix, so you can use it if you want to.

Tuesday 22 February 2011

Proper way to get field values

EDIT: download the module that encapsulates this behaviour and does a lot more besides: read this blog.

If you haven't noticed the stored structure of fields in an entity, like a node, is not simple:

$entity ->fieldname[language][delta] = [item]

The Field API does contain useful functions for manipulating these values, so if you have a $node and a field_myfield, you can get the current items like this:

$items = field_get_items('node', $node, 'field_myfield', $node->language);

Which returns an array of the items in the field, indexed by their delta value.

The actual content of each item will be dependent on what it is, a simple textfield will have array('value' => [text]) while a term reference will have array('tid' => [tid]), a "longtext with summary" (such as the standard "body" field) has several values: for the main body ('value'), for the summary ('summary'), for the filter format ('format'), and already processed "safe" values of both the body (safe_value) and the summary (safe_summary).

You can find this, and other useful functions, at Field API.

EDIT: I now have a super-duper function that extracts values for you here.

Monday 21 February 2011

drupal_write_record() and properties

It would be nice if you could use your own classes and objects with drupal_write_record(), but you can't - that's if you want to have private or protected properties.

Let's say you have this class:

class myClass {
  protected $xid, $nid, $mydata;


  public function __construct($nid, $mydata, $xid = 0) {
    $this->xid = $xid;    $this->nid = $nid;
    $this->mydata = $mydata;
  }

  public function __get($property) {
    if (property_exists($this, $property) {
      $value = $this->$property;
    }
    return $value;
  }

  public function __set($property, $value) {
    switch ($property) {
      case 'xid':
         // Don't allow xid to be set, it's read-only
         break;
      default:
        if (property_exists($this, $property) {
          $this->$property = $value;
        }
    }
  }

  public function __isset($property) {
    return isset($this->$property);
  }

  public function save() {
    if ($this->xid) {
      $this->update();
    }
    else {
      $this->insert();
    }
  }

  protected function insert() {
    drupal_write_record('mydata_table', $this);
  }

  protected function update() {
    drupal_write_record('mydata_table', $this, 'xid');
  }
}

This won't work because the drupal_write_record() function uses 'property_exists()' to check for properties and not 'isset()', and that will not return TRUE for private or protected properties.

One of the features of OOP is encapsulation, making data that should not be accessible, not accessible but that means you cannot use drupal_write_record(). If you want to use that function directly with your own classes you have to make the properties public - defeating the point of encapsulation.

However the solution is not too bad, you can do this:

  protected function insert() {
    $object = (object) array();
    foreach ($this as $field => $value) {
      $object->$field = $value;
    }

    drupal_write_record('mydata_table', $object);
    $this->xid = $object->xid;
  }

  protected function update() {
    $object = (object) array();
    foreach ($this as $field => $value) {
      $object->$field = $value;
    }

    drupal_write_record('mydata_table', $object, 'xid');
  }

If truth be told, for the 'insert()' to work you'd need to make 'xid' publicly accessible anyway (since drupal_write_record() wants to write to it), so perhaps this solution is for the best.

Saturday 19 February 2011

field_delete_field ooops

I'm in the process of converting a D6 module to several D7 module.

The original required manual set up of a node type and attaching various CCK fields before being able to run - a perfectly OK scenario for Drupal 6. However for D7 I wanted the module to create the node and attach the fields automagically - a perfectly acceptable scenario for Drupal 7. (I've learned only too well recently that entities, though powerful, require a lot of effort so that conversion can wait.)

One thing I've done is extracted the specialist field I need into its own module - in a similar fashion to the various field modules (text, number and so on).

So I put it together and installed on a minimum D7 installation. All good. I uninstall and then reinstall - and en exception gets thrown because the DB tables for my new field have not been renamed. Drupal 7, for speed, renames discarded tables then deletes them at its leisure during hook_cron.

I know this process works because I've seen it in action - when I've been creating fields through the User Interface. But it was failing during hook_uninstall() - in other words field_delete_field() just fails completely but without reporting an error.

Turns out this is a known bug. And it's because when a module is disabled its fields are tagged as inactive and the routine that collects the file_info does not collect information about inactive fields.

The only solution is to use this function from the field.install file:


function _update_7000_field_delete_field($field_name) {
  $table_name = 'field_data_' . $field_name;
  if (db_select($table_name)->range(0, 1)->countQuery()->execute()->fetchField()) {
    $t = get_t();
    throw new Exception($t('This function can only be used to delete fields without data'));
  }
  // Delete all instances.
  db_delete('field_config_instance')
    ->condition('field_name', $field_name)
    ->execute();


  // Nuke field data and revision tables.
  db_drop_table($table_name);
  db_drop_table('field_revision_' . $field_name);


  // Delete the field.
  db_delete('field_config')
    ->condition('field_name', $field_name)
    ->execute();
}

There is a patch on the go for fixing but this will have to do until it arrives.

Wednesday 16 February 2011

Allowing and disallowing dates in Date Popup

I have just uploaded a patch for the Date Popup module which allows dates to be allowed and disallowed in the popup calendar.

You can find it here.

This was a critical issue for the current commercial project I'm working on, and after having my knuckles wrapped by KarenS for complaining and not helping - I helped.

Sunday 13 February 2011

Blocks and hook_hook_info()

As an ongoing attempt to keep the amount of code loaded for any given page down to a minimum the Drupal 7 developers came up with hook_hook_info() - which is a completely different hook from the one in Drupal 6.

What this hook does is allow a module to define a "group" for its hooks, so the system itself defines the group "tokens" for any token-related hook.

Which means that, when attempting to find implementations of the hook the core checks to see whether a module has a file modulename.tokens.inc - and if it does, it loads that before looking for the token hook. (And records if it finds it in the .inc file.)

This is all good and allows modules to put its token-related hooks in a file that isn't loaded unless its needed.

But 'tokens' is the only group defined for core hooks. Which seems a bit of a waste.

So one thing I've done is to intercept hook_hook_info_alter() and my own group for blocks, like this:


/**
 * Implements hook_hook_info_alter().
 */
function mymodule_hook_info_alter(&$hooks) {
  static $myhooks = array(
    'blocks' => array('block_configure', 'block_view_alter', 'block_view', 'block_save', 'block_list_alter', 'block_info_alter', 'block_info')
  );
  foreach ($myhooks as $group => $items) {
    foreach ($items as $hook) {
      $hooks[$hook] = array('group' => $group);
    }
  }
}

And that's all you need. So you can now happily put your block-related hooks into modulename.blocks.inc. This is not future-proofed, you should really check to see whether the hook has been defined already before doing this, just to be completely safe.

One point worth noting is that there is now a hook_view_BLOCK_DELTA_alter() as well, which you can't put in a group specifically because you'd need an entry for every defined block. However it's not a problem because that hook is called immediately after hook_view_alter() - which will ensure that the .inc file is loaded first.

Don't bother trying to put node hooks into a separate file - it won't work because the node module just goes its own way and doesn't use the hook_info data.

Tuesday 8 February 2011

Extending Chaos Tools Wizard

The Chaos tools form wizard is a very nice piece of code but I had the need to extend it by adding a control button - which turned out to be a lot easier than you might think.

Adding the control button itself was simple enough:



  $form['buttons']['update'] = array(
    '#type' => 'submit',
    '#value' => t('Update'),
    '#next' => $current_step,
    '#wizard type' => 'update',
    '#weight' => -500,
  );

But notice I have given this button a wizard type of "update" (as opposed to 'next', 'cancel' or 'finish').

Now you can either add a new line to your $form_info array:


$form_info['update callback'] = 'mywizard_update';

Or not, in which case the function $form_info['id'] . '_update' will be called.

Your function will get called when this button is clicked with &$form_state as the parameter. Cool.

As an additional point, I'm using 'update' here which means that I do want to validate the form entries and save the values. However by adding these element attributes:


'#limit_validation_errors' => array(),
'#submit' => array('ctools_wizard_submit'),

You can prevent all validation and ensure the proper Chaos tools wizard function is executed.

Thursday 3 February 2011

Fieldsets and Vertical tabs

The CSS for the vertical tabs, seen mostly on node forms, does something quite unpleasant - it hides all <legend> elements. This is fine when it's converting fieldsets into vertical tabs, but it assumes that every fieldset  should be vertical tab - which is not true.

A quick fix for this, especially if you're using Seven as your admin theme, is to add this simple definition:

div.vertical-tabs .vertical-tabs-panes .fieldset-wrapper legend {
  display:block;
  margin-bottom:2.5em;
}
I also found that the legend seems to overwrite the top of the fieldset contents so required the margin-bottom. YMMV.

Wednesday 2 February 2011

Your very own date field

So here's the problem: you have your own hand-built form and you want to include a date field.

The difficulty that I keep running into with Drupal 7 is that everything is tailored around entities - the whole (amazing) Field API - requires an entity to operate on.

But I just want a configuration page with a date field (or in my case a whole bunch of date fields).

Well I have researched long and hard and come up with something very very easy:

$form['date'] = array(
  '#type' => 'date_popup',
);

Which will do it all for you. Watch out for the help text that says it should be "date-popup" because that's wrong. Your default date should be in the form "YYYY-MM-DD HH:MM:SS", you can also set timezones and lots of other useful stuff.

Find the date_popup.module file and search for "date_popup_element_info". It gives you all the defaults for this element type.

Thursday 27 January 2011

Next and previous

The decision about which page of the multi-page form to go to next is given to the calling code.

You can set up a 'next callback' in $form_info, but not a 'previous callback'. The 'next callback' is called in both cases. The default function name is FORM_ID_next(&$form_state).

In here you control where to go next. The simplest solution is either you want to go to the next, or to the previous. Like this:

<?php
function mywizard_next(&$form_state) {
if (isset($form_state['triggering_element']['#next'])) {
$form_state['my_storage']['step'] = $form_state['triggering_element']['#next'];
}

// set the values built by this page
$form_state['my_storage'][$form_state['step']] = $form_state['values'];

// Update the cache with any changes.
mywizard_cache_set('form_values', $form_state['my_storage']);
}
?>

Why do it like this? Because, being one for elegance and aesthetics, I do not want the current step to appear in the URL (mywizard/step1). I want just the one URL (mywizard). Also notice the use of "triggering_element" which is an addition for Drupal 7, and is identical to "clicked_button". However "clicked_button" will be discontinued in Drupal 8.

The Chaos Tools form wizard needs you to build the $form_info array and call its function 'ctools_wizard_multistep_form' each time the page is to be rebuilt. So, in my function that gets called for the page I do some interesting things.

<?php
function mywizard_wizard() {
ctools_include('wizard');

// Fetch the form info array
$form_info = mywizard_form_info();

// Fetch the current form values from the cache...
$form_values = mywizard_cache_get('form_values');
if (empty($form_values)) {
// ...but if they aren't there, build the initial values
$form_values = array(
// set the first step
'step' => array_shift(array_keys($form_info['order'])),
// and my default start values (if any)
'mywizard_values' => array(),
);
mywizard_cache_set('form_values', $form_values);
}

// Build $form_state array
$form_state = array(
'my_storage' => $form_values,
);

// And build the current step
return ctools_wizard_multistep_form($form_info, $form_values['step'], $form_state);
}
?>
So, if you can follow this, when we enter the function we build the $form_info array which describes the whole multi-page form. If this was a complicated process (it could be) you could also cache the $form_info array so it only needs to be done once.

Then we either fetch the cached form data, which contains the next step, or build a new one if we don't have one (in other words this is the first time through).

We save this information in $form_state and then call the ctools multi-step form builder which finds the right form and builds it.

When "next" is clicked (or "back") we save the current step and cache it. To be picked up when the page is built next time.

Voila.

Edited later to remove some unnecessarily complex code. This should be viewed in conjunction with the Chaos Tools Advanced Help.

No back validate

Here's an undocumented option for your Chaos Tools form wizard (not sure if it's in the D6 code): 'no back validate'.

It prevents validation of the current form when you click the back button which means you can use #required fields and prevent Drupal from insisting the user fills them in before going back through the form.

You can either put it in the root level of the $form_info array, or in one or more of the form description entries. The former means that the back button will always prevent validation, with the latter you can control which forms do and don't have validation on the back button.

Handy.

No form output with Chaos Tools multi-step form wizard

Hey this is my thrilling new Drupal 7 blog - it's for hints and tips and Drupal philosophy which I'll add as I go along.

So here's something on the Chaos Tools multi-step form implementation which might easily catch you out and leave you swearing at the screen for an hour or two (like it did me):

So, are you wondering why your D7 multi-step form doesn't produce any output at all?

The form step builders must "return $form;" instead of having the form passed by reference:

<?php
function mymodule_step_form($form, &$form_state) {
// add my fields
return $form;
}
?>

Hope that saves others some time.

I also posted this in the Chaos Tools issue queue here.