Jul 27 2008
Jul 27

Today I have been playing around with Drupal themeing. I used Raincity's Basic theme (http://drupal.org/project/basic) as a starting block, it is based upon Zen theme, and seems a little easier to customise. With only one hours work I was able to come up with what you see now.

Read the full post on Millwood Online

Jul 26 2008
Jul 26

One of the issues with user generated content multi-lingual sites is that content will often be in only one language, or maybe translated into a few others. You can be certain that not all content that is even translated will be available in all languages.

The way that the Drupal i18n module, for both Drupal 5 and 6 by default handles translated content is to offer you:-

  1. Only current language and no language
  2. Only current and default languages and no language
  3. Only default language and no language
  4. Only current language
  5. All content. No language conditions apply

Anything but number 2 is going to hide a lot of content from your users. Depending on the nature of the site you may well want them to see that there is activity and content from others even if they can't read it, and in a multilingual environment it's not so uncommon for users to comprehend other languages either. Hiding it from them as if it doesn't exist isn't really an option.

The localizer module solves this by letting the user weight their preferences of languages, and making a multiple join weighted query. Other factors about the modules design mean I've not had the chance to try this out in a large site environment yet, but I hope to have a chance to analyse it's performance, maybe when it's ported to Drupal 6.

So back with i18n the problem with option 2 is that it will still hide content from users if it's not translated in their preferred language. In fact despite what you expect it won't even display the content if it is in the default language and has been translated into another language if neither of them are your choice.

The issue comes up with the solution of a 'mixed_single' 'Current, default language when current not present and no language', which does show those translated default language posts. So the only thing you are missing now is content that is in languages other than the users and the default.

You end up with queries like this however (node access is on as well):-

As i18n_node is just indexed on nid this can be really slow. Adding an index including language, trid and nid halved query times, but they can still be really very long, like about 5 minutes on a 2 Xeon CPU with a gig of memory a standard join_buffer_size and around 40,000 nodes. Plenty of default pages will bring up queries like this, so overriding and avoiding anything that won't fit in memory is a tough option. I've stared at it for some time but I can't make the present tables work with a query that would do it better than the collective intelligence on the thread has already.

All this looks sorted In Drupal 6 with the it's in build i18n the new version of the i18n module and the active translation module. Here you get the original if it's not translated into your preferred language. There are a couple of translation tables and (without the node access) were down to one INNER JOIN :-

