Category: Drupal

Selectively Turn Off Drupal Caching

19 February, 2010 | Drupal | No comments

It is sometimes necessary to turn off caching on certain pages on a Drupal site. This might be when trying to do something out of the ordinary, like write information to a file, or randomly generate a section of a template. The following code can be used to turn off caching just on the front page of the site.

function theme_preprocess(&$vars, $hook) {     if ($vars['is_front'] == true) {         $GLOBALS['conf']['cache'] = false;     } }

Adding false to the cache item of the conf globals array at run time will turn off the cache. This is within an if statement that checks to see if the current page is the Drupal front page so it will only be used on the front page.

This is a pretty simple solution, but it should be used with great care. Remember that you are turning off the cache and this will definitely have an effect on the performance of the site. Be sure of what you are doing when you turn off the cache!

Written by Philip Norton.

Drupal Files Migration Script Using Phing

24 November, 2009 | Drupal, Phing | 1 comment

The other day I needed to copy a Drupal project from my source folder to another folder, so rather than manually copy the files I decided to create a Phing script that would do it for me in one go. This Phing script will export your Drupal project into another directory, change the database credentials and create zip and tar files of the project. The first thing to do is create a properties, here is the contents of that file.

drupal.destination.database.host=externalhost drupal.destination.database.database= drupaldatabase drupal.destination.database.password=password drupal.destination.database.username=username drupal.destination.database.prefix=dru_   drupal.destination.database.url=mysql://${drupal.destination.database.username}:${drupal.destination.database.password}@${drupal.destination.database.host}/${drupal.destination.database.database}   drupal.source.directory=D:/drupal/ drupal.source.name=MyDrupalProject   drupal.destination.directory=D:/${drupal.source.name}/ drupal.destination.compressedfile=${drupal.source.name}

Just save this file as build.properties and change the properties to your needed settings.

The build file part of the the contains a fileset to include all of the needed files and then some targets to do things with those files. Those of you used to Phing and Drupal will notice that we are including a lot of files but we are missing one called settings.php. This is deliberate for the reason that trying to match the parameters in our build.properties file into the database settings in the settings.php file is going to be tricky. So rather than enter multiple database parameters we can just use the destination parameters, the default.settings.php file (which we will know the format) and a small amount of regular expression replacing in order to create a new settings.php file for our destination. Here is the contents of the build file.