The active translation table is keeping a row per node of original and any translations, it's going to get big, but it's simple too. It could be possible to use the idea in Drupal 5, (maybe?): also hook into _nodeapi add another table, then patch i18n (we're doing it already above) to use this table for the query instead. Not quite so elegant...

Roll on making sites in D6.

Jul 26 2008
tom
Jul 26
Drupal PNG transparency fix for IE

I only recently discovered about Internet Explorers non-existent handling of transparent PNGs. It only effects versions 5.5 and 6, but there are still a surprising amount of people using these outdated browsers so it's important not to forget them. There has been a fix around for some time - the aptly named, pngfix.js. Now, there is also jquery version of the png fix.

Luckily for us Drupalers, the jquery library hasalready been wrapped up in a nice little Drupal module called pngfix. It's really easy to use. Simply download and install the module in the usual manor and browse to it's configuration page. From there, you can make a comma separated list of class names that you would like the png fix to be applied to. You must include the '.' in the class names, eg .pngfix

To use it, simply add one of the css classes that you specified in the configuration page to the containing element of an image, not to the image itself.

As long as your Internet 5.5 or 6 viewers have Javascript turned on, they will see your PNG images the same way everybody else does.

Jul 26 2008
Jul 26

So this week I had to get search_files module for Drupal 6 running on a shared host, Bluehost, for one of my customers. I had promised that we would be able to search his PDF files, but I didn't realize that search_files, as well as search_attachments modules require a Linux command line utility named pdftotext to be installed.

I did request that Bluehost install it on my box and was told that anything requiring root access wasn't going to happen.

Fine, maybe I can run it myself, after all, I did get SVN running on Bluehost, how hard could it be?

Thanks to rasc on the Sphider PHP Search Engine forums for the solution, which I catered to work for search_files.

Setting up Bluehost

  • First, go to foolabs and grab yourself a hot copy of XPDF
  • Un-archive it, and get get rid of everything other than the pdftotext file
  • Rename this file to pdftotext.script
  • Create a shell script (flavor of your choosing) that calls this pdftotext.script file, for example:
#!/bin/sh
/home/YOURBLUEHOSTUSERNAME/bin/pdftotext/pdftotext.script $1 -
  • As you can tell from the code there, you will need to change YOURBLUEHOSTUSERNAME to the correct name.
  • Log in via FTP or SSH, and create a /bin and a /bin/pdftotext directory from your home directory, NOT FROM public_html, but one directory above that
  • Make /bin/pdftotext writable by all, this is where pdftotext will save the temporary files it creates
  • Upload both pdftotext and pdftotext.script to the /bin/pdftotext directory, and make them executable (chmod 755 should work)
  • If you don't have one already, in your home directory (not public_html) create a .bashrc file, and add the following so that the web server knows where your executable files are:
export PATH=$PATH:$HOME/bin:$HOME/bin/pdftotext:.
export pdftotext_path=/home/YOURBLUEHOSTUSERNAME/bin/pdftotext/pdftotext

Setting up search_files in Drupal 6

  • Go to the search_files project page and download the module
  • Upload the module
  • Go to Admin - Site Building - Modules and activate the module
  • You may need to adjust your permissions to let you use the module, do that if needed
  • Go to /admin/settings/search_files/helpers page and click "PDF"
  • In the Helper Path* box, put in:
/home/YOURBLUEHOSTUSERNAME/bin/pdftotext/pdftotext %file% -
  • Click Update. Please notice the - at the end, it's needed. I have it in both the helper line and the script, it's probably not needed in both, but it DOES work this way
  • Now, find the Directories page (/admin/settings/search_files/directories) and start adding directories where you have those PDF files that you would like to have indexed, such as /home/YOURBLUEHOSTUSERNAME/public_html/files - making sure that you use the full server path

Run it!

I took advantage of this time as an opportunity to setup my cron job on Bluehost, and then cleared my cache in Drupal, ran cron, and watched it find hundreds of files for me.

You MAY need to create a new custom php.ini file for your Drupal installation (Bluehost has a utility to create a default one in c-panel) and increase the limits so that you don't run into memory allocation or timeout issues.

Hope this helps, if you have any questions, go ahead and leave a comment!

Jul 26 2008
P6
Jul 26

Check the previous three posts and you'll see the basic functionality for Amazon Associate Tools is now functional here on Code.

Searching for Books, DVDs and Music is supported; other product types would be added by customizing the include file for the Amazon site you are querying just like last version. Bulk imports are next. Blocks come after that; I believe I'll get both squared away this weekend.

Some folks complained the detail page url didn't wind up crediting their account when folks purchased things. Rather than using the detail page url to like to a product, I used a "approved do-it-yourself" link.

I do have shortcomings to overcome. Notice the Amazon.com search page is not integrated into the site search functionality. It seems to do that you have to actually use the Search API. Maybe I screwed the menu definition when I tried it. Also, the database tables as slightly different...upgradable, but no upgrade yet.

And I have additions to make, in particular a shopping cart. I hesistate on this one because I have no idea if I can use existing Drupal ecommerce shopping carts. I have no idea how the ecommerce modules work. I asked a couple years back when I wrote the first version of this thing. Never got an answer, and to be honest the whole thing looks massive enough that figuring out where to insert some code (if one even can) would slow me down a lot.

I also want to do some sort of wizard thing because I'm targeting setting up a store. That means category pages, author pages and such like. I can wrangle it by hand, of course, but I think a good wizard can turn this into a fairly successful "long tail" product.

So that's where it's at so far. Tomorrow I'll repost my old Drupal 5 modules here other than an AAT page.  I'll likely rename the thing.

LATER: Line 205 of amazon.module (amazon_cron) tried to use db_num_rows() which, of course, doesn't exist in D6. The function ought to look like:

function amazon_cron() {
  // get 10 ASINs whose prices haven't been updated for a week
  $stale_price_date = date('Y-m-d', time() - 604800); //604800 is a week of seconds
  $stale_records = db_query_range("SELECT asin FROM {amazonitem} WHERE pricedate <= '%s' ORDER BY pricedate", $stale_price_date, 0, 10);
  // get the Amazon records for each of them
  while ($stale_asin = db_fetch_object($stale_records)) {
    $asin_to_refresh[] = $stale_asin->asin;
  }
  if (count($asin_to_refresh)) {
    $asinlist = implode(',', $asin_to_refresh);
    $asin_update = _amazon_product_data_from_Amazon($asinlist);
    // update their amazonitem records
    if (count($asin_update) > 0) {
      foreach($asin_update as $asin_data) {
        _refresh_amazonitem_data($asin_data);
      }
    }
  }
}

AttachmentSize 14.39 KB
Jul 25 2008
Jul 25

I just discovered you can use pre-populate module to select a contact category in a Drupal contact form.

For instance, if you have 3 categories for your contact form and want to pass an url to pre-select category 3, just pass ?edit[cid]=3 in your link, e.g. http://yourdomain.com/contact?edit[cid]=3.

Nice.

Jul 24 2008
Jul 24

So back in April I started talking to Keiran at about doing a media and files sprint... well it's finally happening. aaronwinborn in in Portland and dopry is going to be helping remotely. Aaron posted a great writeup on what we're hoping to accomplish so I'll blockquote at length:

Andrew Morton (drewish), Darrel O'Pry (dopry, remotely), and I are heading up a Media Code Sprint in Portland this week! Come help, in person or remotely, if you're interested in multimedia and Drupal! It has now officially started, and as I've volunteered to help keep folks updated, here goes...

First the reasons.

Number One: Better Media Handling in Core

Dries conducted a survey prior to his State of Drupal presentation at Boston Drupalcon 2008, and number one on the top ten (or 11) list of what would make THE KILLER DRUPAL 7 Release was "Better media handling".

Let me repeat that. Better media handling.

People have done really amazing stuff in contrib, but it is difficult (if not impossible in many cases) for developers to coordinate the use of files, as there is no good means for file handling in the core of Drupal. Thus, we have several dozen (or more) media modules doing some small part, or even duplicating functionality, sometimes out of necessity.

We need (better) media and file handling in Drupal core. In particular, there has been a patch for a hook_file in the queue for over a year, which has been in the Patch Spotlight (for the second time, no less) since May! (And has been RTBC several times during that process...) Come on folks.

One of the powers of Drupal is its system of hooks. We have hooks to modify nodes, to notify changes to user objects, to alter nearly any data (such as forms and menus). Noticeably absent is a consistent handling for files or any sort of notification. We need hook_file.

So goal Number One: get media handling in core. The means? Add hook_file and make files into a 1st class Drupal object. We'll be creating a test suite for functionality in the hook_file patch to validate it and "grease the wheels" to get it committed.

The other goals of this sprint pale in comparison to the first in utility, but are still highly desirable and worthwhile.

Number Two: Refactor File Functionality in Core

As an extension to the first goal, there is a lot of inconsistency with how Drupal currently handles files. For instance, in some areas a function may return an object, and in others a string. Additionally, some functions are misnamed, or try to do too much to be useful as a file API.

Some specific examples: for what it does, file_check_directory may be better suited as something like file_check_writable, or maybe even split into that and file_check_make_writable. Also, for instance, file_scan_directory needs to return file objects, rather than the current associative array (keyed on the provided key) of objects with "path", "basename", and "name" members corresponding to the matching files. (The function does what it needs to, but the returned objects have keys not corresponding to anything else used in core.)

So goal Number Two: refactor file functionality in core. The means? Go through and check for (and fix!) existing file functionality for documentation and consistency.

Number Three: Spruce up Existing Contributed Media Modules

There are several much needed multimedia modules that have not yet been upgraded to Drupal 6 (or which are still in heavy progress). This includes (but is not limited to) Image Field, Image API, and Embedded Media Field. Additionally, some major improvements can be made, both to these, and to other essentials, such as the Image module, such as creating a migration path from Image to Image Field (once that module is stable).

So goal Number Three: spruce up existing contributed media modules. The means? Get these modules upgraded!

I want to recognize the valiant and heroic efforts made by everyone to date, as fortunately, there has already been significant progress on all these fronts. That makes our job (relatively) easy. In some respects, we just need to finish up the jobs that have
already been started.

Thus, drewish declared this week the Media Code Sprint!

We need you to help. If you are a developer, or want to be a developer, jump on in! If you aren't ready to develop, or consider yourself too new for that, you can still help test patches and functionality. Jump on in! And please, even if you don't know how to apply a patch, you can still help with documentation and other small (but important) tasks. Jump on in!

If you're in Portland, You Have No Excuse®. If not, you can jump into #drupal in IRC any time you're available.

The official dates for the sprint are today (Wednesday July 23, 2008) through Saturday (the 26th). We'll be online and working most of that time. I'll make sure we continue to post progress as the week develops.

Of course, as is the wonderful nature of Drupal, this is an ongoing process. Even if we achieve our stated goals, there will always be more.

Thanks,
Aaron Winborn

Jul 23 2008
Jul 23
Posted by Heine on 23 Jul 2008 at 19:58 UTC
  • Advisory ID: DRUPAL-SA-2008-046
  • Project: Drupal core
  • Version: 5.x
  • Date: 2008-July-23
  • Security risk: Less critical
  • Exploitable from: Remote
  • Vulnerability: Session fixation

Description

When contributed modules such as Workflow NG terminate the current request during a login event, user module is not able to regenerate the user's session. This may lead to a session fixation attack, when a malicious user is able to control another users' initial session ID. As the session is not regenerated, the malicious user may use the 'fixed' session ID after the victim authenticates and will have the same access.

The advisory SA-2008-044 claims that this session fixation vulnerability was fixed in Drupal 5.8 and 6.3. Unfortunately, Drupal 5.8 still contains this vulnerability.

Versions affected

  • Drupal 5.x before version 5.9

Solution

Install the latest version:

  • If you are running Drupal 5.x then upgrade to Drupal 5.9.

If you are unable to upgrade immediately, you can apply a patch to secure your installation until you are able to do a proper upgrade. The patches fix security vulnerabilities, but do not contain other fixes which were released in these versions.

Reported by

  • The session fixation issue was originally reported by Erich C. Beyrent. Its continued existance in 5.8 was reported by dmnd.

Contact

The security contact for Drupal can be reached at security at drupal.org or via the form at http://drupal.org/contact.

Jul 19 2008
Jul 19

At last I overcame my fear of Views 2.0 and have added it to my D6 effort for Quick Tabs. (Previously my D6 version only allowed you to add blocks to your tabs.) My colleague, Hubert a.k.a. couzinhub, having quickly jumped in and familiarised himself with the wonders of Views 2.0 (well, after all he did help with the UI), helped me out with a very enthusiastic run-though of displays, overriding default displays, etc. and generally getting an overview of the lay of the land in the beta4 release of this super-module.

As far as Quick Tabs integration went, all I needed was a way of providing a drop-down list of all available Views so that people could add one for display in a tab. I had previously used the views_build_view() function then to display the view. There were a couple of hiccups in getting this to work in Drupal 6. Views_build_view() no longer exists but there is views_embed_view() which is slightly different because of the fact that you now have different displays per View. Probably the biggest challenge was getting a dependent drop-down to show all the displays for whichever View is selected. Let's just say I had an interesting journey through the Forms API, after my initial naïve effort that simply used AJAX to replace the options in the drop-down (synopsis: you can't add new elements or even select options to Drupal forms via AJAX without telling Drupal about them - see here for more info: http://drupal.org/node/150859). And the end result is a Quick Tabs creation form which may well get the prize for highest concentration of AJAX and AHAH craziness - which is not necessarily a good thing, as I'm worried my issue queue will prove soon enough.

Actually, the real low point came after I had done the bulk of the work and then suddenly it seemed that the new Views was so powerful it totally reduced the benefit of being able to add a View directly into a Quick Tabs block over just adding that View's block display (i.e. just adding a block that happens to come from a View). Well, fortunately, that's not quite true. The ability to use the same View in multiple tabs but passing in different arguments each time (an example of which is the "My Favourite..." QT block in my right sidebar) is, I think, justification enough for my continuing to include Views integration in Quick Tabs. I'm looking forward to feedback on this question.

Next on my to-do list: SimpleTest unit tests for Quick Tabs. And I might even request some Drupal Tough Love for it... if I dare!

Jul 19 2008
P6
Jul 19

I've just been diddling around with this site. I didn't realize folks had seen it and left comments.

I suppose I'll have to check a bit more regularly.

Jul 17 2008
Jul 17

I like to develop against local virtual hosts when I work with Drupal. Here is the apache httpd.conf configuration that has served me the best so far. The first VirtualHost is the default .. .simply replicate the second VirtualHost entry and edit accordingly. A simple edit of /etc/hosts to enter the virtual host name that matches the virtual host entries in the httpd.conf and a quick reload by apache and you are good to go. I particularly enjoy the independent logging for each site.

This is likely a no-brainer for the server admins out there but I remember a time when this type of information would have been useful to me ... so here it is. This is not a production level configuration. Have suggestions for improving it?

httpd.conf

NameVirtualHost *
ServerName 127.0.0.1
<VirtualHost *>
  ServerAdmin [email protected]
  DocumentRoot /var/www/
  <Directory />
    Options FollowSymLinks
    AllowOverride all
  </Directory>
  <Directory /var/www/>
    Options All Indexes FollowSymLinks MultiViews
    AllowOverride all
    Order allow,deny
    allow from all
    RewriteEngine on
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
  </Directory>
  ErrorLog /var/log/apache2/error.log
  LogLevel warn
  CustomLog /var/log/apache2/access.log combined
  ServerSignature Off
</VirtualHost>

<VirtualHost *>
  ServerName mysite
  DocumentRoot /var/www/mysite/trunk/htdocs
  ErrorLog /var/log/apache2/mysite_error.log
  CustomLog /var/log/apache2/mysite_access.log combined
</VirtualHost>

/etc/hosts

127.0.0.1     localhost.localdomain localhost mysite mysite1 mysite2

Jul 16 2008
Jul 16

You may remember that back in February, Drupal announced that Peter Cawley (Corsix) was chosen as the Google Highly Open Participation (GHOP) winner for the Drupal project. It turns out that finding a time when the 10 grand prize winners, their parents, and a mentor from each of the 10 participating open source projects were all free was not easy, but we finally got together this week for the official awards ceremony.

We started out on Thursday morning by going to breakfast at an "undisclosed location" which turned out to be the International House of Pancakes (IHOP for short). I believe Charlie Gordon (cwgordon7) and Dmitri Gaskin (dmitrig01) are to blame for making us all think of pancakes whenever we think of GHOP.

After breakfast, we headed to Great America, a nearby amusement park with plenty of roller coasters and such. Being a Thursday, the crowds were not bad at all and we were able to enjoy the rides without waiting in line very much. Hanging out for a day at the amusement park gave the students, mentors, and their parents plenty of time to mingle and get to know one another. I won't name any names, but one of the mothers frequently mentioned that she thought it was very cool that all of the students were talking to one another and getting to be friends. Apparently some people think that us geeks don't like to socialize.

On Friday morning, our bus picked us up and took us to the Mountain View headquarters of Google. I believe I can speak for the others in saying that we were all very much looking forward to this experience. After staring at the semi-live screen of search queries being run at the moment while waiting to check in and get our name badges, we had breakfast and then went on a tour of the campus. After the tour was the award ceremony, and the students were all given trophies to mark their accomplishments. As a side note, I do appreciate that the trophies had some resemblance to the Druplicon.

We ate lunch at the main cafeteria, and then after a few group photos and a trip to the Google gift shop, we settled in for an afternoon of very informative and interesting lectures.

Guido van Rossum started out with a talk on Google AppEngine, a relatively new product that allows one to create a rather large and powerful site without ever needing to worry about the site's infrastructure. Being able to create a very scalable site without needing to worry about configuring the web server or database server would be very nice. Unfortunately, for the moment at least, AppEngine only supports the Python programming language, so no Drupal. However, we were told that additional languages would be supported in the future, though in typical Google style we were not told when this would happen or what languages would be supported.

Next up was Romain Guy, one of the developers of Android, Google's soon-to-be released open source mobile phone operating system. He kept mentioning that the phone he was carrying in his pocket could do this and that, but wouldn't actually take said phone out of his pocket and show us! However, he did go through the process of creating a very simple Android application from scratch, and it does look like a very powerful platform with some very cool features. I spoke with some other Googlers, and though none of them would give me any further details about the phone, all promised that it would be cool.

After taking a short break for GHOPcake, we rejoined for a talk by Jeff Dean, who spoke about Google's infrastructure, including GFS, Bigtable, MapReduce, etc. I'm very impressed by the thought that has gone into the design and how resilient the entire network of systems seems to be.

Our final talk was by Bharat Mediratta and Mike Bland, two members of Google's testing team. Though testing is not the most exciting subject, they did a great job and gave a very insightful and interesting talk about the process they have gone through to try to encourage and train Googlers to write tests with their code. Their story of how "Testing on the toilet" came to be was especially entertaining. I found it very interesting that even at Google, with all of those smart people, getting people to always write tests for their code is not easy. I'm not sure if that's encouraging news for our own push to have complete test coverage for Drupal 7, but at least we're not alone. If only we could put testing newsletters in every Drupal developer's toilet, we might make some great progress. Don't give webchick a key to your home, or it just might happen!

Leslie had pizza delivered to the hotel for dinner Friday night, and we talked as a group about how to improve GHOP, if it were to happen again. Though lots of problems and solutions were discussed, a lot of the discussion focused on how to improve the issue tracker used on the Google side of things, and how to help get more mentors involved within each of the participating groups. In response to the concern with the not-so-functional issue tracking system, the Google Open Source team is working on a new Summer of Code/GHOP system, and it will be completely open source and will run on AppEngine. Google is welcoming any interested parties in our community to help join them in making the tracker awesome, so if you're interested check out the project on the Google code site.

Overall I had a great time and want to thank Google for making GHOP and this trip possible. For any high school age students reading this, I would encourage you to participate in the next GHOP program*. If you're not a student you can still help out by keeping in mind small and discrete tasks that we could use as tasks for future incarnations of GHOP.
* If and when such a program happens.

Additional Links:

Jul 16 2008
Jul 16

With apologies to Gloria Jones and a variety of others...

Sometimes I feel there has to be a way
To improve securi-tay
To automatically prevent attacks
The bugs we fix seem not to help one bit
To make the exploit-tays
Not come back. They should stay away!
Oh! Tainted bugs!

As part of Acquia's security testing for Acquia Drupal, I've been experimenting with automated methods for detecting security vulnerabilities in Drupal and contributed modules. The time has come to report on my progress. If you want to learn more about this and are going to DrupalCon Hungary 2008, vote for my session proposal.

Data tainting is a kind of dynamic program analysis (it operates while the program is running) that can automatically detect one of the most frequent sources of security vulnerabilities: insufficiently validated user input. The idea behind data tainting is that when data is received from an untrusted source (such as GET request parameters or some parts of the database), it is marked as "tainted." Any data derived from tainted data (such as by string concatenation, passing function arguments, etc.) is also marked tainted. When tainted data is passed to a security-sensitive destination (such as the HTML response to a page request), a taint error is raised. Finally, when tainted data is validated in specific ways, the taint mark is removed so the data can be used freely.

What I am calling "Taint Drupal" is based on Taint PHP work by Wietse Venema along with some Drupal-specific customization particularly regarding the database. For more details, keep reading.

Tainting memory

Here is a simple example:

<?php
function message($arg) {
  return
'Your name is ' . $arg;
}
$name = $_GET['name'];
print
message($name);
?>

All request parameters in $_GET come directly from the user and can contain anything; therefore, they are "born tainted." $_GET['name'] is assigned to $name so, along with the actual value, $_GET['name']'s "taintedness" is also assigned to $name. When $name is passed to the function message(), the $arg argument also inherits the taint value. In the return statement, the string literal 'Your name is ' is not tainted because it comes from a trusted source (the program source code itself); however, when the non-tainted string literal is concatenated with the tainted $arg, the resulting string is tainted by $arg's, so the function result is also tainted. Finally, the print() statement receives this tainted value and, because it is programmed to, generates a taint error:

print(): Argument contains data that is not converted with htmlspecialchars() or htmlentities() in yourscript.php on line 6

As the error message indicates, the problem is that we did not validate $_GET['name'] with htmlspecialchars(). If we change one line of the script to

<?php
$name
= htmlspecialchars($_GET['name']);
?>

the taint flag is removed from $name and print() no longer generates a warning.

Tainting database reads

This works great for data in memory during a single page request, but what about the database? Consider a simplified version of Drupal's node table, the basis of all locally stored content:

CREATE TABLE node (
   id integer auto_increment,
   uid integer,
   title varchar(255),
   body text
);

When a user submits a node, Drupal stores the exact text provided for the title and body; it performs validation during output (there is a very good reason for this approach). Therefore, all data read from the title and body fields should be "born tainted" just like $_GET because the user has complete control over it. The the title or body are output before being run through htmlspecialchars() or some other similar validator, a taint error should result. The uid field, however, is the internally-generated user number for the author of the node. The user has no control over it. If the user is output in HTML (perhaps as part of a CSS style to allow per-user styling), no taint error should occur.

In Taint Drupal, I mark the title and body fields in the database schema as tainted (see, I told you it would come in handy! :-)). Whenever a SELECT query reads in these columns, my custom database driver marks them as tainted, just as if they had come directly from a user (which, ultimately, they did). Since the uid column is not marked tainted, the database driver leaves it alone.

Taint-checking database writes

Alert readers may have noticed I wrote "the user has no control over" the uid value. Well, what if a bug gives the user control? Taint Drupal prevents this by performing the reverse operation of SELECT-tainting: INSERT/UPDATE-taint-checking. Whenever it writes data to a column that is not marked tainted, it verifies that data is not tainted and logs an error if it is.

Demonstration

Here's a perfect example of Taint Drupal in action. Drupal 6.0 contained this code:

<?php
function node_page_edit($node) {
 
drupal_set_title($node->title);
  return
drupal_get_form($node->type . '_node_form', $node);
}
?>

In Drupal 6, the drupal_set_title() function required that the caller validate the title value, typically with check_plain(), so this code represented a XSS vulnerability. A user could set a node's title to something like "<script>alert('XSS!')</script>" and, when some other user or administrator visited the node's Edit page, the script could execute. (This bug was fixed in Drupal 6.1.)

With Taint Drupal, visiting the Edit page for *any* node while this bug exists generates warnings:

  • warning: print() [function.print]: Argument contains data that is not converted with htmlspecialchars() or htmlentities() in .../themes/garland/page.tpl.php on line 7.
  • warning: print() [function.print]: Argument contains data that is not converted with htmlspecialchars() or htmlentities() in .../themes/garland/page.tpl.php on line 70.

page.tpl.php lines 7 and 70 are the two places where the node title is displayed in HTML. So, instead of waiting for someone to discover the vulnerability, Taint Drupal would have pointed it out the moment it was created.

Current status

Taint Drupal is still very much experimental. It depends on patches to Taint PHP, a custom-built version of the PHP interpreter, minimal Drupal core patches, and a custom module. It is also far from complete. The Taint PHP patches are excellent but incomplete; for example, I discovered that the internal PHP function str_replace() was not taint-enabled and as a result all $_POST data processed by the Form API was being de-tainted automatically! (I fixed that.) Not all of Drupal's core tables are yet taint-marked correctly. I am handling SELECT and INSERT queries, but not UPDATE or DELETE queries. There is a lot left to do.

If you'd like more details about this work, vote for my session proposal at DrupalCon 2008.

Jul 16 2008
tom
Jul 16
Drupal 6 can use to many stylesheets for IE6

Many of the Drupal sites I develop are based on the excellent Zen theme which comes ready made with a special CSS file to fix many of Internet Explorer's well known bugs. However, one such bug affecting IE4 to IE6, can't be fixed with special css rules: all style tags after the first 30 style tags on an HTML page are not applied in Internet Explorer. When your Drupal site uses a lot of extra modules, the number of stylesheets can quickly build up to over this limit.

Luckily, there is a fix built into the Drupal core itself. In the Performance page (Administer >> Site Configuration >> Performance) there is an option entitled Optimize CSS files. This will combine all your stylesheets in to one file and as a result it will fix the bug and all your stylesheets will now load. It will also result in a significantly faster Drupal. On the downside, it is a pain in the ass to develop with this setting enabled since changes to your stylesheets will not take effect unless you manually flush Drupal's cache. Enabling the devel module can make this a very quick process.

Jul 16 2008
ted
Jul 16

The .htaccess file included with Drupal tells Apache to send all 404 requests to Drupal to handle. While this is great in some cases, the performance degradation can have a huge impact on a site that has millions of users.

When Drupal processes a 404, it has to bootstrap Drupal, which includes Apache loading up the PHP process, gathering all of the Drupal PHP files, connecting to the database, and running some queries. This is quite expensive when Apache can be told to simply say "Page not found" without having to incur any of that overhead.

Now you might say your site doesn't have any broken URLs as you haven't changed any. Well that's great, but as your site grows, it is going to be a target for spammers and hackers. They are going to start requesting all sorts of file to see if they can find an exploit. Instead of bootstrapping Drupal each time to tell them that DLL file doesn't exist, it would be much better if Apache could just say that, to save resources for your real users.

So, what can you do? How can you stop Drupal from handling 404s but not break modules like imagecache?

Imagecache is one of the few modules that relies on Drupal's 404 handling. It is a very smart module that automatically resizes images. Instead of resizing every single image as they are uploaded, it only resizes them when they are requested, which is great. So if we're going to tell Drupal not to handle 404s, we need to be careful not to break this highly useful module.

To see this in action, visit the ParentsClick Network and test out some 404s. You'll notice that 404s for files and Drupal paths show the same page. The following is the procedure we used to prevent Drupal from handling 404s.

A note, this functionality should really be in core, and this patch is where the necessary .htaccess code used below comes from, only being slightly modified to prevent Drupal from handling 404s completely. The below code is tested and working on Drupal 5.

Step 1 - Update your .htaccess file

- ErrorDocument 404 /index.php
+ ErrorDocument 404 /sites/all/themes/foundation/404.php # path to your 404 file

+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteCond %{REQUEST_URI} !^/files/ # this makes it work with imagecache
+  RewriteCond %{REQUEST_URI} \.(png|gif|jpe?g|s?html?|css|js|cgi|ico|swf|flv|dll)$
+  RewriteCond %{REQUEST_URI} !^404.%1$
+  RewriteRule ^(.*)$ 404.%1 [L]
 
   RewriteCond %{REQUEST_FILENAME} !-f
   RewriteCond %{REQUEST_FILENAME} !-d
   RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]

What this basically does is it removes Drupal from handling 404s (removing the /index.php part) and tells Apache to use a specific file if it encounters a 404 (like a missing image or CSS file).

Step 2 - Tell Drupal to stop on 404s too

In your template.php inside of the phptemplatevariables(), add in this code:

// show custom 404 page
$headers = drupal_get_headers();
if (strpos($headers, 'HTTP/1.1 404') !== FALSE) {
  // make sure this path = ErrorDocument in .htaccess above
  include_once './sites/all/themes/foundation/404.php';
  exit();
}   

This tell Drupal to serve up this 404 page if it can't find the path. The benefit of this is your designers can work on the same file that handles 404s for both Apache and Drupal. It also stops Drupal from fully executing. In Drupal 6 this could happen much earlier using the preprocess templating functions.

Step 3 - Create a 404 file

Create a 404.php file (or 404.html or whatever you want) and place the file where ever you want. Make sure to update the ErrorDocument in the .htaccess to point to this file along with the Drupal template code.

And voila!

Written by on July 16, 2008

blog comments powered by
Jul 15 2008
P6
Jul 15

I got dismayed by politics the other day, so I decided to write some code. Code is rational. I decided to write another commenting enhancement.