<?xml version="1.0"?> <project name="DrupalExport" default="main">     <!-- Include properties file. -->     <property file="build.properties" />       <fileset dir="${drupal.source.directory}" id="DrupalFiles">         <include name="cron.php" />         <include name="index.php" />         <include name="install.php" />         <include name="robots.txt" />         <include name="update.php" />         <include name="xmlrpc.php" />         <include name=".htaccess" />         <include name="scripts/**" />         <include name="themes/**" />         <include name="misc/**" />         <include name="modules/**" />         <include name="profiles/**" />         <include name="includes/**" />         <include name="sites/all/**" />         <include name="sites/default/files/**" />                 <include name="sites/default/default.settings.php" />     </fileset>       <target name="prepare">         <!-- Reset the permissions on the setttings.php file so we can delete it. -->         <exec command="php -r &quot;var_dump(chmod('${drupal.destination.directory}sites/default/settings.php', 0777));&quot;" />               <!-- Delete the destination directory. -->         <delete dir="${drupal.destination.directory}" />         <echo>Prepare complete.</echo>     </target>          <target name="copyFiles" depends="prepare">         <copy todir="${drupal.destination.directory}" overwrite="false" tstamp="false">             <fileset refid="DrupalFiles" />         </copy>         <echo>copyFiles complete</echo>     </target>       <target name="drupalDatabaseConnection" depends="copyFiles">         <copy file="${drupal.source.directory}/sites/default/default.settings.php"             tofile="${drupal.destination.directory}/sites/default/settings.php"             overwrite="true">             <filterchain>                             <replaceregexp>                     <!-- Add some rules to replace settings with real values. -->                     <regexp pattern="\$db_url = 'mysql:\/\/username:password\@localhost\/databasename';" replace="\$db_url = '${drupal.destination.database.url}';"  ignoreCase="true"/>                     <regexp pattern="\$db_prefix = '';" replace="\$db_prefix = '${drupal.destination.database.prefix}';" ignoreCase="true"/>                 </replaceregexp>             </filterchain>                  </copy>         <!-- Make settings.php readonly -->         <exec command="php -r &quot;var_dump(chmod('${drupal.destination.directory}sites/default/settings.php', 0444));&quot;" />                   <echo>drupalDatabaseConnection complete</echo>     </target>            <target name="zipFiles" depends="drupalDatabaseConnection">         <fileset dir="${drupal.destination.directory}" id="DestinationFiles">             <include name="**" />         </fileset>         <!-- Delete original file. -->         <delete file="${drupal.destination.compressedfile}.zip" />                 <zip destfile="${drupal.destination.compressedfile}.zip">             <fileset refid="DestinationFiles" />         </zip>         <echo>zipFiles complete</echo>         </target>          <target name="tarFiles">         <fileset dir="${drupal.destination.directory}" id="DestinationFiles">             <include name="**" />         </fileset>         <!-- Delete original file. -->                 <delete file="${drupal.destination.compressedfile}.tar" />                   <tar destfile="${drupal.destination.compressedfile}.tar" compression="gzip">             <fileset refid="DestinationFiles" />         </tar>         <echo>tarFiles complete</echo>         </target>                <target name="main">         <phingcall target="copyFiles" />         <phingcall target="zipFiles" />         <phingcall target="tarFiles" />           <echo>Export finished!</echo>     </target>  </project>

To run this script save this xml into a file called drupalexport.xml use the following command to run it.

phing -f drupalexport.xml

You will now have a directory containing your Drupal project, as well as a zip and a tar file with the same files in them in the directory above.

Written by Philip Norton.

Overriding The Poll Module In Drupal 6

27 July, 2009 | Drupal | 10 comments

The Poll module is a useful little module that comes with Drupal and allows the addition of simple polls to pages or blocks. However, there was one major issue that I wanted to correct on a certain site, but I didn't want to directly edit the core functionality of the module. The default behaviour of the module is to record one vote per IP address or user, which is fine for normal uses but in some situations it does tend to fall over. A Drupal site with one administrator that allow anonymous users to vote on your polls seems fine, but lets say that this site wants votes to come from people in the business world. The problem here is that multiple people might work for the same business, be situtated behind a firewall and therefore would have the same IP address. What this means is that if one person in that company votes on your poll it will block all other people from voting for that poll.

To override the default behaviour of the Poll module I created a new module called Poll Override and intercepted the Poll module in key areas to allow a session based vote logging approach. The second thing I wanted to achive from this new module was to change the location of the "you have voted" message so that it appeared within the poll area, rather than at the top of the screen. Doing these two things is a useful way of understanding how Drupal works.

The first task is to create a .info file called poll_override.info.

; $Id$ name = Poll Override description = "Override some of the functionality of the poll module" package = "#! code" dependencies[] = poll core = 6.x

Next, we create a .module file (called poll_override.module) and enter a description.

<?php /**  * @file  * Overrides some of the funcionality of the Poll module.  *  * Changes the way in which anonymous users vote and how voting   * confirmation messages are displayed.  *  */

Put both of these files in a directory called poll_override and put it in the sites/default/modules directory of your Drupal install.

The first thing to do is to create a function called poll_override_form_poll_view_voting_alter(), which is an implementation of hook_form_alter(). The only purpose of this function is to rewrite where the submit of the vote form of the Poll module goes to. In this case we are directing it to a function called poll_override_form_submit().

/**   * Implementation of hook_form_alter().   *   */ function poll_override_form_poll_view_voting_alter(&$form, &$form_state){   // Override the normal form submit for a submit function in this module.   $form['vote']['#submit'] = array('poll_override_form_submit');    }

Next we need to create the poll_override_form_submit() to handle the action of someone voting on a poll. When the poll module is installed it creates three tables, these are as follows:

  • poll - This stores details of the polls avaialable on the system.
  • poll_choices - This stores the questions for each poll.
  • poll_votes - This stores the votes made against each poll.

The table we are interested in is poll_votes. The normal behaviour of the Poll module is to store the vote along with the IP address of the user who submitted the vote and use this to varify the user the next time around. Rather than override this completely we will allow users without cookies enabled to vote using the old IP address method, and include an if statement that stores the session key for everyone else. Finally, we add an item called poll_override_message to the $_SESSION array so that we can print out a message when we refresh the page after the vote has been recorded.

/**  * Submit function for Poll Override module.  *  */ function poll_override_form_submit($form, &$form_state) {   $node = $form['#node'];   $choice = $form_state['values']['choice'];     global $user;   if ($user->uid) {     db_query('INSERT INTO {poll_votes} (nid, chorder, uid) VALUES (%d, %d, %d)', $node->nid, $choice, $user->uid);   }   else {     if ( isset($_COOKIE[session_name()]) ) {       // If a cookie has been set for this user then use the session id to record the vote.       db_query("INSERT INTO {poll_votes} (nid, chorder, hostname) VALUES (%d, %d, '%s')", $node->nid, $choice, $_COOKIE[session_name()]);     } else {       // Otherwise just use the IP address (this is the normal functionality).       db_query("INSERT INTO {poll_votes} (nid, chorder, hostname) VALUES (%d, %d, '%s')", $node->nid, $choice, ip_address());     }   }     // Add one to the votes.   db_query("UPDATE {poll_choices} SET chvotes = chvotes + 1 WHERE nid = %d AND chorder = %d", $node->nid, $choice);     // Instead of using drupal_set_message() to set the message we will just use a session variable.   // This will be deleted after the page has loaded.   $_SESSION['poll_override_message'] = t('Your vote was recorded.');   // Return the user to whatever page they voted from. }

Now we have sorted out how we are storing the votes we need to display the poll. This means that we either display the voting form or a set of results. What we need to do here is to intercept the poll node before it is displayed and detect whether it is still eligable for voting or not. We do this with a call to the poll_nodeapi() function, which is a implentation of hook_nopeapi(). This function might look complicated but we are essentially running the following steps.

  • Check if we are loading the node and if the node is a poll.
  • Load the poll and the associated choices for that poll.
  • Set a variable of the node called allowvotes to false. This is used to display either the results (if false) or a voting form (if true).
  • Detect if the user is able to vote on polls and if the poll is active. If not then we skip the rest of this function and the form is not displayed.
  • If the user object has a uid property then this user is logged in so we load the vote using this information.
  • If the user is not logged in we can either use the session cookie or the IP address depending on the information we have available.
  • Finally, if a vote has been detected in the last 2 steps we pass this onto the node, otherwise we pass nothing and set allowvotes to true so that the user can vote.

Here is the code in full.

/**  * Implementation of hook_nodeapi().  *  */ function poll_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL ) {   if ( $op == load && $node->type == 'poll' ) {           global $user;              $poll = db_fetch_object(db_query("SELECT runtime, active FROM {poll} WHERE nid = %d", $node->nid));         // Load the appropriate choices into the $poll object.       $result = db_query("SELECT chtext, chvotes, chorder FROM {poll_choices} WHERE nid = %d ORDER BY chorder", $node->nid);       while ( $choice = db_fetch_array($result) ) {         $node->choice[$choice['chorder']] = $choice;       }         // Determine whether or not this user is allowed to vote.       $node->allowvotes = FALSE;       if ( user_access('vote on polls') && $poll->active ) {         if ( $user->uid ) {           $result = db_fetch_object(db_query('SELECT chorder FROM {poll_votes} WHERE nid = %d AND uid = %d', $node->nid, $user->uid));         } else {           if ( isset($_COOKIE[session_name()]) ) {                     // If a cookie has been set for this user then use the session id to retreive the vote.             $result = db_fetch_object(db_query("SELECT chorder FROM {poll_votes} WHERE nid = %d AND hostname = '%s'", $node->nid, $_COOKIE[session_name()]));           } else {             // Otherwise just use the IP address (this is the normal functionality).             $result = db_fetch_object(db_query("SELECT chorder FROM {poll_votes} WHERE nid = %d AND hostname = '%s'", $node->nid, ip_address()));           }         }         if ( isset($result->chorder) ) {           $node->vote = $result->chorder;         } else {           $node->vote = -1;           $node->allowvotes = TRUE;         }       }        } }

The Poll module includes a function called theme_preprocess_poll_results(). This is essentially a low level hook that we can override by using a more specific theme function. So by calling a fucntion poll_override_preprocess_poll_results() we can override the theme_preprocess_poll_results() function within the Poll module and change the output of the results block. This code will be run when printing out the results of a poll and does two things. It is possible to cancel the vote on a poll so the first thing we do is to include the form that allows users to do this. The second thing we do it detect if the poll_override_message has been set, and if so assign this value to the $variables array (which will be passed to the template) and unset the item in the $_SESSION array.

/**  * Implementation of theme_preprocess_poll_results()  *  */ function poll_override_preprocess_poll_results(&$variables) {   $variables['links'] = theme('links', $variables['raw_links']);   if ( isset($variables['vote']) && $variables['vote'] > -1 && user_access('cancel own vote') ) {     $variables['cancel_form'] = drupal_get_form('poll_cancel_form', $variables['nid']);   }   $variables['title'] = check_plain($variables['raw_title']);     // If this is a block, allow a different tpl.php to be used.   if ( $variables['block'] ) {     $variables['template_files'][] = 'poll-results-block';     if ( isset($_SESSION['poll_override_message']) ) {       $variables['message'] = $_SESSION['poll_override_message'];       unset($_SESSION['poll_override_message']);     }   } }

The very final step is to create a file called poll-results-block.tpl.php and include the following code.

<div class="poll">   <div class="title"><?php print $title ?></div>   <?php print $results ?>   <div class="total">     <?php print t('Total votes: @votes', array('@votes' => $votes)); ?>   </div>   <?php   if ( isset($message) ) {     print '<p class="voted">'.$message.'</p>';   }   ?>   </div> <div class="links"><?php print $links; ?></div>

The $message variable contains the message that might have been set in the poll_nodeapi() function, if it is set here then we print it out. You can include this file in the poll_module directory or within your template directory, this is up to you.

Written by Philip Norton.

Null Response From Drupal 6

1 July, 2009 | Drupal | No comments

I recently had some trouble with a Drupal 6 site I was updating. I wanted to create a local working copy of the site to test so I downloaded the files and backed up the database, but for some reason I couldn't get the site to run. In every browser I tried I would get "page cannot be displayed" or "host unresponsive". These messages were basically telling me that something on the site was causing it to fall over before it ever got around to producing any HTML, and so the browsers were treating it as best they could. I tried clearing the Drupal cache and disabling some modules but this didn't appear to do anything, or simply broke the site. Searching the internet for this problem yielded very few results, so I eventually had to track the problem down myself.

Adding a few echo statements to the index.php page told me that Drupal was bootstrapping fine, but the problem was somewhere down the line. Obviously, adding echo statements throughout the Drupal source code is a real pain, so a different approach was needed. Using a combination of Netbeans and xdebug I was able to run each line of code one by one until the problem occurred. Fortunately (or unfortunately) when the offending peice of code was run it caused the debugger to stop with no sign of where the issue occurred. But after a while I tracked it down to line 1966 in the file common.inc. Here is the code that was causing the issue.

if ($_optimize) {   // Perform some safe CSS optimizations.   $contents = preg_replace('<     \s*([@{}:;,]|\)\s|\s\()\s* |  # Remove whitespace around separators, but keep space around parentheses.     /\*([^*\\\\]|\*(?!/))+\*/ |   # Remove comments that are not CSS hacks.     [\n\r]                        # Remove line breaks.     >x', '\1', $contents); }

So what is happening here? This piece of code has three functions, which are actually explained in the code, but work together to minimise the stylesheet as much as possible by removing comments, line breaks and white space. Lets take a real world example and run a stylesheet through this code to see what happens. Here is the original.

/**  * Example CSS file  *  */ body{     margin:100px; }   /**  * Make some paragraph tags large  */ p#large{     font-size:20px; }

Here is the altered, or minimised, stylesheet.

body{margin:100px;}p#large{font-size:20px;}

The problem arises on some systems where trying to do too much with preg_replace() causes the PHP engine to fall over. To replicate this I created a long string and then tried to replace that string using preg_replace().

$contents = 'd' . str_repeat('a', 396) . 'b'; /// 397 breaks $contents = preg_replace('/d(a)+b/', '\1', $contents);

On my test system (Windows XP, Apache 2.2, PHP 5.2) this code works fine, but increasing the str_repeat() parameter to 397 causes the system to display nothing upon refresh. Note that there will also be nothing in the error logs for PHP and Apache.

The solution to this problem is quite simple, just remove some of the comments in your CSS or try to reduce the size of it and then explicitly clear the CSS cache. This will force Drupal to fetch your CSS file again and attempt to minify it, and all being well your site will be back up again.

To explicitly clear the CSS cache open up the root index.php file and enter the following lines just after the bootstrap is called.

drupal_clear_css_cache(); exit();

Remove this code once you have accessed the page.

The optimise code is used when you use the optimize CSS files feature within Drupal, found within Site config > Performance. So if this feature is turned on it can cause Drupal to produce a null response on some systems if you have large CSS files. I say some systems because this is an architecture specific issue to do with available stack size, rather than a PHP bug.

Written by Philip Norton.

Clearing The Filter Cache In Drupal

26 June, 2009 | Drupal | No comments

Filters are used in Drupal to change the content of the text of a node when it is viewed. The important thing to note is that Drupal filters should never alter the actual content of the node itself. Instead, when a node is saved it stores the output of the filter in the cache_filter table and displays this content the next time the node is viewed. This is useful because it doesn't mess about with the original text, and it speeds up the displaying of the node by running the filters once, rather than every time the node is loaded.

However, there is one thing that you should watch out for when creating your own modules. If you are using any data from other tables in your filters then you will need to clear the cache when this data changes.

Lets take a real world example where we have a filter that allowed us to automatically filter taxonomy terms in a particular vocabulary within our node content and create links around them. When the node is saved we would have a cached version of the node that would contain a few links. The problem arises when we change any vocabulary information (like adding or deleting terms). The links would still be present in our content, even though the term has been removed.

Drupal provides a hook that allows you to catch any actions that occur upon the taxonomy, this is called hook_taxonomy() and has the following structure.

function hook_taxonomy($op, $type, $array = array()){ }

We only need to intercept any action with terms, so we first make sure that the type is a term. We have previously set a variable called mycustommodule_vocabulary, and we use this to filter by a single vocabulary, rather than everything, so the next step is to run a check for this. Finally, we run an SQL query that deletes everything from the cache_filter table, which effectively clears the filter cache.

function mycustommodule_taxonomy($op, $type, $array = array()){     switch($type){         case 'term':             $vid = variable_get("mycustommodule_vocabulary", '1');             if ( $vid == $array['vid'] ) {                 // If a term has been altered within our selected vocab then delete the filter cache.                 db_query('DELETE FROM {cache_filter};');             }             break;     } }

Note that this might be a little intensive on large sites with lots and lots of nodes, especially if you are doing a lot of term editing.

Written by Philip Norton.