On my political site I require folks to register in order to comment. Sometimes I want to throw the post open, let the world reply to specific items. So I wrote a simple module called Open Thread. It adds a checkbox to the node creation forms (immediately above the comment options so it's easy to find) which, when checked, marks the node as an open thread which anyone who can see the "New comment" link can comment on without administrative intercession. This means you can still disallow folks from commenting at all.

The attached module is Drupal 6 compatible. I wrote it first to begin the process of nudging myself toward the new version, but I got a D5 version which will go into production today. A few more enhancements will go into place, but it'll be in the D6 version...if I make the D5 version too perfect I will never move my production site.

Lotta stuff missing from what you'd need for a proper project...help text, for instance.  I don't think it will ever be a project though. I think it's going to become part of a larger comment enhancing module merging a couple of my modules that I always use. Plus I'm still not up for all the free support. But until then, here it is.

AttachmentSize 1.37 KB
Jul 15 2008
Jul 15

Over at Drupal Tough Love, chx and I just reviewed Signatures for Forums 5.x-2.3 which "provides user signatures that will be familiar to users of popular forum software" such as "the administrator can choose the input filter for signatures", conditional signatures that are hidden "if a post is under a particular length", and showing the signature only once per conversation.

Jul 15 2008
Jul 15

Our second episode features Nate Angell or @xolotl, a name that's hard to remember completely, but one that we will always remember. There's development talk later as he demonstrates the shiny, new iPhone app called iToony.

But first, we chatted about Drupal for large organizations and the relative livability of various European cities—all accompanied by extremely loud (but pleasant) French songs, one of which may or may not be a rendition of Cole Porter's Night And Day.

The image Nate made of Bram Pitoyo using the iToony app is here.

Jul 14 2008
Jul 14

chx and Morbus reviewed Signatures for Forums 5.x-2.3 which "provides user signatures that will be familiar to users of popular forum software" such as "the administrator can choose the input filter for signatures", conditional signatures that are hidden "if a post is under a particular length", and showing the signature only once per conversation. Liam McDermott, the developer, requested this review.

Standard disclaimers: If you have a suggestion on how to better these reviews, or have eyeballed one of our own mistakes, please let us know by leaving a comment. Our reviews are not necessarily security audits: if we don't mention any problems, it doesn't mean we assert the module is Security Team approved (of which we're both members). Finally, if you'd like us to review your own (contributed) module code, don't hesitate to let Morbus or chx know and we'll add it to our queue.

The Reviewed Files

The Review

  1. There's no README.txt. One would assume that, if a developer is going to duplicate a "popular forum" software's functionality, they should also provide handholding on how to migrate users of that software to Drupal. The lack of a README.txt negatively impacts perceptions: the potential migrator asks "do all Drupal modules lack documentation?" or "am I a second-class citizen because I merely want to do what I've done before?" A module's project page on drupal.org should not be used for this purpose. Create a README.txt in the same text format as core: see bot.module's README.txt for an example.
  2. Is it "Signatures for Forums" (the module's project page; uppercase F) or "Signatures for forums" (inside signature_forum.info; lowercase F) or just "signature" (inside hook_perm())? Our inclination would be "Signatures for Forums", since it's a proper name (as opposed to most other strings used inside a module).
  3. The signature_forum.info uses a package of "User goodies" and this is discouraged: "If your module comes with other modules or is meant to be used exclusively with other modules, enter the name of the package here ... In general, this field should only be used by large multi-module packages, or by modules meant to extend these packages, such as CCK, Views, E-Commerce, Organic Groups ... All other modules should leave this blank."
  4. We'll gladly confess ignorance, but we've not personally used other forums that a) hide a signature if the post's content is small or b) only shows the signature once each thread or conversation. It seems that, for example, phpBB needs a mod to show the signature once per thread; vBulletin also needs a change one way or another. We recommend reconsidering "familiar to users of popular forum software". These two features are mentioned in the module description and also implied by signature_forum.info: "Manages users signatures in the style most forum users will be familiar with." Descriptions and documentation should strive to be free of opinions: we'd revise this to something like "Tweaks signatures in ways inspired by other traditional forum software." It says much the same thing, but doesn't make us feel out of touch. This would also apply to the definition in hook_help().
  5. Coder found no errors. Awesome.
  6. Core uses "Implementation of hook_NAME()." - some definitions are missing the ending period.
  7. One of Drupal Tough Love's primary tenets is "to do what core does" and this applies equally to every aspect of module creation. A large number of Drupal 5 contributions don't support PostgreSQL because their developers "don't have it installed", even though their SQL would run just fine if only there were a proper hook_install(). Drupal 6's SchemaAPI solves this issue (for table definition, at least), but there's still column retardation, where a column representing a user's ID (or any other standardized data) is marked up in a different way than in core. For example, in signature_forum.install, we see:

    <?php
    case 'mysql':
    case
    'mysqli':
    case
    'pgsql':
     
    db_query("CREATE TABLE {users_signature} (
        uid integer,
        signature text default NULL,
        PRIMARY KEY (uid)
        ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "
    );
    ?>

    Not only does this force all three databases into one definition (causing the MySQL UTF8 workaround on the final line to be erroneously applied, but thankfully ignored, by PostgreSQL) but uid integer is (letter-for-letter) a different definition than what core uses. When in doubt, steal liberally from core's own .install files. The above would more correctly be written in Drupal 5 as:

    <?php
    case 'mysql':
    case
    'mysqli':
     
    db_query("CREATE TABLE {users_signature} (
        uid int NOT NULL default '0',
        signature text,
        PRIMARY KEY (uid)
        ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "
    );

      break;

    case
    'pgsql':
     
    db_query("CREATE TABLE {users_signature} (
        uid int NOT NULL default '0',
        signature text,
        PRIMARY KEY (uid)
        )"
    );  break;
    ?>

    Eagle-eyed viewers will note we left out the default NULL on the definition for signature text. MySQL "BLOB and TEXT columns cannot have DEFAULT values", but it will gladly ignore the attempt. Since there's no plausible default on MySQL, we did not bother providing one for PostgreSQL. Sure, this is more code for little actual gain, but it's also more accurate and standardized. Even the Signature for Forums .install for Drupal 6's Schema API isn't "like core"; compare the definition of a uid column in node.install with this module's 'uid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0) - there's no description, but there is an unnecessary unsigned.

    Steal liberally from core's definitions and patterns: if you mean the same thing, say the same too.

  8. Drupal core uses constants to give human-readable names to numeric values, and Signature for Forums does the same, even going the extra distance by properly documenting them. Thank you!
  9. This module contains another example of "store our module-specific settings in a single array". We're still not entirely sure where this pattern is coming from (if you know, tell us!). There seems little apparent rationale besides "I'm too lazy to variable_del() each of them in my hook_uninstall().", "I'm gonna forget to update it when I add a new setting.", or the unarguable "I like it!" By forcing your module's settings into an array, you waste a line loading all your settings to check just one value, and also require an extra function to build the defaults (seen also in Printer-Friendly Pages #6, only in slightly different form). We're not seeing how this is such an advantage that inventing something "not like core" is warranted. A healthy amount of code is reinvented in signature_forum_admin_settings_submit() too.

    Take a look at, pseudo-roughly, what Signatures for Forums currently does for its configuration:

    <?php
    function signature_forum_defaults() {
      return array(
        ...
       
    'signature_forum_template' => " [default value] ",
      );
    }function
    signature_forum_form($default_values) {
     
    $form['template'] = array(
        ...
       
    '#default_value' => $default_values['signature_forum_template'],
      );  return
    $form;
    }function
    signature_forum_admin_settings_submit($form_id, $form_values) {
      if (
    $form_values['op'] == 'Reset to defaults') {
       
    variable_set('signature_forum_settings', signature_forum_defaults());
        return;
      }  ...
     
    $settings['signature_forum_template'] = $form_values['template'];
     
    variable_set('signature_forum_settings', $settings);
    }
    ?>

    And, compare it to proper approach for use with system_settings_form():

    <?php
    function signature_forum_form() {
     
    $form['signature_forum_template'] = array(
        ...
       
    '#default_value' => variable_get('signature_forum_template', ' [default value] ');
      );  return
    system_settings_form($form);
    }
    ?>

    The second code sample is literally all you need when using system_settings_form() and module configuration variables properly. One doesn't have to worry about resetting the default values, creating an extra function to hold them all, or renaming them from the Form API definitions to what they actually get stored as. (For Signatures for Forums, an extra #submit function would also be required to handle the deletion of hardcoded user signatures in existing posts.)

    Inventing new approaches to established Drupal patterns merely walls your module into its own little garden, requiring even more special knowledge to enter. Drupal already requires a lot to learning - help folks out by attempting to keep the established patterns prevalent in your modules. One may argue, and many have, that the core approach needlessly replicates the duplicate value across every use of that variable. And, they'd be right, but the proper way to enact change is to get a patch into core so that everyone benefits, such as the proposed hook_variables() to control and define default values.

  10. Use elseif not else if for your conditionals.
  11. The $section argument of hook_help() doesn't need a default value - one is always passed through by core. Also, the documentation within needs to be rewritten: besides a missing grammatical semi-colon, it suggests that users can use BBCode in their signatures, but doesn't provide any help on how.
  12. user_access() properly restricts administrative configuration in your menu definitions, but the same permission is erroneously checked again within signature_forum_admin_settings(). The approach to that function is slightly odd too: we could see the rationale if signature_forum_form() (the actual form definition) was called multiple times throughout the code, but it isn't, instead serving as just another layer of abstraction. We'd rewrite signature_forum_admin_settings() to something like the below, which removes signature_forum_form() entirely. Combining this with #9 (above) will push your module configuration firmly into the established patterns inherent in core.

    <?php
    function signature_forum_admin_settings() {
     
    $settings = variable_get('signature_forum_settings', signature_forum_defaults());  $form['signature'] = array( ... );  // include the rest of signature_forum_form() here.  return system_settings_form($form);
    }
    ?>
  13. Generally speaking, all form elements should have a #title. If the #title isn't descriptive enough, a #description can be used to further explain what's going on. Fieldsets are used to collect similar fields together (literally, a fieldset is a "set of fields"). signature_forum_form(), however, has a fieldset with no #title and only "one" item (checkboxes for a site's node types). While this approach is OK here because it models the "Display post information on" fieldset in Drupal's theme configuration (admin/build/themes/settings), your current #description should move to the #title.
  14. The module's configuration page has a fieldset entitled "Show signatures once per conversation" - this is action text best reserved for an interactive form element, such as a checkbox or radio button. Rename the fieldset to something more descriptive of the fields within, such as "Per-conversation signatures".
  15. t('<strong>%s</strong> will be replaced with user\'s signature.'). Once upon a time, PHP was a bit slower when using double quotes for its strings, so people tried to avoid it all costs. This eventually became "scripture", disregarding the fact that noone even remembers which PHP version cured the lag; we can't bother to look up whether it was PHP 4.0 or PHP 4.1. Use double quotes: they are not your enemy! Needlessly escaped apostrophes, on the other hand, make code hard to read. signature_forum_help(), signature_forum_menu(), signature_forum_form(), and a few others, all contain escaped apostrophes. Lastly, in this particular example, you need "the user's signature", not just "user's signature". UPDATE: You should also remove the <strong> - not only is the use of HTML in t() questionable, but core doesn't markup placeholders in this way.
  16. Signature for Forums offers a signature template that the admin can customize, placing a %s where they want a user's signature to appear by default. To interpolate the user's signature into this template, the code uses sprintf() in signature_forum_get_signature():

    <?php
    $signature
    = sprintf($settings['signature_forum_template'], trim($signature));
    ?>

    The Drupal way to do this, exemplified in situations like mails sent out to users (admin/user/settings), is with strtr(), which also allows the use of named placeholders (i.e., !signature). Consider replacing your existing %s with !signature and the sprintf() with:

    <?php
    $signature
    = strtr($settings['signature_forum_template'], array('!signature' => trim($signature));
    ?>

  17. We use FALSE and TRUE, not the lowercase variants.
  18. A critical bug exists with the use of check_markup() in signature_forum_get_signature(), which is rigged to err on the safe side by defaulting its $check attribute to TRUE. This means that, for every single visitor, they're being checked for whether they have access to the specified format. For example, if the submitter has access to, say, the <img> tag but the visitor does not, then the signature will be filtered into an empty string. You need to add a third parameter, FALSE, to your check_markup(). filter_form() takes care to show only the filter formats the submitter has access to, so that's not a problem.
  19. Replace the longer code in signature_forum_nodeapi() with $node->content['body']['#value'] .= signature_forum_get_signature($node);. The matching logic in signature_forum_comment() is correct.
  20. Some of your SQL uses column=%d. The proper style is a space on both sides of the equal sign.
Jul 14 2008
Jul 14

Change does not necessarily equal quality

The key to a successful website is to change the content on your website as often as possible. right?

Change can be a dangerous notion on a website, and is something that is made easy and readily available by drupal and other CMS systems. But you must remember that arbitrary change for the sake of change serves no function. And can hurt your website, by disorienting users, and hurt your search engine rankings as well.

The change dilemma often comes especially when someone is new to a CMS system and starts publishing their own content. When you first start out with drupal for instancee one of the first things users want to do is edit all the content that is available to edit on the site. And this behavior is perfectly acceptble, in fact I heartily recommend that you customize your drupal site to suit yourself and your needs. The trouble comes with the default model of having two content types: the page and the story

Problem: Users want to update their content.
Solution: User installs a CMS (such as drupal)
Failure: User doesn't understand the best way to engage users with a consistant behavior.

This problem stems from the inability to tell the difference between these two content types. What is story content? What is page content? These two terms are confusing to users and here is why.

A lot of beginning drupal users are new to the world of creating their own content. Often in the past they would send their content to a design studio to have it put in the template, or they would put it on the page themselves with one of the plethora of wysiwyg html editing applications. For these users a CMS is a new experience, and there are little or no classes on writing for the web anyhow.

These users because of their previous knowledge and how they are used to doing things will have a mental model of drupal which is more in line with the page concept. That is these users think in terms of one page which is to be updated instead of the story concept of a string of interrelated pages which chronologically tell a narrative about the website, and the website's content.

The story concept is new to them, and is the same concept as a blog. Because of this lack of grasping this concept the user doesn't think about the consequences of altering these story nodes. What ends up happening then, is a rewritten history which ultimately shows change, but it shows change in such a way that the user is disoriented

When a user has no clear idea of the nature of the event and in its place sees only a recap or congratulations in it's place they are disoriented

Instead of disorienting the user, we should not be supplanting old information with the new, but instead linking through old information to an update, or at the very least including the old text along with the new text when updating. Though I must stress that I highly recommend creating a new page instead if for no other reason than those people reading your content through a feed reader will most likely not see your edits. With a new post you will bring this updated information right back up to the forefront of their reading queue

Related Posts

Debunking the myth: Content doesn't create itself

Content Management systems are the easy end all solution for creating content for the web, a magical thing which makes it easy to create awesome websites which get tons of visitors, right?
Jul 14 2008
Jul 14

With an automated testing framework in core, Drupal is now far along the road to a practice of Test-driven development. But there's one thing missing: a complete, in-depth, up-to-date tutorial on how to actually write these tests. Although there is the SimpleTest Automator module to help you create these tests through the user interface, it is currently imperfect and under development, so this post will be a tutorial on writing these tests manually.

Testing resources

Here's just a general list of resources we should keep handy while writing our test:

  • Testing API functions - This is a quick cheat sheet of some functions that are very helpful during testing. These include controlling the internal testing browser, creating users with specific permissions, and simulating clicking on links that appear in the page.
  • Available assertions - Assertions are how you determine whether your code is working or not - you assert that it should be working, and let the testing framework handle the rest for you. This is a library on the assertions that are available to use in our testing framework.

Know what you're testing

In this example, I will be testing Drupal's ability to change the site-wide theme. I know how to test this manually - I would go to the admin/build/themes page, and select the radio button of a different default theme, and then make sure the theme had changed when I reload the page. In order to automate this test, I will simply write code to repeat the same actions I would have done manually.

Start writing your test case

Ok, now we get to the code. First of all, every test case will be a class that extends the DrupalWebTestCase class. So we'll start out with this code:

<?php
// $Id$

class ThemeChangingTestCase extends DrupalWebTestCase {

}
?>

Now, we have to tell the testing framework a little bit about our test. We give it three pieces of information - the name of the test, a brief description of the test, and the group of tests this test is a part of. We will return this information in our implementation of getInfo() in our test class:

<?php
// $Id$

class ThemeChangingTestCase extends DrupalWebTestCase {

  /**
   * Implementation of getInfo().
   */
 
function getInfo() {
    return array(
     
'name' => t('Changing the site theme.'),
     
'description' => t('Tests the changing of the site-wide theme through the administration pages.'),
     
'group' => t('Theming'),
    );
  }

}
?>

Now, let's add our test function. The most thorough way to test this would be to cycle through all the themes, setting each in turn as the default, and that's what we'll do. So we'll get this:

<?php
// $Id$

class ThemeChangingTestCase extends DrupalWebTestCase {
 
// We need a list of themes to cycle through.
 
var $themes = array('bluemarine', 'chameleon', 'garland', 'marvin', 'minnelli', 'pushbutton');

  /**
   * Implementation of getInfo().
   */
 
function getInfo() {
    return array(
     
'name' => t('Changing the site theme.'),
     
'description' => t('Tests the changing of both the site-wide theme and the user-specific theme, and makes sure that each continues to work properly.'),
     
'group' => t('Theming'),
    );
  }

  /**
   * This is the function where we will actually do the testing.
   * It will be automatically called, so long as it starts with the lowercase 'test'.
   */
 
function testSitewideThemeChanging() {
    foreach (
$this->themes as $theme) {
     
// We need a test user with permission to change the site-wide theme.
     
$user = $this->drupalCreateUser(array('administer site configuration'));
     
$this->drupalLogin($user);
     
// Now, make a POST request (submit the form) on the admin/build/themes page.
     
$edit = array();
      foreach (
$this->themes as $theme_option) {
       
$edit["status[$theme_option]"] = FALSE;
      }
     
$edit["status[$theme]"] = TRUE;
     
$edit['theme_default'] = $theme;
     
$this->drupalPost('admin/build/themes', $edit, t('Save configuration'));
    }
  }
}
?>

Whoa, that's a lot of code. Let's go through what I've done step by step:

  1. I've declared a class variable that lists the themes that we're going to test.
  2. In my test function, I'm cycling through each theme, in turn setting each one as the default.
  3. I create a user with enough permissions to change the site-wide theme.
  4. After logging this user in, I proceed to make a POST request to the admin/build/themes page, enabling only the theme we're testing, and setting that theme to be the theme default.
  5. I then submit the form by clicking on the 'Save configuration' button.

Now this works very well, but one thing is still missing - how am I sure that the theme has changed? If I were doing this manually I could tell by just looking - but when I'm automating this, I will test for the theme's css files in the page source of the reloaded admin/build/themes page upon submission. To do this, I will use the assertRaw() function, which makes sure that some text is found in the raw HTML of the current page in the internal browser:

<?php
// $Id$

class ThemeChangingTestCase extends DrupalWebTestCase {
 
// We need a list of themes to cycle through.
 
var $themes = array('bluemarine', 'chameleon', 'garland', 'marvin', 'minnelli', 'pushbutton');

  /**
   * Implementation of getInfo().
   */
 
function getInfo() {
    return array(
     
'name' => t('Changing the site theme.'),
     
'description' => t('Tests the changing of both the site-wide theme and the user-specific theme, and makes sure that each continues to work properly.'),
     
'group' => t('Theming'),
    );
  }

  /**
   * This is the function where we will actually do the testing.
   * It will be automatically called, so long as it starts with the lowercase 'test'.
   */
 
function testSitewideThemeChanging() {
    foreach (
$this->themes as $theme) {
     
// We need a test user with permission to change the site-wide theme.
     
$user = $this->drupalCreateUser(array('administer site configuration'));
     
$this->drupalLogin($user);
     
// Now, make a POST request (submit the form) on the admin/build/themes page.
     
$edit = array();
      foreach (
$this->themes as $theme_option) {
       
$edit["status[$theme_option]"] = FALSE;
      }
     
$edit["status[$theme]"] = TRUE;
     
$edit['theme_default'] = $theme;
     
$this->drupalPost('admin/build/themes', $edit, t('Save configuration'));
     
// Make sure we've actually changed themes.
     
$this->assertCSS($theme);
    }
  }

  /**
   * Custom assert method - make sure we actually have the right theme enabled.
   *
   * @param $theme
   *   The theme to check for the css of.
   * @return
   *   None.
   */
 
function assertCSS($theme) {
   
// Minnelli is the only core theme without a style.css file, so we'll use
    // minnelli.css as an indicator instead.
   
$file = $theme == 'minnelli' ? 'minnelli.css' : 'style.css';
   
$this->assertRaw(drupal_get_path('theme', $theme) . "/$file", t("Make sure the @theme theme's css files are properly added.", array('@theme' => $theme)));
  }
}
?>

Note that I've added my own assertCSS() function in the above code. You're perfectly free to add whatever functions you may desire—just keep in mind that if they start with the lowercase 'test', they will be automatically called as a test function!

That concludes the basic tutorial. Read on if you're interested in going beyond the basics! :)

Advanced testing techniques

Here I'll go over a few techniques for better, easier, simpler, and just overall awesomer testing.

  • Using setUp() and/or tearDown() methods.

    Sometimes it can be useful to run some code before and/or after our test methods are finished running. To do this, we can implement setUp() and/or tearDown(), which run before and after the test methods, respectively:

    <?php
    // $Id$

    class ThemeChangingTestCase extends DrupalWebTestCase {

      /**
       * Implementation of setUp().
       */
     
    function setUp() {
       
    // Invoke setUp() in our parent class, to set up the testing environment.
       
    parent::setUp();
       
    // Create and log in our test user.
       
    $user = $this->drupalCreateUser(array('administer site configuration'));
       
    $this->drupalLogin($user);
      }
    }
    ?>

    In the above example, we've created and logged in our test user in our setUp() method. As a result, that user will be logged in for our test methods; this can be considered somewhat cleaner code.

    We can also use setUp() to enable additional modules we may need:

    <?php
    // $Id$

    class ThemeChangingTestCase extends DrupalWebTestCase {

      /**
       * Implementation of setUp().
       */
     
    function setUp() {
       
    // Invoke setUp() in our parent class, to set up the testing environment.
        // We're going to test the theming of the search box and the tracker page,
        // so we need those modules enabled.
       
    parent::setUp('search', 'tracker');
       
    // Create and log in our test user.
       
    $user = $this->drupalCreateUser(array('administer site configuration'));
       
    $this->drupalLogin($user);
      }
    }
    ?>

    Note: Make sure to always call parent::setUp() and parent::tearDown() if you override them! If you don't, the testing framework will either fail to be set up, or fail to be teared down, successfully.

  • Dealing with HTML content using SimpleXML.

    Our assertCSS() function in the basic example is far from ideal. The path to the theme's style.css could appear on the page as plain text (not part of a css link), and the test would still pass.

    To get around this weakness, we can handle the HTML content of the fetched page using SimpleXML. For example:

    <?php
    // $Id$

    class ThemeChangingTestCase extends DrupalWebTestCase {

      /**
       * Custom assert method - make sure we actually have the right theme enabled.
       *
       * @param $theme
       *   The theme to check for the css of.
       * @return
       *   None.
       */
     
    function assertCSS($theme) {
       
    // Minnelli is the only core theme without a style.css file, so we use
        // minnelli.css as an indicator instead.
       
    $file = $theme == 'minnelli' ? 'minnelli.css' : 'style.css';
        if (
    $this->parse()) {
         
    $links = $this->elements->xpath('//link');
          foreach (
    $links as $link) {
            if (
    strpos($link['href'], base_path() . drupal_get_path('theme', $theme) . "/$file") === 0) {
             
    $this->pass(t("Make sure the @theme theme's css files are properly added.", array('@theme' => $theme)));
              return;
            }
          }
         
    $this->fail(t("Make sure the @theme theme's css files are properly added.", array('@theme' => $theme)));
        }
      }
    }
    ?>

    The parse() method must be called in order to populate $this->elements. It is not called automatically on every page load because this would lead to a significant performance drain.

  • Creating an unused base class, and then extending it.

    Sometimes, it would make sense for two test cases to share API functions, or even setUp and tearDown() functions. In order to do this, we'll set up one base test case that extends DrupalWebTestCase, and then create several children test cases that extend our base test case. For example:

    <?php
    // $Id$

    class ThemeChangingTestCase extends DrupalWebTestCase {

      function assertCSS($theme) {
       
    // ...
     
    }
    }

    class SiteWideThemeChangingTestCase extends ThemeChangingTestCase {

      /**
       * Implementation of getInfo().
       */
     
    function getInfo() {
       
    // ...
     
    }
    }

    class UserSpecificThemeChangingTestCase extends ThemeChangingTestCase {

      /**
       * Implementation of getInfo().
       */
     
    function getInfo() {
       
    // ...
     
    }
    }
    ?>

    Note that in order for a test case to not actually be run itself (or even show up on the administration interface), all it has to do is not implement getInfo().

Now you are officially qualified to start writing tests! If you're looking for a place to get started, check out the issue queue. Also, the code coverage reports are a great place to see what needs to be tested in core.

Jul 13 2008
Jul 13

I was bored for a couple of hours this weekend and started to wonder if a microblogging site in Drupal.

Read the full post on Millwood Online

Jul 13 2008
Jul 13

So building on my last post for creating CCK fields, here's some code I whipped up to migrate from the D6's core upload.module to the filefield.module. This isn't general purpose code but might help someone else out. The catch is I'd built a video node with and was using the upload module to attach exactly two files, an image and a video. The new node will have separate thumbnail and video fields. If you'll be moving to a multi-value field this code won't work for you.

The gist is the same as before, setup your field for video and your field for images then export using:

<?php
var_export
(content_fields('field_web_video', 'video'), 1);
?>

and

<?php
var_export
(content_fields('field_video_thumb', 'video'), 1);
?>


Then roll that into an update function that also moves the file data around in the database. Code is after the jump.

<?php
/**
* Add filefields to the video nodes and migrate the files.
*/
function foo_video_update_6000() {
 
// Make sure the filefield* modules are installed correctly.
 
drupal_install_modules(array('filefield', 'filefield_image', 'filefield_imagecache'));
 
drupal_flush_all_caches();  module_load_include('inc', 'content', 'includes/content.admin');
 
content_alter_db_cleanup();
 
 
// Need to load the CCK include file where content_field_instance_create() is defined.
 
module_load_include('inc', 'content', 'includes/content.crud');
 
 
$thumb_field = array (
//
// DROPPED THE CCK FIELD DEFINITION FROM HERE
//
 
);
 
content_field_instance_create($thumb_field);  $video_field = array (
//
// DROPPED THE CCK FIELD DEFINITION FROM HERE
//
 
);
 
content_field_instance_create($video_field); 
 
// Migrate the videos
 
$fids = array();
 
$result = db_query("SELECT n.nid, n.vid, f.fid, u.description, u.list FROM {files} f INNER JOIN {upload} u ON f.fid = u.fid INNER JOIN {node} n ON u.vid = n.vid WHERE n.type = 'video' AND filemime LIKE 'video/%'");
  while (
$file = db_fetch_object($result)) {
   
$fids[] = $file->fid;
   
// Check for a record... it adds a bunch more queries but it's simple and we only run this once.
   
if (db_result(db_query("SELECT COUNT(*) FROM {content_type_video} WHERE vid = %d", $file->vid))) {
     
db_query("UPDATE {content_type_video} SET field_web_video_fid = %d, field_web_video_description = '%s', field_web_video_list = %d WHERE vid = %d",
       
$file->fid, $file->description, $file->list, $file->vid);
    }
    else {
     
db_query("INSERT INTO {content_type_video} (nid, vid, field_web_video_fid, field_web_video_description, field_web_video_list) VALUES (%d, %d, %d, '%s', %d)",
       
$file->nid, $file->vid, $file->fid, $file->description, $file->list);
    }
  }
 
db_query("DELETE FROM {upload} WHERE fid IN (". db_placeholders($fids, 'int') .")", $fids);   // Migrate the images
 
$fids = array();
 
$result = db_query("SELECT n.nid, n.vid, f.fid, u.description, u.list FROM {files} f INNER JOIN {upload} u ON f.fid = u.fid INNER JOIN {node} n ON u.vid = n.vid WHERE n.type = 'video' AND filemime LIKE 'image/%'");
  while (
$file = db_fetch_object($result)) {
   
$fids[] = $file->fid;
   
// Check for a record... it adds a bunch more queries but it's simple and we only run this once.
   
if (db_result(db_query("SELECT COUNT(*) FROM {content_type_video} WHERE vid = %d", $file->vid))) {
     
db_query("UPDATE {content_type_video} SET field_video_thumb_fid = %d, field_video_thumb_description = '%s', field_video_thumb_list = %d WHERE vid = %d",
       
$file->fid, $file->description, $file->list, $file->vid);
    }
    else {
     
db_query("INSERT INTO {content_type_video} (nid, vid, field_video_thumb_fid, field_video_thumb_description, field_video_thumb_list) VALUES (%d, %d, %d, '%s', %d)",
       
$file->nid, $file->vid, $file->fid, $file->description, $file->list);
    }
  }
 
db_query("DELETE FROM {upload} WHERE fid IN (". db_placeholders($fids, 'int') .")", $fids);  // No more uploads on video nodes!
 
variable_set('upload_video', 0);  return array();
}
?>

Update: I posted some additional info on this topic over on: http://drupal.org/node/292904

Jul 11 2008
Jul 11

If you really must know, yes, I'm getting an iPhone. It was not a no-brainer until very recently, when Rogers/Fido offered a promotional 6 GB plan for $30 on top of a voice plan. Still not a no-brainer, because after some speculation, about whether my plan was eligible for the most coveted of mobile computing platforms, I called Fido today to find out if I'm eligible for that which must be worshiped and/or bitched about. The plan has nationwide Fido-to-Fido calling, necessary for calling the girl while we had our long distance relationship, my being in Vancouver and her being in Toronto; unlimited weekends and evenings; something called "Can. ID" (can someone enlighten me as to what that does?); and that's it for exactly 30 dollars a month. That last point is important because it qualifies me for the $249 8 GB iPhone, not the $199 8 GB iPhone, which comes with a plan of more than 30 dollars a month.

Added to my current plan are Caller ID and 50 monthly text messages. No voicemail for quite some time now: it was always quicker for me to call the person back and ask them what they were calling about then to listen to the message, find a pen to write down the number (which requires rewinding not being as fast a write as people are talkers) and forget to delete the message, then listen to my voicemail later on wondering if it was a new message or not. Visual Voicemail looks interesting, but I don't get enough phone calls to warrant paying for it. Forgetting to ask the helpful French-accented Fido representative if I could keep the add on features, I still assume the answer is yes.

Frequently Asked Questions

What am I going to do with the iPhone?

Re-document my world, using Drupal of course, since the phone has GPS and there are going to be all kinds of cool applications on it. (Drupal + Location is in currently in a state of flux and I already have some geolocation stuff happening on this site and am planning more.) And listen to music and watch videos. Not making or receiving many phone calls, I don't really care all that much about the phone part of the iPhone.

Could I have bought an unlocked N95 at a cool $600 from an unknown Craigslist posting?

Absolutely. The 3-year contract the Rogers/Fido alliance goes in the "cost" column, and at $400 maximum to exit and not anticipating a move outside Canada in the next 3 years, that's something I can handle. The N95 is nice, but I can't stand the complete lack of usability on the Series 60 operating system. Everything's a pain. Everything on the iPhone looks so smooth.

When am I getting it?

Not today, and likely not this weekend. I'll wait until next week when the rush dies down a little bit. People are saying that stock is low today as well.

Does that mean you, dear reader, should get an iPhone too?

You don't have to get an iPhone.

What about your existing iPod mini, GlobalSat DG-100 GPS logger and Nokia N70 cell phone?

I never got Internet sharing between a Series 60 phone (like my Nokia N70) working with my Mac, and now I don't have to! The iPod, GPS unit and N70 will get new lives for people that don't have an iPhone. Since I don't have the latter yet, it'll be a few months before I give them away.

Jul 11 2008
tom
Jul 11
Howto use geshifilter with FCKEditor in Drupal

If you need to do syntax highlighting in your Drupal posts then geshifilter is the module you are looking for. However, if you also have FCKEditor enabled to give you WYSIWYG editing capabilities then you will need to do a little tweaking to get them to work well together.

The problem

The problem, as far as I can tell, is that FCKEditor converts all special html characters to html entities and then geshi forces these to output to to browser as literal text, so for example:

I want this to display exactly like this

Will probably end up looking more like this, especially if you switch between FCKEditor's source view and WYSIWYG vew:

&lt;p&gt;I want this to display &lt;b&gt;exactly&lt;/b&gt; like this&lt;/p&gt;

The solution

I still haven't found the perfect solution but this is what I did to make things work better:

  1. To allow us to easily use the geshi filter styles, add the style control to the FCKEditor toolbar by editing fckeditor.config.js:
    FCKConfig.ToolbarSets['DrupalFull'] = [
      ...
      ['FontFormat','FontName','FontSize', 'Style'],
      ...
    ];
  2. And then add some custom styles to the style menu by editing fckstyles.xml (I set up 3 styles - php html and css).
    <style element="codeblock" name="Php Code block" type="text/css">
      <Attribute name="language" value="php" ></Attribute>
    </style>
    
    <style element="codeblock" name="HTML Code block" type="text/css">
      <Attribute name="language" value="html" ></Attribute>
    </style>
    
    <style element="codeblock" name="CSS Code block" type="text/css">
      <Attribute name="language" value="css" ></Attribute>
    </style>
  3. To stop FCKEditor messing things up when switching between source and WYSIWYG views, you need to add this to your fckeditor.config.js file:
    FCKConfig.ProtectedSource.Add( /<codeblock language[\s\S]*?<\/codeblock>/gi  );

Caveats

As I said, it's not perfect. You must be in source mode to enter/edit your geshi filtered code, otherwise the HTML entity conversion will still happen. Once you have entered the code in source mode, you can freely switch between WYSIWYG and source modes, however, you will find that the code does not display correctly when in WYSIWYG mode (but it will display fine in your final output).

Jul 11 2008
tom
Jul 11

By default, Drupal allows you to include a search box directly into your theme. In most themes, when enabled, this search box will show up in the primary navigation bar as an input box labeled "Search this site", with a submit button labeled "Search". But what if you want to alter or completely hide these labels, or even add a new class to the input box?

How to do it

There have been loads of suggestions about how to do this on drupal.org, including altering the core Drupal source (not a good idea!), using a string replace function in a custom template file or using the String overrides module. But, the best suggestion came from this post and is the most Drupal friendly way of doing it.

Use Drupal 6's preprocess function in your theme's template.php file to modify the template variables before they are passed into the template files.

/**
* Override or insert PHPTemplate variables into the search_theme_form template.
*
* @param $vars
* A sequential array of variables to pass to the theme template.
* @param $hook
* The name of the theme function being called (not used in this case.)
*/

function mytheme_preprocess_search_theme_form(&$vars, $hook) {

// Modify elements of the search form
$vars['form']['search_theme_form']['#title'] = t('Search mysite.com');

// Set a default value for the search box
$vars['form']['search_theme_form']['#value'] = t('Search');

// Add a custom class to the search box
$vars['form']['search_theme_form']['#attributes'] = array('class' => t('cleardefault'));

// Change the text on the submit button
$vars['form']['submit']['#value'] = t('Go');

// Rebuild the rendered version (search form only, rest remains unchanged)
unset($vars['form']['search_theme_form']['#printed']);
$vars['search']['search_theme_form'] = drupal_render($vars['form']['search_theme_form']);

// Rebuild the rendered version (submit button, rest remains unchanged)
unset($vars['form']['submit']['#printed']);
$vars['search']['submit'] = drupal_render($vars['form']['submit']);

// Collect all form elements to make it easier to print the whole form.


$vars['search_form'] = implode($vars['search']);


}

Note: you must rename the preprocess function so it's right for your theme (replace mytheme with the name of your theme)

In order for this to work, you must also ensure you have a copy of search-theme-form.tpl.php in your theme directory (you can grab it out of modules/search/ from your Drupal core files). You can then edit this as you like for further theming.

Extending it further.

The same techniques can also be applied for the search block. Just use the code from above, but replace all instances of search_theme_form with search_block_form, and make sure that you take a copy of search-block-form.tpl.php into your theme directory.

Jul 11 2008
jh
Jul 11
QR code of this URL QR code for this URL

Just a few hours ago Google announced the latest feature of their chart API: QR Codes. I always thought that it might be cool to have some QR image of the current URL on my website. So, if someone happens to read one of my articles over at some internet café or at a friend's machine, a QR code might be handy for "bookmarking".

Writing a QR encoder with PHP didn't look like much fun though. And then there is the processing overhead, which can be addressed with caching, but that only means more work. There is also that random string DoS attack vector. Far too much hassle, really. But if Google takes care of all that, it's a piece of cake.

That's why I wrote a little test module for Drupal. It lacks width/height options, but apart from that it's fully functional (as far as I can tell).

<?php
function google_qr_block($op = 'list', $delta = 0, $edit = array()) {
  if ($op == 'list') {
    $blocks = array();
    $blocks[] = array('info' => t('Google QR'),'cache' => BLOCK_CACHE_PER_PAGE);
    return $blocks;
  }
  else if ($op == 'view' && user_access('view Google QR')) {
    $query_string = drupal_query_string_encode($_GET, array('q'));
    if (empty($query_string)) {
      $query_string = NULL;
    }
    $request = trim($_REQUEST['q'], '/');
    $alias = drupal_get_path_alias($request);
    $path = $request;
    if ($alias != $request) {
      $path = $alias;
    }
    $url = urlencode(url($path, array('query' => $query_string, 'absolute' => TRUE)));
    $block = array(
    //'subject' => $url,
    'content' =>
    '<div class="google-qr-block">
    <img src="http://chart.apis.google.com/chart?chs=150x150&amp;cht=qr&amp;chl='.$url.'" alt="QR code for this URL" width="150" height="150"/>
    </div>'
    );
    return $block;
  }
}
?>

Using drupal_urlencode instead of urlencode doesn't work by the way.

If you're wondering why I'm not using that module here yet: Well, the right sidebar is already overcrowded the way it is. The only other sensible region is "Header", but that doesn't work well with the current theme. I'll probably reserve some space for it if I ever get around finishing my custom theme.

Download: Google QR module for Drupal 6.x (1kb)

Jul 09 2008
Jul 09
Posted by Heine on 9 Jul 2008 at 21:24 UTC
  • Advisory ID: DRUPAL-SA-2008-044
  • Project: Drupal core
  • Version: 5x, 6.x
  • Date: 2008-July-9
  • Security risk: Moderately critical
  • Exploitable from: Remote
  • Vulnerability: Multiple vulnerabilities

Description

Multiple vulnerabities and weaknesses were discovered in Drupal. Neither of these are readily exploitable.

Cross site scripting

Free tagging taxonomy terms can be used to insert arbitrary script and HTML code (cross site scripting or XSS) on node preview pages. A successful exploit requires that the victim selects a term containing script code and chooses to preview the node. This issue affects Drupal 6.x only.

Some values from OpenID providers are output without being properly escaped, allowing malicious providers to insert arbitrary script and HTML code (XSS) into user pages. This issue affects Drupal 6.x only.

filter_xss_admin() has been hardened to prevent use of the object HTML tag in administrator input.

Cross site request forgeries

Translated strings (5.x, 6.x) and OpenID identities (6.x) are immediately deleted upon accessing a properly formatted URL, making such deletion vulnerable to cross site request forgeries (CSRF). This may lead to unintended deletion of translated strings or OpenID identities when a sufficiently privileged user visits a page or site created by a malicious person.

Session fixation

When contributed modules such as Workflow NG terminate the current request during a login event, user module is not able to regenerate the user's session. This may lead to a session fixation attack, when a malicious user is able to control another users' initial session ID. As the session is not regenerated, the malicious user may use the 'fixed' session ID after the victim authenticates and will have the same access. This issue affects both Drupal 5 and Drupal 6.

SQL injection

Schema API uses an inappropriate placeholder for 'numeric' fields enabling SQL injection when user-supplied data is used for such fields.This issue affects Drupal 6 only.

Versions affected

  • Drupal 5.x before version 5.8
  • Drupal 6.x before version 6.3

Solution

Install the latest version:

  • If you are running Drupal 5.x then upgrade to Drupal 5.8.
  • If you are running Drupal 6.x then upgrade to Drupal 6.3.

If you are unable to upgrade immediately, you can apply a patch to secure your installation until you are able to do a proper upgrade. The patches fix security vulnerabilities, but do not contain other fixes which were released in these versions.

Note for site administrators

Drupal 5.8 and 6.3 no longer support the use of the object HTML tag in many text supplied by administrators. Such texts include the mission statement and taxonomy term descriptions.

Notes for developers

Drupal 6.3 has the new db_query placeholder %n for numeric fields (DECIMAL, NUMERIC). Custom queries should be updated to reflect this change.

Reported by

  • The session fixation issue was reported by Erich C. Beyrent.
  • The Taxonomy term XSS issue was reported by John Morahan.
  • The OpenID CSRF issue was reported by Peter Wolanin (Drupal security team).
  • The OpenID XSS issue was reported by Neil Drumm (Drupal security team).
  • The locale CSRF issue and the numeric SQL injection issue were reported by Heine Deelstra (Drupal security team).

Contact

The security contact for Drupal can be reached at security at drupal.org or via the form at http://drupal.org/contact.

Jul 07 2008
Jul 07

Druplicon on the Mac OS X Leopard login screen as the logoSwap out the Apple logo on your Mac OS X login with a Drupal Druplicon.

I followed the instructions on macosxhints.com and was able to swap my standard Apple logo on my Leopard login screen to a Druplicon. I'm sharing the GPL version of the Druplicon in the proper size and format for the replacement.

However you want to get it there, rename/replace the applelogo.tif file at

/System/Library/CoreServices/SecurityAgentPlugins/loginwindow.bundle/Contents/Resources/

with the one attached to this node or make your own 90x90 tif file.

AttachmentSize 31.93 KB

Jul 07 2008
Jul 07

If you frequently read my blog you may remember I was writing an article for Naked Wales about Drupal.

Read the full post on Millwood Online

Jul 05 2008
Jul 05

It is common to wish to remove the time of day shown in the node “submitted” line. In Drupal 5, there were no less than three ways to achieve this, and I will present these when I'm done covering date formatting for Drupal 6. Drupal 6 allows you to modify the different formats for dates in the Administration area of your site, these different formats being small, medium and large — yep, just like pizza, oh... my... god... You could do the same in Drupal 5. However, you then had to pick among a finite list of options. In Drupal 6, you can create an nth option, if you like none of the options presented to you.

It is the medium-size date format that's used to format the submitted line in nodes, as shown in lines 2449-2460 of modules/node/node.module:

/**
 * Format the "Submitted by username on date/time" for each node
 *
 * @ingroup themeable
 */
function theme_node_submitted($node) {
  return t('Submitted by !username on @datetime',
    array(
      '!username' => theme('username', $node),
      '@datetime' => format_date($node->created),
    ));
}

In Drupal 6, the signature of the Drupal function format_date() is:

format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL)

When the 2nd parameter is not set, 'medium' is used to format the date, and the function theme_node_submitted() is not setting that 2nd parameter when calling format_date().

To modify the medium-size format for dates, head over to the page admin/settings/date-time of your Drupal site. Under Formatting, in the drop-down list of options for “Medium date format:”, you will find a “Custom format” option. Select it. There was no such option in Drupal 5. Note that in Drupal 6 — just like in Drupal 5 — all the preset options still show time of day for the medium-size date.

Look for the preview of the current date in the text below the field on the right, where it says: This format is currently set to display as...

(Take note also that a link to the PHP manual is provided to you, to show you how to format dates in PHP.)

If all you want to do is remove the time of day from your current default, get rid of the - H:i part. Then do a Save configuration.

Now if you look at any node on your site, you will see that the change has taken effect immediately: no more time of day.

If you do not see the change, it can mean either of these two things: your theme is overriding the function theme_node_submitted(), or, the function format_date() is called within your node template file, in your theme, with some custom format, to display the date.

In Drupal 5

In Drupal 5, there were at least three ways of going about changing your date format.

  1. function _phptemplate_variables($hook, $vars) {
      switch($hook) {
        case 'node' :
          $vars['submitted'] = t('Submitted by !username on @date', 
            array(
              '!username' => $vars['name'],
              '@date' => format_date($vars['node']->created, 
                                      'custom', 'l, j M Y'),
            ));
          break;
      }
      return $vars;
    }
  2. Modify your site's settings.php file (http://drupal.org/node/134990#comment-221129) — to change your system's default medium format. Thank you, Mooffie. So you un-comment the $conf array and add one line to it, like so:

    $conf = array(
    #   'site_name' => 'My Drupal site',
    #   'theme_default' => 'minnelli',
    #   'anonymous' => 'Visitor',
        'date_format_medium' => 'D, m/d/Y',
    );
  3. Modify node.tpl.php (http://drupal.org/node/134990#comment-221973) — instead of printing $submitted, you print $node->created formatted. Like so, for example:

    print format_date($node->created, 'custom', 'l, j M Y');

Browse this article

Last edited by Caroline Schnapp about 5 years ago.

Jul 05 2008
Jul 05

Debunking the myth: Content doesn't create itself

Content Management systems are the easy end all solution for creating content for the web, a magical thing which makes it easy to create awesome websites which get tons of visitors, right?

When I work with clients on CMS systems this is often exactly what they want.

Well, I hate to tell you that no this isn't quite what a content management system does. A content management system is a tool, just like dreamweaver or iweb which makes it easy to post web content. But more often than not this leads people to think that it will make the content they are writing magically perfect. Well this isn't quite the case.

What the content management system does is make the formatting of content easier, often times it comes with (or clients will ask for) a WYSIWYG editor. This is great for quickly formatting text or manipulating the text format without knowing too much html. This is much different than making everything magically good for the web.

What people often forget with a CMS is that writing for the web is a totally different medium than writing a press release, or an informational pamphlet. In fact a good website should be much closer to a script for a commercial than a news press release in most cases. Of course the most important thing is to know your audience. And this is the biggest problem I've seen in most web pages

The "I'm in control effect"

When users are in control of their own content they tend to write the content in the way that makes the most sense to them; That is they mostly write as if they were writing for print.

When users are in control of their own data (often for the first time in my experience) they tend to like to write just like they would for print. And this makes sense, this is the way we were taught to write in school in the essay format. Unfortunately this often is not the best format for your content.

On a website you have to target a user and often you have a short period of time (try seconds) to captivate the user. This means that you need to be as concise as possible. Especially above the fold. This is simply not something that most users think about.

This is also a huge hurdle for users, many of whom don't want to or don't have the time, to learn to write for the web. But still this is very important. If you can write for the web, you will inevitably land yourself more users, and improve your search engine ranking. All without having to pull some fancy tricks.

Because of the "I'm in control effect" users often feel that their existing writing skills will tie over into the web world. Which is partially true, but often times cute wording, and that "eloquent style" makes their material all but impossible to find for all but the most advanced of search engine users.

Writing for the web has a learning curve

Even though Content Management systems, such as drupal, or joomla have the capability to create content without knowing html, there is still a learning curve for web writing.

Until users are truly educated in writing for the web, which often times means beating them over the head with a stick, they will continue thinking of content management systems as magical tools which will create beautiful webpages which attract visitors as long as they know how to write.

My Advice: keep a stick handy at all times, and if that doesn't help them, feel free to link to this website.

Related Posts

We'll as a consultant I build webpages mostly, but because it is drupal, this is not average brochure website stuff. I actively build modules, and improve existing modules as well. Currently I am a project maintainer on drupal.org for the quiz module, what this means is that I do some active development on the module, as well as check other people's code for submission to the module.

Change does not necessarily equal quality

The key to a successful website is to change the content on your website as often as possible. right?

Change can be a dangerous notion on a website, and is something that is made easy and readily available by drupal and other CMS systems. But you must remember that arbitrary change for the sake of change serves no function. And can hurt your website, by disorienting users, and hurt your search engine rankings as well.

Jul 05 2008
Jul 05

This morning, before our morning walk, and other family affairs, I poked around making Drupal my personal wiki. Why, that's probably what your asking? Drupal is a CMS, or CMS framework, and there are alternatives to Drupal for sure when it comes to wikis. PHPWiki, DokuWiki, MediaWiki, even the hosted PBWiki all come to mind.

The answer is a little complicated. I was using Zim (desktop wiki for KDE) to basically do a brain dump and to plan out some things, and discovered that I really wanted to use that data on my other machine. Hmm, well, this is a wiki, says I to me, so I should just be able to find some other wiki software and drop it on my local PHP server and pop this stuff into it.

Nope, it's not that easy. What I ran into was that I had, at best, only a couple of the things that I wanted from a wiki in any given package. I guess those things were:

  • CamelCased markup ability to easilly create new pages and easy linking.
  • Easy Privacy. This wiki isn't for the world, it is for my brain-dumps and planning.
  • Hierarchy so that I could organize my content, split it between work and personal, and SEE that organization.
  • Ability to easilly change the appearance (or to just look good off the bat).
  • Ability to be used from any machine I was on (the only drawback to Zim).
  • Familiarity. I wanted something that didn't seem terribly foreign to me, so I wouldn't struggle using it.

Ok, so that's quite a few things. Maybe there are wikis that do all of these, and do them with ease, however the top picks on my Google searches didn't show me any that fit the bill. PBWiki doesn't do CamelCase, or if it does, I couldn't find it, and that's a show stopper for me. DokuWiki was supposed to be easy to setup, document based, no db, but the security page you are supposed to follow was horrid, and changing the look of the thing was impossible to figure out quickly. PHPWiki, I didn't even try after reading comments about it being a bear to install.

In the end, I decided to build it on my current Drupal 6 installation. I did a search on Google about drupal wiki, and though there were many resources, CWGordon's blog was the most helpful. It took me about an hour and a half, but in the end, I have what I expected, and I have it in a design I like :) Plus, I got to learn what it takes to create a Drupal wiki. It wasn't too hard, I used the following modules:

  • pathauto (so that I can have nice URLs :)
  • flexifilter (allows you to set a filter that is wiki style without coding it)
  • recent_changes
  • freelinking (this lets me do CamelCase links which searches first then creates if it doesn't exist, NICE)
  • tableofcontents (not really using this yet, because I am using Book Navigation in the sidebar)
  • token (dev to fix the [book-raw] and [bookpath-raw] issues I was having)
  • node_privacy_byrole (here is how I make the wiki private)
  • wikitools (for... well, guess)

You put it all together, enable permissions to use it all, always a gotcha, then tweak your settings (like your pathauto and enabling freelinking and flexifilter) then securing it if you want it private, or enabling write access for everyone if you want it public, and viola, a new wiki is born.

Jul 04 2008
jh
Jul 04
compression ratio illustration Cut the size in half

Why

I was tired of waiting for this to happen. It's simple stuff and the effect is pretty big if your content primarily consists of text. Well, it's not like this affects the text - that's already compressed by Drupal if you allow it to do so - what I mean is the compression ratio. If you serve megabytes of images, sound, and video, shrinking those few percent of JS and CSS won't have much of an effect.

However, if your site is more like this one you might be able to cut the size of the site in half. For people with a non-primed cache, that is. That's a really big plus if you get a sudden surge of visits. Nowadays the typical resource usage pattern seems to go from one spike to the next with rather little usage in-between. Take a look a Esoteric Curio's excellent article if you're interested in some more details.

As I previously pointed out even your regular visitors often won't have a primed cache, because the cache is simply too small for today's bloaty web. By the time they visit your site again the cache was already overwritten most of the time. Server-sided caching and compression to the rescue!

Drupal already handles caching of pages and blocks, which already helps a lot. It's also able to utilize GZip compression for the markup, which again is cached on the server side. CSS and JS files can be also aggregated, which reduces the number of HTTP requests, which in turn also improves loading times.

However, the next logical step - compressing those aggregated CSS and JS files - was omitted. Fortunately adding this feature is very easy if you don't ever want to turn it off. Well, why would you? :)

How

Modify includes/common.inc

Step 1 - Locate the following line (search for "file_save"):

file_save_data($data, $csspath .'/'. $filename, FILE_EXISTS_REPLACE);

Step 2 - Put the following line below it:

file_save_data(gzencode($data,9), $csspath .'/'. $filename . '.gz', FILE_EXISTS_REPLACE);

Step 3 - Locate the following line (search for "file_save"):

file_save_data($contents, $jspath .'/'. $filename, FILE_EXISTS_REPLACE);

Step 4 - Put the following line below it:

file_save_data(gzencode($contents,9), $jspath .'/'. $filename .'.gz', FILE_EXISTS_REPLACE);

The second parameter of gzencode is the compression level. 9 offers the best compression, but it's also the slowest. Since compression happens very rarely using a level of 9 is alright. A level of 7 or even 5 would be almost as good, while only taking a fraction of the time. But as I said, it literally runs only once in a blue moon - going for the extreme is really alright.

Keep in mind to add those two lines again after upgrading Drupal.

Modify .htaccess

Add the following before the "<IfModule mod_rewrite.c>" block:

<Files *.js.gz>
  AddEncoding gzip .js
  ForceType application/x-javascript
</Files>

<Files *.css.gz>
  AddEncoding gzip .css
  ForceType text/css
</Files>

Insert the following at the very end of the "<IfModule mod_rewrite.c>" block (before "</IfModule>"):

RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}.gz -f
RewriteRule ^(.*)\.css $1.css.gz [L,QSA]

RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}.gz -f
RewriteRule ^(.*)\.js $1.js.gz [L,QSA]

mod_negotiate could have been used instead of mod_rewrite, but the former looked a bit less feasible from my point of view. Well, feel free to experiment with that stuff.

Results

On my rather plain website the results are pretty impressive as illustrated by figure 1:

Compression ratio diagram Figure 1: Compression ratio

The document, external document, images, and the external JavaScript were already heavily compressed. Also compressing the aggregated JavaScript and CSS files saves about 28% on the front page and a whopping 44% on that specific blog entry.

This blog post uses about 30kb for additional images, which means that compression of those aggregated files saves about 34%. That's still pretty awesome, isn't it?

Update 07/10/2008: I forgot a backslash in both rewrite rules. It's now fixed in the .htaccess excerpt above. This mistake caused paths like "sh_css.min.js" to be rewritten as "sh.css.gz" instead of "sh_css.min.js.gz". Whoops. My thanks go to some random guy/gal from the Czech Republic who made it show up in the logs. :)

Jul 04 2008
Jul 04

For the Brown University Featured Events website, I needed to develop a means to use their events for other purposes around the Brown.edu websites. I ultimately created a basic way of exporting iCal, RSS and HTML of a selected span of events.

The goal was to create a self service center for Brown University webmasters to grab a feed of events and use them on their own websites. I had to create a UI where a webmaster could create a feed based on event type, time constraints and tags. This would allow the drama department to display on their website events of all plays occurring in the next three months. The feed would automatically update as new events are added to the Featured Events website.

The User Interface

My first assumption was to generate the iCal and RSS feeds using views and expose date fields to allow a user to pick custom time constraints. Unfortunately, that method created an extremely cumbersome interface.

In order to display events for a desired time span, I needed four exposed date fields: ‘start date before,’ ‘start date after,’ ‘end date before’ and ‘end date after.’ All of those in addition to event type and tag selection fields! To make it even more kludgey and cumbersome, the view was cloned to have one sorting by start date, and the other by end date in order to support exhibitions.

Despite its cumbersomeness, we went with it anyway. Only initially, though.

The Bugs

We really banged our heads against that UI. We hit two critical bugs for this attempt: the inability to expose the same field twice and the date module rejecting valid date formats. In order to support exhibitions, we needed to use the start date twice because the end date would be after and the start date before the beginning of the given time span.

Although we spent a lot of time tracking down and solving the above bugs, we ended up with some rather simple patches for them: [inline:views_query.inc.patch] and [inline:date_views.inc.patch] (However, they won’t work for all websites).

Despite the patches, the UI was still clunky, and it was difficult for a webmaster to create a dynamic feed that will shift dates automatically (say, always showing the next month of events).

The Birth of wholly using a Custom Script

I wanted a UI that was simple and straight forward. I allowed the editor to only enter a start day (which defaults to today) and a time span from the start date. The custom script then automatically generated the events and exhibitions in proper order. It was a very simple interface, asking two questions: start date and time span. Great!

In the end, the new UI would only ask the start date, time span, event type and display format for RSS feeds. Much simpler.

Gather Events in the Time Span

Since I am generating the appropriate events for a given time span, I don’t encounter the issue dealing with exhibitions because I can group and sort the returned events in whatever means is the most appropriate.

For my use the appropriate order was:

  • events in ascending ordered by start date
  • exhibitions that start in the time span ordered by start date
  • exhibitions ending after the beginning of the time span ordered by end date, positioned below exhibitions starting in the time span

This is the script I ended up with: custom_span_events.php.

In Retrospect

My script involves writing a script to query the database directly. However, it could be done by integrating views, and inputting arguments and exposed filters directly into the views_build_view() function.

Utilize URI Fragments

Different events can then be served depending upon the URI. A webmaster can use the self serve interface to generate the URI’s, or just manipulate the URI themselves:

# iCal - All Events
http://<path>/ical?start=today&spanNum=2&spanType=Weeks&type=all

# iCal - For those people who don't need a constant reminder of exhibitions
http://<path>/ical?start=today&spanNum=2&spanType=Weeks&type=events

# RSS
http://<path>/rss?start=today&spanNum=2&spanType=Weeks&type=all&format=default

The page that generates these events would look like this:

<?php
 
// Default to today and two weeks after today
 
$startDate = $_GET['start'] ? $_GET['start'] : 'today';
 
$spanNum = $_GET['spanNum'] ? $_GET['spanNum'] : '2';
 
$spanType = $_GET['spanType'] ? $_GET['spanType'] : 'Weeks';
 
$eventType = $_GET['type'] ? $_GET['type'] : 'all'; $dates = get_span_dates($startDate, $spanNum, $spanType);
 
$events = get_events($dates['start'], $dates['end'], $eventType); $output = '';
 
$output .= $events['events'];
 
$output .= $events['exhibitsStarting'];
 
$output .= $events['exhibitsEnding'];

  print

$output;
?>

Ultimately this becomes a cleaner UI because it involves much fewer input options from the user. Using the dynamic start time, ‘today’, will create a consistently updated feed of events.

Jul 03 2008
Jul 03

As I’ve mentioned before, version control with Drupal is tricky because a lot of configuration changes are stored in the database. Version control is vital because you should be maintaining multiple workspaces so you do not work on production. The last thing you want to do, then, is to make this worse by putting PHP in your nodes and blocks.

2bits wrote up a great post explaining that you should free your content of PHP. Yes! Read that! Follow it and add it to your Drupal bible.

2bits goes on to explain how to pull PHP enabled nodes and blocks into modules, and points out that we’ve been able to export a view into a module all along.

In addition, I should note, that you should also avoid using the contemplate module because it stores all of your theming for your content types in the database. I know I know, some of you absolutely adore it because it makes theming easier. But you should instead learn to put the code you’re adding to contemplate inside template files, which can then be under version control.

The main benefits of contemplate is the exposure of available variables to each content type, and with a click that variable can be added to the template. It makes it easy, then, to display your content just the way you want. It also exposes some PHP scripting to ensure that all content gets displayed (like all terms, or all entries in a multiple values enabled field), which is helpful for those just starting out and are still novices with PHP. Not everyone understands objects, arrays and loops. (All the programmers are likely choking at this comment, but Drupal is accessible enough where scripting is not a requirement in order to build a site.)

For a long time, I would enable contemplate solely so I can browse through the available variables it exposes. However, I now primarily use print_r($node) instead as a tool to see what is available. The Devel module provides all sorts of utilities while developing, including the ability to see all available variables within a given node in a fashion that is nicer than print_r().

Since contemplate’s greatest appeal is to beginner Drupal developers, it would be nice if extra features could be added to help teach how to make a theme, or be a tool to assist in making a theme easier. Suppose if contemplate had an optional feature to preview and then export the desired template into template files in much the same fashion as the template wizard for Views behaves. Such a feature could help bridge the gap and demonstrate how themes work.

Jul 02 2008
Jul 02

I spent some time today trying to figure out how to create a CCK field as part of an hook_update_N function. Unlike previous versions of CCK, in 6 it's very easy to manipulate the fields from code.

The first step is to create the field using CCK's UI. Once you've got the field setup the way you'd like it use PHP's var_export() to dump the contents of the node's field as an array:

var_export(content_fields('field_translator_note', 'feature'));

That'll give you some massive array definition that you can copy and paste into your code.

<?php
$field
= array (
 
'field_name' => 'field_translator_note',
 
'type_name' => 'feature',
 
'display_settings' =>
  array (
   
4 =>
    array (
     
'format' => 'hidden',
    ),
   
2 =>
    array (
     
'format' => 'hidden',
    ),
   
3 =>
    array (
     
'format' => 'hidden',
    ),
   
'label' =>
    array (
     
'format' => 'hidden',
    ),
   
'teaser' =>
    array (
     
'format' => 'hidden',
    ),
   
'full' =>
    array (
     
'format' => 'hidden',
    ),
  ),
 
'widget_active' => '1',
 
'type' => 'text',
 
'required' => '0',
 
'multiple' => '0',
 
'db_storage' => '0',
 
'module' => 'text',
 
'active' => '1',
 
'columns' =>
  array (
   
'value' =>
    array (
     
'type' => 'text',
     
'size' => 'big',
     
'not null' => false,
     
'sortable' => true,
    ),
  ),
 
'text_processing' => '0',
 
'max_length' => '',
 
'allowed_values' => '',
 
'allowed_values_php' => '',
 
'widget' =>
  array (
   
'rows' => '',
   
'default_value' =>
    array (
     
0 =>
      array (
       
'value' => '',
      ),
    ),
   
'default_value_php' => NULL,
   
'label' => 'Translator\'s note',
   
'weight' => NULL,
   
'description' => '',
   
'type' => 'text_textarea',
   
'module' => 'text',
  ),
);
// Need to load the CCK include file where content_field_instance_create() is defined.
module_load_include('inc', 'content', 'includes/content.crud');// I wanted to add the field to several node types so loop over them...
foreach (array('athlete', 'feature', 'product', 'tech') as $type) {
 
// ...and assign the node type.
 
$field['type_name'] = $type;
 
content_field_instance_create($field);
}
?>

High-fives to all the CCK developers for making this so easy.

Jul 02 2008
Jul 02

The event distribution website for Brown University is has been live for awhile now. Although it looks rather simple, the back end is quite robust (plus since it’s summer, there are no events. I posted this a little too late, oops!).

We here at Public Display specialize in all the idiosyncrasies that event content gives us. No one realizes how complicated events actually are. Well, guess what? They are really complicated beasts, let me tell you! We have devoted an enormous amount of engineering time and energy on solving all the various problems of events while developing FuseCal.

Events vs Exhibitions

The featured events of Brown University are categorized as events and exhibitions. Seems simple enough, right? No! It is not simple. Exhibitions span months, some span years. You cannot so nicely put events and exhibitions together. You can’t even sort them together. Hell, you can’t even sort all exhibitions together. However, an ‘exhibition’ is still part of what is going on and is thusly an ‘event’ that needs to be displayed to the user along with the other events.

Events are relatively short. They can last several days, or span a week or two. Sorting in chronological order by start date and ending up with an event that is spanning over a week appear at the top of a list of events for that entire week is generally OK and not too much of an annoyance.

On the other hand, sorting exhibits in chronological order by start date, and having an exhibition that started three years ago at the top of the list is really aggravating. That exhibition should appear at the bottom of the list.

But, what about exhibitions that are starting soon? You don’t want those to be sorted by ending date. You don’t want the exhibit that is starting tomorrow to appear much lower than the exhibit ending in two weeks.

Displaying Events

I decided to keep events and exhibitions always segregated when displayed on the website. The original Featured Events website kept them segregated as well, so this was no problem.

The sorting within exhibitions are also segregated. Exhibits starting within the time span should be displayed above the exhibits that are ending after the start of the given time span. There is no way to mesh these two with one query.

The display of all events within a given time span then resulted in three queries:

  • Events occurring at any time during the given time span (starts during, ends during, or spans the entire length of time), sorted by start date
  • All exhibitions starting within the given time span, sorted by start date
  • All exhibitions that started before and ends after the start of the time span, sorted by end date

Exporting Events

Drupal’s support of iCal is limited, so in order to take advantage of all the input fields available to me with CCK, I created a means of customizing iCal export.

RSS for events on the surface always seems so easy! “Oh sure, we’ll just provide an RSS feed! No problem,” they always say. However, there’s more to think about.

First of all, at what point should the user receive information about the event? Generally the default is the publish date. However, does a user need to know now about an event that is occurring next year? So instead you could control the publish-to-rss date to be some variable before the event-start-date. What, then, would the user do to remember the event? Should the RSS feed then also try to remind the reader that the event is tonight or tomorrow? If the event is an exhibition, should there also be a reminder that the event is ending?

Excuse me for being rudely blunt here, but the answer to those questions is iCal - thats why we have calendars! They’re built to handle these issues! We don’t need event information in our news readers. However, if an event publisher would like to provide a newsletter of events, then RSS would be appropriate (with a link to the events in iCal format so it can be easily imported into the user’s calendar).

For my uses, RSS is handy for distributing content to other websites. I use it on Today at Brown to display today’s events and currently open exhibitions. The RSS feed only contains the needed events, and it is displayed in it’s entirety on the site using a RSS to HTML converter.

Jul 02 2008
Jul 02

Administrators are presented with a special marker when content is new or has been updated in the Administration section of their site at webSite.com/admin/content/node. Additionally, in node lists, the module tracker informs any logged-in user if he or she hasn't read a particular (recently created) node (using the marker new), or if a node he/she's read already was modified (using the marker updated). You, the themer, may want everyone, including “anonymous users”, to be informed of updates to nodes right inside their content. This information may specify who last edited the node and when. Note that the last editor of a content may not be the creator of that content, and I'll take this into account in my “solution”. In the following theming tweak, you'll add Last edited by name some time ago information to your nodes' content.

Here is what you'll try to achieve:

Solution

  1. Edit the file template.php to pass an additional variable to the node template, a variable we'll call $last_edit...

  2. print that variable in our node.tpl.php file, and...

  3. style this added markup as needed in style.css.

Before you apply this solution to your theme, we will review how Drupal deals with node editing and node revisions. Read carefully.

When a node is simply edited, with no new revision created

If the editor of the node doesn't edit the Authored by field under Authoring information, the recorded author of the node remains unchanged. What that means is that the variables $name and $uid in node.tpl.php will output the same HTML. However, if the editor is not the person who authored the node, the revision_uid (user ID) of the current revision does change to show who edited the node. In node.tpl.php, the variable $revision_uid always tells us who was the last person to edit the current version of a node.

If the editor changes the Authored by field under Authoring information, the variables $name and $uid in node.tpl.php will change to reflect whatever user the editor has picked as new author. However, still in node.tpl.php, the variable $revision_uid will provide us with the user ID of the editor.

No one with permission to edit a node can effectively edit it without a record of his action being saved to the Drupal database, a record of his action under his name. The info about any node version in the table {node_revision} will tell us when and by whom that version was last edited.

When a new revision of a node is created

What has been said previously still applies. If the Authored by field isn't edited, the author of the new revision and the author of the old revision will be the same person, regardless of who created the new revision. The variable $revision_uid will provide us with the user ID of the creator of the new revision.

If the editor changes the Authored by field under Authoring information when creating a new revision, the info about the author of the node will reflect that change.

Variables available to node.tpl.php — that you will use

What follows is a partial list of the variables available to node.tpl.php.

Available variables:
* - $page: Flag for the full page state.
* - $name: Themed username of node author output from theme_username().
* - $uid: User ID of the node author.
* - $vid: Current version ID of the node, e.g. the version to output.
* - $revision_uid: User ID of the last editor of the node version with $vid.
* - $created: Time the node was published formatted in Unix timestamp.
* - $changed: Time the node was last edited formatted in Unix timestamp.

For a given revision row in the {node_revisions} table of your Drupal database, the variable $revision_uid corresponds to the uid column. Whereas the $uid variable is read from the uid column in the table {node}.

You can find an exhaustive list of the variables available to node.tpl.php if you inspect a node's content using the Themer Info widget that comes with the Devel module. Look under Template Variables:

Passing a new variable to print in the node template

You will use a prepocess function to pass a new, e.g. additional, variable to your node template, ie: $last_edit. Hence, you will need to add a phptemplate_preprocess_node() function if it has not already been defined to your template.php file. Open your theme template.php file in a text editor, and add the following code (please do read the comments):

/**
* Override or insert PHPTemplate variables into the node template.
*/
function phptemplate_preprocess_node(&$vars) {
  /*
   * If the node is shown on its dedicated page and
   * the node was edited after its creation. 
   */
  if ($vars['page'] && $vars['created'] != $vars['changed']) {
    $time_ago = format_interval(time() - $vars['changed'], 1);
    /* 
     * If the last person who edited the node 
     * is NOT the author who created the node.
     */
    if ($vars['uid'] != $vars['revision_uid']) {
      // Let's get the THEMED name of the last editor.
      $user = user_load(array('uid' => $vars['revision_uid']));
      $edited_by = theme('username', $user);
    }
    /* 
     * If the last person who edited the node 
     * is also the author who created the node,
     * we already have the THEMED name of that person.
     */
    else {
      $edited_by = $vars['name'];
    }
    /* Adding the variable. */
    $vars['last_edit'] = t('Last edited by !name about @time ago.', 
      array('!name' => $edited_by, '@time' => $time_ago));
  }
 
}

After that, you edit your node.tpl.php file to print your new variable. Take note that the variable will be undefined if we're on a node list page, or if the node wasn't edited since its creation. So we won't add a div unless our variable is defined.

<?php if ($last_edit): ?>  
  <div class="last-edit clear-block">
    <small><?php print $last_edit; ?></small>
  </div>
<?php endif; ?>

A bit of styling

You can pick a special color for that added text, and increase the white space above and below it, with this CSS rule, added to the style.css stylesheet of your theme:

/**
 * Last edit rules
 */
 
.last-edit {
  color: #ca481c;
  margin: 1em 0pt; 
}

Because it makes use of a preprocess function, this solution will work in Drupal 6 only. There's an equivalent method for Drupal 5 themes, and if someone asks for it I will provide it.

A few questions and their answer

Question 1. Why use the function theme('username', ...)? Why aren't we just outputing the name of the editor as is?

You could. When using the function theme('username', $user), the name of the user becomes a link to his profile page under certain conditions. It's very likely you may not like that. If you don't, just output the name like so (this is a snippet only):

/* 
 * If the last person who edited the node 
 * is NOT the author who created the node.
 */
if ($vars['uid'] != $vars['revision_uid']) {
  $user = user_load(array('uid' => $vars['revision_uid']));
}
/* 
 * If the last person who edited the node 
 * is also the author who created the node,
 * we already have the THEMED name of that person.
 */
else {
  $user = user_load(array('uid' => $vars['uid'])); 
}
 
/* Reading the name property of the user object */
$edited_by = $user->name;
 
/* Adding the variable. */
$vars['last_edit'] = t('Last edited by @name about @time ago.', 
  array('@name' => $edited_by, '@time' => $time_ago));

Note that I am using @name instead of !name in the translation function now. That's because I want the user name to be escaped properly and sanitized before being output to screen. Always do this for user names. The ! prefix used for a placeholder means that the variable it stands for should be used as is.

Question 2. Are variables publicized as being available in a template are the same ones we can read in the parameter $vars passed to a preprocess function, right?

Riiight.

Question 3. Some of these variables we're using don't seem to be listed at the beginning of the file modules/node/node.tpl.php...

True. They are missing from the comment area, because they are less commonly used. That's why the Devel module is tremendously useful to themers in Drupal 6: as a themer, you can use the Themer info widget to get an exhaustive list of the available variables for any template.

Question 4. You say that the variable $last_edit is undefined if we're on a page with a list of nodes, or if the node wasn't edited after its creation. Why is that?

Because of this condition in the preprocess function:

if ($vars['page'] && $vars['created'] != $vars['changed']) {

We are defining the new variable only when we pass through this condition.

$vars['page'] becomes $page in the node template and it's a TRUE/FALSE flag. It is TRUE when the node is shown on its dedicated page at webSite.com/node/x, and FALSE otherwise. You could change this to say if we're not showing the node in teaser view, and in most cases this would have the same effect, but take note that it is possible to show a list of nodes in full view, with the module Views for example.

Changing $vars['page'] to !$vars['teaser'] — and changing the != sign to a 'greater than' sign — will give your script the same behavior in most situations:

if (!$vars['teaser'] && $vars['created'] < $vars['changed']) {

Browse this article

Last edited by Caroline Schnapp about 4 years ago.

Jul 02 2008
Jul 02

This is part two of my series, The DX Files: Improving Drupal Developer Experience.

Many Drupal APIs accept a boolean argument (TRUE or FALSE) to determine some behavior. I believe that practice should be banned in all but exceptional cases, instead using a defined constant with a descriptive name.

Here is a perfect example from Drupal core:

<?php
    $output
= node_view($node, FALSE, TRUE);
?>

Now, quick! Who can tell me what passing FALSE as the second argument and TRUE as the third argument means? Unless you are a Drupal guru, you almost certainly have no idea. Compare the previous line of code with this one:

<?php
   $output
= node_view($node, NODE_FULL_VIEW, NODE_IS_PAGE_CONTENT);
?>

Now it is much more clear what is going on. We're displaying the full view of a node and it is the page's primary content. Perhaps you do not know exactly what those constants mean ("A 'full view' as compared to what?", you might ask) but you are certainly better off than if you just see "TRUE" or "FALSE". If you later encountered

<?php
   $output
= node_view($node, NODE_TEASER_VIEW, NODE_IS_NOT_PAGE_CONTENT);
?>

everything would become pretty clear(*).

The end result of this change would be an API that is easier to learn and code that is easier to read, understand, and maintain.

* Disclaimer: I'll grant that it is far from obvious exactly what NODE_IS_PAGE_CONTENT actually causes to happen and in fact in a recent query on #drupal, several senior Drupal developers (myself included) couldn't remember, but it is still way better than "TRUE". This is an unrelated issue with node_view() that ought to be improved.

Pages

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web