Developing for Drupal

The Drupal engine is open source. It is possible for each and every user to become a contributor. The fact remains that most Drupal users, even those skilled in programming arts, have never contributed to Drupal even though most of us had days where we thought to ourselves: "I wish Drupal could do this or that ...". Through this section, we hope to make Drupal more accessible to them.

The guide pages found here are collaborative, but not linked to particular Drupal versions. Because of this, documentation can become out of date. To combat this, we are moving most developer documentation into the Doxygen documentation that is versioned by CVS and generated from the source code. Look there for up-to-date and version-specific information.

What about upgrading and backwards compatability? For more details read this overview of the Drupal's philosophy on backwards compatibility

Contributing to Development

Drupal is a collaborative, community-driven project. This means that the software and its supporting features (documentation, the drupal.org website) are collaboratively produced by users and developers all over the world.

There are several ways to contribute to Drupal:

This section focuses on the first of these three.

Types of Contributions

There are two basic types of contributions you can make to Drupal's code base: (a) "contributed" modules or themes and (b) contributions to the drupal "core".

  • The easiest way to help is simply to find a bug or feature request that you want to fix, modify a local copy of the code to fix that problem, create a patch file of your changes, and then attach the patch to an issue for that bug or feature request
  • "Contributed modules" are the community-produced modules and themes available on the Drupal site.  To make a contribution, you need
  • As long as contributions meet some minimal criteria - they do what they claim to and have some demonstrable benefit without unduly replicating already-available functionality, and are coded with an eye towards secure code - they should be ok.
  • You should also read about maintaining a project on Drupal.org and consider joining forces with others to avoid duplication of effort and share the load of your module.
  • If you have major enhancements you wish to contribute, doing so via a contributed module is in many ways the easiest way to begin.  Contributed code has a relatively low set of requirements to meet. It also helps to trial the feature with the community, gain feedback and help the code mature.
  • Your code contribution is welcome. Consider also being a responsible maintainer and helping build a community around your module as it grows in use and popularity. This will help reduce your load as well.
  • In contrast, changes to the Drupal core are made through a thorough consultative process to ensure the overall integrity of the software.

    Changes to the Drupal core are generally of three types:

    • Bug fixes These changes respond to identified problems in
      the existing code.
    • New features These changes are enhancements on what is already available.
    • Code maintenance.  These changes are to improve the quality of the code or bring it up to date with changes elsewhere in Drupal.  This can include bringing code in line with coding standards, improving efficiency (e.g., eliminating unneeded database queries), introducing or improving in-line comments, and doing upgrades for compliance with a new release version.

    While you can create your own issues, you can also begin by simply taking on existing tasks on the task list. See the page "Tips for contributing to the core" for advice on how to get started as a core contributor.

Task list

The Drupal bug database contains many issues classified as "bite-sized" tasks -- tasks that are well-defined and self-contained, and thus suitable for a volunteer looking to get involved with the project. You don't need broad or detailed knowledge of Drupal's design to take on one of these, just a pretty good idea of how things generally work, and familiarity with the coding guidelines. Each task is something a volunteer could pick off in a spare evening or two.

If you start one of these, please update the task and assign it to yourself, so no one else duplicates your effort. Once the task is complete, you can attach a patch that includes your changes into the issue, and set the status to "patch (code needs review)".

Bug reports

Your bug reports play an essential role in making Drupal reliable. If you find a bug, please follow the submission guidelines in this handbook. If you can, it is also helpful to provide a patch that fixes your bug. If you can't provide a patch, hopefully someone else will be able to fix the bug.

Bug reports can be posted in connection with any project hosted on drupal.org. You can submit a new bug via the submit issue form. Provide a sensible title for the bug, and choose the project you think you have found the bug in. After previewing the submission, you will need to choose a related component and you will be able to provide more details about the bug, including the description of the problem itself. Please include any error messages you received and a detailed description of what you were doing at the time.

Note that you DO have to be a logged in member of drupal.org to submit bugs.

Bugs are fixed in Drupal almost every day, often many bugs are fixed in a day. Therfore it is important that you are using the latest version of Drupal and that your bug still hapens in that version.

It is frequently helpful to include the PHP, database and webserver version information.

More detailed advice on submitting high quality bugs and getting them fixed quickly is available in this handbook. Read the next pages for more details.

How to report bugs effectively

Summary

  • The first aim of a bug report is to let the programmer see the failure with their own eyes. If you can't be with them to make it fail in front of them, give them detailed instructions so that they can make it fail for themselves.
  • In case the first aim doesn't succeed, and the programmer can't see it failing themselves, the second aim of a bug report is to describe what went wrong. Describe everything in detail. State what you saw, and also state what you expected to see. Write down the error messages, especially if they have numbers in.
  • When your computer does something unexpected, freeze. Do nothing until you're calm, and don't do anything that you think might be dangerous.
  • By all means try to diagnose the fault yourself if you think you can, but if you do, you should still report the symptoms as well.
  • Be ready to provide extra information if the programmer needs it. If they didn't need it, they wouldn't be asking for it. They aren't being deliberately awkward. Have version numbers at your fingertips, because they will probably be needed.
  • Write clearly. Say what you mean, and make sure it can't be misinterpreted.
  • Above all, be precise. Programmers like precision.

Introduction
Anybody who has written software for public use will probably have received at least one bad bug report. Reports that say nothing ("It doesn't work!"); reports that make no sense; reports that don't give enough information; reports that give wrong information. Reports of problems that turn out to be user error; reports of problems that turn out to be the fault of somebody else's program; reports of problems that turn out to be network failures.

There's a reason why technical support is seen as a horrible job to be in, and that reason is bad bug reports. However, not all bug reports are unpleasant: I maintain free software, when I'm not earning my living, and sometimes I receive wonderfully clear, helpful, informative bug reports.

In this essay I'll try to state clearly what makes a good bug report. Ideally I would like everybody in the world to read this essay before reporting any bugs to anybody. Certainly I would like everybody who reports bugs to me to have read it.

In a nutshell, the aim of a bug report is to enable the programmer to see the program failing in front of them. You can either show them in person, or give them careful and detailed instructions on how to make it fail. If they can make it fail, they will try to gather extra information until they know the cause. If they can't make it fail, they will have to ask you to gather that information for them.

In bug reports, try to make very clear what are actual facts ("I was at the computer and this happened") and what are speculations ("I think the problem might be this"). Leave out speculations if you want to, but don't leave out facts.

When you report a bug, you are doing so because you want the bug fixed. There is no point in swearing at the programmer or being deliberately unhelpful: it may be their fault and your problem, and you might be right to be angry with them, but the bug will get fixed faster if you help them by supplying all the information they need. Remember also that if the program is free, then the author is providing it out of kindness, so if too many people are rude to them then they may stop feeling kind.

"It doesn't work."
Give the programmer some credit for basic intelligence: if the program really didn't work at all, they would probably have noticed. Since they haven't noticed, it must be working for them. Therefore, either you are doing something differently from them, or your environment is different from theirs. They need information; providing this information is the purpose of a bug report. More information is almost always better than less.

Many programs, particularly free ones, publish their list of known bugs. If you can find a list of known bugs, it's worth reading it to see if the bug you've just found is already known or not. If it's already known, it probably isn't worth reporting again, but if you think you have more information than the report in the bug list, you might want to contact the programmer anyway. They might be able to fix the bug more easily if you can give them information they didn't already have.

This essay is full of guidelines. None of them is an absolute rule. Particular programmers have particular ways they like bugs to be reported. If the program comes with its own set of bug-reporting guidelines, read them. If the guidelines that come with the program contradict the guidelines in this essay, follow the ones that come with the program!

If you are not reporting a bug but just asking for help using the program, you should state where you have already looked for the answer to your question. ("I looked in chapter 4 and section 5.2 but couldn't find anything that told me if this is possible.") This will let the programmer know where people will expect to find the answer, so they can make the documentation easier to use.

"Show me"
One of the very best ways you can report a bug is by showing it to the programmer. Stand them in front of your computer, fire up their software, and demonstrate the thing that goes wrong. Let them watch you start the machine, watch you run the software, watch how you interact with the software, and watch what the software does in response to your inputs.

They know that software like the back of their hand. They know which parts they trust, and they know which parts are likely to have faults. They know intuitively what to watch for. By the time the software does something obviously wrong, they may well have already noticed something subtly wrong earlier which might give them a clue. They can observe everything the computer does during the test run, and they can pick out the important bits for themselves.

This may not be enough. They may decide they need more information, and ask you to show them the same thing again. They may ask you to talk them through the procedure, so that they can reproduce the bug for themselves as many times as they want. They might try varying the procedure a few times, to see whether the problem occurs in only one case or in a family of related cases. If you're unlucky, they may need to sit down for a couple of hours with a set of development tools and really start investigating. But the most important thing is to have the programmer looking at the computer when it goes wrong. Once they can see the problem happening, they can usually take it from there and start trying to fix it.

"Show me how to show myself"
This is the era of the Internet. This is the era of worldwide communication. This is the era in which I can send my software to somebody in Russia at the touch of a button, and he can send me comments about it just as easily. But if he has a problem with my program, he can't have me standing in front of it while it fails. "Show me" is good when you can, but often you can't.

If you have to report a bug to a programmer who can't be present in person, the aim of the exercise is to enable them to reproduce the problem. You want the programmer to run their own copy of the program, do the same things to it, and make it fail in the same way. When they can see the problem happening in front of their eyes, then they can deal with it.

So tell them exactly what you did. If it's a graphical program, tell them which buttons you pressed and what order you pressed them in. If it's a program you run by typing a command, show them precisely what command you typed. Wherever possible, you should provide a verbatim transcript of the session, showing what commands you typed and what the computer output in response.

Give the programmer all the input you can think of. If the program reads from a file, you will probably need to send a copy of the file. If the program talks to another computer over a network, you probably can't send a copy of that computer, but you can at least say what kind of computer it is, and (if you can) what software is running on it.

"Works for me, so what goes wrong?"
If you give the programmer a long list of inputs and actions, and they fire up their own copy of the program and nothing goes wrong, then you haven't given them enough information. Possibly the fault doesn't show up on every computer; your system and theirs may differ in some way. Possibly you have misunderstood what the program is supposed to do, and you are both looking at exactly the same display but you think it's wrong and they know it's right.

So also describe what happened. Tell them exactly what you saw. Tell them why you think what you saw is wrong; better still, tell them exactly what you expected to see. If you say "and then it went wrong", you have left out some very important information.

If you saw error messages then tell the programmer, carefully and precisely, what they were. They are important! At this stage, the programmer is not trying to fix the problem: they're just trying to find it. They need to know what has gone wrong, and those error messages are the computer's best effort to tell you that. Write the errors down if you have no other easy way to remember them, but it's not worth reporting that the program generated an error unless you can also report what the error message was.

In particular, if the error message has numbers in it, do let the programmer have those numbers. Just because you can't see any meaning in them doesn't mean there isn't any. Numbers contain all kinds of information that can be read by programmers, and they are likely to contain vital clues. Numbers in error messages are there because the computer is too confused to report the error in words, but is doing the best it can to get the important information to you somehow.

At this stage, the programmer is effectively doing detective work. They don't know what's happened, and they can't get close enough to watch it happening for themselves, so they are searching for clues that might give it away. Error messages, incomprehensible strings of numbers, and even unexplained delays are all just as important as fingerprints at the scene of a crime. Keep them!

If you are using Unix, the program may have produced a core dump. Core dumps are a particularly good source of clues, so don't throw them away. On the other hand, most programmers don't like to receive huge core files by e-mail without warning, so ask before mailing one to anybody. Also, be aware that the core file contains a record of the complete state of the program: any "secrets" involved (maybe the program was handling a personal message, or dealing with confidential data) may be contained in the core file.

"So then, I tried..."
There are a lot of things you might do when an error or bug comes up. Many of them make the problem worse. A friend of mine at school deleted all her Word documents by mistake, and before calling in any expert help, she tried reinstalling Word, and then she tried running Defrag. Neither of these helped recover her files, and between them they scrambled her disk to the extent that no Undelete program in the world would have been able to recover anything. If she'd only left it alone, she might have had a chance.

Users like this are like a mongoose backed into a corner: with its back to the wall and seeing certain death staring it in the face, it attacks frantically, because doing something has to be better than doing nothing. This is not well adapted to the type of problems computers produce.

Instead of being a mongoose, be an antelope. When an antelope is confronted with something unexpected or frightening, it freezes. It stays absolutely still and tries not to attract any attention, while it stops and thinks and works out the best thing to do. (If antelopes had a technical support line, it would be telephoning it at this point.) Then, once it has decided what the safest thing to do is, it does it.

When something goes wrong, immediately stop doing anything. Don't touch any buttons at all. Look at the screen and notice everything out of the ordinary, and remember it or write it down. Then perhaps start cautiously pressing "OK" or "Cancel", whichever seems safest. Try to develop a reflex reaction - if a computer does anything unexpected, freeze.

If you manage to get out of the problem, whether by closing down the affected program or by rebooting the computer, a good thing to do is to try to make it happen again. Programmers like problems that they can reproduce more than once. Happy programmers fix bugs faster and more efficiently.

"I think the tachyon modulation must be wrongly polarised."
It isn't only non-programmers who produce bad bug reports. Some of the worst bug reports I've ever seen come from programmers, and even from good programmers.

I worked with another programmer once, who kept finding bugs in his own code and trying to fix them. Every so often he'd hit a bug he couldn't solve, and he'd call me over to help. "What's gone wrong?" I'd ask. He would reply by telling me his current opinion of what needed to be fixed.

This worked fine when his current opinion was right. It meant he'd already done half the work and we were able to finish the job together. It was efficient and useful.

But quite often he was wrong. We would work for some time trying to figure out why some particular part of the program was producing incorrect data, and eventually we would discover that it wasn't, that we'd been investigating a perfectly good piece of code for half an hour, and that the actual problem was somewhere else.

I'm sure he wouldn't do that to a doctor. "Doctor, I need a prescription for Hydroyoyodyne." People know not to say that to a doctor: you describe the symptoms, the actual discomforts and aches and pains and rashes and fevers, and you let the doctor do the diagnosis of what the problem is and what to do about it. Otherwise the doctor dismisses you as a hypochondriac or crackpot, and quite rightly so.

It's the same with programmers. Providing your own diagnosis might be helpful sometimes, but always state the symptoms. The diagnosis is an optional extra, and not an alternative to giving the symptoms. Equally, sending a modification to the code to fix the problem is a useful addition to a bug report but not an adequate substitute for one.

If a programmer asks you for extra information, don't make it up! Somebody reported a bug to me once, and I asked him to try a command that I knew wouldn't work. The reason I asked him to try it was that I wanted to know which of two different error messages it would give. Knowing which error message came back would give a vital clue. But he didn't actually try it - he just mailed me back and said "No, that won't work". It took me some time to persuade him to try it for real.

Using your intelligence to help the programmer is fine. Even if your deductions are wrong, the programmer should be grateful that you at least tried to make their life easier. But report the symptoms as well, or you may well make their life much more difficult instead.

"That's funny, it did it a moment ago."
Say "intermittent fault" to any programmer and watch their face fall. The easy problems are the ones where performing a simple sequence of actions will cause the failure to occur. The programmer can then repeat those actions under closely observed test conditions and watch what happens in great detail. Too many problems simply don't work that way: there will be programs that fail once a week, or fail once in a blue moon, or never fail when you try them in front of the programmer but always fail when you have a deadline coming up.

Most intermittent faults are not truly intermittent. Most of them have some logic somewhere. Some might occur when the machine is running out of memory, some might occur when another program tries to modify a critical file at the wrong moment, and some might occur only in the first half of every hour! (I've actually seen one of these.)

Also, if you can reproduce the bug but the programmer can't, it could very well be that their computer and your computer are different in some way and this difference is causing the problem. I had a program once whose window curled up into a little ball in the top left corner of the screen, and sat there and sulked. But it only did it on 800x600 screens; it was fine on my 1024x768 monitor.

The programmer will want to know anything you can find out about the problem. Try it on another machine, perhaps. Try it twice or three times and see how often it fails. If it goes wrong when you're doing serious work but not when you're trying to demonstrate it, it might be long running times or large files that make it fall over. Try to remember as much detail as you can about what you were doing to it when it did fall over, and if you see any patterns, mention them. Anything you can provide has to be some help. Even if it's only probabilistic (such as "it tends to crash more often when Emacs is running"), it might not provide direct clues to the cause of the problem, but it might help the programmer reproduce it.

Most importantly, the programmer will want to be sure of whether they're dealing with a true intermittent fault or a machine-specific fault. They will want to know lots of details about your computer, so they can work out how it differs from theirs. A lot of these details will depend on the particular program, but one thing you should definitely be ready to provide is version numbers. The version number of the program itself, and the version number of the operating system, and probably the version numbers of any other programs that are involved in the problem.

"So I loaded the disk on to my Windows . . ."
Writing clearly is essential in a bug report. If the programmer can't tell what you meant, you might as well not have said anything.

I get bug reports from all around the world. Many of them are from non-native English speakers, and a lot of those apologise for their poor English. In general, the bug reports with apologies for their poor English are actually very clear and useful. All the most unclear reports come from native English speakers who assume that I will understand them even if they don't make any effort to be clear or precise.

  • Be specific. If you can do the same thing two different ways, state which one you used. "I selected Load" might mean "I clicked on Load" or "I pressed Alt-L". Say which you did. Sometimes it matters.
  • Be verbose. Give more information rather than less. If you say too much, the programmer can ignore some of it. If you say too little, they have to come back and ask more questions. One bug report I received was a single sentence; every time I asked for more information, the reporter would reply with another single sentence. It took me several weeks to get a useful amount of information, because it turned up one short sentence at a time.
  • Be careful of pronouns. Don't use words like "it", or references like "the window", when it's unclear what they mean. Consider this: "I started FooApp. It put up a warning window. I tried to close it and it crashed." It isn't clear what the user tried to close. Did they try to close the warning window, or the whole of FooApp? It makes a difference. Instead, you could say "I started FooApp, which put up a warning window. I tried to close the warning window, and FooApp crashed." This is longer and more repetitive, but also clearer and less easy to misunderstand.
  • Read what you wrote. Read the report back to yourself, and see if you think it's clear. If you have listed a sequence of actions which should produce the failure, try following them yourself, to see if you missed a step.

HOWTO: Help with QA - OR - What to do when you find a bug

This set of pages is intented for two different types of users who need to get an issue resolved. It helps users understand the Drupal issue (bug) tracking system.

You want to give back to drupal without programming

The first is the situation where you are not a programmer, but you want to help with Drupal in some way. That's great! Ideally every user of Drupal would provide some assistance, though not all are able to write PHP. This guide can help you to give back to the community and help others. While there are many ways to help Drupal, this is one very important and often neglected way.

In this case, the set of "Contributor Links" available in your profile page are very handy in providing the bug bingo system which will take you to a random bug in the issue queue. If you have 10 extra minutes, spend some time clicking on "bug bingo" and see if you can improve the issue reports that you find so they are more clear.

You've been bitten by a bug and want it fixed

Eventually it happens to everyone - a bug "bites" you. That is to say, you find a problem with the way that Drupal works. This can be very frustrating. You want your bug fixed and fast, but either you aren't a programmer or you aren't savvy enough to write the code to fix this particular bug. You need help from someone else to get your bug fixed. How can you do that?

Ultimate Goal: Helping the Developers

Whichever reason you have for writing bugs or working on the issue queue your goal is simple: help the developers. Realistically there is a relative scarcity of people who can write good code. If you help make good issue reports and improve the current issues by following the advice laid out in this set of pages, you will not only make the developer's job easier you will also learn a lot about how Drupal works and will grow in your knowledge of development and coding practices. Working on issues is a great way to work your way into becoming a Drupal developer. After you have improved a few issues and started to provide minor patches you can then move onto more major changes. Proper use of the issue queue brings you respect from other users and developers who will then go out of their way to help you.

Getting a Bug Fixed Sooner

Let's say that you found a bug that affects you, you searched for it and found that it's already submitted. Now you want to ensure that it gets fixed. Here are a couple things you can do to improve the likelihood of the bug getting attention from a developer.

Improving Bug Reports

Many bug reports don't get attention simply because the quality of the report is low making it harder to understand and work on the bug. It will help the developer tremendously if you can take these bugs and improve them so that they have enough information. If necessary, ask questions of the original reporter. Try to repeat the problem yourself and note and differences or similarities in the results and the system configurations. If a developer sees a bug with a clear report that has been repeated then it is much easier for them to fix it.

In addition validating the description, make sure that the status, version, priority, and other values are set correctly.

Increase Visibility of the Issue

Once you have already ensured that the issue is well documented, repeatable, and properly categorized, it's important to make sure that people are aware of the issue. Many times an issue only affects a subset of the population so a developer who might be able to fix your bug might not be aware of it. Several easy ways to help increase the visibility of a particular bug is the write about it in your blog, join #drupal-support on IRC and ask around about the bug, add the bug to your email signature on the account that you use for the mailing lists or to your Drupal.org signature and then be sure to provide lots of help in the forum and the mailing lists so your signature is visible.

How not to increase visibility: send private emails to the module owner. If they are the owner they've probably already seen the bug via email from the issue tracker so another email is just a waste of time to them.

Providing a Patch

Many times the bug report will contain a description of what to change or will have the text of a patch pasted into the bug. While it may seem trivial, a patch makes the maintainer's job easier than just describing the change or pasting the patch into the comments of the bug.

If you can write the necessary code - by all means, create a patch. Again, it makes the maintainer's life simpler if someone creates a patch and all

Improving a Patch

Many times a patch exists but does not conform to Drupal's coding standards or lacks in some other ways. You can help to review the patch and ensure that it conforms to the coding standards. This is relatively advanced for someone who doesn't know how to program PHP, but it isn't terribly difficult and helps you understand how Drupal works.

Hire Someone or Providing a Bounty

Many times a bug just needs a bounty. Putting out some money on a bug (or getting others to join together to create a big bounty) can result in the quick resolution. Sometimes you just need a solution now. In that case, take a look at the Support and Professional Services page in the handbook to try and find someone who might be interested. You can also read the HOWTO: Hire a Drupal site developer to help you in finding the right person.

Ineffective Methods of Getting a Bug Fixed
  • Marking a "closed" or "won't fix" bug as active again without providing a convincing argument as to why it should be opened again. It's better off to provide the convincing argument and let someone else reopen the bug
HOWTO: Make A GOOD issue report

Drupal makes use of the Drupal Project Module to keep track of issues that are found in Drupal Core software, Modules, Themes, Theme Engines, and Translations. This system tracks several pieces of information about each issue but also leaves a large empty box for you to enter your own bug report. If you want your bug to get fixed, it is extremely important that you take the time to enter the proper information into these fields.

You should also see How to troubleshoot Drupal for self-help steps to do BEFORE raising an issue.

Project Information

Make sure you choose the correct version and make a decent guess as to the right Project and Component. Sometimes the correct value can be unclear, but if you make an effort to get it right that helps.

Issue Information

Try to determine the Category, Priority, Assigned, and Status values.

Category: Bug Reports are for situations where the software does not work as was intended by the programmer. Feature Requests are for situations where the software works as designed, but the design can be improved. Tasks are for something that just needs to get done [frankly their use seems a little unclear]. Support Requests are for situations where you wish to ask about a specific component. Support Requests can seem redundant with the forums, IRC, and mailing lists, but these are a good way to ask a question at a targeted audience (the module maintainer).

Note that new features are generally only accepted for the CVS version of code unless they are very important. Even if it seems important to you, it has to be important for a large number of sites to be worth creating a fix for a currently released version of Drupal. You cannot get around this rule by simply labeling a feature request as a bug request. If you want to get a feature applied to a released version of Drupal you need to make the case that it is important, affects a large number of sites, and ensure that the code changed is stable.

Priority: the priority field is an easy one to abuse. Generally, Critical should be reserved for the most problematic and important issues. Abuse of the Critical field will likely get your issue ignored by a developer and that's the last thing you want. Normal and Minor priorities are both fine to use. More detail in Priority Levels of Issues.

Assigned: the Assigned field helps keep track of the person working on a particular issue. If you are working on the issue - e.g. you are writing a patch to fix it - then you should set the issue as assigned to you.

Status: Status is one of the most often overlooked fields. Many developers have filters set to look for issues in certain statuses, so changing this field inappropriately can lead to your issue getting ignored.

Issue Details

Title: you want your title to be descriptive and concise. Compare these two titles:
SITE BROKEN YOU MUST FIX NOW!!!111!!

and

Admin-Modules Page Blank After Installing Module Foo

The first title doesn't explain the problem and tries to impart a sense of priority. The priority should stay in the priority field. The title is for describing your problem, and while "SITE BROKEN" may be what you think of your site it doesn't help a developer when they see three bug reports with different problems all with the title "SITE BROKEN". The second title succinctly describes the specific problem with the site and a possible cause. This makes it easy for the developer to keep track of the bugs in their queue. If you look at many of the issue queues you will see dozens or hundreds of issues. Having unique titles that are descriptive makes a specific issue stand out as easier to work on among the rest of the issues.

Also note that the project issue titles do not behave like a forum thread. Changing the title will change the title of the whole issue, not just your followup. This is useful for renaming a vague issue into a more descriptive one but it makes things worse if it is used to title issues like "I'm getting this too".

Description: The Description field is wide open which leaves you a lot of space to say a lot or a little. The Description takes the title one step further. Generally speaking, the majority of the time spent fixing an issue is spent on understanding the problem and finding the cause of that issue. The goal of the description is to state the exact set of conditions that cause a problem and the resultant undesirable state of the system so that the developer spends as little time as possible understanding your situation. Your issue should reflect the

  • repeatability of the problem - whether it is easy to repeat or happens randomly
  • steps to repeat - what you think caused the problem, ideally these steps should be repeated until they are as simple as possible and still cause the problem
  • desired outcome of the steps - that is, what you WANT to have happen
  • actual outcome of your steps - this is the essence of your problem

Compare these two bug reports:

MY PAGES ARE BLANK!!! I AM ABOUT TO DELETE DRUPAL FROM MY SYSTEM!!!

and

Repeatable: Always
Steps to repeat:
1. Download Module Foo
2. Uncompress foo-4.7.0-tar.gz into modules folder
3. Visit admin/modules page in site

Expected Results:
User sees the modules page and can enable the module Foo

Actual Results:
Page is completely blank.  Checking in the Apache Error Log showed this error:

Fatal error: Allowed memory size of 8388608 bytes exhausted (tried to allocate 418591 bytes) in /path/to/drupal/modules/foo/foo.module on line 42

The first bug report is relatively useless because it forces the developer to waste their time asking questions and guessing at the possible problem. The second one communicates the problem precisely and concisely. The user has described a specific set of steps that they took, the have stated that it is repeatable in their tests, and in addition to the desired and actual results they provided an exact error message from their system. This makes the developer's job relatively easy to try and find the problem.

HOWTO: Submit core issues

Thanks, saw your message, however I could not find where to submit an issue for the core 'Drupal' Project There is no Project in the global navigation, under Downloads, the Tile of the page is Projects, but the core project is not there, nor the core themes.

I had difficulty with it too, in the beginning. Here are the instructions:

  1. In the right menu, select 'Create Content'. Then Select 'Issue. You get the 'Submit issue' screen.
  2. Fill the project field. Examples - If you are trying to submit usability issues for the garland theme, select 'Drupal' (the second item under the 'Drupal Project' category) and click 'Next'. If you are trying to submit general usability issues which are not module specific, select the 'User Experience' project under the 'Drupal Project' category. You get the second issue submission screen.
  3. If you selected 'Drupal', you should now select the component to which the usability issue relates. Select 'Garland Theme' (it's in the bottom of the list) and try to fill as much details as possible about the issue, things which will help people visualize the problem. Add a link to a flickr picture of the usability issue, or record it as an AVI file with the free camstudio and upload it to a video sharing site, then give a link. This will highly improve the change of people understanding you. Or include the image as an attachment.
  4. After filling all details, press the submit bottom. Take care - you won't be able to later modify the text - only add new comments.
  5. Continue to update the issue to reflect new understanding you've got about the situation, and/or links to relevant information.

(adapted from http://drupal.org/node/118045)

Criteria for evaluating proposed changes

The following criteria are used by core developers in reviewing and approving
proposed changes:

  • The changes support and enhance Drupal project aims.
  • The proposed changes are current.Especially for new features, priority is usually given to development for the "HEAD" (the most recent development version of the code, also referred to as the CVS version) as opposed to released versions. There may have been significant changes since the last release, so developing for the CVS version means that
  • The proposed change doesn't raise any significant issues or risks.Specifically, issues that have been raised in the review process have been satisfactorily addressed.
  • The changes are well coded. At a minimum, this means coding in accordance with the Drupal coding standards. But it also means that the coding is intelligent and compact. Elegant solutions will have greater support than cumbersome ones that accomplish the same result.
  • There is demonstrated demand and support for the change. Demand is indicated by, e.g., comments on the drupal.org issues system or comments in forums or the drupal-dev email list.
  • The change will be used by a significant portion of the installed Drupal base as opposed being relevant only to a small subset of Drupal users.
  • The benefits of the change justifies additional code and resource demands. Every addition to the code base increases the quantity of code that must be actively maintained (e.g., updated to reflect new design changes or documentation approaches). Also, added code increases the overall Drupal footprint through, e.g., added procedure calls or database queries. Benefits of a change must outweigh these costs.

Effectively raising money to work on improvements to Drupal (reverse bounty)

So, you have a great idea for Drupal but can't take any time away from your "day job" to work on it. Fear not! This page will focus on some effective tools to raise money from the Drupal community to allow you to work on your beloved Drupal improvement.

Post a reverse bounty

A reverse bounty is an idea and feature description of something that a developer actually wants to work on, along with the money required to complete the task. The developer knows that it can be done, knows what the platform can do, and has the skills to actually implement it. The money allows them to dedicate time to actually work on it.

To learn more about the concept, take a look at Boris Mann's blog post on reverse bounties.

Post your idea in the paid Drupal services forum, and see if you can raise some interest first.

Show me the money!

Once you have an idea, you also need to quickly and effectively raise money to work on it. There are many ways a developer can raise money.

An example of a ChipIn widget.

ChipIn
I'm going to focus on my favorite, ChipIn. With ChipIn, you can create a donation widget where people can easily see what you're raising money for, how much you need, how much has been donated, who's donated, and when you need it by (see the screen shot to the right). They take a service fee, but it's comparable to what PayPal charges for most accounts.
Donorge
Drupal.org currently accepts donations through Donorge which has no donation fee. The motivation behind the open development of Donorge is to help support and increase the effect of donations made towards interests, needs, ideas and organization, either localized or connected around the world.
PayPal
PayPal is still a solid way to accept money, and although it doesn't give you the sweet widget data like ChipIn or have 0% transaction fees like Donorge, it's familiar territory for any scardy-cats.

Complete the work

With reverse bounties, most of the time the developer gets paid in full (or a deposit of 50%) before doing the work. As the developer, you have to be trust-worthy and complete the work in a reasonable amount of time.

Feature suggestions

How many times you have dreamed "Gee...I wish Drupal could do that" or "I like the xxx feature, but it should work better". If you want to improve Drupal, send us you wishes as a feature suggestions. Your suggestions play an essential role in making Drupal more usable and feature-rich.

The core features provided by Drupal are listed on the features page. You can submit a feature request by creating a new issue connected to the component the feature is related to. Please note that there is a Drupal contributed module named 'Features' which is used on the feature page mentioned above. Every module has a feature request subcategory, and thus the 'Feature' module is not the appropriate place to submit feature requests. To properly file a feature request, first choose the project it is related to and then after hitting preview set the other related options. You will be able to categorize the issue as a feature request with the Issue Information / Category dropdown.

Priority levels of Issues (Bugs and Features)

These are bugs that hold back a release

Critical
When a bug breaks all core or renders a popular function unusable. Possible examples: sessions, bootstrap one of the form elements or the ability to create a new node. These are to be fixed immediately because Drupal HEAD is not usable at all.

These are bugs that do not hold a release:

Normal bugs
Just bugs, we could even release with these. Just one functionality. Example: the category filter not working on node admin screen.
Minor bugs
Cosmetic issues, notices etc. Often also referred to as 'tasks' or features.

The revision process

Changes to the Drupal core are usually made after consideration, planning, and consultation.  They are also made on a priority basis--fixes come before additions, and changes for which there is a high demand come before proposals that have gone relatively unnoticed.  Any potential change has to be considered not only on its own merits but in relation to the aims and principles of the project as a whole.

The particular stages that a new feature goes through vary, but a typical cycle for a significant change might include:

  • General discussion of the idea, for example through a posting in a drupal.org forum.  This can be a chance to gauge support and interest, scope the issue, and get some
    direction and suggestions on approaches to take.  If you're considering substantive changes, starting out at the discussion level - rather than jumping straight into code changes - can save you a lot of time. 
  • Posting an issue through the drupal.org project system.
  • Discussion raising issues on the proposed direction or solution, which may include a real-time meeting through IRC. 
    Individual Drupal community members may vote for (+1) or against (-1) the change.  While informal, this voting system can help quantify support.
  • Producing a patch with specific proposed code changes.
  • Review of the changes and further discussion.
  • Revisions to address issues.
  • Possible application of the patch.

The process of discussion and revision might be repeated several times to encompass diverse input.  At any point in the process, the proposal might be:

  • Shelved as impractical or inappropriate.
  • Put off until other logically prior decisions are made.
  • Rolled into another related initiative.
  • Superceded by another change.

If you submit suggestions that don't end up being adopted, please don't be discouraged!  It doesn't mean that your ideas weren't good--just that they didn't end up finding a place.  The discussion itself may have beneficial outcomes.  It's all part of collaboratively building a quality open source project.

Maintaining a project on drupal.org

Each drupal.org project (a contributed theme, module or translation) needs to be maintained in the contributions repository. If you are not using the drupal.org infrastructure, you cannot setup a project page on drupal.org nor can you offer your module for download at drupal.org. Please note that all code which is commited into the Drupal CVS repository must be covered under the terms of the GNU General Public License.

Before creating a project page on drupal.org, you must apply for a CVS account. An administrator regularly reviews these requests and usually responds within one week. If you have included enough information and your proposed project does not duplicate existing work, your request will most likely be approved (via email). Once this happens, you will be able to use the cvs login command with your cvs user name. This will allow you to add a new directory to the CVS repository and commit your files. Please read the Drupal CVS handbook for more information about how to use CVS at drupal.org.

To get your project listed on drupal.org after it has been committed to CVS, fill in the form at http://drupal.org/node/add/project_project/.

  • Make sure that the 'Short project name' matches the directory name in the CVS repository. For example, the contributions/modules/my_module module has the short name my_module.
  • Fill in the correct value for the CVS directory field to match the path in the repository. In this case, you should include everything after the contributions part of the directory name (for example /modules/my_module/).
  • You should enable the issue tracker for your project to allow people will be able to file bugs against your project, add tasks, or request new features.
  • Please note that you must create a project node before you add any CVS tags or branches to your contribution, or you will run into errors when you try to make releases.

Once you submit the project page, it will be visible on drupal.org and people will immediately be able to create issues. You can use the My projects link in the main navigation menu to view any issues that have been submitted for your project(s).

In order for people to download your project, one or more "releases" will need to be created. Learn more about how to create releases at the Managing releases section of the CVS handbook pages.

Controlling CVS access

As the owner of a project, you will have full CVS access to a directory in the contributions repository. This directory holds all the files belonging to a specific contribution. If there is a team of multiple developers working on the same project, you can grant CVS access to other users and give them the ability to to commit and tag files in your project's directory. You do this by using the CVS access tab on your project's home page.

For example, if you are the owner of the event module, which lives at http://drupal.org/node/3238, then when you view this page you will see a new tab called CVS access, which can be found at http://drupal.org/node/3238/cvs-access. (Note: this link will not work unless you really are the maintainer of the event module, you'll have to view one of your projects to see the tab for yourself).

The CVS access tab allows you to grant other users CVS access to your project. There is a text field that will auto-complete with user names from drupal.org, to help you input a valid user. The form will verify that the user you select has an account on drupal.org and that the user has a valid CVS account. If your selection is valid, the user will be added to the table of users with CVS access on the page, and will be granted permission to commit and tag files in your project's directory.

Once you have granted access to a user, you can revoke the access by clicking the delete link next to their user name in the table of users with CVS access.

Any file included in your project that ends in .po or .pot will bypass this access control system. This way, module translators will be able to contribute translations without any additional effort or coordination with the project owner.

HOWTO: Checkout a drupal module from Drupal CVS

Checking out Drupal Modules and moving on with the current revision may be needed for developers and maintainers of drupal. But it took me time to figure out the main repo and the way to checkout from CVS. So, iam documenting here to make it helpful for whoever facing the same issue.

http://cvs.drupal.org/viewcvs/drupal/

If you want to see revisions under particular tag, give it like

http://cvs.drupal.org/viewcvs/drupal/?only_with_tag=DRUPAL-5--2

To checkout drupal, use

cvs -d ":pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal" checkout -r HEAD drupal

or

cvs -d ":pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal" checkout -r <tag-name> drupal

To checkout drupal modules, use

cvs -d ":pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal" checkout -r HEAD contributions/modules/<module-name>

for example,
cvs -d ":pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal" checkout -r HEAD contributions/modules/provisionator

Orphaned projects

If you are no longer capable of maintaining your project, please add a note to your project page and ask in the forums whether someone is willing to take over maintenance. The development mailing list is another good place to ask for volunteers. Proper communication is key so make sure to mark your project as orphaned. If you found a new maintainer or if you are willing to maintain an orphaned project, you should submit a task to the Drupal.org webmasters issue queue to request an ownership transfer (there's even an issue component called "Project ownership" you should use when submitting the task).

Tips for contributing to the core

The following tips might improve the chances of your contributions being accepted. Supplying patches is how to make changes to core or contributed code.

  • Start small. Review other patches and offer constructive suggestions and improvements. Tackle a few small bugs from the issue queue. Prove yourself. Before you wade in with a substantial change, develop a profile as a dependable, collaborative contributor.
  • Take a step back and objectively evaluate whether the changes are appropriate for the Drupal core. Ask yourself:
    • Is the feature already implemented? Search the forums and issue tracker.
    • Could the feature be implemented as a contributed module rather than a patch to the core?
    • Will the change benefit a substantial portion of the Drupal install base?
    • Is the change sufficiently general for others to build upon cleanly?
  • Be explanatory, provide descriptions and illustrations, make a good case. Don't count on others downloading, installing, and testing your changes. Rather, show them in a nutshell what your changes would mean. Anticipate and address questions or concerns. If appropriate, provide screenshots.
  • Be collaborative. Be friendly and respectful. Acknowledge the effort others put in.
  • Be open to suggestions and to other ways of accomplishing what you're aiming for.
  • Be persistent. If you don't get any response right away, don't necessarily give up. If you're still convinced your idea has merit, find another way to present it. Request another developer - ideally, one familiar with your work - to take the time to review your issue.
  • Respond, in a timely way, to suggestions, requests, or issues raised. Revise your work accordingly.
  • If some time has gone by, update your changes to work with the current CVS version.

List of maintainers

This comes from MAINTAINERS.txt, and may not be completely up to date. It roughly lists what users in the community maintain core Drupal modules. See Drupal core for more information on what this means.

BOOTSTRAP
Moshe Weitzman

BLOG API
James Walker

DISTRIBUTED AUTHENTICATION MODULES
Moshe Weitzman

DOCUMENTATION COORDINATOR
Charlie Lowe

FILTER SYSTEM
Steven Wittens

FORM SYSTEM
Károly Négyesi

LOCALE MODULE
Gábor Hojtsy

MENU SYSTEM
Richard Archer

TABLESORT API
Moshe Weitzman

PATH MODULE
Matt Westgate

POSTGRESQL PORT
Piotr Krukowiecki

SECURITY COORDINATOR
Károly Négyesi

STATISTICS MODULE
Jeremy Andrews

SEARCH
Steven Wittens

XML-RPC SERVER/CLIENT
Károly Négyesi

DEBIAN PACKAGE
Hilko Bengen

TRANSLATIONS COORDINATOR
Gerhard Killesreiter

THE REST:
Dries Buytaert

Mailing lists

Support

A list for support questions.

view archive . search archive . mailman page

Development

A list for Drupal developers.

view archive . search archive . mailman page

Themes

A list for Drupal theme developers/designers.

view archive . search archive . mailman page

Documentation

A list for documentation contributors.

view archive . search archive . mailman page

Translations

A list for Drupal UI translators.

view archive . search archive . mailman page

Consulting

A mailing list for Drupal consultants and Drupal service/hosting providers.

view archive . search archive . mailman page

CVS commits

A list with all CVS commit messages.

view archive . search archive . mailman page

CVS applications

A list of all applications for an account in the Drupal contributions CVS repository.

view archive . search archive . mailman page

Webmasters

A list for drupal.org webmasters (e.g. settings and content on the drupal.org website, user management, removing spam, etc.).

view archive . search archive . mailman page

Infrastructure

A list for drupal.org infrastructure maintainers (e.g. drupal.org hardware and server configuration, the CVS repository, mailing lists, etc).

view archive . search archive . mailman page

DrupalCON

A list for the organization of Drupal conferences and events.

view archive . search archive . mailman page

Subscribe

Mailing of project issues

In addition to the various mailing lists you can subscribe to, you can subscribe to project issue updates for any contributed module, theme, or translation. You can either subscribe to all issues for a given project, or only get emails for issues you have participated in (either submitted or posted a follow-up replly to).

All issues for certain projects (for example, the webmasters or infrastructure projects) are emailed to a corresponding mailing list.

Coding standards

Drupal Coding Standards

Note: The Drupal Coding Standards applies to code that is to become a part of Drupal. This document is based on the PEAR Coding standards.

Indenting

Use an indent of 2 spaces, with no tabs.

Control Structures

These include if, for, while, switch, etc. Here is an example if statement, since it is the most complicated of them:

if (condition1 || condition2) {
  action1;
}
elseif (condition3 && condition4) {
  action2;
}
else {
  defaultaction;
}

Control statements should have one space between the control keyword and opening parenthesis, to distinguish them from function calls.

You are strongly encouraged to always use curly braces even in situations where they are technically optional. Having them increases readability and decreases the likelihood of logic errors being introduced when new lines are added.

For switch statements:

switch (condition) {
  case 1:
    action1;
    break;

  case 2:
    action2;
    break;

  default:
    defaultaction;
    break;
}

Function Calls

Functions should be called with no spaces between the function name, the opening parenthesis, and the first parameter; spaces between commas and each parameter, and no space between the last parameter, the closing parenthesis, and the semicolon. Here's an example:

$var = foo($bar, $baz, $quux);

As displayed above, there should be one space on either side of an equals sign used to assign the return value of a function to a variable. In the case of a block of related assignments, more space may be inserted to promote readability:

$short         = foo($bar);
$long_variable = foo($baz);

Function Declarations

function funstuff_system($field) {
  $system["description"] = t("This module inserts funny text into posts randomly.");
  return $system[$field];
}

Arguments with default values go at the end of the argument list. Always attempt to return a meaningful value from a function if one is appropriate.

Arrays

Arrays should be formatted with a space separating each element and assignment operator, if applicable:

$some_array = array('hello', 'world', 'foo' => 'bar');

Note that if the line spans longer than 80 characters (often the case with form and menu declarations), each element should be broken into its own line, and indented one level:

$form['title'] = array(
  '#type' => 'textfield',
  '#title' => t('Title'),
  '#size' => 60,
  '#maxlength' => 128,
  '#description' => t('The title of your node.'),
);

Note the comma at the end of the last array element--this is not a typo! It helps prevent parsing errors if another element is placed at the end of the list later.

Comments

Inline documentation for classes should follow the Doxygen convention. More information about Doxygen can be found here:

Note that Drupal uses the following docblock syntax:

/**
 * Comments.
 */

And all Doxygen commands should be prefixed with a @ instead of a /.

Non-documentation comments are strongly encouraged. A general rule of thumb is that if you look at a section of code and think "Wow, I don't want to try and describe that", you need to comment it before you forget how it works.

C style comments (/* */) and standard C++ comments (//) are both fine. Use of Perl/shell style comments (#) is discouraged.

Including Code

Anywhere you are unconditionally including a class file, use require_once(). Anywhere you are conditionally including a class file (for example, factory methods), use include_once(). Either of these will ensure that class files are included only once. They share the same file list, so you don't need to worry about mixing them - a file included with require_once() will not be included again by include_once().

Note: include_once() and require_once() are statements, not functions. You don't need parentheses around the filename to be included.

PHP Code Tags

Always use <?php ?> to delimit PHP code, not the <? ?> shorthand. This is required for Drupal compliance and is also the most portable way to include PHP code on differing operating systems and setups.

Note that the final ?> should be omitted from all code files--modules, includes, etc. The closing delimiter is optional, and removing it helps prevent unwanted white space at the end of files which can cause problems elsewhere in the system. More information is available from the PHP Code tags portion of the handbook.

Header Comment Blocks

All source code files in the core Drupal distribution should contain the following comment block as the header:

<?php
// $Id$

This tag will be expanded by the CVS to contain useful information

<?php
// $Id: CODING_STANDARDS.html,v 1.7 2005/11/06 02:03:52 webchick Exp $

Using CVS

Include the Id CVS keyword in each file. As each file is edited, add this tag if it's not yet present (or replace existing forms such as "Last Modified:", etc.).

The rest of this section assumes that you have basic knowledge about CVS tags and branches.

CVS tags are used to label which revisions of the files in your package belong to a given release. Below is a list of the required CVS tags:

DRUPAL-X-Y
(required) Used for tagging a release. If you don't use it, there's no way to go back and retrieve your package from the CVS server in the state it was in at the time of the release.

Example URLs

Use "example.com" for all example URLs, per RFC 2606.

Naming Conventions

Functions and Methods

Functions and methods should be named using lower caps and words should be separated with an underscore. Functions should in addition have the grouping/module name as a prefix, to avoid name collisions between modules.

Private class members (meaning class members that are intended to be used only from within the same class in which they are declared; PHP 4 does not support truly-enforceable private namespaces) are preceded by a single underscore. For example:

_node_get()

$this->_status

Constants

Constants should always be all-uppercase, with underscores to separate words. Prefix constant names with the uppercased name of the module they are a part of.

Global Variables

If you need to define global variables, their name should start with a single underscore followed by the module/theme name and another underscore.

Filenames

All documentation files should have the filename extension ".txt" to make viewing them on Windows systems easier. Also, the filenames for such files should be all-caps (e.g. README.txt instead of readme.txt) while the extension itself is all-lowercase (i.e. txt instead of TXT).

Examples: README.txt, INSTALL.txt, TODO.txt, CHANGELOG.txt etc.

Doxygen formatting conventions

Doxygen is a documentation generation system. The documentation is extracted directly from the sources, which makes it much easier to keep the documentation consistent with the source code.

There is an excellent manual at the Doxygen site. The following notes pertain to the Drupal implementation of Doxygen.

General documentation syntax

To document a block of code, the syntax we use is:

/**
* Documentation here.
*/

Doxygen will parse any comments located in such a block. Our style is to use as few Doxygen-specific commands as possible, so as to keep the source legible. Any mentions of functions or file names within the documentation will automatically link to the referenced code, so typically no markup need be introduced to produce links.

Documenting files

It is good practice to provide a comment describing what a file does at the start of it. For example:

<?php
// $Id: theme.inc,v 1.202 2004/07/08 16:08:21 dries Exp $

/**
* @file
* The theme system, which controls the output of Drupal.
*
* The theme system allows for nearly all output of the Drupal system to be
* customized by user themes.
*/

The line immediately following the @file directive is a short description that will be shown in the list of all files in the generated documentation. If the line begins with a verb, that verb should be in present tense, e.g., "Handles file uploads." Further description may follow after a blank line.

To add CVS ID-Tags to your file, add a // $Id$ to your file. CVS will automatically expand it to the format shown above. In the future, you don't have to care about that as CVS will update these information automatically.

Documenting functions

All functions that may be called by other files should be documented; private functions optionally may be documented as well. A function documentation block should immediately precede the declaration of the function itself, like so:

/**
* Verify the syntax of the given e-mail address.
*
* Empty e-mail addresses are allowed. See RFC 2822 for details.
*
* @param $mail
*   A string containing an email address.
* @return
*   TRUE if the address is in a valid format.
*/
function valid_email_address($mail) {

The first line of the block should contain a brief description of what the function does, beginning with a verb in the form "Do such and such" (rather than "Does such and such"). A longer description with usage notes may follow after a blank line. Each parameter should be listed with a @param directive, with a description indented on the following line. After all the parameters, a @return directive should be used to document the return value if there is one. There is no blank line between the @param and @return directives. Functions that are easily described in one line may omit these directives, as follows:

/**
* Convert an associative array to an anonymous object.
*/
function array2object($array) {

The parameters and return value must be described within this one-line description in this case.

Documenting hook implementations

Many modules consist largely of hook implementations. If the implementation is rather standard and does not require more explanation than the hook reference provides, a shorthand documentation form may be used:

/**
* Implementation of hook_help().
*/
function blog_help($section) {

This generates a link to the hook reference, reminds the developer that this is a hook implementation, and avoids having to document parameters and return values that are the same for every implementation of the hook.

Documenting themeable functions

In order to provide a quick reference for theme developers, we tag all themeable functions so that Doxygen can group them on one page. To do this, add a grouping instruction to the documentation of all such functions:

/**
* Format a query pager.
*
* ...
* @ingroup themeable
*/
function theme_pager($tags = array(), $limit = 10, $element = 0, $attributes = array()) {
  ...
}

The same pattern can be used for other functions scattered across multiple files that need to be grouped on a single page.

Configuring Eclipse

This describes how to configure Eclipse to play nicely with Drupal.

For PHPeclipse - PHP editing

We need to ensure that tab/indent inserts two spaces, and also that .inc, .module, .engine, .theme and .install files are recognised as PHP files.

Make the following changes under Window -> Preferences

  1. Expand the left-hand menu to General -> Content Types
    Under "Content types" on the right, click Text -> PHP Source File
    Add the *.engine, *.theme, *.install, *.inc, and *.module file types
  2. In the left-hand menu click on PHPeclipse Web Development -> PHP
    In the Typing tab, make sure the 'Insert Spaces for Tab' setting is checked
    In the Appearance tab, it is reccommended that you leave the 'Displayed tab width' at 4, so it is easier to spot if any (actual) tabs have snuck in
  3. In the left-hand menu click on PHPeclipse Web Development -> PHP -> Formatter
    In the Style tab, enter 2 for 'Number of spaces representing a tab'
    'Compact Assignment' and 'Indentation is represented by a tab' should both be unchecked

For Eclipse Web Tools - CSS, JS, XML and (X)HTML editing

Window -> Preferences -> Web and XML -> CSS Files -> CSS Source
Select 'Indent using spaces'
Set 'Intentation size' to 2

Window -> Preferences -> Web and XML -> Javascript Files -> Javascript Source
Select 'Indent using spaces'
Set 'Intentation size' to 2

Window -> Preferences -> Web and XML -> HTML Files -> HTML Source
Select 'Indent using spaces'
Set 'Intentation size' to 2

Window -> Preferences -> Web and XML -> XML Files -> XML Source
Select 'Indent using spaces'
Set 'Intentation size' to 2

If you use XTemplate:
Window -> Preferences -> General -> Content Types -> Text -> HTML
Add the *.xtmpl file type

Configuring Emacs

This short snippet will extend the php-mode in Emacs to follow drupals coding style. Add this to your $HOME/.emacs:

(defun drupal-mode ()
  (interactive)
  (php-mode)
  (setq c-basic-offset 2)
  (setq indent-tabs-mode nil)
  (setq fill-column 78)
  (c-set-offset 'case-label 2)
  (c-set-offset 'arglist-close 0))

(add-to-list 'auto-mode-alist '("/drupal.*\\.\\(php\\|module\\|inc\\)$" . drupal-mode))

This will automatically set you in drupal-mode if you load a .php, .module or .inc file from beneath a drupal* directory. You may also manually select drupal mode by hitting M-x drupal-mode.

Configuring vim

Indentation

The following commands will indent your code the right amount, using spaces rather than tabs and automatically indent after you start.

set expandtab
set tabstop=2
set shiftwidth=2
set autoindent
set smartindent

Syntax highlighting

If you enjoy syntax highlighting, it is may be worth remembering that many of Drupal's php files are *.module or *.inc, among others.

Using these settings only with Drupal

  • Copy your .vimrc to .vimrc-drupal
  • Append these settings to the end
  • Run vim -u ~/.vimrc-drupal

To make this easier (using bash on Linux), you could create an alias by typing:

alias vid="vim -u ~/.vimrc-drupal"

This allows you to just use the vid command instead of vi when you want to edit a Drupal file.

Vim seems to syntax highlight *.inc files properly by default but doesn't kow that *.module is PHP content. For *.modules use this snippet in .vimrc:

if has("autocmd")
  " Drupal *.module files.
  augroup module
    autocmd BufRead                     *.module set filetype=php
  augroup END
endif

Guidelines for better performing code

This article is a stub, please help expand and correct this

String Comparison

Using empty() is reported to be about 20% faster than comparison with '' or "" (zero-length strings).
So

<?php
if ($var == "") {
}
?>

Should not be used, instead use
<?php
if (empty($var)) {
}
?>

Indenting

Use an indent of 2 spaces, with no tabs. No trailing spaces.

PHP Code tags

Always use <?php ?> to delimit PHP code, not the <? ?> shorthand. This is required for Drupal compliance and is also the most portable way to include PHP code on differing operating systems and setups.

Note that as of Drupal 4.7, the ?> at the end of code files (modules, includes, etc.) is purposely omitted. The full discussion that led to this decision is available from the no ?> needed at the end of modules discussion on the drupal-devel mailing list, but can be summarized as:

  • Removing it eliminates the possibility for unwanted whitespace at the end of files which can cause "header already sent" errors, XHTML/XML validation issues, and other problems.
  • The closing delimiter at the end of a file is optional.
  • PHP.net itself removes the closing delimiter from the end of its files (example: prepend.inc), so this can be seen as a "best practice."

SQL naming conventions

http://drupal.org/node/2497/

-->
  • Don't use (ANSI) SQL / MySQL / PostgreSQL / MS SQL Server / ... Reserved Words for column and/or table names. Even if this may work with your (MySQL) installation, it may not with others or with other databases. Some references:

    Some commonly misused keywords: TIMESTAMP, TYPE, TYPES, MODULE, DATA, DATE, TIME, ...
    See also [bug] SQL Reserved Words.

  • Capitalization, Indentation
    • UPPERCASE reserved words
    • lowercase (or Capitalize) table names
    • lowercase column names

    Example:

      SELECT r.rid, p.perm
      FROM {role} r
        LEFT JOIN {permission} p ON r.rid = p.rid   -- may be on one line with prev.
      ORDER BY name
     
  • Naming
    • Use plural or collective nouns for table names since they are sets and not scalar values. (Others prefer that table names to match field name, so prefer singular table names. Drupal uses both: blocks, filters, users; but book, poll, role.)
    • Name every constraint (primary, foreign, unique keys) yourself. Otherwise you'll see funny-looking system-generated names in error messages. This happened with the moderation_roles table which initially defined a key without explicite name as KEY (mid). This got mysqldump'ed as KEY mid (mid) which resulted in a syntax error as mid() is a mysql function (see [bug] mysql --ansi cannot import install database).
    • Index names should begin with the name of the table they depend on, eg. INDEX users_sid_idx.

References:

List of SQL Reserved Words

The list below represents a combination of the following sources of SQL reserved words:

There are undoubtedly more sources that we should add to this list, but this makes a very good starting point.

Reserved Words
  1. A

  2. ABORT

  3. ABS

  4. ABSOLUTE

  5. ACCESS

  6. ACTION

  7. ADA

  8. ADD

  9. ADMIN

  10. AFTER

  11. AGGREGATE

  12. ALIAS

  13. ALL

  14. ALLOCATE

  15. ALSO

  16. ALTER

  17. ALWAYS

  18. ANALYSE

  19. ANALYZE

  20. AND

  21. ANY

  22. ARE

  23. ARRAY

  24. AS

  25. ASC

  26. ASENSITIVE

  27. ASSERTION

  28. ASSIGNMENT

  29. ASYMMETRIC

  30. AT

  31. ATOMIC

  32. ATTRIBUTE

  33. ATTRIBUTES

  34. AUDIT

  35. AUTHORIZATION

  36. AUTO_INCREMENT

  37. AVG

  38. AVG_ROW_LENGTH

  39. BACKUP

  40. BACKWARD

  41. BEFORE

  42. BEGIN

  43. BERNOULLI

  44. BETWEEN

  45. BIGINT

  46. BINARY

  47. BIT

  48. BIT_LENGTH

  49. BITVAR

  50. BLOB

  51. BOOL

  52. BOOLEAN

  53. BOTH

  54. BREADTH

  55. BREAK

  56. BROWSE

  57. BULK

  58. BY

  59. C

  60. CACHE

  61. CALL

  62. CALLED

  63. CARDINALITY

  64. CASCADE

  65. CASCADED

  66. CASE

  67. CAST

  68. CATALOG

  69. CATALOG_NAME

  70. CEIL

  71. CEILING

  72. CHAIN

  73. CHANGE

  74. CHAR

  75. CHAR_LENGTH

  76. CHARACTER

  77. CHARACTER_LENGTH

  78. CHARACTER_SET_CATALOG

  79. CHARACTER_SET_NAME

  80. CHARACTER_SET_SCHEMA

  81. CHARACTERISTICS

  82. CHARACTERS

  83. CHECK

  84. CHECKED

  85. CHECKPOINT

  86. CHECKSUM

  87. CLASS

  88. CLASS_ORIGIN

  89. CLOB

  90. CLOSE

  91. CLUSTER

  92. CLUSTERED

  93. COALESCE

  94. COBOL

  95. COLLATE

  96. COLLATION

  97. COLLATION_CATALOG

  98. COLLATION_NAME

  99. COLLATION_SCHEMA

  100. COLLECT

  101. COLUMN

  102. COLUMN_NAME

  103. COLUMNS

  104. COMMAND_FUNCTION

  105. COMMAND_FUNCTION_CODE

  106. COMMENT

  107. COMMIT

  108. COMMITTED

  109. COMPLETION

  110. COMPRESS

  111. COMPUTE

  112. CONDITION

  113. CONDITION_NUMBER

  114. CONNECT

  115. CONNECTION

  116. CONNECTION_NAME

  117. CONSTRAINT

  118. CONSTRAINT_CATALOG

  119. CONSTRAINT_NAME

  120. CONSTRAINT_SCHEMA

  121. CONSTRAINTS

  122. CONSTRUCTOR

  123. CONTAINS

  124. CONTAINSTABLE

  125. CONTINUE

  126. CONVERSION

  127. CONVERT

  128. COPY

  129. CORR

  130. CORRESPONDING

  131. COUNT

  132. COVAR_POP

  133. COVAR_SAMP

  134. CREATE

  135. CREATEDB

  136. CREATEROLE

  137. CREATEUSER

  138. CROSS

  139. CSV

  140. CUBE

  141. CUME_DIST

  142. CURRENT

  143. CURRENT_DATE

  144. CURRENT_DEFAULT_TRANSFORM_GROUP

  145. CURRENT_PATH

  146. CURRENT_ROLE

  147. CURRENT_TIME

  148. CURRENT_TIMESTAMP

  149. CURRENT_TRANSFORM_GROUP_FOR_TYPE

  150. CURRENT_USER

  151. CURSOR

  152. CURSOR_NAME

  153. CYCLE

  154. DATA

  155. DATABASE

  156. DATABASES

  157. DATE

  158. DATETIME

  159. DATETIME_INTERVAL_CODE

  160. DATETIME_INTERVAL_PRECISION

  161. DAY

  162. DAY_HOUR

  163. DAY_MICROSECOND

  164. DAY_MINUTE

  165. DAY_SECOND

  166. DAYOFMONTH

  167. DAYOFWEEK

  168. DAYOFYEAR

  169. DBCC

  170. DEALLOCATE

  171. DEC

  172. DECIMAL

  173. DECLARE

  174. DEFAULT

  175. DEFAULTS

  176. DEFERRABLE

  177. DEFERRED

  178. DEFINED

  179. DEFINER

  180. DEGREE

  181. DELAY_KEY_WRITE

  182. DELAYED

  183. DELETE

  184. DELIMITER

  185. DELIMITERS

  186. DENSE_RANK

  187. DENY

  188. DEPTH

  189. DEREF

  190. DERIVED

  191. DESC

  192. DESCRIBE

  193. DESCRIPTOR

  194. DESTROY

  195. DESTRUCTOR

  196. DETERMINISTIC

  197. DIAGNOSTICS

  198. DICTIONARY

  199. DISABLE

  200. DISCONNECT

  201. DISK

  202. DISPATCH

  203. DISTINCT

  204. DISTINCTROW

  205. DISTRIBUTED

  206. DIV

  207. DO

  208. DOMAIN

  209. DOUBLE

  210. DROP

  211. DUAL

  212. DUMMY

  213. DUMP

  214. DYNAMIC

  215. DYNAMIC_FUNCTION

  216. DYNAMIC_FUNCTION_CODE

  217. EACH

  218. ELEMENT

  219. ELSE

  220. ELSEIF

  221. ENABLE

  222. ENCLOSED

  223. ENCODING

  224. ENCRYPTED

  225. END

  226. END-EXEC

  227. ENUM

  228. EQUALS

  229. ERRLVL

  230. ESCAPE

  231. ESCAPED

  232. EVERY

  233. EXCEPT

  234. EXCEPTION

  235. EXCLUDE

  236. EXCLUDING

  237. EXCLUSIVE

  238. EXEC

  239. EXECUTE

  240. EXISTING

  241. EXISTS

  242. EXIT

  243. EXP

  244. EXPLAIN

  245. EXTERNAL

  246. EXTRACT

  247. FALSE

  248. FETCH

  249. FIELDS

  250. FILE

  251. FILLFACTOR

  252. FILTER

  253. FINAL

  254. FIRST

  255. FLOAT

  256. FLOAT4

  257. FLOAT8

  258. FLOOR

  259. FLUSH

  260. FOLLOWING

  261. FOR

  262. FORCE

  263. FOREIGN

  264. FORTRAN

  265. FORWARD

  266. FOUND

  267. FREE

  268. FREETEXT

  269. FREETEXTTABLE

  270. FREEZE

  271. FROM

  272. FULL

  273. FULLTEXT

  274. FUNCTION

  275. FUSION

  276. G

  277. GENERAL

  278. GENERATED

  279. GET

  280. GLOBAL

  281. GO

  282. GOTO

  283. GRANT

  284. GRANTED

  285. GRANTS

  286. GREATEST

  287. GROUP

  288. GROUPING

  289. HANDLER

  290. HAVING

  291. HEADER

  292. HEAP

  293. HIERARCHY

  294. HIGH_PRIORITY

  295. HOLD

  296. HOLDLOCK

  297. HOST

  298. HOSTS

  299. HOUR

  300. HOUR_MICROSECOND

  301. HOUR_MINUTE

  302. HOUR_SECOND

  303. IDENTIFIED

  304. IDENTITY

  305. IDENTITY_INSERT

  306. IDENTITYCOL

  307. IF

  308. IGNORE

  309. ILIKE

  310. IMMEDIATE

  311. IMMUTABLE

  312. IMPLEMENTATION

  313. IMPLICIT

  314. IN

  315. INCLUDE

  316. INCLUDING

  317. INCREMENT

  318. INDEX

  319. INDICATOR

  320. INFILE

  321. INFIX

  322. INHERIT

  323. INHERITS

  324. INITIAL

  325. INITIALIZE

  326. INITIALLY

  327. INNER

  328. INOUT

  329. INPUT

  330. INSENSITIVE

  331. INSERT

  332. INSERT_ID

  333. INSTANCE

  334. INSTANTIABLE

  335. INSTEAD

  336. INT

  337. INT1

  338. INT2

  339. INT3

  340. INT4

  341. INT8

  342. INTEGER

  343. INTERSECT

  344. INTERSECTION

  345. INTERVAL

  346. INTO

  347. INVOKER

  348. IS

  349. ISAM

  350. ISNULL

  351. ISOLATION

  352. ITERATE

  353. JOIN

  354. K

  355. KEY

  356. KEY_MEMBER

  357. KEY_TYPE

  358. KEYS

  359. KILL

  360. LANCOMPILER

  361. LANGUAGE

  362. LARGE

  363. LAST

  364. LAST_INSERT_ID

  365. LATERAL

  366. LEADING

  367. LEAST

  368. LEAVE

  369. LEFT

  370. LENGTH

  371. LESS

  372. LEVEL

  373. LIKE

  374. LIMIT

  375. LINENO

  376. LINES

  377. LISTEN

  378. LN

  379. LOAD

  380. LOCAL

  381. LOCALTIME

  382. LOCALTIMESTAMP

  383. LOCATION

  384. LOCATOR

  385. LOCK

  386. LOGIN

  387. LOGS

  388. LONG

  389. LONGBLOB

  390. LONGTEXT

  391. LOOP

  392. LOW_PRIORITY

  393. LOWER

  394. M

  395. MAP

  396. MATCH

  397. MATCHED

  398. MAX

  399. MAX_ROWS

  400. MAXEXTENTS

  401. MAXVALUE

  402. MEDIUMBLOB

  403. MEDIUMINT

  404. MEDIUMTEXT

  405. MEMBER

  406. MERGE

  407. MESSAGE_LENGTH

  408. MESSAGE_OCTET_LENGTH

  409. MESSAGE_TEXT

  410. METHOD

  411. MIDDLEINT

  412. MIN

  413. MIN_ROWS

  414. MINUS

  415. MINUTE

  416. MINUTE_MICROSECOND

  417. MINUTE_SECOND

  418. MINVALUE

  419. MLSLABEL

  420. MOD

  421. MODE

  422. MODIFIES

  423. MODIFY

  424. MODULE

  425. MONTH

  426. MONTHNAME

  427. MORE

  428. MOVE

  429. MULTISET

  430. MUMPS

  431. MYISAM

  432. NAME

  433. NAMES

  434. NATIONAL

  435. NATURAL

  436. NCHAR

  437. NCLOB

  438. NESTING

  439. NEW

  440. NEXT

  441. NO

  442. NO_WRITE_TO_BINLOG

  443. NOAUDIT

  444. NOCHECK

  445. NOCOMPRESS

  446. NOCREATEDB

  447. NOCREATEROLE

  448. NOCREATEUSER

  449. NOINHERIT

  450. NOLOGIN

  451. NONCLUSTERED

  452. NONE

  453. NORMALIZE

  454. NORMALIZED

  455. NOSUPERUSER

  456. NOT

  457. NOTHING

  458. NOTIFY

  459. NOTNULL

  460. NOWAIT

  461. NULL

  462. NULLABLE

  463. NULLIF

  464. NULLS

  465. NUMBER

  466. NUMERIC

  467. OBJECT

  468. OCTET_LENGTH

  469. OCTETS

  470. OF

  471. OFF

  472. OFFLINE

  473. OFFSET

  474. OFFSETS

  475. OIDS

  476. OLD

  477. ON

  478. ONLINE

  479. ONLY

  480. OPEN

  481. OPENDATASOURCE

  482. OPENQUERY

  483. OPENROWSET

  484. OPENXML

  485. OPERATION

  486. OPERATOR

  487. OPTIMIZE

  488. OPTION

  489. OPTIONALLY

  490. OPTIONS

  491. OR

  492. ORDER

  493. ORDERING

  494. ORDINALITY

  495. OTHERS

  496. OUT

  497. OUTER

  498. OUTFILE

  499. OUTPUT

  500. OVER

  501. OVERLAPS

  502. OVERLAY

  503. OVERRIDING

  504. OWNER

  505. PACK_KEYS

  506. PAD

  507. PARAMETER

  508. PARAMETER_MODE

  509. PARAMETER_NAME

  510. PARAMETER_ORDINAL_POSITION

  511. PARAMETER_SPECIFIC_CATALOG

  512. PARAMETER_SPECIFIC_NAME

  513. PARAMETER_SPECIFIC_SCHEMA

  514. PARAMETERS

  515. PARTIAL

  516. PARTITION

  517. PASCAL

  518. PASSWORD

  519. PATH

  520. PCTFREE

  521. PERCENT

  522. PERCENT_RANK

  523. PERCENTILE_CONT

  524. PERCENTILE_DISC

  525. PLACING

  526. PLAN

  527. PLI

  528. POSITION

  529. POSTFIX

  530. POWER

  531. PRECEDING

  532. PRECISION

  533. PREFIX

  534. PREORDER

  535. PREPARE

  536. PREPARED

  537. PRESERVE

  538. PRIMARY

  539. PRINT

  540. PRIOR

  541. PRIVILEGES

  542. PROC

  543. PROCEDURAL

  544. PROCEDURE

  545. PROCESS

  546. PROCESSLIST

  547. PUBLIC

  548. PURGE

  549. QUOTE

  550. RAID0

  551. RAISERROR

  552. RANGE

  553. RANK

  554. RAW

  555. READ

  556. READS

  557. READTEXT

  558. REAL

  559. RECHECK

  560. RECONFIGURE

  561. RECURSIVE

  562. REF

  563. REFERENCES

  564. REFERENCING

  565. REGEXP

  566. REGR_AVGX

  567. REGR_AVGY

  568. REGR_COUNT

  569. REGR_INTERCEPT

  570. REGR_R2

  571. REGR_SLOPE

  572. REGR_SXX

  573. REGR_SXY

  574. REGR_SYY

  575. REINDEX

  576. RELATIVE

  577. RELEASE

  578. RELOAD

  579. RENAME

  580. REPEAT

  581. REPEATABLE

  582. REPLACE

  583. REPLICATION

  584. REQUIRE

  585. RESET

  586. RESIGNAL

  587. RESOURCE

  588. RESTART

  589. RESTORE

  590. RESTRICT

  591. RESULT

  592. RETURN

  593. RETURNED_CARDINALITY

  594. RETURNED_LENGTH

  595. RETURNED_OCTET_LENGTH

  596. RETURNED_SQLSTATE

  597. RETURNS

  598. REVOKE

  599. RIGHT

  600. RLIKE

  601. ROLE

  602. ROLLBACK

  603. ROLLUP

  604. ROUTINE

  605. ROUTINE_CATALOG

  606. ROUTINE_NAME

  607. ROUTINE_SCHEMA

  608. ROW

  609. ROW_COUNT

  610. ROW_NUMBER

  611. ROWCOUNT

  612. ROWGUIDCOL

  613. ROWID

  614. ROWNUM

  615. ROWS

  616. RULE

  617. SAVE

  618. SAVEPOINT

  619. SCALE

  620. SCHEMA

  621. SCHEMA_NAME

  622. SCHEMAS

  623. SCOPE

  624. SCOPE_CATALOG

  625. SCOPE_NAME

  626. SCOPE_SCHEMA

  627. SCROLL

  628. SEARCH

  629. SECOND

  630. SECOND_MICROSECOND

  631. SECTION

  632. SECURITY

  633. SELECT

  634. SELF

  635. SENSITIVE

  636. SEPARATOR

  637. SEQUENCE

  638. SERIALIZABLE

  639. SERVER_NAME

  640. SESSION

  641. SESSION_USER

  642. SET

  643. SETOF

  644. SETS

  645. SETUSER

  646. SHARE

  647. SHOW

  648. SHUTDOWN

  649. SIGNAL

  650. SIMILAR

  651. SIMPLE

  652. SIZE

  653. SMALLINT

  654. SOME

  655. SONAME

  656. SOURCE

  657. SPACE

  658. SPATIAL

  659. SPECIFIC

  660. SPECIFIC_NAME

  661. SPECIFICTYPE

  662. SQL

  663. SQL_BIG_RESULT

  664. SQL_BIG_SELECTS

  665. SQL_BIG_TABLES

  666. SQL_CALC_FOUND_ROWS

  667. SQL_LOG_OFF

  668. SQL_LOG_UPDATE

  669. SQL_LOW_PRIORITY_UPDATES

  670. SQL_SELECT_LIMIT

  671. SQL_SMALL_RESULT

  672. SQL_WARNINGS

  673. SQLCA

  674. SQLCODE

  675. SQLERROR

  676. SQLEXCEPTION

  677. SQLSTATE

  678. SQLWARNING

  679. SQRT

  680. SSL

  681. STABLE

  682. START

  683. STARTING

  684. STATE

  685. STATEMENT

  686. STATIC

  687. STATISTICS

  688. STATUS

  689. STDDEV_POP

  690. STDDEV_SAMP

  691. STDIN

  692. STDOUT

  693. STORAGE

  694. STRAIGHT_JOIN

  695. STRICT

  696. STRING

  697. STRUCTURE

  698. STYLE

  699. SUBCLASS_ORIGIN

  700. SUBLIST

  701. SUBMULTISET

  702. SUBSTRING

  703. SUCCESSFUL

  704. SUM

  705. SUPERUSER

  706. SYMMETRIC

  707. SYNONYM

  708. SYSDATE

  709. SYSID

  710. SYSTEM

  711. SYSTEM_USER

  712. TABLE

  713. TABLE_NAME

  714. TABLES

  715. TABLESAMPLE

  716. TABLESPACE

  717. TEMP

  718. TEMPLATE

  719. TEMPORARY

  720. TERMINATE

  721. TERMINATED

  722. TEXT

  723. TEXTSIZE

  724. THAN

  725. THEN

  726. TIES

  727. TIME

  728. TIMESTAMP

  729. TIMEZONE_HOUR

  730. TIMEZONE_MINUTE

  731. TINYBLOB

  732. TINYINT

  733. TINYTEXT

  734. TO

  735. TOAST

  736. TOP

  737. TOP_LEVEL_COUNT

  738. TRAILING

  739. TRAN

  740. TRANSACTION

  741. TRANSACTION_ACTIVE

  742. TRANSACTIONS_COMMITTED

  743. TRANSACTIONS_ROLLED_BACK

  744. TRANSFORM

  745. TRANSFORMS

  746. TRANSLATE

  747. TRANSLATION

  748. TREAT

  749. TRIGGER

  750. TRIGGER_CATALOG

  751. TRIGGER_NAME

  752. TRIGGER_SCHEMA

  753. TRIM

  754. TRUE

  755. TRUNCATE

  756. TRUSTED

  757. TSEQUAL

  758. TYPE

  759. UESCAPE

  760. UID

  761. UNBOUNDED

  762. UNCOMMITTED

  763. UNDER

  764. UNDO

  765. UNENCRYPTED

  766. UNION

  767. UNIQUE

  768. UNKNOWN

  769. UNLISTEN

  770. UNLOCK

  771. UNNAMED

  772. UNNEST

  773. UNSIGNED

  774. UNTIL

  775. UPDATE

  776. UPDATETEXT

  777. UPPER

  778. USAGE

  779. USE

  780. USER

  781. USER_DEFINED_TYPE_CATALOG

  782. USER_DEFINED_TYPE_CODE

  783. USER_DEFINED_TYPE_NAME

  784. USER_DEFINED_TYPE_SCHEMA

  785. USING

  786. UTC_DATE

  787. UTC_TIME

  788. UTC_TIMESTAMP

  789. VACUUM

  790. VALID

  791. VALIDATE

  792. VALIDATOR

  793. VALUE

  794. VALUES

  795. VAR_POP

  796. VAR_SAMP

  797. VARBINARY

  798. VARCHAR

  799. VARCHAR2

  800. VARCHARACTER

  801. VARIABLE

  802. VARIABLES

  803. VARYING

  804. VERBOSE

  805. VIEW

  806. VOLATILE

  807. WAITFOR

  808. WHEN

  809. WHENEVER

  810. WHERE

  811. WHILE

  812. WIDTH_BUCKET

  813. WINDOW

  814. WITH

  815. WITHIN

  816. WITHOUT

  817. WORK

  818. WRITE

  819. WRITETEXT

  820. X509

  821. XOR

  822. YEAR

  823. YEAR_MONTH

  824. ZEROFILL

  825. ZONE

String concatenations

Always use a space between the dot and the concatenated part, unless it is a quote. So there is no space between the dot and the quote.
Examples:

<?php

  $string
= 'Foo'. $bar;
 
$string = $bar .'foo';
 
$string = bar() .'foo';
?>

When you concatenate simple variables, you can use double quotes and add the string inside, otherwise use single quotes.
Example:
<?php
$string
= "Foo $bar";
?>

Use of icons

When you include icons, take a few guidelines into consideration:

  • For contributions: Save the icons in your module's directory
  • For core: Save the icons in the /misc directory
  • Use the Tango icons whenever possible
  • If no icons exist, refer to the icon style guidelines of Tango

Note that you we cannot host Tango icons in our CVS due to licensing problems. You will have to download them from their site, install them yourself, and comply with their licensing.

Write E_ALL compliant code

Introductory notice

This document is a very recent addition to the coding guidelines, and is still subject to discussion among developers. Do NOT post comments on this page yet. One issue will be created for each point introduced here. Those issues will be used for discussion and patch submission. I'll keep updating this page as the discussion progresses. When a consensus has been reached on each point, this introduction will be deleted. Eventually, when everything has been patched up, I'll delete the mention about Drupal not being E_STRICT compliant.

E_ALL: a better practice

Currently, the Drupal code is not E_STRICT compliant. When running a Drupal site with E_ALL, each page view creates scores of error notices messages. Many developers agree that it would be good if the source of Drupal could be brought up to par with commonly accepted good practice.

The purpose of this document is twofold:

  1. to show common coding mistakes that prevent Drupal from being E_STRICT compliant.
  2. to set better coding guidelines to use in new code and patches.

Once those guidelines are accepted, it is only a matter of time and some developers' efforts before all the previous coding mistakes are patched up. Then, we will be able to run Drupal with the E_ALL directive.

Common coding mistakes and new coding practice

1- Use of if (isset($var)) or if (!empty($var))

(Note: this first issue is still under review by the developers. See discussion and patches here: http://drupal.org/node/34434 . This section will be updated according to the discussion there, until a broad consensus is reached and relevant patches committed).

If you want to test if an array has been set to any value, don't use:

<?php
if ($foo) {}
?>

but:
<?php

// either
if (isset($foo)) {} // $foo=0 (zero) and $foo= '' return TRUE
// or
if (!empty($foo)) {} // use this when 0 or '' are not expected
// and are not valid values for $foo.
?>

The difference between isset() and !empty() is that unlike !empty(), isset() will return TRUE even if the variable is set to an empty string or to the integer 0. In order to decide which one to use, consider whether 0 or '' are valid and expected values for your variable.

The following code is wrong:

<?php
function _form_builder($form, $parents = array(), $multiple = FALSE) {
 
// (...)
 
if ($form['#input']) {
   
// some code (...)
 
}
}
?>

Here, the variable $form is passed on to the function. If $form['#input'] has been set to any value, some code is executed. The problem is that testing this way outputs the following error message:

notice: Undefined index:  #input in includes/form.inc on line 194.

Even though the array $form is already declared and passed to the function, each array's index must be explicitly declared. The previous code should read:

<?php
function _form_builder($form, $parents = array(), $multiple = FALSE) {
 
// (...)
 
if (!empty($form['#input'])) {
   
// some code (...)
 
}
}
?>

Beware!

The function isset() returns TRUE when the variable is set to the integer 0, but FALSE if the variable is set to NULL. In some cases, is_null() is a better choice, especially when testing the value of a variable returned by an SQL query.

Testing for error notices

If you wish to help to clean up Drupal's code so that it complies with E_STRICT, you can set up a test site and in includes/common.inc change:

<?php
if ($errno & (E_ALL ^ E_NOTICE)) {
?>

to:
<?php
if ($errno & (E_ALL )) {
?>

Functions

Functions should be named using lower caps and words should be separated with an underscore. Functions should have the grouping/module name as a prefix, to avoid name collisions between modules.

Constants

Constants should always be all-uppercase, with underscores to separate words. Prefix constant names with the uppercased name of the module they are a part of.

Control structures

These include if, for, while, switch, etc. Here is an example if statement, since it is the most complicated of them:

<?php
if ((condition1) || (condition2)) {
 
action1;
}
elseif ((
condition3) && (condition4)) {
 
action2;
}
else {
 
defaultaction;
}
?>

Control statements should have one space between the control keyword and opening parenthesis, to distinguish them from function calls.

You are strongly encouraged to always use curly braces even in situations where they are technically optional. Having them increases readability and decreases the likelihood of logic errors being introduced when new lines are added.

For switch statements:

<?php
switch (condition) {
  case
1:
   
action1;
    break;

  case
2:
   
action2;
    break;

  default:
   
defaultaction;
    break;
}
?>

Header comment blocks

All Drupal source code files should start with a header containing the RCS $Id$ keyword:

<?php
// $Id: CODING_STANDARDS,v 1.1 2001/11/05 07:32:17 natrak Exp $

Note that everything after the starting $Id and before the closing $ is automatically generated by CVS - you shouldn't edit this manually. If you add a new file to CVS, just write // $Id$.

Writing secure code

Whether you're writing a PHP snippet or an entire module, it's important to keep your code secure.

Here's how you prevent three major security risks:

  1. Cross site scripting attacks by properly checking output
  2. SQL injection attacks by using the database abstraction layer
  3. Node access restrictions bypass by using db_rewrite_sql

To prevent Cross site scripting (XSS) attacks, read the How to handle text in a secure fashion page. To sum up that page: If something that you output is not surrounded by one of the various check_* functions, it is very likely that it's insecure.

Second, you need to utilize the database layer correctly. Never, ever write user data into your SQL. You need to read db_query docs on the syntax. Common and very insecure practice is to simply end your query with something like

db_query('SELECT foo FROM {table} t WHERE t.name = '. $_GET['user'])

Instead, try
db_query("SELECT foo FROM {table} t WHERE t.name = '%s' ", $_GET['user'])

A non-trivial example is when you want to list all nids that are in an array $content_types:

<?php
$args
= $content_types;
$placeholders = array_fill(0, count($args), "'%s'");
pager_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE n.type in ('. implode(',', $placeholders) .') AND n.status = 1 ORDER BY n.created DESC'), 10, 0, NULL, $args);
?>

We are using array_fill to create an appropriate number of placeholders and then we utilize the possibility to pass the arguments in one array. As there is a variable in the SQL query, we should be very cautious. But as we just created this variable, it's OK.

Another very important point to note: we are dealing with nodes and the node access mechanism kicks in via db_rewrite_sql so we are utilizing it. It's really easy and yet it's so often neglected!

So, once more; There are three kind of errors you need to avoid: XSS with proper checking, SQL injections with proper db_query usage and node access bypass by utilizing db_rewrite_sql.

HOWTO: Report a security issue

Contact the security team

If you discover a vulnerability in Drupal core or in a contributed module, please keep it confidential. Do not post it in the issue tracker but mail us at security@drupal.org. We will investigate your report and create a fix or ask a module maintainer to do so. When this fix is ready, we'll publish an advisory, urging users to upgrade.

Some bugs take a while to correct, mainly because we need to review the codebase for similar problems.

We kindly ask you to not disclose the vulnerability to anyone before the advisory is issued.

A good report

Please provide us with a detailled report. As a minimum we need:

  • Drupal version or module version.
  • CVS Id for modules that were available/downloaded prior to the new release system.
  • Steps to reproduce.

Credit

If you report a previously unknown vulnerability to the Drupal security team, you will be credited in the security announcement.

Input, the root of all evil

Input, whether it comes from visitors or servers is something you should handle with care.

Consider the following example, modified from W. J. Gilmore's A Programmer's Introduction to PHP 4.0. It takes an argument supplied by the user and stuffs it into a query that is subsequently run.

<?php
/** Example 1 - Insecure
  * SQL injection via $keyword
  */
$keyword = $_REQUEST['keyword'];
$query = "SELECT cust_id, cust_name, cust_email FROM customers WHERE category = '$keyword'";
$result = db_query($query);
?>

Just a pity that the code in example 1 enables the user to inject SQL statements into the query. What would happen if a visitor used the keyword '; DROP TABLE customers; -- ? The final (potentially devastating) query would read:
SELECT cust_id, cust_name, cust_email FROM customers WHERE category = ''; DROP TABLE customers; --'

Obviously, we need to handle user supplied data a bit more intelligently.

The Drupal philosophy - Escape or filter when appropriate

You may have found the following expert advice in text dealing with security problems: always validate input. The problem with a content management system as Drupal is that the input is not well defined. A lot of senseless advice is available too. One sometimes encounters recommendations to strip quotes from for example name fields (') to prevent SQL injection. Let's hope none of the users of such a system have the name O'Reilly.

Another frequently touted 'solution' is filtering on keywords. To prevent SQL injection one would prohibit SQL keywords as SELECT or DROP in content. Unfortunately, no user would be able to mention 'insert', 'select' and 'update', not uncommon in normal English. To make matters worse, Dutch users wouldn't be able to mention licorice (drop) anymore. Obviously not a very generic (or even sane) approach.

So what we really have to do is make sure that, regardless the data, its content can never be interpreted as SQL. To do this, we use the escaping functions provided by the database API. We do this escaping in the database layer and not directly after receiving input as it may not be the only escaping (or filtering) we have to do.

<?php
/** Example 2 - Insecure
  * SQL injection via $keyword
  * XSS via $keyword and (possibly) $row->cust_name, $row->cust_email
  */
$keyword = $_REQUEST['keyword'];
$query = "SELECT cust_id, cust_name, cust_email FROM customers WHERE category = '$keyword'";
$result = db_query($query);
echo
"<h2>$keyword</h2>":
while (
$row = db_fetch_object($result)) {
  echo
"$row->cust_name, $row->cust_email <br />";
}
?>

Example 2 — though small, riddled with vulnerabilites — demonstrates the issue with input filtering escaping; the $keyword has to be filtered or escaped twice for different purposes: Once to prevent SQL injection and once more to prevent cross site scripting (XSS) attacks. Applying both filters directly to the input $keyword would result in extraneous slashes showing up in output, or failed search attempted with &quot; in them.

The solution is to use an appropriate filter when needed. For example, just before sending plain text to the browser or mixing plain text with HTML, escape it with check_plain.

<?php
/** Example 2 - corrected
  */
$keyword = $_REQUEST['keyword'];
$query = "SELECT cust_id, cust_name, cust_email FROM customers WHERE category = '%s'";
$result = db_query($query, $keyword);
echo
'<h2>'. check_plain($keyword) .'</h2>':
while (
$row = db_fetch_object($result)) {
 
// Not very elegant, but making a point.
 
echo check_plain($row->cust_name) .','. check_plain($row->cust_email) .'<br />';
}
?>

Database access

Drupal provides several functions to send queries to the database. The canonical form is db_query. Always use functions provided by Drupal to access the database to guard against SQL injections attacks. However, just using the functions is not enough as the following example illustrates:

<?php
/** Example 1 - Insecure
  * SQL injection via $type
  * Display node titles of type $type (input supplied by the user via a form textfield)
  */
$result = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '$type'");

$items = array();
while (
$row = db_fetch_object($result)) {
 
$items[] = l($row->title, "node/{$row->nid}");
}
return
theme('item_list', $items);
?>

Example 1 displays a list of titles depending on the type argument supplied by a user. A list of page nodes will be retrieved when $type is page, a list of story nodes when $type is story. Unfortunately, the example is vulnerable to SQL injection.

The vulnerability can be used on databases with UNION support (MySQL 4.1+) to gain administrator access to the site by supplying as type: story' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1/*.

This will cause the following query to be executed:

SELECT n.nid, n.title FROM {node} n WHERE n.type = 'story' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1/*'

As the snippet will now display valid session ids for the administrator user account, an attacker can instruct his/her browser to use the id and have full permissions on the site.

Parametrized query prevents SQL injection

Preventing SQL injection is easy; db_query provides a way to use parametrized queries. Drupal's database functions replace the sprintf-like placeholders with the properly escaped arguments in order of appearance.

<?php
db_query
("SELECT n.nid FROM {node} n WHERE n.nid > %d", $nid);
db_query("SELECT n.nid FROM {node} n WHERE n.type = '%s'", $type);
db_query("SELECT n.nid FROM {node} n WHERE n.nid > %d AND n.type = '%s'", $nid, $type);
db_query("SELECT n.nid FROM {node} n WHERE n.type = '%s' AND n.nid > %d", $type, $nid);
?>

Valid placeholders are documented in the API documentation:
  • %d - integers
  • %f - floats
  • %s - strings, enclose in ''
  • %b - binary data, do not enclose in ''
  • %% - replaced with %

That leads us to a correction of Example 1:

<?php
/** Example 1 - Corrected
  * Display node titles of type $type (input supplied by the user via a form textfield)
  */
$result = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", $type);

$items = array();
while (
$row = db_fetch_object($result)) {
 
$items[] = l($row->title, "node/{$row->nid}");
}
return
theme('item_list', $items);
?>

File uploads, downloads and management

[This section is work in progress]

Allowing users to manage files is a potentially dangerous operation.

You need to make sure that users cannot

  • view arbitrary files.
  • delete arbitrary files.
  • overwrite 'critical' files.
  • upload and execute arbitrary files.
  • completely fill a device (or quotum).

Directories

For starters, always make sure that actions on uploaded files (upload, view, download, delete) are taking place in the 'files' directory or another designated directory. Beware that in the following code samples a hardcoded directory "files" is used to simplify the examples, in reality this directory is configurable.

Users have no business reading or deleting important system files (such as /etc/passwd or sites/default/settings.php). While the examples concentrate on deletion, keep in mind that reading arbitrary files is also undesirable.

<?php
/** Example 1 - Insecure
  * Arbitrary file deletion.
  * 
  * $file is path/filename (eg files/myfile.txt) provided by the user.
  */
file_delete($file);
?>

A malicious user can abuse the trust granted to him/her in Example 1 by providing filenames in different directories, such as /sites/default/settings.php. The attack is clearly limited by the permissions of the useraccount that executes Drupal (often the webserver) on these files.

<?php
/** Example 2a - Insecure
  * Arbitrary file deletion.
  *
  * $file is a filename (eg. myfile.txt) provided by the user.
  */
file_delete("files/$file");
?>
<?php
/** Example 2b - Insecure
  * Arbitrary file deletion.
  *
  * $file is path/filename (eg files/myfile.txt) provided by the user.
  */
// Check whether $file is files/file
if (strpos($file, "files/") === 0) {
 
file_delete($file);
}
?>

Both Example 2a and 2b try to mitigate the attack by either prepending the filename with a fixed directory or by checking whether the supplied path begins in files/. Both examples are vulnerable to attack with parent paths (..).

What would happen if a malicious user would provide Example 2a with ../sites/default/settings.php and Example 2b with files/../sites/default/settings.php as path?

Both would attempt to delete sites/default/settings.php.

To properly check the real path of a file, use the Drupal function file_check_location.

<?php
/** Example 3
  * No longer vulnerable to parent path (..) attacks.
  *
  * $file is path/filename (eg files/myfile.txt) provided by the user.
  */
// Check whether $file is files/file
if (file_check_location($file, 'files') {
 
file_delete($file);
}
?>

Handle text in a secure fashion

When handling and outputting text in HTML, you need to be careful that proper filtering or escaping is done. Otherwise there might be bugs when users try to use angle brackets or ampersands, or worse you could open up XSS exploits.

When handling data, the golden rule is to store exactly what the user typed. When a user edits a post they created earlier, the form should contain the same things as it did when they first submitted it. This means that conversions are performed when content is output, not when saved to the database (be sure to read the db_query() documentation on how to use the database API securely).

To help you see where checks are needed, it is handy to mentally 'color' in each string depending on which format its data is in. Is it plain-text, HTML, BBcode or Textile? Then, whenever you concatenate two strings, you need to make sure they are both in the same format. If they are not, an appropriate check, conversion or filtering must be applied.

User-submitted data in Drupal can be divided into three categories:

  1. Plain-text

    This is simple text without any markup. What the user entered is displayed exactly on screen as is, and is not interpreted in any form. This is generally the format used for single-line text fields.

    When outputting plain-text, you need to pass it through check_plain() before it can be put inside HTML. This will convert quotes, ampersands and angle brackets into entities, causing the string to be shown literally on screen in the browser.

    Most themable functions and APIs take HTML for their arguments, but there are a few which already have check_plain() in it for convenience:

    • (only for Drupal 5.0) t(): the placeholders (e.g. '%name' or '@name') are passed as plain-text and will be escaped when inserted into the translatable string. You can disable this escaping by using placeholders of the form '!name' (more info).
    • l(): the link caption should be passed as plain-text (unless overridden with the $html parameter).
    • menus: the menu item titles are plain-text.
    • theme('placeholder'): the placeholder text is plain-text.

    Some places require HTML which might not be obvious:

    • page titles set through drupal_set_title(). The page title is displayed in the HTML, where it makes sense to use tags like <em> for clarity. When the page title is displayed in the HTML tag however, all tags will be stripped out.
    • block titles passed in through hook_block(). For the same reason as the page title, using HTML here is commonly done.

    Note that functions which logically take 'data' and not 'output' will almost always take plain-text and require no escaping on your side. A good example is the value passed to form_ functions, e.g. a plain-text field's contents. What the user entered is exactly what you should pass to form_textfield() as the field's value. On the other hand, the field's title or description are passed as HTML so that markup can be used in them.

  2. Rich text

    This is text which is marked up in some language (HTML, Textile, etc). It is stored in the markup-specific format, and converted to HTML on output using the various filters that are enabled. This is generally the format used for multi-line text fields.

    All you need to do is pass the rich text to check_markup() and you'll get HTML returned, safe for outputting. You should also allow the user to choose the input format with a format widget through filter_form() and should pass the chosen format along to check_markup().

    Note that you must make sure that the author of a post is allowed to use a particular input format. As a safe-guard, check_markup() performs this check for the current user by default. However, because content is filtered on output, this is often not the person who originally wrote the content. In that case, you must disable this check by passing $check = false to check_markup(), and making sure that the format is being checked with filter_access() when the content is being submitted.

  3. Admin-only HTML

    As of Drupal 4.7 there is a third way of dealing with text. There are some places in the administration section where it is impractical to invoke the filter system (for rich text), but where some simple markup is desired, such as a link or some emphasis (so plain text is not acceptable).

    Examples include the mission statement, posting guidelines or forum descriptions.

    For such cases, you can use a regular text-area, and pass the text through filter_xss_admin() when you output it. This will allow most HTML tags to pass through, while still blocking possibly harmful script or styles.

URLs across Drupal require special handling in two ways:

  1. If you wish to put any sort of dynamic data into a URL, you need to pass it through urlencode(). If you don't, characters like '#' or '?' will disrupt the normal URL semantics. urlencode() will prevent this by escaping them with %XX syntax. Note that Drupal paths (e.g. 'node/123') are passed through urlencode() as a whole since Drupal 4.7 so you don't need to urlencode individual parts of it. This convenience does not apply to other parts of the URL like GET query arguments or fragment identifiers.
  2. When using user-submitted URLs in a hyperlink, you need to use check_url() rather than just check_plain(). check_url() will call check_plain(), but also perform additional XSS checks to ensure the URL is safe for clicking on.

Note that all Drupal functions which return URLs (url(), request_uri(), etc.) output plain URLs which have not been HTML escaped in any way (in other words, they are plain-text). Remember to use check_url() to escape them when outputting HTML (or XML). Don't use check_url() in situations where a real URL is expected, e.g. in the HTTP Location: ... header.

In practice

All the rules above can be summed up quite easily: no piece of user-submitted content should ever be placed as-is into HTML. If you are unsure of whether this is the case, you can always test it by submitting a piece of text like <u>xss</u> into your module's fields. If the text comes out underlined or mangles existing tags, you know you have a problem.

Here are some examples of good and bad code. $title, $body and $url are assumed to be user-submitted fields containing a title, a piece of marked up text and a URL respectively. They are fresh from the database and thus contain exactly what the user submitted without any changes.

Bad:
<?php print '<tr><td>$title</td><td>'; ?>
<?php print '<a href="/..." title="$title">view node</a>'; ?>

Good (the title is plain-text and may not be placed into HTML as is):
<?php print '<tr><td>'. check_plain($title) .'</td></tr>'; ?>
<?php print '<a href="/..." title="'. check_plain($title) .'">view node</a>'; ?>

Bad:
<?php print l(check_plain($title), url('node/'. $nid)); ?>

Good (l() already contains a check_plain() call by default):
<?php print l($title, url('node/'. $nid)); ?>

Bad:
<?php print '<a href="/$url">'; ?>
<?php print '<a href="/'. check_plain($url) .'">'; ?>

Good (URLs must be checked with check_url):
<?php print '<a href="/'. check_url($url) .'">'; ?>

Writing filters

When writing a filter which translates from another markup language into HTML, you need to ensure you don't open any holes yourself. Generally, the same rules apply: check URLs with check_url() and ensure no literal HTML can be injected by escaping appropriately using check_plain().

JavaScript

[This section is under construction].

A few general guidelines:

  • Do not rely on JavaScript for validation; users can disable JavaScript.
  • Do not assume that data sent to AJAX postback functions is sent by your JavaScript function.
  • Do not assume that data sent to or by a JavaScript function cannot be observed by the user.
  • Beware that certain DOM functions decode HTML entities. Do not reinsert those into a page without escaping.

Session IDs

Session support in PHP allows one to preserve data across subsequent accesses. A visitor accessing your website is assigned a unique ID, the so-called session ID. The session ID is stored in a cookie on the user side and sent to your website on every page request.

Drupal stores the session ID alongside user IDs in the database. On every page access Drupal receives the session ID from the visitor's browser. It then checks the session table to find the associated user ID. The user ID determines which permissions the visitor has on the site.

To keep the system secure it is imperative to keep the session ID secret. If you write a module you should never output session IDs so they can be read by other users. This would allow users to hijack the session of someone else.

Be aware that while your output may not be visible in a page, for example because you send it as part of an AJAX request, it can still be read when the user employs a sniffer such as Wireshark or Fiddler.

When to use db_rewrite_sql

For many site developers who haven't delved into the node access mechanism, the question arises- when do I need to use db_rewrite_sql in my queries?

The answer depends on whether you are writing a snippet to put in a single page/block on your specific site, or whether you are writing core or contribibutions module code that's supposed to be generally usable.

In the first case, if you are not using any access control modules or similar to control who can see content, then it may not be necessary (or even useful), but you may want to use it anyhow in case you install access control in the future.

In the latter case, you should use it essentially any time you are making a query, especially any content-related query (node, terms, comment, etc). The exception would be for queries doing internal module work but not showing any content to users (e.g. a cron task), or perhaps queries for administrative pages where the user is expected to already have full privileges and/or it is necessary to show an unfiltered list.

To get a better sense of this, look through the core code and observe when and where it is used. Note that for non-node queries you'll need to specify the table being used.

Note: in Drupal 4.6.x and before, node_load() would pass it's arguments though db_rewrite_sql. In 4.7.x and later this is not done in order to insure that all nodes are indexed for searching.

Contacted by the security team. Now what?

When the security team finds a vulnerability in your module or receives a report detailing so, you will be contacted and asked to fix it.

What you need to do

  • Review the report.
  • Review your module for similar vulnerabilites.
  • Fix the vulnerability.
  • Commit the fix using a nondescriptive commit message.
  • Inform the security team that your module is fixed. Provide version information as well.
  • Create a new release of your module (see below).
  • Keep the issue confidential until the security team releases a security announcement.

What the security team will do

  • Help you with questions.
  • Track progress.
  • Write an advisory.

If we have not received a reply within one month after contacting you, we will publish an advisory urging users to uninstall the affected module. The relevant project will be unpublished.

Coordinated release and announcement

When a vulnerability has been corrected you have to make a new release. This usually means an increase of the minor version, for example from 4.7.x-1.0 to 4.7.x-1.1. If you have never created proper releases you need to create the first release (4.7.x-1.0).

See Managing releases for background information on releases. Any release created to address a security problem should be classified as a Security update release using the Release type category when creating (or editing) the release node. See Types of releases for more information.

To make sure the release and announcement are published at the same time, contact the security team leader on one of the following IM-addresses when you are ready for release:

IM (ICQ #) : 437838193
IM (MSN/YH): drupal_secteam@yahoo.com
IM (Jabber): drupal-security@jabber.org

If you prefer IRC, contact the nick drupal-security on Freenode (irc.freenode.net).

Note that these IM addresses are for security issues or release coordination only and will not keep contact lists.

An alternative method exists:

  • Create an official release tag, but no release node.
  • Write the text for the release node.
  • Send the text of the release node and the tag to security at drupal.org.
  • We will create a release node and make sure the announcement is published.

CVS

CVS (the concurrent version system) is a tool to manage software revisions and release control in a multi-developer, multi-directory, multi-group environment. It comes in very handy to maintain local modifications.

Thus, CVS helps you if you are part of a group of people working on the same project. In large software development projects, it's usually necessary for more than one software developer to be modifying modules of the code at the same time. Without CVS, it is all too easy to overwrite each others' changes unless you are extremely careful.

In addition, CVS helps to keep track of all changes. Therefore, the CVS server has been setup to mail all CVS commits to all maintainers. Thus, it does not require any effort to inform the other people about the work you have done, and by reading the mails everyone is kept up to date.

CVS Introduction

The Drupal development community uses the Concurrent Versions System (CVS) to manage all the revisions of every file that makes up the Drupal system. If you are unfamiliar with CVS, check out this brief tutorial before reading on. There are also many handy CVS cheat sheets, containing lists of commonly used commands for easy reference. Additional links can be found at the Other CVS resources page.

The following general information describes how the Drupal project makes use of CVS to manage code.

Repository

Drupal has two repositories which can be checked out (downloaded to your local computer):

  • drupal [description] [browse]
    The core Drupal code, i.e. what is downloaded as 'Drupal'.
  • contributions [description] [browse]
    Modules, themes, translations, etc. supplied by contributors, i.e. all Drupal material that is not in the core.
Committing to the drupal repository

Only a few Drupal developers are able to make changes directly to the Drupal source code in the repository. All other users who want to include changes they've made while working on their local copy of Drupal must create a file showing the differences between their local version and the version at drupal.org. This file is known as a patch, and is sent to the rest of the development community by creating an issue in the Drupal core issue queue and attaching the patch file.

Committing to the contributions repository

Modules, themes or translations can be committed to the contributions repository at any time by users who have a CVS account on drupal.org. All additions to the contributions repository are represented by a project on drupal.org. Each project has a home page on this site, where maintainers can describe the project, classify it into various categories, and where users can download releases and submit issues (bugs, feature requests, etc).

If you do not have a CVS account at drupal.org, but want to contribute your changes to a given project, you can still create patches and attach them to issues in that project's issue queue.

Branches and Tags

One of the most important and powerful features of CVS is the ability to create tags and branches.

  • A branch is a way to isolate a set of changes to a group of files in the repository. You can make changes to files on a certain branch, and those modifications won't affect the same files on other branches. Drupal uses a different branch for each major version of Drupal core.
  • A tag is a name given to an exact set of revisions of a group of files. Every time there is an official release of either Drupal core or a specific contribution, that release has a unique version number that identifies it. Before this version is released, a CVS tag is created in the appropriate repository to mark the exact revision of every file that went into that release.

It might be helpful to use an analogy to think about branches and tags. Imagine a living tree as it grows. At some point early in the life of the tree, it has a trunk and maybe a few small twig-like branches. As it ages, the younger, small branches grow thicker and longer, and new branches split off higher up on the trunk. Sometimes, new branches even split off from older branches. Say you wanted to keep track of how the tree was growing, and you decided to tie little paper tags with some word written on them whenever a branch first split off, and periodically at the ends of branches, so you could see how far the branches grew over time. To help you make sense of it, you use red paper for the tags at the start of the branches (and the word on those was a silly name you used to uniquely identify that particular branch), while the tags on the end of the branches at different points use purple paper.

With this growing tree in mind, the branches on the tree would correspond to ... that's right... branches in CVS. The only difference is that in CVS, you can decide when and where new branches split off. The red paper tags at the start of each branch are known in CVS officially as branch tags, or more commonly, just branches. In CVS (just like on your tree) knowing the little word written on the red tag (the name of the branch tag) allows you to identify that particular branch. The purple-colored tags at the ends of branches are known in CVS as tags. Every tag (branch or regular) has to have a unique name.

To continue the analogy (and translate it into software) each branch on the tree corresponds to a different major version of Drupal core. As bugs are fixed in a given release, the branch where the changes are committed would be growing longer (more revisions on that branch). Whenever a release is made, we take the most recent changes of the files (the revisions on the end of the branch) and add a tag to identify it (a marker with a word on it, in this case, based on the version number of the release) and tie it around the end of the branch.

In the contributions repository, it is now possible for project maintainers to create multiple branches that are all compatible with the same version of Drupal core. This is an example of a new, younger branch splitting off of an older, more mature branch, instead of branches always coming off of the trunk of your tree. It's all still associated with the same basic branch (at least in terms of where it split from the trunk -- a given version of Drupal core) but there are now separate branches where you make independent changes (new features) that don't disturb the natural growth of the original branch (stable code for bug fixes and security patches). As always, you can tie your paper tags to the ends of these branches and label how far they have grown whenever you want (to make a new release).

For more information, you can read about how Drupal uses branches and tags. For technical details, you can read the section about tags and the section about branches in the CVS manual.

HEAD is the name that CVS uses to refer to the primary development branch in a repository. If you do not specify a revision for CVS commands like checkout or update, CVS will default to operating on the revisions in the HEAD of your repository. To continue the above analogy, the HEAD is the trunk of your tree.

CVS Usage Policy

The following content is in part sourced from contributions/README.txt in the Drupal CVS repository.

Jump to:

General

This is a free services provided by the Drupal developers for the Drupal
community. By using the repository you agree to the terms of use (see
the TERMS.txt file).

All code must be licensed under GNU/GPL. If there is a problem with this,
contact the repository administrators.

Always make sure there isn't already contributed code that does the same thing
as your code. Instead contact the other project maintainer and try and merge
the projects into one. Do not branch Drupal core modules/themes, instead make
patches against core and submit those for review. In case a patch is not
approved, or very unlikely to be approved, contact the contributions repository
administrators for advice.

In cases where another non-Drupal project is required DO NOT include that code
in the repository. Instead provide a link where the other code can be
downloaded and instructions on how to install it.

File locations

docs/
  Example imlementations of Drupal features, logos, marketing material.

modules/
  Contributed modules. Development versions go into HEAD, not to a sandbox.

profiles/
  Contributed installation profiles. At this time, only files ending in .profile or .txt
  are allowed to be committed, since installation profile maintainers should
  *NOT* include the code of the modules that are enabled by their profiles.
 
theme-engines/
  Engines on which at least one contributed theme is dependant.

themes/
  Contributed themes. Development versions go into HEAD, not to a sandbox.
  Alternate styles for existing themes should be submitted as subdirectories
  of that theme directory in this manner:  themes/main_theme/new_style.
 
theme-styles/
  Use of this folder is deprecated.

translations/
  Gettext Portable Object based Drupal interface translations.

sandbox/
  For any code that modifies the Drupal core a way that it makes little sense
  for it to be a stand alone patch in a project. Anything that is a stand
  alone module should go in modules/. All sandboxes should include a README.txt
  that explains what changes are available and the state of the code. The key
  is to have enough documentation so people who review the code don't spend
  hours doing it, and code clean enough that it can be committed without too
  many changes.

All code should comply to the coding standards. Any patch that fails to do this
will generally be ignored and treated as unfinished work.

Sandbox Guidelines

The sandbox is for use by Drupal developers with CVS access. It can be used to collaborate or share code that is related to the Drupal project -- whether it is core or contributed modules. Inappropriate uses of the sandbox include storing "personal code" -- for example, don't store your website in your sandbox.

CVS GUIs and clients

There are a number of CVS 'front-ends' or GUIs which aim to improve on the command-line tools of CVS. These tools are grouped here by operating system/platform.

Command-line CVS (Mac)

There are a number of good CVS clients for Mac OS X. However, to make the most direct use of the handbook documentation, you may wish to try using the command-line version of cvs though the terminal window utility.

The typical way to get CVS is from the Apple Xcode tools. You may have this on your install disk. Just insert the Install disk and look for it. Once installed you should run a Software Update in case there have been updates since the CD was created.

If you do not have the install disk you can also sign up for Apple's ADC, and download and install the latest version from Apple. Apple also has a tutorial on using the command-line cvs client.

Alternatively, if you use fink you can just 'fink install cvs' to get the binaries.

Cervisa (Linux/Unix)

for *nix, i highly recommend Cervisia.
it's a kde app, and will integrate into konqueror, but i prefer it stand alone.
>>Cervisa screenshot, showing branches on drupal's CHANGELOG.txt

and then configure it to use "kompare" for diff, and it looks real sexy:
>>Kompare screenshot (CHANGELOG.txt)

CVL (Mac)

For Mac users who are unused to the command line, CVS can at first look a bit daunting. Fortunately there is an application called CVL that provides control of CVS through a point and click interface. Most of the instructions for using CVL will also apply when using other applications to control CVS.

Setting up/step by step CVS

Here is a step-by-step guide to installing and setting up CVL.

  1. Install CVS. The first step of using any application is of course to install it - CVS is installed by default with the Apple Developer tools, so if you haven't installed these yet, download the latest version and install them. They also include a lot of other useful stuff like Project Builder and File Merge (Apple Developer Membership is required, but free).

  2. Install CVL

    The CVL package and complete installation instructions are available at http://www.sente.ch/software/cvl/

  3. Setup CVL to work with CVS.

    Set CVS to ignore the 'hidden' .DS_Store files which OS X creates in each folder. To do this you need to open the Terminal (Application->Utilities->Terminal), and type the following:

    cd

    takes you to root of your account

    pico

    opens the Pico text application

    .DS_Store

    specifies which file types you want CVS to ignore

    Now press the keys: Control and x at the same time

    This closes the document you've just written, it will ask you if you want to save it - press y for yes, then type in the name of the file .cvsignore (note the '.') and press return. You've finished with the Terminal, so you can quit it.

  4. Create a folder to put the CVS files into. The best place to do this is in the 'Sites' folder, to make it easy to use them through the Apache server built into your system. You can name the folder anything you want, my one is called 'drupal_cvs'.

  5. Step five open the CVL application, you now need to Checkout (download) the latest version of the Drupal CVS like this:

    Tools->Repositories->Show Repositories

    Click Add

    Choose a repository dialog box will appear.

    In Repository type choose pserver.

    CVS User: your Username (that you applied for Drupal CVS access with)

    Host: cvs.drupal.org

    Path: /cvs/drupal (main distro) or /cvs/drupal-contrib (contributions)

    Password: your password (that you applied for Drupal CVS access with)

    Click Add.

    Next go to Tools->Repositories->Show Repositories

    The Drupal repository is now listed in the Repositories window. Select it and press Checkout...

    Checkout Module dialog box appears.

    Choose Module: drupal (main distro) or contributions (contributions)

    New work area location: Choose... select the folder you created in step four.

    Press Checkout.

    Wait patiently, this may take some time, as the whole of the Repository needs to be downloaded - you can see this happening if you open the console window (Tools->Console->Show Console), don't worry if you don't see anything at first, CVL usually thinks about what it's doing for a minute or two before taking action.

    When this is finished you will have a copy of the Drupal repository files in the folder you created on your hard drive, this is your Work Area, where you work on projects before uploading them to the repository for others to use.

Basic CVS with CVL

You now have a Work Area on your hard drive which is a mirror of the Repository on the Drupal server. You can see this by using the CVL menu Work Area->Open Recent and selecting the repository you just downloaded (drupal or contributions).

You can use this work area in the same way you would any other folder on your hard drive - create new files with BBEdit (or whatever you use), drag files to the trash, add new folders, delete folders - it's just a regular folder.

Once you've done some work you want to upload back to the Drupal server here's what you do:

Update the CVS by selecting the folder the new work is in, then Control+Click on the folder and choose Update from the contextual menu that pops up (or through the menu File->Update). CVS now shows any new files or folders that you have added (with a blue * in front).

Next you need to tell CVS to mark the files and folders for upload next time you send your changes to the Drupal repository. To do this select the files and folders and Control+Click, choose Add To Work Area (or through the menu File->Add To Work Area).

To upload your work to the Drupal repository, select your files and folders and Control+Click, choose Commit... (or through the menu File->Commit...). CVS will now add your work to the Drupal repository.

Preparing a project

New module, theme or translation projects should be started in the Drupal CVS contributions repository.

In the Finder, go to the folder where you saved the CVS contributions working copy, and create a new folder in the appropriate subfolder. For example, if you are working on a new module, create new folder in the module folder. Name the new folder to whatever you want to call the project. Try to make the name short and descriptive. Avoid spaces, use "_" to separate words, but read the Developer guidelines to understand how underscores in module names may interact with code behaviour.

In CVL, open the CVS contributions work area, navigate to the folder containing the new folder you just created, control-click on it and select "Refresh" from the menu that pops up.

The new folder, and any files you put in it, should now show up in CVL with a blue * next to it.

The blue * signifies that the files have not been added to the work area yet.

In CVL select the new folder, control-click on it and select "Mark File(s) for Addition" from the menu that pops up.

The blue * will now change to a green + next to each of the new files and folders, signifying that the files are part of the working copy and can be added to the repository at drupal.org once you want to commit them.

Committing a project

Once your new module, theme or translation is complete you may want to add it to Drupal's contributed repository, and create a project for it at www.drupal.org.

  1. Add your project to CVS

    Your new project should first be added to the trunk of the contrib repository, which is know as the 'cvs' or 'HEAD' branch. (see Setting up)

    Add the files to the 'cvs' version of the contrib repository (see Preparing a project). Once added to CVS, the project folder and each of it's files will have a green '+' next to it, this means they are ready to be commited.

    Select the project's folder, for example:

    • banana.module
      contrib/modules/banana/banana.module
      select folder 'banana'

    With the project folder selected, control-click, select 'Commit...'

    A dialog box will appear into which you can type a log message. The log message should briefly explain what new features have been added to this version of the files or what bugs have been fixed.

  2. Add your project to the Drupal Project tracker

    Your files are now in the contrib repository, now you need to make drupal.org aware of your new project.

    By creating a 'project' at www.drupal.org the files in CVS become available for download on the 'Downloads' page, it also allows users to submit feature requests and bug reports for the project.

    Log in to www.drupal.org, in the side account block click on "create content", then click "project".

    Fill in the project form page to create the new project. The project will apear on drupal.org in a day or two.

Eclipse CVS plug-in (all platforms)

Perhaps the best CVS front-end I've used is the cross-platform tool Eclipse (www.eclipse.org). It has the functionality to do almost anything you can do with the command line, but you can use the console if you need to. As well, you can use it to edit the PHP code with another plugin.

Checkout Drupal CVS into the Eclipse Workspace

Eclipse is a good way for Developers to Contribute to Drupal, it has integrated CVS Support and a good PHP Plugin for full PHP4/PHP5 Support including Debugger, Syntax Higlighting, code completion and more.

Installing Eclipse with PHP Support

First you need a copy of eclipse. The Eclipse Homepage is http://www.eclipse.org.


The Installation is Easy. You need the Java SDK and the right Eclipse SDK package for your OS. Extract the zip File and start Eclipse.

For PHP Support you need PHP Eclipse.
Installation is the same as for eclipse, download the latest stable for your Eclipse Version and unzip it in yor Eclipse folder.

Attention: Please install PHP support before you start with your Projects, it makes anything easier for you.

Checkout Drupal in your Eclipse Workspace

The easiest way to get Drupal CVS into your Eclipse Workbench is the "Import from CVS" feature.

  1. Start Eclipse and open the File/Import... Dialog.

  2. Choose "Checkout Projects from CVS", click "Next" and "Create a new repository Location", click "Next"
  • For Anonymous Access to the Drupal CVS insert the following information:
    Host: cvs.drupal.org
    Repository Path: /cvs/drupal
    User: anonymous
    Password: anonymous
    Connection type: pserver
    Also make sure "Save Password" is checked.

    As a devolper for contrib you must type
    Repository Path: /cvs/drupal-contrib
    User: your username as filled out in the CVS acces form
    Password: your password

  • In the "checkout Module" Dialog select "Use an existing module" and coose the module you want, "drupal" for the Drupal Core, or "contribution" for the 3rd Party modules and themes. Click "Next"

  • .

  • Check out the Module with the "New Project Wizard" and select the Branch you want, this is usaly "HEAD", now press "Finish".

  • In the "New Project Wizard" Select PHP/PHP Project as Project Type. This Project type only appears if you have PHP Eclipse installed, if you don't have PHP Eclipse installed use "Simple/Project" instead.

  • Give the module an usefull Name like "drupal-contrib" and press finish.

  • The CVS Checkout starts, the contrib module take a long time (around 15 Minutes), so don't be impatient.

Eclipse team project set to checkout Drupal & contributions for 4.6, 4.7, 5.0 and CVS

Eclipse has a great feature to allow you to import a whole set of projects from CVS in one go.

To do this, simply:

1: Save the following as drupal.psf:

<?xml version="1.0" encoding="UTF-8"?>
<psf version="2.0">
<provider id="org.eclipse.team.cvs.core.cvsnature">
<project reference="1.0,:pserver:cvs.drupal.org:/cvs/drupal-contrib,contributions,drupal-contrib-head"/>
<project reference="1.0,:pserver:cvs.drupal.org:/cvs/drupal-contrib,contributions,drupal-contrib-4-6,DRUPAL-4-6"/>
<project reference="1.0,:pserver:cvs.drupal.org:/cvs/drupal-contrib,contributions,drupal-contrib-4-7,DRUPAL-4-7"/>
<project reference="1.0,:pserver:cvs.drupal.org:/cvs/drupal-contrib,contributions,drupal-contrib-5,DRUPAL-5"/>
<project reference="1.0,:pserver:cvs.drupal.org:/cvs/drupal,drupal,drupal-head"/>
<project reference="1.0,:pserver:cvs.drupal.org:/cvs/drupal,drupal,drupal-4-6,DRUPAL-4-6"/>
<project reference="1.0,:pserver:cvs.drupal.org:/cvs/drupal,drupal,drupal-4-7,DRUPAL-4-7"/>
<project reference="1.0,:pserver:cvs.drupal.org:/cvs/drupal,drupal,drupal-5,DRUPAL-5"/>
</provider>
</psf>

2: In Eclipse choose File -> Import -> Team -> Team Project Set and browse to find your drupal.psf file. Click finish, enter your CVS user/password (or anonymous/anonymous) and go do something else for an hour or two!

This will import the 4.6, 4.7, 5.0 and CVS versions of both Drupal and the contributions repository into separate projects. If you do not need all of these you can simply remove any unwanted lines from the drupal.psf file before importing.
Note these are all 'proper' checkouts and you can use them to test out and commit patches to your projects. Note that if you want to create release branches of your contrib projects you will need to check these out as separate projects.

How to: Install and configure Eclipse
Overview

Eclipse is a very capable open-source development platform that is available for Windows. This guide should help you get started.

Install Eclipse

Download the latest Eclipse SDK zip file from the Eclipse downloads page. To install, you simply need to unzip this file to the preferred location. There is no installer. To then run eclipse, find and execute eclipse.exe.

Setting up

When you first run Eclipse, there will be some nice graphics asking if you want a tour etc. After you have had your fill, follow the link to go to the workspace. At some point, you will asked for a workspace location. This is the location where Eclipse will place your checked-out files and modules.

Refer to the Coding standards section of the handbook for instructions on how to configure Eclipse for best coding practice.

Eclipse doesn't have a View > Blah option as you'd expect in other Windows programs. You should see a sidebar window called "Package Explorer", but if you don't, go to Window > Show View > Package Explorer.

Checking out

You can checkout code from the Drupal repository as an anonymous user:

  1. Right-click the Package Explorer window and select "Import". Be careful with import/export terminology, it can change direction depending on the software you're using. In this case Import = Checkout.
  2. In the first screen of the import wizard, open the CVS folder and click on "Projects from CVS".
  3. Create a new repository and fill in the details as follows:
    - Host: cvs.drupal.org
    - Repository Name: /cvs/drupal
    - User: anonymous
    - Password: anonymous
  4. In the next screen you can browse the repository. For example go to contributions > modules to checkout the user contributed modules. Select the module you'd like to import and click next.
  5. At this stage select "Checkout the project in the workspace" and type a descriptive name (usually just the name of the module).
  6. At this point you can simply click "Finish", but if you go a couple of screens further you will be able to select a specific branch.

This has been a rough beginner's guide to setting up Eclipse. Additional information about the PHP plugin and making patches will follow.

SmartCVS (all platforms)

SmartCVS is an innovative multi-platform CVS client. It has very powerful features, like built-in File Compare/Merge, Transaction display or List Repository Files, and still is easy and intuitive to use. SmartCVS focusses on your day-to-day tasks and usability and is not limited to the available CVS command set.

Don't waste time with learning command line options and installing several tools. Be productive from the first minute with an easy-to-learn graphical CVS client, which assists you where possible and contains all required functionality out-of-the-box.

See features, screenshots and download.

SmartCVS requires a Java Runtime Environment (JRE) 1.4.1 or newer to run.

There's two versions available. A free 'foundation' version and a commercial 'professional' edition.

It has the typical features, like built-in File Compare/Merge, Show Transactions or List Repository Files.

You can download the latest version from http://www.smartcvs.com/. The checkout / update process is similar to all of the CVS gui programs.

TortoiseCVS (Windows)

TortoiseCVS lets you work with files under CVS version control directly from Windows Explorer. It's freely available under the GPL. The following tutorial teaches how to use TortoiseCVS with Drupal.

  • Download TortoiseCVS from http://www.tortoisecvs.org/download.shtml and install it.
  • In Windows Explorer, select the folder under which you want the Drupal source directory to live. Right-click on it. There are two new sections in the context menu - CVS Checkout and CVS >. Select CVS Checkout.
  • Fill in the following fields:

    Protocol: Password server (:pserver:)
    Server: cvs.drupal.org
    Repository folder: /cvs/drupal (main distro) or /cvs/drupal-contrib (contributions)
    User name: anonymous
    Module:

    • drupal (for Drupal itelf),
    • contributions (for the entire contributions directory), or
    • contributions/path (ex: contributions/modules/event) to retrieve a specific module, theme, etc.
  • press "OK".
  • You will be asked for password. Enter anonymous and press "OK".
  • A log window which monitors the checkout process will appear. Checking out the whole contributions repository will take a while.
  • If everything works, you will see the message "Success, CVS operation completed" at the end of the log. A new directory (named like the module selected before) with the sources will be created.
  • To bring your Drupal source tree up-to-date, select it's root folder ("drupal" / "contributions"), right-click it and do a "CVS Update".

You can also generate patch files with TortoiseCVS. Just select the files which you have patched in Windows Explorer. Then right click into the CVS => Make Patch menu item. Then you may wish to read Creating and sending your patches

Retrieving an older version of Drupal or Drupal modules

The process above retrieves the freshest files from the repository (the so-called HEAD branch). These are sometimes unstable. To get Drupal modules and themes that are stable and ready for production (which you can also download from the Drupal downloads page), follow the process described above, but before hitting "OK" you need to:

  • Click on the "Revision" tab on the CVS checkout dialog.
  • Enable "Get tag/branch".
  • If this is the first time you've used this option, click "Update list..." to obtain a full list of Drupal branch numbers.
  • Select the branch you're interested in from the list. For example, Drupal 4.5 would be DRUPAL-4-5 and Drupal 4.6.3 would be DRUPAL-4-6-3.
  • Hit OK.

WinCVS (Windows/Mac)

WinCVS is another graphical CVS client available for MS Windows and for Macs. You can download the latest version from http://www.wincvs.org/. The checkout / update process is similar to the one described above.

Adding a new project

These instructions are written for WinCVS, but the concepts probably apply to Tortoise as well.

Assumptions: a working installation of WinCVS. A CVS account at drupal.org. A checked-out contributions/modules (or themes or whatever) directory with CVS directories intact giving the correct login (not anonymous) information.

Do not use Remote -> Import module. If you are used to working on Sourceforge this may take some getting used to - remember that Drupal contributions use common branches/tags so you don't want to be creating a whole new CVS module, just adding a subdirectory to the existing tree.

In fact it is easy to add your project, provided you have the contributions directory created when checking out with its CVS subdirectory intact. If you don't, just check out any project using the previous instructions to get it back. From then on the process is really simple.

Copy your project's directory into the contributions/modules directory (or contributions/themes or whatever). It should show up in the right hand pane of WinCVS without a tick in the folder icon. Select the folder in the right hand pane and select Modify -> Add. This will create the directory on the CVS server: you can then add and commit the files within your project.

Checking out a project using WinCVS

These instructions are written for WinCVS, but the concepts probably apply to Tortoise as well. They are intended for developers with a CVS account.

Assumptions: a working installation of WinCVS. A CVS account at drupal.org.

You may have checked out modules anonymously before. You will need to start again as the CVS Root definition (which is stored in each local copy of a checked out directory) needs to contain your login information. It is a good idea to start with an empty folder to keep things clean - I have a folder DrupalSandbox on my Desktop.

  1. Use View -> Browse location -> Change to set the local path to DrupalSandbox or whatever your folder is.
  2. Select Remote -> Checkout module
  3. On the Checkout settings tab, tick CVSROOT and click the '...' button to the right of the box.
  4. In Protocol select pserver, and in Repository path enter /cvs/drupal-contrib. Then under Keyword select username and then the Edit button to enter your username; do the same for password (this is of course the username and password you stated when requesting a CVS account). Select Hostname and enter cvs.drupal.org. Click OK.
  5. Now still in Checkout settings, under Module name and path on the server enter contributions/modules/image (or whatever). Other than CVSROOT the check boxes on this tab should all be unchecked.
  6. Click OK, and WinCVS should connect to the server and check out the code. It will put it in a subdirectory contributions/modules/image (or contributions/theme-engines/phptemplate or whatever). You can move the image directory to your DrupalSandbox folder if you want, but keep contributions/modules etc. for later.

You can now work with WinCVS to manage your local copy of the files, updating and committing as necessary.

Drupal CVS repositories

Main repository

There are two ways to access the latest Drupal sources in the main CVS repository. If you just want to have a quick look at some files, use the ViewCVS web interface. If you need the complete source tree to study and work with the code, follow these steps:

  • If you don't have it yet, install a recent copy of CVS. If you are on Windows, you may check CVS front ends for Windows. Mac OS X users may find the tutorial on the CVL front end for CVS in section CVL: point and click CVS helpful. Note that CVS uses port 2401 so you may need to open this on your firewall in order to perform these commands.
  • To check out the latest Drupal sources, run the command:
    $ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout drupal

    This will create a directory called drupal containing the latest drupal source tree (-z6: use gzip compression so the transfer takes less time. -d: specify the 'directory' where the files are located and how to access them).
  • To check out Drupal to a specific directory (for example, my_drupal):
    $ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d my_drupal drupal
  • Once you have a copy of the Drupal source tree, use
    $ cvs update -dP

    in the source root dir to update all files to their latest versions (-d: Create any (new) directories that exist in the repository if they're missing from the working directory. -P: Prune empty directories - directories that got removed in the repository will be removed in your working copy, too).
  • You can also use the command
    $ cvs -q update -dP

    to suppress informational messages related to the update (-q: quiet).

If you can't or don't want to use CVS, you can download nightly CVS snapshots from http://drupal.org/files/projects/drupal-cvs.tar.gz.

Other useful methods of performing the checkout are:

Checkout a specific Drupal version

$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal co -r DRUPAL-5 drupal

where DRUPAL-5 is one of the branches identified at the bottom of Using CVS with branches and tags.

Checkout Drupal from a specific date

$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal co -D "06 Oct 2005" drupal

Exporting Drupal without CVS folders

When you do a 'checkout' it checks out all files along with CVS files and folders, which enables you to do a 'cvs update' later. Sometimes, however, you want to take a copy of the code without the CVS files, for example to import into your own CVS repository. This can be done with the 'export' command:

cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal export -r HEAD drupal

That will give you a copy of Drupal without any tracking information or extra data to associate it back to CVS. Note that you can, of course, export any branch or date using the above switches.

Contributions repository

The Contributions repository is a seperate CVS repository where people can submit their modules, themes, translations, etc. See the contributions FAQ.txt and README.txt for more information.

Contributions repository CVS topics on this page:

Just like the Main repository, you can browse the contributions repository via the web interface. For anonymous (read-only) access, do the following:

  • To check out the latest drupal contributions, run the command:
    $ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout contributions

    The -z6 switch enables compression during the download. The -d switch indicates the location of the repository. pserver is the login method, the first anonymous is the password, and the second anonymous is the username. You'll be getting the repository from cvs.drupal.org, from the /cvs/drupal-contrib folder.
  • To check out only specific contributions, do
    $ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout contributions/directory/of/contrib

    where <directory/of/contrib> is the directory within contributions you want to check out. For example modules/views

  • It's often useful to check out a contrib module into a specific directory rather than the full contrib path. That allows you to, for instance, pull a single module directly from CVS into your sites/mysite.com/modules directory. To do so, use a command such as the following:
$ cd sites/mysite.com/modules

$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -d modulenamehere contributions/modules/modulenamehere
  • To check out contributions for a certain Drupal version, do
    $ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -r <version tag> contributions

    where <version tag> is one of the tags listed under "Q: How do I control the releases of my module/theme?" here.
  • If you want to add your own modules, themes, translations, etc., the Maintaining a project on drupal.org page has a useful introduction to getting started, while the Maintainer quick-start guide provides an easy reference to useful CVS commands.

Annotated CVS checkout example

The following is a typical checkout command for retrieving a copy of Drupal from CVS:

cvs -z7 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d drupal5 -r DRUPAL-5 drupal

Here is how this line breaks down:

cvs
Invokes the cvs command
-z7
Tells cvs to use compression when transferring the files. This can be from -z1 to -z9 with -z9 being the most compressed.
-d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal
Tells cvs:
  • Which type of authentication to use (pserver = password authenticating server)
  • The username to log in with (anonymous)
  • The password to log in with (anonymous)
  • The server where the CVS repository is located (cvs.drupal.org)
  • The location on the server where the CVS repository is located (/cvs/drupal)
checkout
The CVS command to execute; other examples are update or diff.
-d drupal5
The directory where you want cvs to put your files once it checks them out, regardless of the directory name used in the cvs repository itself.
-r DRUPAL-5
This option tells cvs what revision to use (in this case, we are telling to get files from the Drupal 5 branch).
drupal
The directory from the cvs repository you want.

Drupal CVS branches and tags

To manage the different Drupal versions, we use tags and branches.

A branch specifies a major Drupal version. For example, all 4.7.x versions of Drupal core belong in the DRUPAL-4-7 branch. The HEAD branch is special and is used to refer to the latest development version.

Whenever we release a specific version, we create a tag. A tag is a marker which defines a snapshot of all the files in the CVS at a certain moment. For example, the tag DRUPAL-4-7-4 specifies all files at the time of the Drupal core 4.7.4 release.

Overview of core branches and tags

To understand the naming conventions for the branches and tags used for Drupal core, you should already be familiar with the version numbers for core.

Branches

Each stable release series of Drupal core has its own branch. For example, the 4.7.x releases (4.7.0, 4.7.1, etc.) all live on the DRUPAL-4-7 branch, and the forthcoming 5.x stable releases will all live on a DRUPAL-5 branch. The naming convention is that if you strip off the last digit from any official version number for core, convert all periods (.) to hyphens (-) and add DRUPAL- to the front, you'll have the corresponding branch name that the release came from.

Official release tags

Every stable release of core has a corresponding tag in the CVS repository so that both developers and end users can always re-create the exact set of files that went into that release. The naming convention for these tags is as follows:
BranchName-PatchLevel

  • BranchName indicates what branch (and therefore, the release series, core API, etc) the release is from.
  • PatchLevel indicates the specific set of changes in that release (the final digit of the version number).

For example: the 4.7.4 release is tagged with DRUPAL-4-7-4 to show it is from the DRUPAL-4-7 branch, and has a patch level of 4.

Beta and release candidate tags

As explained in the Betas and release candidates section of the Version information handbook, Drupal core puts out preliminary releases before major new stable versions are released. The CVS tags that correspond with these releases are explained below:

Beta releases

Beta releases are always tagged in CVS, and the naming convention is:
DRUPAL-[Version]-BETA-[X]

  • [Version] indicates the numeric version of core the beta is a precursor to. As with regular branch and tag names, the periods (.) are replaced with hyphens (-).
  • [X] is a number that uniquely identifies the beta release (1, 2, etc.)

For example, the Drupal 5.0 beta 1 release is tagged with DRUPAL-5-0-BETA-1. The Drupal 4.7.0 beta 2 release is tagged with DRUPAL-4-7-0-BETA-2.

Release candidates

The CVS tags for release candidates have the form:
DRUPAL-[Version]-RC-[X]

  • [Version] indicates the numeric version of core the release candidate is about to become. As with regular branch and tag names, the periods (.) are replaced with hyphens (-).
  • [X] is a number that uniquely identifies the release candidate (1, 2, etc.)

For example, Drupal 4.7.0 release candidate 1 is tagged with DRUPAL-4-7-0-RC-1.

Overview of contributions branches and tags

To understand the naming conventions for the branches and tags used for Drupal contributions, you should already be familiar with the version numbers for contributions.

There are three types of CVS identifiers now used in the contributions repository:


Official release tags

All releases of contributions are created based on release tags. These tags determine the specific revisions of each file included in the release. The tag name itself corresponds with the version string for the release. The release tag naming convention is:
[CoreCompatibility]--[Major]-[PatchLevel]<-Extra>

  • [CoreCompatibility] indicates what release series of core (e.g. 4.7.x or 5.x) the release is compatible with. It uses the same form as the Drupal core branch for that core release series. For example: DRUPAL-4-7 or DRUPAL-5 (any value from the complete list of available Drupal core branches).
  • [Major] is an integer that indicates what major revision of the contribution the release is from. Most of the time, this will be 1 to indicate the initial stable release for a given version of core, but occasionally contribution maintainers might provide multiple versions that are all compatibile with the same version of core (e.g. stable and development versions). A maintainer might choose to use major revision 0 to identify releases from the HEAD development branch that are being ported to a new version of the core API, but which aren't yet stable enough to have their own branch.
  • [PatchLevel] is an integer that uniquely defines the specific release. The initial release for a given major number is patch level 0 and subsequent releases increase the patch level.
  • <-Extra> (optional) can be used (in rare cases) to specify additional identification for a release. For example: -BETA-1 to indicate the first beta release.

Some examples of tag names and the corresponding versions they represent should help clarify:

DRUPAL-4-7--1-0
The initial (patch-level 0) stable release (major revision 1) of a module compatible with any version of Drupal core 4.7.*. This would be version 4.7.x-1.0.
DRUPAL-4-7--2-1
An updated (patch-level 1) new-feature release (major revision 2) of a module compatible with any version of Drupal core 4.7.*. this would be version 4.7.x-2.1.
DRUPAL-5--0-1
An updated (patch-level 1) unstable release (major revision 0) of a module that's not done being ported to the 5.x core API. This would be version 5.x-0.1.
DRUPAL-5--1-0
The initial (patch-level 0) stable release (major revision 1) of a module compatible with any version of Drupal core 5.*. This would be version 5.x-1.0.


Stable branches

Once a contribution has been ported to a given version of the Drupal core API, the maintainer should create a branch that matches the branch name used by core. This is not technically required, but is definitely recommended for responsible project maintainers. These initial stable branches have the exact same format as Drupal core branches, for example DRUPAL-5 or DRUPAL-4-7.

Release from these branches must have a major revision number of 1. For example, the initial release from the DRUPAL-4-7 branch would be version 4.7.x-1.0.


Development and other branches

If a project maintainer wishes to provide a development branch for a given version of the core API, they would add a branch of the form:
[CoreCompatibility]--[Major]

  • [CoreCompatibility] indicates what release series of core the branch is compatible with. This is just like the first part of a release tag (explained above). For example: DRUPAL-4-7 or DRUPAL-5.
  • [Major] is an integer that indicates what major revision of the contribution the branch represents. In the case of development and other branches, the major version number must be 2 or higher, since the default stable branch for a given version of core (e.g. DRUPAL-4-7) corresponds with major revision 1, and by convention, releases with the major revision 0 (if used at all) are always from the CVS HEAD.

Again, some examples of these additional branch names should help clarify:

DRUPAL-4-6--2
The first additional branch of a module compatible with any version of Drupal core 4.6.* (by convention, for new feature development) This would be where versions 4.6.x-2.0, 4.6.x-2.1, etc. where released from.
DRUPAL-4-7--3
The second additional branch of a module compatible with any version of Drupal core 4.7.*. Perhaps after a period of developing new features in the 4.7.x-2.* series of a module, the maintainer decided to create a new stable branch and use that as the new default download of their module. This would be where versions 4.7.x-3.0, 4.7.x-3.1, etc. where released from.

List of core branches and tags

This page is a complete reference listing all of the branches and tags in the CVS repository for Drupal core. However, please remember that only the most recent versions of core are actually supported, so you should not check out older copies of core from this list and use it for a website. Instead, you should use the latest stable code as explained in the Version information handbook page. This page is primarily intended for contribution maintainers who want to know what the currently available stable branches are.

Available branches

The branches currently available in Drupal core are:

  • HEAD
  • DRUPAL-5
  • DRUPAL-4-7
  • DRUPAL-4-6
  • DRUPAL-4-5
  • DRUPAL-4-4
  • DRUPAL-4-3
  • DRUPAL-4-2
  • DRUPAL-4-1
  • DRUPAL-4-0
  • DRUPAL-3-0

Available tags

The tags currently available in Drupal core are:

  • DRUPAL-5-1
  • DRUPAL-5-0-RC-2
  • DRUPAL-5-0-RC-1
  • DRUPAL-5-0-BETA-2
  • DRUPAL-5-0-BETA-1
  • DRUPAL-5-0
  • DRUPAL-4-7-6
  • DRUPAL-4-7-5
  • DRUPAL-4-7-4
  • DRUPAL-4-7-3
  • DRUPAL-4-7-2
  • DRUPAL-4-7-1
  • DRUPAL-4-7-0-RC-4
  • DRUPAL-4-7-0-RC-3
  • DRUPAL-4-7-0-RC-2
  • DRUPAL-4-7-0-RC-1
  • DRUPAL-4-7-0-BETA-6
  • DRUPAL-4-7-0-BETA-5
  • DRUPAL-4-7-0-BETA-4
  • DRUPAL-4-7-0-BETA-3
  • DRUPAL-4-7-0
  • DRUPAL-4-6-9
  • DRUPAL-4-6-8
  • DRUPAL-4-6-7
  • DRUPAL-4-6-6
  • DRUPAL-4-6-5
  • DRUPAL-4-6-4
  • DRUPAL-4-6-3
  • DRUPAL-4-6-2
  • DRUPAL-4-6-11
  • DRUPAL-4-6-10
  • DRUPAL-4-6-1
  • DRUPAL-4-6-0
  • DRUPAL-4-5-8
  • DRUPAL-4-5-7
  • DRUPAL-4-5-6
  • DRUPAL-4-5-5
  • DRUPAL-4-5-4
  • DRUPAL-4-5-3
  • DRUPAL-4-5-2
  • DRUPAL-4-5-1
  • DRUPAL-4-5-0
  • DRUPAL-4-4-3
  • DRUPAL-4-4-2
  • DRUPAL-4-4-1
  • DRUPAL-4-4-0
  • DRUPAL-4-3-2
  • DRUPAL-4-3-1
  • DRUPAL-4-3-0
  • DRUPAL-4-2-0
  • DRUPAL-4-1-0
  • DRUPAL-4-0-0
  • DRUPAL-3-0-0

List of contributions branches

This page lists all of the branches that currently exist anywhere in the contributions repository. Not all modules or themes would have each of these branches, but if something is listed below, it at least exists somewhere in the repository. This list is potentially useful for people who like to checkout separate copies of the entire contributions repository for each branch. It is provided solely as a reference.

Also, please remember that not all versions of Drupal are supported, only the most recent ones are. So, just because a module has a DRUPAL-4-4 branch does not mean you should use it. Also, remember that contributions on one branch are not going to be compatible with a version of core from a different branch. Read the Version information handbook page for more about supported versions.

  • HEAD
  • DRUPAL-5-1
  • DRUPAL-5-0
  • DRUPAL-5--4
  • DRUPAL-5--3
  • DRUPAL-5--2
  • DRUPAL-5--1
  • DRUPAL-5--0-1
  • DRUPAL-5
  • DRUPAL-4-7-2
  • DRUPAL-4-7--4
  • DRUPAL-4-7--3
  • DRUPAL-4-7--2-0
  • DRUPAL-4-7--2
  • DRUPAL-4-7
  • DRUPAL-4-6
  • DRUPAL-4-5
  • DRUPAL-4-4
  • DRUPAL-4-3
  • DRUPAL-4-2
  • DRUPAL-4-1
  • DRUPAL-4-0
  • DRUPAL-3-0

Maintaining projects with CVS

This section covers how to use CVS to maintain your Drupal project (a module, translation, or theme), from applying for CVS access to creating project releases.

For an overview of the contribution process, please see the Maintaining a project on drupal.org section of the Contributing to Development section.

A one page Maintainer quick-start guide is also available, which focuses on CVS fundamentals.

Apply for contributions CVS access

When applying for a Drupal Contributions CVS account your application will be reviewed by one of the Drupal.Org CVS administrators. In order to progress your application the administrator will assess the application based upon the motivation message you supply.

The main features of that motivation message that assist the administrator are details of your proposed contributions. For example:-

  • Module contributions: If you wish to contribute a new module to Drupal.Org and the wider community please provide as much detail as possible regarding your modules functionalty and what new features the module will offer end users.
    It's important to note that CVS applications are often denied when an tendered module duplicates the features and functions of an existing module and in these cases applicants are referred to the module's maintainer to offer collaboration on improving that existing module.
  • Theme contributions: If you wish to contribute a theme please ensure your motivation message includes links to either (or both) screenshots of your intended theme contribution or a working demonstartion site that uses your theme.

If you are applying for a CVS account because another module maintainer has agreed for you to become a co-maintainer of their module, please ensure this agreement is publicly available by creating a issue in the project's issue queue that clearly shows the current maintainer agreeing to that fact. Under this condition just stating this in the motivation message alone is insufficient for your application to be progressed. Once you have created the issue and the current maintainer has commented on it please don't forget to place a link to that issue in your motivation message to make the CVS administrators lives a little easier.

More than 80% of all current CVS applications currently go through a dialogue whereby the CVS adminsitrator attempts to clarify the potential applicant's intended contribution. This is directly due to poorly written motivation messages and/or obvious duplicatory contributions. Well written motivation messages help to cut down this overhead so please be verbose when applying.

As a general rule of thumb if you motivation message is similar to "Drupal rocks and I want to give back to the community!" or other single line type motivation message then please don't be shocked when your application is declined (and yes, unfortunately that is a very common motivation message!).

Here is a list of common motivation messages that, more often that not, get declined/rejected. If your motivation message resembles one of these then please, think before applying.

  1. I would like to fix bugs I find as I go or add new features to existing modules. The correct way to leave feedback for a module is to use the patch issue/queue system. And, in fact, for Drupal Core, this is the only way. Please note, this motivation message is appearing in more and more CVS applications and given the administrive overhead of engaging applicants via email, future CVS applications incorporating this will be declined on the basis that you didn't read this page.
  2. Module 'Foo' no longer appears maintained so I have written a new version I would like to contribute. Realistically this forms duplication and Drupal.Org tries to avoid this where possible (see the previous link for why this is so). The best way forward in this situation is to provide feedback to the module with patches rather than strike out afresh. If the module appears abandoned and patches sit by without review of commit, use the Webmasters to ask to take over an orphaned module.
  3. Module 'Foo' is just too complex so I have written a much simpler version. This is just straight duplication of functionality and applications like this will almost certainly be declined. It is worth remembering that many modules start out simple also and, over time, grow into more complex systems. If you have the know how to write a "simple version" from scratch rather than work out how a complex module works then maybe your effort would be better spent making the existing complex module easier to use (improve it's UI, write documentation, etc).

In short, spend some time on your motivation message and read the guidelines on the application form iteself before hitting the Request CVS account button.

Once you are ready use this link to apply for a CVS account.

General guidelines for using Drupal's CVS contrib repository

Here are some guidelines and hints on using Drupal's contrib repository:

  • Upon committing a new module or theme, add a concise description of what the module does or how this theme is unique (e.g. fluid/fixed, tables/css, ...etc.)
  • Do not create a .tar.gz or .zip archive and commit it to the repository. CVS is designed to handle changes and revisions to individual files, and committing an archive defeats its very purpose.
  • Try to group your commits functionally. For example a change that affects several files has to be committed in a single commit. This allows others to know what has changed, roll it back if necessary, create patches, ...etc. Do not commit unrelated changes together in one commit, and do not commit the change to each file separately.
  • Do not commit every file separately when initially committing a module. Do a cvs add on all the files that need to go in, then do a cvs commit to check them into the repository.
  • If you are using a GUI client, check its default settings. For example Eclipse uses -kk which affects the CVS Id tag. Change that to -kkv.
  • Make sure that you always have a commit description for every commit you do. Provide history and give credit in your commit comments. Note that this info gets published at http://drupal.org/cvs.
  • If you are referencing another module or a 3rd party site in your description, it is best if you provide a proper link to it, not just a bare URL, or worse, just the name.
  • It is a good idea to keep up with new releases of Drupal and update your modules/themes for the new release as betas are available. However, do not tag your module/theme with the new tag until you have converted the HEAD version to the new release

Commit messages - providing history and credit

As a module or theme maintainer or contributor, you may be making commits to the Drupal contributions CVS repository on a regular basis, both for your own code and changes contributed by others. Appropriate comments along with those commit messages are key to providing context that others need to understand the development of your code.

Give details

Provide at least a quick summary of what is contained in the commit.

Reference the drupal.org issue, if available

If the change you're committing relates to an issue on drupal.org, give the issue number, preceded by a pound sign (#). This convention is interpreted by the CVS module on drupal.org to provide a link back to the issue.

Give credit

If others have contributed to the change you're committing, take the time to give them credit.

Example

A sample commit message:

#46746 by Matt: fixed inconsistent encoding of path aliases.
Fixes broken URLs on profile pages.

Longer, and more descriptive, for larger issues:

#[issue number] by [username].

A bug in [filename(s)] caused [short description of the problem]. This commit
solves this by:
* [first solution or change]
* [second solution or change]
* [third solution or change]
* Adding $foo as parameter to the API call module_function() in bar.

[Optional short note on the effects for code calling your APIs or functions,
or users using your modules.]

For other examples, have a look at commit messages as available on http://drupal.org/cvs.

Managing releases

Note: to understand how to make releases, you should already be familiar with the version number scheme and naming conventions for branches and tags for Drupal contributions.

Introduction to releases

At some point during while developing your module, theme, or translation, you will probably declare the project formally "done enough" and choose to contribute it to the community, so that they can download it from the Download section for their given Drupal version. This process is called creating a release of your project.

All releases of drupal.org projects are generated based on either a CVS tag or branch. Each release is only packaged for download and available as a version number in the issue queue once you have created a release node for it. Also, please note that you must create a project node before you add any CVS tags or branches to your contribution, or you will run into errors when you try to add the release nodes.

The following sections contain information on how to create releases for your projects.

Types of releases

There are two main types of releases:

In addition, you may categorize any release by giving it a Release type.


Official releases


An official release is created when a project maintainer specifically tags their module for release, thus indicating it as "stable" for a particular version of Drupal. Once files are tagged and an official release is created, the tarball generated will always point to the same group of files; just as downloading Drupal 4.7.4 will always point to the same files tagged DRUPAL-4-7-4.


Snapshot releases


Development snapshots are automatically rebuilt from the end of a CVS branch. All you have to do is make sure you have the right branch created (see below) and submit a single release node pointing to that branch. After that, there's nothing else you have to do. The packaging scripts will detect if there have been any changes made on that branch, and if so, a new package will be created (which will have a new package date, and md5 checksum, but not a new version number).

Since the code keeps changing, but the version string does not, development snapshots are problematic for everyone involved. Users have a moving target, it's hard to identify the exact code someone is running, issues that reference development snapshots are going to be harder to debug, etc. Therefore using development snapshots should be discouraged. The functionality to produce them only exists to give people an easy way to get the latest code if they want to help with testing and debugging. Responsible project maintainers should always provide real releases so that users of their contribution do not have to use development snapshots for production sites.


Release types


When creating or editing a release, you may specify a Release type. The possible values are:
Security update
This release contains a critical security update. Any release associated with this term should have a link to the corresponding security announcement in the release notes. Please see the Contacted by the security team. Now what? handbook page about the proper way to handle security-related releases.
Bug fixes
The release should be marked with this if it contains any non-security related bug fixes.
New features
In general, new features should only be added to development branches for your project, but any release that contains new features should have this term.
Release tags and version numbers

Tags are how you signal an official "release" of your module, so that users may download a set of files that don't change for use in production sites.


Official Release Tags

If you want to add the 4.7.x-1.0 release, (the initial, stable release compatible with Drupal 4.7.x), you need to create a tag on the DRUPAL-4-7 branch of your code called "DRUPAL-4-7--1-0". For example,

cd contributions/modules/your_module
cvs update -r DRUPAL-4-7 # updates to the end of the DRUPAL-4-7 branch
cvs tag DRUPAL-4-7--1-0 # creates the release tag for the "4.7.x-1.0" release.

If you want to add the 4.7.x-2.3 release, (a new feature release compatible with Drupal 4.7.x), you need to create a tag on the DRUPAL-4-7--2 branch of your code called "DRUPAL-4-7--2-3". For example,

cd contributions/modules/your_module
cvs update -r DRUPAL-4-7--2 # updates to the end of the DRUPAL-4-7--2 branch
cvs tag DRUPAL-4-7--2-3 # creates the release tag for the "4.7.x-2.3" release.


Version numbers

For a detailed discussion on version numbers, please see the Branches and tags for contributions page.

Branches

How you maintain the CVS branches for your projects is very important, since that allows you to properly manage changes. It also determines what snapshot releases are available. There are three kinds of branches to consider:

  • Stable branches that are compatible with a fixed version of Drupal core.
  • Development branches for adding new features that are compatible with an older version of Drupal core.
  • The HEAD branch, for active development and porting to the latest version of Drupal core.

Note that branching is restricted to the 'modules', 'themes', 'theme-engines' and 'translations' directories of the contributions repository. You can not branch your sandbox.

For more information on CVS branches see:
http://drupal.org/handbook/cvs/introduction#branches-and-tags
http://www.cvshome.org/docs/manual/cvs-1.11.6/cvs_5.html#SEC54
http://cvsbook.red-bean.com/cvsbook.html#Branches


Stable branches for a specific version of core

Once a contribution has been ported to a given version of the Drupal core API, you should create a new branch for it. This will give you a place to maintain your contribution so it remains compatible with that specific version of core. These stable branches should only be used for fixing bugs and applying security patches. This gives users of your module a stable code-base compatible with a fixed version of Drupal core to build their production sites.

These stable branches should match the branch names used by Drupal core. See Branches and tags for core for details on this or the list of Available Drupal core branches.

For example, to properly branch your code once it is compatible with the 5.x core API, perform the following in your local workspace:

Modules:
cvs tag -b DRUPAL-5 modules/mymodule
Themes:
cvs tag -b DRUPAL-5 themes/mytheme

All later updates on that released version need to be done from the selected branch and committed to that branch.


Development branches for new functionality

If you want to make a new branch off of your DRUPAL-4-7 branch for adding new features, you'd do this:

cd contributions/modules/your_module
cvs update -r DRUPAL-4-7 # updates to the end of the DRUPAL-4-7 branch
cvs tag -b DRUPAL-4-7--2 # creates the new branch


There are a few different approaches to how you can use the HEAD branch for your contribution, depending on how closely you want to "chase" the latest changes to the next version of core under development. In general, the first approach is the recommended one, but both have advantages depending on your needs and abilities.

Use it for the "new feature" branch for the current stable core

In this approach, you would only attempt to port your contribution to a new version of core once the API had stabilized and the code freeze was well under way (for example, once the release candidates were already out). Any bug fixes you make in your current stable branch should be committed to HEAD as well. Additionally, new features or any major changes to your code should be developed directly in HEAD. You can tag official releases in HEAD, but identify them as the development series for the previous version of core.

Eventually, you'll decide it is safe to update the module to the next version of core. At this point you should make a new branch for your previously developed features, before you start porting to the latest version of core. Once that branch was created, you would convert your module in HEAD, and when you consider it stable enough for an official release, you would create a new stable branch that was compatible with the new version of core.

For example, during the entire period when the 5.0 release was being developed, the HEAD of your module would remain compatible with the 4.7.x core API. Any bugs you fix on the DRUPAL-4-7 branch would be committed to HEAD, but you would also add new features and do other major changes there. Once you had a set of new features working well enough to release, you would add the DRUPAL-4-7--2-0 tag to a workspace checked out from HEAD and submit a release node pointing to this tag (which would be version 4.7.x-2.0). Time goes on, and you add more features, so you create the DRUPAL-4-7--2-1 tag for the 4.7.x-2.1 release and so on. At some point, you feel like the 5.x core API is stable enough to port your module to, so you add the DRUPAL-4-7--2 branch off the end of HEAD so you could continue to make 4.7.x-2.x releases if you needed to (for example, to fix a security hole in the 4.7.x-2.2 release). Now, you can update your module, and when it is stable, you add a DRUPAL-5 branch and start the whole process over again as the Drupal core 6.x API is developed in the core repository's HEAD branch.

Keep up with the version of core currently under development

This approach assumes a more active involvement with the development of the next version of Drupal core. Whenever changes are made in the HEAD of core, you would try to update your contribution's HEAD so that it continued to work with the new API or took advantage of the latest feature. In this case, if you wanted to add new features to your contribution that were compatible with the previous version of core, you would make a branch off of your previous stable branch. Bug fixes to the stable branch would be applied to the development branch (if it exists) and HEAD. New features would be applied only to the development branch and HEAD.

Since the core API usually changes a lot during the development cycle for a new version, this approach will often require more work for contribution maintainers. Also, it's less likely that patches for bug fixes against your current stable code would apply to the HEAD version, since you've already modified the code to stay current with the latest changes to core.

Release nodes
Creating a release node for your project
  1. Navigate to your project's page; for example, http://drupal.org/project/signup.
  2. Under the Releases heading, click Add new release. Note that you will only see this link if you are on the list of people with CVS access to the project.
  3. Select the CVS identifier you want to use from the list of available branches and tags that aren't yet associated with a release.
  4. If you chose to create a release from HEAD, you'll see an intermediary page which prompts you to indicate the Drupal version that this release series is compatible with.
  5. Finally, fill in the remaining details of your release, such as a Description and click Submit. Note that there are certain fields on this form that are calculated automatically and cannot be changed.

    Here you may also specify a Release type for your release. This indicates to others whether the release contains bug fixes, new features, or a security update (or some combination). This is very useful, as it helps people to evaluate whether or not they need to change to a particular release, and whether or not updating to a release is critical for security reasons. You can also go back and edit old releases to provide this information. See Types of releases for more detailed information.

    To help generate detailed and accurate release notes, you can use the cvs-release-notes.php script (which lives in the /contributions/tricks/cvs-release-notes directory of the contributions repository). You just checkout a local copy of your project, and in the root directory of your project's source code, you run the script and provide the name of the CVS tag for the previous release and the current release. It will parse all the CVS log messages between the 2 releases, and print out an HTML version of all unique commit messages, sorted chronologically, with any references to #xxxxx replaced with a link to that issue number. You will need to further edit this list for clarity, remove entries that aren't relevant for end users of your project to know about, and so on, but it should help get you started.

  6. You will receive a message to inform you that your release has been submitted. The new release will initially be unpublished; only after the packaging scripts run will it become published, at which time users will be able to download a tarball of your module, theme, or translation. The packaging scripts run every 5 minutes for official releases from release tags. Development snapshots (from the ends of CVS branches) are only packaged every 12 hours.
Things to watch
  • During the deployment of the release system on drupal.org, release nodes were automatically generated for all of the existing branches in the contributions repository that were being packaged (e.g. the old 4.7.0 versions of modules from the DRUPAL-4-7 branch, or the cvs versions of modules from HEAD). If your contribution existed as of 2006-11-11, you do not need to make release nodes for the snapshot releases from these branches, and you will not see them listed as available CVS identifiers when trying to add new release nodes.
  • You will only be able to create releases for projects if you are on the list of people with "CVS access" to that project.
  • Once a release has been added, you will not be able to delete the release, or to remove the corresponding CVS tag. If you don't like what's in the release, your only recourse is to make another, newer one.
  • If you found a bug that needs to be fixed in several releases of your project, make sure to commit the fix to the different branches unless you are no longer maintaining certain releases of your project.
  • You may only create releases for modules, themes, theme engines, and translations. Personal sandboxes, items in the documentation directory of the contributions repository, etc. won't be packaged.
Fixing releases

After you make an official release, sometimes you discover a bug that you want to immediately fix. Maybe you forgot to add the CVS release tag to all of the files in your project before you made the release node, and now the downloadable packaging is missing some files. Or perhaps, a last-minute change to an SQL query ended up breaking your module's support for PostgreSQL, but you didn't test that before you made the release. Maintainers might find themselves wishing they could move the CVS tag for their release, and start generating new packages from the new revisions of the affected files, but retain the same version number.

However, modifying a release after it exists is intentionally prevented by the release system. Once you release something, it's out in public and you can not take it back. Even if it just went out on the web, someone might have already downloaded it, or checked it out from CVS, and deployed it. It would be too confusing if once something was released, its contents might change without having a new unique version number. That's a large part of what made the old release "system" so difficult and confusing.

Once you make a release the only way to fix a problem is to make a new release that includes the fix.

Of course, users of your contribution will start to get annoyed if a new official release comes out every day for minor fixes, or whenever a major new release comes out, another release follows hours or days latter, which actually works. So, it's always a good idea to test an official release as thoroughly as possible before you create the release node and publish your release to the world. Maintainers should also consider making "beta" releases of their modules, if they want more input and testing from their community of users. Normally, maintainers should "batch" bug fixes in groups, and only make another official release once enough smaller things, or a few big things, are fixed. However, certain critical bugs and any security vulnerabilities warrant a new official release immediately, even if the last official release was a day (or potentially even hours) before.

Sandbox maintenance rules

  1. Always document your changes.
  2. Split different set of patches into different directories. It takes longer to find the set of files relating to one change if it is mixed in with 2 other patches.
  3. Keep the documentation current. Try to keep some track of your reasoning too. If I read in a README that change X wasn't a good idea after all it makes the reviewer wonder why.
  4. Document the status of your patch. It is important to know if this is an early test, or considered stable and workable by the author of the patch.
  5. All patches should be against the latest CVS version of Drupal, and include in the README when it was last synced.
  6. Don't use a sandbox for developing modules. There is a different directory structure for that.
  7. If your patch is 4 lines long don't bother to put it in a sandbox. Just file an issue against the Drupal project. Small patches are quick to check and find out if they work. Sandboxes should be for more extensive changes.
  8. Try to maintain patches in the sandbox. They are so much easier to check than compete files. If you are using CVS then you can use diff (cvs -H diff)
  9. Please make sure your script passes the code-style.pl script. It isn't perfect, and sometimes a bit too strict, but it will ensure some level of compliance with the coding standards.

Step-by-step: Create a CVS project

This is a basic how-to that walks you through the steps of using command line CVS to get your new module into Drupal's CVS. The Maintainer quickstart guide is similar and covers some additional, advanced tasks.

BACKGROUND INFO

CVS stands for "concurrent versioning system". This type of system, while a bit cumbersome, is necessary at a place like Drupal.org in order to keep all the different versions of things from getting mixed up. You will need to apply for a cvs account before being able to upload files using the cvs system. (though you can download them without a cvs account) There are many ways/tools to log onto CVS. After playing with a couple GUI-based CVS clients, unsuccessfully, I gave in and just used a unix shell/terminal/command line to do the job. (note CVS is actually an application which must be installed on your system to be able to use it - mac users need to install the developer tools cd to have this functionality available for them on the command line)

Before you follow these instructions, it is good practice to add a CVS Id tag to all files in your project. For PHP files, add them as a comment like this:

// $Id$

Other files can have the same tag, but the syntax for a comment can be different (for example, .css files must use /* $Id$ */ since // is not a valid comment, and .info files must use ; $Id$). In plain text files, (e.g. README.txt, etc.) you can use $Id$ placed anywhere in the file, although this is traditionally done at the top.

INSTRUCTIONS: (paste/type these commands at your shell prompt)

1. export CVSROOT=:pserver:cvsusername@cvs.drupal.org:/cvs/drupal-contrib

....and hit enter.

2. cvs login

...and hit return. You will be asked for your cvs account password. Enter it and hit enter.

3. cvs co -l contributions/modules (substitute "themes" for "modules" if you are creating a theme project)

....and press enter. You should see something like this returned: cvs checkout: Updating contributions/modules

4. mv path-to-your-local-cvs-project contributions/modules
(your local cvs project folder needs to reside in the proper folder of your local cvs application - search for your hard drive for "cvs" if you are unsure where this is!)

...and press enter.

5. cd contributions/modules/

(or contributions/themes if this is for a theme project)

...and press enter.

6. cvs add myprojectname

...and press enter. You should see a long list of all the files in your project directory listed with a "?" in front of them returned after you press enter.

7. cd myprojectname

...and press enter.

8. cvs add *

...and press enter.

. You should see a long list of all the files in your project directory listed with "cvs add " in front of them returned after you press enter. If your module has subdirectories, you will need to then cvs add each subdirectory and cvs add * inside each subdirectory as well.

9. cvs commit -m "Pertinient message about what you are committing"

...and press enter. You will see several messages returned. When they are finished - you are done.

NOW CREATE A PROJECT PAGE

Once you have sucessfully committed your files to the cvs repository you'll then need to create an actual project page. Please note that you must create a project node before you add any CVS tags or branches to your contribution, or you will run into errors when you try to make releases.

* Make sure that the 'Short project name' matches the directory name in the CVS repository. For example, the contributions/modules/my_module module has the short name my_module.

* Fill in the correct value for the CVS directory field to match the path in the repository. In this case, you should include everything after the contributions part of the directory name (for example /modules/my_module/).

* You should enable the issue tracker for your project to allow people will be able to file bugs against your project, add tasks, or request new features.

CREATE A RELEASE

In order for people to download your project, one or more "releases" will need to be created. Learn more about how to create releases at the Managing releases section of the CVS handbook pages. This is the final step necessary for your project to automatically show up on the modules/themes download page on Drupal.org. (it may take up to 12 hours as the Drupal.org packaging system has specific times which is creates the release tarballs)

UPDATING YOUR PROJECT FILES

To update the files in your cvs directory, when you edit your files, you can now run from that folder "cvs diff -u" 'to see what the changes are, and run "cvs commit" to send those changes to the server. Add new files as above "cvs add filename".

Troubleshooting common CVS problems

The following are some problems that can occur when using CVS, along with their fixes.

If you encounter problems while using CVS and your issue is not covered here, please submit a documentation issue.

If you are having trouble with your CVS account, please use the Drupal.org contact form to submit a message with the category Problems with CVS accounts.

CVS implications of moving core modules into their own directories

On 2006-07-12, the modules within the Drupal core repository were moved into their own directories. Previous to this change, all modules in the core Drupal distribution lived in a single directory: drupal/modules. Now, each module has its own directory (for example, drupal/modules/system), just like the modules from the contributions repository.

This change only affected the versions of each module living in the trunk of the CVS repository (also known as the "cvs" or "HEAD" version), which will eventually become Drupal version 4.8.x and beyond. The 4.7.x, 4.6.x, and all prior versions of these modules still exist in the old location. For the most part, the move will be completely transparent to Drupal users and developers. In general, everything will work perfectly if you use CVS commands like cvs log and cvs annotate for the same branch as the copy of the source you have checked out. However, there are a few areas where this move will be noticable when you attempt to query for historical information about a file on a different branch than your currently checked out workspace.

In all examples, modules/x/x.module will be a generic module in the new location, and modules/x.module will be something in the 4.7.x and older location.

  • cvs log will continue to work perfectly for viewing log messages on the same branch as the copy of the source that you check out. However, if you want to use cvs log to view log messages on a different branch than your currently checked out workspace, beware that if you're looking at modules/x/x.module you will not see log messages for commits that are done in the DRUPAL-4-7 or DRUPAL-4-6 branch after the files were moved to their new locations on 2006-06-20. Similarly, the copies in the old location (modules/x.module) will not have log messages that were commited to the HEAD from after the date the files were moved. A few examples might help clarify the situation:
    • If you've checked out a workspace from the HEAD of CVS, but you want to see the most recent cvs commits on the DRUPAL-4-7 branch on the node.module file, you'll want to cd into the modules directory itself, and run the following command:
      cvs log -rDRUPAL-4-7 node.module
      Even though that file doesn't exist in your local workspace, CVS knows how to display the log information to you.
  • Unfortunately, if you've checked out a workspace from the DRUPAL-4-7 branch, but you want to see the most recent cvs commits on the trunk, you're out of luck. The following will not work:
    cvs log -rTRUNK node/node.module
    Since there's no node directory in your workspace, the cvs log command will fail, even though the rest of the options are valid. In this case, your only option is to checkout another copy of the source directly from the TRUNK (so modules will be in modules/x/x.module and you can view the full history along the TRUNK in that workspace.
  • Similarly, the cvs annotate command will have the same sorts of problems when trying to use it on other branches than the currently checked-out workspace. It will not be able to annotate the source with the most recent changes made on a different branch than the one you've checked out (if the files were moved between the two branches you care about). So, cvs annotate -r DRUPAL-4-7 x.module will work fine in a DRUPAL-4-6 branch (assuming it's run from inside the modules directory). However, if you attempt to annotate relative to the TRUNK (which is the default behavior, even if you run it from a workspace that has been checked out of another branch) on the modules/x.module copy, you will only see changes made before 2006-06-20.
  • Normally, so long as you're trying to use CVS commands on the same branch as your current workspace, everything will work perfectly, and you'll probably never have to know that the move took place.

How do I delete a directory from CVS?

Answer: you can't.

There are only 2 ways to "remove" a directory from a cvs repository when people go to check something out:

  1. completely remove all traces of the old directory by directly removing it out of the repository's filesystem itself. this is dangerous, and evil, since you're loosing all the history for the old thing
  2. introduce cvs "modules" (ugh, can you say terminology confusion for drupal users? *sigh*), which is a whole can of complication we probably want to stay the hell away from. basically, these are symbolic names that tell cvs what group of files you want to work on together. so, we could define a cvs module called "CONTRIB-MODULES" that included all the things in contributions/modules/* that we wanted to consider "live". then, whenever someone wanted to checkout the entire contrib modules, they'd do: cvs co -r DRUPAL-4-6 CONTRIB-MODULES" and cvs would only give them the things that were included in the official "CONTRIB-MODULES" list. so, if we wanted to remove a dead contrib drupal module from the live list, we'd just take it out of the corresponding cvs module. however, this would suck for at least 4 reasons:
    1. massive confusion between cvs "modules" and drupal "modules"
    2. more work for the cvs admin(s) to maintain these cvs modules
    3. more confusion for people who'd have to learn to start using the new method for checking out from the contrib repository.
    4. we might decide we'd want different cvs modules per drupal branch, since there can be a very different set of modules that are valid from drupal version to drupal version. then, everyone would be confused between cvs modules and cvs branches and tags. :(

so, either you have to:

  1. live with it
  2. convince a cvs admin that brute-force option #1 is a good idea in your specific case
  3. choose a better name

sorry,
-derek

Resolving a 'sticky tag is not a branch' error

If you get errors such as:

  • cvs commit: sticky tag DRUPAL-x-x for file foo/bar/baz is not a branch
  • DRUPAL-x-x already exists on version x.x, NOT MOVING tag to branch x.x.x.x
  • then it means someone created a regular tag on a file or project, when they should have created a branch instead. This usually happens if someone forgot to use the -b parameter to cvs tag. A tag identifies a certain revision of a file at a specific point in time. You can not commit changes to a tag, only to the end of a branch. You can read the CVS Introduction handbook page for more information about CVS branches and tags.

    You can verify this problem by taking a look at the revision history of a file on http://cvs.drupal.org/. If the output says Tag: DRUPAL-x-x instead of Branch: DRUPAL-x-x, you need to correct it. You can also check on the command-line by using cvs status -v file/you/care/about and look at the Existing Tags section. If it includes a line for DRUPAL-x-x followed by something like "(tag: 1.4)" you need to fix it.

    Implications of the new release system

    Do not attempt to delete a release node to get over this problem.

    Unfortunately, this problem gets more complicated with the new release system in place. To keep users from doing harm to releases that already exist, there is an access check in place to prevent deleting tags that have release nodes pointing to them. However, this check also prevents users from converting a tag into a release. In this case, at step #3 below, you will see an error message like this:

    ** ERROR: You are not allowed to delete or move tags that have
    ** release nodes pointing to them. "DRUPAL-4-7" is being used by
    ** http://drupal.org/node/96631

    Eventually, the user interface for all of this will somehow be modified to make it possible to correct this problem yourself. In the mean time, the only option is to submit a support request asking for help (please set the component to "CVS"). Be sure to include the full path to the release node (the final line of the error message from cvs tag -d listed above). A CVS administrator can temporarily change the tag that the release node is pointing to so you'll be able to remove the tag. Once you've got the branch created, the administrator will have to change the release node to point it to the branch.

    Steps to solving this problem

    Once the release node that is pointing to the old tag has been temporarily modified, you can follow these steps to remove the tag and create a branch from the same set of files and revisions. All earlier revisions history will be kept. We will assume Drupal 4.7 below.

    1. Make sure the release node is no longer pointing to your old tag (see above).
    2. cvs checkout -r DRUPAL-4-7 modules/mymodule
      (get a copy based on the old, non-branch tag)
  • cd modules/mymodule
    (DON'T FORGET THIS!)
  • cvs tag -d DRUPAL-4-7
    (delete the existing, non-branch tag)
  • cvs tag -b DRUPAL-4-7
    (add the new branch tag to the same revisions of the files you've checked out in step #1)
  • cvs update -r DRUPAL-4-7
    (to reset the sticky tag on your workspace to the new branch tag)
  • Verify if the revisions history on http://cvs.drupal.org/ now says Branch: DRUPAL-4-7 or if cvs status -v modules/mymodule/mymodule.module now shows "DRUPAL-4-7 (branch: x.x.x)".

Why drupal.org doesn't host GPL-"compatible" code

While technically the GPL permits inclusion of code with GPL-"compatible" licenses in a GPL package as explained here, the Drupal policy is not to mix licenses. Drupal founder Dries explains this policy as follows:

Drupal.org's package management system automatically adds the GPL license to all packages. If we allow other licenses in CVS, it is going to get messy, and sooner or later we're going to run into licensing issues. Already, we get quite a few questions about Drupal's license. If we are going to add more licenses to the mix, it is going to be harder to audit, or provide answers to such questions. So, not allowing other licenses in CVS is a deliberate choice.

We've also decided against mirroring other projects in our CVS repositories--unless there are good reasons to do so. So when people need a non-GPL library, it is best to instruct them to download that library from that project's website.

CVS maintainer quick-start guide

This document contains a series of cvs command examples that project maintainers can use to maintain their projects. For further background information on any of these commands or concepts, please see the rest of the CVS portion of the handbook.

Note: this document covers modules, but themes, theme engines, and translations are essentially the same.


Basics


Logging in to CVS

if you have not already done so, make the contributions repository your CVSROOT:

export CVSROOT=:pserver:cvs_username@cvs.drupal.org:/cvs/drupal-contrib

cvs_username will correspond to the username you requested when you applied for a Drupal CVS account.

Next, log into CVS:

cvs login

This will prompt you for your CVS password. Note that this is not necessarily the same as your drupal.org password!


Adding a new module to CVS

First, check out the modules directory from the HEAD version of the contributions repository:

cvs checkout -l contributions/modules

The -l parameter (for "local") tells CVS to only check out the given directory, not all of the directories underneath it.

Next, copy your module files into the contributions/modules directory.

cd contributions/modules
cp -r ~/drupal/modules/example example

Finally, add your changes to CVS.

cvs add example
cvs add example/*
cvs commit -m "Initial commit of example module. Here is a brief description of what this module does."


Saving changes to your module

Bugs and features are tracked in your project's issue queue. It is customary in the commit message to reference the node ID of the issue where the bug/feature request was raised, and mention any contributors who helped with the code.

cvs commit -m "#12345 by username. Brief description of changes."


Obtaining latest changes

Both before you begin editing and before you save your changes, it's a good idea to grab the latest changes from CVS with the update command:

cvs update -dP


Branching and tagging

Note: Before you can branch/tag a module, it needs to have a project created.


Branching for a specific Drupal core version

When you're ready to either release your code for the first time, or are ready to port your module to another version of Drupal, the module should be branched. This indicates its compatibility with Drupal core.

The following branches your module for Drupal 4.7.x:

cd contributions
cvs tag -b DRUPAL-4-7 modules/example

Later, when you have finished porting your module to Drupal 5.x, when you are ready to release it, you would issue this command:

cd contributions
cvs tag -b DRUPAL-5 modules/example

This creates a DRUPAL-5 branch, which you would then hold the versions of your module that are compatible with Drupal 5.x.


Creating an official release

Once your module is stable, it's time to create an official release for it. Just as Drupal comes out with 5.0, 5.1, and such, you can (and should!) do this with your module.

The following creates the initial 1.0 release of your Drupal 4.7.x-compatible module (4.7.x-1.0):

cd contributions/modules/example
cvs update -dP -r DRUPAL-4-7
cvs tag DRUPAL-4-7--1-0

If you later fix additional bugs in this module, you may want to put out a 1.1 release (4.7.x-1.1):

cd contributions/modules/example
cvs update -dP -r DRUPAL-4-7
# edit some files and fix a bug
cvs commit -m "#54321: Fixing critical security error"
cvs tag DRUPAL-4-7--1-1

After tagging the module, it is also required to create a release node for it, so that it shows up in the list of downloads on drupal.org.

Note that normally, releases should only be made after major bug fixes or security patches; not for minor bugs like whitespace fixes, small text fixes, and so on.


Branching for new development versions of your module

Once a stable release of a module is created, you may want to continue to add features, leaving the original release of your module intact.

Imagine the current branch having an invisible version number, for example: DRUPAL-4-7--1. For subsequent branches you need to add the version number explicitly to the tag, hence the next branch will be DRUPAL-4-7--2. The following command will create a 2.x branch of your 4.7.x-compatible module:

cd contributions/modules/example
cvs update -r DRUPAL-4-7
cvs tag -b DRUPAL-4-7--2

And eventually, you may wish to release version 2.0 of your module (4.7.x-2.0):

cd contributions/modules/example
cvs update -dP -r DRUPAL-4-7--2
cvs tag DRUPAL-4-7--2-0


Creating a development snapshot release of your module

In order to allow people to test your module while it is in development, you may wish to make a development snapshot. This will create a "dev" snapshot which will always point to the newest version of the module in a particular branch.

To do so, make sure that the module is branched for the appropriate Drupal core version, and then create a release node pointing to that branch, rather than a specific release tag.

Note: Developer snapshots are only generated 2 times per day.


Deleting a tag

To delete a tag that was created in error, use the command:

cvs tag -d DRUPAL-5--1-0

If you need to delete a branch that was created in error, use the command:

cvs tag -dB DRUPAL-5

Note that it's a very bad idea to delete a branch once you have started committing changes to it.

In the event that there is a drupal.org release node pointing to the tag or branch you're trying to delete, CVS will display an error when you attempt to run these commands. You will need to first delete the release, then delete the tag or branch; then create the tag or branch, and then re-create the release.

Note: Only one of the site maintainers may delete a release node from drupal.org. If you think you need to delete a release node, please submit an issue to the drupal.org maintenance queue.


Saving changes to multiple branches

When you commit bug fixes, if they span more than one version, they need to be committed to each affected branch.

So for example, if you have a 4.7.x-1.x and 4.7.x-2.x and 5.x-1.x version of your module, changes that affect both the 4.7.x and 5.x versions need to be committed to the DRUPAL-4-7, DRUPAL-4-7--2, and DRUPAL-5 branches.


Advanced usage


Checking out a copy of each branch of the contributions repository

In the old release system, a lot of people had a checkouts directory, similar to this:

contrib-4-6
contrib-4-7
contrib-5
contrib-head

Each branch of the contributions repository had its own folder, so you could track new modules and changes to existing modules for each major Drupal version in one place.

You now need to track a few more branches:

cvs checkout -r DRUPAL-4-6 -d contrib-4.6.x-1.x contributions
cvs checkout -r DRUPAL-4-7 -d contrib-4.7.x-1.x contributions
cvs checkout -r DRUPAL-4-7--2 -d contrib-4.7.x-2.x contributions
cvs checkout -r DRUPAL-5 -d contrib-5.x-1.x contributions
cvs checkout -d contrib-head contributions

which would give you:

contrib-4.6.x-1.x
contrib-4.7.x-1.x
contrib-4.7.x-2.x
contrib-5.x-1.x
contrib-head

You can also check out only specific areas, such as modules and themes:

cvs checkout -r DRUPAL-4-7 -d contrib-4.7.x-1.x/modules contributions/modules
cvs checkout -r DRUPAL-4-7 -d contrib-4.7.x-1.x/themes contributions/themes

Branches such as DRUPAL-4-7 will always map to the 1.x branch of the 4.7.x-compatible contributions. All 4.7.x modules will be available from this branch.

Branches such as DRUPAL-4-7--2 will grab any contributions that have 2.x versions of their modules. The contents of these types of branches will probably be limited to few, rather complex modules such as Views or Project module.

It's also possible that further branches will be created, such as DRUPAL-5--3 and so on, but they will likely be very rare. See the full list of available branches in contrib.

Quickstart guide for adding to Drupal CVS using a windows client

This article was originally published on ThemeBot to encourage people to submit themes to Drupal and learn how to use CVS. It may also apply to contributing other projects to Drupal CVS.

The following is an essentialized walk-through focusing on the actual process involved when submitting themes to Drupal.org.  This is by no means a detailed CVS guide and I am not a CVS expert.  After spending days combing through the Drupal Handbook to piece it all together, I decided this would be useful as a reference.

It is highly recommended that you read the detailed CVS instructions in the Drupal Handbook at some point. This guide is for Windows users and a CVS client is used, so you don't have to use command-line. There are decent instructions already available for Linux users.

The assumption is that a stable release will be submitted, meaning that the theme has been fine-tuned, the PHP code has been checked for security vulnerabilities, the XHMTL and CSS have been validated etc, and the theme is ready to share with the Drupal community.  Let's get started...

Adding a theme to Drupal CVS

1.  First you will need to apply for a CVS account with Drupal.org.

2.  Once your account is approved you will be able to add projects.  But first, you will want to upload your theme to the Drupal CVS server.  There are several CVS GUI clients available.  I chose to use TortoiseCVS per recommendation from a Drupal developer.  TortoiseCVS can be downloaded Here.

3.  After you have installed TortoiseCVS, browse to the folder that contains all of the files for your theme.  The commands for TortoiseCVS are accessed via the context menu when you right-click on a folder.

4. Right-click the folder for your theme.  Select CVS > Make New Module. Make sure that Password server (:pserver:) is selected for Protocol:.  For CVSROOT: copy and paste or type the following line, replacing “username” with the username for your CVS account with Drupal (note: username is case-sensitive).

:pserver:username@cvs.drupal.org:/cvs/drupal-contrib

For the Module: field, copy and paste or type in the following line replacing “themename” with the name of your theme.  This should match the name of the folder which contains your theme files:

contributions/themes/themename

Press OK.  TortoiseCVS will do it’s thing and should report that the operation was successful.  This has created a directory in Drupal CVS to contain the files for your theme.  You can verify that the directory was created by visiting the Index of contributions.

5.  Now the folder for your theme will have a green checkmark on it.  The next thing to do is create a  CVS branch.  Right-click the folder and select CVS > Branch.  Depending on which version of Drupal you are releasing the theme for, you will want to use one of the following branches for New branch name:

DRUPAL-5
DRUPAL-4-7    

6.  Next you will want to create a CVS tag for the specific version of the theme you are releasing.  This helps to keep track of different releases.  Since this is the initial stable release you will start with 1.0.  Use one of the following tags below depending on the version of Drupal:

DRUPAL-5--1-0
DRUPAL-4-7--1-0

When this is complete, the theme has been registered on Drupal CVS and the branch and tags have been designated.

7. Time to add the files.  Right-click on the theme folder again and select CVS Add Contents.  All of the files in your theme folder will be selected automatically.  Press OK.

8. Now the files have to be commited.  Open the folder for your theme.  You will see orange plus signs on all of the files indicating that they have been added.  Select these files, right-click and choose CVS Commit.  In the Comment field, add something like “Initial Release for Drupal 5.0” and press OK.  If your theme folder contains sub-folders i.e. “/images” etc.  you will need to repeat this procedure for all of the files in each sub-folder since the commit command is not recursive.

Creating a project for your theme on Drupal.org

 9.  It is now time to add your project on Drupal.org.  Login to your account.  Click on Create content > Project.  Most of the fields on the Submit project form should be self-explanatory.  The Short project name should match the name of the folder that contains your theme files.  For CVS tree, use the following URL and replace “themename” with the short project name you entered earlier:

http://cvs.drupal.org/viewcvs/drupal/contributions/themes/themename

After everything applicable has been filled in, click Submit.

10.  Now to add the release that was uploaded to CVS earlier.  Click Add new release for your project.  Select the appropriate CVS identifier.  The next screen is fairly self-explanatory.  Click Submit.

That’s it!  Whew! Now your theme is officially managed through Drupal CVS and included as a project on Drupal.org. If you want to add a screenshot, first read the Theme screenshot guidelines.  You will need to Submit an issue with the Drupal.org webmasters in order to have your screenshot added.

So there you have it.  10 “easy” steps to add your theme to Drupal.  Perhaps not that easy, but it is to be expected that there will be some tedious procedures to deal with in order to keep the quality of contributions high while maintaining a community effort like Drupal.  Hopefully this has been informative and will save some time and headache for people who aren’t familiar with CVS but would like to contribute a Drupal theme.  Again, it is recommended that you read through the Drupal handbook so that you have a more complete understanding of CVS theory.  Here is a good place to start.

Using CVS to maintain a Drupal website

Why maintain a site from CVS?

Maintaining a site from CVS helps in two main areas:

  1. Upgrades: Normally, when it comes time to upgrade Drupal to a new version, there's a long list of upgrade steps that you need to follow. New tarballs need to be downloaded and extracted not only for Drupal core, but for all contributed modules as well. Repeat for each Drupal site you maintain. With CVS, this is just a single command.
  2. Access to bug fixes: New releases of Drupal are only made after major bugs (generally security issues) are fixed. These bugs (along with dozens of smaller bugs) are always fixed in CVS first.

How to maintain a site from CVS

Note: For detailed explanation and further examples of CVS commands, see the Main repository and Contributions repository pages.

Step 1: Check out code from CVS

Let's say, for example, you wanted to build a Drupal 4.7 site with the CCK and Views modules:

cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -r DRUPAL-4-7 -d mysite drupal
cd mysite
cvs checkout -d modules/views -r DRUPAL-4-7 contributions/modules/views
cvs checkout -d modules/cck -r DRUPAL-4-7 contributions/modules/cck

Step 2: Checking for updates

You can check for updates at any time by issuing the command:

cvs -nq update -dP

If updates are available, this will display output similar to:

U modules/taxonomy/taxonomy.module
U modules/upload/upload.module

Step 3: Updating the site

To pull in the changes from CVS:

1. Enter the command:

cvs update -dP

This will update your site to the latest Drupal core and contributed modules.

2. Run update.php to pick up any database changes.

Upgrading to a different version of Drupal

Let's say you'd like to upgrade from Drupal 4.7 to 5.0. Basically, the steps are the same except:

1. First, you need to do research to determine what contributed modules have been ported to the version of Drupal, and take steps to port those that are not. Failing to do so could break your site.

1. Enter the command:

cvs update -r DRUPAL-5 -dP

2. Run update.php to pick up any database changes.

Tips

  • Having a test site is highly recommended; new changes from CVS could potentially bring in additional bugs or changes which break your site (particularly with contributed modules). Testing these updates on a copy of your live server before moving them over is a great idea.
  • Using a version control system for your Drupal code such as CVS or Subversion is also a good idea; this way, you can keep track of what changes were made and when.

Apt-drup - Check out drupal and contribution modules yet perserving orignal CVS keywords

Preserving cvs keywords from Drupal (like $Id: drupal.js,v 1.29 2006/10/14 02:39:48 unconed Exp$) are essential to track down versions of files tou originally used and to be able to patch your local copy of drupal in case you found a suitable patch on drupal.org. To be able to patch, you need to know which version of files we want to patch.

These keywords might get lost when we import this to your own CVS and replaced with your keywords substitution: $Id: drupal.js, v 1.1 2007/01/15 11:30:00 owahab Exp$.

This script does a "cvs export" from Drupal CVS for a version you can specify to a folder you can specify then it adds CVS keywords making the folder ready for a "cvs import" to your CVS, yet preserving Drupal's original CVS keywords.

The script also can be used to download Drupal modules the same way.
1. Download the attached file.
2. $ chmod +x apt-drup.sh
3. $ ./apt-drup.sh folder 5.1

You can substitute: folder with any folder name and 5.1 with any version of drupal.
Note: you can directly request downloading a module a folder that doesn't exist and in this case drupal will be downloaded too.
$ ./apt-drup.sh folder 5.1 event
or
$ ./apt-drup.sh folder 5.1 buddlylist 1.0

http://cvs.drupal.org/viewcvs/drupal/contributions/sandbox/owahab/apt-dr...

cvsdrupal - checkout and maintain a local CVS repository

#!/bin/sh
#
# cvsdrupal v0.0.2 20060628 markc@renta.net
#
# A Bash script to initialize and maintain a CVS checkout
# of the drupal.org codebase. Just run it once a day or once
# a week to keep your checkout uptodate.
#
# License: http://www.gnu.org/licenses/gpl.txt
#
Change these settings to suit your environment:
#
#  REL  is whatever version you want to check out,
#        leave as is for the very latest version
#
#  BASE  root path where the "drupal" folder goes
#
#  UGID  the local user:group perms to use, leave
#        empty to ignore

REL=HEAD
BASE=/home/p
UGID=1:1

# Add the extra modules you want here

MODULES="
  subversion
  svn
"
# Add any themes you want to this section

THEMES="
  meta
"
# Below here should remain untouched

SRC=:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal
export CVSROOT=$SRC

[ -d $BASE/drupal ] || mkdir -p $BASE/drupal

if [ -d $BASE/drupal/CVS ]; then
  cd $BASE/drupal
  cvs -z6 up . -PAd -r $REL
else
  cd $BASE
  cvs -z6 co -r $REL drupal
fi

export CVSROOT=${SRC}-contrib

for M in $MODULES; do
  if [ -d $BASE/drupal/modules/$M/CVS ]; then
    cd $BASE/drupal/modules/$M
    cvs -z6 up . -PAd -r $REL
  else
    cd $BASE/drupal/modules
    cvs -z6 co -r $REL -d $M contributions/modules/$M
  fi
done

for T in $THEMES; do
  if [ -d $BASE/drupal/themes/$T/CVS ]; then
    cd $BASE/drupal/themes/$T
    cvs -z6 up . -PAd
  else
    cd $BASE/drupal/themes
    cvs -z6 co -r $REL -d $T contributions/themes/$T
  fi
done

[ ! -z $UGID ] && chown $UGID -R $BASE/drupal

Advanced: Using a repository to track local changes to Drupal code

Note: many people use subversion as their local repository even thouh drupal.org uses CVS. Hopefully someone will writeup the process here

Note: The following assumes you have both basic knowledge of CVS and your own local repository set up and working.

If you've been modifying the Drupal source code for your own purposes (or developing a module or theme) and manually applying your changes to the Drupal source every time it updates, you may be glad to learn that CVS can help make this easier.

This is usually referred to as 'tracking third-party sources' and requires knowledge of the CVS concepts branching, release tags, and the vendor tag. We'll work through an example here and explain these concepts as we go.

Example

Lets assume we'd like to track current Drupal CVS HEAD, and start by downloading the source. In this case we'll export using anonymous CVS (we could also just download a tarball).

Begin by logging in to the anonymous CVS server, the required password is 'anonymous':

cvs -d:pserver:anonymous@cvs.drupal.org:/cvs/drupal login

Then export the newest development version of drupal using the HEAD release tag:

cvs -d:pserver:anonymous@cvs.drupal.org:/cvs/drupal export -r HEAD drupal

Now that we have a local copy of the drupal source we can import it into our own CVS repository. In this example we import with a log message including the date '-m "message text"', a module location/name of 'sites/drupal' (customize that to suit your own CVS repository), a vendor tag of 'drupal' and a release tag of 'HEAD20040110'. We also use the -ko option to prevent keyword expansion (this preserves the CVS $ Id $ tags used on drupal.org):

cd drupal
cvs import -ko -m "Import CVS HEAD on Jan 10th 2004" sites/drupal drupal HEAD20040110

Before we can customize we need to checkout into a working directory. Then we can modify a file or files and commit:

cvs checkout drupal
cd drupal
...modify a file or files...
cvs commit

We now have a drupal module with a special 'vendor branch' (identified by the vendor tag), which contains the drupal source files we imported, and a main trunk with our modified files. Any files modified at this point are now HEAD on the main trunk of the module, whilst the unmodified files remain HEAD on the vendor branch (HEAD being what is produced by cvs update). For an individual file (fileone.php) the version history now looks like something like this:

                                        HEAD
                                       +-----+
[Main trunk]  fileone.php *------------+ 1.2 +
                           \           +-----+
                        +---------+
[Vendor Branch]         + 1.1.1.1 +
                        +---------+
                      (tag:HEAD20040110)
Managing contributions

Now I've been thinking about the best way to extend this method to incorporate contributions (eg, modules from the separate drupal.org contributions CVS repository or any other non-drupal-core code). I would welcome comments/suggestions on a good model.

The model I'm currently using:

  1. Maintain updated working copies of drupal HEAD and contributions HEAD from drupal.org locally, something like:
    [HOST:~/drupal]$ ls -1
    contributions
    drupal
    imports
  2. Import drupal into my local CVS repository with vendor/release tags as per method outlined above.
  3. Maintain customized versions of drupal using CVS branches which can be diff'd/updated against the latest drupal I've imported into my local CVS repository as suggested at the end of the method above.
  4. Make a temporary copy of the complete, updated directory of whatever contribution I want to add to drupal's core in a different and otherwise empty local directory so I can import it to my local CVS repository alone. For example, say I want to include the event module:
    [MP:~/drupal/contributions/modules]$ cp -r event ../../imports
    [MP:~/drupal/contributions/modules]$ cd ../../imports/
    [MP:~/drupal/imports]$ ls -1a
    .
    ..
    event
  5. Import the contribution with its own vendor/release tags into the proper place in the HEAD branch of drupal I already have in my local CVS repository. In this example, I import the entire event module from the temporary directory that contains the event module directory and only that directory. Note how the event module is imported into the proper directory (ie, "drupal/modules") of the drupal project in my local CVS repository:
    [MP:~/drupal/imports]$ cvs import -ko -m "Import module event HEAD on 04 OCT 2004" drupal/modules module_event MODULE_EVENT_20041004
    cvs import: Importing /home/mediaped/repos/drupal/modules/event
    I drupal/modules/event/CVS
    N drupal/modules/event/CHANGELOG
    N drupal/modules/event/CREDITS
    N drupal/modules/event/INSTALL
    N drupal/modules/event/LICENSE
    N drupal/modules/event/README
    N drupal/modules/event/TODO
    N drupal/modules/event/event.css
    N drupal/modules/event/event.module
    N drupal/modules/event/event.mysql
    N drupal/modules/event/event.pgsql
    N drupal/modules/event/fields.inc
    cvs import: Importing /home/mediaped/repos/drupal/modules/event/po
    I drupal/modules/event/po/CVS
    N drupal/modules/event/po/event.pot
    N drupal/modules/event/po/de.po
    N drupal/modules/event/po/es.po
    N drupal/modules/event/po/he.po
    N drupal/modules/event/po/hu.po
    No conflicts created by this import
  6. Now the contribution is included in the HEAD of the drupal project in my local CVS repository, so if I update a working copy of that local drupal project (using the update -d option to ensure I get any new contribution directories) or checkout a new HEAD working copy, the newly added contribution will be in that working copy.
  7. And, if I would like to add the contribution to one of the branches I have to maintain customized, separate versions of drupal, I just need to update a working copy of that branch with the release tag I established when importing the contribution into my local CVS repository. For example, to get the event module in a working copy of a branch tagged "drupalcustomA":
    [MP:~/projects/drupalcustomA]$ cvs -q up -d -j MODULE_EVENT_20041004
    U modules/event/CHANGELOG
    U modules/event/CREDITS
    U modules/event/INSTALL
    U modules/event/LICENSE
    U modules/event/README
    U modules/event/TODO
    U modules/event/event.css
    U modules/event/event.module
    U modules/event/event.mysql
    U modules/event/event.pgsql
    U modules/event/fields.inc
    U modules/event/po/de.po
    U modules/event/po/es.po
    U modules/event/po/event.pot
    U modules/event/po/he.po
    U modules/event/po/hu.po

Note: if you try an update using your new tag and cvs complains that the tag doesn't exist, apparently you have to keep trying different cvs commands using that tag until cvs updates CVSROOT/val-tags. Once you get the tag to work once, it will work from then on. See the section here on error "cvs [checkout aborted]: no such tag".

  • This adds the event module files to that working copy, but I still have to commit them to that branch in my local CVS repository:
    [MP:~/projects/drupalcustomA]$ cvs -q ci -m "adding event module"
    Checking in modules/event/...
    ...
    done
  • Then I can see that the event module is now a part of that branch by checking the status -v of one of the event module files:
    [MP:~/projects/drupalcustomA]$ cvs st -v modules/event/INSTALL
    ===================================================================
    File: INSTALL           Status: Up-to-date

       Working revision:    1.1.2.1 Tue Oct  5 06:46:30 2004
       Repository revision: 1.1.2.1 /repos/drupal/modules/event/INSTALL,v
       Sticky Tag:          drupalcustomA (branch: 1.1.2)
       Sticky Date:         (none)
       Sticky Options:      -ko

       Existing Tags:
            drupalcustomA                            (branch: 1.1.2)
            MODULE_EVENT_20041004           (revision: 1.1.1.1)
            module_event                    (branch: 1.1.1)
  • Later, if there are significant changes to the event module, I'll update it using the same vendor tag, but a different release tag, just as it is demonstrated in the method above for drupal itself. My local drupal head will get the latest event module and I can get it in my customized branches by updating them with the new release tag I make when importing the new event module.
  • I will import every contribution I wish to include in my local CVS repository this way, giving unique vendor/release tags to each. That way I can pick and choose which contributions are included in any of my customized branches of drupal by updating each branch with the release tags of only the contributions I want in that branch.
Updating the vendor branch

At some later point the drupal source code will have been updated and we'll want to add the updated version to our repository. We do this by repeating the process described above, we get a fresh copy of the source from drupal.org, and import using the same vendor tag but change the release tag from 'HEAD20040110' to reflect the newer version:

cvs import -ko -m "Import CVS HEAD on Jan 11th 2004" sites/drupal drupal HEAD20040111

This updates the vendor branch, a single files revision history can now appear four different ways, depending on whether it has been modified by us, by the vendor (drupal.org), by both, or not at all.

If the file was modified only by us, our modified version remains the head revision:

                                       HEAD
                                      +-----+
[Main trunk] fileone.php *------------+ 1.2 +
                          \           +-----+
                           \
                        +---------+
[Vendor Branch]         + 1.1.1.1 +
                        +---------+
                      (tag:HEAD20040110)

If the file was modified only by the vendor, the new version becomes the HEAD revision:

[Main trunk] filetwo.php *
                          \
                           \                   HEAD
                        +---------+          +---------+
[Vendor Branch]         + 1.1.1.1 +----------+ 1.1.1.2 +
                        +---------+          +---------+
                      (tag:HEAD20040110)   (tag:HEAD20040111)

And if the file was modified by both us and the vendor:

                                         HEAD
                                        +-----+
[Main trunk] filethree.php *------------+ 1.2 +
                            \           +-----+
                             \
                          +---------+          +---------+
[Vendor Branch]           + 1.1.1.1 +----------+ 1.1.1.2 +
                          +---------+          +---------+
                        (tag:HEAD20040110)   (tag:HEAD20040111)

Our version of filethree.php remains the HEAD revision, but this is clearly not desirable since it doesn't carry the latest changes. In fact, during our import of the latest source CVS would have warned us of conflicts between the two versions of filethree.php, we need to merge the changes to remove this conflict:

cvs checkout -jHEAD20040110 -jHEAD20040111 drupal

Examine the merged file to ensure the changes CVS made were sane and then `cvs commit' the changes back to the main trunk. Leaving us with a new revision which becomes HEAD:

                                                       HEAD
                                        +-----+       +-----+
[Main trunk] filethree.php *------------+ 1.2 +-------+ 1.3 +
                            \           +-----+       +-----+
                             \
                          +---------+          +---------+
[Vendor Branch]           + 1.1.1.1 +----------+ 1.1.1.2 +
                          +---------+          +---------+
                        (tag:HEAD20040110)   (tag:HEAD20040111)
Summary

It should now be clear that using the CVS vendor tag to create a vendor branch in your own drupal module you can track changes to the drupal source code whilst also maintaining and developing your own customizations and new features for drupal. This example has been kept very simple for the purposes of explanation, but the basic process can be used to achieve many different things, some examples:

  • Track a specific release of Drupal (e.g. 4.3, or 4.2), instead of the development (CVS HEAD) version.
  • Maintain your customized sites with modules, themes, static pages, images etc all added to your CVS repository, whilst still tracking and importing updates to the drupal core.
  • Branch your module to maintain several customized web sites off a single tracked branch of the drupal core.

Reading the following additional resources is highly recommended.

Additional resources

Patches

Patches are a way to distribute changes to files. In the context of Drupal, it means that patches usually describe changes to code. In fact, patches describe the changes between a before and after state. That means that if you have Drupal in the before state you can arrive at the after state by simply applying the patch.

We use patches for content control even though Drupal is distributed via CVS. This is because patches provide a great deal of control and convenience; they are small, plain-text and can be send via e-mail; they are focussed on a change and therefore easily read and judged.

This also means that a CVS-account is not required to supply patches; anyone with a Drupal account can go to the issues queue to upload a patch. The tips for contributing apply to core as well as contributed modules.

Patches serve a second purpose; some modules require changes to Drupal core files. Some module authors provide modified modules, others give you patches to do it yourself. It is recommended to use already patched files provided by the module author. When these can't be used or are not available, use patches, but exercise caution.

Warning: Patching is something that should never be done on your production site unless you have sufficient backup and testing performed. While patching itself is relatively easy, understanding the implications of a patch is not. Patching your system can lead to loss of data and/or site instabilities

Background: the basic concept of Diff and Patch

diff creates patch

In simple terms, the diff command is used to compare differences between two versions of a file. The resulting file is called a patch, and typically is given (by the user) a ".patch" suffix.

This patch file then can be used on other copies of the "old" file by using the patch command, thus updating their "old" file(s) to match the "new" file(s).

See HOW TO: Apply patches for more on applying patches.

Why you would use diff

When might one use diff to create a patch file? Let's say you are customizing a module to fix a bug, and have saved a new version of the module. How will you pass on your bug fix to others? Simply passing on your version of the module may not work, because it's quite possible someone else has modified some other aspect of the code at the same time and you both would be overwriting each others' changes.

So instead, what you do is run diff between the two files, and then upload the resulting patch -- which others can then apply to their files using the patch command. (And you can apply other people's patches against your files, without losing your own changes.)

The added benefit of this type of workflow is that changes to the code can easily be tracked -- and undone, if necessary -- which is essential in a community-developed project such as Drupal.

The tools at your fingertips

The ability to create patches using diff -- and to apply patches to your files using patch -- is already in most computer systems. Follow the relevant links in this section to learn more about using these powerful-yet-simple commands on your platform.

Applying patches

Applying patches, modifying files according to instructions in the patch file, is the domain of patch programs. There are many different programs with this functionality. some stand-alone (patch), some integrated in IDE's (Eclipse, XCode).

Warning: Patching is something that should never be done on your production site unless you have sufficient backup and testing performed. While patching itself is relatively easy, understanding the implications of a patch is not. Patching your system can lead to loss of data and/or site instabilities

This page only deals with some basic principles using the command line utility patch. Patch can be found on most UNIX systems and is included in the packages UnxUtils and Cygwin for use on Windows. There is also a video on Applying patches to Drupal core in the videocasts section.

Provided that the patch was made relative to the Drupal root directory, navigate to the Drupal directory (using cd) and issue the command:

patch -p0 < path/file.patch

If the patch was not made relative to the Drupal root directory you can place the patch in the same directory as the file being patched and run the patch command without the -p option. To do so, cd to the directory and type:

patch < file.patch

Patch will usually auto-detect the format. You can also use the -u command line option to indicate a unified patch.

You can also reverse the patch if you want to see if a problem has been introduced by a particular patch. You should also reverse a patch prior to adding a newer, updated version of the same patch. To reverse the patch, use the patch command with the -R option:

patch -p0 -R < path/file.patch

Common problems

UNIX vs. Windows end-of-line encoding

Text files originating from UNIX systems use a single linefeed character (LF) to mark the end of a line, whereas Windows systems use a carriage return followed by a linefeed (CRLF).

The GnuWin32 patch program may crash on a patchfile with Unix end-of-line encoding throwing the error "Assertion failed: hunk, file [path]/patch.c, line 339".

Most patches downloaded from drupal.org are UNIX text files. When you use patch on Windows, you need to convert line-endings using a unix2dos utility or a decent text-editor.

One way to do this is to open the file with WORDPAD, choose "Save as" and select as type Text document.

Patch can't find the target file

This usually means that you are either executing patch in the wrong directory or that the patch was made relative to another directory. Open the patchfile and locate the lines describing the changed files to determine the path:

--- modules/codefilter/codefilter.module Tue Mar 28 15:04:48 2006
+++ modules/codefilter/codefilter.module Sun Apr 23 15:51:32 2006

The above patch appears to be made relative to the drupal root directory so you can execute the following command from the Drupal root:

patch -p0 < file.patch

The number after -p determines the number of path elements that should be ignored. This is very useful when you need to apply a patch that was made against an old and new directory:

--- olddir/modules/codefilter/codefilter.module Tue Mar 28 15:04:48 2006
+++ newdir/modules/codefilter/codefilter.module Sun Apr 23 15:51:32 2006

You can apply this patch from the Drupal root by stripping the first path element; 'olddir' / 'newdir':

patch -p1 < file.patch

Apply patches on MacOS X

The program patch is included with MacOS X. You need to use it from the command line. To access the commandline, open Terminal.app. See HOWTO: Apply patches for details on how to use patch.

An alternative GUI solution is Apple's IDE Xcode (free subscription required).

FileMerge: Mac OS X Patching Utility

FileMerge is a great utility for patch and diff commands in the Mac OS X environment. It has a great graphic user interface and is simple to use without learning complicated Terminal command lines. The GUI provides a traditional left and right side comparison layout, and the ability to manually review and save changes. It is possibly the only free diff/patch utility available for Mac OS X developers.

This utility comes free bundled Xcode, a free development tools package offered with any Mac OS X operating system or by a free subscription to the Apple developer site.

FileMerge is only 700k in size but a full install of Apple's Xcode asks for 3.4 GB of disk space! In order to get the application installed and use the minimum amount of storage space, run the Xcode custom installer and select only the "Developer Tools Software". This will provide you with the application you want and only 180 MB of disk usage.

There is an installation walkthrough provided here.

Oreillynet.com has a great tutorial to help you get started using the application for diff and patch functions.

Apply patches on Windows

Patching on Windows can by done by a variety of programs. The command line patch utility can run on Windows either natively (Unxutils) or via an emulation layer (Cygwin).

If you require or fancy a graphical interface, you can use for example TortoiseCVS, TortoiseSVN or the IDE Eclipse.

Cygwin

To use Cygwin to patch in Windows:

1. In your c: drive, create two folders; one named C:\cygwinstuff and another named C:\patchfiles

2. Go to www.cygwin.com and download the setup file.

2. Click on the file downloaded in step 2 to start the install

The next few steps will walk you through the install process for Cygwin.

4. Choose Install from Internet

5. For Root Install Directory: Choose C:\cygwin

6. For Local package directory, choose c:\cygwinstuff -- the folder you created in step 1.

7. Choose the appropriate internet connection -- for most of us, that's the Direct Connection

8. Choose a download site -- I generally pick one at random, but for those of you who are more precise, you can use the domain names to try and choose a site on the same continent.

9. Select packages: This is where things can get tricky. Just make sure to select the patchutils under the Devel section.

10. Begin the install.

11. Once Cygwin has installed, move the file(s) you want to patch, and the patch file, into the C:\patchfiles folder you created in step one.

12. Open Cygwin. Cygwin gives you what you have been looking for: the command line. You will need the following commands:

  • ls -al -- lists the contents of your current directory
  • cd .. -- moves you up to the parent directory (note the space between cd and .. if you come from a DOS background!)
  • cd foldername -- moves you to a specific folder visible from your current directory

13. When you open Cygwin and type ls -al, you will see the contents of your current directory. Different versions of Cygwin start you in different directories. Regardless of where you start, you can use cd c: to navigate to your C: drive (under Cygwin, this will be called /cygwin/c).

14. When you are at C:, you will be able to see the patchfiles folder you created in step one. Use cd patchfiles to navigate into this folder.

15. Type ls -al. You should see the files you moved into this folder in step 11.

16. Type patch filetobepatched < patchfile or patch -p0 < patchfile if there are many files to be patched.

17. Before you copy the patched file(s) to your production (live) site, make a backup copy of the original, unpatched file(s). Better safe than sorry, and you never know if/when you might need them.

These instructions ought to get you started.

For a more comprehensive overview of the options available while patching, see the patch manual (man) page.

Patch

If you only need patch, download the standalone program from Sourceforge.

Note: patch on Windows requires CRLF line-endings; see also Common problems.

UnxUtils

UnxUtils is a collection of GNU utilities for Win32, including patch. Download UnxUtils.zip then extract to a suitable folder. Add the folder [folder]\usr\local\wbin\ to your Windows PATH variable (or invoke patch using [folder]\usr\local\wbin\patch).

You can then invoke patch on the command line (via Start » Run, cmd)

HOWTO: Apply patches contains information on the command line syntax.

Note: patch on Windows requires CRLF line-endings; see also Common problems.

Reviewing patches

General guidelines

  • Write down your thoughts as you are reviewing, not afterwards.
    Keep a text editor open for typing in and write down your thoughts immediately. When you are done, clean it up and structure it.
  • Take a look at both the big picture and the details.
    Simply saying "I [don't] like this feature" or "-1" is of no use and is strongly discouraged. Similarly, diving into a patch and saying nothing but "there is a typo in function so and so" could be a waste of time as you could be continuing a patch that has no hope of being committed.
  • No patch can save the world, not even a Drupal core patch. If it works and does something useful on its own, then it is good to go. One of the worst things you can do is elaborate on other, equivalent approaches and suggest complex extensions to the patch. Additional features can be added later. A scalpel is often better than the Swiss Knife.
  • Don't stop reviewing at the first sign of trouble.
    If there are bugs, imagine how things should've worked. If there are usability problems, try to think of a better interface.
  • Pay attention to what the submitter has said about the patch.
    If some things are not clear, write down your questions. You should have a good idea of what the patch does and who it is for before you start.
  • Apply a patch to a CVS tree if you can.
    This allows you to immediately create an updated patch. Often it takes more time to describe small changes (like typoes and code style) than it is to make those changes yourself.

Reviewing process

  • Challenge the proposed changes.
    What are the benefits? Are there disadvantages? Does it fit in the general ideas of Drupal? Does it make good use of our systems or does it re-invent the wheel? Does it belong in core? Can you see the changes being used elsewhere too?
  • Test the functionality.
    Does it do what it should? Does it work under different settings (e.g. clean URLs) and with different modules?
  • Examine the usability.
    If new pages are added, are they are in a logical place? If controls are added, are they intuitive and logical to use? Does it come with appropriate contextual help? Is the interface consistent?
  • Review the code.
    Is the code understandable and documented? Does it make appropriate use of APIs or does it re-invent the wheel? Are there possible security issues? Does it conform to our coding conventions? Is the functionality separated into logical pieces, or just mashed together? Are appropriate sections made themable?

Authoring a patch review

When posting your issue follow-up, it is important to think critically and speak positively

  • Your goal is to propose an even better solution than the patch currently envisions. Do not post negative feedback without a corresponding solution. A good review helps a patch proceed, instead of obstructing it. It is a rare case that a submitted patch has absolutely no merit and warrants a purely negative review.
  • Post your findings in a clear and structured manner to the original issue. If you can, include an improved patch.

HOWTO: Set up a test environment to help review patches

This guide is aimed at a developer or end user who would like to participate in the Drupal core patch review process, but is unsure of where to start. A similiar process will work for contributed modules, but this page's focus is on core. There are five main sections to this guide:

  1. Retrieving a copy of Drupal HEAD from CVS
  2. Configuring Drupal HEAD
  3. Finding and applying patches
  4. Testing patches for versions other than Drupal HEAD
  5. Creating a test environment from an existing installation of Drupal

This document assumes the reader has shell access to some sort of Linux/Unix/BSD/Cygwin platform.

Retrieving a copy of Drupal HEAD from CVS

Drupal HEAD where work on the next version of Drupal always occurs, so it is against this version of Drupal that any new features will be developed. Testing these features requires a functional copy of the development version of Drupal, which is available from Drupal's CVS repository. For more information on CVS, please refer to the CVS section of the handbook.

  1. Begin by creating a directory to contain the new version of Drupal. For example,
    cd /path/to/web/root/
  2. The next step is to retrieve a copy of Drupal HEAD from the server. Execute the command:
    cvs -z9 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d drupal-cvs drupal

    This will download the Drupal HEAD files to a folder called drupal-cvs.

Configuring Drupal HEAD

Drupal HEAD's installation and configuration is generally done exactly the same as a standard version of Drupal (see the Installing Drupal section of the handbook for more information). However, there are a few additional changes which are recommended:

  1. Create a folder in the sites/ directory with the same name as the domain name of the site, and copy the settings.php file there. This prevents customized settings from being overwritten when Drupal HEAD is updated. For example,
    mkdir sites/example.com
    cp sites/default/settings.php sites/example.com/settings.php
  2. After completing the steps in INSTALL.txt, test the installation to ensure it is working properly.

Finding and applying patches

Patches can be found in the Drupal Patch Queue. Choose from patches which match the Drupal installation (in this case, cvs; see below for instructions on testing patches on other versions of Drupal). An excellent guide to reviewing patches can be found at Tips for reviewing patches in the Contributor's Handbook.

Once an interesting patch has been found, the process to apply the patch in order to test it is as follows:

  1. Ensure Drupal HEAD is updated to the most recent version. This can be done at any time by executing the following command inside the Drupal HEAD root directory:
    cvs update -dP
  2. Download a copy of the most recent version of the patch (often patches are revised further down the issue page) by issuing the command:
    wget http://drupal.org/files/issues/patch-name.patch
  3. Apply the patch by issuing the following:
    patch -p0 -u < patch-name.patch

Finally, test the patch out rigorously and submit feedback to the issue tracker, in order to help identify problems and improve the functionality of Drupal.

Testing patches for versions other than Drupal HEAD

Not all patches in the patch queue are for Drupal HEAD; bug fixes and security updates to release versions of Drupal will also appear here. To setup a test environment for patches other than those intended for Drupal HEAD (for example, Drupal 4.6.1), generally the same steps as above are followed with the following exception:

When retrieving a copy of Drupal from CVS, the same checkout command is used, however a branch must be specified in order to retrieve a version of Drupal other than HEAD. A list of available braches is available from the Using CVS with branches and tags page of the Contributor's Handbook.

For example, to checkout a test version of Drupal 4.6.1, use the command:

cvs -z9 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -r DRUPAL-4-6-1 drupal

Remember that the version assigned to a patch and the version of Drupal to which it is applied must match.

Creating a test environment from an existing installation of Drupal

The best way to see how a patch will affect an already-live installation of Drupal is to apply it directly. However, since patches can sometimes yield unexpected results, the best course of action is always to apply them to a copy of the live installation rather than the installation itself.

Please see Copying Your Live Site to a Test Site (Command-Line version) from the CivicSpace upgrade information for more information on how to do this.

Some additional tips

  • test with a meaningfull database. One that contains different roles, blocks, nodes and comments. Or one tat has no comments, etc. testing with a clean installation makes very little sense for a lot of tests. devel module contains scripts to generate content, taxonom and users
  • a good way to maintain patches and to test only certain maintained patches is to do so on a dedicate installation. That way you can be sure that patches do not conflict, or tat a previous test breaks the current one.
    Here is what I do:
  1. mkdir 28245 (the number is the issue/bug/node id
  2. mysqlhotcopy the "clean" database in there with dbname 28245
  3. edit settings.php to use myhost/28245 as url and 28245 as database.

Might seem a lot of work, but once I got used to it it works very fast.

Creating patches

Generating patches, files containing the difference between files, is the domain of diff programs. There are many programs with this functionality; some stand-alone (diff), some integrated in IDE's (Eclipse, XCode) or version control systems (CVS). Diff can be found on most *nix systems. It is available in the free XCode Developers Tools on Mac and is included in the packages UnxUtils and Cygwin for use on Windows.

This page only deals with some basic principles using the command line utility diff (and the related cvs diff command). There is also a short video available, Rolling Patches in Drupal. Guidelines for patch submission to the issue tracker can be found in the Submitting Patches section.

CVS diff vs. diff

Diff compares two files. The cvs diff command will compare your locally edited file to the CVS repository version if you have a CVS checkout of the file being patched. The regular diff command will compare two local files (the original and and the edited version) to create a patch. The regular command can be used if you do not have an internet connection or do not have CVS set up on your computer (for more information on CVS see the CVS section of the handbook). Both commands are used with the same options and will result in usable patches.

Check your directory

The most important thing to ensure that a patch is usable is that you need to run the diff command from within the Drupal directory structure where you are patching. Ideally you should run diff from the Drupal root directory (the one that contains index.php, cron.php, etc). For example, if I have a copy of Drupal installed on my localhost in a folder called drupaltest (e.g. /www/htdocs/drupaltest) then I need to change into the drupaltest directory before I run the diff command. You can successfully run a diff from outside of the Drupal directory structure and create a patch file but the patch will fail to apply properly for other people since they won't have the same system directory structure that you do. One thing to keep in mind with patching contributed modules/themes vs. core patches is that not everyone puts contribs in the same location so you may want to do the diff from within the module's/theme's directory rather than Drupal root. Core patches should always be made from Drupal root.

Readability

Patch readability is very important for the review process and a patch not created with that in mind is likely to be rejected by reviewers. The two best options to accomplish this are the -u option for unified formatting and -p to show the function closest to the difference in the code, making it easy to see what function changed.

Separating your changes and whitespace

Separate each logical change into its own patch. For example, if your changes include both bug fixes and performance enhancements, separate those changes into two or more patches. Likewise all whitespace and code style cleanup should be in a separate patch and not mixed in with functional changes to the code. This just makes it harder for reviewers to see your substantive changes. Speaking of whitespace, keep in mind that if your text editor is set up to automatically remove whitespace, then you need to turn that setting off when editing code for a patch or you could create a messy patch without intending to.

Line endings and directory separators

Use Unix line endings (LF) and directory separators (/). Line endings can be converted manually with many text editors or by piping diff output through a dos2unix utility.

The command

So the basic command to use is:

cvs diff -up original.php > filename.patch
or
diff -up original.php new.php > filename.patch

Note that the symbol > will redirect the output to the file filename.patch. Give it a name that helps identify what the patch is for, e.g. modulename_code_cleanup.patch. If you do not include the > filename.patch part of the command the diff will go to the standard output for your system, which in most instances will be your monitor. This way you can see what the patch will look like before actually creating a file for it. Note that you can output the patch file to another place like your desktop or a folder you have created just for patches if you want by simply typing the path name in front of the patch name, e.g. path/to/desktop/modulename_code_cleanup.patch.

When you've modified multiple files in the source tree, use diff's ability to compare directories. Add the -r switch to instruct diff to recurse (sub)directories.

cvs diff -urp directory > filename.patch
or
diff -urp original_directory new_directory > filename.patch

Adding/Deleting files

You use the -N switch to account for new or deleted files.

If you are using -N to account for a new file, you will also need to edit the CVS/Entries file for that directory and manually add a line for the new file. For example, if you have created a new file called newfile.inc in the modules/system directory, you will need to add a line at the end of the modules/system/CVS/Entries file like this:

Editing modules/system/CVS/Entries:
/admin.css/1.12/Tue Jan 16 23:15:28 2007//
/defaults.css/1.2/Fri Aug 25 09:01:12 2006//
/system.css/1.22/Wed Feb 7 03:46:21 2007//
/system.info/1.3/Tue Nov 21 20:55:35 2006//
/system.install/1.79/Fri Feb 16 16:39:46 2007//
/system.js/1.1/Thu Feb 22 16:33:29 2007//
/system.module/1.451/Thu Feb 22 16:33:29 2007//
/newfile.inc/0/New file//

If you are working from a tagged version the line will have the tag at the end. Use the other entries in the file to guide you:

/newfile.inc/0/New file//TDRUPAL-5

In this example, the "New file" string is arbitrary, but the 0 must be specified as the file's revision to show that it is new. Once this new line has been added cvs diff -Nup will properly include the new file in the diff.

Here is the command:

cvs diff -Nup directory > filename.patch
or
diff -Nup original_directory new_directory > filename.patch

An example using cvs diff

  1. Make sure you have the latest CVS version of the file(s) before you begin making your changes: change (cd) to the Drupal root directory and run the update command: cvs update -dP.
  2. Make your changes to the file(s).
  3. When you are ready to roll your patch, change back to the Drupal root directory.
  4. Do a cvs diff -up path/to/file/example.module > mypatchname.patch. This creates a new patch file in the Drupal root that you can now upload to an issue.

An example using diff

  1. Make sure you have the latest, clean copy of the file(s), either by downloading the latest tarball or updating from CVS (if possible)
  2. Create a working copy of the file you are editing in the same directory as the original (e.g. example.module copied to exampleNew.module).
  3. Do your edits to the working (new) copy.
  4. When you are ready to roll your patch, go to the command line and change (cd) to the Drupal root directory.
  5. Do a diff -up path/to/file/example.module path/to/file/exampleNew.module > mypatchname.patch. This creates a new patch file in the Drupal root that you can now upload to an issue.

Common issues with patch creation and application

In some instances, patches have been known to fail. This page is designed to help troubleshoot problems that people have in creating and applying patches.

patch unexpectedly ends in middle of line

Applying some patches generates the error:

patch unexpectedly ends in middle of line

This error seems to occur when the patch file ends on a line that includes nothing but whitespace characters. Patch files should always end with a Unix return character. If you encounter this error, check to see that the last line of your patch does not contain whitespace characters.

References: http://drupal.org/node/122734 and http://drupal.org/node/118660

Create patches on MacOS X

The UNIX utility diff is available on MacOS X. You need to open Terminal.app to access the shell. See HOWTO: Create patches for details on how to use diff or cvs diff.

You will need to download and install Xcode (free subscription required - look under "Developer Tools"). This will install diff, cvs, and a whole suite of Apple development tools. Alternative versions (not Xcode) of cvs are also available using Fink

Xcode also includes a GUI solution for creating patches in Apple's IDE.

TextMate + CVS or Diff bundle
Diff Bundle

Diff bundle is included with the default TextMate install.

Usage is pretty simple. Make a copy of the file you want to edit. After making your changes, save the file and select the menu item:

Bundles > Diff > Document with Arbitrary File...

Save with a .patch extension and your done.

CVS Bundle

The following applies to creating patch files for projects checked out through CVS.

There is also a CVS bundle available for the usual CVS tasks, i.e. committing, tagging, etc.. A diff command through CVS is also available but it's not suitable for submitting patch files but there is an way to overcome that.

First you have to get the CVS bundle. It is not included with TextMate by default. It's available through their SVN repository. Look in their documentation for instructions.

The easier alternative is to grab the bundle through the GetBundle bundle. -if it leads to a dead page then google it. validcode.net seems to be having trouble at the time of this writing.

After installing GetBundle goto the menu bar:

Bundles > GetBundle > Install Bundle.

From the drop down list select the CVS bundle and install. It will be located inside:

/Users/yourAccount/Library/Application Support/TextMate/Pristine Copy/Bundles/CVS.tmbundle

Make a backup of the bundle!

Right click CVS.tmbundle and select "Show Package Contents". The file you want to edit is located inside:
Support/versioned_file.rb.

Look for the lines reading:

  if other_revision
    cvs(:diff, "-r #{other_revision} -r #{revision}")
  else
    cvs(:diff, "-r #{revision}")
  end

And change it to:

  if other_revision
    cvs(:diff, "-up -r #{other_revision} -r #{revision}")
  else
    cvs(:diff, "-up -r #{revision}")
  end

Save then go back into TextMate and select the menu item:

Bundles > Bundle Editor > Reload All Bundles

Now when you go and select the cvs diff command, the output can be saved with a .patch extension and uploaded to an issue cue.

Warning! All patches will be created relative to the current directory (the directory in which the file is). This implies that it doesn't work properly for core patches!

Create patches on Windows

A variety of programs on Windows is able to generate patches. The command line utility diff can run on Windows either natively (Unxutils) or via an emulation layer (Cygwin).

If you require or fancy a graphical interface, you can use for example TortoiseCVS, TortoiseSVN or the IDE Eclipse. These programs require that you keep the code you are working on under version control with CVS or Subversion.

CVS

If you keep drupal or selected modules under version control with CVS, you can create patches using the built in diff command.

  • find the cvs.exe of your cvs package (WinCVS, TortoiseCVS, cygwin, ...) and make sure it is in your PATH
  • cd to your working copy (eg drupal root dir)
  • cvs diff -u [[-r rev1|-D date1] [-r rev2|-D date2]] [file_to_diff] [> file_to_diff.patch]
  • -u: unified format
  • -r: revision(s) to diff
    • no -r: compare the working file with the revision it was based on
    • one -r: compare that revision with your current working file
    • two -r: compare those two revisions
  • -D: use a date_spec to specify revisions. examples: "1972-09-24 20:05", "24 Sep 1972 20:05".
  • file_to_diff: path to the file or directory you want to diff. if you specify a directory, the output will include the diff of all differing files in this directory and all subdirectories.
  • > file_to_diff.patch: creates a patch - saves the diff in file_to_diff.patch instead of outputting it on stdout.
  • see the CVS manual for a complete list of and additional options

Line endings: an issue with using diff on windows is that generated patches have windows line endings, which makes them impossible to apply on unix boxes. unfortunately, there seems to be no way to convince "cvs diff" to output unix line endings. so the only way for making a proper patch on windows that i see is to convert / filter the output from "cvs diff" to unix line endings:

  • filter: pipe "cvs diff"s output through some dos2unix tool (like the one from Robert B. Clark, or like cygwins's dos2unix / d2u):
    cvs diff [options] file_to_diff  | unix2dos -u > file_to_diff.patch
  • convert: save "cvs diff"s output to a file:
    cvs diff [options] file_to_diff > file_to_diff.patch
    and manually convert file_to_diff.patch to unix line endings. every developers editor should be capable of this; besides, there are many dos2unix versions that operate on files.
Several win diff programs

(against a cvs source with the cvs.exe built-in diff. do diff local files, you need a windows diff program, command line or visual)

Generic:
  • find the cvs.exe of your cvs package (WinCVS, TortoiseCVS, cygwin, ...) and make sure it is in your PATH
  • cd to your drupal root dir
  • cvs diff -u [[-r rev1|-D date1] [-r rev2|-D date2]] [file_to_diff] [&gt; file_to_diff.patch]
  • -u: unified format
  • -r: revision(s) to diff
    • no -r: compare the working file with the revision it was based on
    • one -r: compare that revision with your current working file
    • two -r: compare those two revisions
  • -D: use a date_spec to specify revisions. examples: "1972-09-24 20:05", "24 Sep 1972 20:05".
  • file_to_diff: path to the file or directory you want to diff. if you specify a directory, the output will include the diff of all differing files in this directory and all subdirectories.
  • &gt; file_to_diff.patch: creates a patch - saves the diff in file_to_diff.patch instead of outputting it on stdout. if you send a patch, make sure it has the proper line endings
  • see the CVS manual for a complete list of and additional options
via WinCVS GUI
Just select the file you edited and right-mouse-click > "diff selection" (or press the "diff selected"-icon on the toolbar, or do Menubar > "Query" > "diff selection"). This brings up a "Diff settings" dialog box that offers some limited options as "revisions to diff" and "ignore whitespace/case" [update 2003-Feb-07: starting with WinCvs 1.3b11, "Full diff options [are] available from the diff dialog"]. The resulting diff is output to the WinCVS-Console and can be copied and pasted.
via WinCVS/TortoiseCVS external diff
  • WinCVS: Menubar > "Admin" > "Preferences" > "WinCVS" > "External diff program ". This program will be invoked by the "Diff selection" when "Use the external diff" is checked.
  • TortoiseCVS: CVS > "Preferences" > "External diff application". This program will be invoked by "CVS Diff ..."
Some external visual diff programs for Windows:

Notes:

  • While these programs do a nice job in showing file differences visually, side by side, non of them (as i can tell) allows to actually save the difference in unified format (most allow to save a standard diff, though) - update: TortoiseCVS lets you save patches. It does unified format by default. See its Make Patch option. Note that this 'Make Patch' option can make recursive patches when applied to directories.
  • You cannot specify the "-u" in the External diff preferences (eg "diff -u") as this will result in "Unable to open 'diff -u' (The system cannot find the file specified.)". A workaround for this is to, in the preferences, specify a batch-file that calls the external diff with the -u option. Another workaround is meta-diff, which allows for launching of special diff programs for certain file types.)

line endings: an issue with using diff on windows is that generated patches have windows line endings, which makes them impossible to apply on unix boxes [1][2]. unfortunately, there seems to be no way to convince "cvs diff" to output unix line endings*. so the only way for making a proper patch on windows that i see is to convert / filter the output from "cvs diff" to unix line endings:

  • filter: pipe "cvs diff"s output through some dos2unix tool (like the one from Robert B. Clark, or like cygwins's dos2unix / d2u):
    cvs diff [options] file_to_diff  | unix2dos -u > file_to_diff.patch
  • convert: save "cvs diff"s output to a file:
    cvs diff [options] file_to_diff > file_to_diff.patch
    and manually convert file_to_diff.patch to unix line endings. every developers editor should be capable of this; besides, there are many dos2unix versions that operate on files.
Subversion

If you keep drupal or selected modules under version control with Subversion, you can create patches using the built in diff command.

Issueing the following command in the directory of the working copy, will create a patch with differences between the latest revision (HEAD) and the working copy.

svn diff > file.patch

If you work incrementally, often commiting changes to your local Subversion repository and you want to generate a patch with changes between the original module (initial import or revision 1) and the working copy issue the command:

svn diff -r1 > file.patch

The output of Subversions built-in diff command misses the function names that make patch evaluation easier. To remedy this, replace Subversions built-in diff with an external diff utility. UnxUtils contains a native Windows binary that is suited for this.

  1. Install UnxUtils in a suitable folder, this page assumes c:\unxutils
  2. Create a batchfile diffup.bat in c:\unxutils containing the commands
    @echo off
    c:\unxutils\user\local\wbin\diff.exe -u -F^function %*
  3. Open %SystemDrive%\Documents and Settings\[Username]\Application Data\Subversion\config (usually on c:)
  4. Locate the section [Helpers] and add the line
    diff-cmd = c:\unxutils\diffup.bat

Subversion will now use the external diff program, generating patches that are easy to interpret.

Note: diff on Windows generates files containing CRLF line-endings. To enable the use of these patches on a UNIX system, convert to LF line-endings with a text editor or a dos2unix utility.

TortoiseCVS
Step 1: Checkout Drupal from CVS
  1. Download and install TortoiseCVS.
  2. Right-click anywhere, Checkout from CVS...
  3. Fill out the following settings:
  4. By default, Drupal HEAD is retrieved. If required, click the Revision tab, Choose branch or tag and select an older version, e.g. DRUPAL-5.
  5. Click OK.
  6. If prompted, enter the password 'anonymous'.
Step 2: Creating a patch
  1. Make whatever changes to the Drupal files.
  2. Right-click the "root" Drupal folder (e.g. "drupal"), CVS > Make Patch....
  3. Enter a name, Save.
TortoiseSVN

If you keep drupal or selected modules under version control with Subversion, you can create patches using the built in diff command.

TortoiseSVN provides a graphical user interface to the Subversion version control system.

To create a patch, simply right click on the working copy in Explorer, choose TortoiseSVN » Create patch. Choose the files to include in the patch and click ok. TortoiseSVN now prompts for the location where to save the patch.

In order to create patches that are easy to read, follow Create patches on Windows » Subversion.

Warning: TortoiseSVN creates patches comparing the latest revision and the working copy.

To create a patch comparing two earlier revisions, right-click the working copy, choose TortoiseSVN » Show log (this dialog can also be accessed from the Repository browser). Select the two revisions you want to compare, right-click one of the selected revisions, then choose Show differences as unified diff. Your default texteditor opens with the patch loaded.

Note: diff on Windows generates files containing CRLF line-endings. To enable the use of these patches on a UNIX system, convert to LF line-endings with a text editor or a dos2unix utility.

UnxUtils

UnxUtils is a collection of GNU utilities for Win32, including diff. Download UnxUtils.zip then extract to a suitable folder. Add the folder [folder]\usr\local\wbin\ to your Windows PATH variable (or invoke diff using the entire path [folder]\usr\local\wbin\diff).

HOWTO: Create patches contains information on the command line syntax.

Note: diff on Windows generates files containing CRLF line-endings. It also uses backslashes as path seperators.

To enable the use of these patches on a UNIX system, convert backslashes to forward slashes. Use a text editor or a dos2unix utility to change the end of line encoding to LF.

WinCVS

If you keep drupal or selected modules under version control with CVS, you can create patches via the WinCVS user interface.

Just right-click the file you edited and select "diff selection" (alternatives: the "diff selected"-icon on the toolbar, or the menu Query » diff selection). This brings up a "Diff settings" dialog box that allows one to select different options for the output. The resulting diff is output to the WinCVS-Console and can be copy-pasted and saved.

WinCVS also allows one to create patches via the command line.

WinMerge

WinMerge can show differences between two files in a visually attractive manner. To save the differences as a unified patch, select Tools » Generate Patch. Set the Style to Unified.

Submitting patches

Here is a list of things to look for when getting ready to submit a patch to a project, be it Drupal core or a contributed project. Following these tips will give your patch a better chance at being reviewed and committed. You can find information about actually creating a patch in the Creating patches section.

Coding style and security

If your code deviates too much from the Coding standards, it will be rejected without further review. You may want to check your code with the code-style.pl helper script or the Coder module to help you find style errors. Well commented/documented code will also be looked upon favorably. Make sure your code follows the security guidelines!. Obviously code that introduces security issues will be rejected.

Describe your changes

Describe the details of the change(s) your patch includes and try to be as specific as possible. Note that we prefer technical reasoning above marketing: give clear reasons why "this way" is good. Justify your changes with solid reasoning. It is important to note the version to which this patch applies. To help people test your patch (vs. reviewing the code itself) you should also provide some basic instructions about what problems to look for or specific steps to take to see the effect of the patch in a test site.

Verify your patch

Reviewers are overloaded reviewing and testing patch submissions. Please make their lives easier by assuring the following:

  • Your code works! Test your code.
  • If your patch is just a quick hack, then don't set your issue to "patch (code needs review)" status, instead set it to "active" or "patch (code needs work)".
  • For core Drupal, patch against HEAD. If the patch is accepted into HEAD it can then be ported back to specific versions. If the patch is for a core bug, you can help speed things along by attaching back-ported versions as separate follow-ups, each one with a version-specific port of your patch. You can only attach a single file to each issue reply, so to attach the DRUPAL-5 and DRUPAL-4-7 versions of your fix, you'd need 2 replies. For contributed projects, you should patch against the latest development version (which may or may not be HEAD - check with the project).

Submit your patch

Patches should be submitted via the issue tracker. Create a bug report or feature request, attach your patch using the file upload form. The issue tracker does not accept .zip, .tar.gz or .tgz files so you must attach a patch, not a package of all of your patched files. Set the issue's status to "patch (code needs review)" or "patch (code needs work)". Setting the status to patch is important as it adds the issue to the patch queue.

Patch spotlight: Code Freeze Cram-a-thon


Help us test patches for core! This page will feature important core patches that need to be reviewed and tested. You can help with this important task even if you don't know how to code.

Description

We've been given an additional four weeks to make Drupal 6 even more awesome, so let's rock it! The following are areas that the core committers have identified as the biggest areas to focus on. So far, we've been able to get every patch featured in the Patch Spotlight into core... can we do so this time? ;)

Cool new features

Internationalization

Please see http://groups.drupal.org/node/3714 for an up-to-date list of patches related to i18n. Some that could use particular attention include:

Performance

Form API improvements

Menu system improvements

File and Image handling

Testing

Each patch is slightly different, but it's good to run a battery of tests, especially against changes that have the potential to impact a great deal of the code. Try normal stuff like installing Drupal, creating a few nodes (including "oddball" nodes like polls), adding a content type, etc. Do some common tasks like creating categories and forums, and do some not-so-common tasks like testing sure node revisions, internationalization, aggregator and profile modules. Be creative and try to think 'outside the box' and try things that the developer might not have thought to test.

What do I do if I find a bug while testing a patch?

Try to reproduce the bug on a 'clean' copy of Drupal HEAD. For patches without database changes, you can "reverse" apply a patch to remove it:

patch -p0 -R < patchfile.patch

For patches that make changes to the database, you'll need a clean checkout elsewhere to compare against.

Check to see if the bug manifests itself without the patch. And note: don't forget to re-apply afterwards:

patch -p0 < patchfile.patch

If not, it's usually safe to assume it's caused by the current patch. Mark the issue "code needs work" and describe in detail what you experienced and how the developer can re-produce the steps.

If so, then there is a deeper problem with core... see if someone has already reported a bug on it by searching the issue queue. If not, feel free to file a bug.

General patch reviewing tips

Here is a list of general resources for applying and testing patches.

Tips and Tricks

Here are some tips and tricks (scripts, etc. to make patching easier).

Patching shortcuts

(Reposted with permission from http://acko.net/blog/handy-drupal-core-development)

Some quick tips for better productivity when developing Drupal core:

  • Alias your editor to e. If you use a GUI editor see if it comes with a command-line shortcut to use (to avoid having to browse to the file again). TextMate by default has /usr/bin/mate. Not nearly short enough ;).
  • Set up a d (diff) command to perform diffs. I use the following:
    cvs diff -u -N -F^f . | grep -v -e ^\? > $1.patch
    e $1.patch

This takes a patch name as the argument. It will do a diff and open up my editor afterwards so I can review the patch before submitting. The grep strips out unnecessary junk (unknown files).

  • Set up a p (patch) command to apply patches. I use the following:
    wget -O - $1 | patch -p0

    This will take a patch URL and apply it locally.

  • Set up a c (clean) command to clean a CVS tree:
    cvs up -C -d .
  • Finally top it off with this fun gem t (try out):
    c; p $1; e .; d d

    This command takes a patch URL as its argument, will apply it to the local check out after cleaning it up, and will then open the editor on the root as well as show you the final diff against latest CVS.

  • The attachment below contains bash scripts for these commands, but requires the e alias to work.

sed - replace text in multiple files

sed - is useful to find and replace text in single multiple files.

  • Replacing foo with foo_bar in a single file.

    sed -i 's/foo/foo_bar/g' somefile.module
    • -i = tell sed to edit the file(s)
    • s = substitute the following text
    • foo = what you want to substitute
    • foo_bar = what you want to replace
    • g = global, match all occurrences in the line
  • Replacing foo with foo_bar in a multiple files.

    sed -i 's/foo/foo_bar/g'  *.module
  • Now you can run cvs diff -up > yourpatchfile.patch to create a patch.
  • sed is available on the Win32 platform by installing GNU utilities for Win32

Drupal's APIs

If you are interested in developing Drupal modules or hacking away at the Drupal core then this is the place to find details about all the functions and classes defined in Drupal.

Drupal.org uses Doxygen to automatically generate documentation from the latest drupal sources. This allows us to ensure that documentation is up-to-date, and to simultaneously track multiple versions of the documentation.

API Documentation is available from api.drupal.org for:

Example modules can be found in contributions/docs directory in CVS. See Your own api.drupal.org site for more information on how it's setup.

Please also read the Drupal Coding Standards page, which contains some guidelines for writing Doxygen comments.

Cache API and caching tutorials

General introduction and tutorials

A good place to start is with Jeff Eaton's tutorial: A Beginner's Guide To Caching Data. It is a nice introduction on why and how to cache data.

Note the approach presented in the tutorial: the data is first cached statically for reuse within the same request, then cached on the database for reuse across requests.

References

Note that because of a limitation of the api.drupal.org web site, you may not be able to view online the proper documentation of cache_set(), cache_get() and cache_clear_all(), because the site will return the documentation for the function declared in cache-install.inc instead of those of the same name declared in cache.inc as one would have expected. The former are empty shell functions, used during the installation process, while the latter are those you would normally use.

Further information

What value to use for $cid?

The $cid (cache ID) uniquely identify a cached element in a {cache} table.

The $cid value doesn't matter -- you make your own (any string). You'll want to make it something that you can easily recreate when it comes time to cache_get the stored data. So your cid could be something like "foo_", $node->nid or whatever. Just take reasonable efforts to make sure that it will never collide with any other cid (if you're putting it in the cache table and not your own cache_foo table). Using a strategy like foo::id is probably fine.

What cache table to use?

If you are going to be caching a lot of entries, it might be useful to create a separate cache table for your module to use (e.g. {cache_foo}).

If a certain piece data is highly perishable (i.e. if the date becomes obsolete quickly), you may want to cache it in the {cache_page} table: this is the default cache table. Many core modules (node, taxonomy, etc.) call cache_clear_all() without argument each time a node or a taxonomy term is added/edited/deleted. The whole {cache_page} table is then flushed.

Clearing obsolete data

As explained in the tutorial (link above), the cache tables may be flushed at any time, so you must at all time be able to recreate the data from the information stored in other places in the database.

You decide when a piece of cached data is no longer relevant and then call cache_clear_all with the cid you want to clear. That's unless you decide to put your data in one of the core {cache} tables, and prefer to rely on synchronizing with the core modules and rely on them to clear the cache.

Development

Note that in good Drupal tradition, the API is constantly changing.

In Drupal 5 you should serialize and unserialize complex data structures when calling cache_get and cache_set. In Drupal 6 these structures will be recognized automatically and the un/serialization is handled by the cache API.

Forms API

This outline is meant as a starting point for those who wish to further documentation of Drupal's form API. Many of the basics have already been addressed in the Quickstart Guide, which we may wish to expand into full forms doc.

  1. Overview
  2. Form creation
    1. Overview
    2. Building individual fields
      1. Example
      2. Declaring field names
        1. Nesting
        2. Tree attribute
      3. Declaring properties/attributes
        1. Link to list of all properties/attributes available
    3. Building form groups
      1. Overview
      2. Example
      3. Fieldset attribute
      4. Collapsible form groups
    4. Declaring markup
      1. Prefix/Suffix
        1. Example
      2. Markup attribute
        1. Example
    5. Weighting elements/groups
      1. Overview
      2. Example
    6. Returning forms
      1. drupal_get_form()
        1. When to use drupal_get_form
        2. Example
      2. Returning form elements
        1. When to return form elements
        2. Example
  3. Theming Forms
    1. Overview
      1. When to use form markup
      2. When to use separate theming function
    2. Example
    3. form_render()
      1. Rendering individual elements
      2. Final render
    4. Looping through the $form array
      1. Using element_children
    5. Theming individual form elements
    6. Theming form elements in a table
      1. Building the rows
  4. Validating forms
    1. Overview
    2. Example
  5. Executing forms
    1. Overview
    2. Example
    3. Converting from the POST/switch approach
    4. confirm_form()
      1. Overview
      2. Example
  6. Overriding Forms – this section needs work. No code has been written for these features as yet AFAIK
    1. Altering form elements
      1. Overview
      2. Example
    2. Custom theming
      1. Overview
      2. Example
    3. Custom validation
      1. Overview
      2. Example
    4. Custom execution
      1. Overview
      2. Example

Forms API Quickstart Guide

For an introduction to how the forms API works, check out the
Forms API Quickstart Guide.

Form API Workflow - Updated to Drupal 5

Getting to know the workflow of forms in Drupal 5 took me a lot of time searching and trying, the differences with 4.7 are often only small.

So here's a story about how a form works in Drupal 5. This information will not be complete, more info can be found in the Forms API Quickstart Guide (http://api.drupal.org/api/5/file/developer/topics/forms_api.html) and the Drupal Form API. This is merely ment as a clarification to the workflow.

To add a form to the page you're building, you put in this code:

<?php
$page_content
.= drupal_get_form('traveltrophy_form_newevent');
?>

'traveltrophy_form_newevent' is the $form_id, and to build the form, Drupal looks for a function with the same name:

<?php
function traveltrophy_form_newevent() {
 
$form = array();
 
 
$form['name'] = array(
    
'#type' => 'textfield',
    
'#title' => t('name'),
      );
                 
 
$form['distance'] = array(
    
'#type' => 'textfield',
    
'#title' => t('distance'),
     );

 
$form['submit'] = array (
    
'#type' => 'submit',
    
'#value' => t('Go'),
     );

  return
$form;
}
?>

This is enough to build a form and display it on the screen. The different elements to be used in $form can be found in the Form API (http://api.drupal.org).

Remember that the $form_id was 'traveltrophy_form_newevent', so we need two more functions for the form to work: (_validate & _submit)

<?php
function traveltrophy_form_newevent_validate($form_id, $form_values) {
    if (
$form_values['name'] == '') {
       
form_set_error('', t('You have to put in a name'));
    }
    if (!
is_numeric($form_values['distance'])) {
       
form_set_error('', t('The input for distance has to be a number'));
    }
    if (
$form_values['distance'] == 0) {
       
form_set_error('', t('You have to fill in a distance'));
    }
}
?>

form_set_error(,) puts the form back as it was with the input the user just gave, and displays an error above it.

_submit is called if _validate did not return errors

<?php
function traveltrophy_form_newevent_submit($form_id, $form_values) {
     if (
user_access('moderate traveltrophy')) {
      
db_query("INSERT INTO reis_events VALUES (NULL , '%s', %d)", $form_values['name'], $form_values['distance']);
      
drupal_set_message(t('The input has been stored'));
    }
     return
'traveltrophy/listevent';
}
?>

The return value is optional, it's the relative path to which the user is redirected after submission. If no return value is given, the current page is refreshed.

If you want to pass some variables to the form while building it, the drupal_get_form function can be called like this:

<?php
$page_content
.= drupal_get_form('traveltrophy_form_newevent',$id,$whatever,..);
?>

And gives input to:

<?php
function traveltrophy_form_newevent($id,$whatever,..) {
 
$form = array();
 
//..
}
?>

Forms API reference

For a comprehensive listing of form elements and their associated attributes, please see the Forms API Reference.

Dynamic and Multipage forms with Forms API

The release of Drupal 4.7 introduced the Form API, a framework for building, displaying, validating, and processing HTML forms. With it, forms are defined as structured arrays, and those structured arrays contain all the information necessary to properly handle the form throughout its life cycle. This approach also makes it possible for modules to customize other forms (adding additional fields to a signup page, for example), and allows designers to customize the on-screen display of forms using overridable theming functions. It also makes validating form input, and avoiding form tampering, much easier. That's great!

The tradeoff of those enhancements was the loss of flexibility in certain complex scenerios -- in particular dynamic forms that change based on user input, and multi-step 'wizard' style forms. With the introduction of Form API 2 in version 5 of Drupal, though, we've eliminated the limitations that made those cases so difficult.

Dynamic Forms: Three Different Scenerios

For the purposes of this article, we'll be looking at three specific kinds of dynamic forms:

  1. The Long Form, a large form divided into multiple steps for ease of use
  2. The Wizard, a form where a user's answers in each step determine the options available in the next
  3. The Form That Builds Itself, a page where some options (clicking 'add more choices' when setting up a poll, for example) add more fields until the user chooses to submit the 'finished product.'

While the first scenerio isn't really dynamic (it simply presents different subsets of options to the user at each step along the way), all of these scenerios work by changing the form depending on the data that the user has just submitted. And that means that all three scenerios encounter the same options in the current Form API. Why is that? Read on...

Form API 1.0

Now that we have a handle on the different kinds of forms we're dealing with, let's take a look at the current state of affairs with Drupal's form-building. It will help us understand where the problem lies.

  1. Your page-building function is called.
  2. It builds a form definition array.
  3. It calls drupal_get_form($form_id, $form), passing in a unique ID for the form and the form's definition array.
  4. drupal_get_form() first checks to see if there is incoming POST data. If there is, and the form_id in that POST data matches the $form_id you passed in, it knows that the user is submitting the form and begins the 'processing' stage.
    1. In the processing stage, drupal_get_form() passes the form array to drupal_validate_form(). Any validation handlers added to specific fields are processed there.
    2. If the validation stage completes and no errors are found, drupal_get_form() hands the form off to drupal_submit_form(). In that stage, data is processed, database changes are made, and so on.
    3. drupal_submit_form() can return a URL to redirect the user to when form processing is done. If it does, drupal_get_form() redirects to that page and the Form API is finished.
  5. If there is no incoming POST data. the processing stage returned validation errors, or no redirect URL was returned by drupal_submit_form(), the form array is passed to drupal_render_form() and the actual HTML is generated.
  6. Your page building function receives the generated HTML from drupal_get_form(), complete with hilighted errors if appropriate, and displays it to the user.
  7. If the user enters in values (either filling them in for the first time, or correcting errors) and clicks the Submit button, the form submits to itself, triggering the page building function in step 1... And the cycle repeats.

The Problem With Dynamic Forms

That system works well for static forms: build the array, check for incoming POST values, validate, submit, and optionally render for display. Since a Form API form always submits to itself, the same definition array is used when first displaying the form, and validating it. Unless, that is, you need to display forms that change...

In that case, you run into the following problem:

  1. Your page building function generates the 'starting point' for your dynamic form.
  2. drupal_get_form() renders it, and displays it.
  3. The user enters their choices and clicks submit.
  4. Your page building function looks at the incoming POST values, and creates a form with new or additional options based on the user's choices.
  5. Your page building function hands the form off to drupal_get_form()... but the fields it contains no longer match the values coming in from the user! Validation functions throw errors, and everything grinds to a halt.

What we really need to do is build two form definition arrays. The first should be a duplicate of the form from step 1, to use during the processing of incoming form values. The second should be the 'new' form from step 4, to display to the user if the first one passes validation.

Form API 2.0: The Solution


In Drupal 5.0, the freshly retooled Form API adds just that: the ability to process incoming values with one form array, while displaying a new form array for user input. Three changes make this possible.
  1. Forms are now built by dedicated functions, and only the form's ID needs to be submitted to drupal_get_form().
  2. These form building functions can accept parameters, like the node object to be edited or a collection of values representing the user's submitted data.
  3. When a form submits to itself, drupal_get_form() can save the parameters used to build the form array. The next time it submits, it will re-use the stored parameters to recreate the first form for processing, then display the newly built form to the user.

The third item in that list is one of the most important: all you need to do is create your form building function, and drupal_get_form() will handle pulling up the right version of the form (one for processing, one for display) during each phase. Because this behavior is only necessary for some forms, drupal_get_form() only stores that information if the #multistep property is set to TRUE in your form definition array.

How to Handle the Three Scenerios

Let's step back for a moment. We now understand the cause of the problem in Form API 1.0: a mismatch between the user's submitted input, and the form array that's build based on it. We also understand how Form API 2.0 fixes that, allowing one form array to be used for processing and another for display. With that in mind, how can your module handle the three dynamic form scenerios outlined at the beginning of the article?

The Long Form

As with all of these scenerios, the real action will happen in your form building function. It will use hidden fields to indicate what 'stage' is currently being displayed, and to store the user input from previous stages. While this was theoretically possible using Form API 1.0, the new features make it much simpler. Here's an example of how your form-building code would look:

<?php
function my_form($param1, $param2, $form_values = NULL) {
 
// In a multistep form, drupal_get_form() will always
  // pass the incoming form values to you after any other
  // parameters that you specify manually. Do this instead
  // of looking at the incoming $_POST variable manually.

 
if (!isset($form_values)) {
   
$step = 1;
  }
  else {
   
$step = $form_values['step'] + 1;
  }
 
 
$form['step'] = array(
   
'#type' => 'hidden',
   
'#value' => $step,
  );

  switch (
$step) {
    case
1:
     
// Create the fields for the first step of your form here
     
break;

    case
2:
     
// First, add a hidden field for each of the incoming
      // form values.
      // Then, add the fields regular form fields that the user
      // will see in this second step.
     
break;
   
    case
3:
     
// And so on and so forth, until you've reached the final
      // step.
     
break;
  }
 
 
// This part is important!
 
$form['#multistep'] = TRUE;
 
$form['#redirect'] = FALSE;
 
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Submit'),
  );

  return
$form;
}
?>

In the validation code for your form, you can check the 'step' field to see which set of fields you need to check, and display any errors. The function will keep accumulating hidden values and displaying a new set of fields until it reaches the final step. You'll probably want to prevent your form submission handler from processing the form data until all the steps have been completed. To do that, something like this would be effective:

<?php
function my_form_submit($form_id, $form_values) {
 
$final_step = 10;
 
  if (
$form_values['step'] == $final_step) {
   
// Process the form here!
 
}
}
?>

The Wizard

This scenerio, from a code perspective, is almost exactly the same as The Long Form. Depending on how your Wizard works, though, you may want to have your form submission handler actually process each step's data, rather than storing it as hidden fields in the next step. Or, you may want to process 'batches' of steps together before proceeding. For example:

<?php
function my_form_submit($form_id, $form_values) {
  switch (
$form_values['step']) {
    case
3:
     
// Process the form data accumulated in the first three steps
     
break;
    case
9:
     
// Process the form data accumulated in steps 4 through 9
     
break;
    case
10:
     
// Process the form data from step 10...
     
break;
  }
}
?>

For very complex wizards, you may also want to split out individual steps as helper functions, but you'll still need to use the 'core' function as the central dispatcher. It's the one that the Form API knows to call automatically during the building and processing stages.

The Form That Builds Itself

The final scenerio is a bit trickier. Rather than dividing the form submission process into discrete steps, it involves a user making choices that continue to change the form (by adding new fields, in most cases) until they're happy with the results. How would we handle it?

In the example below, our hypothetical module is displaying a form that allows a user to create a quiz. Some fields, like the title and description of the quiz, will always be present. The first three slots for 'quiz questions' will always be available, too. But if the user selects the 'Give me more questions' checkbox, it will build the form with three more boxes.

<?php
function my_form($param1, $param2, $form_values = NULL) {
 
// Build the fields that stay the same from form to form...
  // And populate the default_values for each field with the
  // corresponding entry from $form_values
 
$form['title'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Quiz title'),
   
'#default_value' => isset($form_values) ? $form_values['title'] : '',
  );

 
// The current number of questions, plus three more if
  // the user requested them.
 
if (isset($form_values)) {
   
$question_count = $form_values['question_count'];
   
    if (
$form_values['more_questions'] == 1) {
     
$question_count = $question_count + 3;
    }
  }
  else {
   
$question_count = 3;
  }
 
 
$form['question_count'] = array(
   
'#type' => 'hidden',
   
'#value' => $question_count,
  );

 
// We'll loop from 1 to n, where n is the current number of questions to
  // be displayed. We'll automatically populate each question with any data
  // that was entered in the previous trip through the form.
 
for ($i = 1; $i <= $question_count; $i++) {
   
$form['question_' . $i] = array(
     
'#type' => 'textfield',
     
'#title' => t('Question !count', array('!count' => $i)),
     
'#default_value' => isset($form_values) ? $form_values['question_' . $i] : '',
    );
  }

 
$form['more_questions'] = array(
   
'#type' => 'checkbox',
   
'#title' => t('Give me more questions'),
   
'#return_value' => 1,
  );

 
// This part is important!
 
$form['#multistep'] = TRUE;
 
$form['#redirect'] = FALSE;
 
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Submit'),
  );

  return
$form;
}

function
my_form_submit($form_id, $form_values) {
  if (
$form_values['more_questions'] == 1) {
   
// Don't process the form. We're rebuilding it with more question fields.
 
}
  else {
   
// Loop through all of the questions
   
for ($i = 1; $i <= $form_values['question_count']; $i++) {
     
$current_question = $form_values['question_' . $i];
     
// Process $current_question
   
}
  }
}
?>

The above code is a bit more complicated than the previous scenerios, but what it's trying to accomplish is pretty simple. When it first displays, it presents the user with a Title field and three empty question fields. It also has a hidden field storing the current number of questions, and a checkbox indicating that the user wants more options.

The form submission function checks to make sure that the user isn't in the process of adding more options before trying to process anything. When they finally submit without that checkbox selected, it loops through the list of question fields and saves each one individually.

The Wrapup

What have we learned? With the new features in Form API 2.0, it's possible to create forms that change each time a user submits them, validating as they go, and processing the results at arbitrary points along the way. There are different approaches to this task, depending on what kind of user interface your form requires, but most depend on storing information about the form in hidden fields, and using it to control how the next step is built.

Large, complex forms can lead to large, complex code. If you run into challenges, or can't get something working, remember to break it down into pieces and trace your way through the Form API workflow. Make sure you're doing the right thing at the right time, and that the values you expect to be getting in your builder function are arriving properly.

Happy coding!

Multipage forms with the Forms API (4.7)

In most cases, web-based forms are one-page affairs: fill in all these fields, click "Submit", and get your result. Naturally, Drupal's Forms API excels at making these types of forms. Occasionally, however, there is a need to make a multipage form: a form which stretches across multiple pages and isn't truly "submitted" until the final part is completed. This document addresses an approach to these types of forms. We will assume the following requirements, which are probably more than you actually need, but contain useful tips for style and code structure.

  • Your multipage form has four pages.
  • Your multipage form isn't finished until the "Submit" on page 4.
  • Your multipage form is a node type, and exists in hook_form.
  • Your multipage form hides certain elements on different parts.
  • Your multipage form requires certain elements to be filled in.
Why can't I make multipage forms with what I already know?

Before we begin, the most worrisome question is: why does this document need to exist? To answer that, you'll have to understand the regular workflow of a form in the Drupal Forms API. At its simplest:

  • Build
  • Validate
  • Submit
  • Display

The "build" phase is what happens when you code your form - Drupal will take all your form elements and build them internally. Since this built form is often pre-filled with data (in the case of editing, or based on values from $_POST), the next step is "validation", which determines if the values of the form are legitimate for the elements in question (ie., #required fields have values, select #types have values which match an entry in its $options, etc.). Finally, there's the optional "submit" which only happens if the form has been submitted, and the "display" which kicks in when the form is actually being visually rendered in the browser.

This is just dandy for your regular one-page forms.

But, when you consider multipage forms, there comes a time when you need two "build" phases: one for the previous part (that the user has just "submitted") so that it may be properly validated, and another build phase for the current part (that the user is about to view):

  • Build the previous part for validation purposes.
  • Validate the data received from the previous part.
  • Submit (as before and above, again optional).
  • Build the next part if there were no validation errors.
  • Display the next part.

That's where the Forms API #pre_render comes in. The #pre_render is an array of function names that you set during your normal "build" phase, and which will be called the split-second before the form is actually displayed to the user. This creates the following workflow, which gives us exactly what we need: the chance to modify our form for the current part after the validation phase for the previous part has finished:

  • Build
  • Validate
  • Submit
  • #pre_render
  • Display
A Fully Working Example

Let's show off some code. As per our feature requirements at the beginning of this document, we're doing a custom multipage node type, so take a look at multipage_form_example_form in the fully functional multipage_form_example.module.

  • Always build your form as if all your form elements were on a single page - this keeps your declarations all in one place, which is stylistically neater. The hiding of the elements on their specific parts will be handled in the #pre_render function.
  • Don't use #required in these declarations either.
  • You can use fieldsets to group your form elements together, but you won't be able to set the #type until the #pre_render function. If you set the fieldset in the initial declarations, you'll have to choose between having the fieldset appear on every page of your multipage (which may be fine for some forms), or not passing the fieldset's child values from part to part (which would generally be a bad idea, unless the fieldset happened to occur on the final part of the form).

Besides #pre_render, which we'll get to shortly, the other important part of our multipage is the powerful hook_form_alter of Drupal's Forms API. Look at multipage_form_example_form_alter: you'll notice that we initialize or, in the case of an existing value, set the current part number that we're on, and modify it as needed depending on if the "Back" or "Preview" has been clicked. Notice that we also modify the #validate and #submit hooks. Instead of just adding our custom functions, we merge with any existing values, which is especially important for custom node types (as we'll need the nodeapi to kick in as normal once our multipage is finally submitted).

The last crucial bit needed for multipages is that #pre_render function we've been talking about - astute readers may have guessed that it is run twice: once in multipage_form_example_form_alter to set the validation requirements for our current/previous page, and then again by Drupal's Forms API, which is when we know that we should increment our counter and show the next form part (as validation has been passed successfully for our previous part).

multipage_form_example_pre_render is the workhorse behind our demo module, and is responsible for setting visibility of elements, whether they are #required, what buttons are available for clicking, and any other visual indication that the user has progressed from page to page. Our logic here is based around using in_array to determine if our current part number is in an array of valid part numbers - which makes it quite simple to allow an element to appear on multiple pages without increasing the size of the code with equality checks.

More information about multipage forms is sprinkled throughout this demo code. Realize, however, that there are still hiccups along the way - 'checkboxes' and multiselect arrays (or any hidden form value that requires array values) are still difficult to pull off. These issues will probably be addressed in a future version of Drupal.

Redirecting Users After Submitting a Form in Drupal 5

If you create web sites based on Drupal you know that controlling forms, how they are themed, validated and processed, is a highly valued skill for any serious developer. This article will discuss a few ways to redirect your users to another page after they've submitted a form, overriding Drupal's defaults.

Drupal 5 provides developers with a Forms API, a clever way to craft HTML forms using logical and extremely flexible arrays.

Drupal Forms API also allows developers to theme every little detail of a form. For simpler cases using the #prefix and #suffix properties can be enough, but if you need extreme power, and sooner or later you will, then you can use forms-exclusive theme functions and drupal_render().

I'll explain the basics of Forms API in a future article, if you can't wait I suggest taking a look at the Forms API Quickstart Guide, for now let's focus in redirecting users after form submission in Drupal.

In the beginning was the form function

Every form in Drupal starts as a function that creates an array, usually named $form. Then the function's name is passed to drupal_get_form().

As every function in a module, a form creation function should start with the name of the module, for example:

boogeeks_notify_form()

First, this assumes my module is named boogeeks, the first word in my function's name. I'm separating each word with an underscore and I suggest you do the same.

Second, I've chosen the verb notify to have a clear idea of what the form will do. I used something like this in a recent project for hacking a form where users opt-in to receive email alerts.

Third, I added form, helpful when you have many functions in your module and need a quick way to tell apart the ones creating forms.

Notice that the only requirement for naming your form function is the first one, starting with your module name, the other two are my suggestions for cleaner and easier to understand code.

Theme, validate and submit

Now that we have boogeeks_notify_form() taking charge of form creation we need a few additional functions, all of them named based on the original.

  • theme_boogeeks_notify_form(): Will take care of the looks of your form, this is the theming function.
  • boogeeks_notify_form_validate(): Will check if user entered information that validates according to rules established here.
  • boogeeks_notify_form_submit(): Will process the form and redirect the user to other page.

For this article's purposes we are interested in the third function, the one ending in _submit.

Where to go now?

Every form submission function, boogeeks_notify_form_submit() in our example, needs to return a value, this value is the url where the user will go after successfully submitting a form.

So, if you want to thank your user after opting in to receive email alerts then you can finish boogeeks_notify_form_submit() with something like:

return 'thankyou-for-subscribing';

That's the url of some page with the usual thank-you-we-love-you-dear-visitor stuff.

All Drupal provided forms have pre-defined values in their submit functions. Two well known and often used ones are user_login() and user_register(), used to create new accounts, both are created in user.module.

How to override default form redirection in Drupal

There are two ways to override default form redirection, with a destination parameter in a url and using the #redirect property of Forms API. After some testing, I know this could be obvious to hardcore Drupal developers but it's still useful for new comers, I determined the redirection processing flow is as follows:

  • The url returned from the form submit function,
  • which is overriden by #redirect,
  • which is overriden by destination passed in the url.

You'll need to modify a form using hook_form_alter() to use #redirect.

If you want to work with destination you'll have to include it in the url used to call the page showing your form.

Now you can take your users whenever you want after submitting a form, no matter if it's a Drupal provided form or one that you've coded.

Bonus: got a headache with redirection after registration?

Yeah, me too, at least until finding a small bug in user.module, maybe that's your case.

Upgrading to forms API

NOTE: Further details about how the form API works can also be found in the API reference on the following pages:

Also see the Form Updater module, which can help you get a head start in converting your modules.


Before embarking on the journey to update a module to the new forms API, it is helpful to understand more about the conversion process in general, and the benefits it provides. First, it should be noted that for many module maintainers, converting their module to use the new forms API will be the most difficult and time-consuming conversion yet. "Why is this so much harder to implement than the old approach?" This thought will probably come up more than once if the module being converted has any degree of complexity regarding forms. And the answer to the question is twofold:

  1. The new API is more complex.
  2. The approach is fundamentally different.

It's much easier to get a handle on reason #1 if reason #2 is understood and accepted. :)

The change requires more than simply shifting some code around--it requires re-learning how forms are implemented in Drupal. Unlike the pre-4.7 approach, the creation and handling of form elements is now completely separate from the theming of those elements. This brings both a greater degree of complexity, and a greater degree of flexibility and power to Drupal forms.

Taking the time to learn and implement the API has the following major advantages:

  1. Security
    • It filters the variables passed through _POST, so that only variables that were on the form will be accepted as input
    • It validates select, checkboxes and radios form elements so that only listed options can be checked. Example: the filter_form now validates itself (huge security benefit)
    • It allows you to add additional validation through the form_validate hook, which means that it's a lot simpler to write validation code, and to reuse said validation code. Example: you only need to write the valid_integer function once, and then you can specify whichever form elements need to conform to that.
  2. Extensibility
    • Forms can be reordered
    • Additional form fields can be added
    • The default way a form is validated/executed can be overridden
    • Additional form element types can be added
    • Behavior of default element types can be overridden
  3. Themability
    • Themers can override the default layout of any form or a part of it in their theme
    • Themers can individually theme a single element, a group of elements, or all elements of the same type. Example: one can write a module which would theme multiple selects with checkboxes.

These advantages are available in any form, including those generated by Drupal core!

In addition, this functionality is a major and necessary step towards CCK.

Example module conversion: Project Module
Overview

This document provides step-by-step instructions on how various parts of the Project Module were translated to the Drupal 4.7 Forms API. It covers a wide range of situations module developers are likely to encounter, including:

  • Converting a hook_settings implementation
  • Converting various hook_form implementations
  • Breaking form markup out into theme functions
  • Working with fieldsets and form inupts
  • Implementing a 2-page "wizard" style form

The functions are organized by approximate order of difficulty, with the easiest at the beginning and the most difficult towards the end. While in most cases a reader may skip directly to sections which interest him/her, it is highly recommended that the first function translation be read in its entirety, in order to provide a detailed look at how the process of form conversion works.

If you wish to follow along, you can obtain a copy of the project module from November 5, 2005 (just before the new form changes went through) by performing the command:

$ cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -D "05 Nov 2005" -d "project" -P "contributions/modules/project"
Conversion Tips
Form Updater

Download the Form Updater module and install it in the usual fashion (download it, extract it, copy the folder to your modules directory, and enable it through administer >> modules). Then click on the new form updater link in the menu to bring up the form updater interface.

Form Updater module

Form Updater module

Then simply copy and paste the contents of .module and .inc files, and it will automatically attempt to find legacy form function calls and recommend Drupal 4.7 equivalents:

Form Updater recommendations

Form Updater results

While this module is not a complete solution to forms API conversion (it does not catch everything, and human intervention is still required in order to place the code in the correct place), it is nevertheless an essential tool.

Common legacy form errors

If you try and use a contributed module in Drupal HEAD (Drupal 4.7) you may be confronted with errors about any of the following functions:

  • _form_get_error
  • form
  • form_checkbox
  • form_checkboxes
  • form_group
  • form_radio
  • form_radios
  • form_textarea
  • form_textfield

This is an indication that this module has yet to be converted to the Drupal 4.7 Forms API. Lines which contain calls to any of these functions inside a module will need to be translated to the new API.

When performing the conversion, once you have identified a hook or function where an error is occuring, it is often helpful to comment out the entire inside of the function, and then uncomment lines as you get them converted, reloading the offending page to view your changes as you go forward.

project_settings: Converting a hook_settings implementation (Easy)
Overview

The first form to investigate is the project module's settings form. This is a very straight-forward form conversion which only requires taking existing form_* function calls and converting them into arrays.

Project settings form

Project settings form

Original function

The following is the original project_settings function which generates this form:

function project_settings() {

  $project_directory = file_create_path(variable_get('project_directory_issues', 'issues'));
  if (!file_check_directory($project_directory)) {
    $error['project_directory_issues'] = theme('error', t('Directory does not exist, or is not writable.'));
  }

  $versions = array(-1 => t('all')) + project_releases_list();

**  $output = form_textfield(t('Release directory'), 'project_release_directory', variable_get('project_release_directory', ''), 50, 255, t('Leave this blank if project maintainers are to create their own release packages. This is useful if releases are generated by an external tool.'));
**  $output .= form_radios(t('Unmoderate projects with releases'), 'project_release_unmoderate', variable_get('project_release_unmoderate', 0), array('Disabled', 'Enabled'));
**  $output .= form_checkbox(t('Browse projects by releases'), 'project_browse_releases', 1, variable_get('project_browse_releases', 1), t('Checking this box will cause the project browsing page to have version subtabs.'));
**  $output .= form_radios(t('Default release overview'), 'project_release_overview', variable_get('project_release_overview', -1), $versions, t('Default release version to list on the overview page'));
**  $output .= form_textfield(t('Issue directory'), 'project_directory_issues', variable_get('project_directory_issues', 'issues'), 30, 255, t("Subdirectory in the directory '%dir' where attachment to issues will be stored.", array('%dir' => variable_get('file_directory_path', 'files') .'/')));

  if (module_exist('mailhandler')) {
    // TODO: move this stuff to mailhandler.module ?
    $items = array(t('
<none>'));
    $result = db_query('SELECT mail FROM {mailhandler} ORDER BY mail');
    while ($mail = db_result($result, $i++)) {
      $items[$mail] = $mail;
    }

**    $output .= form_select(t('Reply-to address on e-mail notifications'), 'project_reply_to', variable_get('project_reply_to', ''), $items);
  }

**  return $output;
}

Note that lines prefixed with ** indicate a line that will cause problems with Drupal 4.7.

Form conversion

Begin by navigating to administer >> settings >> project. You will receive an error:

Fatal error: Call to undefined function: form_textfield() in \modules\project\project.module on line 119

The error tells the name of the file from which the error is originating: project.module. Copy and paste the contents of this file into the Form Updater module, and it will identify six calls to the old forms API (out of seven lines identified as problematic). Here is the first call as an example:

form_textfield(t('Release directory'), 'project_release_directory',  variable_get('project_release_directory', ''), 50, 255, t('Leave this  blank if project maintainers are to create their own release packages.  This is useful if releases are generated by an external tool.'));

This creates a textfield with:

  • a title of Release Directory
  • an internal name of project_release_directory
  • a default value containing either the contents of the project_release_directory variable in the Drupal variable table, or an empty string if that does not exist
  • a size (or length) of 50 characters
  • a maximum length of 255 characters
  • a description of "Leave this blank if project maintainers are to create their own release packages. This is useful if releases are generated by an external tool."

In the new API, this translates to:

$form['project_release_directory'] = array(
  '#type' => 'textfield',
  '#title' => t('Release directory'),
  '#default_value' => variable_get('project_release_directory', ''),
  '#size' => 50,
  '#maxlength' => 255,
  '#description' => t('Leave this blank if project maintainers are to create their own release packages. This is useful if releases are generated by an external tool.'),
);

Notice that the element name is now moved to the name of the array index in $form (in this case, project_release_directory). Also note that each array index is labeled specifically what it means. #title => t('Release Directory') is the form element's title (or text displayed above it). #default_value is its default value, and so on. You are no longer required to memorize what order function arguments go in, or to refer to the API reference to determine this. Attributes can be placed in any order (although it is customary to keep them in the same order throughout), or even removed altogether if they are not required.

Replace the form_textfield call with the code generated by Form Updater. Repeat for each element that Form Updater finds.

hook_settings changes in 4.7

Save project.module at this point and reload the settings page.

Project settings page after replacing form values

Project settings page displaying no form

While there is no longer an error displayed, something is clearly amiss. Where did the form go? Check the very last highlighted line for the answer:

return $output;

In Drupal 4.6, hook_settings returned an HTML string containing the form output. In Drupal 4.7, hook_settings (as well as hook_form) return the actual $form array instead. Change this line to:

return $form;

Upon reloading the page, you should see the full form as expected.

Default values

One last minor thing is that there are a series of default values (determined by the system_elements function) assigned to form elements. These are redundant if declared again in your form. In this case, the only setting that applies is the #return_value => 1 in the checkbox field.

Converted function

The following is the completed code after translating project_settings:

function project_settings() {

  $project_directory = file_create_path(variable_get('project_directory_issues', 'issues'));
  if (!file_check_directory($project_directory)) {
    $error['project_directory_issues'] = theme('error', t('Directory does not exist, or is not writable.'));
  }

  $versions = array(-1 => t('all')) + project_releases_list();

  $form['project_release_directory'] = array(
    '#type' => 'textfield',
    '#title' => t('Release directory'),
    '#default_value' => variable_get('project_release_directory', ''),
    '#size' => 50,
    '#maxlength' => 255,
    '#description' => t('Leave this blank if project maintainers are to create their own release packages. This is useful if releases are generated by an external tool.'),
  );
  $form['project_release_unmoderate'] = array(
    '#type' => 'radios',
    '#title' => t('Unmoderate projects with releases'),
    '#default_value' => variable_get('project_release_unmoderate', 0),
    '#options' => array('Disabled', 'Enabled'),
  );
  $form['project_browse_releases'] = array(
    '#type' => 'checkbox',
    '#title' => t('Browse projects by releases'),
    '#default_value' => variable_get('project_browse_releases', 1),
    '#description' => t('Checking this box will cause the project browsing page to have version subtabs.'),
  );
  $form['project_release_overview'] = array(
    '#type' => 'radios',
    '#title' => t('Default release overview'),
    '#default_value' => variable_get('project_release_overview', -1),
    '#options' => $versions,
    '#description' => t('Default release version to list on the overview page'),
  );
  $form['project_directory_issues'] = array(
    '#type' => 'textfield',
    '#title' => t('Issue directory'),
    '#default_value' => variable_get('project_directory_issues', 'issues'),
    '#size' => 30,
    '#maxlength' => 255,
    '#description' => t("Subdirectory in the directory '%dir' where attachment to issues will be stored.", array('%dir' => variable_get('file_directory_path', 'files') .'/')),
  );

  if (module_exist('mailhandler')) {
    // TODO: move this stuff to mailhandler.module ?
    $items = array(t('<none>'));
    $result = db_query('SELECT mail FROM {mailhandler} ORDER BY mail');
    while ($mail = db_result($result, $i++)) {
      $items[$mail] = $mail;
    }

    $form['project_reply_to'] = array(
      '#type' => 'select',
      '#title' => t('Reply-to address on e-mail notifications'),
      '#default_value' => variable_get('project_reply_to', ''),
      '#options' => $items,
    );
  }

  return $form;
}
Core module before and after examples

The following examples are taken directly from Drupal core, and illustrate the difference between the previous forms approach and the new forms API. The 'after' examples are notated to further describe some of the salient points of the API. They are arranged in order of difficulty--it's recommended that you go through them in order.

Please note that editorial comments, which call attention to certain aspects of the examples, begin with ###

Standard example: Path Form

This example illustrates the creation of a simple form with a few fields and a submit button.

Before
<?php

function path_form($edit = '') {

 
$form .= form_textfield(t('Existing system path'), 'src', $edit['src'], 50, 64, t('Specify the existing path you wish to alias. For example: node/28, forum/1, taxonomy/term/1+2.'));
 
$form .= form_textfield(t('New path alias'), 'dst', $edit['dst'], 50, 64, t('Specify an alternative path by which this data can be accessed.  For example, type "about" when writing an about page.  Use a relative path and don\'t add a trailing slash or the URL alias won\'t work.'));

  if (
$edit['pid']) {
   
$form .= form_hidden('pid', $edit['pid']);
   
$form .= form_submit(t('Update alias'));
  }
  else {
   
$form .= form_submit(t('Create new alias'));
  }
  return
$form
}

?>
After
<?php

function path_form($edit = '') {
 
### Notice that name of the form field is declared as $form['src'], for example.
  ### The type of element is declared by using '#type'.  If a form element needs a
  ### pre-filled value, use '#default_value'
 
$form['src'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Existing system path'),
   
'#default_value' => $edit['src'],
   
'#size' => 60,
   
'#maxlength' => 64,
   
'#description' => t('Specify the existing path you wish to alias. For example: node/28, forum/1, taxonomy/term/1+2.'),
  );
 
$form['dst'] = array(
   
'#type' => 'textfield',
   
'#default_value' => $edit['dst'],
   
'#size' => 60,
   
'#maxlength' => 64,
   
'#description' => t('Specify an alternative path by which this data can be accessed.  For example, type "about" when writing an about page.  Use a relative path and don\'t add a trailing slash or the URL alias won\'t work.'),
  );

  if (
$edit['pid']) {
   
### Note the declaration of the types.  Also, all values that are not subject to user input
    ### use the '#value' attribute
   
$form['pid'] = array('#type' => 'hidden', '#value' => $edit['pid']);
   
$form['submit'] = array('#type' => 'submit', '#value' => t('Update alias'));
  }
  else {
   
$form['submit'] = array('#type' => 'submit', '#value' => t('Create new alias'));
  }
 
 
### This is the master function that coordinates building, validating, executing, and displaying
  ### the constructed form.  The first arg is the $form_id of the form, which by convention
  ### is usually the name of the function in which the form is created.  Second arg is the
  ### constructed form array
 
return drupal_get_form('path_form', $form);
}

?>
Fieldsets and advanced fields: system_view_general

This rather lengthy example introduces how fieldsets are handled, and also shows some additional field types.

Before
<?php
function system_view_general() {
  global
$conf;

 
// General settings:
 
$group  = form_textfield(t('Name'), 'site_name', variable_get('site_name', 'drupal'), 70, 70, t('The name of this web site.'));
 
$group .= form_textfield(t('E-mail address'), 'site_mail', variable_get('site_mail', ini_get('sendmail_from')), 70, 128, t('A valid e-mail address for this website, used by the auto-mailer during registration, new password requests, notifications, etc.'));
 
$group .= form_textfield(t('Slogan'), 'site_slogan', variable_get('site_slogan', ''), 70, 128, t('The slogan of this website. Some themes display a slogan when available.'));
 
$group .= form_textarea(t('Mission'), 'site_mission', variable_get('site_mission', ''), 70, 5, t('Your site\'s mission statement or focus.'));
 
$group .= form_textarea(t('Footer message'), 'site_footer', variable_get('site_footer', ''), 70, 5, t('This text will be displayed at the bottom of each page.  Useful for adding a copyright notice to your pages.'));
 
$group .= form_textfield(t('Anonymous user'), 'anonymous', variable_get('anonymous', 'Anonymous'), 70, 70, t('The name used to indicate anonymous users.'));
 
$group .= form_textfield(t('Default front page'), 'site_frontpage', variable_get('site_frontpage', 'node'), 70, 70, t('The home page displays content from this relative URL.  If you are not using clean URLs, specify the part after "?q=".  If unsure, specify "node".'));

 
// We check for clean URL support using an image on the client side.
  ### NOTE: Description shortened because of issue with formatting
 
$group .= form_radios(t('Clean URLs'), 'clean_url', variable_get('clean_url', 0), array(t('Disabled'), t('Enabled')), t('This option makes Drupal emit clean URLs ...'));
 
variable_set('clean_url_ok', 0);
  global
$base_url;
 
// We will use a random URL so there is no way a proxy or a browser could cache the "no such image" answer.
  ### NOTE: Image removed because of issue with formatting

 
$output = form_group(t('General settings'), $group);

 
// Error handling:
 
$period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
 
$period['1000000000'] = t('Never');
 
$group = form_textfield(t('Default 403 (access denied) page'), 'site_403', variable_get('site_403', ''), 70, 70, t('This page is displayed when the requested document is denied to the current user.  If you are not using clean URLs, specify the part after "?q=". If unsure, specify nothing.'));
 
$group .= form_textfield(t('Default 404 (not found) page'), 'site_404', variable_get('site_404', ''), 70, 70, t('This page is displayed when no other content matches the requested document.  If you are not using clean URLs, specify the part after "?q=". If unsure, specify nothing.'));
 
$group .= form_select(t('Error reporting'), 'error_level', variable_get('error_level', 1), array(t('Write errors to the log'), t('Write errors to the log and to the screen')), t('Where Drupal, PHP and SQL errors are logged. On a production server it is recommended that errors are only written to the error log. On a test server it can be helpful to write logs to the screen.'));
 
$group .= form_select(t('Discard log entries older than'), 'watchdog_clear', variable_get('watchdog_clear', 604800), $period, t('The time log entries should be kept.  Older entries will be automatically discarded.  Requires crontab.'));

 
$output .= form_group(t('Error handling'), $group);

 
// Caching:
 
$group  = form_radios(t('Cache support'), 'cache', variable_get('cache', 0), array(t('Disabled'), t('Enabled')), t('Enable or disable the caching of rendered pages.  When caching is enabled, Drupal will flush the cache when required to make sure updates take effect immediately.  Check the <a href="/%documentation">cache documentation</a> for information on Drupal\'s cache system.', array('%documentation' => url('admin/help/system#cache', NULL, NULL, 'cache'))));

 
$output .= form_group(t('Cache settings'), $group);

 
// File system:
 
$directory_path = variable_get('file_directory_path', 'files');
 
file_check_directory($directory_path, FILE_CREATE_DIRECTORY, 'file_directory_path');

 
$directory_temp = variable_get('file_directory_temp', FILE_DIRECTORY_TEMP);
 
file_check_directory($directory_temp, FILE_CREATE_DIRECTORY, 'file_directory_temp');

 
$group = form_textfield(t('File system path'), 'file_directory_path', $directory_path, 70, 255, t('A file system path where the files will be stored. This directory has to exist and be writable by Drupal. If the download method is set to public this directory has to be relative to Drupal installation directory, and be accessible over the web. When download method is set to private this directory should not be accessible over the web. Changing this location after the site has been in use will cause problems so only change this setting on an existing site if you know what you are doing.'));
 
$group .= form_textfield(t('Temporary directory'), 'file_directory_temp', $directory_temp, 70, 255, t('Location where uploaded files will be kept during previews. Relative paths will be resolved relative to the file system path.'));
 
$group .= form_radios(t('Download method'), 'file_downloads', variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC), array(FILE_DOWNLOADS_PUBLIC => t('Public - files are available using http directly.'), FILE_DOWNLOADS_PRIVATE => t('Private - files are transferred by Drupal.')), t('If you want any sort of access control on the downloading of files, this needs to be set to <em>private</em>. You can change this at any time, however all download URLs will change and there may be unexpected problems so it is not recommended.'));
 
$output .= form_group(t('File system settings'), $group);

 
// Image handling:
 
$group = '';
 
$toolkits_available = image_get_available_toolkits();
  if (
count($toolkits_available) > 1) {
     
$group .= form_radios(t('Select an image processing toolkit'), 'image_toolkit', variable_get('image_toolkit', image_get_toolkit()), $toolkits_available);
  }
 
$group .= image_toolkit_invoke('settings');
  if (
$group) {
   
$output .= form_group(t('Image handling'), $group);
  }

 
// Date settings:
 
$zones = _system_zonelist();

 
// Date settings: possible date formats
 
$dateshort = array('Y-m-d H:i','m/d/Y - H:i', 'd/m/Y - H:i', 'Y/m/d - H:i',
          
'm/d/Y - g:ia', 'd/m/Y - g:ia', 'Y/m/d - g:ia',
          
'M j Y - H:i', 'j M Y - H:i', 'Y M j - H:i',
          
'M j Y - g:ia', 'j M Y - g:ia', 'Y M j - g:ia');
 
$datemedium = array('D, Y-m-d H:i', 'D, m/d/Y - H:i', 'D, d/m/Y - H:i',
         
'D, Y/m/d - H:i', 'F j, Y - H:i', 'j F, Y - H:i', 'Y, F j - H:i',
         
'D, m/d/Y - g:ia', 'D, d/m/Y - g:ia', 'D, Y/m/d - g:ia',
         
'F j, Y - g:ia', 'j F, Y - g:ia', 'Y, F j - g:ia');
 
$datelong = array('l, F j, Y - H:i', 'l, j F, Y - H:i', 'l, Y,  F j - H:i',
       
'l, F j, Y - g:ia', 'l, j F, Y - g:ia', 'l, Y,  F j - g:ia');

 
// Date settings: construct choices for user
 
foreach ($dateshort as $f) {
   
$dateshortchoices[$f] = format_date(time(), 'custom', $f);
  }
  foreach (
$datemedium as $f) {
   
$datemediumchoices[$f] = format_date(time(), 'custom', $f);
  }
  foreach (
$datelong as $f) {
   
$datelongchoices[$f] = format_date(time(), 'custom', $f);
  }

 
$group  = form_select(t('Default time zone'), 'date_default_timezone', variable_get('date_default_timezone', 0), $zones, t('Select the default site time zone.'));
 
$group .= form_radios(t('Configurable time zones'), 'configurable_timezones', variable_get('configurable_timezones', 1), array(t('Disabled'), t('Enabled')), t('Enable or disable user-configurable time zones.  When enabled, users can set their own time zone and dates will be updated accordingly.'));
 
$group .= form_select(t('Short date format'), 'date_format_short', variable_get('date_format_short', $dateshort[0]), $dateshortchoices, t('The short format of date display.'));
 
$group .= form_select(t('Medium date format'), 'date_format_medium', variable_get('date_format_medium', $datemedium[0]), $datemediumchoices, t('The medium sized date display.'));
 
$group .= form_select(t('Long date format'), 'date_format_long', variable_get('date_format_long', $datelong[0]), $datelongchoices, t('Longer date format used for detailed display.'));
 
$group .= form_select(t('First day of week'), 'date_first_day', variable_get('date_first_day', 0), array(0 => t('Sunday'), 1 => t('Monday'), 2 => t('Tuesday'), 3 => t('Wednesday'), 4 => t('Thursday'), 5 => t('Friday'), 6 => t('Saturday')), t('The first day of the week for calendar views.'));

 
$output .= form_group(t('Date settings'), $group);

  return
$output;
}
?>
After
<?php
function system_view_general() {
 
// General settings:
  ### Notice the type declaration as a fieldset.  '#title' in this case will be the legend
  ### for the fieldset.  Also note the two properties which make it a collapsed fieldset
  ### by default the collapsing attributes are FALSE, so they need to be declared if a
  ### collapsed fieldset is desired
 
$form['general'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('General settings'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE,
  );

 
### A form element is placed under a fieldset by adding it under the fieldset in the $form
  ### array.  Also note that size and maxlength attributes aren't declared here.  Many attributes
  ### have system defaults which are used if no value is explicitly declared
 
$form['general']['site_name'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Name'),
   
'#default_value' => variable_get('site_name', 'drupal'),
   
'#description' => t('The name of this web site.'),
  );
 
$form['general']['site_mail'] = array(
   
'#type' => 'textfield',
   
'#title' => t('E-mail address'),
   
'#default_value' => variable_get('site_mail', ini_get('sendmail_from')),
   
'#maxlength' => 128,
   
'#description' => t('A valid e-mail address for this website, used by the auto-mailer during registration, new password requests, notifications, etc.'),
  );
 
$form['general']['site_slogan'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Slogan'),
   
'#default_value' => variable_get('site_slogan', ''),
   
'#maxlength' => 128,
   
'#description' => t('The slogan of this website. Some themes display a slogan when available.'),
  );
 
 
### Declaration of a text area.  This field is using the default '#cols' width, so it's not
  ### declared explicitly, and a custom '#rows' attribute
 
$form['general']['site_mission'] = array(
   
'#type' => 'textarea',
   
'#title' => t('Mission'),
   
'#default_value' => variable_get('site_mission', ''),
   
'#rows' => 5, '#description' => t('Your site\'s mission statement or focus.'),
  );
 
$form['general']['site_footer'] = array(
   
'#type' => 'textarea',
   
'#title' => t('Footer message'),
   
'#default_value' => variable_get('site_footer', ''),
   
'#rows' => 5,
   
'#description' => t('This text will be displayed at the bottom of each page.  Useful for adding a copyright notice to your pages.'),
  );
 
$form['general']['anonymous'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Anonymous user'),
   
'#default_value' => variable_get('anonymous', 'Anonymous'),
   
'#description' => t('The name used to indicate anonymous users.'),
  );
 
$form['general']['site_frontpage'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Default front page'),
   
'#default_value' => variable_get('site_frontpage', 'node'),
   
'#description' => t('The home page displays content from this relative URL.  If you are not using clean URLs, specify the part after "?q=".  If unsure, specify "node".'),
  );

 
// We check for clean URL support using an image on the client side.
  ### Radio buttons element.  Notice the '#options' attribute, which takes the the same array
  ### structure for it's value as pre-4.7 did.
 
  ### NOTE: Description shortened because of issue with formatting
 
$form['general']['clean_url'] = array(
   
'#type' => 'radios',
   
'#title' => t('Clean URLs'),
   
'#default_value' => variable_get('clean_url', 0),
   
'#options' => array(t('Disabled'), t('Enabled')),
   
'#description' => t('This option makes Drupal emit clean URLs ...'),
  );

 
variable_set('clean_url_ok', 0);
  global
$base_url;
 
// We will use a random URL so there is no way a proxy or a browser could cache the "no such image" answer.
  ### The markup type.  This is the default type for forms elements.  '#value' is used since
  ### it can't be altered by the user, and the value is output directly as markup.
 
$form['general']['clean_url_test'] = array(
   
'#type' => 'markup',
   
'#value' => '<img style="position: relative; left: -1000em;" src="/'. $base_url. '/system/test/'. user_password(20) .'.png" alt="" />',
  );

 
// Error handling:

 
$form['errors'] = array(
   
'#type' => 'fieldset',
   
'#title' =>t('Error handling'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE
 
);
 
$form['errors']['site_403'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Default 403 (access denied) page'),
   
'#default_value' => variable_get('site_403', ''),
   
'#description' => t('This page is displayed when the requested document is denied to the current user.  If you are not using clean URLs, specify the part after "?q=". If unsure, specify nothing.'),
  );

 
$form['errors']['site_404'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Default 404 (not found) page'),
   
'#default_value' =>  variable_get('site_404', ''),
   
'#description' => t('This page is displayed when no other content matches the requested document.  If you are not using clean URLs, specify the part after "?q=". If unsure, specify nothing.'),
  );

 
### A select element.  '#default_value' will be the initially selected value when the form
  ### is rendered.  Again note the '#options' attribute, which takes an array as it's value.
 
$form['errors']['error_level'] = array(
   
'#type' => 'select',
   
'#title' => t('Error reporting'),
   
'#default_value' => variable_get('error_level', 1),
   
'#options' => array(t('Write errors to the log'), t('Write errors to the log and to the screen')),
   
'#description' =>  t('Where Drupal, PHP and SQL errors are logged. On a production server it is recommended that errors are only written to the error log. On a test server it can be helpful to write logs to the screen.'),
  );

 
$period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
 
$period['1000000000'] = t('Never');
 
$form['errors']['watchdog_clear'] = array(
   
'#type' => 'select',
   
'#title' => t('Discard log entries older than'),
   
'#default_value' => variable_get('watchdog_clear', 604800),
   
'#options' => $period,
   
'#description' => t('The time log entries should be kept.  Older entries will be automatically discarded.  Requires crontab.'),
  );


 
// Caching:
 
$form['cache'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('Cache settings'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE,
  );

 
$form['cache']['cache']  = array(
   
'#type' => 'radios',
   
'#title' => t('Page cache'),
   
'#default_value' => variable_get('cache', CACHE_DISABLED),
   
'#options' => array(CACHE_DISABLED => t('Disabled'), CACHE_ENABLED => t('Enabled')),
   
'#description' => t("Drupal has a caching mechanism which stores dynamically generated web pages in a database.  By caching a web page, Drupal does not have to create the page each time someone wants to view it, instead it takes only one SQL query to display it, reducing response time and the server's load.  Only pages requested by \"anonymous\" users are cached.  In order to reduce server load and save bandwidth, Drupal stores and sends compressed cached pages."),
  );

 
$period = drupal_map_assoc(array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400), 'format_interval');
 
$period[0] = t('none');
 
$form['cache']['cache_lifetime'] = array(
   
'#type' => 'select',
   
'#title' => t('Minimum cache lifetime'),
   
'#default_value' => variable_get('cache_lifetime', 0),
   
'#options' => $period,
   
'#description' => t('Enabling the cache will offer a sufficient performance boost for most low-traffic and medium-traffic sites.  On high-traffic sites it can become necessary to enforce a minimum cache lifetime.  The minimum cache lifetime is the minimum amount of time that will go by before the cache is emptied and recreated.  A larger minimum cache lifetime offers better performance, but users will not see new content for a longer period of time.'),
  );

 
// File system:
 
$form['files'] = array('#type' => 'fieldset', '#title' => t('File system settings'), '#collapsible' => TRUE, '#collapsed' => TRUE);

 
$directory_path = variable_get('file_directory_path', 'files');
 
file_check_directory($directory_path, FILE_CREATE_DIRECTORY, 'file_directory_path');

 
$form['files']['file_directory_path'] = array(
   
'#type' => 'textfield',
   
'#title' => t('File system path'),
   
'#default_value' => $directory_path,
   
'#maxlength' => 255,
   
'#valid' => 'directory',
   
'#description' => t('A file system path where the files will be stored. This directory has to exist and be writable by Drupal. If the download method is set to public this directory has to be relative to Drupal installation directory, and be accessible over the web. When download method is set to private this directory should not be accessible over the web. Changing this location after the site has been in use will cause problems so only change this setting on an existing site if you know what you are doing.'),
  );

 
$directory_temp = variable_get('file_directory_temp', ini_get('upload_tmp_dir'));
 
file_check_directory($directory_temp, FILE_CREATE_DIRECTORY, 'file_directory_temp');

 
### Note the use of the '#valid' attribute--the value is a custom validation function
  ### for this element.  See the full doc for more details
 
$form['files']['file_directory_temp'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Temporary directory'),
   
'#default_value' => $directory_temp,
   
'#maxlength' => 255,
   
'#valid' => 'directory',
   
'#description' => t('Location where uploaded files will be kept during previews. Relative paths will be resolved relative to the file system path.'),
  );

 
$form['files']['file_downloads'] = array(
   
'#type' => 'radios',
   
'#title' => t('Download method'),
   
'#default_value' => variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC),
   
'#options' => array(FILE_DOWNLOADS_PUBLIC => t('Public - files are available using http directly.'), FILE_DOWNLOADS_PRIVATE => t('Private - files are transferred by Drupal.')),
   
'#description' => t('If you want any sort of access control on the downloading of files, this needs to be set to <em>private</em>. You can change this at any time, however all download URLs will change and there may be unexpected problems so it is not recommended.'),
  );

 
/*
  // Image handling:
  $group = array();
  $toolkits_available = image_get_available_toolkits();
  if (count($toolkits_available) > 1) {
    $group['image_toolkit'] = array(
      '#type' => 'radios',
      '#title' => t('Select an image processing toolkit'),
      '#default_value' => variable_get('image_toolkit', image_get_toolkit()),
      '#options' => $toolkits_available
    );
  }
  $group['toolkit'] = image_toolkit_invoke('settings');
  if (is_array($group)) {
    $form['image'] = array(

    '#type' => 'fieldset', '#title' => t('Image handling'), '#collapsible' => TRUE, '#collapsed' => true);
   
    ### Notice here how the above created $group is merged into the correct section of the
    ### $form array
    $form['image'] = array_merge($form['image'], $group);
  }
  */

  // Feed settings
 
$form['feed'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('RSS feed settings'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE,
  );
 
$form['feed']['feed_default_items'] = array(
   
'#type' => 'select',
   
'#title' => t('Number of items per feed'),
   
'#default_value' => variable_get('feed_default_items', 10),
   
'#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30)),
   
'#description' => t('The default number of items to include in a feed.'),
  );
 
$form['feed']['feed_item_length'] = array(
   
'#type' => 'select',
   
'#title' => t('Display of XML feed items'),
   
'#default_value' => variable_get('feed_item_length','teaser'),
   
'#options' => array(
     
'title' => t('Titles only'),
     
'teaser' => t('Titles plus teaser'),
     
'fulltext' => t('Full text')
    ),
   
'#description' => t('Global setting for the length of XML feed items that are output by default.'),
  );

 
// Date settings:
 
$zones = _system_zonelist();

 
// Date settings: possible date formats
 
$dateshort = array('Y-m-d H:i','m/d/Y - H:i', 'd/m/Y - H:i', 'Y/m/d - H:i',
          
'm/d/Y - g:ia', 'd/m/Y - g:ia', 'Y/m/d - g:ia',
          
'M j Y - H:i', 'j M Y - H:i', 'Y M j - H:i',
          
'M j Y - g:ia', 'j M Y - g:ia', 'Y M j - g:ia');
 
$datemedium = array('D, Y-m-d H:i', 'D, m/d/Y - H:i', 'D, d/m/Y - H:i',
         
'D, Y/m/d - H:i', 'F j, Y - H:i', 'j F, Y - H:i', 'Y, F j - H:i',
         
'D, m/d/Y - g:ia', 'D, d/m/Y - g:ia', 'D, Y/m/d - g:ia',
         
'F j, Y - g:ia', 'j F, Y - g:ia', 'Y, F j - g:ia');
 
$datelong = array('l, F j, Y - H:i', 'l, j F, Y - H:i', 'l, Y,  F j - H:i',
       
'l, F j, Y - g:ia', 'l, j F, Y - g:ia', 'l, Y,  F j - g:ia');

 
// Date settings: construct choices for user
 
foreach ($dateshort as $f) {
   
$dateshortchoices[$f] = format_date(time(), 'custom', $f);
  }
  foreach (
$datemedium as $f) {
   
$datemediumchoices[$f] = format_date(time(), 'custom', $f);
  }
  foreach (
$datelong as $f) {
   
$datelongchoices[$f] = format_date(time(), 'custom', $f);
  }

 
$form['dates'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('Date settings'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE,
  );
 
$form['dates']['date_default_timezone'] = array(
   
'#type' => 'select',
   
'#title' => t('Default time zone'),
   
'#default_value' => variable_get('date_default_timezone', 0),
   
'#options' => $zones, '#description' => t('Select the default site time zone.'),
  );

 
$form['dates']['configurable_timezones'] = array(
   
'#type' => 'radios',
   
'#title' => t('Configurable time zones'),
   
'#default_value' => variable_get('configurable_timezones', 1),
   
'#options' => array(t('Disabled'), t('Enabled')),
   
'#description' => t('Enable or disable user-configurable time zones.  When enabled, users can set their own time zone and dates will be updated accordingly.'),
  );

 
$form['dates']['date_format_short'] = array(
   
'#type' => 'select',
   
'#title' => t('Short date format'),
   
'#default_value' => variable_get('date_format_short', $dateshort[0]),
   
'#options' => $dateshortchoices,
   
'#description' => t('The short format of date display.'),
  );

 
$form['dates']['date_format_medium'] = array(
   
'#type' => 'select',
   
'#title' => t('Medium date format'),
   
'#default_value' => variable_get('date_format_medium', $datemedium[0]),
   
'#options' => $datemediumchoices,
   
'#description' => t('The medium sized date display.'),
  );

 
$form['dates']['date_format_long'] = array(
   
'#type' => 'select',
   
'#title' => t('Long date format'),
   
'#default_value' => variable_get('date_format_long', $datelong[0]),
   
'#options' => $datelongchoices,
   
'#description' => t('Longer date format used for detailed display.'),
  );

 
$form['dates']['date_first_day'] = array(
   
'#type' => 'select',
   
'#title' => t('First day of week'), '#default_value' => variable_get('date_first_day', 0),
   
'#options' => array(0 => t('Sunday'), 1 => t('Monday'), 2 => t('Tuesday'), 3 => t('Wednesday'), 4 => t('Thursday'), 5 => t('Friday'), 6 => t('Saturday')),
   
'#description' => t('The first day of the week for calendar views.'),
  );

 
// Site offline/maintenance settings
 
$form['site_status'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('Site maintenance'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE,
  );

 
$form['site_status']['site_offline'] = array(
   
'#type' => 'radios',
   
'#title' => t('Site status'),
   
'#default_value' => variable_get('site_offline', 0),
   
'#options' => array(t('Online'), t('Offline')),
   
'#description' => t('When set to "Online", all visitors will be able to browse your site normally. When set to "Offline", only users with the "administer site configuration" permission will be able to access your site to perform maintenance, all other visitors will see the site offline message configured below.'),
  );

 
$form['site_status']['site_offline_message'] = array(
   
'#type' => 'textarea',
   
'#rows' => 5,
   
'#title' => t('Site offline message'),
   
'#default_value' => variable_get('site_offline_message', t('%site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('%site' => variable_get('site_name', t('This drupal site'))))),
   
'#description' => t('Message to show visitors when site is offline.'),
  );

 
// String handling: report status and errors.
 
$form['strings'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('String handling'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE,
  );
 
$form['strings'] = array_merge($form['strings'], unicode_settings());

 
### Note here that since this form is eventually being returned to hook_settings,
  ### drupal_get_form is not called, and only the $form array is returned.  In any case where
  ### a calling function will process the form array, return only the $form array without
  ### calling drupal_get_form.  This will be the case for most core hooks.
 
return $form;
}
?>
Validation and execution functions: contact_mail_page

This example demonstrates how to make use of the API's validation and execution functions.

Before
<?php
function contact_mail_page() {
  global
$user;

  if (!
flood_is_allowed('contact', CONTACT_HOURLY_THRESHOLD)) {
   
$output = t("You can't send more than %number messages per hour. Please try again later.", array('%number' => CONTACT_HOURLY_THRESHOLD));
  }
  else {
    if (isset(
$_POST['edit'])) {
     
$edit = $_POST['edit'];
    }

    if (
$edit) {
     
// Validate the fields:
     
if (!$edit['name']) {
       
form_set_error('name', t('You must enter a name.'));
      }
      if (!
$edit['mail'] || !valid_email_address($edit['mail'])) {
       
form_set_error('mail', t('You must enter a valid e-mail address.'));
      }
      if (!
$edit['subject']) {
       
form_set_error('subject', t('You must enter a subject.'));
      }
      if (!
$edit['message']) {
       
form_set_error('message', t('You must enter a message.'));
      }
      if (!
$edit['category']) {
       
// Look if there is only one category
       
$result = db_query('SELECT category FROM {contact}');
        if (
db_num_rows($result) == 1) {
         
$category = db_fetch_object($result);
         
$edit['category'] = $category->category;
        }
        else {
         
form_set_error('category', t('You must select a valid category.'));
        }
      }
     
form_validate($edit, $user->name . $user->mail);

      if (!
form_get_errors()) {
       
// Prepare the sender:
       
$from = $edit['mail'];

       
// Compose the body:
       
$message[] = t("%name sent a message using the contact form at %form:", array('%name' => $edit['name'], '%form' => url($_GET['q'], NULL, NULL, TRUE)));
       
$message[] = $edit['message'];

       
// Tidy up the body:
       
foreach ($message as $key => $value) {
         
$message[$key] = wordwrap($value);
        }

       
// Format the category:
       
$subject = '['. $edit['category'] .'] '. $edit['subject'];

       
// Prepare the body:
       
$body = implode("\n\n", $message);

       
// Load the category information:
       
$contact = db_fetch_object(db_query("SELECT * FROM {contact} WHERE category = '%s'", $edit['category']));

       
// Send the e-mail to the recipients:
       
user_mail($contact->recipients, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from");

       
// If the user requests it, send a copy.
       
if ($edit['copy']) {
         
user_mail($from, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from");
        }

       
// Send an auto-reply if necessary:
       
if ($contact->reply) {
         
user_mail($from, $subject, wordwrap($contact->reply), "From: $contact->recipients\nReply-to: $contact->recipients\nX-Mailer: Drupal\nReturn-path: $contact->recipients\nErrors-to: $contact->recipients");
        }

       
// Log the operation:
       
flood_register_event('contact');
       
watchdog('mail', t('%name-from sent an e-mail regarding %category.', array('%name-from' => theme('placeholder', $edit['name'] ." <$from>"), '%category' => theme('placeholder', $contact->category))));

       
// Set a status message:subject
       
drupal_set_message(t('Your message has been sent.'));

       
// Jump to contact page:
       
drupal_goto('contact');
      }
    }
    else if (
$user->uid) {
     
$edit['name'] = $user->name;
     
$edit['mail'] = $user->mail;
    }

   
$result = db_query('SELECT category FROM {contact} ORDER BY category');
   
$categories[] = '--';
    while (
$category = db_fetch_object($result)) {
     
$categories[$category->category] = $category->category;
    }

    if (
count($categories) > 1) {
     
$output  = variable_get('contact_form_information', t('You can leave us a message using the contact form below.'));
     
$output .= form_textfield(t('Your name'), 'name', $edit['name'], 60, 255, NULL, NULL, TRUE);
     
$output .= form_textfield(t('Your e-mail address'), 'mail', $edit['mail'], 60, 255, NULL, NULL, TRUE);
     
$output .= form_textfield(t('Subject'), 'subject', $edit['subject'], 60, 255, NULL, NULL, TRUE);
      if (
count($categories) > 2) {
       
$output .= form_select(t('Category'), 'category', $edit['category'], $categories, NULL, NULL, NULL, TRUE);
      }
     
$output .= form_textarea(t('Message'), 'message', $edit['message'], 60, 5, NULL, NULL, TRUE);
     
$output .= form_checkbox(t('Send me a copy.'), 'copy', $edit['copy']);
     
$output .= form_token($user->name . $user->mail);
     
$output .= form_submit(t('Send e-mail'));
     
$output  = form($output);
    }
    else {
     
$output = t('The contact form has not been configured.');
    }
  }

  return
$output;
}
?>
After
<?php
function contact_mail_page() {
  global
$user;

  if (!
flood_is_allowed('contact', CONTACT_HOURLY_THRESHOLD)) {
   
$output = t("You can't send more than %number messages per hour. Please try again later.", array('%number' => CONTACT_HOURLY_THRESHOLD));
  }
  else {
    if (
$user->uid) {
     
$name = $user->name;
     
$mail = $user->mail;
    }

   
$result = db_query('SELECT category FROM {contact} ORDER BY category');
   
$categories[] = '--';
    while (
$category = db_fetch_object($result)) {
     
$categories[$category->category] = $category->category;
    }

    if (
count($categories) > 1) {
     
### Implementation of the token feature.  See the full doc for more information
     
$form['#token'] = $user->name . $user->mail;
     
$form['contact_information'] = array(
       
'#type' => 'markup',
       
'#value' => variable_get('contact_form_information', t('You can leave us a message using the contact form below.')),
      );
     
$form['name'] = array(
       
'#type' => 'textfield',
       
'#title' => t('Your name'),
       
'#maxlength' => 255,
       
'#default_value' => $name,
       
'#required' => TRUE,
      );
     
$form['mail'] = array(
       
'#type' => 'textfield',
       
'#title' => t('Your e-mail address'),
       
'#maxlength' => 255,
       
'#default_value' => $mail,
       
'#required' => TRUE,
      );
     
$form['subject'] = array(
       
'#type' => 'textfield',
       
'#title' => t('Subject'),
       
'#maxlength' => 255,
       
'#required' => TRUE,
      );
      if (
count($categories) > 2) {
       
$form['category'] = array('#type' => 'select', '#title' => t('Category'), '#options' => $categories, '#required' => TRUE);
      }
     
$form['message'] = array(
       
'#type' => 'textarea',
       
'#title' => t('Message'),
       
'#rows' => 5,
       
'#required' => TRUE,
      );
     
$form['copy'] = array('#type' => 'checkbox', '#title' => t('Send me a copy.'));
     
$form['submit'] = array('#type' => 'submit', '#value' => t('Send e-mail'));

     
### Note the form_id of this form--it will be used to name the validate/execute functions
     
$output = drupal_get_form('contact_mail_page', $form);
    }
    else {
     
$output = t('The contact form has not been configured.');
    }
  }

  return
$output;
}
?>

To insert a validation function for the specified form, simply create a validate function, appending _validate to the form_id of the form you wish to validate. The function has two args--the form_id of the form being validated, and the already built form array of the form being validated

<?php
function contact_mail_page_validate($form_id, &$form) {

 
### Here the global variable where form values are stored is brought into the function
  ### for possible editing
 
global $form_values;

 
### To check values, simply access them w/ the same name with which they were declared
 
if (!$form['name']) {
   
form_set_error('name', t('You must enter a name.'));
  }
  if (!
$form['mail'] || !valid_email_address($form['mail'])) {
   
form_set_error('mail', t('You must enter a valid e-mail address.'));
  }
  if (!
$form['subject']) {
   
form_set_error('subject', t('You must enter a subject.'));
  }
  if (!
$form['message']) {
   
form_set_error('message', t('You must enter a message.'));
  }
  if (!
$form['category']) {
   
// Look if there is only one category
   
$result = db_query('SELECT category FROM {contact}');
    if (
db_num_rows($result) == 1) {
     
$category = db_fetch_object($result);
   
     
### Here the global form array is edited before passing it along to the execute function
     
$form_values['category'] = $category->category;
    }
    else {
     
form_set_error('category', t('You must select a valid category.'));
    }
  }
}
?>

To insert an execute function for the specified form, create an execution function, appending _submit to the form_id of the form you wish to execute. The function has two args--the form_id of the form being executed, and the already built form array of the form being executed. Notice how the $_POST['edit']/switch statement approach is eliminated, which has major security benefits

<?php
function contact_mail_page_submit($form_id, $edit) {

 
// Prepare the sender:
 
$from = $edit['mail'];

 
// Compose the body:
  ### Note how the form values are accessed the same way they were accessed in the validate
  ### function
 
$message[] = t("%name sent a message using the contact form at %form:", array('%name' => $edit['name'], '%form' => url($_GET['q'], NULL, NULL, TRUE)));
 
$message[] = $edit['message'];

 
// Tidy up the body:
 
foreach ($message as $key => $value) {
   
$message[$key] = wordwrap($value);
  }

 
// Format the category:
 
$subject = '['. $edit['category'] .'] '. $edit['subject'];

 
// Prepare the body:
 
$body = implode("\n\n", $message);

 
// Load the category information:
 
$contact = db_fetch_object(db_query("SELECT * FROM {contact} WHERE category = '%s'", $edit['category']));

 
// Send the e-mail to the recipients:
 
user_mail($contact->recipients, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from");

 
// If the user requests it, send a copy.
  // Here the checkbox value is examined in the form array
 
if ($edit['copy']) {
   
user_mail($from, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from");
  }

 
// Send an auto-reply if necessary:
 
if ($contact->reply) {
   
user_mail($from, $subject, wordwrap($contact->reply), "From: $contact->recipients\nReply-to: $contact->recipients\nX-Mailer: Drupal\nReturn-path: $contact->recipients\nErrors-to: $contact->recipients");
  }

 
// Log the operation:
 
flood_register_event('contact');
 
watchdog('mail', t('%name-from sent an e-mail regarding %category.', array('%name-from' => theme('placeholder', $edit['name'] ." <$from>"), '%category' => theme('placeholder', $contact->category))));

 
// Set a status message:subject
 
drupal_set_message(t('Your message has been sent.'));

 
// Jump to contact page:
  ### Note that the function ends with the same redirect as in the original approach,
  ### so the page is reloaded after the form is executed
 
drupal_goto('contact');
}
?>
Theming forms: system_themes

This function demonstrates how to make use of multiple checkboxes, theming of form elements in a table, and a seperate theming function which allows for rendering of the table and inline HTML elements.

Before
<?php
function system_themes() {
 
system_listing_save();
 
$form = system_theme_listing();
 
$form .= form_submit(t('Save configuration'));
  print
theme('page', form($form));
}

function
system_theme_listing() {
 
$themes = system_theme_data();
 
ksort($themes);

  foreach (
$themes as $info) {
   
$info->screenshot = dirname($info->filename) . '/screenshot.png';
   
$row = array();

   
// Screenshot column.
   
$row[] = file_exists($info->screenshot) ? theme('image', $info->screenshot, t('Screenshot for %theme theme', array('%theme' => $info->name)), '', 'class="screenshot"', false) : t('no screenshot');

   
// Information field.
   
$row[] = "<strong>$info->name</strong><br /><em>" . dirname($info->filename) . '</em>';

   
// enabled, default, and operations columns
   
$row[] = array('data' => form_checkbox('', 'status]['. $info->name, 1, $info->status), 'align' => 'center');
   
$row[] = array('data' => form_radio('', 'theme_default', $info->name, (variable_get('theme_default', 'bluemarine') == $info->name) ? 1 : 0), 'align' => 'center');
    if (
function_exists($info->prefix . '_settings') || function_exists($info->prefix . '_features')) {
     
$row[] = array('data' => l(t('configure'), 'admin/themes/settings/' . $info->name), 'align' => 'center');
    }
    else {
     
$row[] = '';
    }
   
$rows[] = $row;
  }

 
$header = array(t('Screenshot'), t('Name'), t('Enabled'), t('Default'), t('Operations'));
 
$output = form_hidden('type', 'theme');
 
$output .= theme('table', $header, $rows);
  return
$output;
}
?>
After
<?php
function system_themes() {
 
$themes = system_theme_data();
 
ksort($themes);

  foreach (
$themes as $info) {
   
$info->screenshot = dirname($info->filename) . '/screenshot.png';
   
$screenshot = file_exists($info->screenshot) ? theme('image', $info->screenshot, t('Screenshot for %theme theme', array('%theme' => $info->name)), '', array('class' => 'screenshot'), false) : t('no screenshot');

   
### Note both the use of '#markup' to drop the screenshot info into the form, and
    ### the grouping of the screenshot/description under the theme name in the form array
   
$form[$info->name]['screenshot'] = array('#type' => 'markup', '#value' => $screenshot);

   
### Use of a form item.  Notice how '#value' is used, because the value is not editable
    ### by a user
   
$form[$info->name]['description'] = array(
     
'#type' => 'item',
     
'#title' => $info->name,
     
'#value' => dirname($info->filename),
    );

   
### Here the options array for all checkboxes is built.  Notice that the theme name is used
    ### for the key--this will be important later when the form is themed
   
$options[$info->name] = '';

   
### Here the status array is built conditionally--only checkboxes that are checked are
    ### added to this array.  Notice in this array the theme name is put in the element's value,
    ### unlike the $options array
   
if ($info->status) {
     
$status[] = $info->name;
    }
    if (
$info->status && (function_exists($info->prefix . '_settings') || function_exists($info->prefix . '_features'))) {

     
### Note that links can also be included in a markup element.  Markup can hold any
      ### kind of markup that needs to get into the form
     
$form[$info->name]['operations'] = array(
       
'#type' => 'markup',
       
'#value' => l(t('configure'),
       
'admin/themes/settings/' . $info->name),
      );
    }
    else {
     
// Dummy element for form_render. Cleaner than adding a check in the theme function.
     
$form[$info->name]['operations'] = array();
    }
  }

 
### Now that all checkbox options have been built, and all checked boxes are know, the
  ### checkboxes element can be declared.  Notice that the $status array is dropped directly
  ### into '#default_value'
 
$form['status'] = array(
   
'#type' => 'checkboxes',
   
'#options' => $options,
   
'#default_value' => $status,
  );

 
### Radio button groups are built the same basic way, using the same $options array in
  ### this case
 
$form['theme_default'] = array(
   
'#type' => 'radios',
   
'#options' => $options,
   
'#default_value' => variable_get('theme_default', 'bluemarine')
  );

 
### Notice that the two submit buttons are grouped under a 'buttons' group in the form array
  ### This will be important when we examine the execute function
 
$form['buttons']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration') );
 
$form['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset to defaults') );

 
### Drop the form array into the master form function
 
return drupal_get_form('system_themes', $form);
}
?>

In this case, the theming of the form is fairly complex, so a custom theming function is used. Custom theme functions are declared by prepending theme_ to the form_id of the form you wish to theme. The single arg is the constructed form array.

<?php
function theme_system_themes($form) {

 
### The constructed form array has a number of internal record-keeping elements, so directly
  ### looping through the array would result in errors.  The element_children function
  ### extracts only those form elements that have a value.
 
foreach (element_children($form) as $key) {

   
### Here the table rows are constructed using the previous convention of a rows array.
    ### Notice that there is a check to make sure that the particular form array element
    ### is has valid theme info in it, otherwise an empty row is added
   
$row = array();
    if (
is_array($form[$key]['description'])) {

     
### In order to manually render a portion of the form, form_render is called.  It's
      ### single argument is the section of the form array that is to be rendered.  Note
      ### that form_render is recursive--it will render all form array elements in the portion
      ### of the array that you declare.
     
$row[] = form_render($form[$key]['screenshot']);
     
$row[] = form_render($form[$key]['description']);

     
### $form['status'] is the checkboxes element.  Notice that by using $form['status'][$key],
      ### only the checkbox for the current theme $key gets rendered.  If the rendered checkbox
      ### has a matching value in the above created $status array (which was passed to the
      ### checkboxes element), then it will be rendered as a checked box
     
$row[] = array('data' => form_render($form['status'][$key]), 'align' => 'center');
      if (
$form['theme_default']) {

       
### The radio buttons are rendered using the same logic as the checkboxes
       
$row[] = array('data' => form_render($form['theme_default'][$key]), 'align' => 'center');
       
$row[] = array('data' => form_render($form[$key]['operations']), 'align' => 'center');
      }
    }
   
$rows[] = $row;
  }

 
### Now the table is created using the usual theme_table approach
 
$header = array(t('Screenshot'), t('Name'), t('Enabled'), t('Default'), t('Operations'));
 
$output = theme('table', $header, $rows);

 
### The rendering code remembers which form elements have already been rendered--therefore,
  ### to render any remaining elements (in this case the submit buttons), simply call form_render
  ### using the entire form array, and only unrendered elements will be rendered.  It's good
  ### practice to always end with this, in case other modules may have used form_alter to
  ### include additional form elements
 
$output .= form_render($form);

 
### Finally, the constructed output is returned in the standard fashion.
 
return $output;
}
?>

A custom submit function also exists for this form. Note that the form is executed before it is themed/displayed.

<?php
function system_themes_submit($form_id, $values) {

 
db_query("UPDATE {system} SET status = 0 WHERE type = 'theme'");

 
### $_POST['op'] can be examined just as before to determine which button was pressed
 
if ($_POST['op'] == t('Save configuration')) {
    if (
is_array($values['status'])) {

     
### Only those checkboxes that were checked are returned in the processed form array
     
foreach ($values['status'] as $key => $choice) {
        if (
$choice) {
         
// If theme status is being set to 1 from 0, initialize block data for this theme if necessary.
         
if (db_num_rows(db_query("SELECT status FROM {system} WHERE type = 'theme' AND name = '%s' AND status = 0", $key))) {
           
system_initialize_theme_blocks($key);
          }
         
db_query("UPDATE {system} SET status = 1 WHERE type = 'theme' and name = '%s'", $key);
        }
      }
    }

   
### Likewise, only the selected radio button's value is available in the processed form,
    ### so it can be used to set the default
   
variable_set('theme_default', $values['theme_default']);
  }
  else {
   
variable_del('theme_default');
  }

 
drupal_set_message(t('The configuration options have been saved.'));

 
### Redirecting back to the page, which will reload the form with the updated data
 
drupal_goto('admin/themes');
}
?>
Advanced themeing: system_user

This example demonstrates how to build and implement a custom theming function that themes only a portion of the form array. This can be used when:

  • A section of the form needs to be themed identically multiple times, in which case the function can be written once and reused.
  • A form array needs to be returned without the use of drupal_get_form (as in the case of a core hook), but a portion of the array needs to be custom themed.
Before
<?php

function system_user($type, $edit, &$user, $category = NULL) {
  if (
$type == 'form' && $category == 'account') {
   
$allthemes = list_themes();

   
// list only active themes
   
foreach ($allthemes as $key => $theme) {
      if (
$theme->status) {
       
$themes[$key] = $theme;
      }
    }

    if (
count($themes) > 1) {
     
$rows = array();
      foreach (
$themes as $key => $value) {
       
$row = array();

       
// Screenshot column.
       
$screenshot = dirname($value->filename) .'/screenshot.png';
       
$row[] = file_exists($screenshot) ? theme('image', $screenshot, t('Screenshot for %theme theme', array('%theme' => $value->name)), '', 'class="screenshot"', false) : t('no screenshot');

       
// Information field.
       
$field = '<strong>'. $value->name .'</strong>';
       
$row[] = $field;

       
// Reset to follow site default theme if user selects the site default
       
if ($key == variable_get('theme_default', 'bluemarine')) {
         
$key = '';
          if (
$edit['theme'] == variable_get('theme_default', 'bluemarine')) {
           
$edit['theme'] = '';
          }
        }

       
// Selected column.
       
$row[] = array('data' => form_radio('', 'theme', $key, ($edit['theme'] == $key) ? 1 : 0), 'align' => 'center');

       
$rows[] = $row;
      }
     
$header = array(t('Screenshot'), t('Name'), t('Selected'));
     
$data[] = array('title' => t('Theme settings'), 'data' => form_item('', theme('table', $header, $rows), t('Selecting a different theme will change the look and feel of the site.')), 'weight' => 2);
    }

    if (
variable_get('configurable_timezones', 1)) {
     
$zones = _system_zonelist();
     
$data[] = array('title' => t('Locale settings'), 'data' => form_select(t('Time zone'), 'timezone', strlen($edit['timezone']) ? $edit['timezone'] : variable_get('date_default_timezone', 0), $zones, t('Select your current local time. Dates and times throughout this site will be displayed using this time zone.')), 'weight' => 2);
    }
    return
$data;
  }
}
?>
After
<?php
function system_user($type, $edit, &$user, $category = NULL) {
  if (
$type == 'form' && $category == 'account') {
   
$themes = list_themes();
   
ksort($themes);

   
// Reset to follow site default theme if user selects the site default
   
if ($key == variable_get('theme_default', 'bluemarine')) {
     
$key = '';
      if (
$edit['theme'] == variable_get('theme_default', 'bluemarine')) {
       
$edit['theme'] = '';
      }
    }

   
### Notice the use of the '#weight' attribute--individual elements can now be weighted
    ### to reorder the form.  Also note the '#theme' attribute--this is a reference to the
    ### custom theming function for this section of the form.  The function is named by
    ### prepending theme_ to the value of the '#theme' attribute.
   
$form['themes'] = array(
     
'#type' => 'fieldset', '#title' => t('Theme configuration'), '#description' => t('Selecting a different theme will change the look and feel of the site.'), '#weight' => 2, '#collapsible' => TRUE, '#collapsed' => FALSE, '#theme' => 'system_user');

    foreach (
$themes as $info) {
     
$info->screenshot = dirname($info->filename) . '/screenshot.png';
     
$screenshot = file_exists($info->screenshot) ? theme('image', $info->screenshot, t('Screenshot for %theme theme', array('%theme' => $info->name)), '', array('class' => 'screenshot'), false) : t('no screenshot');

     
$form['themes'][$info->name]['screenshot'] = array('#type' => 'markup', '#value' => $screenshot);
     
$form['themes'][$info->name]['description'] = array('#type' => 'item', '#title' => $info->name '#value' => dirname($info->filename));
     
$options[$info->name] = '';
    }

   
$form['themes']['theme'] = array('#type' => 'radios', '#options' => $options, '#default_value' => $edit['theme'] ? $edit['theme'] : variable_get('theme_default', 'bluemarine'));

    if (
variable_get('configurable_timezones', 1)) {
     
$zones = _system_zonelist();
     
$form['locale'] = array('#type'=>'item', '#title' => t('Locale settings'), '#weight' => 6);
     
$form['locale']['timezone'] = array(
       
'#type' => 'select', '#title' => t('Time zone'), '#default_value' => strlen($edit['timezone']) ? $edit['timezone'] : variable_get('date_default_timezone', 0),
       
'#options' => $zones, '#description' => t('Select your current local time. Dates and times throughout this site will be displayed using this time zone.')
      );
    }

   
### This is an implementation of a core hook, hook_user--so the constructed form array is
    ### returned without calling drupal_get_form.  This makes the use of the individual theming
    ### function necessary, otherwise no theming information could be passed back to the hook
   
return $form;
  }
}
?>

This is the individual theming function whose callback was specified above. Notice that it follows the same basic approach as the custom theming function in the previous example, including returning $output. The only difference is that this function only themes the part of the form where it's callback was declared.

<?php
function theme_system_user($form) {
  foreach (
element_children($form) as $key) {
   
$row = array();
    if (
is_array($form[$key]['description'])) {
     
$row[] = form_render($form[$key]['screenshot']);
     
$row[] = form_render($form[$key]['description']);
     
$row[] = form_render($form['theme'][$key]);
    }
   
$rows[] = $row;
  }

 
$header = array(t('Screenshot'), t('Name'), t('Selected'));
 
$output = theme('table', $header, $rows);
  return
$output;
}
?>
Drupal 4.6 vs. Drupal 4.7 Forms API Flowcharts

The following shows a comparison in the way forms were done pre-4.7 and how they are done in Drupal 4.7. Attached below is a Dia file containing an editable version of the diagrams.

See the print-friendly version if images are being hidden by menus.

Drupal Pre-4.7 Forms API

Example:

<?php
function example_menu($may_cache) {
 
$items = array();
  if (
$may_cache) {
   
$items[] = array(
     
'path' => 'example_page',
     
'callback' => 'example_page',
     
'title' => 'example page',
     
'access' => TRUE,
    );
  }
  return
$items;
}

function
example_page() {
 
$op = isset($_POST['op']) ? $_POST['op'] : '';
 
$edit = isset($_POST['edit']) ? $_POST['edit'] : '';
  if (
$op == t('Submit')) {
    if (!(
$edit['field_1'] >= 1 && $edit['field_1'] <= 2)) {
     
form_set_error('field_1', t('Enter a value between 1 and 2'));
    }
    if (!
form_get_errors()) {
     
db_query('INSERT INTO {example} (data) VALUES (%d)', $edit['field_1']);
     
drupal_goto();
    }
  }
 
$form = form_textfield(t('first textfield'), 'field_1',
    isset(
$edit['field_1']) ? $edit['field_1'] : 1, 60, 255,
   
t('Enter a value between 1 and 2 ')
  );
 
$form .= form_submit(t('Submit'));
  return
form($form);
}
?>
Drupal 4.7 Forms API

Example:

<?php
function example_menu($may_cache) {
 
$items = array();
  if (
$may_cache) {
   
$items[] = array(
     
'path' => 'example_page',
     
'callback' => 'example_page',
     
'title' => 'example page',
     
'access' => TRUE,
    );
  }
  return
$items;
}

function
example_page() {
 
$form['field_1'] = array(
   
'#type' => 'textfield',
   
'#default_value' => 1,
   
'title' => 'first textfield',
   
'description' => t('Enter a value between 1 and 2'),
  );
 
$form['field_2'] = array(
   
'#type' => 'submit',
   
'#value' => t('Submit')
  );
 
drupal_get_form('example_form_id', $form);
}

function
example_form_id_validate($form_id, $form_values) {
  if (!(
$form_values['field_1'] >= 1 && $form_values['field_1'] <= 2)) {
   
form_set_error('field_1', t('Enter a value between 1 and 2'));
  }
}

function
example_form_id_submit($form_id, $form_values) {
 
db_query('INSERT INTO {example} (data) VALUES (%d)', $form_values['field_1']);
 
drupal_goto();
}
?>
Forms API FAQ
Validation
How do you use the #validation arguments?

Example:

#valid => 'integer' , #validation arguments => array(1, 13)

This would map to:

valid_integer($form['element'], 1, 13)

where 1, 13 could be min/ max allowed values.

If you have multiple validation functions, use:

#valid => array('integer', 'uid'), #validation arguments = array(array(1, 13), array('anonymous'))

which would map to: valid_integer($form['element'], 1, 13) and valid_uid($form['element'], 'anonymous') where the second parameter could be role, for instance, so you could see if it's a valid uid in a certain role or '#valid' => 'filename', '#validation_arguments' => 'rwx' where it becomes valid_filename($form, $permissions)

How do you validate a URL, e-mail address or integer value?

Until they are written for core, you would need to write a valid_url, valid_email and valid_integer function.

is the nodeapi 'validate' op to be used anymore given the validation features of the new api? if so, when?

nodeapi validate is for node objects. It gets called by the form api though (node_validate), but it can also be called from code. (programmatically creating nodes)

Form logic
How do you handle multiple submit buttons in a form? Where do you put the 'dispatch logic'?

Examine $_POST['op'] to find the pressed button. It's a good idea to have an execute function for a form, and the dispatch logic can go in there.

What does #after_build do?

#after_build is used to make alterations to the form, after the form has already been processed.

This particularly comes into play during Node Preview. We can't display unfiltered data to the user from the $_POST, so we have a post_process on the node form which adds the node_preview form element using the already filtered values. See Image.module for an example.

Miscellaneous
How do I add a taxonomy selection to my form?

As of Drupal 4.7, you no longer have to do this -- it is done automatically!

So, where you used to have something like:

  if (function_exists('taxonomy_node_form')) {
    $output = implode('', taxonomy_node_form($node->type, $node));
  }

You now have... nothing!

Why was the #attribute_name convention chosen?

Observe the following example:

$form['fieldset'] = array('#type' => 'fieldset', '#title' => 'something'); $form['fieldset']['title'] = array('#type' => 'textfield')

If we didn't use the # in the beginning, the new title field would override the title property of the fieldset. we tried using defines, which looked nice, but were troublesome, and eventually we went witht the #, because if you look at your html page DOM with your dom inspector, you see all the text is named as '#TEXT', so it seemed like using that for standardization seemed like a good idea.

Tips and Tricks

Post your tips and tricks and sample module conversions, as well as any forms API-related questions you've had answered here as comments. They'll be incorporated into the text.

#tree and #parents

Summary: The form API compresses two structures into one, one is the rendering structure defined by the keys of the form array. The other is the data structure which is defined by #parents from which #name (HTML element name) and #id is computed. There is an automated feature which creates #parents from the array keys--it is controlled by #tree and described below.

All forms are a tree, for example:

           (root)
             |
       Foo  / \
           /   \
          |     |
   Bar   /     /\
        /     /  \
       |
Baz  /
     /

In code, this is written:

$form['foo']['bar']['baz']

As long as the #tree attribute is true at any point in the tree, the form element is aware that it is in a tree, and traverses the tree towards the root (from baz, to bar, to foo). Along the way, the names of the modules passed are stored in #parents. #parents is used to create the name/ID of the form element itself.

So, if:

$form['foo']['#tree'] == TRUE

And

$form['foo']['bar']['#tree'] == TRUE

And

$form['foo']['bar']['baz']['#tree'] == TRUE

Then #parents for baz will be array('foo', 'bar', 'baz') and the name of the element in the HTML will be $edit['foo']['bar']['baz']

If, on the other hand,

$form['foo']['bar']['baz']['#tree'] == FALSE

Then #parents will only be array('baz') and the name of the element in the HTML edit['baz']

There are shortcuts to traversing the full tree each time. If you set #tree = TRUE at a closer point to the root of the tree, as in:

$form['foo']['#tree'] = TRUE

and you have not specifically set #tree anywhere else, then it will cascade and make all of the sub-elements' #tree = TRUE. This is very useful because otherwise you would need to write #tree = TRUE for each element in the tree.

A common use of #tree is fieldsets. Another example is the checkboxes element type where #tree is set to TRUE internally before expanding to multiple checkbox elements.

The following is the section of code from which deals with #tree and #parents, taken from _form_builder():

  foreach (element_children($form) as $key) {
    // don't squash an existing tree value
    if (!isset($form[$key]['#tree'])) {
      $form[$key]['#tree'] = $form['#tree'];
    }

    // don't squash existing parents value
    if (!isset($form[$key]['#parents'])) {
      // Check to see if a tree of child elements is present. If so, continue
      // down the tree if required.
      $form[$key]['#parents'] = $form[$key]['#tree'] && $form['#tree'] ?
        array_merge($form['#parents'], array($key)) : array($key);
    }

Note that the code is not the same to the explanation above for performance reasons: instead each element walking towards to the root as long as #tree is TRUE, we pass #parents down as long as #tree is TRUE.

You can set #parents manually, but the need for this is rare. More common is to read #parents to determine where in the form tree the current element is. Setting #parents does not affect the rendering of the form, that's decided by the indexes. However, setting #parents does effect placement $form_values as can be seen from filter_form().

Adding a custom element type & expanding elements

the start and end date selection boxes proved to be a particular challenge in my upgrade of eventrepeat module. i handled it by creating my own form element type.

first, use hook_elements to declare the new type--'eventrepeat_date' is the name of the new type. also, if you want any default values, be sure to declare them either here or in the process function:

function eventrepeat_elements() {
   $type['eventrepeat_date'] = array('#input' => TRUE,);
   return $type;
}

here's how the element type is invoked when in a $form array. notice in particular how the #process attribute is declared here as an array--the key is the name of the callback used to expand the type into multiple elements (you may not need a function such as this if your new type is simple), and the value is an array of arguments to be passed to the callback function. normally, #process would be declared as a default value in the element type declaration, but since i needed to pass $node and $edit as function arguments in this case, i declare it for each instance of the element type:

$form['end_controls']['end_date'] = array(
  '#type' => 'eventrepeat_date',
  '#title' => t('Repeat end date'),
  '#process' => array('_eventrepeat_form_date' => array($edit, $node, 'eventrepeat_end')),
);

next, write the expand function. the first argument is always the element that is being passed for processing, and the other arguments are the optional arguments i added in my #process attribute. notice that i'm adding to $element and returning it:

function _eventrepeat_form_date($element, $edit = NULL, $node = NULL, $prefix = NULL) {

  // Get current year, and drop next 10 years into an array.
  $date = getdate(time());
  $curyear = $date['year']; 
  $years = array(0 => '--'.t('Select').'--');
  while ($i < 10) {
    $years[$curyear + $i] = $curyear + $i;
    $i++;
  }

  // Months array.
  $months = array(
    '--'.t('Select').'--',
    t('January'),
    t('February'),
    t('March'),
    t('April'),
    t('May'),
    t('June'),
    t('July'),
    t('August'),
    t('September'),
    t('October'),
    t('November'),
    t('December')
  );
 
  // Days array.
  $days = array(0 => '--'.t('Select').'--');
  for ($i = 1; $i <= 31; $i++) {
  $days[$i] = $i;
  }

  //compose the select boxes, and add the exception editor button if necessary
  $element[$prefix.'month'] = array(
    '#type' => 'select',
    '#default_value' => $edit ? $edit[$prefix.'month'] : ($node->{$prefix.'month'} ? $node->{$prefix.'month'} : 0),
    '#options' => $months,
  );
  $element[$prefix.'day'] = array(
    '#type' => 'select',
    '#default_value' => $edit ? $edit[$prefix.'day'] : ($node->{$prefix.'day'} ? $node->{$prefix.'day'} : 0),
    '#options' => $days,
  );
  $element['comma'] = array(
    '#type' => 'markup',
    '#value' => ', ',
  );
  $element[$prefix.'year'] = array(
    '#type' => 'select',
    '#default_value' => $edit ? $edit[$prefix.'year'] : ($node->{$prefix.'year'} ? $node->{$prefix.'year'} : 0),
    '#options' => $years,
  );

  if ($prefix == 'eventrepeat_EXDATE_edit') {
    $element['exception_button'] = array(
      '#type' => 'submit',
      '#value' => t('Add/Delete Exception'),
    );
  }

  return $element;
}

then, a theming function to pretty it up. this is basically a theming function for a form item, but i put an inline container and $element['#children'] (which is the constructed select boxes, etc. from my expand function) into it. naming convention for the theme function is theme_typename:

function theme_eventrepeat_date($element) {
  return theme(
    'form_element',
    array(
      '#title' => $element['#title'],
      '#description' => $element['#description'],
      '#id' => $element['#id'],
      '#required' => $element['#required'],
      '#error' => $element['#error'],
    ),
    '<div class="container-inline">'. $element['#children']. '</div>'
  );
}
#process

the semantics of #process have changed so that you need to have the function name as the key in an array

'#process' => array('_my_function' => array())
Adding and theming additional fields to a node form

Adding a form element to an existing node form is a matter of using hook_form_alter and checking for the appropriate form id.

Observe the following example from taxonomy.module:

<?php
function taxonomy_form_alter($form_id, &$form) {
  if (isset(
$form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
...
 
// Add your form array here, for example:
       
$form['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield', '#default_value' => $typed_string, '#maxlength' => 100, '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid, '#required' => $vocabulary->required, '#title' => $vocabulary->name, '#description' => t('A comma-separated list of terms describing this content (Example: funny, bungie jumping, "Company, Inc.").'));

}
}
?>

The only thing in the form_alter hook should be things related to the form declration itself. If you'd like to theme this in a particular way (for example, place the results in a table), set the form's #theme attribute:

<?php
$form
['custom_form']['#theme'] = 'custom_form';
?>

Then later, declare a function to handle the theming of this form. Here's an example from system.module:

<?php
function theme_system_user($form) {
  foreach (
element_children($form) as $key) {
   
$row = array();
    if (
is_array($form[$key]['description'])) {
     
$row[] = form_render($form[$key]['screenshot']);
     
$row[] = form_render($form[$key]['description']);
     
$row[] = form_render($form['theme'][$key]);
    }
   
$rows[] = $row;
  }

 
$header = array(t('Screenshot'), t('Name'), t('Selected'));
 
$output = theme('table', $header, $rows);
  return
$output;
}
?>
Adding your own extra _submit or function using hook_form_alter

When using Forms API to alter an existing form, if you add your own items you need a way to get the data back. When doing this to the node or user form, this is easy, but not so easy when modifying some of the more obscure forms.

In this instance, you need to add your own hook into the _submit chain, but how to do this isn't entirely straightforward, because you have to provide the hook with the arguments; one of these arguments needs to be a reference to $form_values, which you don't have yet. Or do you?

Turns out, forms API actually does provide the reference for you -- on the existing submit hook. All you have to do is clone it:

In your hook_form_alter:

<?php
  $form
['#submit']['my_very_own_form_submit'] = current($form['#submit']);
?>

Just put your function name as the key to the array. If you like, add additional arguments using array_merge. ...= array_merge(current($form['#submit']), array($arg1, $arg2, $arg3);

This same approach will also work with #validate, #process, or #after_build callbacks.

An alternative approach to columns and other detailed layout control
An alternative approach to columns and related things
Introduction

The solution to columns provided on one of the other pages assumes that you are building a module, since it requires you to add a hook. The following is an approach that allows theme-based control of form layout. It is geared, to some extent, toward the 'webform' module, but is useful for any form.

More IDs

The first problem here is that the form module is stingy with ID attributes, which makes it disproportionately difficult to take CSS control. There are many 'form-item' divs that have no unique attributes visible to CSS selectors.

This is easy to fix by theming the form_element function

<?php
// we override this to get css-able id's on the elements.
function phptemplate_form_element($title, $value, $description = NULL, $id = NULL, $required = FALSE, $error = FALSE) {

    if(
$id) {
       
$output  = '<div class="form-item" id="form-item-' . form_clean_id($id) . '">'."\n";
    } else {
       
$output  = '<div class="form-item">'."\n";
    }
 
$required = $required ? '<span class="form-required" title="'. t('This field is required.') .'">*</span>' : '';

  if (
$title) {
    if (
$id) {
     
$output .= ' <label for="'. form_clean_id($id) .'">'. t('%title: %required', array('%title' => $title, '%required' => $required)) . "</label>\n";
    }
    else {
     
$output .= ' <label>'. t('%title: %required', array('%title' => $title, '%required' => $required)) . "</label>\n";
    }
  }

 
$output .= " $value\n";

  if (
$description) {
   
$output .= ' <div class="description">'. $description ."</div>\n";
  }

 
$output .= "</div>\n";

  return
$output;
}
?>

The entire effect of this is to put an ID on the overall form-item div, instead of only on some of its interior components.

How about an ID for the form?

At least with webform, there is no unique ID at the top of the form. For webform, here is a solution using the themeing provided in that module:

<?php
function phptemplate_webform_form_1666 ($form) {

    return
'<div id="member_info_form">'. form_render($form) . '</div>';
}
?>
Now, how about getting some use out of all this?

Given ID's on form-item divs, you can use CSS. here's an example with checkboxes in columns. To create this, I used the Firefox 'view formatted source' extension to grab the IDs of all the elements I cared about. The result: two columns of checkboxes without any per-form PHP except to add the unique ID.

#member_info_form div.description {
    margin-top: 0;
}

#member_info_form div.webform-component-textfield div.form-item {
    min-height: 3em;
    height: auto;
    }

#member_info_form div.form-item input.form-text {
    float: right;
    margin-right: 30px;
    top: -10px;
    position: relative;
}

#member_info_form div.form-item > label {
    display: block;
    clear: both;
}

#member_info_form div.description {
    display: block;
    clear: both;
    margin-top: 0;
    margin-bottom: 0;
}


#form-item-edit-submitted-1161135984-1160572783-preschool_2_to_5_years_old {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160572783-religious_school_k7 {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160572783-special_needs_education {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
    clear: left;
}
#form-item-edit-submitted-1161135984-1160572783-prozdor_offsite_hebrew_high_school {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160572783-family_education {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160572783-adult_education {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}

#form-item-edit-submitted-1161135984-1160008632-youth_toddlerspreschool {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160008632-youth_15_grade {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160008632-youth_56_grade {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
    clear: left;
}
#form-item-edit-submitted-1161135984-1160008632-youth_712_grade {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160008632-adult_singles_programming {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160008632-adult_social_programming {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160008632-family_programming {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160010108-social_action_programs {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160010108-committee_work {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
    clear: left;
}
#form-item-edit-submitted-1161135984-1160010108-israel_programming {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160010240-shabbat_services {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160010240-daily_worship_services {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160010240-family_religious_services {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
#form-item-edit-submitted-1161135984-1160010240-alternative_religious_services {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
    clear: left;
}
#form-item-edit-submitted-1161135984-1160010240-holiday_celebrations {
    margin-top: 0;
    margin-bottom: 0;
    width: 20em;
    float: left;
}
changing title of a node

In some cases you want to construct the title of a CCK node automatically, for example, you may want to construct it from other values on the form.

<?php
function mymodule_form_alter($form_id, &$form) {
 
// Check if we are on a node editing form for our type.
 
if (isset($form['#node']) && ($node = $form['#node']) && $form_id == $node->type .'_node_form') {
    if (
$node->type == 'type-to-change') {
     
// As we are going to construct the title ourself, there should
      // not be any edit box for the title. As there is still a validator
      // on the title field, we need to input a dummy value.
     
$form['title']['#type'] = 'value';
     
$form['title']['#value'] = 'this will not be used';
    }
  }
  return
$form;
}

function
mymodule_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  if (
$op == 'validate' && $node->type == 'type-to-change') {
   
// We can change the value of the title in the validate hook
    // for nodes. The form is actually passed as 3rd parameter
    // to hook_nodeapi() for $op == 'validate'.
   
$form = $teaser; //messy
   
form_set_value($form['title'], 'some new value');
  }
}
?>

After some discussion with chx on #drupal, this seemed like the best solution.

Creating an array of form elements

As someone who's done a lot of work with the "old" form methods, the new Forms API is a lot of new information to chew on. Almost everything is different. In the spirit of practicality, here's how I solved the riddle of "how to I make an array of form elements?" The answer is #tree.

In this case I want to create an array of select fields to assign one of a pre-defined set of values to a given piece of data. It's meant to help assign CVS field headers (and the content in their columns) to node data rows. Another example might be a mass-categorization page for lots of posts.

Here's the code before:

<?php
$fields
= array('pre', 'defined', 'values', 'to', 'assign');
foreach (
$array_of_stuff as $i => $value) {
 
$output .= form_select('', 'assign]['. $i, $edit['assign'][$i], $fields));
}
?>

Here's the code after:

<?php
$form
['assign'] = array('#tree' => 1);
$fields = array('pre', 'defined', 'values', 'to', 'assign');
foreach (
$array_of_stuff as $i => $value) {
$form['assign'][$i] = array(
       
'#type' => 'select',
       
'#title' => '',
       
'#default_value' => $edit['assign'][$i],
       
'#options' => $fields
     
);
?>

I'm sure all this information is contained in the big docs about Forms API, but it can be a bit dense. I hope this helps people in getting to know the new API, which offers a lot more power once you learn it's ins and outs.

Creating fieldsets outside forms with minimal code

Every now and again there comes a time when you want to use form groups (HTML fieldset elements) without having a real form, simply because they make good containers for content.

With the old form code, we could happily write something like the following:

<?php
$output
.= form_group(t('My Fieldset'), $group);
?>

I dreaded the thought of creating a new $form, and all kinds of form API code just to create a fieldset, but luckily we can accomplish the same effect with only a minimal amount of fuss:

<?php
$output
.= theme('fieldset', array('#title' => t('My Fieldset'), '#children' => $group));
?>
Creating multi-part forms

There's no question that the new forms API will make life easier for Drupal developers. However, not everything about them is intuitive. Creating multi-part forms is one of them. I spent many, many hours trying to figure out the best way to do this. Granted, I'm not a hard-core coder like some others Drupalers out there, but I think even advanced coders will also have their share of frustration. And so to help you avoid the pain I went through, I offer this tip. Note that what follows assumes you are familiar with forms API.

It wasn't until I studied the code in node.module that I was actually able to figure this out. Specifically, the three functions in that module that I learned from were node_admin_nodes(), node_multiple_delete_confirm(), and node_multiple_delete_confirm_submit(). Together, these functions allow users to select which nodes to delete (part 1 of the multi-part form) and then confirm the deletion (part 2 of the the form).

But rather than use that module as a basis for this discussion, I created a simple module that simulates those functions more compactly to avoid losing the forest through the tangles of code in node.module.

Anyway, here's the boiled-down, multi-part form simulation module:

<?php
function formtest_menu() {
  if (!
$may_cache) {
   
$items[] = array('path' => 'formtest', 'title' => t('initial form'),
     
'callback' => 'formtest_page',
     
'access' => TRUE,
    );
  }
  return
$items;
}

// Main function
function formtest_page() {

 
// If user has already performed an operation, handle it.
 
if ($_POST['op'] == 'Submit' || $_POST['edit']['confirm']) {
    return
formtest_confirm_form();
  }

 
// Main form

  // This is a parent element so #tree is set to TRUE
 
$form['checkboxes'] = array(
       
'#tree' => TRUE,
    );

   
// Checkbox #1 child element
 
$form['checkboxes'][1]['checked'] = array(
     
'#type' => 'checkbox',
     
'#title' => 'Checkbox #1',
     
'#default_value' => 0,
  );
 
$form['checkboxes'][1]['title'] = array(
   
'#type' => 'hidden',
   
'#value' => 'Checkbox #1',
  );

 
// Checkbox #2 child element
 
$form['checkboxes'][2]['checked'] = array(
   
'#type' => 'checkbox',
   
'#title' => 'Checkbox #2',
   
'#default_value' => 0,
  );
 
$form['checkboxes'][2]['title'] = array(
   
'#type' => 'hidden',
   
'#value' => 'Checkbox #2',
  );

 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => 'Submit',
  );

  return
drupal_get_form('simple_form', $form);
}

function
formtest_confirm_form() {
 
$edit = $_POST['edit'];

 
// Here, we create a form element that will hold the $_POST data
  // which contains an array representing the checkboxes the user checked.
  // Notice we give the first dimension of this element the name "checkboxes".
  // This is the same name as the parent form element in the formtest_page.
  // Giving it the same name allows us to add children to the parent (with $_POST
  // acting as our surrogate parent).  We must do this so we can pass our data on
  // to the formtest_confirm_submit().
 
$form['checkboxes'] = array('#tree' => TRUE);

 
// Now we populate the element with our data.
 
foreach ($edit['checkboxes'] as $checkbox_id => $data) {
    if (
$data['checked']) {
     
$form['checkboxes'][$checkbox_id] = array(
       
'#type' => 'hidden',
       
'#value' => $checkbox_id,
      );
      
$checked[] = check_plain($data['title']);
    }
  }

 
$form['data'] = array(
   
'#type' => 'markup',
   
'#value' => theme('item_list', $checked),
  );
 
$form['operation'] = array(
    
'#type' => 'hidden',
    
'#value' => 'delete',
  );
 
$output = confirm_form('formtest_confirm', $form,
   
t('delete these boxes?'),
   
'formtest', t('This action cannot be undone.'),
   
t('Delete'), t('Cancel')
  );
  return
$output;
}

function
formtest_confirm_submit($form_id, $edit) {
 
//Simulate deletion of our data
 
_formtest_simulate_deletions($edit);

 
//Return user back to main page.
 
drupal_goto('formtest');
}

//This code is for simulation purposes only.
function _formtest_simulate_deletions($edit) {
 
drupal_set_message('If this code was real, the checkbox(es) you deleted would not appear.');
 
// Code to delete data here
}
?>

You can actually install this chunk of code in your modules directory and see it work. At the time of this writing, you'll need cvs but by the time you read this, it will likely work with version 4.7+ of Drupal.

The code basically speaks for itself. There are a few tricky catches in there. Lengthy comments have been supplied where this is the case. The three functions to focus on are formtest_page(), formtest_confirm_form(), formtest_confirm_submit(). The other functions aren't important to creating multi-part forms.

This code is extremely simple. A more realistic example would be more complex and require to be very careful to run security checks on the $_POST data which ultimately ends up in our form. Under normal circumstances, this is a no-no.

If anyone can offer suggestions on how to improve this code or simplify it, please share!

This tip courtesy of: Steve Dondley, Dondley Communications with special thanks to Earl Miles (a.k.a. merlinofchaos) for guidance offered.

Creating submit buttons with images

Instead of the default and boring grey submit buttons, it's possible to use clickable images to submit forms. This has been possible with HTML for a long time.

Here's how to do it with the new forms api:

1) Place the following code in your module:

/**
* Custom form element to do our nice images.
*/
function hook_elements() {  // Change this line
  $type['imagebutton'] = array(
    '#input' => TRUE,
    '#button_type' => 'submit',
    '#executes_submit_callback' => TRUE,
    '#name' => 'op',
    '#process'=> array('hook_imagebutton_process' => array()),
  );

  return $type;
}

function theme_imagebutton($element) {
  return '<input type="image" class="form-'. $element['#button_type'] .'" name="'. $element['#name'] .'" id="'. $element['#id'] .'" value="'. check_plain($element['#default_value']) .'" '. drupal_attributes($element['#attributes']) . ' src="' . $element['#image'] . '" alt="' . $element['#title'] . '" title="' . $element['#title'] . "\" />\n";

}


function imagebutton_value() {
  // null function guarantees default_value doesn't get moved to #value.
}

function hook_imagebutton_process($form) {
  $form['op_x'] = array(
    '#name' => $form['#name'] . '_x',
    '#input' => TRUE,
    '#button_type' => 'submit',
    '#form_submitted' => TRUE,
  );
  return $form;
}

Be sure to "hook" in hook_elements() above with the name of your module.

2. Now use the following piece of code in your forms to create the submit button:

  $form['submit'] = array(
    '#type' => 'imagebutton',
    '#image' => '/submit.jpg',  // provide the path to your image here
    '#default_value' => t('Login'), // original value of button text
  );

Special thanks to Earl Miles, (aka merlinofchaos) for this tip.

Develop a simple block module with a simple form

Develop a simple block module with a simple form. The form has 2 textfields and the values should be saved to a db table. The block developed with the 4.7 form api. Here's the code:

Special thanks to Meinolf Droste, (aka meinolf) for this tip.

<?php
function quickmail_perm(){
    return array (
'can send quickmail');
}

function
quickmail_block($op = "list", $delta = 0) {
  if (
$op == 'list') {
   
$block[0]['info'] = t('Quickmail Block');
    return
$block;
  }
  elseif (
$op == 'view') {
     
$block['subject'] = 'Quickmail';
     
$block['content'] = quickmail_form();
      return
$block;
  }
}

function
quickmail_form($edit = null) {

 
$form['details']['firma'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Firma'),
   
'#default_value' => $edit['firma'],
   
'#size' => 15,
   
'#maxlength' => 128,
   
'#description' => null,
   
'#attributes' => null,
   
'#required' => true,
  );
 
$form['details']['telefon'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Telefon Nr.'),
   
'#default_value' => $edit['telefon'],
   
'#size' => 15,
   
'#maxlength' => 128,
   
'#description' => t('Geben Sie hier die Telefon Nr. ein unter der Sie unseren Rückruf erwarten.'),
   
'#attributes' => null,
   
'#required' => true,
  );

 
$form['details']['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Senden'),
   
'#submit' => TRUE,
  );
  return
drupal_get_form('quickmail_form', $form);
}

function
quickmail_form_submit($form_id, $form_values) {

 
db_query("INSERT INTO {callback} (firma, telefon) VALUES ('%f', '%t')", $form_values['firma'], $form_values['telefon']);

drupal_goto('thank_you_page');//the page the user should see, after submit the form.
}
?>
Easier debugging of forms code

Replace the element_child in form.inc with this:

<?php
foreach($element as $k => $v) {
 
$result = array();
  if (
$k[0] != '#') {
    if (
is_array($v)) {
     
$result[] = $k;
    }
    else {
      die(
"You have a wrong key: $k");
    }
  }
  return
$result;
}
?>
Getting a form element without a form

I'm using AJAX to replace form elements within a form and I need to get a form element, such as a select box, without the surrounding form. Here's how!

Build your $form like normal, then call this:

<?php
print form_render(form_builder('my_select', $form));
?>
Handling File Uploads

In order to use file uploads with the Form API, you will have to include the following in your form:

<?php
$form
['#attributes'] = array('enctype' => "multipart/form-data");
?>

You can then handle the upload with file_check_upload. Here's an example:

<?php
function form() {
 
$form['#attributes'] = array('enctype' => "multipart/form-data");
 
//'upload' will be used in file_check_upload()
 
$form['upload'] = array(
   
'#type' => 'file');
}

function
form_verify() {
  if(!
file_check_upload('upload')) {
   
// If you want to require it, you'll want to do it here... something like this:
 
}
}

function
form_submit() {
 
$file = file_check_upload();
 
//handle the file, using file_save_upload, or something similar
}
?>
Modifying checkboxes to display in multiple columns

This tip will show you how to modify the checkboxes element type so that checkboxes display in multiple columns instead of one single column. This is particulary useful for modules that would have users select from a very long list of choices. This tip will help you present the checkboxes in a more compact form, making your site more user friendly. Disclaimer: This document shares how I solved this particular problem and doesn't claim to be anything more. There may be other ways of accomplishing the same thing. If you are a forms API guru and know an easier way, please share!

The first job is to override the expand_element_checkboxes() function in the form.inc. Here's how it looks:

<?php
function expand_checkboxes($element) {
 
$value = is_array($element['#value']) ? $element['#value'] : array();
 
$element['#tree'] = TRUE;
  if (
count($element['#options']) > 0) {
    if (!isset(
$element['#default_value']) || $element['#default_value'] == 0) {
     
$element['#default_value'] = array();
    }
    foreach (
$element['#options'] as $key => $choice) {
      if (!isset(
$element[$key])) {
       
$element[$key] = array('#type' => 'checkbox', '#processed' => TRUE, '#title' => $choice, '#default_value' => in_array($key, $value), '#attributes' => $element['#attributes']);
      }
    }
  }
  return
$element;
}
?>

To override it, the first step is to actually put the new function in our module. Here it is:

<?php
function expand_checkbox_columns($element) {
 
$value = is_array($element['#value']) ? $element['#value'] : array();
 
$element['#type'] = 'checkboxes';
 
$element['#tree'] = TRUE;
  if (
count($element['#options']) > 0) {
    if (!isset(
$element['#default_value']) || $element['#default_value'] == 0) {
     
$element['#default_value'] = array();
    }
    foreach (
$element['#options'] as $key => $choice) {
     
$class = ($column % $element['#columns']) && $column ? 'checkbox-columns' : 'checkbox-columns-clear';
      if (!isset(
$element[$key])) {
       
$element[$key] = array('#type' => 'checkbox', '#processed' => TRUE, '#title' => $choice, '#default_value' => in_array($key, $value), '#attributes' => $element['#attributes'], '#prefix' => '<div class="' . $class . '">', '#suffix' => '</div>');
      }
     
$column++;
    }
  }
  return
$element;
}
?>

The above function contains the logic that will create our multi-column list of checkboxes. Notice it is the same as the expand_checkboxes_function except it has a different name, "expand_checkbox_columns", and it has some code that puts some div tags in front of the checkboxes. The div tags are how we ultimately control which column the checkboxes appear. Also notice this line:

<?php
$element
['#type'] = 'checkboxes';
?>

That line is in there to suppress a bug in the current version of forms api. In the interests of brevity, I'll skip over exactly why that's there. But basically, all it does is fool forms.inc that all it's dealing with is your standard checkboxes element type.

The next step is to let Drupal know about the new "expand_checkbox_columns" function you just created. You do that by putting a hook_elements function in your module like so:

<?php
function pol_tracker_elements() {
 
$type['checkbox_columns'] = array('#input' => TRUE, '#process' => array('expand_checkbox_columns' => array()), '#tree' => TRUE);
  return
$type;
}
?>

Simple enough. But see the other tip about creating your own elements for more details on what exactly this does.

Next, of course, we need some code that actually puts our new element function to use. Here's an example:

<?php
  $form
['example_of_multicolumn_checkbox_element'] = array(
   
'#type' => 'checkbox_columns',
   
'#title' => t('Check off which kinds of politicians you\'d like to track'),
   
'#default_value' => $value,
   
'#columns' => 3,
   
'#options' => pol_tracker_get_pol_type_names(),
   
'#suffix' => '<br style="clear:both;"/>',
  );
?>

Couple of things to point out here. First the '#type' is set to 'checkbox_columns'. This is how we ensure our new expand_checkbox_columns function gets called. Second, notice the "#columns' property. This is how you can change the number of columns displayed. If you look at the new expand_checkbox_columns() function we created above, you'll see that it makes use of the '#columns' value.

Our last step is to add some CSS to our style sheet so this will work. Here's how:

.checkbox-columns .form-item {
  width: 15em;
  margin-right: 10px;
  float: left;
  display: inline;
}

.checkbox-columns-clear .form-item {
  width: 15em;
  margin-right: 10px;
  clear: left;
  float: left;
  display: inline;
}

And that should do it. Notice that you can control the width of the columns by changing the width property (set to 15em). You will want to change this according to the length of your longest checkbox selection to give your list a nice neat appearance.

Module dependency checker

Do you have a module that depends on other modules? Have you ever wished there was a way to ensure they were installed before your module is enabled?? Well, I've figured out a fairly elegant way for contrib modules to do simple dependency checking, via hook_form_alter. Here's how it's done:

  1. Add this section of code to your module's hook_form_alter:


    <?php
    /* this is a check for module dependencies.  the only way we
        can ensure this check happening when the module is initially
        enabled is to insert the check for when the form is initially
        built, which will also be caught when the admin/module page is
        reloaded upon submission.  this means we never want to call this
        function when the form has been submitted, so make sure there's
        no $_POST. */
    if ($form_id == 'system_modules' && !$_POST) {
     
    modulename_system_module_validate($form);
    }
    ?>


  • Add the dependency check function to the module:


    <?php
    /**
    * Validates module dependencies for the module.
    *
    * @param $form The form array passed from hook_form_alter.
    *
    * Set the $module variable to a string which is the name of the module, minus
    * the .module extension.  Set $dependencies to an array of module names which
    * the module is dependent on--each element is a string which is the module name
    * minus the .module extension.  Note that this will not check for any dependencies
    * for the modules this module depends on--only those that are explicitly listed in
    * the $dependencies array.
    */
    function modulename_system_module_validate(&$form) {

     
    $module = 'modulename';
     
    $dependencies = array('dependentmodule1', 'dependentmodule2', 'etc');

      foreach (
    $dependencies as $dependency) {
          if (!
    in_array($dependency, $form['status']['#default_value'])) {
           
    $missing_dependency = TRUE;
           
    $missing_dependency_list[] = $dependency;
          }
      }

      if (
    in_array($module, $form['status']['#default_value']) && isset($missing_dependency)) {
       
    db_query("UPDATE {system} SET status = 0 WHERE type = 'module' AND name = '%s'", $module);
       
    $key = array_search($module, $form['status']['#default_value']);
        unset(
    $form['status']['#default_value'][$key]);
       
    drupal_set_message(t('The module %module was deactivated--it requires the following disabled/non-existant modules to function properly: %dependencies', array('%module' => $module, '%dependencies' => implode(', ', $missing_dependency_list))), 'error');

      }

    }
    ?>


  • I think the code comments explain the feature pretty well. The main trick is that you can't check the modules dependencies upon it's initial form submission in admin/modules because, well, it's not enabled yet... :): fortunately, there's a drupal_goto which sends you right back to the admin/modules page--so we can catch it there and manually disable the module in the database.

    This works both when the module is initially enabled, and if somebody happens to try and disable any dependent modules later. To keep it simple, I just warn the user and disable the module. This solution is not foolproof (with layered dependencies it's possible that a module might pass when it shouldn't--but those are almost non-existent in Drupal ATM AFAIK), but it will work for probably 98% of the use cases--and providing this protection is as hard as dropping in the code above and setting your dependencies.

The form_set_value function

This is a very useful function if for example you wish to make changes to a property on _validate, and see those changes on _submit.

Here is an example from http://drupal.org/node/51063.

Background info

On OG's invite form is a textarea called 'mails' where people may enter a list of e-mail addresses or usernames, either comma- or newline-separated:

<?php
function og_invite_form() {
 
$form['mails'] = array('#type' => 'textarea', '#title' => t('Email addresses or usernames'),  '#description' => t('Enter up to %max email addresses or usernames. Separate multiple addresses by commas or new lines. Each will receive an invitation message from you.', array('%max' => $max)));
?>

During the _validate process, the textarea input is parsed, a series of checks take place to ensure valid e-mail addresses exist:

<?php
function og_invite_validate($form_id, $form_values, $form) {
 
$mails = $form_values['mails'];
 
$mails = str_replace("\n", ',', $mails);
 
$emails = explode(',', $mails);
  if (
count($emails) > $max) {
   
form_set_error('mails', t("You may not specify more than %max email addresses or usernames.", array('%max' => $max)));
  }
  else {
   
$valid_emails = array();
   
$bad = array();
    foreach (
$emails as $email) {
     
$email = trim($email);
      if (empty(
$email)) {
        continue;
      }
      if (
valid_email_address($email)) {
       
$valid_emails[] = $email;
      }
      else {
       
$account = user_load(array('name' => check_plain($email)));
        if (
$account->mail) {
         
$valid_emails[] = $account->mail;
        }
        else {
         
$bad[] = $email;
        }
      }
    }
    if (
count($bad)) {
     
form_set_error('mails', t('invalid email address or username: '). implode(' ', $bad));
    }
  }
}
?>

At the end of this process, we have a nice array called $valid_emails, which contains all of our valid e-mail addresses. However, we normally can't change form values during the _validate stage.

The problem

This means that when we get to our _submit function, we have to do the vast majority of this processing again:

1. Replacing the newlines with commas
2. Exploding the string into an array
3. Looping through each value
4. Trimming it and seeing if it's empty
5. Checking whether it's an e-mail or a username

What a waste, when $valid_emails is right there with all the data we need in it!

The solution

With form_set_value we can store a reference to the $valid_emails array, and then look at it when we're in _submit!

So first, add a property to the form itself to hold the valid e-mails:

<?php
og_invite_form
:
 
$form['valid_emails'] = array('#type' => 'value', '#value' => array());
?>

Next, in the validate function, after all of the processing is complete, store the value of the $valid_emails:

<?php
og_form_validate
:

// ... all of that code from above ...

   
if (count($bad)) {
     
form_set_error('mails', t('invalid email address or username: '). implode(' ', $bad));
    }
    else {
     
// Store valid e-mails so we don't have to go through that looping again on submit
     
form_set_value($form['valid_emails'], $valid_emails);
    }
  }
?>

Finally, in the submit function, retrieve the value:

<?php
function og_invite_submit() {
 
$emails = $form_values['valid_emails'];
?>

And now we can just loop through them knowing that they've already been verified by the validation function.

Using form builder for "normal" forms

Here at Electric Word plc, we recently needed an email capture form which we wanted to build using the Drupal Forms API. An example of the pre-drupal version can be seen on our pponline.co.uk email collection page. The problem is that the Drupal Forms API prefixes fields into an edit array, for example:

<input type="text" name="edit[foo]" value="bar" />

What happens if you don't want the results to be in the edit array? What do you do if the form you're writing needs to be sent to a 3rd party script which has predefined names for the data that needs to be sent to them?

The prefixing or 'edit' is something that's hard-coded into the core Forms API (it happens quite early on in the form_builder function). So how do we change drupal form names into "normal" form names? Enter Regualr Expressions and preg_replace.

Instead of doing...

<?php
function example_admin_section() {
 
$form['foo'] = array(
   
'#type' => 'textfield',
   
'#default_value' => 'bar',
  );

  return
drupal_get_form('example_admin_section', $form);
}
?>

You'd need to first generate the content into a variable and then do a regular expression on that to find and replace content... Like this..

<?php
function example_admin_section() {
 
$form['foo'] = array(
   
'#type' => 'textfield',
   
'#default_value' => 'bar',
  );

 
$content = drupal_get_form('example_admin_section', $form);
 
$content = preg_replace('|name="edit\[(.+?)\]"|i', 'name="$1"', $content);
  return
$content;
}
?>

That regular expression basically says find anything that looks like name="edit[something]" and replace it with name="something".

The only issue with this is that it will only work properly with non-nested forms... If your form has an element named edit[foo][bar] then the regex wont match and the element will be ignored and left untouched. The regex could be tweaked to allow for this - however the naming could be difficult and it would be much easier to just code your form to use a non-tree structure (ie flat).

I hope this helps someone!

Writing forms in tables

When you want your form elements to be presented in tables (as, more generally, whenever you want to customize the presentation of your form), you follow a two-step process. First, you generate the appropriate form elements. Then, when it comes time to present the form, you pass it through a theme.

Good examples of how to do this are in the issue.inc file in the Project module.

The simplest is used to present the issue filtering interface, seen at the top of the page http://drupal.org/project/issues. Note that the form elements here are enclosed in a table, so that they appear beside each other in a row.

To accomplish this, the elements are first generated in the function project_issue_query_result():

<?php
// Make quick search form:
$form['projects'] = array(
 
'#type'=> 'select',
 
'#title' => t('Project'),
 
'#default_value' => $query->projects,
 
'#options' => $projects,
);
// etc.
?>

A drupal_get_form() call generates the form output:

<?php
$group
.= drupal_get_form('project_issue_query_result_quick_search', $form);
?>

Normally this would return a standard-formatted form, but in this case the module includes a theme function to customize the output of this form: theme_project_issue_query_result_quick_search().

The theme function simply takes the form elements and generates an array of cells, then outputs the result as a table:

<?php
$rows
[] = array(
 
form_render($form['projects']),
 
// etc.
);
// ...
$output = theme('table', array(), $rows);

// Render the remainder of the form.
// This is crucial for proper function.
$output .= form_render($form);
return
$output;
?>

Look at other forms in issue.inc for more complex examples, e.g., project_issue_admin_states_page(), which uses nested arrays to generate multi-line tables with one row per record.

Tip: If your theme function requires extra data, include it in the forms array:

<?php
$form
['data'] = array();
$form['data']['#message'] = t('A message...');
?>

Schema API

Introduced in Drupal 6, the Schema API allows modules to declare their database tables in a structured array (similar to the Form API) and provides API functions for creating, dropping, and changing tables, columns, keys, and indexes.

Database schemas and their abstractions are a large topic in computer science and software development. Drupal's Schema API 1 is a fairly simple approach. Even so, it provides several benefits:

  • No more separate CREATE TABLE or ALTER TABLE statements for each database. Module developers only need to create a schema structure and/or use the Schema API functions, and Drupal takes care of the rest. This makes writing install and update functions much simpler.
  • Since the API makes it equally easy for module developers to support all databases, Drupal's support for PostgresQL will improve substantially. Also, as new database engines such as Oracle, SQLite, or Microsoft SQL Server are supported, modules using the Schema API will automatically work with them.
  • Several advanced capabilities, such as incremental database updates, a simple and consistent CRUD API, form scaffolding, simpler CCK and Views, schema and data validation, become much easier to implement in future enhancements.

A sample schema structure

As an example, here is an excerpt of the schema definition for Drupal's 'node' table:

<?php
$schema
['node'] = array(
 
'fields' => array(
   
'nid'      => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
   
'vid'      => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
   
'type'     => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''),
   
'title'    => array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''),
  ),
 
'primary key' => array('nid'),
 
'unique keys' => array(
   
'vid'     => array('vid')
  ),
 
'indexes' => array(
   
'nid'                 => array('nid'),
   
'node_title_type'     => array('title', array('type', 4)),
  ),
);
?>

In this excerpt, the table 'node' has four fields (table columns) named 'nid', 'vid', 'type', and 'title'. Each field specifies its type ('serial', 'int', or 'varchar' in this example) and some additional optional parameters.

The table's primary key is the single field 'nid'. There is one unique key named 'vid' on field 'vid' and two indexes, one named 'nid' on field 'nid' and one named 'node_title_type' on the field 'title' and the first four bytes of the field 'type'.

A complete Schema API reference is also available.

Creating tables: hook_schema, .schema and .install files

For the Schema API to manage a module's tables, the module must have a .schema file that implements hook_schema(). For example, mymodule's mymodule.schema file might contain:

<?php
function mymodule_schema() {
 
$schema['mytable1'] = array(
    
// specification for mytable1
 
);
 
$schema['mytable2'] = array(
    
// specification for mytable2
 
);
  return
$schema;
}
?>

Once mymodule_schema() is written, mymodule.install becomes much simpler:

<?php
function mymodule_install() {
 
// Create my tables.
 
drupal_install_schema('mymodule');
}

/**
* Implementation of hook_uninstall().
*/
function mymodule_uninstall() {
 
// Drop my tables.
 
drupal_uninstall_schema('mymodule');
}
?>

Updating your schema for new versions works just as it has since Drupal 4.7, using a hook_update_n() function. Suppose you add a new column called 'newcol' to mytable1. First, be sure to update your schema structure in mymodule_schema() so that newly created tables get the new column. Then, add an update function to mymodule.install:

<?php
function mymodule_update_1() {
 
$ret = array();
 
db_add_field($ret, 'mytable1', 'newcol', array('type' => 'int', 'not null' => TRUE));
  return
$ret;
?>

Updating tables #1: hook_update_N() functions

Updating your database tables for new versions works just as it has since Drupal 4.7, using a hook_update_N() function. Suppose that mymodule adds a new column called 'newcol' to mytable1. Prior to Schema API, you would:

  1. Add newcol to the CREATE TABLE statements in mymodule_install().
  2. Create a mymodule_update_N() function to add newcol to existing mytable1 tables with ALTER TABLE statements.

Using Schema API, you perform the same two steps:

  1. Add newcol to the table definition array in mymodule_schema() in mymodule.schema.
  2. Create a mymodule_update_N() function to add newcol to existing mytable1 tables with the Schema API function db_add_field():
    <?php
    function mymodule_update_1() {
     
    $ret = array();
     
    db_add_field($ret, 'mytable1', 'newcol', array('type' => 'int', 'not null' => TRUE));
      return
    $ret;
    ?>

Similarly, suppose that mymodule now needs a completely new table called mytable2. You perform the same two steps:

  1. Add the new table to mymodule_schema() in mymodule.schema.
  2. Create a mymodule_update_N() function to create mytable 2 with the Schema API function db_create_table():
    <?php
    function mymodule_update_2() {
     
    $schema['mytable2'] = array(
        
    // table definition array goes here
     
    );
     
    $ret = array();
     
    db_create_table($ret, 'mytable2', $schema['mytable2']);
      return
    $ret;
    }
    ?>

Important note: You may be tempted to pass a table definition from your own hook_schema function directly to db_create_table(). . Please read why you cannot use hook_schema from within hook_update_N().

Updating tables #2: Don't use hook_schema!

When writing hook_update_N() functions to, say, create a new table, it seems natural and obvious to use the module's hook_schema() function to access the current definition of the table in order to avoid duplicating the table definition in the hook_update_N() function. However, you cannot safely use hook_schema() from within a hook_update_N() function!

Consider the following scenario: You create module M. Initially, it has no tables, so its schema version 0 is empty. For update 1, you define a new table T. This means that you create the file M.schema and, in it, create the function M_schema() which defines the table T:

<?php
function M_schema() {
 
$schema['T'] = array(
   
'fields' => array(
     
'a' => array('type' => 'int'),
     
'b' => array('type' => 'int'),
  ));
  return
$schema;
}
?>

You also write an update function to bring module M from version 0 to version 1 by creating table T:

<?php
function M_update_1() {
 
$schema = drupal_get_schema('M');
 
$ret = array();
 
db_create_table($ret, 'T', $schema['T']);  // DON'T DO THIS!
 
return $ret;
}
?>

For update 2, M no longer needs the field T.b. So, you update M_schema() to reflect the current schema and add an update function to bring version 1 to version 2:

<?php
function M_schema() {
 
$schema['T'] = array(
   
'fields' => array(
     
'a' => array('type' => 'int'),
     
// NOTE: field 'b' has been removed
 
));
  return
$schema;
}

function
M_update_1() {
 
$schema = drupal_get_schema('M');
 
$ret = array();
 
db_create_table($ret, 'T', $schema['T']);  // DON'T DO THIS
 
return $ret;
}

function
M_update_2() {
 
$ret = array();
 
db_drop_field($ret, 'T', 'b');
  return
$ret;
}
?>

Everything looks fine, but it isn't. The problem is that M_update_2() cannot assume the field T.b exists. Dropping it can result in a failed SQL query. To understand why, consider two case histories:

  1. User 1 installs M at version 0, upgrades to version 1 when available, and upgrades to version 2 when available. This works fine.
  2. User 2 installs M at version 0, does not upgrade to version 1 when available, but does upgrade to version 2 when available. When user 2 upgrades to version 2 and runs update.php, M_update_1() and M_update_2() both run but M_schema() already contains the version 2 structure. When M_update_1() calls db_create_table($ret, $schema['T']), field T.b is not created. Therefore, when M_update_2() tries to drop it, an error occurs.

The crux of the problem is that hook_schema() always defines the current schema for the module. However, hook_update_N() functions have to assume that the database matches the schema as it was when the hook_update_N() function was written.

The solution is simple. Do not refer to your own module's hook_schema() from within an update function. Instead, make everything explicit. In the above example, M_update_1() has to specify the table structure itself, like this:

<?php
function M_update_1() {
 
$schema['T'] = array(
   
'fields' => array(
     
'a' => array('type' => 'int'),
     
'b' => array('type' => 'int),
  ));

  $ret = array();
  db_create_table($ret, '
T', $schema['T']);  // THIS IS SAFE BECAUSE $schema IS EXPLICIT
  return $ret;
}
?>

This way, the sequence of update functions always know exactly what they are doing. Sure, it looks redundant to include the definition of table T in both M_schema() and M_update_1() but that is temporary. In the future when the definition of table T has radically changed, M_update_1() will still contain its original definition and look nothing at all like the definition in M_schema().

Schema API Reference

Placeholder.

Schema data structure

A Drupal schema definition is an array structure representing one or more tables and their related keys and indexes. A schema is defined by hook_schema(), which usually lives in a modulename.schema file. hook_schema() should return an array mapping 'tablename' => array(table definition) for each table that the module defines. The following keys in the table definition are processed during table creation:

  • 'fields': An array mapping 'fieldname' => array(field definition) that describes the table's database columns. The specification is also an array. The following specification parameters are defined:
    • 'type': The generic datatype: 'varchar', 'int', 'serial', 'float', 'numeric', 'text', 'blob' or 'datetime'. The types map to the underlying database engine specific datatypes. Use 'serial' for auto incrementing fields.
    • 'size': The data size: 'tiny', 'small', 'medium', 'normal', 'big'. This is a hint about the largest value the field will store and determines which of the database engine specific datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT). 'normal', the default, selects the base type (e.g. on MySQL, INT, VARCHAR, BLOB, etc.).

      TO DO: Add a page defining all type:size maps for each supported database.

    • 'not null': If true, no NULL values will be allowed in this database column. Defaults to false.
    • 'default': The field's default value. The PHP type of the value matters: '', '0', and 0 are all different. If you specify '0' as the default value for a type 'int' field it will not work because '0' is a string containing the character "zero", not an integer.

      Note that type 'text' and 'blob' fields cannot have default values.

    • 'length': The maximal length of a type 'varchar' or 'text' field. Ignored for other field types.
    • 'unsigned': A boolean indicating whether a type 'int', 'float' and 'numeric' only is signed or unsigned. Defaults to FALSE. Ignored for other field types.
    • 'precision', 'scale': For type 'numeric' fields, indicates the precision (total number of significant digits) and scale (decimal digits right of the decimal point). Both values are mandatory. Ignored for other field types.

    All parameters apart from 'type' are optional except that type 'numeric' columns must specify 'precision' and 'scale'.

  • 'primary key': An array of one or more key column specifers (see below) that form the primary key.
  • 'unique key': An associative array of unique keys ('keyname' => specification). Each specification is an array of one or more key column specifiers (see below) that form a unique key on the table.
  • 'indexes': An associative array of indexes ('indexame' => specification). Each specification is an array of one or more key column specifiers (see below) that form an index on the table.
Schema API functions

The Schema API defines a number of functions for manipulating the database schema. These functions all operate directly on the database at the time they are called. They are generally most useful in writing hook_update_N() functions which need to incrementally update a database from one schema to another.

NOTE: The following links point to the API documentation for Drupal HEAD. When Drupal 6 documentatin is added to api.drupal.org, these links should be updated.

XML-RPC API

See http://drupal.org/node/28913

HOWTO: Connect Flash to Drupal via XML-RPC

This example assumes you're using Flash MX 2004.

The purpose of this document is to explain how to interface with Drupal via Flash, using XML-RPC.

Flash Setup
  1. Create an empty folder on your computer entitled 'example'. This folder will house the working Flash and library files needed to connect to the Drupal XML-RPC interface.
  2. Visit: http://sourceforge.net/projects/xmlrpcflash and download the "XML-RPC Client for ActionScript 2" (xmlrpcflash_0.9.3.zip). The current version is 0.9.3.
  3. Uncompress xmlrpcflash_0.9.3.zip. This will result in a folder entitled 'com'. Move the 'com' folder to your 'example' folder. The 'com' folder provides the XML-RPC libraries which Flash will use to communicate with the Drupal XML-RPC interface.
  4. Create a new Flash file named 'test.fla' and save it in the 'example' folder.
Flash

The "XML-RPC Client for Actionscript" isn't a terribly complicated beast to tame. To get started, here's a very simple example. Place the following ActionScript code on the first frame of your timeline. Remember to alter the 'url' accordingly.

import com.mattism.http.xmlrpc.Connection;
import com.mattism.http.xmlrpc.ConnectionImpl;
onLoadListing = function (r:Array) {
for (var i = 0; i<r.length; i++) {
trace('method: '+r[i]);
}
};
// the complete url/path to your xmlrpc interface.
var url:String = "http://example.com/xmlrpc.php";
var c:Connection = new ConnectionImpl();
c.setUrl(url);
c.onLoad = onLoadListing;
c.call('system.listMethods');

The end result is a simple list of public methods, returned for posterity sake. What you do with the information is up to you and outside the scope of this document.

Drupal

If you choose to develop a custom module which utilitizes hook_xmlrpc, consult the 'blogapi' module for some great examples.

Services and amfphp modules

Services and amfphp modules do this as well.

Your own api.drupal.org site

I decided to spend the evening figuring out how to make my own copy of api.drupal.org so that if it should ever become unavailable, I wouldn't be stuck. Hopefully this will help other people as well. :)

Step 1: Gathering the files

You'll need two things:

  1. A copy of the API module; found in the contributions repository, under /modules/api/
  2. A copy of the development documentation; found in the contributions repository, under /docs/developer/. i suggest checking this out from CVS.

Step 2: Installation

  1. Install the api modules as usual.
  2. Enable the API module, by going to administer > modules, checking the Enable box next to the api module, and finally clicking Save Configuration. This will give you two additional menu items: API reference (at the very top) and administer > API reference.

Step 3: Indexing

  1. Go to administer > API reference.
  2. Under branches to index, enter a Short name, Long name, and Directory for your Drupal installation. For example, my Drupal installation is from HEAD, so I entered:
  3. Short name: HEAD
    Long name: Drupal HEAD

    Directory: /absolute/path/to/your/drupal/installation (in my case, this was /home/username/drupal) -- Make sure to leave off the trailing slash!

    Click Save changes when finished.

  4. Repeat for any other Drupal branches you might have installed. In my case, I only had one, so I left it as-is.
  5. Next, click the Index PHP manual pages button. This will parse PHP-specific function names and so on so they can be referenced in the code samples.
  6. NOTE: My host has disabled remote use of file_get_contents() for security reasons, so I had to replace line 13 in parser.inc with cURL function calls:

    //$source = file_get_contents($location);
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, $location);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
      $source = curl_exec($ch);
      curl_close($ch);
  7. Click Reindex to tag all files for reindexing. A message will appear at the top notifying you that this has been done, and informing you that the index will be rebuilt during the next few runs of cron.php. Waiting for cron.php will take way too long, though (it only indexes 10 files at a time, and cron.php runs every 5 minutes). Sooo...

Step 4: Finishing touches

  1. Instead, open the link to cron.php and reload the page numerous times, until you quit seeing messages like Parsing "/some/path/somefile.ext".... For me, this was after about 10 or so refreshes, but it will probably be much more if you have a full-featured Drupal installation.
  2. Finally, and I have no idea why this is, but it seems you have to go back to administer > modules and Save Configuration again. After that, clicking on API reference up at the top will supply a sub-menu containing whatever long name(s) you entered in the previous steps, which will then take you to the documentation for your installation of Drupal (without this step, API reference is just a blank page).

And voila!

The one thing I couldn't get working were the example modules (e.g. "How to define blocks"). They're in the /module/developer/examples/ folder, but for some reason don't match up with where the API module thinks they should be. Any hints anyone might have would be greatly appreciated. :)

Notes from chx. For me, example modules worked fine. But I needed to raise PHP memory limit significantly.

HOWTO: Benchmark Drupal code

Note: this page is mostly a result of me Googling around and jotting down some notes as I find them; I need someone who actually knows about this stuff to look it over and ensure I actually know wtf I'm talking about. ;)

Introduction

When attempting to create a large change in Drupal core, or even during the course of developing Drupal sites for clients, often times you are asked to provide benchmark results to ensure that your change doesn't negatively impact performance (or to see how much it improves performance). This document will talk about the various strategies involved in benchmarking Drupal code.

Tools of the Trade

This document will discuss in-depth the ab tool, as it is most commonly used in the community due to its being a free, open source performance testing solution that comes with Apache.

In addition, you'll also need all the "normal" developer tools, such as:

Setting up a performance testing environment

Benchmarking is best done on your local computer so you can eliminate the possibility of network traffic and other things impairing the results.

Begin by changing into your local web directory:

cd /Applications/MAMP/htdocs

First, perform a CVS checkout of Drupal:

cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d head drupal

This will retrieve a copy of the current HEAD version of Drupal into a folder called "head." You may also want a copy of the current "stable" version of Drupal for benchmarking purposes:

cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d drupal-4-7 -r DRUPAL-4-7 drupal

Install Drupal in the normal way and create the first user.

Following Dries's recommendations, we're going to setup an environment with:

Download a copy of Devel module for its handy content generation scripts. Note that since HEAD can be very volatile, always check the issue queue for updated patches and such.

cd head
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -d modules/devel contributions/modules/devel
cp modules/devel/generate/*.* .

Enable a few different node type modules (blog, page, story, forum, etc.)

You cannot yet run devel module's generate scripts from the command line, so here's what you'll need to change in each file. After making the specified changes, execute the script in your browser by going to http://localhost/head/generate-XXX.php. It's important that these be run in order, or you'll get weird results.

Note: you should disable MySQL query caching. To do this edit the MySQL configuration file my.cnf and add/replace the appropriate line:
query_cache_type = 0
On *nix systems, this file is located in /etc or /etc/mysql. After changing the setting, restart the MySQL server (I use webmin to do this...).

Benchmarking code

Begin by performing a

cvs update -dP

in order to ensure your source tree is up-to-date.

Then, execute the following from the command-line before applying your new patch. This will establish a baseline from which to judge performance improvements/decreases.

ab2 -c10 -n500 http://localhost/head/index.php

In this command, -c specifies the number of concurrent requests and -n specifies the total number of page requests.

This will produce output similar to:

This is ApacheBench, Version 2.0.41-dev <$Revision: 1.121.2.12 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright
(c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
Benchmarking
localhost (be patient).....done


Server Software:        Apache/2.0.55
Server Hostname:        localhost
Server Port:            80

Document Path:          /head/index.php
Document Length:        16668 bytes

Concurrency Level:      1
Time taken for tests:   36.695602 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      1718500 bytes
HTML transferred:       1666800 bytes
Requests per second:    2.73 [#/sec] (mean)
Time per request:       366.956 [ms] (mean)
Time per request:       366.956 [ms] (mean, across all concurrent requests)
Transfer rate:          45.73 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   316  366  29.7    362     608
Waiting:      298  346  29.3    342     588
Total:        316  366  29.7    362     608

Percentage of the requests served within a certain time (ms)
  50%    362
  66%    367
  75%    370
  80%    373
  90%    384
  95%    390
  98%    462
  99%    608
100%    608 (longest request)

// todo: explanation of results

Now, apply your patch:

...and re-execute the command above, and compare the results:

This is ApacheBench, Version 2.0.41-dev <$Revision: 1.121.2.12 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright
(c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
Benchmarking
localhost (be patient).....done


Server Software:        Apache/2.0.55
Server Hostname:        localhost
Server Port:            80

Document Path:          /head/index.php
Document Length:        17100 bytes

Concurrency Level:      1
Time taken for tests:   37.903596 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      1761700 bytes
HTML transferred:       1710000 bytes
Requests per second:    2.64 [#/sec] (mean)
Time per request:       379.036 [ms] (mean)
Time per request:       379.036 [ms] (mean, across all concurrent requests)
Transfer rate:          45.38 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   318  378  35.5    376     690
Waiting:      301  358  35.4    355     672
Total:        318  378  35.5    376     690

Percentage of the requests served within a certain time (ms)
  50%    376
  66%    381
  75%    385
  80%    388
  90%    396
  95%    401
  98%    436
  99%    690
100%    690 (longest request)

interpreting results

Some things to watch out for ... // todo

Drupal benchmark results

List of modules seeking help or maintainers

This list is not created automatically, so it is probably not all-inclusive and probably not up-to-date. If you notice anything that needs to be changed, feel free to do so. Please add your own modules, and remove them once they are taken over by someone. Contact module maintainers on their personal contact form, if you are interested in helping out / taking over development of some module.

A better, automated solution is discussed in this issue thread.

Setting up a development environment

Discovering the best tools, and learning how to use them effectively, takes time and effort. While that effort is duly rewarded, spending weeks looking for and trying to configure software can be frustrating. This chapter aims to help by providing a central resource where Drupal developers can share what they have learnt so far, list a breakdown of favorite software and share notes on setting up and using these tools with Drupal in mind.

Please help by adding to this resource where you can. Thank you.

Development tools

Below you will find a list of useful tools for use when developing Drupal sites.

Browsers

Firefox (Free. Open Source. Linux, Mac, Windows and others)

-- Web Developer Extension -- Adds a toolbar and menu to the browser with various web developer tools. For example you can disable form fields; reveal hidden form elements; edit CSS in real time; and clear the browser cache and session cookies.

-- Firebug -- More tools for the web developer. Program CSS, HTML and JavaScript in real time; easily locate divs; examine a breakdown of the time components of your page take to download; discover errors in Javascript, CSS and XML; and explore the DOM.

-- How to setup a Firefox quick search for the Drupal API

Microsoft Internet Explorer (Free. Proprietary. Windows only)

-- Internet Explorer Developer Toolbar

Safari (Free. Proprietary. Mac only)

Opera (Free. Proprietary. Linux, Mac, Windows and others)

Editors

Emacs and vi are historically the two most popular editors. Which is best is a matter of some debate and so before deciding which to try you may like to read this Wikipedia article. VIM is based on vi and is much more feature rich.

VIM (Free. Open source. Linux, Mac, Windows and others) -- Vim is an advanced text editor based on vi, but more powerful. The learning curve is reasonably steep and the interface can be confusing at first, but this is soon rewarded with greater editing efficiency. (Details about configuring VIM for Drupal)

Emacs -- (Free. Open source. Runs on Linux, Mac, Windows and others) (Details about configuring for Drupal)

Other editors:

jEdit (Free. Open source. Runs on Linux, Mac, Windows and others. Java based)

Textmate ($$$. Proprietary. Mac only) -- A Drupal specific bundle is offered by Steven Wittens and another by Konstantin Käfer.

BBEdit ($$$. Proprietary. Mac only)

Dreamweaver ($$$. Proprietary. Mac and Windows) -- A WYSIWYG HTML editor.

Integrated development environments

Eclipse (Free. Open source. Linux, Mac, Windows and others. Java based) -- Requires plugins for PHP such as Zend/PDT or PHPEclipse. (Details for installing and configuring and setting up for Drupal coding standards.)

Komodo ($$$. Proprietary. Linux, Mac and Windows)

Zend Studio ($$$. Proprietary. Linux, Mac and others. Java based)

Other Tools

Filezilla (Free. Open Source. Windows only) -- FTP client

7 Zip (Free. Open Source. Windows only) -- Extract archived and compacted files (such as tarballs).

Cygwin (Free. Open Source. Windows only) -- Allows Windows users to use a Unix like shell. This is particularly useful for interfacing with CVS and patching files... read more

Using Cygwin

On a Windows box, Cygwin is a useful command line utility that provides a Unix like shell prompt. This is particularly useful for patching files and interfacing with CVS. The alternative is to use GUI based software, but using the shell will make it easier for you to follow handbook instructions, understand what is happening 'under-the-hood' and work under different operating systems.

Setting up Cygwin

Cygwin offers many packages that enhance its functionality. You won't want all of these since you'd be clogging up your hard drive, so this guide will simply suggest those that are useful for working with Drupal.

If you already have Cygwin on your computer, but don't have all the packages you need, you will need to run the setup file again -- so follow along with these instructions.

  • Go to www.cygwin.com and grab the setup.exe file.
  • Run setup.exe and proceed through the screens.
    1. At the title screen just click 'Next'
    2. Choose 'Install from Internet'
    3. Root directory is usually best kept as default as are the other defaults
    4. Choose a location for downloading the installation files into
    5. Select your Internet connection settings (probably the default 'Direct connection')
    6. Choose a download mirror
    7. Select packages
      • 'cvs' is in the Devel section
      • 'patchutils' is also in the Devel section
    8. Then click install

Cygwin should now be installed on your system. If you want to add further packages later, you will need to run setup.exe again; so you may want to keep hold of the setup file.

Getting to know Cygwin

If you've never played with the Unix shell you will want to spend a few minutes getting aquainted with some basic operations. The Lifehacker website has a nice introduction to this (parts 1, 2 and 3).

Checkout Drupal files from CVS

Checking out from CVS with Cygwin requires the 'cvs' package (see 'Setting up Cygwin' above)

You can check to see whether you have CVS installed in Cygwin by typing cvs. If you get an error, it probably hasn't installed properly. If all is well you should get some basic CVS information.

Now simply follow the instructions from the Drupal CVS repositories section of the handbook.

Patch Files

You will need the 'patchutils' package in order to apply patches using Cygwin (see 'Setting up Cygwin' above).

To patch files simply follow the instructions in the patch section of the handbook.

Usability research

This is a place to post original usability research for Drupal. There are many ways to help with usability.

Administrators Survey

This survey was conducted September to October 2006.

Methodology

Start by Interviewing Administrators

Interviews with administrators give us a chances to look for patterns in problems, and discover the appropriate language that administrators use to describe their work. We conducted 10 interviews selected from volunteers from the mailing list, referrals, and administrators identified in the #drupal-support IRC channel.

The interviews focused on identifying four types of information: administrators self description, situation, goals, and tasks.

Designing the Survey

The survey was designed with two goals in mind. The first was to conduct audience research about Drupal administrators which would help developers better meet their goals. The second goals was to allow the extended Drupal community participate in improvement of the Drupal user experience by contributing to this work. We allowed the administrators to self identify as contributors to the effort to improve the drupal user experience. We hoped that by including self identifying responses, we would get more serious responses to the survey.

Collecting Results and Analysis
Biases and Caveats
Respondent self-selection

The survey was advertised with a post on the front page of Drupal.org and an ad which appeared periodically appeared above the fold on Drupal.org's home page. This highly visible location does come with biases. The respondents are a subset of those who visited the Drupal.org front page. Filling out a survey does have some cost, so the respondents likely have some level of interest in improving Drupal and commitment to the platform.

Reponse rate

Grpah showing what % of users filled out each question.
The first page was completed by almost all respondents. The second page was missed by approximately 12% of respondents. The final question asked for personal identification, only 40.1% of administrators chose to give this optional information. Question 13, "What are some important administration tasks that did not fit into the categories above for you?" lost almost 10% of respondents and was noted to be confusing in a comment thread about the survey.

Answer prompt ordering

All answer prompts were always presented in the same order for everyone. The first answer choice may have inflated response rate on some questions because of its position.

FIlling out multiple surveys

Survey Monkey does record IP addresses; we did not find any irregularities and do not believe anyone tried to unfairly influence the results.

Overall results

  1. How would you describe yourself as a Drupal administrator? (pick the best one)
  2. How many Drupal sites do you administer? (select one)
  3. Which types of Drupal websites do you administer? (select all that apply)
  4. How frequently do you administer your Drupal site? (pick the best one)
  5. How long do you administer your Drupal site in a single sitting approximately? (pick the best one)
  6. How does Drupal help you accomplish your goals as a web site administrator? (select all that apply)
  7. Why do you use Drupal? (select all that apply)
  8. How does Drupal help your users? (select all that apply)
  9. What are your most common Drupal administration tasks?
  10. What are your least common Drupal tasks? (select all that apply)
  11. When you administer your site you find it easy to: 5 options, Not Easy - Very Easy
  12. When you administer your site you find it hard to: 5 options, Not Hard - Very Hard
  13. What are some important administration tasks that did not fit into the categories above for you? (select all that apply)
  14. Optional If you complete all the questions in this survey and would like to be recognized as a contributor to improving Drupal user experience please indicate your Drupal.org username: (answers will not be anonymized)
    List of responses.

Appendices

The source data for the survey analysis and design.

Summary of Interview Responses

This is a summary of 5 interviews conducted with Drupal administrators. The results of these interviews will lead to a survey to be posted on Drupal.org and will run for 45 days.

Interview
 Question
Drupal task
How would you describe
yourself as a Drupal administrator?
Beginner/Intermediate

works on small sites

Beginner

very inexperienced but a fast learner

Newbie.

Wet-behind-the-ears, enquiring

certainly not an expert



Know well how works in the inside, which makes admin tasks easier to
grok

Make way through confusing tasks with the handbook



Experienced

Comfortable

Second nature

Fluent

Can rapidly configure

Quite experienced



Work with code side of things

Understand what's going on behind the scenes
How frequently do you
administer you your Drupal site?
Daily

At least several times a week, if including recurring admin tasks
(viewing logs, etc...).

maybe once a fortnight - less since the administrator switched to
Drupal, actually.

not frequently

an average of twice a week.

I administer my site frequently if not daily.

once a month.
How long do you administer
your Drupal site in a single sitting approximately?
30 minutes to an hour

about 30 minutes to an hour

could be 30 minutes

2-3 hours ( A few hours)

4-5 hours

somedays I could sit for half a day,

All day long


How does Drupal help you
accomplish your goals as a web site administrator
Add features rapidly

Speed of meeting customers functional requirements

Lots of functionality that you can install

Speeds up the process

Spend more time on custom theming and features



See what content users have posted

Collaboration on content creation

Users can create content

it lets me publish news stories and re-edit them

It makes managing content fairly simple.

I document a lot of processes

Web based work is faster



Teach customers to use it

Know customers will be happy with it

Reuse same codebase

Pass duties on to successors once built

Lower learning curve

Creation content is in one easy to understand place

Easy to admin, web interface. Very little technical knowledge required.



Monitor logs/stats

Monitor and fix problems

Logging



Exciting dynamics of community

fun to install and play around with modules



Module capabilities

configure modules

Easier to modify clean code

gives me a set of tools allowing me to create the kind of web sites I
want fairly easily (integration of blogs, forums, menus , etc.)

Create tools to help myself and my co-workers out so the easy to build
modules and the resource of modules is extremely helpful.




Why do you use Drupal? Functionality

Implements features that wouldn't have been possible

How the pieces are come together, and accomplish a larger task

Node system



Deploy a new website in a
few minutes with multi-site

Using a mature CMS is a no-brainer, today, especially if you want to
createinteractives/community web sites.

like how bareboned it is and i can build a site pretty streamlined by
using the base plus my desire of modules

nice looking presentation and doesn't require any effort on my part to
maintain and manage. Ease of implementation.

Don't have to roll your own as much

Code let's you do what you need 



Community is impressive and active on IRC, Forums, Groups site

Drupal people are helpful and smart

Evangelization.  It's good to develop for.

I use drupal cause I am fascinated by it. I opened it up and was
floored by its architecture.

Meets requirements not just technical solutions

Community is working in a similar space



Users can create a community navigation system

Community catetegorization helps users find what they need

Great for solving problems

It lets visitors become contributors with their own account - that's a
good idea for those who want people to participate but not anonymously



Because the main site administrator chose to roll it out.
How does Drupal help your
users?
Implement features fast

Deliver functionality to users really quickly

Add audio and playlists, easy integration of Flickr

Can restrict and unrestrict access to modules and content quickly



Benefit from the larger Drupal community

Integration with CRM is big for political, membership, non-profits

Get engaged on a higher level, membership, motivate, and mobilize a
community.

Provide community growth



lets them respond to stories and create their own blogs

making it easy for me to do my job and configure an easy to navigate
site.

Web based content update....Low technical requirement to add content.



Customers demand it, it's beyond technical audiences

Cost savings
What some common Drupal
administration tasks?
Check logs, look for
spikes, errors, strange stuff

Look for signs of trouble

Monitor watchdog

check logs.

check user activity





Give users permissions

Add users

user administration



set-up modules and configure them

test and troubleshoot

Add block to highlight new features

Ban users



Moderate comments

Delete spam

Dealing with spam :)

(trackback, comments, forum posts...)



Update code

Test code

Lot of module installing and testing, configuring

Install or update modules

Process of using modules, find, install, test, review issues, contact
author, learn it's not working

Enabling / disabling modules. Editing menus. Configuring modules.

improve menus,

Track fixes for modules

Patch Drupal

testing/providing patches
to fix problems or missing features ...



Deal with user feedback on tests and make fixes to it

reply where needed,



Modify permissions





Modify themes

Little bit of theming

manage themes.

spend some time improving the themes,

add theme template file to customize module output



Learning about Drupal through Drupal.org, Planet

Understand vocabulary

planning for features I would
like to introduce, checking modules out



Let it run itself

create content..  customize/create modules.. backups..

create content > story, enabling full html on those posts

create and organize content to introduce the site...




What are some infrequent
Drupal tasks?
code new features

add new modules.



ban users,

remove spam posts,

moderate comments

delete unpublished content

monitoring comments.



modify existing content,

making posts sticky or not sticky

re-editing a headline



modify navigation system as well



work directly on database

Non-GUI administrative tasks



send newsletters, he copy and paste from a node and copied to form.

Send a test. and then test it.

tasks limited by technical capabilities



Planning content positioning

Blocks are pretty much a one time
setup..

Build view of the content of the section



Design and theme main page for a section



Managing users.

i dont build a lot of content types yet..

I manage logs but not really since i expect to screw up..



change theme.




When you administer your
site you find it easy to?
install working modules

install

add new functions via simple modules



have automated tasks with cron



editing user information



post content

I love the ability to version content..

edit nodes



change themes

theming: it is easy with phptemplate to change the layout of the
template, and add some php where needed.



find it easy to create sections and promotional areas of the site



Interface is now intuitive

general configuration settings options  are familiar to me,
ie. titling
your site, setting date output options; this is reasonably easy to grasp



Creating menus, blocks, promotting nodes, etc... are frequent tasks
that are easy to perform...



interface is not intuitive
When you administer your
site you find it hard to?
Discover where
administration settings are

discover how to moderate feed items

community sites require half a dozen stops to make sure there's nothing
I need to fix or take care of.



concepts need to be expressed more simply

images could be used in the handbook documentation, and examples

understand the concepts, read the glossary without pictures



upgrading automatically

Have to download modules separately

learn what modules are available for upgrade on a site

add new modules.... they don't always work well together.



moderate aggregator feed items



Work with regions across themes



Create new content types

I never got the (content) diff to work but its cool to me

Import content



hard to work with large groups of anything.
nodes, comments, users, etc.

categorizing large amounts of content

Approving/Deleting/Moving/Categorizing things in groups



Filtering on content types is limited



awareness of incoming trackbacks, incoming comments, new nodes posted,
new users, error conditions with modules, etc



-structure the site



user permission. Drupal's permission are great and generally more
performant than that of other CMS, yet I find I'd like more fine-tuning.



allow users to edit one menu but not others, etc...

menus

editing the weight of menu items can be difficult bec ause you cannot
edit all the menus items at the same time (like you can adjust the
weight of all the blocks on the same page...)



Assigning permissions on admin/access can be a pain if you have many
modules, many roles with long names...

preview/submit option... and when you preview, you should just see it
all straight away, not have it repeated
What other important tasks
did we not cover?
Discovering the state of
the site from the logs versus
events

looking at categories of logs to see what's happening

Robust and detailed logging



How to connect module  X and module Y together

fix visual clashing of modules



create role for testers to try out

Trying functionality out with users and iterating improvements



work in the theme and the templates to make things 'make sense' in the
structure of the site.

make sections look 'distinct'



hiding certain links on the nodes

overriding module's browser



design site on paper or whiteboard first



User was having difficulty logging in and logging out.

Recreating a
user is a terrible things to do. Manage to delete a bunch of content.
Managing users how they related to content it's a bit of a mine field.



Ability to manage permissions for a community



Identify popular content



Better menu editing interface

bulk editing or cloning menus

adding/removing "menu" options



Settings are in obscure places

Browse and install modules via GUI

Drupal community(versions, upgrades) information in administration

Integrate with other systems like vbulletin



Administer users, moderate content directly on page

Session management is too long to be secure from public computers

need the moderation module to accept comments by anonymous users, that
I publish or delete after verification.

referrer spam, and registration spam...



admin is not asked to make a decision to authorize or not an account if
the email registered is bogus...

comment moderation



create a site that aggregates news from various sources, displays it in
various categories, allows registered users to respond to and rate
items, as well as post their own stories alongside



I mean the configuration of drupal itself. The stuff in settings.php



performance is something i am not worried about for now cause since
everything is internal.. but thats going to be a major task for me to
explore



enable/add a content type I don't currently have or use, grow bored
with the theme,
List of all respondents
Survey Questions
  1. How would you describe yourself as a Drupal administrator? (pick the best one)
    • New user
    • Non-technical user forced to become "accidental technologist"
    • Inexperienced with Drupal but learning fast, based on other website-building experience
    • Experienced administrator, comfortable with configuring site rapidly
    • Module developer who understands what's going on behind the scenes
    • Other (please specify)
  2. How many Drupal sites do you administer? (select one)
    • 1
    • 2-5
    • 6-11
    • 12-24
    • 25 or more
  3. Which types of Drupal websites do you administer? (select all that apply)
    • Personal Blog
    • Community site
    • Social Change site
    • Brochure site
    • News site
    • E-Commerce site
    • Other (please specify)
  4. How frequently do you administer your Drupal site? (pick the best one)
    • Monthly
    • Once every couple of weeks
    • Several times a week
    • Daily for 30-60 minutes of website management
    • Daily for several hours in site development and deployment
    • Other (please specify)
  5. How long do you administer your Drupal site in a single sitting approximately? (pick the best one)
    • Thirty minutes to an hour
    • Few hours
    • Half a day
    • All day long
    • Other (please specify)
  6. How does Drupal help you accomplish your goals as a web site administrator? (select all that apply)
    • Rapid deployment of features and ability to meet customer requirements
    • Web based content publishing is easy and allows for end user contributions
    • Easy-to-learn website that can be taught to users
    • Allows website developers to leverage previous experience when building new sites
    • Allows for monitoring and logging of the website
    • Module configuration, extensibility with new modules, and clean code make it easy to customize the website as you need it
    • Dynamic and exciting developer community allows for rapid and fun learning
    • Other (please specify)
  7. Why do you use Drupal? (select all that apply)
    • Features, extensibility, integration of modules, clean code allow you to customize your site
    • Speed of deployment and re-use of existing functionality
    • Community is helpful, has smart people, is very active, and is working on community solutions not just technical capabilities
    • Allows users to create content, categorize content, and add navigation to the site
    • Other (please specify)
  8. How does Drupal help your users? (select all that apply)
    • Gives them the features they want quickly
    • Allows users to create web based content such as forum posts, or blogs
    • Drupal community innovates and provides new and improved community-building tools
    • Cost effective, easy to convince non-technical decision makers
    • Other (please specify)
  9. What are your most common Drupal administration tasks?
    • Monitor site through reviewing logs, looking at user activity
    • Manage spam through comments, trackbacks, forum, and user registration
    • Configure modules
    • Update modules, install modules, test patches, track fixes for modules
    • Work on themes and edit theme templates to customize module output
    • Learn about Drupal capabilities and features, understand terminology, and plan improvements
    • Manage users accounts, change permissions
    • Respond to user feedback during testing and make changes
    • Create web pages through the creating content types
    • Other (please specify)
  10. What are your least common Drupal tasks? (select all that apply)
    • Add new features by adding modules or coding new features
    • Manage spam, delete content and comment, and banning users
    • Modify existing content such publishing to the front page, making content sticky, or changing titles
    • Layout content in positions with blocks, views, or designing a content section
    • Change site theme or theme a content section
    • Modify site navigation
    • Other (please specify)
  11. When you administer your site you find it easy to: 5 options, Not Easy - Very Easy
    • Add new features and install modules
    • Automate tasks using cron
    • Manage user information by editing user account permissions
    • Change themes or make changes to layout with theme templates
    • Post, edit, and version web pages and other content types
    • General settings such as changing titles, or date format
  12. When you administer your site you find it hard to: 5 options, Not Hard - Very Hard
    • Discover where to configure site settings, or where all the different places where a site needs to be configured are
    • Understanding Drupal specific concepts or understand technical language without pictures
    • Upgrade modules manually, theme/customize module output, determine module dependencies
    • Manage content specifically creating new content types, viewing differences in node versions, importing content, or seeing multiple previews of content on a single page
    • Manage large groups or users, nodes, menu items, comments, blocks
    • Get overview of new content and activities on site
    • Configure user permissions with increasing granularity
    • Manage menus such as changing menu block weights for all menus on a page, or ordering menu items within a menu
  13. What are some important administration tasks that did not fit into the categories above for you? (select all that apply)
    • Analyzing logs to understand the state of your site
    • Integrating modules, resolving incompatibilities, theming the output
    • Setting up a testing process including creating a test role, soliciting feedback from users, configuring corrections, and working with module development process to get bugs resolved
    • Work on the theme and templates to create structure and distinct looks to sections of the site
    • Hide output from modules such as node links or browsing links
    • Managing menus with the menu editing interface, bulk menu editing, and menu options
    • Manage performance of the site
    • Identify popular content
    • Aggregate content from other sources into a site
    • Other (please specify)
      10% 164
  14. Optional If you complete all the questions in this survey and would like to be recognized as a contributor to improving Drupal user experience please indicate your Drupal.org username: (answers will not be anonymized)

Portland UI-SIG cognitive walkthrough

The Portland Usability Special Interest Group did a cognitive walkthrough of Drupal 5.0 in January 2007. A group of usability practitioners tried doing usual first tasks of setting up a web site with Drupal.

We used screen sharing software and a conference call line. The following is the screencasts, about 1.5 hours total, notes for each section, and high-resolution screenshots of the pages we are talking about.

Introduction to Drupal

for usability practitioners

Watch 14 min 30 sec

Neil went over the basics of the Drupal project, focusing on how the project is developed and who our users are. The development of Drupal happens quite differently from the company-backed products which the usability-types might be familiar with. The ~492 person development team self-identifies by contributing and determines development goals without a formal structure.

Who uses Drupal is important to know when thinking about usability issues. We divided Drupal's users into four groups:

  • Developers
  • Webmasters
  • Site editors
  • Visitors

In all levels, except visitor, time commitment ranges from side-job for a small website with little time spent to full-time professional. One person may fulfill any number of roles. On many sites, one person does everything; on larger sites, tasks like development may be contracted out to a dedicated development shop.

Review tasks and installation

Watch 7 min 5 sec

The following tasks were to be completed and evaluated by the participants:

  1. A non-profit wants to set up a new site for a fundraising campaign. Give the site a name and customize the theme.
  2. Publish the mission statement and a news item of the upcoming fundraising drive.
  3. Add an image to the mission statement. Set up a new account for your colleague who will manage the content going forward.

While Frank went over these tasks, I used Drupal 5's new installer to set up a new site.

Creating the first account

Start of group review, the interesting part

Watch 12 min 20 sec

  • Fantastico is mentioned as a convenient way to install Drupal. I responded by summarizing "Fantastico De Luxe: an insecure recipe for disaster"
  • Creating the first account went smoothly, leading to the edit account screen.
  • The password being set for user 1 was surprising.
  • A dynamic password strength indicator would be a nice addition.
  • The status and signature controls do not have enough context. Are these for the site, all users, or the specific user account?
  • Deleting user 1 should not be offered.

Customizing the theme

Watch 22 min 35 sec

  • Administer doesn't seem like the best word.
  • Initial configuration tasks are not provided up-front, but are spread throughout the administration section.
  • On the 'Themes' administration page, enabled and default columns can be confusing. Nothing mnetions what enabled does.
  • The indication of someone being logged in isn't obvious.
  • Since users can choose their own theme, figuring out what each user will actually see becomes confusing.
  • The 'Operations' column heading is a confusing word.
  • 'Configure' is too generic and doesn't provide any context for what will be on the next page.
  • The top links behave like tabs, but are not visually treated as tabs.
  • The behavior of the lock controls on the color configuration is not obvious.
  • The 'The directory files does not exist' error message is vaguely written. What happened, why it happened, and how to correct it are not answered.
  • The error messages do not have enough contrast. It could be mistaken for a design element of the page.
  • Customizing the page elements and the color scheme are different tasks and might want to be on separate pages.
  • Placing 'Reset to defaults' next to 'Save configuration' are regressive and progressive actions. The buttons don't have much visual distinction.

Creating a page

Watch 25 min 35 sec

  • The "Content management >> Content" administration page doesn't link to the create content page.
  • The navigation menu can make it hard to figure out what is in each section since all you get is the top-level links.
  • The navigation menu has subtle indications for the current page and hierarchy.
  • Required field indication doesn't obviously indicate its meaning.
  • "Submit page" isn't the best title, "Create page" or "Create new page" might be better options.
  • The text area resizing should save the size. An additional "resize" label might be helpful.
  • The collapsed field set titles don't look clickable and can be mistaken for simple headings if there are other form elements directly below them.
  • Authored on control requires knowledge of date formatting. A clock control would not have this problem.
  • Authored on control could be either manually changing the created date or scheduling.
  • The defaults in collapsed field sets are not visible.
  • Previewing can make the page quite long. Tabbing between the preview and editing can be a good way to conserve space.
  • HTML ability isn't immediately obvious since it is hidden in the input format collapsed field set.
  • WYSIWYG editing would be a nice addition.
  • The upload module might want to be enabled by default.

Ending comments

Watch 7 min 54 sec

  • Mary: Labeling is the biggest concern. Many labels seem to be in developer-speak. Developers won't mind if things are in clear English.
  • Donair: More descriptions, such as flyout descriptions for collapsed field sets.
  • Sarah: More modules should be built in to make more things work off the shelf, but this can be a departure from the platform nature of Drupal.
  • Jeff: Wizards to make themes can help people get people started more quickly.

Who uses Drupal?

This is a project answer the recurring question, 'Who uses Drupal?' This should be used whenever we need to take a step back from a design problem and ask how the design will work for the user or a specific subset of users.

A 'user' is someone who is posting content or using more advanced features on a Drupal website. They often encounter standard Drupal user interfaces.

The goals are to determine:

  • Who uses Drupal?
  • What are their goals?

We have some idea of who uses Drupal:

  • Wants to build and/or add content to a website.
  • Has some website building experience.
  • Have widely varied amounts of effort to put into his or her site.

This is a start, but more research-backed details and dividing into user groups or personas would benefit the Drupal project.

The research will consist of a series of 10 interviews followed by a widely-distributed survey. The raw data will then be analyized to meet the goals of this project.

Interview script

Status: incomplete draft

  • Rephrase questions using phrases from the interviewee.
  • Let the interviewee talk.
  • Keep the goals of the questions in mind.
  • Ask 'why?' and other follow-up questions.
  1. Introduction
    • Hello, I am ...
    • Before we begin, is it okay to record this conversation?
    • We are interviewing Drupal users to help Drupal contributors better understand who uses Drupal.
    • Your anonymized answers will be available for Drupal contributors to review.
    • There are no right or wrong answers. Your opinion is what matters.
    • If you have more feedback, we can talk about it at the end.
  2. How long have you been building websites?
    Start thinking about website building experience, not just with Drupal.
  3. Tell me about your website building experience.
  4. What sites have you built or contributed to? What were your goals with the sites?
  5. What other tools have you used to build web sites? For building, editing, hosting, on the web or desktop. Did you like them? Do you still use them?
  6. Where do you go to learn more about website building?
  7. How long have you been using Drupal?
  8. How did you hear about Drupal?
  9. Why did you choose Drupal?
  10. Do you think Drupal was a good choice?
  11. How would you describe yourself as a Drupal user?
  12. How many Drupal sites do you use? What do you do on each?
  13. How long do you usually spend building a website? How much of that time is learning?
  14. Where do you go to learn more about Drupal?
  15. How customized or designed would you like your site?
  16. Is there anything else you would like to mention?
  17. Thanks!

Join forces with others

Too often new modules are contributed that do nothing new, only do it in a different way. We are then stuck with two modules that offer nearly similar functionality, but both do not do it well enough. This leads to confusion, clutter and a lot of inefficiency.

So please consider the following guidelines or ideas:

Respecting these guidelines will help you and the community get better. Only then will we be able to "stand on the shoulders of giants" as they say in Open Source Land. If you keep reinventing wheels, you will be stuck with lots of incompatible and half finished wheels. When you use someone else's existing wheel, and build a car on top of it, you can actually get somewhere.

Module developer's guide

Developer documentation can be found at http://api.drupal.org and in the remainder of the Drupal developer's guide below.

Introduction to Drupal modules

When developing Drupal it became clear that we wanted to have a system which is as modular as possible. A modular design will provide flexibility, adaptability, and continuity which in turn allows people to customize the site to their needs and likings.

A Drupal module is simply a file containing a set of routines written in PHP. When used, the module code executes entirely within the context of the site. Hence it can use all the functions and access all variables and structures of the main engine. In fact, a module is not any different from a regular PHP file: it is more of a notion that automatically leads to good design principles and a good development model. Modularity better suits the open-source development model, because otherwise you can't easily have people working in parallel without risk of interference.

The idea is to be able to run random code at given places in the engine. This random code should then be able to do whatever needed to enhance the functionality. The places where code can be executed are called "hooks" and are defined by a fixed interface.

In places where hooks are made available, the engine calls each module's exported functions. This is done by iterating through the modules directory where all modules must reside. Say your module is named foo (i.e. modules/foo.module) and if there was a hook called bar, the engine will call foo_bar() if this was exported by your module.

See also the overview of module hooks, which is generated from the Drupal source code.

Creating modules - tutorials

The following are tutorials to help in the creation of modules. They are divided according to the version of Drupal they relate to.

An important note- when developing a new module (or new theme), you must be sure that there is no module with the same name as any theme being used on the site because the function names may collide and your site may no longer function correctly.

Creating modules - a tutorial: Drupal 4.3.1

This tutorial describes how to create a module for Drupal-CVS
(i.e. Drupal version > 4.3.1). A module is a collection of functions that link into Drupal, providing
additional functionality to your Drupal installation. After reading this tutorial, you
will be able to create a basic block module and use it as a template for
more advanced modules and node modules.

This tutorial will not necessarily prepare you to write modules for
release into the wild. It does not cover caching, nor does it elaborate
on permissions or security issues. Use this tutorial as a starting
point, and review other modules and the [Drupal handbook] and [Coding
standards] for more information.

This tutorial assumes the following about you:

  • Basic PHP knowledge, including syntax and the concept of PHP objects
  • Basic understanding of database tables, fields, records and SQL statements
  • A working Drupal installation
  • Drupal administration access
  • Webserver access

This tutorial does not assume you have any knowledge about the inner
workings of a Drupal module. This tutorial will not help you write
modules for Drupal 4.3.1 or before.

Getting Started

To focus this tutorial, we'll start by creating a block module that
lists links to content such as blog entries or forum discussions that
were created one week ago. The full tutorial will teach us how to
create block content, write links, and retrieve information from Drupal
nodes.

Start your module by creating a PHP file and save it as 'onthisdate.module'.

<?php

?>

As per the [Coding standards], use the longhand <?php tag,
and not <? to enclose your PHP code.

All functions in your module are named {modulename}_{hook}, where
"hook" is a well defined function name. Drupal will call these
functions to get specific data, so having these well defined names means
Drupal knows where to look.

Telling Drupal about your module

The first function we'll write will tell Drupal information about your
module: its name and description. The hook name for this function is
'help', so start with the onthisdate_help function:

<?php
function onthisdate_help($section) {

}
?>

The $section variable provides context for the help: where in Drupal or
the module are we looking for help. The recommended way to process this
variable is with a switch statement. You'll see this code pattern in
other modules.

<?php
/* Commented out until bug fixed */
/*
function onthisdate_help($section) {
  switch($section) {
    case "admin/system/modules#name":
      $output = "onthisdate";
      break;
    case "admin/system/modules#description":
      $output = "Display a list of nodes that were created a week ago.";
      break;
    default:
      $output = "onthisdate";
      break;
  }
  return $output;
}
*/
?>

You will eventually want to add other cases to this switch statement to
provide real help messages to the user. In particular, output for
"admin/help#onthisdate" will display on the main help page accessed by
the admin/help URL for this module (/admin/help or ?q=admin/help).

Note:This function is commented out in the above code. This is
on purpose, as the current version of Drupal CVS won't display the
module name, and won't enable it properly when installed. Until this
bug is fixed, comment out your help function, or your module may not
work.

Telling Drupal who can use your module

The next function to write is the permissions function. Here, you can
tell Drupal who can access your module. At this point, give permission
to anyone who can access site content or administrate the module.

<?php
function onthisdate_perm() {
  return array(
"administer onthisdate");
}
?>

If you are going to write a module that needs to have finer control over
the permissions, and you're going to do permission control, you may want
to define a new permission set. You can do this by adding strings to
the array that is returned:

<?php
function onthisdate_perm() {
  return array(
"access onthisdate", "administer onthisdate");
}
?>

You'll need to adjust who has permission to view your module on the
administer » accounts » permissions page. We'll use the user_access
function to check access permissions later.

Be sure your permission strings must be unique to your module. If they
are not, the permissions page will list the same permission multiple
times.

Announce we have block content

There are several types of modules: block modules and node modules are
two. Block modules create abbreviated content that is typically (but
not always, and not required to be) displayed along the left or right
side of a page. Node modules generate full page content (such as blog,
forum, or book pages).

We'll create a block content to start, and later discuss node content.
A module can generate content for blocks and also for a full page (the
blogs module is a good example of this). The hook for a block module is
appropriately called "block", so let's start our next function:

<?php
function onthisdate_block($op='list', $delta=0) {
 
}
?>

The block function takes two parameters: the operation and the offset,
or delta. We'll just worry about the operation at this point. In
particular, we care about the specific case where the block is being
listed in the blocks page. In all other situations, we'll display the
block content.

<?php
function onthisdate_block($op='list', $delta=0) {

 
// listing of blocks, such as on the admin/system/block page
 
if ($op == "list") {
   
$block[0]["info"] = t("On This Date");
    return
$block;
  } else {
 
// our block content
 
}
}
?>
Generate content for a block

Now, we need to generate the 'onthisdate' content for the block. In
here, we'll demonstrate a basic way to access the database.

Our goal is to get a list of content (stored as "nodes" in the database)
created a week ago. Specifically, we want the content created between
midnight and 11:59pm on the day one week ago. When a node is first
created, the time of creation is stored in the database. We'll use this
database field to find our data.

First, we need to calculate the time (in seconds since epoch start, see
http://www.php.net/manual/en/function.time.php for more information on
time format) for midnight a week ago, and 11:59pm a week ago. This part
of the code is Drupal independent, see the PHP website (http://php.net/)
for more details.

<?php
function onthisdate_block($op='list', $delta=0) {

 
// listing of blocks, such as on the admin/system/block page
 
if ($op == "list") {
   
$block[0]["info"] = t("On This Date");
    return
$block;
  } else {
 
// our block content

    // Get today's date
   
$today = getdate();

   
// calculate midnight one week ago
   
$start_time = mktime(0, 0, 0,
                        
$today['mon'], ($today['mday'] - 7), $today['year']);

   
// we want items that occur only on the day in question, so calculate 1 day
   
$end_time = $start_time + 86400 // 60 * 60 * 24 = 86400 seconds in a day
   
...
  }
}
?>

The next step is the SQL statement that will retrieve the content we'd
like to display from the database. We're selecting content from the
node table, which is the central table for Drupal content. We'll get
all sorts of content type with this query: blog entries, forum posts,
etc. For this tutorial, this is okay. For a real module, you would
adjust the SQL statement to select specific types of content (by adding
the 'type' column and a WHERE clause checking the 'type' column).

Note: the table name is enclosed in curly braces: {node}.
This is necessary so that your module will support database table name
prefixes. You can find more information on the Drupal website by
reading the [Table Prefix (and sharing tables across instances)] page in
the Drupal handbook.

<?php
  $query
= "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= '" . $start_time .
          
"' AND created <= '". $end_time . "'";
?>

Drupal uses database helper functions to perform database queries. This
means that, for the most part, you can write your database SQL statement
and not worry about the backend connections.

We'll use db_query() to get the records (i.e. the database rows) that
match our SQL query, and db_fetch_object() to look at the individual
records:

<?php
 
// get the links
 
$queryResult db_query($query);

 
// content variable that will be returned for display
 
$block_content = '';

  while (
$links = db_fetch_object($queryResult)) {
   
$block_content .= '<a href="/' . url('node/view/' . $links->nid ) . '">' .
                      
$links->title . '</a><br />';
  }

 
// check to see if there was any content before setting up the block
 
if ($block_content == '') {
   
/* No content from a week ago.  If we return nothing, the block
     * doesn't show, which is what we want. */
   
return;
  }

 
// set up the block
 
$block['subject'] = 'On This Date';
 
$block['content'] = $block_content;
  return
$block;
}
?>

Notice the actual URL is enclosed in the url() function. This adjusts
the URL to the installations URL configuration of either clean URLS:
http://sitename/node/view/2 or http://sitename/?q=node/view/2

Also, we return an array that has 'subject' and 'content' elements.
This is what Drupal expects from a block function. If you do not
include both of these, the block will not render properly.

You may also notice the bad coding practice of combining content with
layout. If you are writing a module for others to use, you will want to
provide an easy way for others (in particular, non-programmers) to
adjust the content's layout. An easy way to do this is to include a
class attribute in your link, and not necessarily include the <br
/> at the end of the link. Let's ignore this for now, but be aware
of this issue when writing modules that others will use.

Putting it all together, our block function looks like this:

<?php
function onthisdate_block($op='list', $delta=0) {

 
// listing of blocks, such as on the admin/system/block page
 
if ($op == "list") {
   
$block[0]["info"] = t("On This Date");
    return
$block;
  } else {
 
// our block content

    // content variable that will be returned for display
   
$block_content = '';

   
// Get today's date
   
$today = getdate();

   
// calculate midnight one week ago
   
$start_time = mktime(0, 0, 0,
                        
$today['mon'], ($today['mday'] - 7), $today['year']);

   
// we want items that occur only on the day in question, so calculate 1 day
   
$end_time = $start_time + 86400 // 60 * 60 * 24 = 86400 seconds in a day

   
$query = "SELECT nid, title, created FROM " .
            
"{node} WHERE created >= '" . $start_time .
            
"' AND created <= '". $end_time . "'";


   
// get the links
   
$queryResult db_query($query);

    while (
$links = db_fetch_object($queryResult)) {
     
$block_content .= '<a href="/'.url('node/view/'.$links->nid).'">'.
                       
$links->title . '</a><br />';
    }

   
// check to see if there was any content before setting up the block
   
if ($block_content == '') {
     
// no content from a week ago, return nothing.
     
return;
    }

   
// set up the block
   
$block['subject'] = 'On This Date';
   
$block['content'] = $block_content;
    return
$block;
  }
}
?>
Installing, enabling and testing the module

At this point, you can install your module and it'll work. Let's do
that, and see where we need to improve the module.

To install the module, you'll need to copy your onthisdate.module file
to the modules directory of your Drupal installation. The file must be
installed in this directory or a subdirectory of the modules directory,
and must have the .module name extension.

Log in as your site administrator, and navigate to the modules
administration page to get an alphabetical list of modules. In the
menus: administer » configuration » modules, or via URL:

http://.../admin/system/modules or
http://.../?q=admin/system/modules

Note: You'll see one of three things for the 'onthisdate' module at this point:

  • You'll see the 'onthisdate' module name and no description
  • You'll see no module name, but the 'onthisdate' description
  • You'll see both the module name and the description

Which of these three choices you see is dependent on the state of the
CVS tree, your installation and the help function in your module. If
you have a description and no module name, and this bothers you, comment
out the help function for the moment. You'll then have the module name,
but no description. For this tutorial, either is okay, as you will just
enable the module, and won't use the help system.

Enable the module by selecting the checkbox and save your configuration.

Because the module is a blocks module, we'll need to also enable it in
the blocks administration menu and specify a location for it to display.
Navigate to the blocks administration page: admin/system/block or
administer » configuration » blocks in the menus.

Enable the module by selecting the enabled checkbox for the 'On This
Date' block and save your blocks. Be sure to adjust the location
(left/right) if you are using a theme that limits where blocks are
displayed.

Now, head to another page, say select the module. In some themes, the
blocks are displayed after the page has rendered the content, and you
won't see the change until you go to new page.

If you have content that was created a week ago, the block will display
with links to the content. If you don't have content, you'll need to
fake some data. You can do this by creating a blog, forum topic or book
page, and adjust the "Authored on:" date to be a week ago.

Alternately, if your site has been around for a while, you may have a
lot of content created on the day one week ago, and you'll see a large
number of links in the block.


Create a module configuration (settings) page

Now that we have a working module, we'd like to make it better. If we
have a site that has been around for a while, content from a week ago
might not be as interesting as content from a year ago. Similarly, if
we have a busy site, we might not want to display all the links to
content created last week. So, let's create a configuration page for
the administrator to adjust this information.

The configuration page uses the 'settings' hook. We would like only
administrators to be able to access this page, so we'll do our first
permissions check of the module here:

<?php
function onthisdate_settings() {
 
// only administrators can access this module
 
if (!user_access("admin onthisdate")) {
    return
message_access();
  }
}
?>

If you want to tie your modules permissions to the permissions of
another module, you can use that module's permission string. The
"access content" permission is a good one to check if the user can view
the content on your site:

<?php
 
...
 
// check the user has content access
 
if (!user_access("access content")) {
    return
message_access();
  }
  ...
?>

We'd like to configure how many links display in the block, so we'll
create a form for the administrator to set the number of links:

<?php
function onthisdate_settings() {
 
// only administrators can access this module
 
if (!user_access("admin onthisdate")) {
    return
message_access();
  }

 
$output .= form_textfield(t("Maximum number of links"), "onthisdate_maxdisp",
            
variable_get("onthisdate_maxdisp", "3"), 2, 2,
            
t("The maximum number of links to display in the block."));

  return
$output;
}
?>

This function uses several powerful Drupal form handling features. We
don't need to worry about creating an HTML text field or the form, as
Drupal will do so for us. We use variable_get to retrieve
the value of the system configuration variable "onthisdate_maxdisp",
which has a default value of 3. We use the form_textfield function to
create the form and a text box of size 2, accepting a maximum length of
2 characters. We also use the translate function of t(). There are
other form functions that will automatically create the HTML form
elements for use. For now, we'll just use the form_textfield function.

Of course, we'll need to use the configuration value in our SQL SELECT,
so we'll need to adjust our query statement in the onthisdate_block
function:

<?php
  $limitnum
= variable_get("onthisdate_maxdisp", 3);

 
$query = "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= '" . $start_time .
          
"' AND created <= '". $end_time . "' LIMIT " . $limitnum;
?>

You can test the settings page by editing the number of links displayed
and noticing the block content adjusts accordingly.

Navigate to the settings page: admin/system/modules/onthisdate or
administer » configuration » modules » onthisdate. Adjust the number
of links and save the configuration. Notice the number of links in the
block adjusts accordingly.

Note:We don't have any validation with this input. If you enter
"c" in the maximum number of links, you'll break the block.

Adding menu links and creating page content

So far we have our working block and a settings page. The block
displays a maximum number of links. However, there may be more links
than the maximum we show. So, let's create a page that lists all the
content that was created a week ago.

<?php
function onthisdate_all() {
 
}
?>

We're going to use much of the code from the block function. We'll
write this ExtremeProgramming style, and duplicate the code. If we need
to use it in a third place, we'll refactor it into a separate function.
For now, copy the code to the new function onthisdate_all(). Contrary
to all our other functions, 'all', in this case, is not a Drupal hook.
We'll discuss below.

<?php
function onthisdate_all() {

 
// content variable that will be returned for display
 
$page_content = '';

 
// Get today's date
 
$today = getdate();

 
// calculate midnight one week ago
 
$start_time = mktime(0, 0, 0,
                      
$today['mon'], ($today['mday'] - 7), $today['year']);

 
// we want items that occur only on the day in question, so calculate 1 day
 
$end_time = $start_time + 86400 // 60 * 60 * 24 = 86400 seconds in a day

  // NOTE!  No LIMIT clause here!  We want to show all the code
 
$query = "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= '" . $start_time .
          
"' AND created <= '". $end_time . "'";

 
// get the links
 
$queryResult db_query($query);

  while (
$links = db_fetch_object($queryResult)) {
   
$page_content .= '<a href="/'.url('node/view/'.$links->nid).'">'.
                     
$links->title . '</a><br />';
  }
 
  ...
}
?>

We have the page content at this point, but we want to do a little more
with it than just return it. When creating pages, we need to send the
page content to the theme for proper rendering. We use this with the
theme() function. Themes control the look of a site. As noted above,
we're including layout in the code. This is bad, and should be
avoided. It is, however, the topic of another tutorial, so for now,
we'll include the formatting in our content:

<?php
   
print theme("page", $content_string);
?>

The rest of our function checks to see if there is content and lets the
user know. This is preferable to showing an empty or blank page, which
may confuse the user.

Note that we are responsible for outputting the page content with the
'print theme()' syntax. This is a change from previous 4.3.x themes.

<?php
function onthisdate_all() {

  ...

 
// check to see if there was any content before setting up the block
 
if ($page_content == '') {
   
// no content from a week ago, let the user know
   
print theme("page",
               
"No events occurred on this site on this date in history.");
    return;
  }

  print
theme("page", $page_content);
}
?>
Letting Drupal know about the new function

As mentioned above, the function we just wrote isn't a 'hook': it's not
a Drupal recognized name. We need to tell Drupal how to access the
function when displaying a page. We do this with the _link hook and
the menu() function:

<?php
function onthisdate_link($type, $node=0) {

}
?>

There are many different types, but we're going to use only 'system' in
this tutorial.

<?php
function onthisdate_link($type, $node=0) {
  if ((
$type == "system")) {
   
// URL, page title, func called for page content, arg, 1 = don't disp menu
   
menu("onthisdate", t("On This Date"), "onthisdate_all", 1, 1);
  }
}
?>

Basically, we're saying if the user goes to "onthisdate" (either via
?q=onthisdate or http://.../onthisdate), the content generated by
onthisdate_all will be displayed. The title of the page will be "On
This Date". The final "1" in the arguments tells Drupal to not display
the link in the user's menu. Make this "0" if you want the user to see
the link in the side navigation block.

Navigate to /onthisdate (or ?q=onthisdate) and see what you get.

Adding a more link and showing all entries

Because we have our function that creates a page with all the content
created a week ago, we can link to it from the block with a "more" link.

Add these lines just before that $block['subject'] line, adding this to
the $block_content variable before saving it to the $block['content']
variable:

<?php
 
// add a more link to our page that displays all the links
  
$block_content .= "<div class=\"more-link\">". l(t("more"), "onthisdate", array("title" => t("More events on this day."))) ."</div>";
?>

This will add the more link.

Conclusion

We now have a working module. It created a block and a page. You
should now have enough to get started writing your own modules. We
recommend you start with a block module of your own and move onto a node
module. Alternately, you can write a filter or theme.

As is, this tutorial's module isn't very useful. However, with a few
enhancements, it can be entertaining. Try modifying the select query
statement to select only nodes of type 'blog' and see what you get.
Alternately, you could get only a particular user's content for a
specific week. Instead of using the block function, consider expanding
the menu and page functions, adding menus to specific entries or dates,
or using the menu callback arguments to adjust what year you look at the
content from.

If you start writing modules for others to use, you'll want to provide
more details in your code. Comments in the code are incredibly valuable
for other developers and users in understanding what's going on in your
module. You'll also want to expand the help function, providing better
help for the user. Follow the Drupal [Coding standards], especially if
you're going to add your module to the project.

Two topics very important in module development are writing themeable
pages and writing translatable content. Please check the [Drupal
Handbook] for more details on these two subject.

Creating modules - a tutorial: Drupal 4.6/4.7

This tutorial describes how to create a module for Drupal 4.6 or 4.7. It is an update to the tutorial for Drupal 4.3. Please see comments there, also. Most of this tutorial is valid for Drupal 4.7 as well, but you should check the API documentation, as well as the documentation on how to update your modules from one version of Drupal to another.

A module is a collection of functions that link into Drupal, providing additional functionality to your Drupal installation. After reading this tutorial, you will be able to create a basic block module and use it as a template for more advanced modules and node modules.

This tutorial will not necessarily prepare you to write modules for release into the wild. It does not cover caching, nor does it elaborate on permissions or security issues. Use this tutorial as a starting point, and review other modules and the Drupal handbook and Coding standards for more information.

This tutorial assumes the following about you:

  • Basic PHP knowledge, including syntax and the concept of PHP objects
  • Basic understanding of database tables, fields, records and SQL statements
  • A working Drupal installation
  • Drupal administration access
  • Webserver access

This tutorial does not assume you have any knowledge about the inner workings of a Drupal module. This tutorial will not help you write modules for versions of Drupal earlier than 4.5.

01. Getting started

To focus this tutorial, we'll start by creating a block module that lists links to content such as blog entries or forum discussions that were created one week ago. The full tutorial will teach us how to create block content, write links, and retrieve information from Drupal nodes.

Start your module by creating a PHP file and save it as 'onthisdate.module' in the modules directory of your Drupal installation.

<?php
/* $Id$ */

As per the Coding standards, omit the closing ?> tag and use the longhand <?php tag. The $Id$ string will help keep track of the revision number and date when you commit the file to CVS.

All functions in your module are named {modulename}_{hook}, where "hook" is a well defined function name. Drupal will call these functions to get specific data, so having these well defined names means Drupal knows where to look.

The module is not operational yet: it hasn't been activated. We'll activate the module later in the tutorial.

02. Telling Drupal about your module

The first function we'll write will tell Drupal information about your module: its name and description. The hook name for this function is 'help', so start with the onthisdate_help function:

<?php
function onthisdate_help($section='') {

}
?>

The $section variable provides context for the help: where in Drupal or the module are we looking for help. The recommended way to process this variable is with a switch statement. You'll see this code pattern in other modules.

<?php
/**
* Display help and module information
* @param section which section of the site we're displaying help
* @return help text for section
*/
function onthisdate_help($section='') {

 
$output = '';

  switch (
$section) {
    case
"admin/modules#description":
     
$output = t("Displays links to nodes created on this date");
      break;
  }

  return
$output;
}
// function onthisdate_help
?>

The admin/modules#description case is used by the Drupal core as the module description on the modules administration page (/admin/modules or ?q=admin/modules).

You will eventually want to add other cases to this switch statement to provide real help messages to the user. In particular, output for "admin/help#onthisdate" will display on the main help page accessed by the admin/help URL for this module (/admin/help or ?q=admin/help).

More information about the help hook:

Download the code so far, (4.7 version) renaming to onthisdate.module before saving in your Drupal installation.

03. Telling Drupal who can use your module

Main topic described: Permissions
Drupal hook described: hook_perm

The next function to write is the permissions function, using the _perm hook. This is where you will define the names of the permissions of your module. This function doesn't grant permission, it just specifies what permissions are available for this module. Access based on these permissions is defined later in the {module}_access function, later in the tutorial.

<?php
/**
* Valid permissions for this module
* @return array An array of valid permissions for the onthisdate module
*/

function onthisdate_perm() {
  return array(
'access onthisdate content');
}
// function onthisdate_perm()
?>

If you are going to write a module that needs to have finer control over the permissions, and you're going to do permission control (by checking permissions), you should expand this permission set. For example:

<?php
function onthisdate_perm() {

return array(
'access onthisdate content', 'administer onthisdate');

}
// function onthisdate_perm
?>

For this tutorial, start with the first one. We'll later move to the second version.

You'll need to adjust who has permission to view your module on the administer » access control page. We'll use the user_access function to check access permissions later (whoa, so many "laters!").

Your permission strings are arbitrary, but each must be unique among all installed modules. Otherwise, one occurrence of the name will take the permissions of the other. The permission strings should each usually contain your module name, since this helps avoid name space conflicts with other modules.

The suggested naming convention for permissions is "action_verb modulename". For example:

<?php
function newmodule_perm() {

return array(
'access newmodule', 'create newmodule', 'administer newmodule');

}
// function newmodule_perm
?>

The setup of the module is now done. You can add the code above to your module file. Next, we'll start generating content.

More information about the permission hook: Drupal 4.7.x

04. Declare we have block content

There are several types of modules: block modules and node modules are two. Block modules create abbreviated content that is typically (but not always, and not required to be) displayed along the left or right side of a page. Node modules generate full page content (such as blog, forum, or book pages).

We'll create a block content to start, and later discuss node content, as well as filtering content. A module can generate content for blocks and also for a full page (the blogs module is a good example of this). The hook for a block module is appropriately called "block", so let's start our next function:

<?php
/**
* Generate HTML for the onthisdate block
* @param op the operation from the URL
* @param delta offset
* @returns block HTML
*/
function onthisdate_block($op='list', $delta=0) {
 
}
// end function onthisdate_block
?>

The block function takes two parameters: the operation and the offset, or delta. The offset allows you to create different content for different blocks, all within the same block function. We'll just worry about the operation at this point. In particular, we care about the specific case where the block is being listed in the blocks page. In all other situations, we'll display the block content.

When the module will be listed on the blocks page, the $op parameter's value will be 'list':

<?php
/**
* Generate HTML for the onthisdate block
* @param op the operation from the URL
* @param delta offset
* @returns block HTML
*/
function onthisdate_block($op='list', $delta=0) {
 
// listing of blocks, such as on the admin/block page
 
if ($op == "list") {
    
$block[0]["info"] = t('On This Date');
     return
$block;
  }

}
// end onthisdate_block
?>

Next, we'll generate the block content.

More information about the block hook:

Download the code so far, (4.7 version) renaming to onthisdate.module before saving in your Drupal installation.

05. Generate the block content

Now, we need to generate the 'onthisdate' content for the block. Here we'll demonstrate a basic way to access the database.

Our goal is to get a list of content (stored as "nodes" in the database) created a week ago. Specifically, we want the content created between midnight and 11:59pm on the day one week ago. When a node is first created, the time of creation is stored in the database. We'll use this database field to find our data.

First, we need to calculate the time (in seconds since epoch start, see http://www.php.net/manual/en/function.time.php for more information on time format) for midnight a week ago, and 11:59pm a week ago. This part of the code is Drupal independent, see the PHP website (http://php.net/) for more details.

<?php
/**
* Generate HTML for the onthisdate block
* @param op the operation from the URL
* @param delta offset
* @returns block HTML
*/
function onthisdate_block($op='list', $delta=0) {

 
// listing of blocks, such as on the admin/block page
 
if ($op == "list") {
   
$block[0]["info"] = t('On This Date');
    return
$block;
  } else if (
$op == 'view') {

   
// our block content
    // Get today's date
   
$today = getdate();

   
// calculate midnight one week ago
   
$start_time = mktime(0, 0, 0,
                        
$today['mon'], ($today['mday'] - 7), $today['year']);

   
// we want items that occur only on the day in question, so  
    // calculate 1 day
   
$end_time = $start_time + 86400
   
// 60 * 60 * 24 = 86400 seconds in a day
   
...
  }
}
?>

The next step is the SQL statement that will retrieve the content we'd like to display from the database. We're selecting content from the node table, which is the central table for Drupal content. We'll get all sorts of content type with this query: blog entries, forum posts, etc. For this tutorial, this is okay. For a real module, you would adjust the SQL statement to select specific types of content (by adding the 'type' column and a WHERE clause checking the 'type' column).

Note: the table name is enclosed in curly braces: {node}. This is necessary so that your module will support database table name prefixes. You can find more information on the Drupal website by reading the Table Prefix (and sharing tables across instances) page in the Drupal handbook.

We'll use db_query() to get the records (i.e. the database rows) with our SQL query

<?php
$result
db_query("SELECT nid, title, created FROM {node} WHERE created >= '%s' AND created <= '%s'", $start_time, $end_time);
?>

Drupal uses database helper functions to perform database queries. This means that, for the most part, you can write your database SQL statement and not worry about the backend connections.

We use db_fetch_object() to look at the individual records:

<?php
 
// content variable that will be returned for display   
 
$block_content = ''
  while (
$links = db_fetch_object($result)) {
   
$block_content .=  l($links->title, 'node/' . $links->nid) . '<br />';
  }

 
// check to see if there was any content before setting up
  //  the block 
 
if ($block_content == '') {   
   
/* No content from a week ago.  If we return nothing, the block  
     * doesn't show, which is what we want. */
   
return;
  }

 
// set up the block 
 
$block['subject'] = 'On This Date'
 
$block['content'] = $block_content;
  return
$block;
}
?>

Notice the actual URL is enclosed in the l() function. l generates <a href="link"> links, adjusting the URL to the installation's URL configuration of either clean URLS: http://(sitename)/node/2 or not http://(sitename)/?q=node/2

Also, we return an array that has 'subject' and 'content' elements. This is what Drupal expects from a block function. If you do not include both of these, the block will not render properly.

You may also notice the bad coding practice of combining content with layout. If you are writing a module for others to use, you will want to provide an easy way for others (in particular, non-programmers) to adjust the content's layout. An easy way to do this is to include a class attribute in your link, or surround the HTML with a <div> tag with a module specific CSS class and not necessarily include the <br /> at the end of the link. Let's ignore this for now, but be aware of this issue when writing modules that others will use.

Putting it all together, our block function at this point looks like this:

<?php
function onthisdate_block($op='list', $delta=0) {
 
// listing of blocks, such as on the admin/block page
 
if ($op == "list") {
   
$block[0]["info"] = t("On This Date");
    return
$block;
  } else if (
$op == 'view') {
 
// our block content
    // content variable that will be returned for display
   
$block_content = '';

   
// Get today's date
   
$today = getdate();

   
// calculate midnight one week ago
   
$start_time = mktime(0, 0, 0,$today['mon'],
                               (
$today['mday'] - 7), $today['year']);

   
// we want items that occur only on the day in question, so
    //calculate 1 day
   
$end_time = $start_time + 86400;
   
// 60 * 60 * 24 = 86400 seconds in a day

   
$result db_query("SELECT nid, title, created FROM {node} WHERE created >= '%s' AND created <= '%s'", $start_time, $end_time);
    while (
$links = db_fetch_object($result)) {
     
$block_content .= l($links->title, 'node/'.$links->nid) . '<br />';
    }
   
// check to see if there was any content before setting up the block
   
if ($block_content == '') {
     
// no content from a week ago, return nothing.
     
return;
    }
   
// set up the block
   
$block['subject'] = 'On This Date';
   
$block['content'] = $block_content;
    return
$block;
  }
}
?>

Download the code so far, (4.7 version) renaming to onthisdate.module before saving in your Drupal installation.

Our module is now functional - we can install, enable and test it.

06. Installing, enabling and testing the module

At this point, you can install your module and it'll work. Let's do that, and see where we need to improve the module.

To install the module, you'll need to copy your onthisdate.module file to the modules directory of your Drupal installation. The file must be installed in this directory or a subdirectory of the modules directory, and must have the .module name extension.

Log in as your site administrator, and navigate to the modules administration page to get an alphabetical list of modules. In the menus: administer » modules, or via URL:

http://.../admin/modules or
http://.../?q=admin/modules

When you scroll down, you'll see the onthisdate module listed with the description next to it.

Enable the module by selecting the checkbox and save your configuration.

Because the module is a blocks module, we'll need to also enable it in the blocks administration menu and specify a location for it to display. Node modules may or may not need further configuration depending on the module. Any module can have settings, which affect the functionality/display of a module. We'll discuss settings later. For now, navigate to the blocks administration page: admin/block or administer » blocks in the menus.

Enable the module by selecting the enabled checkbox for the 'On This Date' block and save your blocks. Be sure to adjust the location (left/right) if you are using a theme that limits where blocks are displayed.

Now, head to another page, say, select the modules menu. In some themes, the blocks are displayed after the page has rendered the content, and you won't see the change until you go to new page.

If you have content that was created a week ago, the block will display with links to the content. If you don't have content, you'll need to fake some data. You can do this by creating a blog, forum topic or book page, and adjust the "Authored on:" date to be a week ago.

Alternately, if your site has been around for a while, you may have a lot of content created on the day one week ago, and you'll see a large number of links in the block.

Test with large loads

Some modules need to be tested with large loads. Here are some situations where your module might slow down a large or busy site.

  • You add SQL.
  • You clear the cache.

The cache_clear_all function makes Drupal rebuild variables, menus and other items. Cache is used for anonymous users and is switched off for users when they log in. If you clear the whole cache frequently then your Web site slows down to rebuild cached information for people browsing the site and the extra overhead slows down the site for everyone.

You can be selective when clearing the cache and that will reduce the rebuilding. If you delete a specific cache entry then MySQL will find the single entry quickly, delete just the one entry, and only the one entry will be rebuilt. When you use a wildcard to select a range of entries then MySQL may have to scan the whole cache table index to find the right entries and rebuilding all those entries will take time.

There is at least once cache entry per node which means a site with a lot of nodes will suffer a greater slowdown than a site with a few nodes. If your test system has only a few nodes then you may never see a slowdown. You need to test with a site that has lots of nodes.

SQL can have the equivalent of a wildcard. The cache_clear_all wildcard translates to an SQL " where like" combination and the 'like" allows wildcards. Those wildcards and some other SQL scan a whole table instead of finding an individual row. Scanning a table is far slower than finding specific entries using an index.

http://drupal.org/node/35909 is an example of a discussion of a site slowdown caused by SQL. Read the discussion to get an idea of when a site is big enough to slow down and the type of symptoms you might see.

The 35909 problem was caused by a database table scan and the scan was replaced with a faster index lookup. The change to the SQL in a module was implemented at http://drupal.org/node/36429.

If your SQL runs once in an administration page then your SQL will not slow down a Web site unless you are scanning the whole of a very large frequently used site. If your SQL runs once per page view then your SQL will slow down your site even if the SQL is just slightly inefficient.

Think about an administration page that finds all nodes containing the text 'how-to' and changes the text to 'how to'. The SQL has to read all the text in every node. There are 40000 nodes in drupal.org. A search of drupal.org for 'how-to' will take several seconds and slow down the whole site for the whole search. If there are 100 people using the search and they each search 10 times per hour then drupal.org will slow down 1000 times per hour. You will never see the slow response on a test site with a few nodes and one user.

Now think about SQL that selects a specific row from one table but does it every page view. Your current Web site might retrieve 5 rows per page view. You add one more row. That is a total of 6 rows which is a 20 percent increase in database activity. If your module reads 10 rows instead of one, then your site jumps from 5 rows per page view to 15 rows per page view. Again you will not see the slowdown in a test site but you will see it in a site the size of Drupal.

Test your module with a large site. If you do not have a large site then offer the code for review by other Drupal developers and for testing at large sites. If you are reading rows from a table then you can search for the table name to see if anyone else has comments about the use of the table.

Look at the indexes used for table access. If you access table xyz using "where abc = '123'" then the database software will look for an index on column abc. If there is no index then the database software reads through every row of table xyz.

Indexes slow down database updates, do nothing to database reads and speed up searches. Adding an index to column abc will slightly slow down updates of the table and will speed up those cases where you search for a value in abc. The index will help if there are few updates and lots of searches. If there are many updates and searches are rare then an index might slow down the site more than you save on the occasional search.

Joins and compound where clauses are worth looking at. If you search a table with 100 entries then the search is so fast that you may not notice it. If you join two tables, each with 100 entries, and then search the result, you can be searching a result set containing 10000 rows. When you join three tables of 100 rows each then you could be searching 1000000 rows.

Memory is a limited resource that does not show up in a test system but becomes significant in a Web server with many active users. If your changes to code mean that you have to increase PHP's default memory size setting then you may slow down your Web site.

CPU power is also a limited resource and can be chewed up by frequent use of CPU intensive operations including string searches and regular expressions. If you add several CPU intensive operations to every page view then you will slow down the site by a noticeable amount. Drupal's cache mechanism may help you avoid performing the operations for every page view.

You can imagine how quickly you can drag a Web site to a complete halt with just a slight change to SQL or a big change to code. If your module adds SQL to every page view, every node view, or creates a database scan then you need to test with large numbers of rows in your tables and many users. If you module clears the cache or does something else to chew up resources, the result will show up in larger sites. If your module contains a potential slow down and you cannot test on a large site then ask other developers about ways to perform the task at maximum efficiency.

07. Create a module configuration (settings) page

Now that we have a working module, we'd like to make it better. If we have a site that has been around for a while, content from a week ago might not be as interesting as content from a year ago. Similarly, if we have a busy site, we might not want to display all the links to content created last week. So, let's create a configuration page for the administrator to adjust this information.

A module's configuration is set up with the settings hook. We would like only administrators to be able to access this page, so we'll do our first permissions check of the module here:

<?php
/**
* Module configuration settings
* @return settings HTML or deny access
*/
function onthisdate_settings() { 
 
// only administrators can access this module
 
if (!user_access("administer onthisdate")) {
    return
message_access(); 
  }
}
?>

If you want to tie your modules permissions to the permissions of another module, you can use that module's permission string instead of defining a specific permission string for this module. The "access content" permission is a good one to check if the user can view the content on your site:

<?php
 
...
 
// check the user has content access
 
if (!user_access("access content")) {
    return
message_access();
  }

  ...
?>

To minimize the number of permissions an administrator has to deal with, we're going to use the global administration permission for administrating our module:

<?php
 
...
 
// check the user has administration access
 
if (!user_access('access administration pages')) {
    return
message_access();
  }
  ...
?>

This example is actually a bit artificial, as settings hook pages are available only to users with the 'access administration pages' permissions - this check isn't technically needed. As you'll see later in the tutorial, page access is most easily controlled in the _menu hook. For this tutorial example, however, we'll leave in the permissions check.

Now, we'd like to configure how many links display in the block, so we'll create a form for the administrator to set the number of links:

For 4.5-4.6 modules, use the form_textfield function:

<?php
function onthisdate_settings() {
 
// only administrators can access this function
 
if (!user_access('access administration pages')) {
    return
message_access();
  }
 
$output .= form_textfield(t("Maximum number of links"), "onthisdate_maxdisp",
            
variable_get("onthisdate_maxdisp", "3"), 2, 2,
            
t("The maximum number of links to display in the block."));  return $output;}
}
?>

For 4.7 modules, use the updated form API:

<?php
function onthisdate_settings() {
 
// only administrators can access this module
 
if (!user_access('access administration pages')) {
    return
message_access();
  }
 
$form['onthisdate_maxdisp'] = array('#type' => 'textfield', '#title' => t('Maximum number of links'), '#default_value' => variable_get('onthisdate_maxdisp', 3), '#description' => t("The maximum number of links to display in the block."), '#maxlength' => '2', '#size' => '2');
  return
$form;
}
?>

This function uses several powerful Drupal form handling features. We don't need to worry about creating an HTML text field or the form, as Drupal will do so for us for this settings page. We use variable_get to retrieve the value of the system configuration variable "onthisdate_maxdisp", and define the default value to be 3. We use the form_textfield function to create the form and a text box of size 2, accepting a maximum length of 2 characters. We also use the translate function of t(). There are other form functions that will automatically create the HTML form elements for use. For now, we'll just use the form_textfield() function for 4.5 - 4.6 nodes, and the forms API for 4.7 and later.

When you save a settings variable for any module, the variable (in our case, 'onthisdate_maxdisp') and the value is stored in the variables table. Programmatically, you can retrieve the values with the variable_get('variable_name', default_value) function.

Of course, we'll need to use the configuration value in our SQL SELECT, so we'll need to adjust our query statement in the onthisdate_block function. One way to do this is with a LIMIT value in our query:

<?php
  $limitnum
= variable_get("onthisdate_maxdisp", 3);
 
$query = "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= '" . $start_time .
          
"' AND created <= '". $end_time . "' LIMIT " . $limitnum;
?>

However, this method may or may not translate across databases (really). Better to use one of Drupals select methods. In this case, let's leave the original query the same, and call db_query_range:
<?php
  $limitnum
= variable_get("onthisdate_maxdisp", 3);

 
$query = "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= '" . $start_time .
          
"' AND created <= '". $end_time;

  
$queryResult = db_query_range($query, 1, $limitnum);
?>

You can test the settings page by editing the number of links displayed and noticing the block content adjusts accordingly. Navigate to the settings page: admin/settings/onthisdate or administer » settings » onthisdate. (If the page doesn't exist, you may have to disable and enable the module for the system to register the new settings page.) Adjust the number of links and save the configuration. Notice the number of links in the block adjusts accordingly.

Note:We don't have any validation with this input. If you enter "c" in the maximum number of links, you'll break the block.

More information about the settings hook:

Note: the settings hook will no longer be used in Drupal 4.8/5.0. See the documentation on hook_menu for how to create a menu item.

Download the code so far, (4.7 version) renaming to onthisdate.module before saving in your Drupal installation.

08. Generate a page content

So far we have our working block and a settings page. The block displays a maximum number of links. However, there may be more links than the maximum we show. So, let's create a page that lists all the content that was created a week ago.

<?php
function onthisdate_all() {}
?>

We're going to use much of the code from the block function. We'll write this ExtremeProgramming style, and duplicate the code. If we need to use it in a third place, we'll refactor it into a separate function. For now, copy the code to the new function onthisdate_all(). Contrary to all our other functions, 'all', in this case, is not a Drupal hook.

If you want to call this function from another module, use the standard naming scheme we've been using: modulename_action. It can be called using the function module_invoke function. If you want the function to remain private (because, say, it's merely a helper function in your module) and easily accessible by only your module, prefix the function name with an underscore. We want the former.

<?php
function onthisdate_all() {
 
// content variable that will be returned for display
 
$page_content = '';

 
// Get today's date
 
$today = getdate();

 
// calculate midnight one week ago
 
$start_time = mktime(0, 0, 0, $today['mon'], ($today['mday'] - 7), $today['year']);

 
// we want items that occur only on the day in question,
  // so calculate 1 day
 
$end_time = $start_time + 86400;
 
// 60 * 60 * 24 = 86400 seconds in a day

 
$query = "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= '" . $start_time .
          
"' AND created <= '". $end_time . "'";

 
// get the links (no range limit here)
 
$queryResult db_query($query);
  while (
$links = db_fetch_object($queryResult)) {
   
$page_content .= l($links->title, 'node/'.$links->nid).'<br />';
  }
  ...
}
?>

We have the page content at this point, but we want to do a little more with it than just return it. When creating pages, we need to send the page content to the theme for proper rendering. We use this with the theme() function. Themes control the look of a site. As noted before, we're including layout in the code. This is bad, and should be avoided. It is, however, the topic of another tutorial, so for now, we'll include the formatting in our content.

The rest of our function checks to see if there is content and lets the user know. This is preferable to showing an empty or blank page, which may confuse the user.

Note that in 4.6.x we are responsible for outputting the page content with the print theme('page') syntax. In Drupal 4.7.x and above, we simply return the content, and Drupal displays it within a themed page.

<?php
function onthisdate_all() {

  ...

 
// check to see if there was any content before
  // setting up the block
 
if ($page_content == '') {
   
// no content from a week ago, let the user know
   
$page_content = "No events occurred on this site on this date in history.";
  }

  return
$page_content
 
// for 4.6: print theme("page", $page_content);
}
?>

Even though we have this function that will output links to the content generated a week ago, we haven't specified what URL will cause this page to render. We'll do that next.

Download the code so far, (4.7 version) renaming to onthisdate.module before saving in your Drupal installation.

09. Letting Drupal know about the new function

As mentioned previously, the function we just wrote isn't a 'hook': it's not a Drupal recognized name. We need to tell Drupal how to access the function when displaying a page. We do this with the menu() hook. The menu() hook defines the association between a URL and the function that creates the content for that url. The hook also does permission checking, if desired.

<?php
function onthisdate_menu() {

 
$items = array();

 
$items[] = array('path' => 'onthisdate',
                                 
'title' => t('on this date'),
                                 
'callback' => 'onthisdate_all',
                                 
'access' => user_access('access onthisdate content'),
                                 
'type' => MENU_CALLBACK);

  return
$items;
}
?>

Basically, we're saying if the user goes to "onthisdate" (either via ?q=onthisdate or http://.../onthisdate), the content generated by onthisdate_all will be displayed. The title of the page will be "on this date". The type MENU_CALLBACK tells Drupal to not display the link in the user's menu, just use this function when the URL is accessed. Use MENU_LOCAL_TASK if you want the user to see the link in the side navigation block.

More information on the menu system:

As mentioned before, the menu hook can handle permission checking before rendering the page. The 'access' entry in the menu item array is where this check is done. If you added a value in your permissions array in the perm hook function, you can use that string as a parameter in the user_access function. If the user isn't in a role that has that permission, the page will not render for the user.

If the module has not be enabled, enable it. If you have already enabled it, in order to reset the menu definitions in the system, you'll need to disable, then reenable it.

Now, navigate to /onthisdate (or ?q=onthisdate) and see what you get.

Download the code so far, (4.7 version) renaming to onthisdate.module before saving in your Drupal installation.

10. Adding a 'more' link and showing all entries

We now have a function that creates a page with all the content created a week ago. Let's link to it from the block with a "more" link.

Add these lines just before that $block['subject'] line. These lines will add the more link to the end of the $block_content variable before saving it to the $block['content'] variable:

<?php
// add a more link to our page that displays all the links
 
$block_content .=
        
"<div class=\"more-link\">".
        
l(t("more"), "onthisdate", array("title" => t("More events on this day.")))
         .
"</div>";
?>

This will add the more link to the block. Note the extra parameters used in the l() function. You can add additional values, such as 'class', in the array to customize the link.

More information on the l() function:

Download the final module, (4.7 version) renaming to onthisdate.module before saving in your Drupal installation.

Conclusion

We now have a working module. It created a block and a page. You should now have enough to get started writing your own modules. We recommend you start with a block module of your own and move onto a node module. Alternately, you can write a filter or theme.

As is, this tutorial's module isn't very useful. However, with a few enhancements, it can be entertaining. Try modifying the select query statement to select only nodes of type 'blog' and see what you get. Alternately, you could get only a particular user's content for a specific week. Instead of using the block function, consider expanding the menu and page functions, adding menus to specific entries or dates, or using the menu callback arguments to adjust what year you look at the content from.

If you start writing modules for others to use, you'll want to provide more details in your code. Comments in the code are incredibly valuable for other developers and users in understanding what's going on in your module. You'll also want to expand the help function, providing better help for the user. Follow the Drupal [Coding standards], especially if you're going to add your module to the project.

Two topics very important in module development are writing themeable pages and writing translatable content. Please check the Drupal Handbook for more details on these two subjects.

Creating modules - a tutorial: Drupal 5.x

This tutorial describes how to create a module for Drupal 5. It is an update to the tutorial for Drupal 4.6. Please see comments there, also. Most of this tutorial is valid for Drupal 4.7 as well, but you should check the API documentation, as well as the documentation on how to update your modules from one version of Drupal to another.

A module is a collection of functions that link into Drupal, providing additional functionality to your Drupal installation. After reading this tutorial, you will be able to create a basic block module and use it as a template for more advanced modules and node modules.

This tutorial will not necessarily prepare you to write modules for release into the wild. It does not cover caching, nor does it elaborate on permissions or security issues. Use this tutorial as a starting point, and review other modules and the Drupal handbook and Coding standards for more information.

This tutorial assumes the following about you:

  • Basic PHP knowledge, including syntax and the concept of PHP objects
  • Basic understanding of database tables, fields, records and SQL statements
  • A working Drupal installation
  • Drupal administration access
  • Webserver access

This tutorial does not assume you have any knowledge about the inner workings of a Drupal module. This tutorial will not help you write modules for versions of Drupal earlier than 4.5.

01. Getting started

To focus this tutorial, we'll start by creating a block module that lists links to content such as blog entries or forum discussions that were created one week ago. The full tutorial will teach us how to create block content, write links, and retrieve information from Drupal nodes.

Start your module by creating a folder in your Drupal installation at the path: sites/all/modules/onthisdate You may need to create the sites/all/modules directory first. Create a PHP file and save it as onthisdate.module in the directory sites/all/modules/onthisdate. As of Drupal 5.x, sites/all/modules is the preferred place for non-core modules (and sites/all/themes for non-core themes), since this places all site-specific files in the sites directory. This allows you to more easily update the core files and modules without erasing your customizations.

<?php
/* $Id$ */

As per the Coding standards, omit the closing ?> tag and use the longhand <?php tag. The $Id$ string will help keep track of the revision number and date when you commit the file to CVS.

All functions in your module that will be used by Drupal are named {modulename}_{hook}, where "hook" is a pre-defined function name suffix. Drupal will call these functions to get specific data, so having these well-defined names means Drupal knows where to look. We will come to hooks in a while.

The module is not operational yet: it hasn't been activated. We'll activate the module later in the tutorial.

02. Telling Drupal about your module

Main topic described: Let Drupal know your module exists
Drupal hook described: hook_help

In Drupal 5.x the basic information about your module, its name and description, is no longer provided by hook_help. Instead, all modules now need to have a modulename.info file, containing meta information about the module (for details see Writing .info files (Drupal 5.x)). For our example, "onthisdate.info'.

The general format is:

; $Id$
name = Module Name
description = "A description of what your module does."

Without this file, your module will not show up in the module listing!.

for our example, it could contain the following:

; $Id$
name = On this date
description = "A block module that lists links to content such as blog entries or forum discussions that were created one week ago."

Add the source above to a file named to onthisdate.info before saving in your module's directory at sites/all/modules/onthisdate.

There are also three optional lines that may appear in the .info file:

dependencies = module1 module2 module3
package = "Your arbitrary grouping string"
version = "$Name$"

For our example module, these don't apply and we will simply omit them. If you assign dependencies for your module, Drupal will not allow it to be activated until the required dependencies are met.

If you assign a package string for your module, on the admin/build/modules page it will be listed with other modules with the same category. If you do not assign one, it will simply be listed as 'Uncategorized'. Not assigning a package for your module is perfectly ok; in general packages are best used for modules that are distributed together or are meant to be used together. If you have any doubt, leave this field blank.

Suggested examples of appropriate items for the package field:

  • Audio
  • Bot
  • CCK
  • Chat
  • E-Commerce
  • Event
  • Feed Parser
  • Organic groups
  • Station
  • Video
  • Views
  • Voting (if it uses/requires VotingAPI)

The version line will provide the version string for users getting their modules directly from CVS rather than using the tarball package that is created with a release.

The files use the ini format and can include a ; $Id$ to have CVS insert the file ID information.

For more information on ini file formatting, see the PHP.net parse_ini_file documentation.

We can also provide help and additional information about our module. Because of the use of the .info file described above, this hook is now optional. However, it is a good idea to implement it. The hook name for this function is 'help', so start with the onthisdate_help function:

<?php
function onthisdate_help($section='') {

}
?>

The $section variable provides context for the help: where in Drupal or the module are we looking for help. The recommended way to process this variable is with a switch statement. You'll see this code pattern in other modules.

<?php
/**
* Display help and module information
* @param section which section of the site we're displaying help
* @return help text for section
*/
function onthisdate_help($section='') {

 
$output = '';

  switch (
$section) {
    case
"admin/help#onthisdate":
     
$output = '<p>' t("Displays links to nodes created on this date"). '</p>';
      break;
  }

  return
$output;
}
// function onthisdate_help
?>

The admin/help#modulename case is used by the Drupal core to linked from the main help page (/admin/help or ?q=admin/help). You will eventually want to add more text to provide a better help message to the user.

More information about the help hook:
Drupal HEAD

Add the source above to a file named to onthisdate.module before saving in your Drupal installation.

03. Telling Drupal who can use your module

Main topic described: Permissions
Drupal hook described: hook_perm

The next function to write is the permissions function, using the _perm hook. This is where you will define the names of the permissions of your module. This function doesn't grant permission, it just specifies what permissions are available for this module. Access based on these permissions is defined later in the {module}_access function, later in the tutorial.

<?php
/**
* Valid permissions for this module
* @return array An array of valid permissions for the onthisdate module
*/

function onthisdate_perm() {
  return array(
'access onthisdate content');
}
// function onthisdate_perm()
?>

If you are going to write a module that needs to have finer control over the permissions, and you're going to do permission control (by checking permissions), you should expand this permission set. For example:

<?php
function onthisdate_perm() {

return array(
'access onthisdate content', 'administer onthisdate');

}
// function onthisdate_perm
?>

For this tutorial, start with the first one. We'll later move to the second version.

You'll need to adjust who has permission to view your module on the Administer » User management » Access control permissions page. We'll use the user_access function to check access permissions later (whoa, so many "laters!").

Your permission strings are arbitrary, but each must be unique among all installed modules. Otherwise, one occurrence of the name will take the permissions of the other. The permission strings should each usually contain your module name, since this helps avoid name space conflicts with other modules.

The suggested naming convention for permissions is "action_verb modulename". For example:

<?php
function newmodule_perm() {

return array(
'access newmodule', 'create newmodule', 'administer newmodule');

}
// function newmodule_perm
?>

The setup of the module is now done. You can add the code above to your module file. Next, we'll start generating content.

More information about the permission hook: Drupal 5.x

04. Declare we have block content

Main topic described: A content block
Drupal hook described: hook_block

There are several types of modules: block modules and node modules are two. Block modules create abbreviated content that is typically (but not always, and not required to be) displayed along the left or right side of a page. Node modules generate full page content (such as blog, forum, or book pages).

We'll create a block content to start, and later discuss node content, as well as filtering content. A module can generate content for blocks and also for a full page (the blogs module is a good example of this). The hook for a block module is appropriately called "block", so let's start our next function:

<?php
/**
* Generate HTML for the onthisdate block
* @param op the operation from the URL
* @param delta offset
* @returns block HTML
*/
function onthisdate_block($op='list', $delta=0) {
 
}
// end function onthisdate_block
?>

The block function takes two parameters: the operation and the offset, or delta. The offset allows you to create different content for different blocks, all within the same block function. We'll just worry about the operation at this point. In particular, we care about the specific case where the block is being listed in the blocks page. In all other situations, we'll display the block content.

When the module will be listed on the blocks page, the $op parameter's value will be 'list':

<?php
/**
* Generate HTML for the onthisdate block
* @param op the operation from the URL
* @param delta offset
* @returns block HTML
*/
function onthisdate_block($op='list', $delta=0) {
 
// listing of blocks, such as on the admin/block page
 
if ($op == "list") {
    
$block[0]["info"] = t('On This Date');
     return
$block;
  }

}
// end onthisdate_block
?>

Next, we'll generate the block content.

More information about the block hook:
Drupal HEAD

05. Generate the block content

Main topic described: Creating content through blocks
Drupal hook described: hook_block

Now, we need to generate the 'onthisdate' content for the block. Here we'll demonstrate a basic way to access the database.

Our goal is to get a list of content (stored as "nodes" in the database) created a week ago. Specifically, we want the content created between midnight and 11:59pm on the day one week ago. When a node is first created, the time of creation is stored in the database. We'll use this database field to find our data.

First, we need to calculate the time (in seconds since epoch start, see http://www.php.net/manual/en/function.time.php for more information on time format) for midnight a week ago, and 11:59pm a week ago. This part of the code is Drupal independent, see the PHP website (http://php.net/) for more details.

<?php
/**
* Generate HTML for the onthisdate block
* @param op the operation from the URL
* @param delta offset
* @returns block HTML
*/
function onthisdate_block($op='list', $delta=0) {

 
// listing of blocks, such as on the admin/block page
 
if ($op == "list") {
   
$block[0]["info"] = t('On This Date');
    return
$block;
  } else if (
$op == 'view') {

   
// our block content
    // Get today's date
   
$today = getdate();

   
// calculate midnight one week ago
   
$start_time = mktime(0, 0, 0,
                        
$today['mon'], ($today['mday'] - 7), $today['year']);

   
// we want items that occur only on the day in question, so  
    // calculate 1 day
   
$end_time = $start_time + 86400
   
// 60 * 60 * 24 = 86400 seconds in a day
   
...
  }
}
?>

The next step is the SQL statement that will retrieve the content we'd like to display from the database. We're selecting content from the node table, which is the central table for Drupal content. We'll get all sorts of content type with this query: blog entries, forum posts, etc. For this tutorial, this is okay. For a real module, you would adjust the SQL statement to select specific types of content (by adding the 'type' column and a WHERE clause checking the 'type' column).

Note: the table name is enclosed in curly braces: {node}. This is necessary so that your module will support database table name prefixes. You can find more information on the Drupal website by reading the Table Prefix (and sharing tables across instances) page in the Drupal handbook.

<?php
$query
= "SELECT nid, title, created FROM " .
        
"{node} WHERE created >= '" . $start_time .
        
"' AND created <= '". $end_time . "'";
?>

Drupal uses database helper functions to perform database queries. This means that, for the most part, you can write your database SQL statement and not worry about the backend connections.

We'll use db_query() to get the records (i.e. the database rows) that match our SQL query, and db_fetch_object() to look at the individual records:

<?php
 
// get the links
 
$queryResult db_query($query);

 
// content variable that will be returned for display   
 
$block_content = ''
  while (
$links = db_fetch_object($queryResult)) {
   
$block_content .=  l($links->title, 'node/' . $links->nid) . '<br />';
  }

 
// check to see if there was any content before setting up
  //  the block 
 
if ($block_content == '') {   
   
/* No content from a week ago.  If we return nothing, the block  
     * doesn't show, which is what we want. */
   
return;
  }

 
// set up the block 
 
$block['subject'] = 'On This Date'
 
$block['content'] = $block_content;
  return
$block;
}
?>

Notice the actual URL is enclosed in the l() function. l generates <a href="link"> links, adjusting the URL to the installation's URL configuration of either clean URLS: http://(sitename)/node/2 or not http://(sitename)/?q=node/2

Also, we return an array that has 'subject' and 'content' elements. This is what Drupal expects from a block function. If you do not include both of these, the block will not render properly.

You may also notice the bad coding practice of combining content with layout. If you are writing a module for others to use, you will want to provide an easy way for others (in particular, non-programmers) to adjust the content's layout. An easy way to do this is to include a class attribute in your link, or surround the HTML with a <div> tag with a module specific CSS class and not necessarily include the <br /> at the end of the link. Let's ignore this for now, but be aware of this issue when writing modules that others will use.

Putting it all together, our block function at this point looks like this:

<?php
function onthisdate_block($op='list', $delta=0) {
 
// listing of blocks, such as on the admin/block page
 
if ($op == "list") {
   
$block[0]["info"] = t("On This Date");
    return
$block;
  } else if (
$op == 'view') {
 
// our block content
    // content variable that will be returned for display
   
$block_content = '';

   
// Get today's date
   
$today = getdate();

   
// calculate midnight one week ago
   
$start_time = mktime(0, 0, 0,$today['mon'],
                               (
$today['mday'] - 7), $today['year']);

   
// we want items that occur only on the day in question, so
    //calculate 1 day
   
$end_time = $start_time + 86400;
   
// 60 * 60 * 24 = 86400 seconds in a day

   
$query = "SELECT nid, title, created FROM " .
            
"{node} WHERE created >= '" . $start_time .
            
"' AND created <= '". $end_time . "'";

   
// get the links
   
$queryResult db_query($query);
    while (
$links = db_fetch_object($queryResult)) {
     
$block_content .= l($links->title, 'node/'.$links->nid) . '<br />';
    }
   
// check to see if there was any content before setting up the block
   
if ($block_content == '') {
     
// no content from a week ago, return nothing.
     
return;
    }
   
// set up the block
   
$block['subject'] = 'On This Date';
   
$block['content'] = $block_content;
    return
$block;
  }
}
?>

Add the code above to your module file. Our module is now functional - we can install, enable and test it.

06. Installing, enabling and testing the module

At this point, you can install your module and it'll work. Let's do that, and see where we need to improve the module.

To install the module, you'll need to copy your onthisdate.module file to the modules directory of your Drupal installation (which itself can be a subdirectory of your Drupal install or a subdirectory of sites/all or if you are coding a site specific module for a multisite install then sites/hostname). The file must be installed in this directory or a subdirectory of the modules directory, and must have the .module name extension and must have a correspoding .info file.

Log in as your site administrator, and navigate to the modules administration page to get an alphabetical list of modules. In the menus: administer » modules, or via URL:

http://.../admin/modules or
http://.../?q=admin/modules

When you scroll down, you'll see the onthisdate module listed with the description next to it.

Enable the module by selecting the checkbox and save your configuration.

Because the module is a blocks module, we'll need to also enable it in the blocks administration menu and specify a location for it to display. Node modules may or may not need further configuration depending on the module. Any module can have settings, which affect the functionality/display of a module. We'll discuss settings later. For now, navigate to the blocks administration page: admin/block or administer » blocks in the menus.

Enable the module by selecting the enabled checkbox for the 'On This Date' block and save your blocks. Be sure to adjust the location (left/right) if you are using a theme that limits where blocks are displayed.

Now, head to another page, say, select the modules menu. In some themes, the blocks are displayed after the page has rendered the content, and you won't see the change until you go to new page.

If you have content that was created a week ago, the block will display with links to the content. If you don't have content, you'll need to fake some data. You can do this by creating a blog, forum topic or book page, and adjust the "Authored on:" date to be a week ago.

Alternately, if your site has been around for a while, you may have a lot of content created on the day one week ago, and you'll see a large number of links in the block.

07. Create a module configuration (settings) page

Main topic described: Module settings
Drupal hook used: hook_menu

Now that we have a working module, we'd like to make it better. If we have a site that has been around for a while, content from a week ago might not be as interesting as content from a year ago. Similarly, if we have a busy site, we might not want to display all the links to content created last week. So, let's create a configuration page for the administrator to adjust this information.

(Note: This was done using hook_settings in Drupal 4.7 which no longer exists in Drupal 5)

Create the configuration function

We'd like to configure how many links display in the block, so we'll create a form for the administrator to set the number of links. This is done in our function onthisdate_admin. Note that _admin is not a hook and we could have used whatever we wanted there.

<?php
function onthisdate_admin() {

 
$form['onthisdate_maxdisp'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Maximum number of links'),
   
'#default_value' => variable_get('onthisdate_maxdisp', 3),
   
'#size' => 2,
   
'#maxlength' => 2,
   
'#description' => t("The maximum number of links to display in the block.")
  );

  return
system_settings_form($form);
}
?>

This function uses several powerful Drupal form handling features. We don't need to worry about creating an HTML text field or the form, as Drupal will do so for us for this settings page. We use variable_get to retrieve the value of the system configuration variable "onthisdate_maxdisp", and define the default value to be 3. We use an array ( named $form here ) to create the form which contains a textfield of size 2 ( #size ), accepting a maximum length of 2 characters ( #maxlength ). We also use the translate function of t(). The system_settings_form() function adds default buttons to a form and set its prefix.

Refer to Drupal Forms API Reference and
Drupal Forms API Quickstart Guide for more detailed information on what more you can do with Drupal Form API.

When you save a settings variable for any module, the variable (in our case, 'onthisdate_maxdisp') and the value is stored in the variables table. Programmatically, you can retrieve the values with the
variable_get('variable_name', default_value)
function.

Of course, we'll need to use the configuration value in our SQL SELECT, so we'll need to adjust our query statement in the onthisdate_block function. One way to do this is with a LIMIT value in our query:

<?php
  $limitnum
= variable_get('onthisdate_maxdisp', 3);
 
$query = "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= '" . $start_time .
          
"' AND created <= '". $end_time . "' LIMIT " . $limitnum;
?>

However, this method may or may not translate across databases (really). Better to use one of Drupal's select methods. In this case, let's leave the original query the same, and call db_query_range:
<?php
  $limitnum
= variable_get("onthisdate_maxdisp", 3);

 
$query = "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= %d " .
          
"AND created <= %d";

 
$queryResult = db_query_range($query, $start_time, $end_time, 0, $limitnum);
?>
Add the page to hook_menu

Once you have created the function with your settings, the page is added with a callback to the function which you specify in hook_menu. In hook_menu we will return an array which describes to Drupal which URL path to use, the title to display, the function to use and the permissions required.

We would like only administrators to be able to access this page, so we'll place the permissions check for the module here in hook_menu so that Drupal can itself check the appropriate permission. To minimize the number of permissions an administrator has to deal with, we're going to use the global administration permission for administrating our module instead of creating a new custom permission.

<?php
function onthisdate_menu() {

 
$items = array();

 
$items[] = array(
   
'path' => 'admin/settings/onthisdate',
   
'title' => t('On this date module settings'),
   
'description' => t('Description of your On this date settings control'),
   
'callback' => 'drupal_get_form',
   
'callback arguments' => 'onthisdate_admin',
   
'access' => user_access('access administration pages'),
   
'type' => MENU_NORMAL_ITEM,
   );

  return
$items;
}
?>

You can test the settings page by editing the number of links displayed and noticing the block content adjusts accordingly. Navigate to the settings page: admin/settings/onthisdate or administer » settings » onthisdate. (If the page doesn't exist, you may have to disable and enable the module for the system to register the new settings page.) Adjust the number of links and save the configuration. Notice the number of links in the block adjusts accordingly.

Note: We don't have any validation with this input. If you enter "c" in the maximum number of links, you'll break the block.

More information on hook_menu:
Drupal 5 hook_menu

08. Generate a page content

Main topic described: Displaying content

So far we have our working block and a settings page. The block displays a maximum number of links. However, there may be more links than the maximum we show. So, let's create a page that lists all the content that was created a week ago.

<?php
function onthisdate_all() {}
?>

We're going to use much of the code from the block function. We'll write this ExtremeProgramming style, and duplicate the code. If we need to use it in a third place, we'll refactor it into a separate function. For now, copy the code to the new function onthisdate_all(). Contrary to all our other functions, 'all', in this case, is not a Drupal hook.

If you want to call this function from another module, use the standard naming scheme we've been using: modulename_action. It can be called using the function module_invoke function. If you want the function to remain private (because, say, it's merely a helper function in your module) and easily accessible by only your module, prefix the function name with an underscore. We want the former.

<?php
function onthisdate_all() {
 
// content variable that will be returned for display
 
$page_content = '';

 
// Get today's date
 
$today = getdate();

 
// calculate midnight one week ago
 
$start_time = mktime(0, 0, 0, $today['mon'], ($today['mday'] - 7), $today['year']);

 
// we want items that occur only on the day in question,
  // so calculate 1 day
 
$end_time = $start_time + 86400;
 
// 60 * 60 * 24 = 86400 seconds in a day

 
$query = "SELECT nid, title, created FROM " .
          
"{node} WHERE created >= '" . $start_time .
          
"' AND created <= '". $end_time . "'";

 
// get the links (no range limit here)
 
$queryResult db_query($query);
  while (
$links = db_fetch_object($queryResult)) {
   
$page_content .= l($links->title, 'node/'.$links->nid).'<br />';
  }
  ...
}
?>

We have the page content at this point. As noted before, we're including layout in the code. This is bad, and should be avoided. It is, however, the topic of another tutorial, so for now, we'll include the formatting in our content.

The rest of our function checks to see if there is content and lets the user know. This is preferable to showing an empty or blank page, which may confuse the user.

Note that we simply return the content, and Drupal displays it within a themed page.

<?php
function onthisdate_all() {

  ...

 
// check to see if there was any content before
  // setting up the block
 
if ($page_content == '') {
   
// no content from a week ago, let the user know
   
$page_content = "No events occurred on this site on this date in history.";
  }

  return
$page_content;
}
?>

Even though we have this function that will output links to the content generated a week ago, we haven't specified what URL will cause this page to render. We'll do that next.

09. Letting Drupal know about the new function

Main topic described: Using Drupal Menu system
Drupal hook described: hook_menu

As mentioned previously, the function we just wrote isn't a 'hook': it's not a Drupal recognized name. We need to tell Drupal how to access the function when displaying a page. We do this with Drupal's hook_menu. Remember that we have already used hook_menu in here. The hook_menu defines the association between a URL and the function that creates the content for that url. The hook also does permission checking, if desired. We will use the hook_menu made earlier and continue with it.

<?php
function onthisdate_menu() {

 
$items = array();

 
//this was created earlier in tutorial 7.
 
$items[] = array(
   
'path' => 'admin/settings/onthisdate',
   
'title' => t('On this date module settings'),
   
'callback' => 'drupal_get_form',
   
'callback arguments' => 'onthisdate_admin',
   
'access' => user_access('access administration pages'),
   
'type' => MENU_NORMAL_ITEM,
   );

 
//this is added for this current tutorial.
 
$items[] = array(
   
'path' => 'onthisdate',
   
'title' => t('on this date'),
   
'callback' => 'onthisdate_all',
   
'access' => user_access('access onthisdate content'),
   
'type' => MENU_CALLBACK
 
);

  return
$items;
}
?>

Basically, we're saying if the user goes to "onthisdate" (either via ?q=onthisdate or http://.../onthisdate), the content generated by onthisdate_all will be displayed. The title of the page will be "on this date". The type MENU_CALLBACK tells Drupal to not display the link in the user's menu, just use this function when the URL is accessed. Use MENU_NORMAL_ITEM if you want the user to see the link in the side navigation block.

More information on the menu system:
Drupal 5.x

As mentioned before, the menu hook can handle permission checking before rendering the page. The 'access' entry in the menu item array is where this check is done. If you added a value in your permissions array in the perm hook function, you can use that string as a parameter in the user_access function. If the user isn't in a role that has that permission, the page will not render for the user.

If the module has not be enabled, enable it. If you have already enabled it, in order to reset the menu definitions in the system, you'll need to disable, then reenable it.

Now, navigate to /onthisdate (or ?q=onthisdate) and see what you get.

10. Adding a 'more' link and showing all entries

We now have a function that creates a page with all the content created a week ago. Let's link to it from the block with a "more" link.

Add these lines just before that $block['subject'] line. These lines will add the more link to the end of the $block_content variable before saving it to the $block['content'] variable:

<?php
// add a more link to our page that displays all the links
 
$block_content .=
   
"<div class=\"more-link\">".
   
l(
     
t("more"),
     
"onthisdate",
      array(
       
"title" => t("More events on this day.")
      )
    ).
"</div>";
?>

This will add the more link to the block. Note the extra parameters used in the l() function. You can add additional values, such as 'class', in the array to customize the link.

More information on the l() function:
Drupal HEAD

Conclusion

We now have a working module. It created a block and a page. You should now have enough to get started writing your own modules. We recommend you start with a block module of your own and move onto a node module. Alternately, you can write a filter or theme.

As is, this tutorial's module isn't very useful. However, with a few enhancements, it can be entertaining. Try modifying the select query statement to select only nodes of type 'blog' and see what you get. Alternately, you could get only a particular user's content for a specific week. Instead of using the block function, consider expanding the menu and page functions, adding menus to specific entries or dates, or using the menu callback arguments to adjust what year you look at the content from.

If you start writing modules for others to use, you'll want to provide more details in your code. Comments in the code are incredibly valuable for other developers and users in understanding what's going on in your module. You'll also want to expand the help function, providing better help for the user. Follow the Drupal [Coding standards], especially if you're going to add your module to the project.

Two topics very important in module development are writing themeable pages and writing translatable content. Please check the Drupal Handbook for more details on these two subjects.

dummy template module with all hooks for drupal 5

Well most of them. The list taken from api.drupal.org/hooks and the docs and examples too. handy, makes starting a new module easier. modifying an existing one is a pain, having to remove crap before you can add your own.

I don't seem to be able to attach a file, so here it is:

<?php
// replace "function hook_" with "function mymodule_"
// uncomment and edit functions required


/* hook_access

Define access restrictions.

This hook allows node modules to limit access to the node types they define.

The administrative account (user ID #1) always passes any access check, so this hook is not called in that case. If
this hook is not defined for a node type, all access checks will fail, so only the administrator will be able to see
content of that type. However, users with the "administer nodes" permission may always view and edit content through
the administrative interface.

Parameters

$op The operation to be performed. Possible values:
    * "create"
    * "delete"
    * "update"
    * "view"
$node The node on which the operation is to be performed, or, if it does not yet exist, the type of node to be created.

Return value

TRUE if the operation may be performed;
FALSE if the operation may not be returned;
NULL to not override the settings in the node_access table.

*/


function hook_access($op, $node){}


/* hook_auth

Verify authentication of a user.

The _auth hook is the heart of any authentication module. This function is called whenever a user is attempting to
log in using your authentication module. The module uses this information to allow or deny access to the site.

Parameters

$username The substring before the final '@' character in the username field.
$password The whole string submitted by the user in the password field.
$server The substring after the final '@' symbol in the username field.

Return value

For successful authentications, this function returns TRUE.
Otherwise, it returns FALSE.

*/

/*
function hook_auth($username, $password, $server) {
}
*/

function hook_auth($username, $password, $server) {}


/* hook_block

Declare a block or set of blocks.

Any module can export a block (or blocks) to be displayed by defining the _block hook. This hook is called by
theme.inc to display a block, and also by block.module to procure the list of available blocks.

As of 4.7, all block properties (except theme) can be set in hook_block's 'view' operation. You can give your
blocks an explicit weight, enable them, limit them to given pages, etc. These settings will be registered when
the block is first loaded at admin/block, and from there can be changed manually via block administration.

Note that if you set a region that isn't available in a given theme, the block will be registered instead to
that theme's default region (the first item in the _regions array).

The functions mymodule_display_block_1 and 2, as used in the example, should of course be defined somewhere in
your module and return the content you want to display to your users. If the "content" element is empty, no
block will be displayed even if "subject" is present.

After completing your blocks, do not forget to enable them in the block admin menu.

For a detailed usage example, see block_example.module.

Parameters

$op What kind of information to retrieve about the block or blocks. Possible values:
    * 'list': A list of all blocks defined by the module.
    * 'configure': A configuration form.
    * 'save': Save the configuration options.
    * 'view': Information about a particular block and default settings.
$delta Which block to return (not applicable if $op is 'list'). Although it is most commonly an integer starting at 0,
    this is not mandatory. For instance, aggregator.module uses string values for $delta
$edit If $op is 'save', the submitted form data from the configuration form.

Return value

If $op is 'list', return an array of arrays, each of which must define an 'info' element describing the block.
If $op is 'configure', optionally return a string containing the configuration form. If $op is 'save', return nothing,
If $op is 'view', return an array which must define a 'subject' element and a 'content' element defining the block
indexed by $delta.

*/

/*
function hook_block($op = 'list', $delta = 0, $edit = array()) {
  if ($op == 'list') {
    $blocks[0] = array('info' => t('XXX block #1 shows ...'),
      'weight' => 0, 'enabled' => 1, 'region' => 'left');
    $blocks[1] = array('info' => t('XXX block #2 describes ...'),
      'weight' => 0, 'enabled' => 0, 'region' => 'right');
    return $blocks;
  }
  else if ($op == 'configure' && $delta == 0) {
    $form['items'] = array(
      '#type' => 'select',
      '#title' => t('Number of items'),
      '#default_value' => variable_get('XXX_block_items', 0),
      '#options' => array('1', '2', '3'),
    );
    return $form;
  }
  else if ($op == 'save' && $delta == 0) {
    variable_set('XXX_block_items', $edit['items']);
  }
  else if ($op == 'view') {
    switch($delta) {
      case 0:
        $block = array('subject' => t('Title of block #1'),
          'content' => "XXX");
        break;
      case 1:
        $block = array('subject' => t('Title of block #2'),
          'content' => "XXX");
        break;
    }
    return $block;
  }
}
*/

function hook_block($op = 'list', $delta = 0, $edit = array()) {}


/* hook_comment

Act on comments.

This hook allows modules to extend the comments system.

Parameters

$a1 Dependent on the action being performed.
    * For "form", passes in the comment form.
    * For "validate","update","insert", passes in an array of form values submitted by the user.
    * For all other operations, passes in the comment the action is being performed on.

$op What kind of action is being performed. Possible values:
    * "insert": The comment is being inserted.
    * "update": The comment is being updated.
    * "view": The comment is being viewed. This hook can be used to add additional data to the comment before theming.
    * "form": The comment form is about to be shown. Modules may add fields to the form at this point.
    * "validate": The user has just finished editing the comment and is trying to preview or submit it. This hook can be used to check or even modify the node. Errors should be set with form_set_error().
    * "publish": The comment is being published by the moderator.
    * "unpublish": The comment is being unpublished by the moderator.
    * "delete": The comment is being deleted by the moderator.

Return value - Dependent on the action being performed.

    * For "form", an array of form elements to add to the comment form.
    * For all other operations, nothing.

*/

/*
function hook_comment($a1, $op) {
  if ($op == 'insert' || $op == 'update') {
    $nid = $a1['nid'];
  }
  cache_clear_all_like(drupal_url(array('id' => $nid)));
}
*/

function hook_comment($a1, $op) {}


/* hook_cron

Perform periodic actions.

Modules that require to schedule some commands to be executed at regular intervals can implement hook_cron().
The engine will then call the hook at the appropriate intervals defined by the administrator. This interface
is particularly handy to implement timers or to automate certain tasks. Database maintenance, recalculation
of settings or parameters, and automatic mailings are good candidates for cron tasks.

This hook will only be called if cron.php is run (e.g. by crontab).

Return value

None.

*/

/*
function hook_cron() {
  $result = db_query('SELECT * FROM {site} WHERE checked = 0 OR checked + refresh < %d', time());
  while ($site = db_fetch_array($result)) {
    cloud_update($site);
  }
}
*/

function hook_cron() {}


/* hook_delete

Respond to node deletion.

This is a hook used by node modules. It is called to allow the module to take action when a node is being
deleted from the database by, for example, deleting information from related tables.

To take action when nodes of any type are deleted (not just nodes of the type defined by this module),
use hook_nodeapi() instead.

For a detailed usage example, see node_example.module.

Parameters
&$node The node being deleted.

Return value

None.

*/

/*
function hook_delete(&$node) {
  db_query('DELETE FROM {mytable} WHERE nid = %d', $node->nid);
}
*/

function hook_delete(&$node) {}


/* hook_disable

Perform necessary actions before module is disabled.

The hook is called everytime module is disabled.

*/

/*
function hook_disable() {
  mymodule_cache_rebuild();
}
*/

function hook_disable() {}


/* hook_elements

Allows modules to declare their own form element types and specify their default values.

Return value

An array of element types

*/

/*
function hook_elements() {
  $type['filter_format'] = array('#input' => TRUE);
  return $type;
}
*/

function hook_elements() {}


/* hook_enable

Perform necessary actions before module is enabled.

The hook is called everytime module is enabled.

*/

/*
function hook_enable() {
  mymodule_cache_rebuild();
}
*/

function hook_enable() {}


/* hook_exit

Perform cleanup tasks.

This hook is run at the end of each page request. It is often used for page logging and printing
out debugging information.

Only use this hook if your code must run even for cached page views. If you have code which must
run once on all non cached pages, use hook_menu(!$may_cache) instead. Thats the usual case. If you
implement this hook and see an error like 'Call to undefined function', it is likely that you are
depending on the presence of a module which has not been loaded yet. It is not loaded because Drupal
is still in bootstrap mode. The usual fix is to move your code to hook_menu(!$may_cache).
Parameters

$destination If this hook is invoked as part of a drupal_goto() call, then this argument will be a
fully-qualified URL that is the destination of the redirect. Modules may use this to react appropriately;
for example, nothing should be output in this case, because PHP will then throw a "headers cannot be
modified" error when attempting the redirection.

Return value

None.

*/

/*
function hook_exit($destination = NULL) {
  db_query('UPDATE {counter} SET hits = hits + 1 WHERE type = 1');
}
*/

function hook_exit($destination = NULL) {}


/* hook_file_download:

Allow file downloads.

Parameters

$file String of the file's path.

Return value

If the user does not have permission to access the file, return -1.
If the user has permission, return an array with the appropriate headers.

*/

/*
function hook_file_download($file) {
  if (user_access('access content')) {
    if ($filemime = db_result(db_query("SELECT filemime FROM {fileupload} WHERE filepath = '%s'", file_create_path($file)))) {
      return array('Content-type:' . $filemime);
    }
  }
  else {
    return -1;
  }
}
*/

function hook_file_download($file) {}


/* hook_filter

Define content filters.

Content in Drupal is passed through all enabled filters before it is output. This lets a module modify
content to the site administrator's liking.

This hook contains all that is needed for having a module provide filtering functionality.

Depending on $op, different tasks are performed.

A module can contain as many filters as it wants. The 'list' operation tells the filter system which
filters are available. Every filter has a numerical 'delta' which is used to refer to it in every operation.

Filtering is a two-step process. First, the content is 'prepared' by calling the 'prepare' operation for
every filter. The purpose of 'prepare' is to escape HTML-like structures. For example, imagine a filter
which allows the user to paste entire chunks of programming code without requiring manual escaping of
special HTML characters like @< or @&. If the programming code were left untouched, then other filters
could think it was HTML and change it. For most filters however, the prepare-step is not necessary, and
they can just return the input without changes.

Filters should not use the 'prepare' step for anything other than escaping, because that would short-circuits
the control the user has over the order in which filters are applied.

The second step is the actual processing step. The result from the prepare-step gets passed to all the
filters again, this time with the 'process' operation. It's here that filters should perform actual
changing of the content: transforming URLs into hyperlinks, converting smileys into images, etc.

An important aspect of the filtering system are 'input formats'. Every input format is an entire
filter setup: which filters to enable, in what order and with what settings. Filters that provide settings
should usually store these settings per format.

If the filter's behaviour depends on an extensive list and/or external data (e.g. a list of smileys, a list
of glossary terms) then filters are allowed to provide a separate, global configuration page rather than
provide settings per format. In that case, there should be a link from the format-specific settings to
the separate settings page.

For performance reasons content is only filtered once; the result is stored in the cache table and retrieved
the next time the piece of content is displayed. If a filter's output is dynamic it can override the cache
mechanism, but obviously this feature should be used with caution: having one 'no cache' filter in a
particular input format disables caching for the entire format, not just for one filter.

Beware of the filter cache when developing your module: it is advised to set your filter to 'no cache' while
developing, but be sure to remove it again if it's not needed. You can clear the cache by running the SQL
query 'DELETE FROM cache';

For a detailed usage example, see filter_example.module. For an example of using multiple filters in one
module, see filter_filter() and filter_filter_tips().
Parameters

$op Which filtering operation to perform. Possible values:

    * list: provide a list of available filters. Returns an associative array of filter names with numerical keys.
    These keys are used for subsequent operations and passed back through the $delta parameter.
    * no cache: Return true if caching should be disabled for this filter.
    * description: Return a short description of what this filter does.
    * prepare: Return the prepared version of the content in $text.
    * process: Return the processed version of the content in $text.
    * settings: Return HTML form controls for the filter's settings. These settings are stored with variable_set()
    when the form is submitted. Remember to use the $format identifier in the variable and control names to store
    settings per input format (e.g. "mymodule_setting_$format").

$delta Which of the module's filters to use (applies to every operation except 'list'). Modules that only contain one
    filter can ignore this parameter.

    * $format Which input format the filter is being used in (applies to 'prepare', 'process' and 'settings').

$text The content to filter (applies to 'prepare' and 'process').

Return value

The return value depends on $op. The filter hook is designed so that a module can return $text for operations
it does not use/need.

*/

/*
function hook_filter($op, $delta = 0, $format = -1, $text = '') {
  switch ($op) {
    case 'list':
      return array(0 => t('Code filter'));

    case 'description':
      return t('Allows users to post code verbatim using &lt;code&gt; and &lt;?php ?&gt; tags.');

    case 'prepare':
      // Note: we use the bytes 0xFE and 0xFF to replace < > during the
      // filtering process. These bytes are not valid in UTF-8 data and thus
      // least likely to cause problems.
      $text = preg_replace('@<code>(.+?)</code>@se', "'\xFEcode\xFF'. codefilter_escape('\\1') .'\xFE/code\xFF'", $text);
      $text = preg_replace('@<(\?(php)?|%)(.+?)(\?|%)>@se', "'\xFEphp\xFF'. codefilter_escape('\\3') .'\xFE/php\xFF'", $text);
      return $text;

    case "process":
      $text = preg_replace('@\xFEcode\xFF(.+?)\xFE/code\xFF@se', "codefilter_process_code('$1')", $text);
      $text = preg_replace('@\xFEphp\xFF(.+?)\xFE/php\xFF@se', "codefilter_process_php('$1')", $text);
      return $text;

    default:
      return $text;
  }
}
*/

function hook_filter($op, $delta = 0, $format = -1, $text = '') {}


/* hook_filter_tips

Provide tips for using filters.

A module's tips should be informative and to the point. Short tips are preferably one-liners.

Parameters

$delta Which of this module's filters to use. Modules which only implement one filter can ignore this parameter.

$format Which format we are providing tips for.

$long If set to true, long tips are requested, otherwise short tips are needed.

Return value

The text of the filter tip.

*/

/*
function hook_filter_tips($delta, $format, $long = false) {
  if ($long) {
    return t('To post pieces of code, surround them with &lt;code&gt;...&lt;/code&gt; tags. For PHP code, you can use &lt;?php ... ?&gt;, which will also colour it based on syntax.');
  }
  else {
    return t('You may post code using &lt;code&gt;...&lt;/code&gt; (generic) or &lt;?php ... ?&gt; (highlighted PHP) tags.');
  }
}
*/

function hook_filter_tips($delta, $format, $long = false) {}


/* hook_footer

Insert closing HTML.

This hook enables modules to insert HTML just before the \</body\> closing tag of web pages. This is useful for
including javascript code and for outputting debug information.

Parameters

$main Whether the current page is the front page of the site.

Return value

The HTML to be inserted.

*/

/*
function hook_footer($main = 0) {
  if (variable_get('dev_query', 0)) {
    return '<div style="clear:both;">'. devel_query_table() .'</div>';
  }
}
*/

function hook_footer($main = 0) {}


/* hook_form

Display a node editing form.

This hook, implemented by node modules, is called to retrieve the form that is displayed when one attempts to "create/edit" an item. This form is displayed at the URI http://www.example.com/?q=node/<add|edit>/nodetype.

The submit and preview buttons, taxonomy controls, and administrative accoutrements are displayed automatically by node.module. This hook needs to return the node title, the body text area, and fields specific to the node type.

For a detailed usage example, see node_example.module.

Parameters

&$node The node being added or edited.

&$param The hook can set this variable to an associative array of attributes to add to the enclosing \<form\> tag.

Return value

An array containing the form elements to be displayed in the node edit form.

*/

/*
function hook_form(&$node, &$param) {
  $type = node_get_types('type', $node);

  $form['title'] = array(
    '#type'=> 'textfield',
    '#title' => check_plain($type->title_label),
    '#required' => TRUE,
  );
  $form['body'] = array(
    '#type' => 'textarea',
    '#title' => check_plain($type->body_label),
    '#rows' => 20,
    '#required' => TRUE,
  );
  $form['field1'] = array(
    '#type' => 'textfield',
    '#title' => t('Custom field'),
    '#default_value' => $node->field1,
    '#maxlength' => 127,
  );
  $form['selectbox'] = array(
    '#type' => 'select',
    '#title' => t('Select box'),
    '#default_value' => $node->selectbox,
    '#options' => array(
      1 => 'Option A',
      2 => 'Option B',
      3 => 'Option C',
    ),
    '#description' => t('Please choose an option.'),
  );

  return $form;
}
*/

function hook_form(&$node, &$param) {}


/* hook_forms
Map form_ids to builder functions.

This hook allows modules to build multiple forms from a single form "factory" function but each form will
have a different form id for submission, validation, theming or alteration by other modules.

The callback arguments will be passed as parameters to the function. Callers of drupal_get_form() are also
able to pass in parameters. These will be appended after those specified by hook_forms().

See node_forms() for an actual example of how multiple forms share a common building function.

Return value

An array keyed by form id with callbacks and optional, callback arguments.

*/

/*
function hook_forms() {
  $forms['mymodule_first_form'] = array(
    'callback' => 'mymodule_form_builder',
    'callback arguments' => array('some parameter'),
  );
  $forms['mymodule_second_form'] = array(
    'callback' => 'mymodule_form_builder',
  );
  return $forms;
}
*/

function hook_forms() {}


/* hook_form_alter

Perform alterations before a form is rendered. One popular use of this hook is to add form
elements to the node form.

The node object of the node whose form you are altering can be retrieved at $form['#node'].

Parameters

$form_id String representing the name of the form itself. Typically this is the name of the
    function that generated the form.

$form Nested array of form elements that comprise the form.

Return value

None.

*/

/*
function hook_form_alter($form_id, &$form) {
  if (isset($form['type']) && $form['type']['#value'] .'_node_settings' == $form_id) {
    $form['workflow']['upload_'. $form['type']['#value']] = array(
      '#type' => 'radios',
      '#title' => t('Attachments'),
      '#default_value' => variable_get('upload_'. $form['type']['#value'], 1),
      '#options' => array(t('Disabled'), t('Enabled')),
    );
  }
}
*/

function hook_form_alter($form_id, &$form) {}


/* hook_help

Provide online user help.

By implementing hook_help(), a module can make documentation available to the engine or to other modules. All user help should be returned using this hook; developer help should be provided with Doxygen/api.module comments.

For a detailed usage example, see page_example.module.
Parameters

$section Drupal URL path (or: menu item) the help is being requested for, e.g. admin/node or user/edit. Recognizes special descriptors after a "#" sign. Some examples:

    * admin/modules#name The name of a module (unused, but there)
    * admin/modules#description The description found on the admin/system/modules page.
    * admin/help#modulename The module's help text, displayed on the admin/help page and through the module's individual help link.
    * user/help#modulename The help for a distributed authorization module (if applicable).
    * node/add#nodetype The description of a node type (if applicable).

Return value

A localized string containing the help text. Every web link, l(), or url() must be replaced with %something and put into the final t() call: $output .= 'A role defines a group of users that have certain privileges as defined in !permission.'; $output = t($output, array('!permission' => l(t('user permissions'), 'admin/user/permission')));

*/

/*
function hook_help($section) {
  switch ($section) {
    case 'admin/help#block':
      return t('<p>Blocks are the boxes visible in the sidebar(s)
        of your web site. These are usually generated automatically by
        modules (e.g. recent forum topics), but you can also create your
        own blocks using either static HTML or dynamic PHP content.</p>');
      break;
    case 'admin/modules#description':
      return t('Controls the boxes that are displayed around the main content.');
      break;
  }
}
*/

function hook_help($section) {}


/* hook_info

Declare authentication scheme information.

This hook is required of authentication modules. It defines basic information about the authentication scheme.

Parameters

$field The type of information requested. Possible values:

    * "name"
    * "protocol"

Return value

A string containing the requested piece of information. If $field is not provided, an array containing all
the fields should be returned.

*/

/*
function hook_info($field = 0) {
  $info['name'] = 'Drupal';
  $info['protocol'] = 'XML-RPC';

  if ($field) {
    return $info[$field];
  }
  else {
    return $info;
  }
}
*/

function hook_info($field = 0) {}


/* hook_init

Perform setup tasks.

This hook is run at the beginning of the page request. It is typically used to set up global parameters
which are needed later in the request.

Only use this hook if your code must run even for cached page views. If you have code which must run once
on all non cached pages, use hook_menu(!$may_cache) instead. Thats the usual case. If you implement this
hook and see an error like 'Call to undefined function', it is likely that you are depending on the presence
of a module which has not been loaded yet. It is not loaded because Drupal is still in bootstrap mode. The
usual fix is to move your code to hook_menu(!$may_cache).

Return value

None.

*/

/*
function hook_init() {
  global $recent_activity;

  if ((variable_get('statistics_enable_auto_throttle', 0)) &&
    (!rand(0, variable_get('statistics_probability_limiter', 9)))) {

    $throttle = throttle_status();
    // if we're at throttle level 5, we don't do anything
    if ($throttle < 5) {
      $multiplier = variable_get('statistics_throttle_multiplier', 60);
      // count all hits in past sixty seconds
      $result = db_query('SELECT COUNT(timestamp) AS hits FROM
        {accesslog} WHERE timestamp >= %d', (time() - 60));
      $recent_activity = db_fetch_array($result);
      throttle_update($recent_activity['hits']);
    }
  }
}
*/

function hook_init() {}


/* hook_insert

Respond to node insertion.

This is a hook used by node modules. It is called to allow the module to take action when a new node is
being inserted in the database by, for example, inserting information into related tables.

To take action when nodes of any type are inserted (not just nodes of the type(s) defined by this module),
use hook_nodeapi() instead.

For a detailed usage example, see node_example.module.

Parameters

$node The node being inserted.

Return value

None.

*/

/*
function hook_insert($node) {
  db_query("INSERT INTO {mytable} (nid, extra)
    VALUES (%d, '%s')", $node->nid, $node->extra);
}
*/

function hook_insert($node) {}


/* hook_install

Install the current version of the database schema.

The hook will be called the first time a module is installed, and the module's schema version will be
set to the module's greatest numbered update hook. Because of this, anytime a hook_update_N() is added
to the module, this function needs to be updated to reflect the current version of the database schema.

Table names in the CREATE queries should be wrapped with curly braces so that they're prefixed correctly,
see db_prefix_tables() for more on this.

Note that since this function is called from a full bootstrap, all functions (including those in modules
enabled by the current page request) are available when this hook is called. Use cases could be displaying
a user message, or calling a module function necessary for initial setup, etc.

*/

/*
function hook_install() {
  switch ($GLOBALS['db_type']) {
    case 'mysql':
    case 'mysqli':
      db_query("CREATE TABLE {event} (
                  nid int(10) unsigned NOT NULL default '0',
                  event_start int(10) unsigned NOT NULL default '0',
                  event_end int(10) unsigned NOT NULL default '0',
                  timezone int(10) NOT NULL default '0',
                  PRIMARY KEY (nid),
                  KEY event_start (event_start)
                );"
      );
      break;

    case 'pgsql':
      db_query("CREATE TABLE {event} (
                  nid int NOT NULL default '0',
                  event_start int NOT NULL default '0',
                  event_end int NOT NULL default '0',
                  timezone int NOT NULL default '0',
                  PRIMARY KEY (nid)
                );"
      );
      break;
  }
}
*/

function hook_install() {}


/* hook_link

Define internal Drupal links.

This hook enables modules to add links to many parts of Drupal. Links may be added in nodes or in
the navigation block, for example.

The returned array should be a keyed array of link entries. Each link can be in one of two formats.

The first format will use the l() function to render the link:

    * href: Required. The URL of the link.
    * title: Required. The name of the link.
    * attributes: Optional. See l() for usage.
    * html: Optional. See l() for usage.
    * query: Optional. See l() for usage.
    * fragment: Optional. See l() for usage.

The second format can be used for non-links. Leaving out the href index will select this format:

    * title: Required. The text or HTML code to display.
    * html: Optional. If not set to true, check_plain() will be run on the title before it is displayed.

Parameters

$type An identifier declaring what kind of link is being requested. Possible values:

    * node: Links to be placed below a node being viewed.
    * comment: Links to be placed below a comment being viewed.

$node A node object passed in case of node links.

$teaser In case of node link: a 0/1 flag depending on whether the node is displayed with its teaser or its
full form (on a node/nid page)

Return value

An array of the requested links.

*/

/*
function hook_link($type, $node = NULL, $teaser = FALSE) {
  $links = array();

  if ($type == 'node' && isset($node->parent)) {
    if (!$teaser) {
      if (book_access('create', $node)) {
        $links['book_add_child'] = array(
          'title' => t('add child page'),
          'href' => "node/add/book/parent/$node->nid",
        );
      }
      if (user_access('see printer-friendly version')) {
        $links['book_printer'] = array(
          'title' => t('printer-friendly version'),
          'href' => 'book/export/html/'. $node->nid,
          'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
        );
      }
    }
  }

      $links['sample_link'] = array(
    'title' => t('go somewhere'),
    'href' => 'node/add',
    'query' => 'foo=bar',
    'fragment' => 'anchorname',
    'attributes' => array('title' => t('go to another page')),
  );

  // Example of a link that's not an anchor
  if ($type == 'video') {
    if (variable_get('video_playcounter', 1) && user_access('view play counter')) {
      $links['play_counter'] = array(
        'title' => format_plural($node->play_counter, '1 play', '@count plays'),
      );
    }
  }

  return $links;
}
*/

function hook_link($type, $node = NULL, $teaser = FALSE) {}


/* hook_link_alter

Perform alterations before links on a node are rendered. One popular use of this hook is to add/delete
links from other modules.

Parameters

$node A node object for editing links on

$links Nested array of links for the node

Return value

None.

*/

/*
function hook_link_alter(&$node, &$links) {
  foreach ($links AS $module => $link) {
    if (strstr($module, 'taxonomy_term')) {
      // Link back to the forum and not the taxonomy term page
      $links[$module]['#href'] = str_replace('taxonomy/term', 'forum', $link['#href']);
    }
  }
}
*/

function hook_link_alter(&$node, &$links) {}


/* hook_load

Load node-type-specific information.

This is a hook used by node modules. It is called to allow the module a chance to load extra information
that it stores about a node, or possibly replace already loaded information - which can be dangerous.

For a detailed usage example, see node_example.module.

Parameters

$node The node being loaded. At call time, node.module has already loaded the basic information about the
node, such as its node ID (nid), title, and body.

Return value

An object containing properties of the node being loaded. This will be merged with the passed-in $node to
result in an object containing a set of properties resulting from adding the extra properties to the
passed-in ones, and overwriting the passed-in ones with the extra properties if they have the same name
as passed-in properties.

*/

/*
function hook_load($node) {
  $additions = db_fetch_object(db_query('SELECT * FROM {mytable} WHERE nid = %s', $node->nid));
  return $additions;
}
*/

function hook_load($node) {}


/* hook_mail_alter

Alter any aspect of the emails sent by Drupal. You can use this hook to add a common site footer to
all outgoing emails; add extra header fields and/or modify the mails sent out in any way. HTML-izing
the outgoing mails is one possibility. See also drupal_mail().

Parameters

$mailkey A key to indetify the mail sent. Look into the module source codes for possible mailkey values.

$to The mail address or addresses where the message will be send to.
    The formatting of this string must comply with RFC 2822.

$subject Subject of the e-mail to be sent. This must not contain any newline characters,
    or the mail may not be sent properly.

$body Message to be sent. Drupal will format the correct line endings for you.

$from The From, Reply-To, Return-Path and Error-To headers in $headers are already
    set to this value (if given).

$headers Associative array containing the headers to add.
    This is typically used to add extra headers (From, Cc, and Bcc).

Return value

The return value is discarded. Modify the parameters directly.

*/

/*
function hook_mail_alter(&$mailkey, &$to, &$subject, &$body, &$from, &$headers) {
  $body .= "\n\n--\nMail sent out from " . variable_get('sitename', t('Drupal'));
}
*/

function hook_mail_alter(&$mailkey, &$to, &$subject, &$body, &$from, &$headers) {}


/* hook_menu

Define menu items and page callbacks.

This hook enables modules to register paths, which determines whose requests are to be handled.
Depending on the type of registration requested by each path, a link is placed in the the
navigation block and/or an item appears in the menu administration page (q=admin/menu).

Drupal will call this hook twice: once with $may_cache set to TRUE, and once with it set to
FALSE. Therefore, each menu item should be registered when $may_cache is either TRUE or FALSE,
not both times. Setting a menu item twice will result in unspecified behavior.

This hook is also a good place to put code which should run exactly once per page view.
Put it in an if (!may_cache) block.

For a detailed usage example, see page_example.module.

Parameters

$may_cache A boolean indicating whether cacheable menu items should be returned. The menu cache
    is per-user, so items can be cached so long as they are not dependent on the user's current
    location. See the local task definitions in node_menu() for an example of uncacheable menu items.

Return value

An array of menu items. Each menu item is an associative array that may contain the following key-value pairs:
    * "path": Required.
        The path to link to when the user selects the item.
    * "title": Required.
        The translated title of the menu item.
    * "callback":
        The function to call to display a web page when the user visits the path. If omitted, the parent
        menu item's callback will be used instead.
    * "callback arguments":
        An array of arguments to pass to the callback function.
    * "access":
        A boolean value that determines whether the user has access rights to this menu item.
        Usually determined by a call to user_access(). If omitted and "callback" is also absent,
        the access rights of the parent menu item will be used instead.
    * "weight":
        An integer that determines relative position of items in the menu; higher-weighted items sink.
        Defaults to 0. When in doubt, leave this alone; the default alphabetical order is usually best.
    * "type":
        A bitmask of flags describing properties of the menu item. Many shortcut bitmasks are provided in menu.inc:
          o MENU_NORMAL_ITEM:
              Normal menu items show up in the menu tree and can be moved/hidden by the administrator.
          o MENU_ITEM_GROUPING:
              Item groupings are used for pages like "node/add" that simply list subpages to visit.
          o MENU_CALLBACK:
              Callbacks simply register a path so that the correct function is fired when the URL is accessed.
          o MENU_DYNAMIC_ITEM:
              Dynamic menu items change frequently, and so should not be stored in the database for
              administrative customization.
          o MENU_SUGGESTED_ITEM:
              Modules may "suggest" menu items that the administrator may enable.
          o MENU_LOCAL_TASK:
              Local tasks are rendered as tabs by default.
          o MENU_DEFAULT_LOCAL_TASK:
              Every set of local tasks should provide one "default" task, that links to the same path as its
              parent when clicked.
      If the "type" key is omitted, MENU_NORMAL_ITEM is assumed.

*/

/*
function hook_menu($may_cache) {
  global $user;
  $items = array();

  if ($may_cache) {
    $items[] = array('path' => 'node/add/blog', 'title' => t('blog entry'),
      'access' => user_access('maintain personal blog'));
    $items[] = array('path' => 'blog', 'title' => t('blogs'),
      'callback' => 'blog_page',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
    $items[] = array('path' => 'blog/'. $user->uid, 'title' => t('my blog'),
      'access' => user_access('maintain personal blog'),
      'type' => MENU_DYNAMIC_ITEM);
    $items[] = array('path' => 'blog/feed', 'title' => t('RSS feed'),
      'callback' => 'blog_feed',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
  }
  return $items;
}
*/

function hook_menu($may_cache) {}


/* hppk_nodeapi

Act on nodes defined by other modules.

Despite what its name might make you think, hook_nodeapi() is not reserved for node modules. On the
contrary, it allows modules to react to actions affecting all kinds of nodes, regardless of whether
that module defined the node.

If you are writing a node module, do not use this hook to perform actions on your type of node alone.
Instead, use the hooks set aside for node modules, such as hook_insert() and hook_form().

Parameters

&$node The node the action is being performed on.

$op What kind of action is being performed. Possible values:
    * "delete": The node is being deleted.
    * "delete revision": The revision of the node is deleted. You can delete data associated with that revision.
    * "insert": The node is being created (inserted in the database).
    * "load": The node is about to be loaded from the database. This hook can be used to load additional data at this time.
    * "prepare": The node is about to be shown on the add/edit form.
    * "search result": The node is displayed as a search result. If you want to display extra information with the result, return it.
    * "print": Prepare a node view for printing. Used for printer-friendly view in book_module
    * "update": The node is being updated.
    * "submit": The node passed validation and will soon be saved. Modules may use this to make changes to the node before it is saved to the database.
    * "update index": The node is being indexed. If you want additional information to be indexed which is not already visible through nodeapi "view", then you should return it here.
    * "validate": The user has just finished editing the node and is trying to preview or submit it. This hook can be used to check the node data. Errors should be set with form_set_error().
    * "view": The node content is being assembled before rendering. The module may add elements $node->content prior to rendering. This hook will be called after hook_view(). The format of 4node->content is the same as used by Forms API.
    * "alter": the $node->content array has been rendered, so the node body or teaser is filtered and now contains HTML. This op should only be used when text substitution, filtering, or other raw text operations are necessary.
    * "rss item": An RSS feed is generated. The module can return properties to be added to the RSS item generated for this node. See comment_nodeapi() and upload_nodeapi() for examples. The $node passed can also be modified to add or remove contents to the feed item.

$a3
    * For "view", passes in the $teaser parameter from node_view().
    * For "validate", passes in the $form parameter from node_validate().

$a4
    * For "view", passes in the $page parameter from node_view().

Return value

This varies depending on the operation.

    * The "submit", "insert", "update", "delete", "print' and "view" operations have no return value.
    * The "load" operation should return an array containing pairs of fields => values to be merged into the node object.

*/

/*
function hook_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'submit':
      if ($node->nid && $node->moderate) {
        // Reset votes when node is updated:
        $node->score = 0;
        $node->users = '';
        $node->votes = 0;
      }
      break;
    case 'insert':
    case 'update':
      if ($node->moderate && user_access('access submission queue')) {
        drupal_set_message(t('The post is queued for approval'));
      }
      elseif ($node->moderate) {
        drupal_set_message(t('The post is queued for approval. The editors will decide whether it should be published.'));
      }
      break;
    case 'view':
      $node->content['my_additional_field'] = array(
        '#value' => theme('mymodule_my_additional_field', $additional_field),
        '#weight' => 10,
      );
      break;
  }
}
*/

function hook_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {}


/* hook_node_access_records

Set permissions for a node to be written to the database.

When a node is saved, a module implementing node access will be asked if it is interested in the access
permissions to a node. If it is interested, it must respond with an array of array of permissions for that node.

Each item in the array should contain:

'realm' This should only be realms for which the module has returned grant IDs in hook_node_grants. 'gid' This
is a 'grant ID', which can have an arbitrary meaning per realm. 'grant_view' If set to TRUE a user with the gid
in the realm can view this node. 'grant_edit' If set to TRUE a user with the gid in the realm can edit this node.
'grant_delete' If set to TRUE a user with the gid in the realm can delete this node. 'priority' If multiple
modules seek to set permissions on a node, the realms that have the highest priority will win out, and realms
with a lower priority will not be written. If there is any doubt, it is best to leave this 0.

*/

/*
function hook_node_access_records($node) {
  if (node_access_example_disabling()) {
    return;
  }

  // We only care about the node if it's been marked private. If not, it is
  // treated just like any other node and we completely ignore it.
  if ($node->private) {
    $grants = array();
    $grants[] = array(
      'realm' => 'example',
       'gid' => TRUE,
       'grant_view' => TRUE,
       'grant_update' => FALSE,
       'grant_delete' => FALSE,
      'priority' => 0,
    );

    // For the example_author array, the GID is equivalent to a UID, which
    // means there are many many groups of just 1 user.
     $grants[] = array(
      'realm' => 'example_author',
       'gid' => $node->uid,
       'grant_view' => TRUE,
       'grant_update' => TRUE,
       'grant_delete' => TRUE,
      'priority' => 0,
    );
    return $grants;
  }
}
*/

function hook_node_access_records($node) {}


/* hook_node_grants

Inform the node access system what permissions the user has.

This hook is for implementation by node access modules. In addition to managing access rights for
nodes, the node access module must tell the node access system what 'grant IDs' the current user has.
In many cases, the grant IDs will simply be role IDs, but grant IDs can be arbitrary based upon
the module.

For example, modules can maintain their own lists of users, where each list has an ID. In that case,
the module could return a list of all IDs of all lists that the current user is a member of.

A node access module may implement as many realms as necessary to properly define the access
privileges for the nodes.

For a detailed example, see node_access_example.module.

Parameters

$user The user object whose grants are requested.

$op The node operation to be performed, such as "view", "update", or "delete".

Return value

An array whose keys are "realms" of grants such as "user" or "role",
and whose values are linear lists of grant IDs.

*/

/*
function hook_node_grants($account, $op) {
  if (user_access('access private content', $account)) {
    $grants['example'] = array(1);
  }
  $grants['example_owner'] = array($user->uid);
  return $grants;
}
*/

function hook_node_grants($account, $op) {}


/* hook_node_info

Define module-provided node types.

This is a hook used by node modules. This hook is required for modules to define one or more node
types. It is called to determine the names and the attributes of a module's node types.

Only module-provided node types should be defined through this hook. User- provided (or 'custom')
node types should be defined only in the 'node_type' database table, and should be maintained by
using the node_type_save() and node_type_delete() functions.

The machine-readable name of a node type should contain only letters, numbers, and underscores.
Underscores will be converted into dashes for the purpose of contructing URLs.

All attributes of a node type that are defined through this hook (except for 'locked') can be
edited by a site administrator. This includes the machine-readable name of a node type, if
'locked' is set to FALSE.

For a detailed usage example, see node_example.module.

Return value

An array of information on the module's node types. The array contains a sub-array for each node type,
with the machine-readable type name as the key. Each sub-array has up to 10 attributes. Possible attributes:

    * "name": Required.
        the human-readable name of the node type.
    * "module": Required.
        a string telling Drupal how a module's functions map to hooks (i.e. if module is defined as example_foo,
        then example_foo_insert will be called when inserting a node of that type). This string is usually
        the name of the module in question, but not always.
    * "description": Required.
        a brief description of the node type.
    * "help": Optional (defaults to '').
        text that will be displayed at the top of the submission form for this content type.
    * "has_title": Optional (defaults to TRUE).
        boolean indicating whether or not this node type has a title field.
    * "title_label": Optional (defaults to 'Title').
        the label for the title field of this content type.
    * "has_body": Optional (defaults to TRUE).
        boolean indicating whether or not this node type has a body field.
    * "body_label": Optional (defaults to 'Body').
        the label for the body field of this content type.
    * "min_word_count": Optional (defaults to 0).
        the minimum number of words for the body field to be considered valid for this content type.
    * "locked": Optional (defaults to TRUE).
        boolean indicating whether the machine-readable name of this content type can (FALSE) or
        cannot (TRUE) be edited by a site administrator.


*/

function hook_node_info() {}


/* hook_node_operations

Add mass node operations.

This hook enables modules to inject custom operations into the mass operations dropdown found at
admin/content/node, by associating a callback function with the operation, which is called when
the form is submitted. The callback function receives one initial argument, which is an array
of the checked nodes.

Return value

An array of operations. Each operation is an associative array that may contain the following key-value pairs:

    * "label": Required. The label for the operation, displayed in the dropdown menu.
    * "callback": Required. The function to call for the operation.
    * "callback arguments": Optional. An array of additional arguments to pass to the callback function.

*/

/*
function hook_node_operations() {
  $operations = array(
    'approve' => array(
      'label' => t('Approve the selected posts'),
      'callback' => 'node_operations_approve',
    ),
    'promote' => array(
      'label' => t('Promote the selected posts'),
      'callback' => 'node_operations_promote',
    ),
    'sticky' => array(
      'label' => t('Make the selected posts sticky'),
      'callback' => 'node_operations_sticky',
    ),
    'demote' => array(
      'label' => t('Demote the selected posts'),
      'callback' => 'node_operations_demote',
    ),
    'unpublish' => array(
      'label' => t('Unpublish the selected posts'),
      'callback' => 'node_operations_unpublish',
    ),
    'delete' => array(
      'label' => t('Delete the selected posts'),
    ),
  );
  return $operations;
}
*/

function hook_node_operations() {}


/* hook_node_tupe

Act on node type changes.

This hook allows modules to take action when a node type is modified.

Parameters

$op What is being done to $info. Possible values:

    * "delete"
    * "insert"
    * "update"

$info The node type object on which $op is being performed.

Return value

None.

*/

function hook_node_tupe() {}


/*
function hook_node_type($op, $info) {

  switch ($op){
    case 'delete':
      variable_del('comment_'. $info->type);
      break;
    case 'update':
      if (!empty($info->old_type) && $info->old_type != $info->type) {
        $setting = variable_get('comment_'. $info->old_type, COMMENT_NODE_READ_WRITE);
        variable_del('comment_'. $info->old_type);
        variable_set('comment_'. $info->type, $setting);
      }
      break;
  }
}
*/

function hook_node_type($op, $info) {}


/* hook_perm

Define user permissions.

This hook can supply permissions that the module defines, so that they can be selected on the
user permissions page and used to restrict access to actions the module performs.

The permissions in the array do not need to be wrapped with the function t(), since the string
extractor takes care of extracting permission names defined in the perm hook for translation.

Permissions are checked using user_access().

For a detailed usage example, see page_example.module.

Return value

An array of permissions strings.

*/


function hook_perm() {}


/* hook_ping

Ping another server.

This hook allows a module to notify other sites of updates on your Drupal site.

Parameters

$name The name of your Drupal site.

$url The URL of your Drupal site.

Return value

None.

*/

/*
function hook_ping($name = '', $url = '') {
  $feed = url('node/feed');

  $client = new xmlrpc_client('/RPC2', 'rpc.weblogs.com', 80);

  $message = new xmlrpcmsg('weblogUpdates.ping',
    array(new xmlrpcval($name), new xmlrpcval($url)));

  $result = $client->send($message);

  if (!$result || $result->faultCode()) {
    watchdog('error', 'failed to notify "weblogs.com" (site)');
  }

  unset($client);
}
*/

function hook_ping($name = '', $url = '') {}


/* hook_prepare

This is a hook used by node modules. It is called after load but before the node
is shown on the add/edit form.

For a usage example, see image.module.

Parameters

&$node The node being saved.

Return value

None.

*/

/*
function hook_prepare(&$node) {
  if ($file = file_check_upload($field_name)) {
    $file = file_save_upload($field_name, _image_filename($file->filename, NULL, TRUE));
    if ($file) {
      if (!image_get_info($file->filepath)) {
        form_set_error($field_name, t('Uploaded file is not a valid image'));
        return;
      }
    }
    else {
      return;
    }
    $node->images['_original'] = $file->filepath;
    _image_build_derivatives($node, true);
    $node->new_file = TRUE;
  }
}
*/

function hook_prepare(&$node) {}


/* hook_profile_alter

Perform alterations profile items before they are rendered. You may omit/add/re-sort/re-categorize, etc.

Parameters

$account A user object specifying whose profile is being rendered

$fields An array of $field objects, with unique module specified keys. Use this $key to find the item you care about.

Return value

None.
*/

function hook_profile_alter() {}


/* hook_requirements

Check installation requirements that need to be satisfied.

A module is expected to return a list of requirements and whether they are satisfied.
This information is used both during installation and on the status report in the administration
section.

Appropriate checks are for library or server versions, maintenance tasks, security, ... Module
dependencies on the other hand do not belong here. Install-time requirements must be checked
without access to the full Drupal API.

Requirements can have one of four severity levels: REQUIREMENT_INFO: For info only. REQUIREMENT_OK: The
requirement is satisfied. REQUIREMENT_WARNING: The requirement failed with a warning. REQUIREMENT_ERROR:
The requirement failed with an error.

Note that you need to use $t = get_t(); to retrieve the appropriate localisation function name (t or st).

'runtime: the runtime requirements are being checked and shown on the status report page. Any requirement
with REQUIREMENT_ERROR severity will cause a notice to appear at /admin.

Parameters

$phase 'install': the module is being installed (either during install.php, or later by hand).
    Any requirement with REQUIREMENT_ERROR severity will cause install to abort.

Return value

$requirement A keyed array of requirements. Each requirement is itself an array with the following items:
    'title': the name of the requirement.
    'value': the current value (e.g. version, time, level, ...).
    'description': optional notice for the status report.
    'severity': the requirement's result/severity (defaults to OK).

*/

/*
function hook_requirements($phase) {
  $requirements = array();
  // Ensure translations don't break at install time
  $t = get_t();

  // Report Drupal version
  if ($phase == 'runtime') {
    $requirements['drupal'] = array(
      'title' => $t('Drupal'),
      'value' => VERSION,
      'severity' => REQUIREMENT_INFO
    );
  }

  // Test PHP version
  $requirements['php'] = array(
    'title' => $t('PHP'),
    'value' => ($phase == 'runtime') ? l(phpversion(), 'admin/logs/status/php') : phpversion(),
  );
  if (version_compare(phpversion(), DRUPAL_MINIMUM_PHP) < 0) {
    $requirements['php']['description'] = $t('Your PHP installation is too old. Drupal requires at least PHP %version.', array('%version' => DRUPAL_MINIMUM_PHP));
    $requirements['php']['severity'] = REQUIREMENT_ERROR;
  }

  // Report cron status
  if ($phase == 'runtime') {
    $cron_last = variable_get('cron_last', NULL);

    if (is_numeric($cron_last)) {
      $requirements['cron']['value'] = $t('Last run !time ago', array('!time' => format_interval(time() - $cron_last)));
    }
    else {
      $requirements['cron'] = array(
        'description' => $t('Cron has not run. It appears cron jobs have not been setup on your system. Please check the help pages for <a href="@url">configuring cron jobs</a>.', array('@url' => 'http://drupal.org/cron')),
        'severity' => REQUIREMENT_ERROR,
        'value' => $t('Never run'),
      );
    }

    $requirements['cron']['description'] .= ' '. t('You can <a href="@cron">run cron manually</a>.', array('@cron' => url('admin/logs/status/run-cron')));

    $requirements['cron']['title'] = $t('Cron maintenance tasks');
  }

  return $requirements;
}
*/

function hook_requirements($phase) {}


/* hook_search

Define a custom search routine.

This hook allows a module to perform searches on content it defines (custom node types, users,
or comments, for example) when a site search is performed.

Note that you can use form API to extend the search. You will need to use hook_form_alter() to add
any additional required form elements. You can process their values on submission using a custom
validation function. You will need to merge any custom search values into the search keys using a
key:value syntax. This allows all search queries to have a clean and permanent URL. See
node_form_alter() for an example.

The example given here is for node.module, which uses the indexed search capabilities. To do this,
node module also implements hook_update_index() which is used to create and maintain the index.

We call do_search() with the keys, the module name and extra SQL fragments to use when searching.
See hook_update_index() for more information.

Parameters

$op A string defining which operation to perform:
    * 'name':
        the hook should return a translated name defining the type of items that are searched for with
        this module ('content', 'users', ...)
    * 'reset':
        the search index is going to be rebuilt. Modules which use hook_update_index() should update
        their indexing bookkeeping so that it starts from scratch the next time hook_update_index() is called.
    * 'search':
        the hook should perform a search using the keywords in $keys
    * 'status':
        if the module implements hook_update_index(), it should return an array containing the following keys:
          o remaining: the amount of items that still need to be indexed
          o total: the total amount of items (both indexed and unindexed)

$keys The search keywords as entered by the user.

Return value

An array of search results. Each item in the result set array may contain whatever information the module
wishes to display as a search result. To use the default search result display, each item should be an array
which can have the following keys:

    * link: the URL of the found item
    * type: the type of item
    * title: the name of the item
    * user: the author of the item
    * date: a timestamp when the item was last modified
    * extra: an array of optional extra information items
    * snippet: an excerpt or preview to show with the result (can be generated with search_excerpt())

Only 'link' and 'title' are required, but it is advised to fill in as many of these fields as possible.

*/

/*
function hook_search($op = 'search', $keys = null) {
  switch ($op) {
    case 'name':
      return t('content');
    case 'reset':
      variable_del('node_cron_last');
      return;
    case 'search':
      $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid '. node_access_join_sql() .' INNER JOIN {users} u ON n.uid = u.uid', 'n.status = 1 AND '. node_access_where_sql());
      $results = array();
      foreach ($find as $item) {
        $node = node_load(array('nid' => $item));
        $extra = node_invoke_nodeapi($node, 'search result');
        $results[] = array('link' => url('node/'. $item),
                           'type' => node_invoke($node, 'node_name'),
                           'title' => $node->title,
                           'user' => theme('username', $node),
                           'date' => $node->changed,
                           'extra' => $extra,
                           'snippet' => search_excerpt($keys, check_output($node->body, $node->format)));
      }
      return $results;
  }
}
*/

function hook_search($op = 'search', $keys = null) {}


/* hook_search_item

Format a search result.

This hook allows a module to apply custom formatting to its search results.

Default formatting can be had by omitting this hook and returning your search results as arrays with the
right keys. See hook_search().

Parameters

$item The search result to display, as returned in an array by hook_search().

Return value

A string containing an HTML representation of the search result.

*/

/*
function hook_search_item($item) {
  $output .= ' <b><u><a href="'. $item['link']
    .'">'. $item['title'] .'</a></u></b><br />';
  $output .= ' <small>' . $item['description'] . '</small>';
  $output .= '<br /><br />';

  return $output;
}
*/

function hook_search_item($item) {}


/* hook_search_preprocess

Preprocess text for the search index.

This hook is called both for text added to the search index, as well as the keywords users have
submitted for searching.

This is required for example to allow Japanese or Chinese text to be searched. As these languages
do not use spaces, it needs to be split into separate words before it can be indexed. There are
various external libraries for this.

Parameters

$text The text to split. This is a single piece of plain-text that was extracted from between two HTML tags.
    Will not contain any HTML entities.

Return value

The text after processing.

*/

/*
function hook_search_preprocess($text) {
  // Do processing on $text
  return $text;
}
*/

function hook_search_preprocess($text) {}


/* hook_submit

This is a hook used by node modules. It is called after validation has succeeded and before insert/update.
It is used to for actions which must happen only if the node is to be saved. Usually, $node is changed in
some way and then the actual saving of that change is left for the insert/update hooks.

For a detailed usage example, see fileupload.module.

Parameters

&$node The node being saved.

Return value

None.

*/

/*
function hook_submit(&$node) {
  // if a file was uploaded, move it to the files directory
  if ($file = file_check_upload('file')) {
    $node->file = file_save_upload($file, file_directory_path(), false);
  }
}
*/

function hook_submit(&$node) {}


/* hook_taxonomy

Act on taxonomy changes.

This hook allows modules to take action when the terms and vocabularies in the taxonomy are modified.

Parameters

$op What is being done to $object. Possible values:

    * "delete"
    * "insert"
    * "update"

$type What manner of item $object is. Possible values:

    * "term"
    * "vocabulary"

$array The item on which $op is being performed. Possible values:

    * for vocabularies, 'insert' and 'update' ops: $form_values from taxonomy_form_vocabulary_submit()
    * for vocabularies, 'delete' op: $vocabulary from taxonomy_get_vocabulary() cast to an array
    * for terms, 'insert' and 'update' ops: $form_values from taxonomy_form_term_submit()
    * for terms, 'delete' op: $term from taxonomy_get_term() cast to an array

Return value

None.

*/

/*
function hook_taxonomy($op, $type, $array = NULL) {
  if ($type == 'vocabulary' && ($op == 'insert' || $op == 'update')) {
    if (variable_get('forum_nav_vocabulary', '') == ''
        && in_array('forum', $array['nodes'])) {
      // since none is already set, silently set this vocabulary as the
      // navigation vocabulary
      variable_set('forum_nav_vocabulary', $array['vid']);
    }
  }
}
*/

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


/* hook_uninstall

Remove any tables or variables that the module sets.

The uninstall hook will fire when the module gets uninstalled.

*/

/*
function hook_uninstall() {
  db_query('DROP TABLE {profile_fields}');
  db_query('DROP TABLE {profile_values}');
  variable_del('profile_block_author_fields');
}
*/

function hook_uninstall() {}


/* hook_update

Respond to node updating.

This is a hook used by node modules. It is called to allow the module to take action when an edited
node is being updated in the database by, for example, updating information in related tables.

To take action when nodes of any type are updated (not just nodes of the type(s) defined by this module),
use hook_nodeapi() instead.

For a detailed usage example, see node_example.module.

Parameters

$node The node being updated.

Return value

None.

*/

/*
function hook_update($node) {
  db_query("UPDATE {mytable} SET extra = '%s' WHERE nid = %d",
    $node->extra, $node->nid);
}
*/

function hook_update($node) {}


/* hook_update_index

Update Drupal's full-text index for this module.

Modules can implement this hook if they want to use the full-text indexing mechanism in Drupal.

This hook is called every cron run if search.module is enabled. A module should check which of
its items were modified or added since the last run. It is advised that you implement a throttling
mechanism which indexes at most 'search_cron_limit' items per run (see example below).

You should also be aware that indexing may take too long and be aborted if there is a PHP time limit.
That's why you should update your internal bookkeeping multiple times per run, preferably after every
item that is indexed.

Per item that needs to be indexed, you should call search_index() with its content as a single HTML
string. The search indexer will analyse the HTML and use it to assign higher weights to important words
(such as titles). It will also check for links that point to nodes, and use them to boost the ranking
of the target nodes.

*/

/*
function hook_update_index() {
  $last = variable_get('node_cron_last', 0);
  $limit = (int)variable_get('search_cron_limit', 100);

  $result = db_query_range('SELECT n.nid, c.last_comment_timestamp FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE n.status = 1 AND n.moderate = 0 AND (n.created > %d OR n.changed > %d OR c.last_comment_timestamp > %d) ORDER BY GREATEST(n.created, n.changed, c.last_comment_timestamp) ASC', $last, $last, $last, 0, $limit);

  while ($node = db_fetch_object($result)) {
    $last_comment = $node->last_comment_timestamp;
    $node = node_load(array('nid' => $node->nid));

    // We update this variable per node in case cron times out, or if the node
    // cannot be indexed (PHP nodes which call drupal_goto, for example).
    // In rare cases this can mean a node is only partially indexed, but the
    // chances of this happening are very small.
    variable_set('node_cron_last', max($last_comment, $node->changed, $node->created));

    // Get node output (filtered and with module-specific fields).
    if (node_hook($node, 'view')) {
      node_invoke($node, 'view', false, false);
    }
    else {
      $node = node_prepare($node, false);
    }
    // Allow modules to change $node->body before viewing.
    node_invoke_nodeapi($node, 'view', false, false);

    $text = '<h1>'. drupal_specialchars($node->title) .'</h1>'. $node->body;

    // Fetch extra data normally not visible
    $extra = node_invoke_nodeapi($node, 'update index');
    foreach ($extra as $t) {
      $text .= $t;
    }

    // Update index
    search_index($node->nid, 'node', $text);
  }
}
*/

function hook_update_index() {}


/* hook_user

Act on user account actions.

This hook allows modules to react when operations are performed on user accounts.

Parameters

$op What kind of action is being performed. Possible values:

    * "after_update":
        The user object has been updated and changed. Use this if (probably along with 'insert')
        if you want to reuse some information from the user object.
    * "categories":
        A set of user information categories is requested.
    * "delete":
        The user account is being deleted. The module should remove its custom additions to the user
        object from the database.
    * "form":
        The user account edit form is about to be displayed. The module should present the form elements
        it wishes to inject into the form.
    * "submit":
        Modify the account before it gets saved.
    * "insert":
        The user account is being added. The module should save its custom additions to the user object
        into the database and set the saved fields to NULL in $edit.
    * "login":
        The user just logged in.
    * "logout":
        The user just logged out.
    * "load":
        The user account is being loaded. The module may respond to this and insert additional information
        into the user object.
    * "register":
        The user account registration form is about to be displayed. The module should present the form
        elements it wishes to inject into the form.
    * "update":
        The user account is being changed. The module should save its custom additions to the user object
        into the database and set the saved fields to NULL in $edit.
    * "validate":
        The user account is about to be modified. The module should validate its custom additions to the user
        object, registering errors as necessary.
    * "view":
        The user's account information is being displayed. The module should format its custom additions
        for display.

&$edit The array of form values submitted by the user.

&$account The user object on which the operation is being performed.

$category The active category of user information being edited.

Return value

This varies depending on the operation.

    * "categories": A linear array of associative arrays. These arrays have keys:
          o "name": The internal name of the category.
          o "title": The human-readable, localized name of the category.
          o "weight": An integer specifying the category's sort ordering.
    * "submit": None:
    * "insert": None.
    * "update": None.
    * "delete": None.
    * "login": None.
    * "logout": None.
    * "load": None.
    * "form", "register": A $form array containing the form elements to display.
    * "validate": None.
    * "view":
        An associative array of associative arrays. The outer array should be keyed by category name. The
        interior array(s) should have a unique textual key and have 'title', 'value' and 'class' elements.
        See theme_user_profile() and an example at user_user()

*/

/*
function hook_user($op, &$edit, &$account, $category = NULL) {
  if ($op == 'form' && $category == 'account') {
    $form['comment_settings'] = array(
      '#type' => 'fieldset',
      '#title' => t('Comment settings'),
      '#collapsible' => TRUE,
      '#weight' => 4);
    $form['comment_settings']['signature'] = array(
      '#type' => 'textarea',
      '#title' => t('Signature'),
      '#default_value' => $edit['signature'],
      '#description' => t('Your signature will be publicly displayed at the end of your comments.'));
    return $form;
  }
}
*/

function hook_user($op, &$edit, &$account, $category = NULL) {}


/* hook_user_operations

Add mass user operations.

This hook enables modules to inject custom operations into the mass operations dropdown found at
admin/user/user, by associating a callback function with the operation, which is called when the form
is submitted. The callback function receives one initial argument, which is an array of the checked users.

Return value

An array of operations. Each operation is an associative array that may contain the following key-value pairs:

    * "label": Required. The label for the operation, displayed in the dropdown menu.
    * "callback": Required. The function to call for the operation.
    * "callback arguments": Optional. An array of additional arguments to pass to the callback function.

*/

/*
function hook_user_operations() {
  $operations = array(
    'unblock' => array(
      'label' => t('Unblock the selected users'),
      'callback' => 'user_user_operations_unblock',
    ),
    'block' => array(
      'label' => t('Block the selected users'),
      'callback' => 'user_user_operations_block',
    ),
    'delete' => array(
      'label' => t('Delete the selected users'),
    ),
  );
  return $operations;
}
*/

function hook_user_operations() {}


/* hook_validate

Verify a node editing form.

This is a hook used by node modules. It is called to allow the module to verify that the node is in
a format valid to post to the site. Errors should be set with form_set_error().

To validate nodes of all types (not just nodes of the type(s) defined by this module), use
hook_nodeapi() instead.

Changes made to the $node object within a hook_validate() function will have no effect. The preferred
method to change a node's content is to use hook_submit() or hook_nodeapi($op='submit') instead. If
it is really necessary to change the node at the validate stage, you can use function form_set_value().

For a detailed usage example, see node_example.module.

Parameters

$node The node to be validated.

Return value

None.

*/

/*
function hook_validate($node) {
  if (isset($node->end) && isset($node->start)) {
    if ($node->start > $node->end) {
      form_set_error('time', t('An event may not end before it starts.'));
    }
  }
}
*/

function hook_validate($node) {}


/* hook_view

Display a node.

This is a hook used by node modules. It allows a module to define a custom method of displaying its
nodes, usually by displaying extra information particular to that node type.

For a detailed usage example, see node_example.module.

Parameters

$node The node to be displayed.

$teaser Whether we are to generate a "teaser" or summary of the node, rather than display the whole thing.

$page Whether the node is being displayed as a standalone page. If this is TRUE, the node title should not
    be displayed, as it will be printed automatically by the theme system. Also, the module may choose
    to alter the default breadcrumb trail in this case.

Return value

$node. The passed $node parameter should be modified as necessary and returned so it can be properly
    presented. Nodes are prepared for display by assembling a structured array in $node->content,
    rather than directly manipulating $node->body and $node->teaser. The format of this array is
    the same used by the Forms API. As with FormAPI arrays, the #weight property can be used to
    control the relative positions of added elements. If for some reason you need to change the
    body or teaser returned by node_prepare(), you can modify $node->content['body']['#value'].
    Not that this will be the un- rendered content. To modify the rendered output, see
    hook_nodeapi($op = 'alter').

*/

/*
function hook_view($node, $teaser = FALSE, $page = FALSE) {
  if ($page) {
    $breadcrumb = array();
    $breadcrumb[] = array('path' => 'example', 'title' => t('example'));
    $breadcrumb[] = array('path' => 'example/'. $node->field1,
      'title' => t('%category', array('%category' => $node->field1)));
    $breadcrumb[] = array('path' => 'node/'. $node->nid);
    menu_set_location($breadcrumb);
  }

  $node = node_prepare($node, $teaser);
  $node->content['myfield'] = array(
    '#value' => theme('mymodule_myfield', $node->myfield),
    '#weight' => 1,
  );

  return $node;
}
*/

function hook_view($node, $teaser = FALSE, $page = FALSE) {}


/* hook_xmlrpc

Register XML-RPC callbacks.

This hook lets a module register callback functions to be called when particular XML-RPC methods are invoked by a client.

Return value

An array which maps XML-RPC methods to Drupal functions. Each array element is either a pair of
method => function or an array with four entries:

    * The XML-RPC method name (for example, module.function).
    * The Drupal callback function (for example, module_function).
    * The method signature is an array of XML-RPC types. The first element of this array is the type of return value and then you should write a list of the types of the parameters. XML-RPC types are the following (See the types at <a href="http://www.xmlrpc.com/spec" title="http://www.xmlrpc.com/spec" rel="nofollow">http://www.xmlrpc.com/spec</a>):
          o "boolean": 0 (false) or 1 (true).
          o "double": a floating point number (for example, -12.214).
          o "int": a integer number (for example, -12).
          o "array": an array without keys (for example, array(1, 2, 3)).
          o "struct": an associative array or an object (for example, array('one' => 1, 'two' => 2)).
          o "date": when you return a date, then you may either return a timestamp (time(), mktime() etc.) or an ISO8601 timestamp. When date is specified as an input parameter, then you get an object, which is described in the function xmlrpc_date
          o "base64": a string containing binary data, automatically encoded/decoded automatically.
          o "string": anything else, typically a string.
    * A descriptive help string, enclosed in a t() function for translation purposes.

Both forms are shown in the example.

*/

/*
function hook_xmlrpc() {
  return array(
    'drupal.login' => 'drupal_login',
    array(
      'drupal.site.ping',
      'drupal_directory_ping',
      array('boolean', 'string', 'string', 'string', 'string', 'string'),
      t('Handling ping request'))
  );
}
*/

function hook_xmlrpc() {}
?>

Creating new node types - a tutorial

Okay, you've read the tutorial on creating modules and know the basics of hooks, blocks and possibly forms. You'd now like to use more of the various Drupal APIs, and create a new node type.

Before creating a new node type, first decide if you really need to create a new one: if you can use CCK and Views to gather and manipulate your data, you may not need this tutorial. However, if you need to manipulate your data in a different way, or have multiple data dependencies that can't be done with CCK, or if you just want to learn more about Drupal internals, then this tutorial may just be for you.

Note that you can probably still use CCK+Views if the custom parts of your node just includes variations of the built-in commenting system, paths, publishing information and other built-in Drupal goodies.

An alternate to this tutorial on learning how to write modules with new Drupal node types is to copy an existing node module, such as the story module, replace the module name/node type with your new node type, then go through the module and adjust the functionality, as described on How to create your own simple node type (from story node) (Drupal 4.7).

For this tutorial, we're going to make a to-do list module. We want to use the title of the node for the quick item description (textfield) and the body of the node for an extended description (textarea). We also want to add a date field for a due date (date), a priority level for the tasks (drop down list), and a status field for the task's status (drop down list).

Also note, if you are going to create a module for public consumption, you should check to see if a similar module already exists in the contributions repository (published or otherwise). The community benefits far more from one well developed module than from many fragmented, half-completed or unmaintained modules. This node, for example, duplicates the task module from the contributions repository (this duplication is not good).

This tutorial is written for Drupal 4.7.

01. Getting started

Having read the previous tutorial, we can start with the basics of a module. None of this should be new. Functions that are new-node-type specific functions aren't included in this skeleton module, so that they can be described later.

In particular, we'll define the help, perm, access and menu hooks. We won't completely fill in the functionality for these functions, we just want enough to get started developing our new-node-type module.

<?php
/**
* @file
* submit, view, and manage to-do items and lists
*/

/**
* Implementation of hook_help()
* Display help text for the todo module
*/
function todo_help($section) {
  switch (
$section) {
    case
'admin/help#todo':
     
$o .= '<p>' . t('Submit, view and manage todo lists.') . '</p>';
      return
$o;
    case
'admin/modules#description':
      return
t('Allows users to submit, view and manage to-do items and lists.');
    case
'node/add#todo':
      return
t('Add a task as a to-do item here.  You can assign a due date and priority, as desired');
  }
}

/**
* Implementation of hook_perm().
* Define the permissions this module uses
*/
function todo_perm() {
  return array(
'create todo items', 'manage own todo items');
}

/**
* Implementation of hook_access().
*/
function todo_access($op, $node) {
  global
$user;

  if (
$op == 'create') {
    return
user_access('create todo items');
  }

  if (
$op == 'update' || $op == 'delete') {
    if (
user_access('manage own todo items') && ($user->uid == $node->uid)) {
      return
TRUE;
    }
  }
}

/**
* Implementation of hook_menu().
*/
function todo_menu($may_cache) {
 
$items = array();

  if (
$may_cache) {
   
$items[] = array('path' => 'node/add/todo', 'title' => t('todo'),
     
'access' => user_access('create todo items'));
  }

  return
$items;
}
?>

02. Name the new node type

With our basic outline of a module handy, we want to start defining the node. First, we need to inform Drupal that we have a new node type, by specifying its name and how Drupal should access the module's functions.

The array returned by the node_info hook specifies the human readable module name, "to-do" in our case, and the prefix of our module's functions, "todo" for this module. This means, our function names will be accessed via todo_insert and todo_validate. If we used "pineapple" as our "base" value, we would need to define our functions with a pineapple_ prefix: pineapple_insert, pineapple_validate, etc.

Read more about the node_info hook.

<?php
/**
* Implementation of hook_node_info().
* Define the node type
*/
function todo_node_info() {
  return array(
'todo' => array('name' => t('to-do'), 'base' => 'todo'));
}
?>

At this point, if you install the module, enable it, grant the module's permissions to a user, and access the create content link, you'll see the link to create a to-do node in the node/add page (your page may differ, depending on your installation and configuration):

Selecting this link will show you the surrounding information associated with a new node, based on what modules you have installed, activated and permissions for. However, there is no way to create the content for this node.

03. Create an input form

We'd next like to create the input form. We've already defined what fields we'd like, so generating the form will be straight forward.

In particular, we'd like:

  1. Summary as a textfield (the node title)
  2. Description as a textarea (the node body)
  3. Due date as a date selector (custom field)
  4. Priority as a drop down list select (custom field)
  5. Current status as a drop down list select (custom field)

So, let's create the form, based on these fields. There are many pages that describe the Form API. Using the Forms API documentation, we'll add the form:

<?php
function todo_form(&$node) {

 
// summary / title
 
$form['title'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Task'),
   
'#required' => TRUE,
   
'#default_value' => $node->title,
   
'#weight' => -5,
   
'#description' => t('Task summary')
  );

 
// full description / body
 
$form['body_filter']['body'] = array(
   
'#type' => 'textarea',
   
'#title' => t('Task details'),
   
'#default_value' => $node->body,
   
'#rows' => 10,
   
'#required' => FALSE
 
);
 
$form['body_filter']['format'] = filter_form($node->format);

 
// due date
 
$form['duedate'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Due date'),
   
'#required' => FALSE,
   
'#default_value' => $node->duedate,
   
'#weight' => 0,
   
'#description' => 'Format dd/mm/yyyy'
 
);
 
 
// priority
 
$priorities = _todo_priorities();
 
$form['priority'] = array(
   
'#type' => 'select',
   
'#title' => t('Priority'),
   
'#required' => FALSE,
   
'#default_value' => $node->priority,
   
'#options' => $priorities,
   
'#weight' => 1
 
);

 
// current status
 
$statuses = _todo_statuses();
 
$form['taskstatus'] = array(
   
'#type' => 'select',
   
'#title' => t('Current status'),
   
'#required' => TRUE,
   
'#default_value' => $node->taskstatus,
   
'#options' => $statuses,
   
'#weight' => 2
 
);
  return
$form;
}

/**
* Return an array of priorities
* @return array of priorities
*/
function _todo_priorities() {
  return array(
'none', 'low', 'medium', 'high');
}

/**
* Return an array of task statuses
* @return array of statuses
*/
function _todo_statuses() {
  return array(
   
'unknown',
   
'not started',
   
'in progress',
   
'dependency blocked',
   
'complete'
 
);
}
?>

A few notes about these additions to our module:

The priority and status values are returned in separate functions so that we have the values in one place: we can use these arrays in other functions without declaring the arrays in each of those other functions. Later, we can make these arrays user-configurable, saving the array values in the database, and loading them from the database in these functions.

We use the underscore-prefix naming convention for the two functions to indicate these functions are internal functions and not for external module access. module_invoke will not work with these two functions, preventing other modules from calling these functions. We may not want this limitation later, and may rename the functions.

The order matters with the priority and status arrays. The order of the values in the array is the order they will display as the drop-down select options. If you want a default option, either put it first in the list, or programmatically determine the correct default value and set it with the #default_value field.

The priority and status values map to numbers so that we can search more quickly: SELECT * FROM {todo} WHERE taskstatus >= 2 will execute faster than SELECT * FROM {todo} WHERE taskstatus in ('medium', 'high'), for example. However, if these values are made configurable, each new option would need to be given a new number, so that nodes with old values don't have new values assigned to them. For example, if the priority values array is changed to array('none', 'low', 'medium low', 'medium', 'medium high', 'high', 'critical'), any current nodes with priority 3 will go from high priority to medium priority, which may not be the desired result.

Our task status variable cannot be named 'status', as the node already has a field named 'status'.

The value of each field is specified in the #default_value field, and not the #value field. Any input that can be edited by the user should go into the #default_value parameter. You can adjust the value during the module's validation or save functions, but using #value during the form creation will prevent the user from changing the value. Conversely, if the value should never change, use #value instead of #default_value.

More details about creating a form can be found in the Forms API documentation.

More details about the form hook can be found in the hook_form documentation.

04. Validate the form submission

If you have downloaded, installed, enabled, and set permissions for a user to add a todo item, then said user can view the todo add page via the node/add/todo URL. If you have other modules and permissions set for this user, your user will also see the other form fields. These might include input formats, file attachments, comment settings, menu or path settings, publishing options and authoring information.

We'll adjust these later in the form editing process, removing what we don't need. For now, we want to validate the data when the user submits the form. The validation we'll use will be somewhat artificial, but will demonstrate the process.

Forms will automatically validate for invalid choices for a select element, so we don't need to check the priority or status values.

Eventually, we'll want to use a javascript calendar to make date selection easier for the user. In anticipation of using this widget, we'll use the javascript calendar's date input format for our form. The format the javascript calendar uses is somewhat configurable, so it can later be updated to match the date format desired.

<?php
/**
* Validate our forms
* @param string form id
* @param array form values
*/
function todo_validate($form_id, $form_values) {

   
// if the user specified a date, validate the format
   
if (isset($form_values['duedate']) && (trim($form_values['duedate']['#value']) != '')) {
       
       
// check the date format
       
if (!preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d)$/', $form_values['duedate']['#value'], $matches)) {
           
form_set_error('duedate', t('Due date should be in the format dd/mm/yyyy'));
        } else {

           
// make sure the date provided is a validate date
           
if (!checkdate($matches[2], $matches[1], $matches[3])) {
               
form_set_error('duedate', t('Please enter a valid date'));
            }
           
        }
    }
}
?>

This validation checks the due date is of the format ##/##/####, and that the date entered is a valid one. For example, 49/13/2001 is an invalid date, but 28/02/2001 is valid.

If you need to validate other fields, access the form value via $form_values[fieldname]['#value']. To see other values available,

An easy way to view all the values in the form array is to print them out and look at all of them on submit, using the print_r function:

<?php
// dump the form validate variables
function todo_validate($form_id, $form_values) {
  print
'<pre>' . print_r($form_values, true); exit();
}
?>

Download the module thus far, and rename to todo.module before saving in your Drupal installation.

05. Create the node specific database table

Our new node form displays. The data validates on submission. We now need to save the node information to the database, as well as our additional information.

Basic node information, such as a node id, node type, title, created by and when, etc., is saved in the node table. The node body is saved in the node_revisions table. This is different in 4.7 from previous versions, which had the body saved in the node table, also.

Our additional information needs to be saved somewhere, so let's create a new table to store our information.

We need a node id field, which will track the node information in the node table. We'll also need a field for the duedate, priority and taskstatus fields we defined in our form.

Drupal uses Unix timestamps (seconds since epoch) to save dates, to maintain portability among different databases, each which may store dates and timestamps differently. Our duedate will be an int(11) field to follow this convention.

For the priority and taskstatus fields, we'll use integer fields to save the values. Recall that for future versions, these fields may be configurable, and any new values added need to be assigned unique numbers, so we'll allow up to 1000 values, instead of 10 (which is the limit of unique numbers we'd have if we use tinyint(1)).

Our database table looks like this now:

--
-- Table structure for table 'todo'
--
CREATE TABLE todo (
  nid int(10) unsigned NOT NULL default '0',
  duedate int(11) unsigned NOT NULL default '0',
  priority tinyint(3) unsigned NOT NULL default '0',
  taskstatus tinyint(3) unsigned NOT NULL default '0',
  PRIMARY KEY (nid)
)
DEFAULT CHARACTER SET utf8;

Later, we'll create an install file to install this table into the database automatically. For development, at this point in the process, however, you'll need to create this table by hand.

06. Save node-specific information to the database

Now that we have a table to save data into, and our new node has a form that displays and validates on submit, we need to save the submitted values into the database.

<?php
/**
* Implementation of hook_insert, which saves todo-specific information
* into the todo table
* @param node object
*/
function todo_insert($node) {

   
// normally, we'd try strtotime, but it won't handle the inverse
    // dd/mm/yyyy formats, so generate the timestamp ourselves
   
preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d)$/', $node->duedate, $m);
   
$ts = mktime(0, 0, 0, $m[2], $m[1], $m[3]);
   
   
db_query("INSERT INTO {todo} (nid, duedate, priority, taskstatus) VALUES (%d, %d, %d, %d)", $node->nid, $ts, $node->priority, $node->taskstatus);
}
?>

Normally, we'd prefer to use the php function strtotime. However, it won't handle the inverse dd/mm/yyyy format, so we parse the string ourselves. Because we've already validated the due date string format, we can just use the preg_match as before.

Take a look at the line with the db_query function call. The first argument of the call is the SQL command to run, containing an argument substitution string '%d'. The following arguments in the db_query call are values that replace these argument substitution strings. Drupal provides protection from SQL injection attacks by using string substitution in its database calls.

See also db_queryd for a version of db_query that outputs the query to the browser for debugging.

If your SQL contains strings, be sure to quote your strings for substitution:

<?php
db_query
('SELECT * FROM {table} WHERE field LIKE \'%s\'', $argument);
?>

At this point, the module will save the data into the database. You can't edit the form, and the data won't display quite right, but it will save.

Download the module thus far, and rename to todo.module before saving and enabling in your Drupal installation.

07. Load the data for viewing and editing

In order for the new nodes to display the additional information, Drupal needs to know where the data is. Implementing the load hook will load the additional node information for our new node from the database, allowing us to both display the additional information when displaying our node type, as well as edit the node.

<?php
/**
* Implemenation of hook_load
* @param node object to load additional information for
* @return object with todo fields
*/
function todo_load($node) {
 
$t = db_fetch_object(db_query('SELECT duedate, priority, taskstatus FROM {todo} WHERE nid = %d', $node->nid));
 
 
// adjust the date to human readable format
 
$t->duedate = date('d/m/Y', $t->duedate); 

  return
$t;
}
?>

Because we're displaying a date to the user, but storing a unix timestamp in the database, we need to manipulate the date field to convert it to human readable format after retrieving the data from the database. This is a simple example of what can be done in the load hook.

However, if we adjust the date in the load hook, we prevent all other subsequent methods of viewing from adjusting the display format of our date - we've forced the format to be day-month-year, which may look odd to some people, and confuse others. We still want to convert it to the default format when editing, though.

Fortunately, we have the prepare hook. Prepare hook functions are called after the load function, but before the node is rendered in a submit form view, and is the best place for data manipulations that need to be done before the node is used when adding or editing.

Rewriting our function, using the prepare hook, we get

<?php
/**
* Implemenation of hook_load
* @param node object to load additional information for
* @return object with todo fields
*/
function todo_load($node) {
   
$t = db_fetch_object(db_query('SELECT duedate, priority, taskstatus FROM {todo} WHERE nid = %d', $node->nid));
    return
$t;
}

/**
* Implemenation of hook_prepare
* @param node object to display
*/
function todo_prepare(&$node) {
   
$node->duedate = date('d/m/Y', $node->duedate);
}
?>

More information about the load hook can be found in the hook_load documentation.

More information about the prepare hook can be found in the hook_prepare documentation.

Download the module thus far, and rename to todo.module before saving and enabling in your Drupal installation.

08. Adjust forms for editing

At this point we can create a node of our new type, save it to the database, reload it from the database, and display it in our edit form (after creating go to the edit url of node/{the new node id}/edit to view the edit form). However, if we try to save an edited version of the node at this point, it won't save.

The workflow for saving a node is slightly different than adding the node. In particular, where the insert hook saves the new node's data, the update hook saves an existing node's data to the database.

For this function, we'll update the database fields as we did with the insert fields. Once again, we need to adjust the date field, converting from human readable format to the unix timestamp.

<?php
/**
* Implementation of hook_update, which saves updated todo-specific
* information into the todo table
* @param node object
*/
function todo_update($node) {

   
// normally, we'd try strtotime, but it won't handle the inverse
    // dd/mm/yyyy formats, so generate the timestamp ourselves
   
preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d)$/', $node->duedate, $m);
   
$ts = mktime(0, 0, 0, $m[2], $m[1], $m[3]);
   
   
db_query("UPDATE {todo} set duedate=%d, priority=%d, taskstatus=%d WHERE nid=%d", $ts, $node->priority, $node->taskstatus, $node->nid);
}
?>

At this point, we can add, save, edit and save our new node type. Problem is, when the node displays, only the task title and description, information stored in the node and node_revision tables, central to Drupal's node system, actually display. We want to display our other fields by default. We'll do this next.

Download the module thus far, and rename to todo.module before saving and enabling in your Drupal installation.

09. Different views of your data

We can now define, add, save, edit and update nodes of the node type todo. However, as mentioned earlier, when viewing nodes of our new node type, only the task title and description display automatically. We need to display the rest of our data.

To render a node with its new node types, we need to implement the view hook. This function will allow us to add our fields to the node content before rendering.

<?php
/**
* Implementation of hook_view, add our node specific information
* @param node object to display
* @param boolean is this a teaser or full node?
* @param boolean is this displaying on its own page
*/
function todo_view(&$node, $teaser = FALSE, $page = FALSE) {
   
$node = node_prepare($node, $teaser);
   
$todo_info = theme('todo_basic_view', $node);
   
$node->body .= $todo_info;
   
$node->teaser .= $todo_info;
}
?>

This function calls node_prepare to process the node through the node's output filters. It then appends the output of the function theme_todo_basic_view.

<?php
/**
* Theme function to display additional node data
* @param node to display
* @return HTML string with additional node information
*/
function theme_todo_basic_view($node) {

 
$priorities = _todo_priorities();
 
$statuses = _todo_priorities();

 
$output = '<div class="todo_view_duedate">';
 
$output .= t('Due date: %duedate', array('%duedate' => format_date($node->duedate, 'custom', 'd/m/Y')));
 
$output .= '</div>';

 
$output .= '<div class="todo_view_priority">';
 
$output .= t('Priority: %priority', array('%priority' => $priorities[$node->priority]));
 
$output .= '</div>';

 
$output .= '<div class="todo_view_taskstatus">';
 
$output .= t('Current status: %taskstatus', array('%taskstatus' => $statuses[$node->taskstatus]));
 
$output .= '</div>';

  return
$output;
}
?>

The node_example.module shows another example of this functionality.

At this point, the node's additional information will display for the node. The view can be changed either with CSS defining the additional classes we added around the content parts, or with a custom node template if your site is using the phptemplate theming engine.

More information about the view hook can be found in the hook_view documentation.

Download the module thus far, and rename to todo.module before saving and enabling in your Drupal installation.

10. Create an install file

// TODO: how to create an install file

Conclusion

This tutorial provides the basics of creating a module that creates a new node type. Futher enhancements to this module could include adding Ajax-y goodness to help with task assignments.

Other enhancements could include adding additional fields such as a dependency list of other nodes (perhaps as a textfield, inputting node ids as a comma separated list) or an assigned-to field (assigning the task to a different user), or making the due date input configurable to the user's desired date input format (mm/dd/yyyy, for example).

Documenting your code

Drupal uses PHPDoc syntax for code documentation with a couple exceptions. See the api.module docs. This api.module creates all the system documentation which you see on http://api.drupal.org. You can use this module to host your own offline copy of drupaldocs.org along with Contrib documentation if you wish.

Drupal Object Reference

Here is a list of the objects and properties that Drupal offers to functions.

&$node - Node object

[name] => Username of node creator
[date] => Date the node was created
[status] => TRUE/FALSE = published/unpublished
[moderate] => Moderation enabled (0|1)
[promote] => Promoted to front page (0|1)
[sticky] => Sticky (0|1)
[revision] => TRUE/FALSE this is a new revision (if TRUE, will be saved as a separate entry in the database)
[comment] => whether comments can be added, read, or accessed, for this node
[simple_access] => Array -- A list of permissions for the Simple Access module
(
[view] => 0
)

[title] => Page title
[taxonomy] => Array -- Taxonomy classification
(
[0] => 0
)

[body] => Body content of node

[format] => which filter applies to this content.
[uid] => User ID of node creator
[created] => UNIX timestamp of node creation date.
[type] => Type of node (e.g. book, page, forum)
[teaser] => Teaser (the initial part of the body)

[validated] => has the node passed validation? (0|1) (is it ready to be saved).
[changed] => UNIX timestamp of last time node was changed.
[nid] => Node ID

Drupal's caching mechanism

In addition to turning on page caching to improve performance for anonymous users, Drupal developers can take advantage of the caching mechanisms to store pieces of data that are expensive to calculate. Start by reading the Lullabot article A Beginners Guide to Caching Data.

How to create your own simple node type (5.x)

A simple node-type (or content type) is the most basic of node types (content types) such as "page" and "story", with a title, teaser, body, etc.

Why would you want or need this...

Drupal 5.x already allows you to easily create these "simple" node-types, doesn't it? The answer is a resounding YES, and everyone is absolutely thrilled with that (thanks Drupal developers!!!).

When Drupal 5.x was ready, I thought this post wouldn't be needed. But later someone told me that it was a great way for people to learn about drupal code, creating modules, etc. So I thought I should put this together, for the folks new to drupal that needed a primer of sorts.

There is a great example of creating your own node-type in the API at node_example. There is also a great tutorial at Creating Modules 5.x Tutorial.

In previous examples "How to create your own simple node type (from story node) (4.7) and (4.6) the "story.module" file was copied as a starting point, but that module no longer exists because of how Drupal 5.x handles node-types (content types), so this example will require you to start with a new file. This example does use the example in the API node_example as the code reference point (and you should definately read that).

There are no database changes or hacking of code, the result will be a new module that you will need to upload into its own module directory.

What you need...

  • means to download/upload files to your modules directory
  • a text editor
  • a little php knowledge is helpful, but not required
  • some information about your node type
    • a TECHNICAL-NAME for your node type - limit 32 characters - don't use the name of an existing module - no spaces, numbers or punctuation unless you know what you're doing
    • a USER-FRIENDLY-NAME for your node type (and a plural version of this name) (this is how your node type will appear in most places on your site) - spaces and numbers are ok but again no punctuation unless you know what you're doing, note: you can use the same name as your TECHNICAL-NAME above
    • a MODULE-DESCRIPTION - a short description of your node type that will appear on the admin->modules page - don't use quotes or apostrophes unless you know what you are doing
    • a CREATE-CONTENT-DESCRIPTION - a short description of your node type that will appear on the create content page - don't use quotes or apostrophes unless you know what you are doing
    • a HELP-DESCRIPTION - a short description of your node type that will appear on the help page - don't use quotes or apostrophes unless you know what you are doing

As an example... say you want a simple press release node type.

  • TECHNICAL-NAME = "press_release" (note the underscore)
  • USER-FRIENDLY-NAME = "Press Release"
  • USER-FRIENDLY-PLURAL = "Press Releases"
  • MODULE-DESCRIPTION = "Enables the creation of press releases."
  • CREATE-CONTENT-DESCRIPTION = "Create a press release."
  • ADMIN-HELP-TEXT = "This module was created by [your name here]."

I specifically use an example that has two words: "Press Release" - this provides a slightly different TECHNICAL-NAME: "press_release" (note the underscore) and USER-FRIENDLY-NAME: "Press Release" (no underscore) so that it is clear in the code changes where the two are used, but many node-types that exist (or that you may want to create) use the same value.

In the following, bolded text is intended to show where the code changes (so PHP formatting is not used). Quotes and apostrophes in the code are important, please place close attention. Where you see ----- this is intended to show where the code starts and ends and is not needed in your code.

There is a huge change that came about with drupal 5.x -- for your module to work you need 2 files. In earlier versions only one file was needed.

The first file is a .info file (say it out loud: "dot info file"). This is the "new" file required by drupal 5.x. This file does not appear to be addressed in the API node_example, in a place I could find. But, you can read more about .info files at Writing .info files (Drupal 5.x) or in the Creating Modules 5.x Tutorial series in section 02. Telling Drupal about your module.

This is not a PHP file so you don't start with the <?php

The general format is:

; $Id$
name = Module Name
description = "A description of what your module does."

NOTE: Without this file, your module will not show up in the module listing at admin/build/modules

This breaks down to:
-----
; $Id$
name = USER-FRIENDLY-NAME
description = "MODULE-DESCRIPTION"
-----

In our example:
-----
; $Id$
name = Press Release
description = "Enables the creation of press releases."
-----

Yes, you only need those 3 lines (minimally)

Save this file as "TECHNICAL-NAME.info" (in our example: "press_release.info")

The second file needed is the .module file (say it out loud "dot module file"). This is the file that does all the heavy lifting.

In the node_example there are a number of hooks listed and exampled. The node_example is one where the node-type has its own additional fields. This how-to does not address that need, but is a only a primer to get you started. If you need additional fields, you will want to learn about those hooks and node_example is a great place to start that.

The .module file is where all the "hooks" appear. Hooks are pretty easy to understand.

This note from the Creating Modules 5.x Tutorial series in section 01. Getting started provides a nice way of describing them.

All functions in your module that will be used by Drupal are named {modulename}_{hook}, where "hook" is a pre-defined function name suffix. Drupal will call these functions to get specific data, so having these well-defined names means Drupal knows where to look.

The following hooks are outlined in node_example in the API

- these hooks should be implemented in every node-type module. This example will implement these 4 hooks.

  • hook_node_info()
  • hook_perm()
  • hook_access()
  • hook_form()

- these hooks are needed if you have additional fields that your node-type creates (note you can use CCK with your node-type to add additional fields). This example will not implement these hooks.

  • hook_insert()
  • hook_update()
  • hook_delete()
  • hook_validate()
  • hook_nodeapi()
  • hook_view()
  • hook_load()

- this hook does not appear in node_example but is a good idea to use. This example will implement this hook.

  • hook_help()

Note: it is considered a good practice to include the following comment before each hook

/**
* Implementation of hook_{hook name here}().
*/

but it is not included in the following.

So let's get started. This is a PHP file so the very first line should be <?php

Implementation of hook_node_info(). This function replaces hook_node_name() and hook_node_types() from 4.6. Drupal 5 expands this hook significantly.

This is a required hook and can define a lot of things about the node-type, minimally the following is required.

From node_example:

<?php
function node_example_node_info() {
  return array(
   
'node_example' => array(
     
'name' => t('example node'),
     
'module' => 'node_example',
     
'description' => t("This is an example node type with a few fields."),
    )
  );
}
?>

This breaks down to:
-----
function TECHNICAL-NAME_node_info() {
  return array(
    'TECHNICAL-NAME' => array(
      'name' => t('USER-FRIENDLY-NAME'),
      'module' => 'TECHNICAL-NAME',
      'description' => t("CREATE-CONTENT-DESCRIPTION"),
    )
  );
}
-----

In our example:
-----
function press_release_node_info() {
  return array(
    'press_release' => array(
      'name' => t('Press Release'),
      'module' => 'press_release',
      'description' => t("Create a press release."),
    )
  );
}
-----

Implementation of hook_perm(). Since we are limiting the ability to create new nodes to certain users, we need to define what those permissions are here. We also define a permission to allow users to edit the nodes they created.

The permissions you define will be referenced by the next hook (hook_access) in the example.
For the curious, this is the same as version 4.7

from node_example:

<?php
function node_example_perm() {
  return array(
'create example node', 'edit own example nodes');
}
?>

node_example uses USER-FRIENDLY-PLURAL to define its permissions, but this note from the Creating Modules 5.x Tutorial series in section 03. Telling Drupal who can use your module provides other advice.

Your permission strings must be unique within your module. If they are not, the permissions page will list the same permission multiple times. They should also contain your module name, to avoid name space conflicts with other modules.

The current naming convention is "action_verb modulename".

This example will follow that advice (although using USER-FRIENDLY-PLURAL is probably fine.

This hook then breaks down to:
-----
function TECHNICAL-NAME_perm() {
  return array('create TECHNICAL-NAME', 'edit own TECHNICAL-NAME');
}
-----

In our example, this becomes:
-----
function press_release_perm() {
  return array('create press_release', 'edit own press_release');
}
-----

Implementation of hook_access(). Node modules may implement node_access() to determine the operations users may perform on nodes. This example uses a very common access pattern.

It is important to note that you should use the same permissions you defined in the hook above
For the curious, this is the same as version 4.7

from node_example:

<?php
function node_example_access($op, $node) {
  global
$user;

  if (
$op == 'create') {
   
// Only users with permission to do so may create this node type.
   
return user_access('create example node');
  }

 
// Users who create a node may edit or delete it later, assuming they have the
  // necessary permissions.
 
if ($op == 'update' || $op == 'delete') {
    if (
user_access('edit own example nodes') && ($user->uid == $node->uid)) {
      return
TRUE;
    }
  }
}
?>

Again, we are following the advice to use the TECHNICAL-NAME and not the USER-FRIENDLY-PLURAL

This breaks down to:
-----
function TECHNICAL-NAME_access($op, $node) {
  global $user;

  if ($op == 'create') {
    // Only users with permission to do so may create this node type.
    return user_access('create TECHNICAL-NAME');
  }

  // Users who create a node may edit or delete it later, assuming they have the
  // necessary permissions.
  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own TECHNICAL-NAME') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}
-----

In our example:
-----
function press_release_access($op, $node) {
  global $user;

  if ($op == 'create') {
    // Only users with permission to do so may create this node type.
    return user_access('create press_release');
  }

  // Users who create a node may edit or delete it later, assuming they have the
  // necessary permissions.
  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own press_release') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}
-----

Implementation of hook_form(). Now it's time to describe the form for collecting the information specific to this node type. This hook requires us to return an array with a sub array containing information for each element in the form.

Forms are in my mind one of the most difficult aspects of drupal to learn. But because of the amazing way drupal implements forms, in this example we have very little code to change.

For the curious, this has been changed somewhat since 4.7

from node_example:

<?php
function node_example_form(&$node) {
 
$type = node_get_types('type', $node);

 
// We need to define form elements for the node's title and body.
 
$form['title'] = array(
   
'#type' => 'textfield',
   
'#title' => check_plain($type->title_label),
   
'#required' => TRUE,
   
'#default_value' => $node->title,
   
'#weight' => -5
 
);
 
// We want the body and filter elements to be adjacent. We could try doing
  // this by setting their weights, but another module might add elements to the
  // form with the same weights and end up between ours. By putting them into a
  // sub-array together, we're able force them to be rendered together.
 
$form['body_filter']['body'] = array(
   
'#type' => 'textarea',
   
'#title' => check_plain($type->body_label),
   
'#default_value' => $node->body,
   
'#required' => FALSE
 
);
 
$form['body_filter']['filter'] = filter_form($node->format);

 
// NOTE in node_example there is some addition code here not needed for this simple node-type

 
return $form;
}
?>

The only change needed for this function is the function name itself - all the code stays the same

This breaks down to:
-----
function TECHNICAL-NAME_form(&$node) {
  (all the code stays the same)
}
-----

In our example, it becomes:
-----
function press_release_form(&$node) {
  (all the code stays the same)
}
-----

Implementation of hook_help().

This hook is not included in the node_example - not sure why

For the curious, this is the same as version 4.7 (as far as I can tell), though some of its functionalities are now incorporated in hook_node_info()

from the hook_form() example in the api

<?php
function hook_help($section) {
  switch (
$section) {
    case
'admin/help#block':
      return
t('<p>Blocks are the boxes visible in the sidebar(s)
        of your web site. These are usually generated automatically by
        modules (e.g. recent forum topics), but you can also create your
        own blocks using either static HTML or dynamic PHP content.</p>'
);
      break;
  }
}
?>

This breaks down to:
-----
function TECHNICAL-NAME_help($section) {
  switch ($section) {
    case 'admin/help#TECHNICAL-NAME':
      return t('ADMIN-HELP-TEXT');
      break;
  }
}
-----

This breaks down to:
-----
function press-release_help($section) {
  switch ($section) {
    case 'admin/help#press_release':
      return t('This module was created by [your name here].');
      break;
  }
}
-----

That's it for the code. Hopefully not too difficult.

Save the file as "TECHNICAL-NAME.module", (in our example: "press_release.module") you're now ready to upload.

Next

Create a directory in the modules directory with your TECHNICAL-NAME (in our example: "press_release")
Upload both files (the .module file and the .info file) to your new directory

Go to your site, administer -> modules, (admin/build/modules) you should see your new module listed, enable it as with any other module.
Note: if after you enable the module, you get a white screen, it means you have an error in the php code of your new module, delete the files from your new directory and everything should be fine.
You need to fix your new module, but it is probably easier to start over, re-read the directions and follow them carefully, if it still doesn't work, then consider these directions a bunch of bunk and try something else.
Your new node type should work the same as with page and story nodes, you will need to enable access to them (administer-> users -> access control), configure them (administer->content->content types), allow for categorization (administer->content->categories), etc.

Sorry for the long directions, I hope they are understandable and usable.

Code that you can copy and paste, then find and replace:

for the .info file

; $Id$
name = USER-FRIENDLY-NAME
description = "MODULE-DESCRIPTION"

for the .module file

<?php
/**
* Implementation of hook_node_info().
*/
function TECHNICAL-NAME_node_info() {
  return array(
   
'TECHNICAL-NAME' => array(
     
'name' => t('USER-FRIENDLY-NAME'),
     
'module' => 'TECHNICAL-NAME',
     
'description' => t("CREATE-CONTENT-DESCRIPTION"),
    )
  );
}

/**
* Implementation of hook_perm().
*/
function TECHNICAL-NAME_perm() {
  return array(
'create TECHNICAL-NAME', 'edit own TECHNICAL-NAME');
}

/**
* Implementation of hook_access().
*/
function TECHNICAL-NAME_access($op, $node) {
  global
$user;

  if (
$op == 'create') {
   
// Only users with permission to do so may create this node type.
   
return user_access('create TECHNICAL-NAME');
  }

 
// Users who create a node may edit or delete it later, assuming they have the
  // necessary permissions.
 
if ($op == 'update' || $op == 'delete') {
    if (
user_access('edit own TECHNICAL-NAME') && ($user->uid == $node->uid)) {
      return
TRUE;
    }
  }
}

/**
* Implementation of hook_form().
*/
function TECHNICAL-NAME_form(&$node) {
 
$type = node_get_types('type', $node);

 
// We need to define form elements for the node's title and body.
 
$form['title'] = array(
   
'#type' => 'textfield',
   
'#title' => check_plain($type->title_label),
   
'#required' => TRUE,
   
'#default_value' => $node->title,
   
'#weight' => -5
 
);
 
// We want the body and filter elements to be adjacent. We could try doing
  // this by setting their weights, but another module might add elements to the
  // form with the same weights and end up between ours. By putting them into a
  // sub-array together, we're able force them to be rendered together.
 
$form['body_filter']['body'] = array(
   
'#type' => 'textarea',
   
'#title' => check_plain($type->body_label),
   
'#default_value' => $node->body,
   
'#required' => FALSE
 
);
 
$form['body_filter']['filter'] = filter_form($node->format);

 
// NOTE in node_example there is some addition code here not needed for this simple node-type

 
return $form;
}

/**
* Implementation of hook_help().
*/
function TECHNICAL-NAME_help($section) {
  switch (
$section) {
    case
'admin/help#TECHNICAL-NAME':
      return
t('ADMIN-HELP-TEXT');
      break;
  }
}
?>

How to create your own simple node type (from story node)

How to create your own simple node type...

  • A simple node type is the most basic of node types such as 'page' and 'story' node types, with a title, teaser, body, etc.
  • Why would you want or need this...
    The answer stems from a discussion about the Difference Between Page and Story
    and that ultimately each node type allows for separate theming, categorization, descriptions, etc...
  • This example walks you through copying the 'story.module' and making the appropriate changes to the php code for your new simple node type.
  • There are no database changes or hacking of code, the result will be a new module file that you will need to upload into it's own module directory.
  • What you need...
    • means to download/upload files to your modules directory
    • a text editor
    • a little php knowledge is helpful, but not required
    • a technical name for your node type (16 character limit) - no spaces, numbers or punctuation unless you know what you're doing (don't use the name of a module you already have)
    • a user-frieldly name for your node type (and a plural version of this name) (this is how your node type will appear in most places on your site) - spaces and numbers are ok but again no punctuation unless you know what you're doing, note: you can use the same name as your technical name above
    • a module description - a short description of your node type that will appear on the administer->modules page - don't use quotes or apostrophes unless you know what you are doing
    • a create content description - a short description of your node type that will appear on the create content page - don't use quotes or apostrophes unless you know what you are doing
  • In the following bolded text in the code boxes is intended to show where the code changes. Quotes and apostrophes in the code are important, please place close attention. It should be noted also that these directions were developed from drupal version 4.6

As an example... say you want a simple press release node type.
technical name = release
user-friendly name = press release
user-friendly plural = press releases
module description = Enables the creation of press releases.
create content description = Create a press release.

  • Download the story.module file from the modules directory
  • Save it as your technical name .module in our example the file will be named release.module
  • Open the new file in your text editor

Now, without getting into too many details on drupal modules and php code....
The first line of code should be <?phpdon't touch that

The next line of code should be

<?php
// $Id: story.module,v 1.167 2005/04/01 15:55:01 dries Exp $
?>

You should remove this, so that there is no confusion with the story module

The next couple lines of code should be
-----
/**
 * @file
 * Enables users to submit stories, articles or similar content.
 */
-----

change the bolded text above to your user-friendly plural, in our example press releases seen below
-----
/**
 * @file
 * Enables users to submit press releases.
 */
-----

The story.module file allows for story node types. The module file implements the following 6 drupal hooks

  • hook_help
  • hook_node_name
  • hook_perm
  • hook_access
  • hook_menu
  • hook_form

Before each hook is implemented you should you will see a php comment

for hook_help...

<?php
/**
* Implementation of hook_help().
*/
?>

and for hook_node_name...
<?php
/**
* Implementation of hook_node_name().
*/
?>

etc...

After the comment, the hook function is called, but instead of the word hook the word story is used as this module was originally for story nodes.

for hook_help...

<?php
function story_help($section) {
?>

and for hook_node_name...
<?php
function story_node_name($node) {
?>

etc...

In the function calls change story to your technical name, in our example release

for hook_help...

<?php
function story_help($section) {
?>

becomes
<?php
function release_help($section) {
?>

and for hook_node_name

<?php
function story_node_name($node) {
?>

becomes
<?php
function release_node_name($node) {
?>

etc...

We will be changing all 6 in the following steps, plus a few more things...

Here are the changes, moving down the file, starting at the top
Again noting that in the following bolded text separtarated by ----- is intended to show where the code changes (note you do note need the ----) in your code. Quotes and apostrophes in the code are important, please place close attention.

1. hook_help
hook_help shows helpful information in various places...

hook_help orginal code from story.module

<?php
function story_help($section) {
  switch (
$section) {
    case
'admin/modules#description':
      return
t('Allows users to submit stories, articles or similar content.');
    case
'node/add#story':
      return
t('Stories are articles in their simplest form: they have a title, a teaser and a body, but can be extended by other modules. The teaser is part of the body too. Stories may be used as a personal blog or for news articles.');
  }
}
?>

here we need our technical name and our two descriptions as indicated
-----
function technical_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('module description');
    case 'node/add#technical':
      return t('create content description');
  }
}
-----

hook_help using our press release example the code becomes
-----
function release_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('Enables the creation of press releases.');
    case 'node/add#release':
      return t('Create a press release.');
  }
}
-----

2. hook_node_name
hook_node_name registers the node type with drupal

hook_node_name original code from story.module

<?php
function story_node_name($node) {
  return
t('story');
}
?>

here we our technical name and our user-friendly name as indicated
-----
function technical_node_name($node) {
  return t('user-friendly name');
}
-----

hook_node_name using our press release example the code becomes
-----
function release_node_name($node) {
  return t('press release');
}
-----

3. hook_perm
hook_perm assigns names for permissions that may exist for this node type (these need to aggree with hook_access below)

hook_perm original code from story.module

<?php
function story_perm() {
  return array(
'create stories', 'edit own stories');
}
?>

here we need our technical name and our plural as indicated
-----
function technical_perm() {
  return array('create plural', 'edit own plural');
}
-----

hook_perm using our press release example the code becomes
-----
function release_perm() {
  return array('create press releases', 'edit own press releases');
}
-----

4. hook_access
hook_access checks your permissions against a user and what the permission allows (these need to aggree with hook_perm above)

hook_access original code from story.module

<?php
function story_access($op, $node) {
  global
$user;

  if (
$op == 'create') {
    return
user_access('create stories');
  }

  if (
$op == 'update' || $op == 'delete') {
    if (
user_access('edit own stories') && ($user->uid == $node->uid)) {
      return
TRUE;
    }
  }
}
?>

here we need our technical name and plural as indicated
-----
function technical_access($op, $node) {
  global $user;

  if ($op == 'create') {
    return user_access('create plural');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own plural') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}
-----

hook_access using our press release example the code becomes
-----
function release_access($op, $node) {
  global $user;

  if ($op == 'create') {
    return user_access('create press releases');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own press releases') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}
-----

5. hook_menus
hook_menus allow the node type to show in various menus, in this case just the create content menu (note the 'create' permission should agree with those above)

hook_menu original code from story.module

<?php
function story_menu($may_cache) {
 
$items = array();

  if (
$may_cache) {
   
$items[] = array('path' => 'node/add/story', 'title' => t('story'),
     
'access' => user_access('create stories'));
  }

  return
$items;
}
?>

here we need our technical name, user-friendly name and plural as indicated
-----
function technical_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array(
      'path' => 'node/add/technical',
      'title' => t('user-friendly name'),
      'access' => user_access('create plural'),
    );
  }

  return $items;
}
-----

hook_menu using our press release example the code becomes
-----
function release_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array(
      'path' => 'node/add/release',
      'title' => t('press release'),
      'access' => user_access('create press releases'),
    );
  }

  return $items;
}
-----

6. hook_form
hook_form allows for a form for adding, editing and deleting nodes of the type

hook_form original code from story.module

<?php
function story_form(&$node) {
 
$output = '';

  if (
function_exists('taxonomy_node_form')) {
   
$output .= implode('', taxonomy_node_form('story', $node));
  }

 
$output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE);
 
$output .= filter_form('format', $node->format);

  return
$output;
}
?>

here we need just our technical name as indicated
-----
function technical_form(&$node) {
  $output = '';

  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('technical', $node));
  }

  $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE);
  $output .= filter_form('format', $node->format);

  return $output;
}
-----

hook_form using our press release example the code becomes
-----
function release_form(&$node) {
  $output = '';

  if (function_exists('taxonomy_node_form')) {
  $output .= implode('', taxonomy_node_form('release', $node));
  }

  $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE);
  $output .= filter_form('format', $node->format);

  return $output;
}
-----

That's it for the code changes. Hopefully not too diffucult.

Next

  • Save the file, you're now ready to upload
  • Create a directory in the modules directory with your technical name in our example the directory will be named release
  • Upload your new module file to your new directoy
  • Go to your site, administer -> modules, you should see your new module listed, enable it as with any other module.
    Note: if you go to your administer -> modules page and get a white screen, it means you have an error in the php code of your new module, delete the file from your new directory and everything should be fine.
    You need to fix your new module, but it is probably easier to start over, re-read the directions and follow them carefully, if it still doesn't work, then consider these directions a bunch of bunk and try something else.

Your new node type should work the same as with page and story nodes, you will need to enable access to them (administer->access control), configure them (administer->content->configure), allow for categorization (administrer->categories), etc.

Sorry for the long directions, I hope they are understandable and usable.

How to create your own simple node type (from story node) (Drupal 4.7)

How to create your own simple node type...

  • A simple node type is the most basic of node types such as 'page' and 'story' node types, with a title, teaser, body, etc.
  • Why would you want or need this...
    The answer stems from a discussion about the Difference Between Page and Story
    and that ultimately each node type allows for separate theming, categorization, descriptions, etc...
  • This example walks you through copying the 'story.module' and making the appropriate changes to the php code for your new simple node type.
  • There are no database changes or hacking of code, the result will be a new module file that you will need to upload into its own module directory.
  • What you need...
    • means to download/upload files to your modules directory
    • a text editor
    • a little php knowledge is helpful, but not required
    • some information about your node type
      • a technical-name for your node type - limit 32 characters - don't use the name of an existing module - no spaces, numbers or punctuation unless you know what you're doing
      • a user-friendly-name for your node type (and a plural version of this name) (this is how your node type will appear in most places on your site) - spaces and numbers are ok but again no punctuation unless you know what you're doing, note: you can use the same name as your technical-name above
      • a module-description - a short description of your node type that will appear on the administer->modules page - don't use quotes or apostrophes unless you know what you are doing
      • a create-content description - a short description of your node type that will appear on the create content page - don't use quotes or apostrophes unless you know what you are doing
      • a help-description - a short description of your node type that will appear on the help page - don't use quotes or apostrophes unless you know what you are doing
      • drupal-help-text and link - (these are specifically provided for you below)
      • an optional title-caption for the title field in the node entry/edit form if something other than 'Title' is desired - don't use quotes or apostrophes unless you know what you are doing
      • an optional body-caption for the body field in the node entry/edit form if something other than 'Body' is desired - don't use quotes or apostrophes unless you know what you are doing
  • In the following bolded text in the code boxes is intended to show where the code changes. Quotes and apostrophes in the code are important, please place close attention.

As an example... say you want a simple press release node type.

  • technical-name = release
  • user-friendly-name = press release
  • user-friendly-plural = press releases
  • module-description = Enables the creation of press releases.
  • create-content-description = Create a press release.
  • help-description = Press Releases are notices sent to various media outlets informing them of some note-worthy information.
  • drupal-help-text = This module was created by [your name here], following directions posted at drupal.org <a href="%release">How to create your own simple node type (from story node) (Drupal 4.7)</a>
    IMPORTANT: Note the use of our technical-name release in the drupal help text above (underlined for emphasis only), you should change the underlined text to your technical-name. You should also change [your name here] to your actual name. The rest of the text should be left as is (unless you know what you are doing).
  • drupal-help-link = http://www.drupal.org/node/40684
  • title-caption (optional) = Press Release Title
  • body-caption (optional) = Press Release Body

I specifically use an example that has a different technical-name and user-friendly-name so that it is a little more clear in the changes where the two are used, but many node-types that exist (or that you may want to create) use the same value.

  • Download the story.module file from the modules directory
  • Save it as your technical-name .module in our example the file will be named release.module
  • Open the new file in your text editor

Now, without getting into too many details on drupal modules and php code....
The first line of code should be <?phpdon't touch that

The next line of code will be something like...

// $Id: story.module,v 1.179 2005/12/05 09:11:33 dries Exp $

You should remove this, so that there is no confusion with the story module

The next couple lines of code should be

/**
* @file
* Enables users to submit <strong>stories, articles or similar content</strong>.
*/

change the bolded text above to your user-friendly-plural, in our example press releases seen below
/**
* @file
* Enables users to submit <strong>press releases</strong>.
*/

The story.module file allows for story node types. The module file implements the following 7 drupal hooks

  • hook_help
  • hook_node_info
  • hook_perm
  • hook_access
  • hook_menu
  • hook_form

Before each hook is implemented you should you will see a php comment

for hook_help...

/**
* Implementation of hook_help().
*/

and for hook_node_info...
/**
* Implementation of hook_node_info().
*/

etc...

After the comment, the hook function is called, but instead of the word hook the word story is used as this module was originally for story nodes.

for hook_help...

function story_help($section) {

and for hook_node_info...
function story_node_info() {

etc...

In the function calls change story to your technical-name, in our example release

for hook_help...

function <strong>story</strong>_help($section) {

becomes
function <strong>release</strong>_help($section) {

and for hook_node_info

function <strong>story</strong>_node_info() {

becomes
function <strong>release</strong>_node_info() {

etc...

We will be changing all 7 in the following steps, plus a few more things...

Here are the changes, moving down the file, starting at the top
Again noting that in the following bolded text in the code boxes is intended to show where the code changes. Quotes and apostrophes in the code are important, please place close attention.

1. hook_help
hook_help shows helpful information in various places...
NOTE - this function has the most changes, so if you can get thru this one, the rest are a breeze.
for the curious: this 4.7 hook_help implementation was expanded with additional help text from 4.6

hook_help original

function <strong>story</strong>_help($section) {
  switch ($section) {
    case 'admin/help#<strong>story</strong>':
      $output = '<p>'. t('The <strong>story</strong> module is used to create a
       content post type called <em><strong>stories</strong>.</em>
       <strong>Stories are articles in their simplest form: they have a title,
       a teaser and a body. Stories are typically used to post news articles
       or as a group blog.</strong>') .'</p>';
      $output .= '<p>'. t('The <strong>story</strong> administration interface allows
       for complex configuration. It provides a submission form, workflow,
       default view permission, default edit permission, permissions for permission,
       and attachments.  Trackbacks can also be enabled.') .'</p>';
      $output .= t('<p>You can</p>
<ul>
<li>post a <strong>story</strong> at <a href="%node-add-<strong>story</strong>">
create content &gt;&gt; <strong>story</strong></a>.</li>
<li>configure <strong>story</strong> at <a href="%admin-node-configure-types">
administer &gt;&gt; content &gt;&gt; configure types &gt;&gt;
<strong>story</strong> configure</a>.</li>
</ul>
', array('%node-add-<strong>story</strong>' => url('node/add/<strong>story</strong>'),
'%admin-node-configure-types' => url('admin/node/configure/types')));
      $output .= '<p>'. t('<strong>For more information please read the configuration
       and customization handbook <a href="%story">Story page</a>.</strong>',
       array('%<strong>story</strong>' => '<strong>http://www.drupal.org/handbook/modules/story/</strong>')) .'</p>';
      return $output;
    case 'admin/modules#description':
      return t('<strong>Allows users to submit stories, articles or similar content</strong>.');
    case 'node/add#<strong>story</strong>':
      return t('<strong>Stories are articles in their simplest form: they have a title,
       a teaser and a body, but can be extended by other modules.
       The teaser is part of the body too.
       Stories may be used as a personal blog or for news articles.</strong>');
  }
}

to change this function, we need most of our new node type information...
function <strong>technical-name</strong>_help($section) {
  switch ($section) {
    case 'admin/help#<strong>technical-name</strong>':
      $output = '<p>'. t('The <strong>technical-name</strong> module is used to create a
       content post type called <em><strong>user-friendly-plural</strong>.</em>
       <strong>help-description</strong>') .'</p>';
      $output .= '<p>'. t('The <strong>user-friendly-name</strong> administration interface allows
       for complex configuration. It provides a submission form, workflow,
       default view permission, default edit permission, permissions for permission,
       and attachments.  Trackbacks can also be enabled.') .'</p>';
      $output .= t('<p>You can</p>
<ul>
<li>post a <strong>user-friendly-name</strong> at <a href="%node-add-<strong>technical-name</strong>">
create content &gt;&gt; <strong>user-friendly-name</strong></a>.</li>
<li>configure <strong>user-friendly-name</strong> at <a href="%admin-node-configure-types">
administer &gt;&gt; content &gt;&gt; configure types &gt;&gt;
<strong>user-friendly-name</strong> configure</a>.</li>
</ul>
', array('%node-add-<strong>technical-name</strong>' => url('node/add/<strong>technical-name</strong>'),
'%admin-node-configure-types' => url('admin/node/configure/types')));
      $output .= '<p>'. t('<strong>drupal-help-text</strong>',
       array('%<strong>technical-name</strong>' => '<strong>drupal-help-link</strong>')) .'</p>';
      return $output;
    case 'admin/modules#description':
      return t('<strong>module-description</strong>.');
    case 'node/add#<strong>technical-name</strong>':
      return t('<strong>create-content-description</strong>');
  }
}

hook_help using our press release example
function <strong>release</strong>_help($section) {
  switch ($section) {
    case 'admin/help#<strong>release</strong>':
      $output = '<p>'. t('The <strong>release</strong> module is used to create a
       content post type called <em><strong>press releases</strong>.</em>
       <strong>Press Releases are notices sent to various media outlets
       informing them of some note-worthy information.</strong>') .'</p>';
      $output .= '<p>'. t('The <strong>press release</strong> administration interface allows
       for complex configuration. It provides a submission form, workflow,
       default view permission, default edit permission, permissions for permission,
       and attachments.  Trackbacks can also be enabled.') .'</p>';
      $output .= t('<p>You can</p>
<ul>
<li>post a <strong>press release</strong> at <a href="%node-add-<strong>release</strong>">
create content &gt;&gt; <strong>press release</strong></a>.</li>
<li>configure <strong>press release</strong> at <a href="%admin-node-configure-types">
administer &gt;&gt; content &gt;&gt; configure types &gt;&gt;
<strong>press release</strong> configure</a>.</li>
</ul>
', array('%node-add-<strong>release</strong>' => url('node/add/<strong>release</strong>'),
'%admin-node-configure-types' => url('admin/node/configure/types')));
      $output .= '<p>'. t('<strong>This module was created by [your name here],
       following directions posted at drupal.org <a href="%<u>release</u>">
       How to create your own simple node type (from story node) (Drupal 4.7)</a></strong>',
       array('%<strong>release</strong>' => '<strong>http://www.drupal.org/node/40684</strong>')) .'</p>';
      return $output;
    case 'admin/modules#description':
      return t('<strong>Enables the creation of press releases.</strong>.');
    case 'node/add#<strong>release</strong>':
      return t('<strong>Create a press release.</strong>');
  }
}

2. hook_node_info
hook_node_info registers the node type with drupal
for the curious: hook_node_info is new in 4.7 and replaced hook_node_name in 4.6

hook_node_info original

function <strong>story</strong>_node_info() {
  return array('<strong>story</strong>' =>
   array('name' => t('<strong>story</strong>'),
   'base' => '<strong>story</strong>'));
}

to change this function, we need our technical-name and our user-friendly-name
function <strong>technical-name</strong>_node_info() {
  return array('<strong>technical-name</strong>' =>
   array('name' => t('<strong>user-friendly-name</strong>'),
   'base' => '<strong>technical-name</strong>'));
}

hook_node_info using our press release example
function <strong>release</strong>_node_info() {
  return array('<strong>release</strong>' =>
   array('name' => t('<strong>press release</strong>'),
   'base' => '<strong>release</strong>'));
}

3. hook_perm
hook_perm assigns names for permissions that may exist for this node type (these need to agree with hook_access below)
for the curious: this 4.7 hook_perm implementation is the same as the 4.6 implementation

hook_perm original

function <strong>story</strong>_perm() {
  return array('create <strong>stories</strong>',
   'edit own <strong>stories</strong>');
}

to change this function, we need our technical-name and our user-friendly-plural
function <strong>technical-name</strong>_perm() {
  return array('create <strong>user-friendly-plural</strong>',
   'edit own <strong>user-friendly-plural</strong>');
}

hook_perm using our press release example
function <strong>release</strong>_perm() {
  return array('create <strong>press releases</strong>',
   'edit own <strong>press releases</strong>');
}

4. hook_access
hook_access checks your permissions against a user and what the permission allows (these need to agree with hook_perm above)
for the curious: this 4.7 hook_access implementation is the same as the 4.6 implementation

hook_access original

function <strong>story</strong>_access($op, $node) {
  global $user;

  if ($op == 'create') {
    return user_access('create <strong>stories</strong>');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own <strong>stories</strong>')
     && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}

to change this function, we need our technical-name and user-friendly-plural
function <strong>technical-name</strong>_access($op, $node) {
  global $user;

  if ($op == 'create') {
    return user_access('create <strong>user-friendly-plural</strong>');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own <strong>user-friendly-plural</strong>')
     && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}

hook_access using our press release example
function <strong>release</strong>_access($op, $node) {
  global $user;

  if ($op == 'create') {
    return user_access('create <strong>press releases</strong>');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own <strong>press releases</strong>')
     && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}

5. hook_menu
hook_menu allows the node type to show in various menus, in this case just the create content menu (note the 'create' permission should agree with above)
for the curious: this 4.7 hook_menu implementation is the same as the 4.6 implementation

hook_menu original

function <strong>story</strong>_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array('path' => 'node/add/<strong>story</strong>',
     'title' => t('<strong>story</strong>'),
      'access' => user_access('create <strong>stories</strong>'));
  }

  return $items;
}

to change this function, we need our technical-name, user-friendly-name and user-friendly-plural
function <strong>technical-name</strong>_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array('path' => 'node/add/<strong>technical-name</strong>',
     'title' => t('<strong>user-friendly-name</strong>'),
      'access' => user_access('create <strong>user-friendly-plural</strong>'));
  }

  return $items;
}

hook_menu using our press release example
function <strong>release</strong>_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array('path' => 'node/add/<strong>release</strong>',
     'title' => t('<strong>press release</strong>'),
      'access' => user_access('create <strong>press releases</strong>'));
  }

  return $items;
}

6. hook_form hook_form allows for a form for adding, editing and deleting nodes of the type for the curious: this 4.7 implementation of hook_form is substantially different from 4.6 hook_form original

function <strong>story</strong>_form(&$node) {

  $form['title'] = array('#type' => 'textfield',
   '#title' => t('<strong><u>Title</u></strong>'),
   '#required' => TRUE, '#default_value' => $node->title);

  $form['body'] = array('#type' => 'textarea',
   '#title' => t('<strong><u>Body</u></strong>'),
   '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
  $form['format'] = filter_form($node->format);

  $form['log'] = array('#type' => 'fieldset', '#title' => t('Log message'),
   '#collapsible' => TRUE, '#collapsed' => TRUE);
  $form['log']['message'] = array('#type' => 'textarea', '#default_value' => $node->log,
   '#description' => t('An explanation of the additions or updates being made to
  help other authors understand your motivations.')
  );

  return $form;
}

to change this function, we need our technical-name and our two optional captions (note the optional captions are underlined for emphasis and do not need to be changed)
function <strong>technical-name</strong>_form(&$node) {

  $form['title'] = array('#type' => 'textfield',
   '#title' => t('<strong><u>title  caption</u></strong>'),
   '#required' => TRUE, '#default_value' => $node->title);

  $form['body'] = array('#type' => 'textarea',
   '#title' => t('<strong><u>body caption</u></strong>'),
   '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
  $form['format'] = filter_form($node->format);

  $form['log'] = array('#type' => 'fieldset', '#title' => t('Log message'),
   '#collapsible' => TRUE, '#collapsed' => TRUE);
  $form['log']['message'] = array('#type' => 'textarea', '#default_value' => $node->log,
   '#description' => t('An explanation of the additions or updates being made to
   help other authors understand your motivations.')
  );

  return $form;
}

hook_form using our press release example
function <strong>release</strong>_form(&$node) {

  $form['title'] = array('#type' => 'textfield',
   '#title' => t('<strong><u>Press Release Title</u></strong>'),
   '#required' => TRUE, '#default_value' => $node->title);

  $form['body'] = array('#type' => 'textarea',
   '#title' => t('<strong><u>Press Release Body</u></strong>'),
   '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
  $form['format'] = filter_form($node->format);

  $form['log'] = array('#type' => 'fieldset', '#title' => t('Log message'),
   '#collapsible' => TRUE, '#collapsed' => TRUE);
  $form['log']['message'] = array('#type' => 'textarea', '#default_value' => $node->log,
   '#description' => t('An explanation of the additions or updates being made to
   help other authors understand your motivations.')
  );

  return $form;
}

That's it for the code changes. Hopefully not too difficult.

Next

  • Save the file, you're now ready to upload
  • Create a directory in the modules directory with your technical-name; in our example the directory will be named release
  • Upload your new module file to your new directory
  • Go to your site, administer -> modules, you should see your new module listed, enable it as with any other module.
    Note: if you go to your administer -> modules page and get a white screen, it means you have an error in the php code of your new module, delete the file from your new directory and everything should be fine.
    You need to fix your new module, but it is probably easier to start over, re-read the directions and follow them carefully, if it still doesn't work, then consider these directions a bunch of bunk and try something else.

Your new node type should work the same as with page and story nodes, you will need to enable access to them (administer->access control), configure them (administer->settings->content types), allow for categorization (administer->categories), etc.

Sorry for the long directions, I hope they are understandable and usable.

Find and replace template for node

I created this to make it easier to create simple nodes quickly using find & replace. It is for the final version of 4.7 and appears to work but I could be missing something as I am new to drupal. All you have to do is fill out the information above for technical-name, user-friendly-plural, etc and then, in a text editor, find and replace **technical-name**, **user-friendly-plural**, **etc** with that information. All the variables have **two asteriks** around them which need to be replaced as well.

<?php
// $Id: **technical-name**.module,v 1.186 2006/03/27 18:02:48 killes Exp $

/**
* @file
* Enables users to submit **user-friendly-plural**.
*/

/**
* Implementation of hook_help().
*/
function **technical-name**_help($section) {
  switch ($section) {
    case 'admin/help#**technical-name**':
      $output = '<p>'. t('The **technical-name** module is used to create a content post type called <em>**user-friendly-plural**.</em> **help-description** ') .'</p>';
      $output .= '<p>'. t('The **user-friendly-name** administration interface allows for complex configuration. It provides a submission form, workflow, default view permission, default edit permission, permissions for permission, and attachments.  Trackbacks can also be enabled.') .'</p>';
      $output .= t('<p>You can</p>
<ul>
<li>post a **user-friendly-name** at <a href="%node-add-**technical-name**">create content &gt;&gt; **user-friendly-name**</a>.</li>
<li>configure **user-friendly-name** at <a href="%admin-settings-content-types-**technical-name**">administer &gt;&gt; settings &gt;&gt; content types &gt;&gt; configure **user-friendly-name**</a>.</li>
</ul>
', array('%node-add-**technical-name**' => url('node/add/**technical-name**'), '%admin-settings-content-types-**technical-name**' => url('admin/settings/content-types/**technical-name**')));
      $output .= '<p>'. t('**drupal-help-text** <a href="%**technical-name**">**user-friendly-name** page</a>.', array('%**technical-name**' => '**drupal-help-link**')) .'</p>';
      return $output;
    case 'admin/modules#description':
      return t('**module-description**');
    case 'node/add#**technical-name**':
      return t('**create-content-description**');
  }
}

/**
* Implementation of hook_node_info().
*/
function **technical-name**_node_info() {
  return array('**technical-name**' => array('name' => t('**user-friendly-name**'), 'base' => '**technical-name**'));
}

/**
* Implementation of hook_perm().
*/
function **technical-name**_perm() {
  return array('create **user-friendly-plural**', 'edit own **user-friendly-plural**');
}

/**
* Implementation of hook_access().
*/
function **technical-name**_access($op, $node) {
  global $user;

  if ($op == 'create') {
    return user_access('create **user-friendly-plural**');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own **user-friendly-plural**') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}

/**
* Implementation of hook_menu().
*/
function **technical-name**_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array('path' => 'node/add/**technical-name**', 'title' => t('**user-friendly-name**'),
      'access' => user_access('create **user-friendly-plural**'));
  }

  return $items;
}

/**
* Implementation of hook_form().
*/
function **technical-name**_form(&$node) {
  $form['title'] = array('#type' => 'textfield', '#title' => t('**title-caption**'), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -5);
  $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => t('**body-caption**'), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
  $form['body_filter']['format'] = filter_form($node->format);
  return $form;
}

How to make a duplicate Book module

Why Do It?

For my particular application I noted a few pros/cons.
Pros

  • have something other than 'book page' appear in the search results (e.g. Employee Handbook)
  • use along with an access module (e.g. Node Access) to restrict different roles to different book instances

Cons

  • After doing this tutorial the core 'printer-friendly version' functionality will be lost. If you know how to fix this please comment or send me a message.

How to Do it

Preparing the Original Book Module to Work with a Duplicate

  1. make sure the book module deactivated on your site.
  2. duplicate the book module and place a copy in your site's /sites/default/modules directory. You may need to create the modules directory.
  3. open up the duplicated file in a text editor use the find-replace tool to replace all instances of if ($type == 'node' && isset($node->parent)) { with if ($type == 'node' && isset($node->parent) && $node->type == 'book') {so the book module isn't confused with the new duplicated one.
  4. delete line 2 beginning with // $Id: book.module because it isn't needed.
  5. Now it's ok to activate the book module. Drupal looks in ths /sites/default/modules directory before looking in the /modules directory so it will ignore the original script.

Duplicating the book module into a new module

figure out what you want the name of the book type to be called. if it's more than one word it will need to contain an underscore. I'll use the convention new_book_type in the rest of this tutorial.

  1. Open up the file you created above and copy the contents to a new file called new_book_type.module
  2. use the find-replace tool to replace all instances of book with new_book_type
  3. save the file and upload to your /sites/default/modules directory.
  4. create a new table in your database using the following SQL query
    CREATE TABLE `<b>new_book_type</b>` (
    `vid` int( 10 ) unsigned NOT NULL default '0',
    `nid` int( 10 ) unsigned NOT NULL default '0',
    `parent` int( 10 ) NOT NULL default '0',
    `weight` tinyint( 3 ) NOT NULL default '0',
    PRIMARY KEY ( `vid` ) ,
    KEY `nid` ( `nid` ) ,
    KEY `parent` ( `parent` )
    ) ENGINE = MYISAM DEFAULT CHARSET = utf8
  5. Enable the module.

Other things you may want to do

  • If the module doesn't appear once you enable it you have an error in your new module. Start over and try again
  • Go to the end of the new file and reword the help text and other instructional text so it is appropriate (e.g. update "A book is a collaborative writing effort: ..." to whatever)
  • Edit your CSS stylesheet to include the new classes or you can style the new module differently

Javascript, jQuery and AJAX

Drupal includes built-in methods for implementing Javascript. Using these methods when you use Javascript will help to keep your code clean and to ensure compatibility with other modules' implementations.

A couple of simple principles guide Drupal's Javascript approach:

  • All pages should be perfectly functional without scripts. Javascript provides alternatives or supplements - not replacements - for standard elements.
  • No Javascript is hard-coded onto pages. Rather, actions are attached dynamically to page elements--and only if needed Javascript support is present.

Javascript in Drupal 4.7

Javascript was introduced to Drupal in the 4.7 release. If you are developing for a later release, see the documentation for Drupal 5+.

Drupal's Javascript tools

Drupal's 4.7 javascript toolkit has three basic components:

  1. drupal.js
    The javascript file drupal.js contains methods for implementing Javascript solutions, including AJAX (see below).
  2. PHP functions
    Specific Drupal functions in PHP help Javascript developers. These include drupal_add_js(), used for adding a js file to a page; drupal_call_js(), used to generate a javascript call; and drupal_to_js(), used to translate data from Drupal PHP to Javascript objects.
  3. Prebuilt tools
    As well as the generic tools in drupal.js and the PHP Javascript functions, Drupal core ships with three prebuilt tools: collapse, used to collapse and expand form fieldsets; progress, used to indicate progress of an action; and autocomplete, used to simulate drop-down select options when users enter text into a textbox.
drupal.js functions

The file drupal.js is a collection of utility functions providing functionality commonly needed in Javascript applications. As drupal.js is loaded automatically the first time a module calls drupal_add_js(), its methods are always available to other scripts.

Testing for appropriate Javascript support

function isJsEnabled() tests for availablility of a number of Javascript methods commonly needed and used in Drupal Javascript applications. This function is commonly used in combination with addLoadEvent() so that a particular behaviour can be implemented only if appropriate Javascript support is available. This test is referred to as a "killswitch". An example is the first lines of autocompete.js:

// Global Killswitch
if (isJsEnabled()) {
  addLoadEvent(autocompleteAutoAttach);
}
AJAX data exchanges

Two functions in drupal.js provide cross-browser methods for AJAX--dynamic data exchanges between the client and server. These can be used, e.g., to dynamically refresh selected page content based on user actions.

function HTTPGet() and its pair function HTTPPost() are used to send data to a server and specify a handler for the response.

function HTTPGet(uri, callbackFunction, callbackParameter) accepts three arguments: the uri to post to, the function to handle the response, and a callback parameter--usually, a reference to the calling object. The uri will generally be a Drupal path pointing (via a menu item) to the function that parses the client request and sends back a response. Since a Get operation sends information in url-encoded arguments, the data being sent is typically appended to the uri. If a node id is being sent, for example, the uri might look like:

'modulename/action/' + nid;

In the PHP handler function, the nid could then be accessed as arg(2).

function HTTPPost(uri, callbackFunction, callbackParameter, object) is similar to HTTPGet() except that it uses a Post rather than Get operation and includes the additional argument, object. This is the data to be posted to the server.

For more information on using these functions, see the tutorial on using AJAX.

Working with CSS class names

CSS class names play an important part in Drupal's Javascript implementation. Several drupal.js methods focus on adding, removing, and manipulating class selectors.

If you're wanting to add a class name to an element, you might be tempted simply to set its classname attribute:

elt.classname = 'name';

But this method would overwrite any existing class name. By using function addClass(node, className), you ensure that a new name is added to any existing ones. addClass() takes as arguments the node being acted upon and the specified class name.

Similarly, removeClass(node, className) (same two arguments) removes only the specified class name, leaving any others in place.

hasClass(node, className) tests if a given element has a given class name, while toggleClass(node, className) will add a class name if its not present, or remove it if it is.

Element position

function absolutePosition(el) will return the absolute position of an element on the screen. The return value is an object with properties of x and y, representing the absolute x and y coordinates of the element.

Adding events

Similar to addClass(), two functions provide methods for adding events to elements. addLoadEvent(func) adds a function to the window onload event. This is typically used in combination with isJsEnabled(), see above.

function addSubmitEvent(form, func) adds a function to a given form's submit event. This is useful for changing the behaviour of a form. See autocomplete.js for a sample usage (to prevent a form from submitting if the suggestions popup is open).

Collapsible Fieldsets
What it is

Makes use of Drupal's collapsible fieldsets for use on your custom forms. There are 2 states that can be specified when the page loads, collapsed and expanded.

How to use it
  1. In your custom forms, break the fields into sections with the <fieldset></fieldset> object.
  2. Place the following scrpt references in your page. You may have to put in absolute URLs.
  3. <script type="text/javascript" src="/misc/drupal.js"></script>
    <script type="text/javascript" src="/misc/collapse.js"></script>
  4. For the fieldsets that you wish to be collapsable, add class=" collapsible" within the fieldset brackets (see example below)
  5. If you want the fieldset to be collapsed, add class=" collapsible collapsed" instead
  6. You may need to put in the < ! --break-- > tag above the form so it doesn't muck up your template when the page is in 'teaser' view.
Example form
<script type="text/javascript" src="/misc/drupal.js"></script>
<script type="text/javascript" src="/misc/collapse.js"></script>

<form name="form1" method="post" action="www.site.com">
  <p>
  <fieldset class=" collapsible"><legend>Collapsible</legend>
  <table>
    <tr>
      <td>Name:</td>
      <td><input type="text" name="name" value="<?php
global $user;if($user->uid) { print check_plain($user->name);}?>
" ></td>
    </tr>
    <tr>
      <td>Email: </td>
      <td><input type="text" name="email" value="<?php
global $user;if($user->uid) { print check_plain($user->mail);}?>
" ></td>
    </tr>
  </table>
  </fieldset>
  <fieldset class=" collapsible collapsed"><legend>Collapsed</legend>
  <table>
    <tr>
      <td>Name:</td>
      <td><input type="text" name="name" value="<?php
global $user;if($user->uid) { print check_plain($user->name);}?>
" ></td>
    </tr>
    <tr>
      <td>Email: </td>
      <td><input type="text" name="email" value="<?php
global $user;if($user->uid) { print check_plain($user->mail);}?>
" ></td>
    </tr>
  </table>
  </fieldset>
Drupal 5

With the new PHP form API (FAPI), collapsible forms can be easily created:

<?php
$form
['contact'] = array(
 
'#type' => 'fieldset',
 
'#title' => t('Contact settings'),
 
'#weight' => 5,
 
'#collapsible' => TRUE,
 
'#collapsed' => FALSE,
);
?>

See the FAPI reference.

Tutorial 1: Creating new Javascript widgets

If you want to implement functionality not already available, you'll create a new Javascript file and then write PHP calls that add to the page what's needed for the Javascript to work.

So, to come up with a simple if fairly useless example, say we want to write a module that allows users to click on specific words and get a message with further information. Here's how we might do it.

  • Use CSS class selectors to identify the elements to add the Javascript to.

    You might be familiar with the approach of adding Javascript to an element through parameters, like this:

    <span onclick="doSomething()">click me</span>

    In Drupal, we consider such hard-coding inelegant and also error-prone. Instead, we provide a way to identify elements that Javascript can be attached to selectively, after the page loads. Anything that can identify an element will do (e.g., an id attribute), but we tend to use CSS class selectors. So, instead of the above, we would output something like:

    <span class="clickInfo">click me</span>

    Often you'll need to pass information to your scripts. In this case, we want to pop up a message--but of what? The simplest way to pass the information is to use another attribute of the element you're giving the class to--in our case, a <span> element. Candidates are id and title attributes. Example:

    <span class="clickInfo" title="Hello world">click me</span>

    Of course, rather than manually outputting the class and other attributes, you'll want to write a PHP function that adds them. In our case, in a click_info.module file, we might use something like the following:

    <?php
    function click_info_make($text, $words) {
      foreach (
    $words as $word => $message) {
       
    $text = str_replace ($word, '<span class="clickInfo" title="' . $message . '">' . $word . '</span>', $text);
      }
      return
    $text;
    }
    ?>

    Now we can add the needed elements to a particular string through a call to that function. So, if we want to make every occurrence of the word "Drupal" clickable, with the message being "rocks!", we could use a _nodeapi hook:

    <?php
    /**
    * Implementation of hook_nodeapi().
    */
    function click_info_nodeapi(&$node, $op, $teaser, $page) {
      switch (
    $op) {
        case
    'view':
         
    $words = array('Drupal' => 'rocks!');
         
    $node->body = click_info_make($node->body, $words);
      }
    }
    ?>
  • Put your functionality in a new Javascript file.

    In most or all cases, you'll want to create a new Javascript file for your new functionality.

    In the file, you'll need at the very least an "autoattach" function. The purpose of this is to attached the desired behaviours to page elements. Note that function and variable names in Drupal Javascript are written in "camel case". Call your javascript file, and its "autoattach" function, after the behaviour. So in our case, we would create a clickInfo.js file, and include in it a clickInfoAutoAttach function.

    The first thing you need to do in your autoattach function is answer the question "What elements do I attach this behaviour to?" Typically, in your autoattach function you will first select all elements of a given node type, then iterate through them and see if they have right class. In our case, we're looking for (a) span elements that (b) have classset to clickInfo. When we find one, we want to attach a particular behaviour to it: that, when clicked, it will pop an alert up, with the data we've encoded in an attribute called info. Just to be fancy, we're going to make the onclick action trigger a custom function. So our autoattach function will look like this:

    function clickInfoAutoAttach() {
      var spans = document.getElementsByTagName('span');
      for (var i = 0; span = spans[i]; i++) {
        if (span && hasClass(span, 'clickInfo')) {
          // Read in the message from the 'title' attribute
          span.message = span.getAttribute('title');
          // Set the title to NULL, so no tooltip will display
          span.removeAttribute('title');
          span.onclick = function() {
            alert(this.message);
          }
        }
      }
    }

    Now all we need to do is ensure this code is run when it should be. To do this, we add a call at the beginning of our script using two more drupal.js functions:

    if (isJsEnabled()) {
      addLoadEvent(clickInfoAutoAttach);
    }

    This snippet (a) tests if appropriate Javascript support is present, and, if so (b) registers our autoattach function to be called when the page has loaded (and, therefore, all the page elements are available to have behaviours attached to them).

  • Add style (optional)
    In many cases, but not all, you'll want to change the display of the elements you're working with. This is best done through an included .css file. In our case, we want to let users know that specific words/phrases are clickable. We can do this through a .css file, click_info.css:
html.js .clickInfo {
  background: yellow;
  cursor: pointer;
}

Note the html.js selector: it selects the class 'js' on the <html> tag. Drupal adds this class through JavaScript, so it means this style will only be used if JavaScript is enabled.

  • Send the needed files

    We've got all the needed elements, so all that remains is to send them to the user. We do this with drupal_add_js(). Ideally, we'll add the Javascript only when we know it's needed on a page--i.e., when we've created target elements. In this case, since we add those elements in a _nodeapi() hook, we can add calls to that function.

    <?php
    /**
    * Implementation of hook_nodeapi().
    */
    function click_info_nodeapi(&$node, $op, $teaser, $page) {
      switch (
    $op) {
        case
    'view':
         
    $path = drupal_get_path('module', 'click_info');
         
    drupal_add_js($path . '/click_info.js');
         
    theme_add_style($path . '/click_info.css');
         
    $words = array('Drupal' => 'rocks');
         
    $node->body = click_info_make($node->body, $words);
      }
    }
    ?>

    Note that we don't need a separate call for adding drupal.js; it's added automatically with the first drupal_add_js() call. Similarly, we don't need to worry about scripts being added twice, as a static variable in drupal_add_js() ensures that already-added files are skipped.

  • See the completed module. To use it:

    • Install and enable the module
    • Create a new node (e.g., a 'story'), and include the word Drupal.
    • View the page. The word Drupal should be highlighted. Click on it to get the alert 'rocks'.
Tutorial 2: Using existing Javascript widgets: autocomplete

This tutorial will lead you through the steps of implementing an autocomplete textfield in your module.

Autocomplete is implemented in Drupal through AJAX. When users type into a textbox, code on the client page dynamically loads new data from the server (a Drupal website) and uses these data to update the user display (provide a drop-down list of matching options, which the user can select from).

Handily, all the mechanics of exchanging data between client and server and of updating the display are handled by the widget, autocomplete.js. Implementing a particular autocomplete textfield requires two parts: (a) a caller (the textfield, with special additions) and (b) a handler, which is a PHP function that parses the request and returns a response.

Prebuilt autocomplete functions

Two autocomplete functions ship with the Drupal core. Each is referenced by an "autocomplete_path"--the uri to which autocomplete requests are sent.

  • user_autocomplete()
    Use this function to load matching user names. Autocomplete path: user/autocomplete.
  • taxonomy_autocomplete()
    Use this function to load matching taxonomy terms from a given vocabulary. Autocomplete path: taxonomy/automplete.

If one of these matches your needs, then all you need to do is include the special #autocomplete_path selector in a form field. Here's an example for user autocomplete (from comment.module):

<?php
$form
['admin']['author'] = array('#type' => 'textfield', '#parents' => array('author'), '#title' => t('Authored by'), '#size' => 30, '#maxlength' => 60, '#autocomplete_path' => 'user/autocomplete', '#default_value' => $author, '#weight' => -1);
?>

For taxonomy autocomplete, you include a vocabulary id, as in this example from taxonomy.module:

<?php
        $form
['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield', '#default_value' => $typed_string, '#maxlength' => 100, '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid, '#required' => $vocabulary->required, '#title' => $vocabulary->name, '#description' => t('A comma-separated list of terms describing this content (Example: funny, bungie jumping, "Company, Inc.").'));
?>
Building a custom autocomplete function

If you want to make your own autocomplete function to answer a need not already met, there are a couple of additional steps.

  • Write a handler function. This will receive an autocomplete request and return data to the client in a form ready to be parsed by automplete.js.

    user_autocomplete() is a good model:

    <?php
    /**
    * Retrieve a pipe delimited string of autocomplete suggestions for existing users
    */
    function user_autocomplete($string) {
     
    $matches = array();
     
    $result = db_query_range("SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER('%s%%')", $string, 0, 10);
      while (
    $user = db_fetch_object($result)) {
       
    $matches[$user->name] = check_plain($user->name);
      }
      print
    drupal_to_js($matches);
      exit();
    }
    ?>

    Note that you are (a) finding matching records based on user input (b) constructing an array of matches (c) converting the array to JavaScript code and (d) outputting the results.

    Also note that we need to escape the user name in the $matches array's values: this is because the values are HTML. Not escaping would open up XSS holes. On the other hand, it also means that you can mark-up the autocomplete suggestions any way you like (for example, by adding a small user picture to each match).

  • Create a menu path to your function.
    Users need to be able to reach your handler. The user_menu() lines are:
<?php
    $items
[] = array('path' => 'user/autocomplete', 'title' => t('user autocomplete'),
     
'callback' => 'user_autocomplete', 'access' => $view_access, 'type' => MENU_CALLBACK);
?>
  • Reference your path in form textfields.
    Having put a handler in place, you can now reference it as an autocomplete path in form fields, as with the existing user/autocomplete and taxonomy/autocomplete handlers.
  • Security note: make sure your autocomplete handler has appropriate menu permissions set on it, and respects existing access control mechanisms. Otherwise, you might be exposing sensitive information through the autocomplete.

Tutorial 3: Creating new widgets with AJAX

AJAX is a catchy acronym used for Javascript applications that dynamically load data from a server, enabling the updating of content without fully refreshing a page.

In Drupal, AJAX functionality is provided through functions in the Javascript file drupal.js. Different web browsers handle client-server communications differently; these functions provide cross-browser methods.

Some AJAX widgets (autocomplete, progress) ship with Drupal, see Tutorial 2.

How you build your own AJAX widgets will of course depend a lot on what you're wanting to do. But here are some basic steps to get you started.

AJAX solution components

At their most basic, AJAX widgets will have three components.

  • Caller
    Page element(s) that call an AJAX update, generally as a result of a user action (click, mouseover, etc.).
  • PHP handler
    A PHP function that receives the user input and returns data to the caller.
  • JS handler
    A Javascript function that receives the PHP response and acts on it (e.g., updates user display).
Example: click_info with AJAX

In Tutorial 1 we coded a stunningly useless module for popping up messages when users click on select words. Here, we'll extend that example to load dynamically the message text from the server using - you guessed it - AJAX.

If you haven't already read it, you might want to glance over Tutorial 1 first.

So here it is, AJAX in three easy steps.

1. Marking up content

As with non-AJAX Javascripting, we want to begin with plain HTML elements, to which behaviours will be attached dynamically after the page loads. The only difference here is that the elements need to have a way of directing the Javascript to the appropriate path on the server. That is, they need to pass to the Javascript the path to post requests to, which will lead to the PHP handler (see below).

Let's call the path of our PHP handler click_info_ajax/example_handler.

In Tutorial 1 we wrote a function that added a CSS class selector and a special info attribute to span elements.

<?php
function click_info_make($text, $words) {
  foreach (
$words as $word => $message) {
   
$text = str_replace ($word, '<span class="clickInfo" t="' . $message . '">' . $word . '</span>', $text);
  }
  return
$text;
}
?>

Here we'll do something similar, only we'll attach the PHP handler path instead, with the word appended to the uri (so it can later be fed into the Javascript as an argument). Note that we generate the uri with url(), so that it will work whether or not clean urls are enabled. (We're using substr_replace() instead of str_replace() so the behaviour will be attached only to the first occurrence of the word.)

<?php
/**
* Add spans to words.
*/
function click_info_ajax_make($text, $words) {
  foreach (
$words as $word) {
   
$pos = strpos($text, $word);
    if (
$pos === false) {
      continue;
    }
   
$text = substr_replace($text, '<span class="clickInfo" title="' . url('click_info_ajax/example_handler/' . $word) . '">' . $word . '</span>', $pos, strlen($word));
  }
  return
$text;
}
?>

Now the Javascript will have what it needs to post a request to the server.

2. The Javascript

Step 2 is writing Javascript to post information to the server, and to interpret the response.

Objects and methods

To attach our new behaviour, we're going to create a new object type, "click info data base" (CIDB, for short) and assign an object instance to our page element. The AJAX functionality will be methods of this new object. This approach might sound complicated at first, but, don't worry, it's actually straightforward.

We'll start with the click_info.js file we wrote in Tutorial 1.

if (isJsEnabled()) {
  addLoadEvent(clickInfoAutoAttach);
}

function clickInfoAutoAttach() {
  var spans = document.getElementsByTagName('span');
  for (var i = 0; span = spans[i]; i++) {
    if (span && hasClass(span, 'clickInfo')) {
      // Read in the message from the 'title' attribute
      span.message = span.getAttribute('title');
      // Remove the title, so no tooltip will display
      span.removeAttribute('title');
      span.onclick = function() {
        alert(this.message);
      }
    }
  }
}

In that case we were popping up text loaded from the element itself. Now, we'll get the data from the server instead. So, instead of attaching a behaviour directly to the element (a span), we'll create a new object type and use that object to do both the behaviour attaching and the AJAX handling.

if (isJsEnabled()) {
  addLoadEvent(clickInfoAutoAttach);
}

function clickInfoAutoAttach() {
  var cidb = [];
  var spans = document.getElementsByTagName('span');
  for (var i = 0; span = spans[i]; i++) {
    if (span && hasClass(span, 'clickInfo')) {
      // Read in the path to the PHP handler
      uri = span.getAttribute('title');
      // Remove the title, so no tooltip will display
      span.removeAttribute('title');
      // Create an object with this uri. Because
      // we feed in the span as an argument, we'll be able
      // to attach events to this element.
      if (!cidb[uri]) {
        cidb[uri] = new CIDB(span, uri);
      }
    }
  }
}

Now we need to define the CIDB object type and give it AJAX methods--the ability to send requests (using a function defined in drupal.js and to receive and act on responses.

We declare a new object type in Javascript simply by creating a function and setting properties and/or methods. In this case, we'll add a method that calls the drupal.js function HTTPGet().

Like its pair HTTPPost() (used when you want to use a Post rather than a Get operation), HTTPGet() provides a cross-browser method for posting data to the server. It takes three arguments: the uri being requested, the method that should be called when data is returned, and a reference to the calling object (so that the return data can be linked with the correct object instance).

We can use the prototype method to add a new method to the database object--receive, which will handle the data returned by the server.

/**
* A click info DataBase object
*/
function CIDB(elt, uri) {
  var db = this;
  // By making the span element a property of this object,
  // we get the ability to attach behaviours to that element.
  this.elt= elt;
  this.uri = uri;
  this.elt.onclick = function() {
    HTTPGet(db.uri, db.receive, db);
  }
}

/**
* HTTP callback function. Raises an a-lert box
*/
CIDB.prototype.receive = function(string, xmlhttp, cidb) {
  if (xmlhttp.status != 200) {
    return alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ cidb.uri);
  }
  // We have access to the span element, since it's an attribute of the cidb object.
  // Remove the 'clickInfo' class, to show that this is already clicked.
  // We do this with another of the functions in drupal.js
  removeClass(cidb.elt, 'clickInfo');
  alert(string);
}
3. The Handler

To handle the requests sent by AJAX clients, you need at least two pieces of code in your module: (1) a request handler function, and (2) a menu item allowing access to the handler.

The PHP handler is simply a function that receives input and returns data. In some cases you might wish to return XML or some other sort of encoded data to be parsed by your Javascript (see user_autocomplete() for an example), or fully formed HTML elements to be appended to user display.

In our case, all we need is a phrase that's going to be output.

<?php
function click_info_ajax_example_handler() {
 
// We've appended the word in question to the uri as the third argument.
 
$string = arg(2);
  switch (
$string) {
    case
'Drupal':
      print
t('Great software!');
      break;
    default:
      print
t('Nothing');
  }
  exit();
}
?>

In some contexts and depending on the browser being used you might run into caching issues, where a browser won't repeat an AJAX request. If this is an issue, try including additional headers in your PHP responder (before any output) instructing the browser not to cache the data.

<?php
  header
("Cache-Control: no-cache, must-revalidate");
 
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
?>

Now make sure users can reach your handler:

<?php
/**
* Implementation of hook_menu
*/
function click_info_ajax_menu($may_cache) {
 
$items = array();
  if (
$may_cache) {
   
$items[] = array(
     
'path' => 'click_info_ajax/example_handler',
     
'title' => t('click info example'),
     
'access' => user_access('access content'),
     
'type' => MENU_CALLBACK,
     
'callback' => 'click_info_ajax_example_handler'
    
);
   }
  return
$items;
}
?>

Now ensure you send the JS and CSS files as needed (see the section "Send the needed files" in Tutorial 1), and you're away.

See the completed module. To use it:

  • Install and enable the module
  • Create a new node (e.g., a 'story'), and include the word Drupal.
  • View the page. The word Drupal should be highlighted. Click on it to get the alert 'Great software!'.
More examples

Of course, this tutorial has only scratched the surface of what can be done with AJAX. To find out more, study the AJAX widgets that ship with Drupal (e.g., autocomplete.js), or contributed modules using AJAX. Examples include chat_window.js and Javascript Tools modules, including Ajaxsubmit, Dynamicload, and Activemenus.

Happy coding!

Tutorial 4: Drupalizing external libraries

Why reinvent, especially when there's so much great open source software available out there?

Probably the quickest and easiest way to get rich functionality is to "Drupalize" external libraries. This tutorial gives some pointers on how to make existing Javascript libraries work seamlessly with Drupal.

Likely candidates

The first step is to identify a likely candidate for Drupalization.

Of course, the best candidates are going to be those that most closely match the Drupal approach. Keep in mind the Drupal criteria of (a) "graceful degradation" or "progressive enhancement"--that is, Javascript should contribute additional functionality to pages that are fully usable without the Javascript, and (b) avoiding mixing of code and content--attaching behaviours to page elements on the basis of CSS classes.

If you're planning to share your work with the community, it will also help to work with code that's GPL or LGPL licensed, so that you can include it in Drupal's CVS repository.

Sample Drupalization: Jscalendar

As an example, we're going to take the popular and richly featured Jscalendar library. Jscalendar is a popup (or static) interactive calendar often used as a date picker.

Step 1: analyze the library

To start off, we need to look at what the library does out of the box. What page content does it use or produce? How does it attach behaviours to page elements?

After loading its needed files, Jscalendar typically includes code like the following (taken from the Jscalendar documentation):

<form ...>

  <input type="text" id="data" name="data" />
  <button id="trigger">...</button>
</form>

A textfield and accompanying button are output. Then an inline script adds calendar behaviours to the elements:

<script type="text/javascript">
  Calendar.setup(
    {
      inputField  : "data",         // ID of the input field
      ifFormat    : "%m %d, %Y",    // the date format
      button      : "trigger"       // ID of the button
    }
  );
</script>

Straightforward, but some red flags in terms of the Drupal approach. We don't want to be outputting page elements (e.g., the button) that will have no use if Javascript is disabled. And we want to avoid inline scripts. So our challenge starts to clarify: we're going to need to dynamically append a button (or similar selector) as needed, and then attach behaviours to it. We'll take our usual approach: use class selectors to designate content that behaviours should apply to. In place of the two code blocks given in the Jscalendar documentation, we'll look for a simple text field element with a class assigned to it:

<form ...>

  <input type="text" id="data" name="data" class="jscalendar" />

</form>
Step 2: PHP work

Before we get to whatever Javascript we need to write, we first need a module to handle the server-side tasks. At the least, we need to:

  • load files as needed--the .js and .css files of the Jscalendar distribution
  • output content--the textfield with a class

We may come up with some additional tasks as we dig into things.

The actual adding of class names to textfields we can leave to users/developers using our module. They'll use the regular Forms API approach:

<?php
  $form
['date'] = array(
   
'#type' => 'textfield',
   
'#attributes' => array('class' => 'jscalendar')
  );
?>

We just need to determine if the library files need to be loaded. We can do so in a _form_alter hook, by testing if any form element has the 'jscalendar' class set:

<?php
function jscalendar_form_alter($form_id, &$form) {
  foreach (
element_children($form) as $key) {
    if (isset(
$form[$key]) && isset($form[$key]['#attributes']) && isset($form[$key]['#attributes']['class']) && !(strpos($form[$key]['#attributes']['class'], 'jscalendar') === FALSE)) {
     
jscalendar_load();
    }
  }
}
?>

In jscalendar_load() we'll use drupal_add_js() and theme_add_style to add the appropriate .js and .css files.

Step 3: Javascript

With the needed library loaded and appropriately classed textfields to work with, we just need to insert the needed additional content - a button - and attach behaviours.

We can do so in a very few lines.

if (isJsEnabled()) {
  addLoadEvent(function() {
    // Select all input elements
    inputs = document.getElementsByTagName('input');
    for (var i = 0; input = inputs[i]; ++i) {
      if (input && (input.getAttribute('type') == 'text') && hasClass(input, 'jscalendar')) {

This first part of the script will look familar if you've read the previous tutorials. As usual, we're adding a load event if appropriate Javascript support is present. In the load function, we grab all the inputs (this is the node type of the text fields we're interested in) and iterate through them, seeing if they have the 'jscalendar' class.

        var button = document.createElement('button');
        button.appendChild(document.createTextNode(' ... '));
        button.setAttribute('id', input.getAttribute('id') + '-button');
        input.parentNode.insertBefore(button, input.nextSibling);

In this second part of the script, we're creating the button we need and then inserting it next to the textfield in question. Before inserting the button, we give it an id. This is in preparation for the next step.

        Calendar.setup(
          {
            inputField  : input.id,         // ID of the input field
            ifFormat    : "%Y-%m-%d %H:%M:%S",    // the date format
            button      : input.getAttribute('id') + '-button',       // ID of the button
            showsTime   : true,
            timeFormat  : 12
          }
        );
      }
    }
  });
}

The last part of the script is taken pretty much straight out of that Jscalendar documentation. We're attaching the calendar behaviour, passing the ids of the textfield and it's newly minted button.

And that's it.

Step 4: Bugfixes and refinements

But not so fast. It turns out that there's an ugly bug showing up: the calendar displays, yes, but it's stretched the full width of the page. What's this about?

Finding the problem might take some digging--or, in this case, a friendly hint from another developer, yched, who points to conflicting CSS in drupal.css:

.calendar table {
  border-collapse: collapse;
  width: 100%;
  border: 1px solid #000;
}

Jscalendar, it turns out, also uses the 'calendar' class for a div, and also has a table within that div. That   width: 100%; is our culprit. And the fix, thankfully, is easy. A tiny .css file:

.calendar table {
  width: auto;
}

Then, of course, we'll inevitably want to do a but of tweaking. Jscalendar comes with localization language files--we'll want to load the appropriate one. We can do this by looking at the global $locale variable. The above PHP code tested only the first-level $form elements, but would miss elements nested in trees. Rather than hard-coding everything in the Javascript file, it would be handy to allow form designers to designate what calendar behaviours they want--we can do this by setting $form attributes and then reading them into the Javascript.

And so on. For the current code, see the Jscalendar module in the Javascript Tools package.

All in all, though, we've been able to leverage quite a bit from a small amount of work and a few lines of code. We get the functionality, without the heavy lifting. But, by taking a bit of time to do it "right", we get code that works easily and seamlessly in Drupal. Hence the attraction of integrating external libraries.

Additional tools and approaches

In addition to Javascript and AJAX approaches used in Drupal core, there is an emerging body of other tools and approaches for introducing "Web 2.0" functionality into Drupal.

  • The Javascript Tools module is a collection of ready-made tools and libraries for achieving common tasks. Features and tools include feature-rich forms, client-side tabbed pages, popup calendars, and more. See the module description for details. This is also a place to consider sharing your Javascript tools and widgets.
  • The S/P Ajax module introduces effects using the increasingly popular Scriptaculous library, built on Prototype. S/P Ajax includes numerous effects, including draggable page elements, pulsing message, and even expanding menus (through the helper module S/P Magic Menus).
  • See also How to use ajax for your modules (with Xajax) for tips on using the Xajax toolkit.

The Drupal 5 version of this page is here. It is already more complete than this page.

Javascript in Drupal 5.0

As of version 5.0, Drupal's built-in methods for implementing Javascript have changed a bit.
Drupal 5.0's new stable of javascript files includes:

drupal.js
General Drupal Javascript Library
jquery.js
jQuery Library, simplifies many javascript functions
autocomplete.js
autocomplete tools
collapse.js
collasping item tools
progress.js
Progressbar tools
textarea.js
Text Area tools
update.js
upload.js
Upload helpers

These files can be found in the /misc directory.
There are also several php functions in includes/common.inc for adding various javascript features
into your modules:

drupal_add_js
Add a JavaScript file, setting or inline code to the page.
drupal_get_js
Returns a themed presentation of all JavaScript code for the current page. References to JavaScript files are placed in a certain order: first, all 'core' files, then all 'module' and finally all 'theme' JavaScript...
drupal_to_js
Converts a PHP variable into its Javascript equivalent.
Changes for 5.0 include
  • addLoadEvent(somefunctionjsfunction) Replaced by: $(document).ready(somefunctionjsfunction) from the jQuery library
  • Many JS functions have now been wrapped in the Drupal object, and some functions have become properties. I'll add these as I find them. (Example: isJsEnabled() function has been replaced by Drupal.jsEnabled property.
F.A.Q.
  • Why isn't jquery.js included on all pages by default?
    Drupal only adds the jquery.js file if at least one call to drupal_add_js() was made. On most pages however, Drupal already does this seemingly automatically, but it really is a core module doing this. Collapsing fields for example, which are virtually everywhere.

Javascript page snippets

Drupal pages can contain javascript code as well as PHP. Utility modules are available to help you in developing Javascript on your site. More info on using Javascript with Drupal here. The pages below contain javascript that you may find useful.

Input Validator

I've only tested this with 4.6. 4.7 and above has a much more sophisticated forms API than 4.6, so you probably wouldn't need this in 4.7 and above (though the code may help with other projects). It may be that you don't need to do client side validation in 4.6, I'm still just learning to navigate the drupal framework. :)

When you display the form you need to have the onsubmit function call Blank_TextField_Validator

form($form, $method = 'POST', $action = "apply/application_update", array('onsubmit'=> 'return Blank_TextField_Validator()'));

The page must also contain the script:

script Language="JavaScript">
<!--
function Blank_TextField_Validator()
{
inputs = document.getElementsByTagName("input");
for (var i = 0; input = inputs[i]; ++i) {
  // className is for IE, class for FF
  if (input.value == "" && ((input.getAttribute("className") == "form-text required") || (input.getAttribute("class") == "form-text required"))){
    alert("Please fill in all required fields.");
    input.focus();
    return(false);
    }
  }
return (true);
}
-->
</script>
Make sidebar blocks collapseable.

Thought I'd share this:
Here's how I made the Block Div's hidable using javascript (and scriptaculous.js). Put it somewhere in the page, or in a separate file, that gets included in the page..

<?php
<script language="javascript" type="text/javascript">
// make blocks collapse by clicking on title.
blocks=document.getElementsByTagName("div");
blocks=$A(blocks);
blocks.each (
function(
block){
  if (
block.className=='block') {
  
title=block.childNodes[1];
  
title.onclick = function () {
   
content=this.parentNode.childNodes[3];
   
content.style.display!='none' ? content.style.display='none' : content.style.display='block';   
  }
}
}
);
</script>
?>

Maybe later I'll add some cookie stuff to let the browser remember which blocks you've got hiding/showing.

Two columns on pages with lists of nodes

This javascript code can be placed in the footer (and the footer must be enabled). It will restyle the nodes so they're narrower, and float them left and right, down the page. I'm using it with the box_gray theme. The theme may affect the operation of this script.

The element ID in the second line may need to be changed for other themes. Do a View Source on the page, and see what ID is used in the HTML.

<script type="text/javascript"><!--
var content = document.getElementById("content-left");
var nodes = content.getElementsByTagName("div");
var nodeNodes = new Array();
for(i=j=0;i<nodes.length;i++)
{
   if (nodes[i].className=='node')
       nodeNodes[j++] = nodes[i];
}
if (nodeNodes.length > 1)
{
   for(i=0;i<nodeNodes.length;i++)
   {
      theNode = nodeNodes[i];
      theStyle = theNode.style;
if ( (i%2)==0 )
{
theStyle.cssFloat = theStyle.styleFloat = "left";
theStyle.width = "45%";
theStyle.clear = "both";
}
else
{
theStyle.cssFloat = theStyle.styleFloat = "right";
theStyle.width = "45%";
}
   }
}
// --></script>
Valid XHTML Link Popups using jQuery

Instead of using a Link Target which breaks XHTML 1.1 validity, you can create a popup link using jQuery.

To do this, you can use XPATH in jQuery functions

Now, in your template file (e.g. node.tpl.php) insert this code:

<?php
drupal_add_js
('$(document).ready(function(){$("a[@href^=http:]").click(function() { window.open(this.href); return false; }    ); } );', 'inline' );
?>

Now, all links external links on your site will now pop up, but internal links will not be affected provided they are not programmed with http: in front of them (most Drupal automatic links are not)

javascript utility modules

Several contrib modules listed in the Utility section offer some more javascript and jQuery functionality.

Here are some:

  • Javascript Tools: Javascript Tools provides both an integrated set of Javascript and AJAX modules and a common set of methods extending those available in Drupal core (drupal.js) for Javascript and AJAX module development in Drupal.
  • JQuery Update: This module facilitates the upgrade JQuery in Drupal 5. JQuery 1.0.1 is included with Drupal 5, however it is not very well supported in the JQuery community. In order to use most current and advanced JQuery functionality you will want to build off a newer version of JQuery. This module includes John Resig's compat-1.0.js plugin that provides backwards compatiblity for newer versions of JQuery to work with the JS code in Drupal 5.
  • JQuery Interface: Interface is a collection of rich interface components which utilizes the lightweight JavaScript library jQuery. With these components you can build rich client web applications and interfaces with the same simplicity as writing JavaScript with jQuery. This module simply places Interface in a central location with one command that can be called by any module that wants to use the library.
  • jQuery Interface Sortable: The Interface Sortable module makes it easy for developers to add JavaScript drag-and-drop form elements to forms and drag-and-drop widgets to non-form pages. It depends on the JQuery Interface module which in turn relies on the JQuery Update module.
  • S/P Ajax: this module introduces effects using the increasingly popular Scriptaculous library, built on Prototype. S/P Ajax includes numerous effects, including draggable page elements, pulsing message, and even expanding menus (through the helper module S/P Magic Menus).
  • AHAH Forms Framework: Ahah forms is a utility module, for adding Ajax/javascript driven incremental page reloading, without needing to write any Javascript.
  • : and more to come...

jQuery Hints and Snippets

Developing sites that use Javascript has been made even easier with the inclusion of jQuery into core as of Drupal 5.0. jQuery syntax is easy and powerful, and a robust library of plug-ins means a lot of the effects you could ever dream to include in your modules and sites are already supported.

Manipulating the Document Object Model (DOM) with jQuery is as simple as knowing a little bit of CSS and a little bit of Javascript. Using a selector function that works using CSS selectors, you tell jQuery you plan to act on a certain set of elements from the DOM. You then chain jQuery methods to the selector to manipulate or query the data in any way imaginable.

To see a list of jQuery methods, keep the documentation site, Visual jQuery, on hand.

The child pages in this section should be related to understanding jQuery syntax in relation to Drupal class and ID naming conventions, providing example code for various jQuery methods, and offering code tips that answer the question, "But how do I do this with jQuery?"

More information on jQuery in Drupal may be found here: http://drupal.org/node/88978

HOWTO: Select Drupal Form Elements

It is easy to manipulate a form element or query an element's attributes with jQuery. Because jQuery's selector function accepts CSS selectors, you can simply select the form element by ID and chain any jQuery method to it to return or manipulate its value or appearance.

When the Drupal Forms API renders a form, it names elements based on their key in the form array. The following element would be rendered with an ID of #edit-code-name:

<?php
  $form
['code-name'] = array(
   
'#type' => 'hidden',
   
'#value' => t('James Bond'),
  );
?>

The syntax to select this form element via Javascript with jQuery is:

$('#edit-code-name')

Note that spaces and underscores are filtered into hyphens when rendered, so a form array key of code-name and code_name would be selected per the example above.

So, if you want to use Javascript to display to the user the value of the hidden code name field, you would use the following:

alert($('#edit-code-name').val());
HOWTO: Tell if a Checkbox is Checked

A simple way to tell if a checkbox has been checked using jQuery is with the :checked CSS "pseudo class." Your selector should use the following syntax:

$('#edit-checkbox-id:checked')

Obviously, you need to replace checkbox-id with the actual name of the form element. This will attempt to grab any checked checkbox with that ID. If none are found, the result set will be null. So, combining this with jQuery's .val() method, you can use the following conditional statement to execute code if a checkbox has been checked:

if ($('#edit-checkbox-id:checked').val() !== null) {
  // Insert code here.
}

(Note: the checkboxes element in the Forms API produces unique IDs for each option. View the source to see what I mean.)

Using jQuery

As of Drupal version 5.0, most of what can be achieved with Javascript will be made possible by the jQuery Javascript library. To quote from the jQuery website:

jQuery is a new type of Javascript library. It is not a huge, bloated framework promising the best in AJAX - nor is it just a set of needlessly complex enhancements - jQuery is designed to change the way that you write Javascript.

Note: Most information about jQuery can and should be obtained from the jQuery website, this section of the handbook will avoid duplicating information better maintained there.

Resources on the web:

History of jQuery in Drupal
History

In versions before Drupal 5.0, javascript effects are made possible through a decent library of functions in /misc/drupal.js. Then effects are achieved with additional aptly named files like collapse.js and autocomplete.js.

One limitation of drupal.js was the fact that its development would never match the pace of dedicated javascript libraries

At the same time, module developers were starting to incorporate advanced effects libraries like to achieve nice effects in their own modules. Aside from the lack of consistency, there were technical issues whereby implementing a couple of cool modules would cause name-space conflicts between the javascript libraries that they implemented.

Shortly after the birth of Drupal Groups the AJAX developers group began looking for agreement about which library should be used. After a few threads like this one jQuery was identified as the best candidate, especially after active support by John Resig the jQuery project lead who agreed to dual-licence jQuery as GPL so that it could be distributed with Drupal.

Other important benefits of jQuery:

  • The core jQuery library is very small (about 15kb)
  • jQuery has a clean, modular approach to plugins
  • The core library will stable, being in perpetual feature freeze from version 1.0.

jQuery is currently incorporated into Drupal HEAD (to be 5.0) and work remains to remove dependencies on drupal.js and friends.

HOWTO: Pass php variables to the javascript

Before starting this I'd suggest you at least walk through the HOWTO: Add a jQuery effect tutorial. This will give you some basic ground to stand on before moving forward. Additionally it would be of some benefit if you at least understood the syntax for variable_get and what it does. Docs for the various functions we will be using can be found here:

The Problem:
The problem is that we'd like to be able to create and store variables in our drupal database that the javascript ultimately uses. The problem with this is that javascript doesn't directly talk to a database, and so we need an intermediate step. Most of the time this would be done with XML in an AJAX style approach, but a typical AJAX implementation is a little bit of overkill for what we're trying to achieve. We don't want to modify the DOM, we just want to pull a variable from the variables table and use it in our javascript.

The Solution:
Drupal has some built in functions that make doing this rather easy: namely drupal_add_js & drupal_to_js.

We're going to start with the assumption that you already have a variable in your variables table named "my_variable". In your module or whatever sort of code you happen to be writing that needs this you almost certainly have a line that calls your .js file. For the purposes of this example, I'll be using a file called "my_js_file.js". $path is a variable that contains the path to my module.

<?php
drupal_add_js
("var my_js_variable = " . drupal_to_js(variable_get('my_variable', '')) . ";", 'inline');
drupal_add_js("$path/my_js_file.js");
?>

Drupal_add_js is doing a number of things for us here, not only is it adding our .js file to the page, but it's also allowing us to format a javascript style variable and add it inline to the page, making it available to our other javascript files. drupal_to_js is taking a variable (retrieved by variable_get) and formatting it for use in javascript. The ";" ends our line of javascript for us, and then the "inline" explicitly states that the script should be placed inline.

The resulting code should look something like this:

<script type="text/javascript" src="/sites/all/modules/my_module/my_js_file.js"></script>
<script type="text/javascript">var my_js_variable = "variable_value";</script>

Hopefully this should enable you to make your modules more dynamic and enable you to start some interactions between your jQuery and the drupal database.

some simple jquery examples

Hello,
there are not many jquery examples on this site yet, so I thought I'd post a couple. Bare in mind, that I am new at jquery. This is aimed at people confortable developing drupal modules/forms but have no/limited experience with jquery. I can see that jquery is a powerful tool and it would be nice to have more drupal specific examples/tutorials available to the drupal community. Constructive criticism and suggestions are welcome.

example 1) checking/unchecking a checkbox to modify a textfield

sample drupal form code

<?php


drupal_add_js
('foobar.js');

   
$form['foobar_A_check'] = array(
   
'#type' => 'checkbox',
    
'#prefix' => '<div class="foobar_A_group">',
       );
    
    
$form['foobar_A_text'] = array(
   
'#type' => 'textfield',
   
'#suffix' => '</div>'
    
);
?>

Drupal will generate html like the following

<div class="foobar_A_group">
   <div class="form_item">
       <input type="checkbox"  ...>
   </div>

   <div class="form_item">
         <label>
           <input type="text"  ...>
         </label>
   </div>

</div>

sample jquery javascript file

if (Drupal.jsEnabled) {
  $(document).ready(function () {
    $("div.foobar_A_group/div.form_item).each(
            function(){
              $(this).children("input:checkbox").click(
                  function(){
                     message = (this.checked) ? "message1" : "message2";
                     $(this).parents().siblings().children("input:text").val(message);
             }); 
            });     
  });
}

Of course there are simpler ways of doing this task, but this code comes in handy when the naming of the checkbox/textfield are dynamic and you do not know the id values.

The code loops through the document, finding all div classes of type "form_item" that are descendants of class "foobar_A_group" divs. It then finds the child input element of type checkbox, and adds a click event to it which switches the message based on it's checked property.

Then it traverses up to its parents' siblings' children (cousin) changing the value of the input element of type text.

example 2a) toggling div visibility

desired html:

<div class="toggle_area">
    <div class="toggle_label">Description</div>
    <div class="toggle_content">Content:
     .
     .
     .
    </div>
</div>

jquery code
if (Drupal.jsEnabled) {
  $(document).ready(function () {
   $('div.toggle_area').find('div.toggle_content').hide().end().find('div.toggle_label').click(function() {
     $(this).next().slideToggle(); });
   });
}
<?php
drupal_add_js
('foobar.js');
   
$form['foobar_A'] = array(
    
'#prefix' => '<div class="toggle_area"><div class="toggle_label">Descriptive Label</div><div class="toggle_content">',
       );
    
$form['foobar_B'] = array(
   
'#suffix' => '</div></div>'
    
);
?>

The jquery code, together with properly placed div's in prefix and suffix elements allows for slideable form elements.

example 2b) toggling div visibility with a checkbox
There is one major drawback of using div's for toggling slideable elements. When you process your form you cannot tell if that region should be processed (if it is hidden, it still contains all of the data that may have been entered in it). You can try wrapping the div around a checkbox, but then the whole region of the div (not just the checkbox) is the toggle switch. To restrict the area to only be the checkbox element, you can modify the above code to:

desired html:

<div class="toggle_area">
    <input type="checkbox">
    <div class="toggle_content">Content:
     .
     .
     .
    </div>
</div>

jquery code
if (Drupal.jsEnabled) {
  $(document).ready(function () {
     $('div.toggle_area').find('div.toggle_content').hide().end().find('input:checkbox').click(function() {
       $(this).parents().children(div.toggle_content').slideToggle();  });
   });
}
<?php
drupal_add_js
('foobar.js');
   
$form['foobar_A'] = array(
     
'#type' => 'checkbox',
     
'#prefix' => '<div class="toggle_area">',
     
'#suffix' => '<div class="toggle_content">',
       );
    
$form['foobar_B'] = array(
   
'#suffix' => '</div></div>'
    
);
?>

HOWTO: jQuery with Drupal 4.7

Update: There is now a jquery47 module

While jQuery has been incorporated in the lastest version of Drupal, which will soon become Drupal 5.0. However, you may like to incorporate jQuery effects in your 4.7 installation. This is not recommended if you are not able to patch code and troubleshoot your Drupal installation PHP and Javascript. Much of the information for this tutorial is taken from using jQuery in a 4.7 module on Drupal Groups and jQuery + new drupal_add_js for 4.7 in the Drupal issues queue.

Warning: Backup your database and codebase first. It is likely that following these instructions will break your upgrade path to Drupal 5.0. Proceed at your own risk!

These are the steps I have taken most recently. These instructions will go stale and need to be improved over time.

  1. Grab a copy of the current Drupal 4.7 codebase.
  2. Apply the this patch add jQuery which changes some Drupal core functions and adds jQuery library in the /misc directory
  • Currently the patch does not apply fully, be prepared for some manual patch of the drupal.js part
  • Go through each of the *.js files in /misc (other than jquery.js) and replace all instances of $( with $id(
  • . This will restore standard drupal effects where the "$" namespace was overwritten by jQuery.

    You should now have jquery.js output in your webpages, without sacrificing the existing Drupal javascript effects.

HOWTO: Add a jQuery effect

As a very simple introduction to using jQuery, this tutorial demonstrates how a jQuery effect can be added to a DOM element. The purpose is to help those with very little Javascript experience to understand the basic steps.

To start, your Drupal installation needs to have a functioning jQuery library. jQuery is included in Drupal 5.x. Alternatively, you can following these instructions to enable jQuery in 4.7.x (not recommended for production websites).

Once jQuery is loading into every page header, create a new node with the Input Format PHP Code and the following content.

<?php

drupal_add_js
(
   
'$(document).ready(function(){$("p.jtest").fadeIn(6000);}); ',
   
'inline');
?>


<p class="jtest" style="background-color: palegreen; width: 30em;">

This is an example of an effect which is built into the core jQuery
library. This text should fade in after the DOM is loaded. <a
href="http://api.drupal.org/api/HEAD/function/drupal_add_js">
drupal_add_js()</a> was used to add the <a href="http://jquery.com/api/"> fadeIn</a> effect to any paragraph with the class <b>jtest</b>.

</p>

The actual code to attach the fadeIn effect is one line. The usage of all the jQuery functions are documented in these jQuery API docs.

Plug-ins can be added anywhere in your Drupal installation (eg. /sites/default/plugins/) and inserted into the web page using drupal_add_js(). The plugin will then be referenced in the head of the web page.

The plug-in authors usually provide examples to help you use their plug-ins. Be careful to note the licence of each plug-in you use (look in the .js file) as some may not be licenced under an open-source licence like GPL, and therefore you cannot use them in a production website without agreement from the author.

PostgreSQL for modules

Hello everyone, I am writing this to explain to others how I got Drupal and most of the current modules up and running in a exclusively PostgreSQL environment.

First off you have to ask yourself is it really worth it? In most cases no absolutely not, MySQL is just fine for sites that are small and want to remain that way, this includes personal sites and blogs etc.

PostgreSQL's real strength at least in my opinion comes from its ability to seamlessly cluster, thereby distributing the load among many DB servers rather than bogging down a single server. If you have a big website with lots of traffic and have the resources for multiple servers, then PostgreSQL is definitely the way to go.

The first thing you want to do after deciding you want a particular module is to look at the SQL dump file and see if the table create statements are going to need to be converted from MySQL. In many cases the author is kind enough to include a proper .pgsql file, however there are many others who have not gone this little extra distance, and for this we need a proper conversion tool.

The tool that comes with pgsql for this is pretty weak, so I don't bother to use it. Also you need command line access which may not be something that is convenient at the time.

I use this tool, EasyMOD :: SQL Parser.
It does an excellent job of converting to/from MySQL and PostgreSQL.

It's far from perfect, and of course the MySQL dump files are not proper SQL, so you will have to work on it.
2 things that will be a constant annoyance. Table=MyISAM or Table=InnoDB those 2 types of statements are unknown and irrelevant to the rest of the SQL world, and should be removed prior to entering the SQL dump file into the conversion tool.

Another thing I've found that the tool can't understand is multibound keys, the statement looks like this
KEY nid(nid,mnid)
Should be changed to read
KEY (nid)

The ENUM datatype used by some of the dumps should be changed to
char(1)

And anything that has AUTO_INCREMENT should have AUTO_INCREMENT removed and have its type changed to SERIAL or BIGSERIAL.

Other than that, the converter works fantastic, just run your code through the converter, take the resulting SQL and paste it into whatever tool you use to query your DB.

After all that has been done, just test your modules, and see if they throw any errors, also check the DB and make sure no errant code is messing with the tables.

In 7/10 cases no patching is required, there are only 3 modules I can think of at present that need any patching at all. One was in ecommerce, the other was in User Points, the final one was in Terms & Conditions.

The most common thing to have to fix in modules is a non-sql compliant query.
In the only cases I have seen, the problem stems from an improper ORDER BY.

In MySQL "SELECT field1,field2,field3 FROM tablename WHERE condtion, ORDER BY field1" is perfectly valid syntax.
PostgreSQL will throw an error that looks something like
Query ERROR: Statement ORDER BY requires field2.

In this case, you will want to add field2 and field3 to the ORDER BY statement.
The resultant SQL should look something like this.
"SELECT field1,field2,field3 FROM tablename WHERE condition ORDER BY field1,field2,field3"

One other thing I have noticed is a NOT NULL violation, the only place I have seen this occur is in the terms and conditions module, after attempting to post your T&C,

This is caused by the module not placing a value in the T&C index field, and yet having the T&C index field set to not allow a NULL value. The best fix I have found to this is to ALTER the table at the tc_id column and change it to SERIAL. (If you get a "SERIAL is not a valid datatype" error or something like that the easiest solution then is to drop the entire table and remake with tc_id as SERIAL and conditions as TEXT).

Well those are all the tips I have at the moment, I hope you find this information useful.

Third party applications integration guide

This section addresses developer needs beyond implementing new modules. Existing sites with unique applications and needs may require additional development to integrate, migrate or co-exist with Drupal. This will be a repository of tips and techniques shared by those who managed to develop such integration projects. Some tips may be unique to a third party application, and some may be reusable by several applications. The most frequent asked questions relate to:

Session handler issues

Managing your session issues will greatly depend on how your application handles sessions, But the Drupal side of things is straight forward. The session.inc file is included in the bootstrap.inc file.

If you decide to exclusively use your application's session handler, you can simply comment out that line from bootstrap.inc. Or you may decide to allow both to do different parts, where your application session handles authentication, and let Drupal sessions handle the rest of Drupal functions. If you decide to use both, make sure the two session variables are not in conflict. Drupal uses $_SESSION.

If you decide to eliminate Drupal sessions, these are the parts of Drupal core you will need to modify. Drupal core will use sessions for 4 functions:

Authentication: This part can be ignored, as you will be using your application's session to handle this part, to identify your users and their roles, as explained in the authentication section.

Content filtering: Drupal sessions are used to store the current filters you use to filter your site's content list in the content administration page. This is only done in the node module. If you decide to store this date into your application session, this is the part where you will need to save these filters into your session:

<?php
 
// Initialize/reset filters
 
if (!isset($_SESSION['node_overview_filter']) || !is_array($_SESSION['node_overview_filter']) || $op == t('Reset')) {
   
$_SESSION['node_overview_filter'] = array();
  }
 
$session = &$_SESSION['node_overview_filter'];
 
$filter = $edit['filter'];
  if ((
$op == t('Filter') || $op == t('Refine')) && isset($filter)) {
    if (isset(
$filters[$filter]['options'][$edit[$filter]])) {
     
$session[] = array($filter, $edit[$filter]);
    }
  }
  if (
$op == t('Undo')) {
   
array_pop($session);
  }
?>

Messages: These are the messages Drupal displays to the user following an action, such "The changes have been saved." These messages are set into the session in one central place, functions drupal_set_message() and drupal_get_messages() in bootstrap.inc. You can use those locations to store the message into your application sessison.

Comment preferences: This is done in the comment module to store comment preferences for guests. You can modify function comment_save_settings() in the comment module to store these into your application session.

Sharing a user base

The two obvious choices are to either have Drupal use a third party user table, or the other way around (Synchronization is covered in another section). This section will address how Drupal can be made to use an alternative user table.

There is a strategic location in includes/database.inc, where Drupal performs table prefix translation. By trapping the '{users}' you have the power to rewrite all Drupal users query, and can perform your own SQL rewrites.

This has the advantage of not modifying other Drupal files, or minimizing any needed edits.

Use something similar (and cleaner) to this code to perform your own user table query manipulation. This is used at the beginning of db_prefix_tables(), before Drupal performs any table translation.

<?php
if (strpos($sql, '{users}')) {

  if (
eregi('^update', $sql)) {
   
// someone installed a module that updates the user table
    // this is not supported, Issue and error, and quit.
   
CallCustomException();
  }
 
$DrupalUserSQL = array(
   
'@{users}@i',
   
'@= u.uid@i',
   
'@=u.uid@i',
   
'@u.uid =@i',
   
'@u.uid=@i',
   
'@u.uid@i',    // general selct
   
'@u.uid@i',    // group by
   
'@u.uid@i',    // order by
   
'@u.name@i',   // group by
   
'@u.name@i',   // order by
   
'@u.pass@i',
   
'@u.mail@i',
   
'@u.language@i',
   
'@u.picture@i',
   
'@u.picture,@i',
   
'@u.picture,@i',
   
'@u.data@i',
   
'@, u.data@i',
   
'@u.data,@i',
   
'@u.status@i');
 
$ThirdPartyUserSQL = array(
   
TABLE_PREFIX . 'user', // custom db prefix
   
'= u.userid',
   
'=u.userid',
   
'u.userid =',
   
'u.userid=',
   
'u.userid as uid',
   
'u.userid',
   
'u.userid',
   
'u.username',
   
'u.username',
   
'u.password',
   
'u.email',
   
'u.languageid',
   
'1',         // removes group/order by picture, data, status, etc.
   
'',
   
'',
   
'1',
   
'',
   
'',
   
'2');

    if (
strpos($sql, 'registered_name')) {
     
$DrupalUserSQL = array_merge(array('@u.name AS registered_name@i') , $DrupalUserSQL);
     
$ThirdPartyUserSQL = array_merge(array('u.username AS registered_name') , $ThirdPartyUserSQL);
    }
    else {
     
$DrupalUserSQL = array_merge(array('@u.name@i') , $DrupalUserSQL);
     
$ThirdPartyUserSQL = array_merge(array('u.username AS u.name') , $ThirdPartyUserSQL);
    }

   
$sql = preg_replace($DrupalUserSQL, $ThirdPartyUserSQL, $sql, 1);
?>

The exception/error thrown above is trap any locations during the development where the users table is being updated. It could remain there, to ensure all new modules have been tested or modified to handle user table updates.

Note that you will probably still need to edit or replace the user module, since allowing both Drupal and your third party application to administer users can have negative side effects.

Theme engine integration

Your site's theme may have special content and features that do not exist in Drupal themes. This section will explain how you can use your own themes using Drupal's theme engines. If all you need to achieve a common "look and feel", you do not need to modify a theme engine, you can simply create a new template based on your site's design elements.

Drupal currently has the following theme engines: Xtemplate, PHPTemplate, Smarty and Plain PHP. You can read an in-depth explanation of these template engines in the Theme developer's guide.

Of these four theme engines, the simplest for integration purposes is the Plain PHP method.

In most cases, your theme integration will require a header and a footer. Those two regions will require elements from both Drupal and your application, particularly to include Java scripts used by both applications and onload elements.

Having read the Plain PHP guide, the integration with your templates will simply require including your application's template engine (if not already included elsewhere, like in settings.php), and adding any required globals from within your plain PHP theme.

Example:

"Example to follow after checking why PHP snippets cannot be added here.."

Using the APIs available through contributed modules

An application program interface (API) is a set of methods and routines for building software applications. As well as the core APIs (the node, user, and taxonomy systems, etc.), there are several important APIs made available through contributed modules. Using these contributed APIs can both greatly ease your module development and also increase interoperability with other Drupal modules.

Views

The Views module provides a flexible method to control how lists of posts are retrieved and presented. See the Views Module Developer API for detailed instructions on how to expose your module's tables and fields to views operations.

Actions and Workflows

The actions module allows the configuration of Drupal actions. A Drupal action is a specially written PHP function whose parameters are configured through the web. For example, the Send Email action has parameters Recipient, Subject, and Message.

The workflow module allows the creation and assignment of arbitrary workflows to Drupal node types. Workflows are made up of workflow states. For example, a workflow with the states Draft, Review, and Published could be assigned to the Story node type.

Together, actions and workflows provide powerful and handy module implementation tools.

E-Commerce

E-Commerce comes with a developed API to facilitate extension of the suite of modules. The developer documentation provides a quick introduction to the API designed to get you started developing E-Commerce solutions.

Location

The Location module provides geographic functionality related to addresses, and includes an API for working with and extending the included functionality. For developer documentation, see the files location_API.txt and extending_support.txt included with the Location distribution.

Organic Groups

Module can add/remove/edit subscribers from groups using the API functions provided in the module. Modules can also create/delete groups since groups are just nodes of type=og. See the contrib directory in the og package to see examples of modules using this API (ecommerce integration, civicrm integration, forum integration, book integration, etc.).

Writing .install files

As of version 4.7, modules authors can use .install files to do module setup work.

A .install file is run the first time a module is enabled, and is used to do setup required by the module. The most common task is creating database tables and fields (which prior to version 4.7 was done manually).

.install files are also used to perform updates when a new version of a module needs it.

Install instructions

Install instructions are enclosed in a _install() function. An typical example, creating a module table:

<?php
// nodereference.install

function nodereference_install() {
  switch (
$GLOBALS['db_type']) {
    case
'mysql':
    case
'mysqli':
     
// the {tablename} syntax is so multisite installs can add a
      // prefix to the table name as set in the settings.php file
     
db_query("CREATE TABLE {node_field_nodereference_data} (
          vid int unsigned NOT NULL default '0',
          field_name varchar(32) NOT NULL default '',
          delta int unsigned NOT NULL default '0',
          field_nid int unsigned NOT NULL default '0',
          PRIMARY KEY  (vid,field_name,delta)
        ) /*!40100 DEFAULT CHARACTER SET utf8 */;"
);
      break;

    case
'pgsql':
     
db_query("CREATE TABLE {node_field_nodereference_data} (
          vid serial CHECK (vid >= 0),
          field_name varchar(32) NOT NULL default '',
          delta integer NOT NULL default '0' CHECK (delta >= 0),
          field_nid integer NOT NULL default '0' CHECK (field_nid >= 0),
          PRIMARY KEY  (vid, field_name, delta)
        )"
);

     
// Pgsql requires keys and indexes to be defined separately.
      // It's important to name the index as {tablename}_fieldname_idx
      // (the trailing _idx!) so update scripts can be written easily
     
db_query("CREATE INDEX {node_field_nodereference_data}_field_name_idx
                ON {my_table} (field_name)"
);
      break;
  }
}
?>

Update instructions

.install files can also include update instructions, which are used through update.php like regular Drupal updates. Each update is placed in a modulename_update_x() function (where x is an incrementing integer). Updates should always increment by 1.

All updates should return an array listing all the actions performed and whether they succeeded. Each action is an array containing a success boolean and a query string.

Simple usage

The update_sql() function returns such a success/query pair, so it is the easiest way to perform updates. For example:

<?php
// example.install

function example_update_1() {
 
$items = array();
 
$items[] = update_sql("ALTER TABLE {example} ADD new_column text");
 
$items[] = update_sql("ALTER TABLE {example} DROP old_column");
  return
$items;
}
?>

Also see database/updates.inc for lots of examples.

Multi-part updates

Some updates may take quite a while depending on the size of the site. To avoids time-outs, they can be performed in smaller pieces across multiple PHP requests.

To make a multi-part update, all you need to do is add a special #finished entry to the return array, set to 0. The update system will keep calling your update function until you return 1 for #finished (or provide no #finished flag at all).

However, you may return any number between 0 and 1 for #finished, which represents the progress in your particular update. This is used to provide more accurate feedback to the user while it is going on.

Note that, because your update may be spread over multiple requests, any state information that you need to keep needs to be stored in the user session. Global or static variables will not work.

For example:

<?php
// Make all node titles uppercase, 20 at a time.
function example_update_2() {
 
// See if we are being called for the first time
 
if (!isset($_SESSION['example_update_2_nid'])) {
   
// These variables keep track of our progress
   
$_SESSION['example_update_2_nid'] = 0;
   
$_SESSION['example_update_2_max'] = db_result(db_query('SELECT MAX(nid) FROM {node}'));
  }

 
// Fetch the next 20 nodes
 
$result = db_query_range('SELECT nid, title FROM {node} WHERE nid > %d ORDER BY nid ASC', $_SESSION['example_update_2_nid']);
  while (
$node = db_fetch_object($result)) {
   
$node->title = drupal_strtoupper($node->title);
   
db_query("UPDATE {node} SET title = '%s' WHERE nid = %d", $node->title, $node->nid, 0, 20);
   
$_SESSION['example_update_2_nid'] = $node->nid;
  }

 
// See if we are done
 
if ($_SESSION['example_update_2_nid'] < $_SESSION['example_update_2_max']) {
   
// Not done yet. Return the progress.
   
return array('#finished' => $_SESSION['example_update_2_nid'] / $_SESSION['example_update_2_max']);
  }
  else {
   
// Done. Clean up and indicate we're finished.
   
unset($_SESSION['example_update_2_nid']);
    unset(
$_SESSION['example_update_2_max']);
    return array(
'#finished' => 1);
  }
}
?>

Some things to note in this example:

  • We decided not to return information about each individual query, because it would become unwieldy very quickly.
  • We chose 20 for the amount of nodes to process at a time. Any sufficiently small number will do as the update system will repeat your update in a single request if there is time.
  • We unset the session variables again when we're done.

UTF-8 update (4.6 to 4.7)

For Drupal 4.7, modules need to perform a special update. Check the relevant section in the module upgrading guide for that.

Writing .schema files

Placeholder for Schema API documentation. Coming soon.

Drupal's page serving mechanism

This is a commentary on the process Drupal goes through when serving a page. For convenience, we will choose the following URL, which asks Drupal to display the first node for us. (A node is a thing, usually a web page.)

http://127.0.0.1/~vandyk/drupal/?q=node/1

A visual companion to this narration can be found here; you may want to print it out and follow along. Before we start, let's dissect the URL. I'm running on an OS X machine, so the site I'm serving lives at /Users/vandyk/Sites/. The drupal directory contains a checkout of the latest Drupal CVS tree. It looks like this:

CHANGELOG.txt
cron.php
CVS/
database/
favicon.ico
includes/
index.php
INSTALL.txt
LICENSE.txt
MAINTAINERS.txt
misc/
modules/
phpinfo.php
scripts/
themes/
tiptoe.txt
update.php
xmlrpc.php

So the URL above will be be requesting the root directory / of the Drupal site. Apache translates that into index.php. One variable/value pair is passed along with the request: the variable 'q' is set to the value 'node/1'.

So, let's pick up the show with the execution of index.php, which looks very simple and is only a few lines long.

Let's take a broad look at what happens during the execution of index.php. First, the includes/bootstrap.inc file is included, bringing in all the functions that are necessary to get Drupal's machinery up and running. There's a call to drupal_page_header(), which starts a timer, sets up caching, and notifies interested modules that the request is beginning. Next, the includes/common.inc file is included, giving access to a wide variety of utility functions such as path formatting functions, form generation and validation, etc. The call to fix_gpc_magic() is there to check on the status of PHP "magic quotes" and to ensure that all escaped quotes enter Drupal's database consistently. Drupal then builds its navigation menu and sets the variable $status to the result of that operation. In the switch statement, Drupal checks for cases in which a Not Found or Access Denied message needs to be generated, and finally a call to drupal_page_footer(), which notifies all interested modules that the request is ending. Drupal closes up shop and the page is served. Simple, eh?

Let's delve a little more deeply into the process outlined above.

The first line of index.php includes the includes/bootstrap.inc file, but it also executes code towards the end of bootstrap.inc. First, it destroys any previous variable named $conf. Next, it calls conf_init(). This function allows Drupal to use site-specific configuration files, if it finds them. The name of the site-specific configuration file is based on the hostname of the server, as reported by PHP. conf_init returns the name of the site-specific configuration file; if no site-specific configuration file is found, sets the variable $config equal to the string $confdir/default. Next, it includes the named configuration file. Thus, in the default case it will include sites/default/settings.php. The code in conf_init() would be easier to understand if the variable $file were instead called $potential_filename. Likewise $conf_filename would be a better choice than $config.

The selected configuration file (normally /sites/default/settings.php) is now parsed, setting the $db_url variable, the optional $db_prefix variable, the $base_url for the website, and the $languages array (default is "en"=>"english").

The database.inc file is now parsed, with the primary goal of initializing a connection to the database. If MySQL is being used, the database.mysql.inc files is brought in. Although the global variables $db_prefix, $db_type, and $db_url are set, the most useful result of parsing database.inc is a global variable called $active_db which contains the database connection handle.

Now that the database connection is set up, it's time to start a session by including the includes/session.inc file. Oddly, in this include file the executable code is located at the top of the file instead of the bottom. What the code does is to tell PHP to use Drupal's own session storage functions (located in this file) instead of the default PHP session code. A call to PHP's session_start() function thus calls Drupal's sess_open() and sess_read() functions. The sess_read() function creates a global $user object and sets the $user->roles array appropriately. Since I am running as an anonymous user, the $user->roles array contains one entry, 1->"anonymous user".

We have a database connection, a session has been set up...now it's time to get things set up for modules. The includes/module.inc file is included but no actual code is executed.

The last thing bootstrap.inc does is to set up the global variable $conf, an array of configuration options. It does this by calling the variable_init() function. If a per-site configuration file exists and has already populated the $conf variable, this populated array is passed in to variable_init(). Otherwise, the $conf variable is null and an empty array is passed in. In both cases, a populated array of name-value pairs is returned and assigned to the global $conf variable, where it will live for the duration of this request. It should be noted that name-value pairs in the per-site configuration file have precedence over name-value pairs retrieved from the "variable" table by variable_init().

We're done with bootstrap.inc! Now it's time to go back to index.php and call drupal_page_header(). This function has two responsibilities. First, it starts a timer if $conf['dev_timer'] is set; that is, if you are keeping track of page execution times. Second, if caching has been enabled it retrieves the cached page, calls module_invoke_all() for the 'init' and 'exit' hooks, and exits. If caching is not enabled or the page is not being served to an anonymous user (or several other special cases, like when feedback needs to be sent to a user), it simply exits and returns control to index.php.

Back at index.php, we find an include statement for common.inc. This file is chock-full of miscellaneous utility goodness, all kept in one file for performance reasons. But in addition to putting all these utility functions into our namespace, common.inc includes some files on its own. They include theme.inc, for theme support; pager.inc for paging through large datasets (it has nothing to do with calling your pager); and menu.inc. In menu.inc, many constants are defined that are used later by the menu system.

The next inclusion that common.inc makes is xmlrpc.inc, with all sorts of functions for dealing with XML-RPC calls. Although one would expect a quick check of whether or not this request is actually an XML-RPC call, no such check is done here. Instead, over 30 variable assignments are made, apparently so that if this request turns to actually be an XML-RPC call, they will be ready. An xmlrpc_init() function instead may help performance here?

A small tablesort.inc file is included as well, containing functions that help behind the scenes with sortable tables. Given the paucity of code here, a performance boost could be gained by moving these into common.inc itself.

The last include done by common.inc is file.inc, which contains common file handling functions. The constants FILE_DOWNLOADS_PUBLIC = 1 and FILE_DOWNLOADS_PRIVATE = 2 are set here, as well as the FILE_SEPARATOR, which is \\ for Windows machines and / for all others.

Finally, with includes finished, common.inc sets PHP's error handler to the error_handler() function in the common.inc file. This error handler creates a watchdog entry to record the error and, if any error reporting is enabled via the error_reporting directive in PHP's configuration file (php.ini), it prints the error message to the screen. Drupal's error_handler() does not use the last parameter $variables, which is an array that points to the active symbol table at the point the error occurred. The comment "// set error handler:" at the end of common.inc is redundant, as it is readily apparent what the function call to set_error_handler() does.

The Content-Type header is now sent to the browser as a hard coded string: "Content-Type: text/html; charset=utf-8".

If you remember that the URL we are serving ends with /~vandyk/drupal/?q=node/1, you'll note that the variable q has been set. Drupal now parses this out and checks for any path aliasing for the value of q. If the value of q is a path alias, Drupal replaces the value of q with the actual path that the value of q is aliased to. This sleight-of-hand happens before any modules see the value of q. Cool.

Module initialization now happens via the module_init() function. This function runs require_once() on the admin, filter, system, user and watchdog modules. The filter module defines FILTER_HTML* and FILTER_STYLE* constants while being included. Next, other modules are include_once'd via module_list(). In order to be loaded, a module must (1) be enabled (that is, the status column of the "system" database table must be set to 1), and (2) Drupal's throttle mechanism must determine whether or not the module is eligible for exclusion when load is high. First, it determines whether the module is eligible by looking at the throttle column of the "system" database table; then, if the module is eligible, it looks at $conf["throttle_level"] to see whether the load is high enough to exclude the module. Once all modules have been include_once'd and their names added to the $list local array, the array is sorted by module name and returned. The returned $list is discarded because the module_list() invocation is not part of an assignment (e.g., it is simply module_list() and not $module_list = module_list()). The strategy here is to keep the module list inside a static variable called $list inside the module_list() function. The next time module_list() is called, it will simply return its static variable $list rather than rebuilding the whole array. We see that as we follow the final objective of module_init(); that is, to send all modules the "init" callback.

To see how the callbacks work let's step through the init callback for the first module. First module_invoke_all() is called and passed the string enumerating which callback is to be called. This string could be anything; it is simply a symbol that call modules have agreed to abide by, by convention. In this case it is the string "init".

The module_invoke_all() function now steps through the list of modules it got from calling module_list(). The first one is "admin", so it calls module_invoke("admin","init"). The module_invoke() function simply puts the two together to get the name of the function it will call. In this case the name of the function to call is "admin_init()". If a function by this name exists, the function is called and the returned result, if any, ends up in an array called $return which is returned after all modules have been invoked. The lesson learned here is that if you are writing a module and intend to return a value from a callback, you must return it as an array. [Jonathan Chaffer: Each "hook" (our word for what you call a callback) defines its own return type. See the full list of hooks available to module developers, with documentation about what they are expected to return.]

Back to common.inc. There is a check for suspicious input data. To find out whether or not the user has permission to bypass this check, user_access() is called. This retrieves the user's permissions and stashes them in a static variable called $perm. Whether or not a user has permission for a given action is determined by a simple substring search for the name of the permission (e.g., "bypass input data check") within the $perm string. Our $perm string, as an anonymous user, is currently "0access content, ". Why the 0 at the beginning of the string? Because $perm is initialized to 0 by user_access().

The actual check for suspicious input data is carried out by valid_input_data() which lives in common.inc. It simply goes through an array it's been handed (in this case the $_REQUEST array) and checks all keys and values for the following "evil" strings: javascript, expression, alert, dynsrc, datasrc, data, lowsrc, applet, script, object, style, embed, form, blink, meta, html, frame, iframe, layer, ilayer, head, frameset, xml. If any of these are matched watchdog records a warning and Drupal dies (in the PHP sense). I wondered why both the keys and values of the $_REQUEST array are examined. This seems very time-consuming. Also, would it die if my URL ended with "/?xml=true" or "/?format=xml"?

The next step in common.inc's executable code is a call to locale_init() to set up locale data. If the user is not an anonymous user and has a language preference set up, the two-character language key is returned; otherwise, the key of the single-entry global array $language is returned. In our case, that's "en".

The last gasp of common.inc is to call init_theme(). You'd think that for consistency this would be called theme_init() (of course, that would be a namespace clash with a callback of the same name). This finds out which themes are available, which the user has selected, and then include_once's the chosen theme. If the user's selected theme is not available, the value at $conf["theme_default"] is used. In our case, we are an anonymous user with no theme selected, so the default xtemplate theme is used. Thus, the file themes/xtemplate/xtemplate.theme is include_once'd. The inclusion of xtemplate.theme calls include_once("themes/xtemplate/xtemplate.inc"), and creates a new object called xtemplate as a global variable. Inside this object is an xtemplate object called "template" with lots of attributes. Then there is a nonfunctional line where SetNullBlock is called. A comment indicates that someone is aware that this doesn't work.

Now we're back to index.php! A call to fix_gpc_magic() is in order. The "gpc" stands for Get, Post, Cookie: the three places that unescaped quotes may be found. If deemed necessary by the status of the boolean magic_quotes_gpc directive in PHP's configuration file (php.ini), slashes will be stripped from $_GET, $_POST, $_COOKIE, and $_REQUEST arrays. It seems odd that the function is not called fix_gpc_magic_quotes, since it is the "magic quotes" that are being fixed, not the magic. In my distribution of PHP, the magic_quotes_gpc directive is set to "Off", so slashes do not need to be stripped.

The next step is to set up menus. This step is crucial. The menu system doesn't just handle displaying menus to the user, but also determines what function will be handed the responsibility of displaying the page. The "q" variable (we usually call the Drupal path) is matched against the available menu items to find the appropriate callback to use. Much more information on this topic is available in the menu system documentation for developers. We jump to menu_execute_active_handler() in menu.inc. This sets up a $_menu array consisting of items, local tasks, path index, and visible arrays. Then the system realizes that we're not going to be building any menus for an anonymous user and bows out. The real meat of the node creation and formatting happens here, but is complex enough for a separate commentary; Drupal's node building mechanism. Back in index.php, the switch statement doesn't match either case and we approach the last call in the file, to drupal_page_footer in common.inc. This takes care of caching the page we've built if caching is enabled (it's not) and calls module_invoke_all() with the "exit" callback symbol.

Although you may think we're done, PHP's session handler still needs to tidy up. It calls sess_write() in session.inc to update the session database table, then sess_close() which simply returns 1.

We're done.

Drupal's node building mechanism

(This walkthrough done on pre-4.5 CVS code in August 2004.)

The node_page controller checks for a $_POST['op'] entry and, failing that, sets $op to arg(1) which in this case is the '1' in node/1. A numeric $op is set to arg(2) if arg(2) exists, but in this case it doesn't ('1' is the end of the URL, remember?) so the $op is hardcoded to 'view'. Thus, we succeed in the 'view' case of the switch statement, and are shunted over to node_load(). The function node_load() takes two arguments, $conditions (an array with nid set to desired node id -- other conditions can be defined to further restrict the upcoming database query) for which we use arg(1), and $revision, for which we use _GET['revision']. The 'revision' key of the _GET array is unset so we need to make brief stop at error_handler because of an undefined index error. That doesn't stop us, though, and we continue pell-mell into node_load using the default $revision of -1 (that is, the current revision). The actual query that ends up being run is

SELECT n.*, u.uid, u.name, u.picture, u.data FROM node n INNER JOIN users u on u.uid WHERE n = '1'

We get back a joined row from the database as an object. The data field from the users table is serialized, so it must be unserialized. This data field contains the user's roles. How does this relate to the user_roles table? Note that the comment "// Unserialize the revisions and user data fields" should be moved up before the call to drupal_unpack().

We now have a complete node that looks like the following:

Attribute Value
body This is a test node body
changed 1089859653
comment 2
created 1089857673
data a:1:{s:5... (serialized data)
moderate 0
name admin
nid 1
picture ''
promote 1
revisions ''
roles array containing one key-value pair, 0 = '2'
score 0
status 1
sticky 0
teaser This is a test node body
title Test
type page
uid 1
users ''
votes 0

All of the above are strings except the roles array.

So now we have a node loaded from the database. It's time to notify the appropriate module that this has happened. We do this via the node_invoke($node, 'load') call. The module called via this callback may return an array of key-value pairs, which will be added to the node above.

The node_invoke() function asks node_get_module_name() to determine the name of the module that corresponds with the node's type. In this case, the node type is a page, so the page.module is the one we'll call, and the specific name of the function we'll call is page_load(). If the name of the node type has a hyphen in it, the left part is used. E.g., if the node type is page-foo, the page module is used.

The page_load() function turns out to be really simple. It just retrieves the format, link and description columns from the page table. The 'format' column specifies whether we're dealing with a HTML or PHP page. The 'link' and 'description' fields are used to generate a link to the newly created page, however, those will be deprecated with the improved menu system. To that extend, the core themes no longer use this information (unlike some older themes in the contributions repository). We return to node_load(), where the format, link and description key-value pairs are added to the node's definition.

Now it's time to call the node_invoke_nodeapi() function to allow other modules to do their thing. We check each module for a function that begins with the module's name and ends with _nodeapi(). We hit paydirt with the comment module, which has a function called comment_nodeapi(&$node, $op, arg = 0). Note that the node is passed in by reference so that any changes made by the module will be reflected in the actual node object we built. The $op argument is 'load', in this case. However, this doesn't match any of comment_nodeapi()'s symbols in its controller ('settings', 'fields', 'form admin', 'validate' and 'delete' match). So nothing happens.

Our second hit is node_nodeapi(&$node, $op, $arg = 0) in the node.module itself. Again, no symbols are matched in the controller so we just return.

We'll try again with taxonomy_nodeapi(&$node, $op, $arg = 0). Again, no symbols match; the taxonomy module is concerned only with inserts, updates and deletes, not loads.

Note that any of these modules could have done anything to the node if they had wished.

Next, the node is replaced with the appropriate revision of the node, if present as an attribute of $node. It is odd that this occurs here, as all the work that may have been done by modules is summarily blown away if a revision other than the default revision is found.

Finally, back in node_page(), we're ready to get down to business and actually produce some output. This is done with the statement

print theme('page', node_show($node, arg(3)), $node->title);

And what that statement calls is complex enough to again warrant another commentary. (Not yet done.)

Drupal menu system (Drupal 6.x)

The Drupal menu system was always much more than what the name suggests. It's not only used to display menu but also to map Drupal paths to their callbacks with proper access checking. The likely rationale behind this; once you define a link to a page, you might want to define what happens when you click that link.

This eventually led to a very complex data structure which is stored as a serialized array in the database -- per user. Unserializing this on every non-cached page load uses tons of memory. Altering this either on build or run time needs dirty hacks.

Some misunderstandings about how access to an element applies to their children led to grave security holes with some contributed modules. This stresses the need for thought out, cleanly defined inheritance rules.

We have a new menu system in Drupal 6.x. The data is divided between two tables: {menu_router} and {menu_links}. The {menu_router} table is built based on the callbacks defined by implementations of hook_menu, and Drupal now looks in this table to determine access and the appropriate callback function when a site visitor tries to navigate to a particular path. Everything belonging to one path is one row in a database table, so the memory footprint is significantly smaller. The inheritance rules for access, etc. are cleanly laid out in the documentation. The {menu_links} table contains the links that are displayed in the Navigation and other menu blocks. Some of these items are derived automatically form {menu_router}, but others may be added by the site administrator using the menu module or other modules.

Menu system overview

In hook_menu, you define a huge menu array which is an associative array. The keys are Drupal paths (with a twist, see the wildcard page for more), the values are menu entries. One menu entry is again an associative array. A typical entry is:

<?php
  $items
['node/%'] = array(
   
'title' => 'View',
   
'page callback' => 'node_page_view',
   
'page arguments' => array(1),
   
'access callback' => 'node_access',
   
'access arguments' => array('view', 1),
   
'map callback' => 'node_load_map',
   
'type' => MENU_CALLBACK,
  );
?>

The menu builder collects these, applies inheritance rules and saves each entry in its row in the menu table.

When you open a page, then the system will generate the ancestors of the given path, and ask the database for the menu entry of the ancestor which best fits this path. Next, if there is a map callback, it applies that on the parts of the path. Then it calls the access callback to determine access. If necessary, If it's given, then it hands over execution to the page callback.

The system determines that it has not found a path if no item can be retrieved from the database or if the map callback returns FALSE. Access denied is solely determined by the access callback/arguments.

Wildcard, ancestors and fitness

Our big problem is that there are paths like node/12345/edit. To handle these and others, we use a wildcard: the percent sign. So, when the system tries to find which menu entry specifies node/12345/edit it will look at the menu definitions of the following paths:

  • node/12345/edit
  • node/12345/%
  • node/%/edit
  • node/%/%
  • node/12345
  • node/%
  • node

These we call the ancestors of node/12345/edit.

Fit

As a wildcard is less specific than the path itself -- for example, if we defined a menu entry for node/12345/edit it will only deal with this one node only but node/%/edit deals with all nodes. We can say that node/12345/edit is a better fit than node/%/edit.

So let's add fitness number to these paths:

Path Fitness Fitness in binary
node/12345/edit 7 111
node/12345/% 6 110
node/%/edit 5 101
node/%/% 4 100
node/12345 3 11
node/% 2 10
node 1   1

I think it's quite visible that if we replace the % wildcard with 0 and the specific part with 1 then we get the binary number. We will use this both ways: we can calculate the value fitness of any path easily and also we can generate the ancestors of any given path by generating these binary numbers and replacing backwards.

Ancestors and fitness are used only on run time, not on build time.

Inheritance rules

Before divulging into the various callbacks, we shall know the simple but very powerful inheritance rule. We are using the parents of a given path for this, for example the parents of node/%/view are node/% and node.

If a page or access callback is not defined then we look at the closest parent which has a and use it. If the current item does not have arguments for this callback but the parent does have one, it will be used as well. In other words, a callback and its arguments are inherited as a whole but the arguments can be overwritten. So,

<?php
  $items
['admin/user/roles'] = array(
   
'title' => 'Roles',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('user_admin_new_role'),
  );
 
$items['admin/user/roles/edit'] = array(
   
'title' => 'Edit role',
   
'page arguments' => array('user_admin_role'),
  );
?>

This is a shorthand for:

<?php
  $items
['admin/user/roles'] = array(
   
'title' => 'Roles',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('user_admin_new_role'),
  );
 
$items['admin/user/roles/edit'] = array(
   
'title' => 'Edit role',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('user_admin_role'),
  );
?>

In this case only the callback is inherited. If you would have

<?php
  $items
['admin/user/roles/edit'] = array(
   
'title' => 'Edit role',
  );
?>

then it would equal

<?php
  $items
['admin/user/roles/edit'] = array(
   
'title' => 'Edit role',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('user_admin_new_role'),
  );
?>

In this case both the callback and the arguments are inherited.

We only use inheritance if a definition is missing. In most cases it is better to specify the callbacks. A good use case for inheritance is the admin pages where all pages without an access callback inherit the call to user_access('access administration pages').

Access control

The new menu system will check every visible parent of the menu item for access and if a visible element is access denied then so will be current element. This is done so that the navigation block's behaviour matches the page behaviour.

The access callback and the access arguments decide whether the user has access to a given menu entry or not.

<?php
  $items
['admin/user/roles'] = array(
   
'title' => 'Roles',
   
'description' => t('List, edit, or add user roles.'),
   
'access callback' => 'user_access',
   
'access arguments' => array('administer access control'),
  );
?>

The menu system will call the function user_access with the arguments administer access control. Actually, user_access is so usual is that this is the default. If there is no access callback, even after applying the inheritance rule but the access arguments are defined then the system will add user_access for you:

<?php
  $items
['admin/user/roles'] = array(
   
'title' => 'Roles',
   
'description' => 'List, edit, or add user roles.',
   
'access arguments' => array('administer access control'),
  );
?>

is enough.

Once user_access is applied as default, it's inherited as well. Expecting this can lead to some minor problems.

<?php
  $items
['user'] = array(
   
'title' => 'My account',
   
'page callback' => 'user_view',
   
'page arguments' => array(1),
   
'access callback' => 'user_view_access',
   
'access arguments' => array(1),
  );
 
$items['user/%/delete'] = array(
   
'title' => 'Delete',
   
'page callback' => 'user_edit',
   
'access callback' => 'user_access',
   
'access arguments' => array('administer users'),
   
'type' => MENU_CALLBACK,
  );
?>

The access callback for 'user/%/delete' needs to be defined because otherwise it would inherit the callback from 'user'. Still, both the inheritance rule and the default user_access is so useful that I have not thrown them out because of this small exception.

We support 'access callback' => TRUE (and FALSE of course).

Dynamic arguments replacement

This part is easiest to understand if we begin from the old menu system.

In the past we had code that run on every page request:

<?php
if (arg(0) == 'node' && arg(1) && is_numeric(arg(1)) && ($node = node_load(arg(1)))) {
 
$items[] = array(
   
'path' => 'node/'. $node->nid,
   
'access' => node_access('view', $node),
   
'callback' => 'node_view_page',
   
'callback arguments' => array($node),
  );
}
?>

And then for comment module:

<?php
if (arg(0) == 'comment' && arg(1) == 'reply' && arg(2) && is_numeric(arg(2)) && ($node = node_load(arg(2)))) {
 
$items[] = array(
   
'path' => 'comment/reply/'. $node->nid,
   
'access' => node_access('view', $node),
   
'callback' => 'comment_reply',
   
'callback arguments' => array($node),
  );
}
?>

Now look at these two definitions! If you take apart the if the first part makes sure you are at a given path (node/123, comment/reply/123) and the second half loads the node with an id of 123. The new menu system already knows how to match dynamic paths, even our broken definitons take care for that. For the second half, the is_numeric check can be centralized and then all the menu system needs to know is that we want to perform a load of an object identified by a specific argument and this object happens to be a node.

A very crude translation to new menu system would be:

<?php
  $items
['node/%'] = array(
   
'access callback' => 'node_access',
   
'access argumments' => array('view', '$node'),
   
'page callback' => 'node_view_page',
   
'page arguments' => array('$node'),
   
'the argument that specifies $node' => 1,
   
'object type to load for argument 1' => 'node',
  );
?>

We needed to quote $node because the new menu system does not run this definition on every page reques -- but based on the above it can find out how to produce it because

all the menu system needs to know is that we want to perform a load of an object identified by a specific argument and this object happens to be a node

and we specified the argument and the object type both. We could actually use this notation, but it's simply not nice. Our first observation is that 'the argument that specifies $node' => 1, could be moved in the place of '$node':

<?php
  $items
['node/%'] = array(
   
'access callback' => 'node_access',
   
'access argumments' => array('view', 1),
   
'page callback' => 'node_view_page',
   
'page arguments' => array(1),
   
'object type to load for argument 1' => 'node',
  );
?>

Now, why we are performing object loading for argument 1? Because it's a wildcard. Now, why can't the wildcard tell us something about the object type it replaces?

<?php
  $items
['node/%node'] = array(
   
'access callback' => 'node_access',
   
'access argumments' => array('view', 1),
   
'page callback' => 'node_view_page',
   
'page arguments' => array(1),
  );
?>
<?php
  $items
['comment/reply/%node'] = array(
   
'access callback' => 'node_access',
   
'access arguments' => array('view', 2),
   
'page callback' => 'comment_reply',
   
'page arguments' => array(2),
  );
?>

Note that for matching purposes we will still only use the % wildcard. But when calling comment_reply, the 2 in arguments will be replaced by node_load(arg(2)) if arg(2) is numeric for both occurences of 2.

What happens with the edit tab?

<?php
  $items
['node/%node/edit'] = array(
   
'access callback' => 'node_access',
   
'access argumments' => array('update', 1),
   
'page callback' => 'node_edit_page',
   
'page arguments' => array(1),
   
'title' => 'edit',
   
'type' => MENU_LOCAL_TASK
 
);
?>

If you are on the path node/123 you will expect this item to produce a tab pointing to node/123/edit. This is rather easy, we replace node/%/edit with node/123/edit where 123 is simply the relevant arg. When you click this tab, then you will land on node/123/edit where the usual object substitution will happen.

You can overwrite the latter behavior for the situation when you want to substitute a value dynamically into a link (or tab) that's being displayed. By defining a function called node_to_arg and then node_to_arg(arg(1)) will be called and whatever the function returns will be the replacement of the wildcard. The best example of the use of this in core is function user_current_to_arg() which is used to dynamically change the 'My account' link to point to the account page for the current user. Note that the naming of this function is based on the name of the object that's loaded- the corresponding menu path is user/%user_current.

If you want to pass an integer number to a menu callback, use '0' as the menu_unserialize function uses an is_int check. If you are passing in a variable as an argument, it's highly recommended to surround it with double quotes like "$foo".

Menu item title and description callbacks, localization

Menu item titles and descriptions provided by modules are stored in English from Drupal 6.x. This means that title and description values should not be wrapped in t() calls, because Drupal translates the titles and descriptions of menu items on demand, to the language used to display the page at that time. Descriptions (if present) are always translated with t(), without passing arguments for replacement, so the previous practice of presenting literal strings in descriptions is the enforced rule now. Titles are translated with t() by default, and could receive additional arguments to replace placeholders with. Alternatively, you can also use a custom callback to translate titles if required. The following examples show how localization fits into the menu system. The following $items array keys are related to localization:

  • "title": Text of title in English.
  • "title callback": Optional name of callback function used to "translate" the title. Defaults to "t".
  • "title arguments": Optional indexed array of arguments passed to "title callback". Defaults to array().
  • "description": Optional text of description in English.

The above arguments interact in the following ways (with pseudo code) to compute the title:

  • If only the title is given, and the callback is omitted (or specified as "t"), the title is translated with t(). Pseudo code: t($title)
  • If title is given with arguments, and the callback is omitted (or specified as "t"), the title is translated with t() and the arguments given. Pseudo code: t($title, $arguments)
  • If title is given and a callback is given which is different from "t", the title is translated with that callback. Pseudo code: $callback($title).
  • If a callback is given which is different from "t", and arguments are given, the callback is invoked with the exact arguments list (the value of title is not passed). Pseudo code: $callback($arguments).

Actual examples from Drupal:

<?php
function block_menu() {
 
$items['admin/build/block'] = array(
   
// Title as literal string, callback not defined, so falls back to the default t() callback
   
'title' => 'Blocks',
   
// Description as literal string, always translated with t().
   
'description' => 'Configure what block content appears in your site\'s sidebars and other regions.',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('block_admin_display'),
   
'access arguments' => array('administer blocks'),
  );

 
//...

 
$default = variable_get('theme_default', 'garland');
  foreach (
list_themes() as $key => $theme) {
   
$items['admin/build/block/list/'. $key] = array(
     
// Title is string with placeholder, callback not defined, so falls back to the default t()
     
'title' => '!key settings',
     
// "title arguments" specify the arguments to pass on to the title callback
     
'title arguments' => array('!key' => $theme->info['name']),
     
'page arguments' => array('block_admin_display', $key),
     
'type' => $key == $default ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
     
'weight' => $key == $default ? -10 : 0,
    );
  }
  return
$items;
}

function
search_menu() {
 
//...

 
foreach (module_implements('search') as $name) {
   
$items['search/'. $name .'/%'] = array(
     
// Custom callback to get the title from
     
'title callback' => 'module_invoke',
     
// List of arguments to pass to "title callback"
     
'title arguments' => array($name, 'search', 'name', TRUE),
     
'page callback' => 'search_view',
     
'page arguments' => array($name),
     
'access callback' => '_search_menu',
     
'access arguments' => array($name),
     
'type' => $name == 'node' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
     
'parent' => 'search',
    );
  }
  return
$items
}
?>

How the system maintains menu order

We have six int columns each representing a piece of the materialized path, p1 .. p6 . This algorithm is Peter Wolanin's implementation of materialized path, heavy exploiting the fact that we know the max depth of the tree.

All numbers here are mlid (a numeric, unique key for each menu link), assume p1, p2, p3 = X.Y.Z (only 3 for ease of typing). Note that for queries, the list of "parents" is derived from p1.p2.p3, not stored separately. We still need a separate mlid, plid.

5.0.0           mlid = 5, plid = 0, parents = 0
  5.6.0         mlid = 6, plid = 5, parents = 5, 0
7.0.0           mlid = 7, plid = 0,  parents = 0
  7.13.0       mlid = 13, plid = 7, parents = 7. 0
  7.15.0       mlid  = 15, plid = 7, parents = 7, 0
     7.15.23  mlid = 23, plid = 15, parents = 15, 7, 0
     7.15.16  mlid = 16, plis = 15, parents = 15, 7, 0
  7.10.0       mlid  = 10, plid = 7, parents = 7, 0
     7.10.22  mlid = 23, plid = 15, parents = 15, 7, 0
12.0.0         mlid = 12, plid = 0, parents = 0

to get the tree for mlid = 23, we construct parents from p2, p1, 0
SELECT * from {menu_links} WHERE plid in (15, 7, 0) ORDER BY p1 ASC, p2 ASC, p3 ASC

result (showing p1.p2.p3):
5.0.0
7.0.0
7.10.0
7.13.0
7.15.0
7.15.16
7.15.23
12.0.0

which we can use with essentially the tree-building algorithm we have now but we need to sort each subtree by weight, title in PHP. The need to sort and algorithm is the same in the mleft/mright code.

If we did the a select on the whole table to get the full tree:

5.0.0
5.6.0
7.0.0
7.10.0
7.10.22
7.13.0
7.15.0
7.15.16
7.15.23
12.0.0

Now inserting is simple and so is reparenting. For example, if we
reparent 5.0.0 under 7.0.0 we get:
7.0.0          mlid = 7, plid = 0,  parents = 0
  7.5.0        mlid = 5, plid = 7, parents = 7, 0
    7.5.6      mlid = 6, plid = 5, parents = 7, 5, 0
  7.13.0      mlid = 13, plid = 7, parents = 7. 0
  7.15.0      mlid  = 15, plid = 7, parents = 7, 0
     7.15.23 mlid = 23, plid = 15, parents = 15, 7, 0
     7.15.16 mlid = 16, plis = 15, parents = 15, 7, 0
  7.10.0       mlid  = 10, plid = 7, parents = 7, 0
     7.10.22 mlid = 23, plid = 15, parents = 15, 7, 0
12.0.0        mlid = 12, plid = 0, parents = 0

note- plid only changed for one item (mlid = 5, plid was 0, now 7)- like the current book module.

All the children get updated with a single query like

SET p1 = 7, p2 = p1, p3 = p2 WHERE p1 = 5 and p2 != 0

Thus, in general, reparenting involves two steps:

1) Put in the new value in the 'plid' column and p1...p6 columns for the repareted item (involves one UPDATE query)

2) run an update for the materialized path columns for all children of the reparented item to reflect the path that their list of parents (path to root or materialzed path) has changed.

Page handler include files

The menu system introduced in Drupal 6 also supports conditional loading of an extra include file for each menu entry. That allows module developers to separate off their page handler functions to separate files that are loaded only as-needed. In many modules the majority of the code is page handlers, but on any give page request only one page handler is called in all of Drupal. Separating those functions out to conditional files can save a great deal of overhead from parsing functions that will never be used.

Page handler includes

To tell Drupal that a page handler lives in a separate include file, use the "file" array key. For example:

<?php
  $items
['admin'] = array(
   
'title' => 'Administer',
   
'access arguments' => array('access administration pages'),
   
'page callback' => 'system_main_admin_page',
   
'weight' => 9,
   
'file' => 'system.admin.inc',
  );
?>

The above directive, taken from system_menu(), tells Drupal to include the file "system.admin.inc", located in the same directory as the system.module file, before calling system_main_admin_page(). system_main_admin_page() can then be placed in system.admin.inc, so it never has to be parsed unless it is needed.

Form page handlers

A great many page handlers are actually just calls to drupal_get_form(), with the ID of the form for that page as a callback argument. That is especially true for system configuration pages. drupal_get_form() is always available, of course, but the form callback function can then be placed in a separate include file. Remember to move the form definition function as well as its validate and submit handlers, since they will only be needed if the page for that form is loaded.

Page handlers provided by other modules

In some cases, you will want to use a page callback provided by a different module. In that case, you will need to also specify a "file path" key, like so (taken from node_menu()):

<?php
  $items
['admin/content'] = array(
   
'title' => 'Content management',
   
'description' => "Manage your site's content.",
   
'position' => 'left',
   
'weight' => -10,
   
'page callback' => 'system_admin_menu_block_page',
   
'access arguments' => array('administer site configuration'),
   
'file' => 'system.admin.inc',
   
'file path' => drupal_get_path('module', 'system'),
  );
?>

Drupal will include the file located at "file path"/"file", with the file path based on the Drupal base directory. If no file path is specified, the path of the module defining the callback is used. That is:

<?php
example_menu
() {
 
$items['example/path'] = array(
   
'page callback' => 'example_page',
   
'file' => 'example.pages.inc',
   
'file path' => drupal_get_path('module', 'example'),
  );
}
?>

The "file path" key in the above example is unnecessary, as that is what Drupal does by default.

Inheritance

If a menu item does not define a page callback, then it will inherit the callback from its parent. If it does so, it will also inherit the file directives from its parent. Since they define how Drupal should access the callback function, that should make intuitive sense.

Best practices

Module developers are free to split off page handlers for their modules however they choose. However, the following guidelines and standards are recommended:

  • Any module that has more than ~50 lines of code for page handler functions (including form handling functions if applicable) should split them off into a separate file. That reduces the overhead for PHP when loading modules, and therefore speeds up every single request on the site.
  • Page include files should be named in the form modulename.key.inc, where "modulename" is the name of the module and "key" is a one-word descriptive term for the types of page handlers it includes. If the module has only one page file, it should be named modulename.pages.inc.
  • For most modules, splitting page handlers into two files -- example.admin.inc (for administrator-only pages) and example.pages.inc (for pages accessible by non-administrator users) -- is sufficient, and is the recommended practice.
  • Modules that have a large number of page handlers may choose to separate out page handlers even further. If so, each file should be grouped logically by function (for instance, admin pages related to theming, admin pages related to logging, other admin pages, and user-accessible pages) and clearly marked. Remember that splitting the module's page handlers up too far makes maintenance more difficult, and only one page handler include file is ever loaded regardless of how finely-grained the handler functions are separated.

Upgrading to the new menu system

Cached part

This is very simple, a search-and-replace operation and we even provide a script to help you with the gruntwork. Here is a list of changes:

  • The value of path is the new index for $items.
  • callback becomes page callback
  • callback arguments becomes page arguments
  • access becomes access callback and access arguments. For example, access => user_access('administer nodes') becomes 'access callback' => 'user_access', 'access arguments' => array('access arguments' => array('administer nodes'). However, the default for 'access callback' is 'user_access' so you can leave that out. Complex acccess things must be moved to a function which can be called on runtime, user_is_anonymous and user_is_logged_in are useful helpers. See access for more.
  • The title and description arguments should not have strings wrapped in t(), because translation of these happen in a later stage in the menu system. This allows translation of menu items to any language required on a site, adapting to the language used on the page.
Non-cached part

Let's suppose you had

<?php
   
if (arg(0) == 'aggregator' && is_numeric(arg(2))) {
      if (
arg(1) == 'sources') {
       
$feed = aggregator_get_feed(arg(2));
        if (
$feed) {
         
$items[] = array('path' => 'aggregator/sources/'. $feed['fid'] .'/configure',
           
'title' => t('Configure'),
           
'callback' => 'drupal_get_form',
           
'callback arguments' => array('aggregator_form_feed', $feed),
           
'access' => $edit,
           
'type' => MENU_LOCAL_TASK,
           
'weight' => 1);
?>

this becomes

<?php
  $items
['aggregator/sources/%/configure'] = array(
   
'title' => 'Configure',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('aggregator_form_feed', 2),
   
'access callback' => 'menu_access_and',
   
'access arguments' => array('administer news feeds', 2),
   
'map arguments' => array('aggregator_get_feed', 2, array()),
   
'type' => MENU_LOCAL_TASK,
   
'weight' => 1,
  );
?>

Note: you won't find this in core literally because some parts are inherited.

The percent sign aggregator/sources/%/configure is the wildcard -- note that this matches non-numeric values as well, like aggregator/sources/foo/configure. However, with the map_callback doing an aggregator_get_feed, anything that's not a valid feed will lead to a 404.

Let's another example where the paths can't really be replaced with a wildcard:

<?php
 
if (arg(0) == 'search') {
   
// To remember the user's search keywords when switching across tabs,
    // we dynamically add the keywords to the search tabs' paths.
   
$keys = search_get_keys();
   
$keys = strlen($keys) ? '/'. $keys : '';
    foreach (
module_implements('search') as $name) {
     
$title = module_invoke($name, 'search', 'name');
     
$items[] = array('path' => 'search/'. $name . $keys, 'title' => $title,
       
'callback' => 'search_view',
       
'callback arguments' => array($name),
       
// The search module only returns a title when the user is allowed to
        // access that particular search type.
       
'access' => user_access('search content') && $title,
       
'type' => MENU_LOCAL_TASK,
      );
    }
  }
?>

This became:

<?php
 
foreach (module_implements('search') as $name) {
   
$items['search/'. $name] = array(
     
'page callback' => 'search_view',
     
'page arguments' => array($name),
     
'access callback' => FALSE,
     
'type' => MENU_LOCAL_TASK,
    );
  }
  return
$items;
}

function
search_init() {
 
// To remember the user's search keywords when switching across tabs,
  // we dynamically add the keywords to the search tabs' paths.
 
if (arg(0) == 'search') {
   
$keys = search_get_keys();
   
$keys = strlen($keys) ? '/'. $keys : '';
    foreach (
module_implements('search') as $name) {
     
$title = module_invoke($name, 'search', 'name');
     
$item = menu_get_item('search/'. $name);
     
$item->title = $title;
     
$item->access = user_access('search content') && $title;
     
menu_set_item('search/'. $name, $item);
     
menu_set_item('search/'. $name . $keys, $item);
    }
  }
}
?>

hook_init is somewhat new, previously we called hook_init twice, once early in the bootstrap process, second just after the bootstrap has finished. The first instance is now called boot instead of init. In here, we load the item pertaining to the path and move it to the correct path by first copying and then setting the original to FALSE. Also, this solution is not slow: the items would have been needed anyways for the search tabs and menu_set_item does remarkably litte.

Drupal's menu building mechanism (4.7x and 5.x)

(Note: this is an analysis of the menu building mechanism in pre-4.5 CVS as of August 2004. It does not include menu caching.)

This continues our examination of how Drupal serves pages. We are looking specifically at how the menu system works and is built, from a technical perspective. See the excellent overview in the menu system documentation.

We begin in index.php, where menu_execute_active_handler() has been called. Diving in from menu_execute_active_handler(), we immediately set the $menu variable by calling menu_get_menu(). The latter function declares the global $_menu array (note the underline, it means a 'super global', which is a predefined array in PHP lore) and calls _menu_build() to fill the array, then returns $_menu. Although menu_get_menu() initializes the $_menu array, the _menu_build() function actually reinitializes the $_menu array. Then it sets up two main arrays within $_menu: the items array and the path index array.

The items array is an array keyed to integers. Each entry contains the following fields:

Required fields
path string the partial URL to the page for this menu item
title string the title that this menu item will have in the menu
type integer a constant denoting the menu item type (see comments in menu.inc)
Optional fields
access boolean
pid integer
weight integer
callback string name of the function to be called if this menu item is selected
callback arguments array

An array called $menu_item_list is populated by sending a 'menu' callback to all modules with 'menu' hooks (that is, they have a function called foo_menu() where foo is the name of the module). So each module has a chance to register its own menu items. It is interesting that when the node module receives the menu callback through node_menu(), and the path is something like 'node/1' as it is in our present case, the complete node is actually loaded via the node_load() function so it can be examined for permissions. The $node variable into which it was loaded then goes out of scope, so the node is gone and needs to be rebuilt completely later on. This seems like a golden opportunity for the node module to cache the node.

The $menu_item_list array is normalized by making sure each array entry has a path, type and weight entry. As each entry is examined, the path index array of the $_menu array is checked to see if the path of this menu item exists. If an equivalent path is already there in the path index array, it is blasted away. The path index of this menu item is then added as a key with the value being the menu id. In the items array of the $_menu array, the menu id is used as the key and the entire array entry is the value.

Note: the $temp_mid and $mid variables seem to do the same thing. Why, syntactically, cannot only one be used?

The path index array contained 76 items when serving out a simple node with only the default modules enabled.

Next the menu table from the database is fetched and its contents are used to move the position of existing menu items from their current menu ids to the menu ids saved in the database. The comments says "reassigning menu IDs as needed." This is probably to detect if the user has customized the menu entries using the menu module. The path index array entries generated from the database can be recognized because their values are strings, whereas up til now the values in the path index array have been integers.

Now I get sort of lost. It looks like the code is looking at paths to determine which menu items are children of other menu items. Then _menu_build_visible_tree is a recursive function that builds a third subarray inside $_menu, to go along with items and path index. It is called visible and takes into account the access attribute and whether or not the item is hidden in order to filter the items array. As an anonymous user, all items but the Navigation menu item are filtered out. See also the comments in menu.inc for menu_get_menu(). In fact, read all the comments in menu.inc!

Now the path is parsed out from the q parameter of the URL. Since node/1 is present in the path index, we successfully found a menu item. It points to menu item -44 in our case, to be precise, but there must be a bug in the Zend IDE because it shows item -44 as null. Anyway, the menu item entry is checked for callback arguments (there are none) and for additional parameters (also none), and execution is passed off to node_page() through the call_user_func_array function.

Hook menu

How do you make a menu into a structure? The path attribute creates the structure.

Suppose you have the following page structure:
aa
aa/bbb
aa/bbb/cc
aa/dd
xx
xx/yy
xx/yy/zzz

You want a menu that starts with:
aa
xx

You want the menu to expand as you click on entries.

The trick is the path attribute in the item returned through hook_menu. If you write a module named example_module then you create your menu through a function named example_module_menu that returns an array containing menu items with each item containing an attribute named path. hook_menu magically decodes the path structure in each item and builds the menu structure from those path structures.

To build the structure shown above, you would start with the following code. Note that I left out useful attributes including weight and callback so we can focus on path.

$items[] = array('path' => 'aa', 'title' => t('Aa'));
$items[] = array('path' => 'aa/bbb', 'title' => t('Bbb'));
$items[] = array('path' => 'aa/bbb/cc', 'title' => t('Cc'));
$items[] = array('path' => 'aa/dd', 'title' => t('Dd'));
$items[] = array('path' => 'xx', 'title' => t('Xx'));
$items[] = array('path' => 'xx/yy', 'title' => t('Yy'));
$items[] = array('path' => 'xx/yy/zzz', 'title' => t('Zzz'));
menu.inc

includes/menu.inc builds the menu from the items array. As of 4.7.2, menu.inc contains _menu_build() to gather the items from modules and build the menu. If path is missing, _menu_build() sets path to an empty string. If type is missing, _menu_build() sets type to MENU_NORMAL_ITEM. If weight is missing, _menu_build() sets weight to zero.

Reference

This section is intended as a handy reference, collecting things which you may need to look up as you code to Drupal.

Drupal database documentation

This place is under construction.

For now, Drupal database documentation can be acquired from http://cvs.drupal.org/viewcvs/drupal/contributions/docs/developer/databa...

If you'd like to help, please see http://drupal.org/project/comments/28046

Main content-related hooks

This table shows how many of the hooks related to content in all its forms fit together, in terms of high-level operations.

When Performing user comment node type term / vocabulary node
When Performing user comment node type term / vocabulary node
before create hook_user('register') hook_access('create')
during hook_user('insert')

-
-

hook_comment('insert')
-
-
hook_node_type('insert')

-
-

hook_taxonomy('insert')
-
-
hook_insert()

hook_nodeapi('insert')
hook_node_access_records()

after load hook_user'('load') hook_load()
hook_nodeapi('load')
before update -

hook_profile_alter()
-
hook_user('submit')
hook_user('validate')
-

-
-

-
-
hook_comment('validate')
-

hook_access('update')

-
hook_submit
hook_nodeapi('submit')
hook_validate()
hook_nodeapi('validate')

during hook_user('update')
-
hook_comment('update')
-
hook_node_type('update')
-
hook_taxonomy('update')
-
hook_update
hook_nodeapi('update')
after hook_user('after_update')
before delete -
hook_user('delete')
-
-
hook_comment('delete')
-
-
hook_node_type('delete')
-
-
hook_taxonomy('delete')
-
hook_access('delete')
hook_delete()
hook_nodeapi('delete')
during delete revision hook_nodeapi('delete revision')
before edit -
-
hook_user('form')
-
-
-

hook_comment('form')
-

hook_prepare()
hook_nodeapi('prepare')
hook_form()

[hook_form_alter()]

before view -
hook_user('view')
-
-
-
hook_comment('view')

-
-

hook_access('view')
hook_view()
hook_nodeapi('view')

hook_nodeapi('alter')

before print hook_nodeapi('print')
during indexing hook_nodeapi('update index')
before view as search result
(hook_search_item
exists)
hook_nodeapi('search result')
hook_user('categories')
during set active hook_user('login')
hook_auth()
hook_comment('publish') could be hook_update() and hook_nodeapi('update')
during set inactive hook_user('logout') hook_comment('unpublish') could be hook_update() and hook_nodeapi('update')
during feed generation hook_nodeapi('rss item')
as comment_nodeapi
hook_nodeapi('rss item')

Many hooks are not listed: only those related to a typical create/retrieve/update/delete workflow. For details on the hooks, refer to the API site.

Tables: layout and navigation (core)

Here are the tables, from the standard (core) Drupal database, with data that effects navigation and layout of the Drupal website. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

blocks
Block configuration settings. Eg. theme, status, region, weight, throttle, etc.
boxes
User-defined custom blocks.
menu
Menu data, including parent, path, title, description, weight.
url_alias
User-defined and module-defined alternative urls.

Tables: localization (core)

These tables in the standard (core) Drupal database are required for localization. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

locale_meta
The general list of locales (eg. English) available.
locale_source
References to translatable strings.
locale_target
Translated strings.

Tables: node (core)

Here is a list of the tables that store the central node data in a (core) Drupal database. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

node
Basic node info: type, title, status, user id, created on, is moderated, is sticky.
node_revisions
This is where the node teaser and body is stored along with previous versions if applicable.
node_access
This where "node access" modules can register their interest.
node_counter
Basic statistics about how often a node has been accessed.
node_comment_statistics
Special information about each node's latest comment, and how many comments there are.

Tables: node types (core)

These are the tables in the Drupal standard (core) installation that are used to store data that extends the basic node type. For example, the poll.module extends the behavior of the basic node and uses additional tables in the process. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

poll
Table for 'poll' node types. A poll is topic or question that visitors can vote on.
poll_choices
The poll choices that can be voted on, along with the number of votes for each choice.
poll_votes
A record of voters so as to prevent repeat votes.
book
Table for book page nodes that know their place in a book structure.
forum
Table for forum nodes, being lists of other nodes that relies on taxonomy.

Tables: other (core)

Here are a few more tables from the standard (core) Drupal database. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

file_revisions
Details of about file revisions.
file
Details of uploaded files, the name, path, type.
contact
Here is where contact categories for the site-wide contact form are stored.
filter_formats
Defined filter formats like "PHP code" and “Filtered "HTML”
filters
The actual filters used for each "filter format" and their order of operation.

Tables: RSS aggregation (core)

Here is a list of the tables are used to manage data aggregation for your feeds in your standard (core) Drupal installation. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

aggregator_feed
Defined RSS feeds, each based on a url.
aggregator_item
Items that are generated from feeds.
aggregator_category
User-defined categories for feeds and feed items.
aggregator_category_feed
Assignments of feeds into categories.
aggregator_category_item
Assignments of items into categories.

Tables: search (core)

Here is a list of the tables are used to manage searching in your standard (core) Drupal installation. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

search_dataset
Each row of this table is the simplified content of a node (or other type).
search_index
An index of every word of the content in search_dataset.
search_total
Unique list of words from search_index, with frequency statistics.

Tables: system (core)

Here are the auxiliary system tables in the standard (core) Drupal database that are not defined elsewhere.

sequences
Record of the latest ID for various tables. (For compatibility with older MySQL versions.)
system
List of modules and themes. Name, path, status, throttle, etc.
variable
A flexible table for storing site settings.
client
Information about remote clients. (xmlrpc related?)
client_system
Information about remote client systems. (xmlrpc related?)

Tables: taxonomy (core)

Here is a list of the tables, in a standard (core) Drupal database installation, that store information related to taxonomy and categories. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

term_data
A term is a label that can be applied to nodes. This table is where the terms are defined.
term_node
This table links terms to nodes.
term_hierarchy
Vocabulary hierarchies are achieved by defining parent-child relationships between terms.
term_relation
Any terms that have a relationship can be linked to each. These links are defined here.
term_synonym
Defines alternatives to existing terms. Eg. "bike" might be given a synonym of "bicycle".
vocabulary
A vocabulary is list or group of related terms.
vocabulary_node_types
You can choose which vocabularies can be used with various node types. Thus when you create a new node, you will only have certain vocabulary terms to choose from.

Tables: tracking (core)

These tables are generally associated with keeping track of user data and statistics in a standard (core) Drupal database. We can loosely divide these tables into short-term (eg. the current session) and long-term (history and statistics). Please refer to the database documentation for progress on a detailed database schema and data dictionary.

Short-term

cache
Data may be cached to speed up page loading, especially for anonymous visitors.
flood
For tracking how often the contact page (and other resources) are used.
sessions
Data related to the user's visit to the site.

Long-term

accesslog
Record of general visitor access statistics.
history
Record of page (node) visits per user.
watchdog
Modules can record events here. These events range from quite normal activities to serious problems.

Tables: user (core)

Here is a list of the tables associated with user data in a standard (core) Drupal database. Please refer to the database documentation for progress on a detailed database schema and data dictionary.

user
The primary user table which stores user info like: username, email, signature, status, picture and selected language
user_roles
List of assignments between users and roles.
role
This is where the roles are defined. Roles are user groups, like 'Admin' and ‘Editors'.
permission
Assigns each role permissions like “access content”, “post comments”.
profile_fields
Custom fields that have been set up with the profile module.
profile_values
The user data associated with custom fields.
authmap
Stores information about loading user data from external sources.
access
Rules governing the use of email addresses, user names and originating hostnames.

'Status' field values for nodes and comments

Note: we now use CONSTANTS in the code so it isn't usually necesasary to know these values. You only need them if you are querying the DB from a non drupal page.

Just documenting the status field for the following tables

NODES

  • 0: not published
  • 1: published

COMMENTS

  • 0: published
  • 1: not published
  • 2: deleted (no longer exists in Drupal 4.5 and above)

Module how-to's

This section collects various 'How-to' articles of interest to module writers and hackers.

How to connect to multiple databases within Drupal

Drupal can connect to different databases with elegance and ease!

First define the database connections Drupal can use by editing the $db_url string in the Drupal configuration file (settings.php for 4.6 and above, otherwise conf.php). By default only a single connection is defined

<?php
$db_url
= 'mysql://drupal:drupal@localhost/drupal';
?>

To allow multiple database connections, convert $db_url to an array.

<?php
$db_url
['default'] = 'mysql://drupal:drupal@localhost/drupal';
$db_url['mydb'] = 'mysql://user:pwd@localhost/anotherdb';
$db_url['db3'] = 'mysql://user:pwd@localhost/yetanotherdb';
?>

Note that database storing your Drupal installation should be keyed as the default connection.

To query a different database, simply set it as active by referencing the key name.

<?php
db_set_active
('mydb');

db_query('SELECT * FROM table_in_anotherdb');

//Switch back to the default connection when finished.
db_set_active('default');
?>

Make sure to always switch back to the default connection so Drupal can cleanly finish the request lifecycle and write to its system tables.

How to make tablesorting work with multiple tables on the same page

With Drupal 5.0 and beyond, it is now possible to provide independent sorting of tables when they appear on the same page.

This is accomplished by inserting a unique label for each table in both the tablesort_sql and theme('table'... calls for each relevant table.

For example, suppose you had two sortable tables on a page, one a list of users, and the other a list of nodes. To make each table sortable independently, you would do something like:

For the user table:
...

$user_sort = tablesort_sql($header, NULL, 'user');

...and then in the user table theme call...
$output = theme('table', $header, $rows, array('class' => 'some-class'), NULL, 'user');

For the node table:
...

$node_sort = tablesort_sql($header, NULL, 'node');

...and then in the node table theme call...
$output = theme('table', $header, $rows, array('class' => 'some-class'), NULL, 'node');

Note that for each table, the label you use for tablesort_sql and theme('table'... must be identical for the independent sorting to be handled properly.

How to rebuild node_comment_statistics

Sometimes you need to move comments around at the database level. This is easy enough, but what about the node_comment_statistics table? If this gets messed up it can really throw your site off. Here's a handy bit of PHP to rebuilt that table:

<?php
db_query
("DELETE FROM {node_comment_statistics}");

$nodes = db_query("SELECT nid,uid,changed FROM {node} WHERE status=1");
while (
$m = db_fetch_object($nodes)) {
 
$nid = $m->nid;

 
$comments=db_query("SELECT nid,max(cid) FROM {comments} WHERE status=0 AND nid=$nid GROUP by nid");
  if(
$l = db_fetch_object($comments)) {
   
$cid = $l->{"max(cid)"};

   
$lastcomments = db_query("SELECT uid,name,timestamp FROM {comments} WHERE cid=$cid");
   
$n = db_fetch_object($lastcomments);
   
$uid = $n->uid;
   
$name = $n->name;
   
$timestamp = $n->timestamp;

   
$counts = db_query("SELECT count(*) FROM {comments} WHERE status=0 AND nid=$nid");
   
$n = db_fetch_object($counts);
   
$count = $n->{"count(*)"};
  } else {
   
$timestamp = $m->changed;
   
$name = NULL;
   
$uid = $m->uid;
   
$count = 0;
  }
 
db_query("INSERT INTO {node_comment_statistics} (nid,last_comment_timestamp,last_comment_name,last_comment_uid,comment_count) VALUES ('$nid','$timestamp','$name','$uid','$count')");
}
?>

Credit goes to ahoeben on this thread.

How to use watchdog() in your own code

This function is invaluable for debugging your own code.

In its simplest form, one might use the following code to insert new lines into the watchdog log.

<?php
watchdog
('error title', 'error message');
?>

The watchdog module in modules/watchdog.module has everything but the function itself. You can find the watchdog() function in includes/bootstrap.inc where the parameters are explained:

<?php
// snippet taken from 4.7 -- includes/bootstrap.inc
/**
* Log a system message.
*
* @param $type
*   The category to which this message belongs.
* @param $message
*   The message to store in the log.
* @param $severity
*   The severity of the message. One of the following values:
*   - WATCHDOG_NOTICE
*   - WATCHDOG_WARNING
*   - WATCHDOG_ERROR
* @param $link
*   A link to associate with the message.
*/

function watchdog($type, $message, $severity = WATCHDOG_NOTICE, $link = NULL) { ... }
?>

How to write a node module

This information is superseded by the Doxygen documentation. In particular, its example node module is a good tutorial.

How to write automated tests

Writing an automated test might be a bit disorienting for some Drupal developers. It isn't hard, but is different from rest of Drupal in a couple of ways

  • Tests use classes instead of functions like rest of Drupal. This is a byproduct of our choice to use the simpletest library. Simpletest if a wonderful piece of software, and this minor nit shouldn't upset a rational developer.
  • To write a test, you have to think like a tester. You have to exercise all the important branches of your code and poke on corner cases. At first, this can feel foreign but please persevere - the rewards are great.

The simpletest.module is using the simpletest php library as framework for our tests. Excellent documentation is available for both general tests and our popular classes, web testing and unit testing.

You need not understand all the pages linked above. Feel free to learn from existing tests in this package and elsewhere in Contrib.

The basic class structure

By using the simpletest framework the drupal tests are defined as classes in object oriented style. Here is a skeleton test:

<?php
/**
* Description
*/
class ModuleFeatureTest extends DrupalTestCase {
  function
get_info() {
    return array(
'name'  => 'CoolFeature Test',
                
'desc'  => t('Assure that all cool features of your module work.'),
                
'group' => 'Your Module Tests');
  }
 
  function
testYourCoolFeature() {
   
/* Test code here */
 
}
}
?>

Your new class must implement a method named get_info:
It returns an associative array with your test's name, description, and which 'group' it belongs to. You may create a new group or insert your test into an existing group by using the same name.

This would go into a .test file.

The DrupalTestCase features

By extending DrupalTestCase your test inherits all features of both WebTestCase and UnitTestCase, as well as the Drupal-specific features of DrupalTestCase.

function $this->drupalModuleEnable($name)
function $this->drupalModuleDisable($name)

Enables or disables a Drupal module by name.
This is useful for testing modules that are not required or disabling modules that might interfere with your test by adding required input.
The module settings will be restored after your test method has completed.
ATTENTION: Make sure you use return instead of exit/die in your test! When you extend the tearDown() / setUp() methods please call the parent:: methods!

Example:

<?php
/* make sure the profile module is disabled to avoid conflicts */
$this->drupalModuleDisable('profile');
?>
function $this->drupalVariableSet($name, $value)

This function sets a Drupal variable (like variable_set), but restores the original value after your test method has completed.

Example:

<?php
/* We first allow every user to login instantly. */
$this->drupalVariableSet('user_register', 1);
?>
function $this->randomName($number = 4, $prefix = 'simpletest_')

Returns a string with $number alphanumerical character(s) prefixed by $prefix.
The first character will not be a number.

The internal browser

The DrupalTestCase features an internal browser that can be used to navigate on your test site. Please read the basic documentation of the WebTestCase class for more information.

function $this->drupalPostRequest($path, $edit, $submit, $reporting = TRUE)

This function does a post request on a drupal page.
The $path indicates a page containing a form that will be filled with $edit data. Then the button indicated by $submit will be clicked (submit caption will be translated by this method).

It also does assertion that the requests were successful and form fields could be set.

Example:

<?php
$name
= $this->randomName();
$mail = "$name@example.com";
$edit = array('name' => $name,
             
'mail' => $mail);
$this->drupalPostRequest('user/register', $edit, 'Create new account');
?>
function $this->clickLink($label, $index = 0)

Follows a link on the current page by name. Will click the first link found with this link text by default, or a later one if an $index is given. The $label is automatically translated.
An assertion is done about the availability of the link and the URL it points to. Also gives some output including current and requested URL.

Example:

<?php
$this
->clickLink('log out');
?>
function $this->drupalCreateUserRolePerm($permissions = NULL)

This function creates a user and returns the user object with an additional value pass_raw containing the non-hashed password.
It also creates a role with the specified $permissions that is assigned to the returned user.
The $permissions are specified as an array of strings. If it is omitted or NULL, the default permissions for a registered user will be used:
'access comments, access content, post comments, post comments without approval'
An assertion for success is done as well as clean-up on the user and role tables.

function $this->drupalLoginUser($user = NULL)

This function logs a user into your site via the internal browser. You can just hand it a $user object (required is a pass_raw value).
If the argument is omitted this function will create a user and role with the standard permissions mentioned above.

After the user is logged in, you can now navigate with the internal browser.

Example:

<?php
/* Prepare a user to do the stuff */
$user = $this->drupalCreateUserRolePerm(array('access content', 'create pages'));
$this->drupalLoginUser($user);

/* now do something with the users */
$this->get(url('node/view/' . $node->nid));
?>

This method also does several assertions about the login process from the browsers perspective.

function $this->drupalCreateRolePerm($permissions = NULL)

This function is rarely useful. The $permissions parameter behaves exactly like in drupalCreateUserRolePerm.
The return value is a role-id integer or FALSE on failure.
A success assertion is done, as well as decent clean-up of the role and permission tables.

Implementing hook_simpletest

This hook allows your module to tell the simpletest.module where its test files reside.
Contrib module tests should be placed in a sub-directory called 'tests' in the module directory. The file extension for your tests should be '.test'.

Implementing this hook is usually just a matter of copying the code below and substituting your own module name for 'example'.

<?php
/**
* Implementation of hook_simpletest().
*/
function example_simpletest() {
 
$dir = drupal_get_path('module', 'example'). '/tests';
 
$tests = file_scan_directory($dir, '\.test');
  return
array_keys($tests);
}
?>
Running tests

You can run finished test classes that are referenced by hook_simpletests from administer > simpletest. The adminster unit tests permission is required to run tests. You will get a list of core tests and module tests. Select your test(s) and choose 'Run Selected Tests'.
If the test run outputs green text like the following:

Drupal Unit Tests
1/1 test cases complete: 10 passes, 0 fails and 0 exceptions.

then the test(s) completed successfully. Currently the passes are not reported.

Otherwise you will get a report like the following:

Drupal Unit Tests
Fail: Taxonomy Module -> Test taxonomy's functions -> testVocabularyFunctions -> Checking value of nodes at line [45]
Fail: File API Tests -> Upload user picture -> testUploadPicture -> Checking response on proper image at line [56]
Fail: File API Tests -> Upload user picture -> testUploadPicture -> Checking response on proper image at line [69]
Fail: File API Tests -> Upload user picture -> testUploadPicture -> Checking response on proper image at line [106]
Fail: File API Tests -> Upload user picture -> testUploadPicture -> [browser] Setting edit[picture_delete]="1"

2/2 test cases complete: 32 passes, 5 fails and 0 exceptions.

For each test failure, the test framework outputs a detailed trace, including which assertion failed.

Testing core modules and APIs

The simpletest.module comes with a set of tests which exercise Drupal's core modules and APIs. These can be helpful when making patches against Drupal's core. The best idea is to run the tests before you write a core fix; everything should pass. If you get fails in a HEAD install please file an issue including as much information about your environment as possible (Drupal version, db version, php_info, ...).

After you write your patch, run the same test set again. Any new failures may indicate bugs introduced by your patch.

Function testing vs. browser testing

There are many different tools in the Simpletest framework; however we focused on two: browser and function tests. Both are very useful during various stages of application development.

If you want to test a new module which doesn't contain a user interface yet, function tests are the best option. You can quickly check if our code is valid before the whole application is finished.

After your module has been written, it is essential to test the user experience. At this stage, the best solution is testing with browser tests.

Function-based tests

In order to test a function, we simply call the function and check the result. For example, assume we want to test the user_validate_mail function (you can see this in user_validation.test).

At the beginning we will look at our examined function:

<?php
function user_validate_mail($mail) {
  if (!
$mail) return t('You must enter an e-mail address.');
  if (!
valid_email_address($mail)) {
    return
t('The e-mail address %mail is not valid.', array('%mail' => theme('placeholder', $mail)));
  }
}
?>

As you can see, if $mail contains an invalid e-mail address, an error message will be displayed. Otherwise, the function returns nothing.

Let's write a test class. We start with get_info(), which is required in every test.

<?php
class OurFirstTest extends DrupalTestCase {
function
get_info() {
    return array(
'name' => 'User\'s email validation', 'desc' => 'Exercise email address validation.' , 'group' => 'Example tests');
  }
?>

after that, we add :

<?php
function testInvalidMail() {
   
$name = 'abc';
   
$result = user_validate_mail($name);
   
$this->assertNotNull($result, 'Invalid mail');
  }
  function
testValidMail() {
   
$name = 'absdsdsdc@dsdsde.com';
   
$result = user_validate_mail($name);
   
$this->assertNull($result, 'Valid mail');
  }
?>

As we mentioned before, we check if the variable is set or not using assertNotNull and assertNull.

After writing this we should have our test on the admin/simpletest page:
image 1

When you run this test you see something like this:

Drupal Unit Tests
1/1 test cases complete: 2 passes, 0 fails and 0 exceptions.

See image 2 for a screenshot.

Browser-based tests

Now it's time to try a browser test. We don't have to change too much in our class, because DrupalTestCase class contains tools for both tests.

We would to like check Drupal's response when specifying an invalid email address in the registration form (user/register). Let's look into the source:

<?php
<div class="form-item">
<
label for="edit-name">Username:</label><span class="form-required">*</span><br>
<
input maxlength="64" class="form-text required" name="edit[name]" id="edit-name" size="30" value="" type="text">
<
div class="description">Your full name or your preferred username; only letters, numbers and spaces are allowed.</div>
</
div>
<
div class="form-item">
<
label for="edit-mail">E-mail address:</label><span class="form-required">*</span><br>
<
input maxlength="64" class="form-text required" name="edit[mail]" id="edit-mail" size="30" value="" type="text">
<
div class="description">A password and instructions will be sent to this e-mail address, so make sure it is accurate.</div>
</
div>
?>

It is important to notice the name of fields in which we want to put data. There are two of them: edit[name] and edit[mail].

We begin with the same thing everytime:

<?php
class OurSecondTest extends DrupalTestCase {
// we need this function to notify the world about our great test
function get_info() {
    return array(
'name' => 'Be a browser', 'desc' => 'This tests the browser's response on invalid mail input in the registration process.' ,
      '
group' => 'Example tests');
  }
?>

Then we write our test function:

<?php
function testBrowserResponse() {
   
// let's create a random name and email
   
$name = $this->randomName(10);
   
$mail = $this->randomName(10);
   
// try to register
   
$edit = array('name' => $name, 'mail' => $mail);
   
$this->drupalPostRequest('user/register', $edit, 'Create new account');
   
$expectation = t('The e-mail address %mail is not valid.', array('%mail' => $mail));
   
$this->assertWantedText($expectation, 'Checkin response on invalid e-mail address');
  }
?>

Let's look at each line individually:

<?php
    $name
= $this->randomName(10);
?>

This line creates a 10 character random string, which is by default prefixed with 'simpletest_'.

Example: simpletest_XK3sKW5lFx

<?php
    $edit
= array('name' => $name, 'mail' => $mail);
   
$this->drupalPostRequest('user/register', $edit, 'Create new account');
?>

In these two lines, we prepare and send data to our form.
We run drupalPostRequest with the following parameters:

  1. The path to our form (e.g. 'user/register'). We don't use ?q=, or the whole address http://www.example.com/?q=user/register, because this function creates the proper address for us.
  2. Array of sent data. Look at the special construction of this array ( => ). We just use the form field names in brackets from above: 'name' and 'mail'. The rest is done by this function.
  3. The name of the button which we have to click to send our form. In our example it is: 'Create new account'.
<?php
    $expectation
= t('The e-mail address %mail is not valid.', array('%mail' => $mail));
?>

We prepare our expectation using t() function. We took this from user.module.

<?php
$this
->assertWantedText($expectation, 'Checkin response on invalid e-mail address');
?>

At last we check if our expectation appears on the content page.

When we run this test we should receive the following output:

Drupal Unit Tests
1/1 test cases complete: 5 passes, 0 fails and 0 exceptions

See Image 3 for a screenshot.

If you look closely, you notice that we have 5 passes in the result, but in the test code we only have one check (using assertWantedText). Where is the rest hidden?

The answer is: $this->drupalPostRequest('user/register', $edit, 'Create new account');
If we don't specify the 4th parameter in the drupalPostRequest function we will have additional checking. In our example it is:

  1. checking if url is valid
  2. checking if data was inserted in form's fields
  3. checking if we clicked our button

and that's why we have 1 + 4 = 5 passes. To avoid this, give the 4th parameter the value 0.

<?php
$this
->drupalPostRequest('user/register', $edit, 'Create new account', 0);
?>
Tips

Here is a place where you can find various tips and hints which will help you in writing tests

Viewing source during a browser test

Use $this->showSource() to output the source that the simpletest browser is receiving. Useful for debugging during development. Comes from simpletest/simpletest/webtester.php.

How to write database independent code

In order to ensure that your module works with all compatible database servers (currently Postgres and MySQL), you'll need to remember a few points.

  • When you need to LIMIT your result set to certain number of records, you should use the db_query_range() function instead of db_query(). The syntax of the two functions is the same, with the addition of two required parameters at the end of db_query_range(). Those parameters are $from and then $count. Usually, $from is 0 and $count is the maximum number of records you want returned.
  • If possible, provide SQL setup scripts for each supported database platform. The differences between each platform are slight - we hope documentation on these differences will be forthcoming.
  • Reserved words checker for all database management systems.
  • You should test any complex queries for ANSI compatibility using this tool by Mimer.
  • If you are developing on MySQL, use it's ANSI compatibility mode.
  • If you can install all database servers in your environment, it is helpful to create shell databases in each and then run sample queries in each platform's query dispatch tool. Once your query succeeds in all tools, congratulate yourself.
  • Don't use '' when you mean NULL.
  • Avoid table and field names that might be reserved words on any platform.
  • Don't use auto-increment or SERIAL fields. Instead, use an integer field and leverage Drupal's own sequencing wrapper: db_next_id(<tablename_fieldname>).
  • Use curly brackets when referencing table names in your SQL statements. This ensures that Drupal installations that use a database prefix will work correctly. Example: SELECT * FROM {accesslog} WHERE ... instead of SELECT * FROM accesslog WHERE ....

How to write efficient database JOINs

This page is based on an e-mail posted by Craig Courtney on 6/21/2003 to the drupal-devel mailing list: http://drupal.org/node/view/322.

There are 3 kinds of join: INNER, LEFT OUTER, and RIGHT OUTER. Each requires an ON clause to let the RDBMS know what fields to use joining the tables. For each join there are two tables: the left table and the right table. The syntax is as follows:

{left table} {INNER | LEFT | RIGHT} JOIN {right table} ON {join criteria}

An INNER JOIN returns only those rows from the left table having a matching row in the right table based on the join criteria.

A LEFT JOIN returns ALL rows from the left table even if no matching rows where found in the right table. Any values selected out of the right table will be null for those rows where no matching row is found in the right table.

A RIGHT JOIN works exactly the same as a left join but reversing the direction. So it would return all rows in the right table regardless of matching rows in the left table.

It is recommended that you not use right joins, as a query can always be rewritten to use left joins which tend to be more portable and easier to read.

With all of the joins, if there are multiple rows in one table that match one row in the other table, that row will get returned many times.

For example:
Table A
tid, name
1, 'Linux'
2, 'Debian'

Table B
fid, tid, message
1, 1, 'Very Cool'
2, 1, 'What an example'

Query 1:
SELECT a.name, b.message FROM a INNER JOIN b ON a.tid = b.tid
Result 1:
Linux, Very Cool
Linux, What an example

Query 2:
SELECT a.name, b.message FROM a LEFT JOIN b ON a.tid = b.tid
Result 2:
Linux, Very Cool
Linux, What an example
Debian, <null>

Hope that helps in reading some of the queries.

Howto: Update a module's weight

In Drupal 4.7 and 5.x the order in which a module's hooks get called is dependent on the weight of your module in the system table. You can set a low weight (negative number) to get your module to execute before others. Or, you can set a high weight to execute after other modules.

Code to update weight

You will want to modify and then place this code into your module's modulename.install file in a modulename_install function. See more details on the hook_install in the hook_install API documentation.

db_query("UPDATE {system} SET weight = [yournumber] WHERE name = 'yourmodulename'");
Module weights in use

Core modules all use a weight of zero. Here is a table created on January 17th showing the weights in use in the contributed modules:



File Weight set
modules/authorship/authorship.install 1001
modules/autologout/autologout.install 1000
modules/betterdate/betterdate.install 255
modules/boost/boost.install -90
modules/category/contrib/cac_lite/cac_lite.install 9
modules/category/contrib/cac_lite/cac_lite.install 9
modules/community_tags/community_tags.install taxonomy + 1
modules/contemplate/contemplate.install 10
modules/devel/devel.install 88
modules/erp/erp_formfixes/erp_formfixes.install 1
modules/erp/erp_job_billable/erp_job_billable.install 1
modules/feed/feed.install 1
modules/forms_no_js/forms_no_js.module 1
modules/httpauth/httpauth.module -50
modules/jstools/jscalendar/jscalendar.install 10
modules/ljxp/ljxp.install 10
modules/na_arbitrator/na_arbitrator.install 9
modules/nodegoto/nodegoto.install 10
modules/nodeorder/nodeorder.install 5
modules/nodewords/nodewords.install 10
modules/notify/notify.install scheduler + 10
modules/og2list/og2list.install og+1 or 1
modules/og_forum/og_forum.install 2
modules/og_galleries/og_galleries.install 5
modules/og_gradebook/og_gradebook.install 2
modules/og_vocab/og_vocab.install 5
modules/primary_term/primary_term.install 9
modules/project/project.install 2
modules/project/project.install 2
modules/slideshow/slideshow.install 1
modules/tac_lite/tac_lite.install 9
modules/taxonomy_access/taxonomy_access.install 9
modules/taxonomy_defaults/taxonomy_defaults.install -1
modules/taxonomy_theme/taxonomy_theme.install -10
modules/trace/trace.install -99
modules/views/views.install 10

How to write themable modules

Note: this page describes Drupal's theming from the code side of things.

Drupal's theme system is very powerful. You can accommodate rather major changes in overall appearance and significant structural changes. Moreover, you control all aspects of your drupal site in terms of colors, mark-up, layout and even the position of most blocks (or boxes). You can leave blocks out, move them from right to left, up and down until it fits your needs.

At the basis of this are Drupal's theme functions. Each theme function takes a particular piece of data and outputs it as HTML. The default theme functions are all named theme_something() or theme_module_something(), thus allowing any module to add themeable parts to the default set provided by Drupal. Some of the basic theme functions include: theme_error() and theme_table() which as their name suggest return HTML code for an error message and a table respectively. Theme functions defined by modules include theme_forum_display() and theme_node_list().

Custom themes can implement their own version of these theme functions by defining mytheme_something() (if the theme is named mytheme). For example, functions named: mytheme_error(), mytheme_table(), mytheme_forum_display(), mytheme_node_list(), etc. corresponding to the default theme functions described above.

Drupal invokes these functions indirectly using the theme() function. For example:

<?php
$node
= node_load(array('nid' => $nid));
$output .= theme("node", $node);
?>

By default, this will call theme_node($node). However, if the currently active theme is "mytheme", and this theme has defined a function mytheme_node(), then mytheme_node($node) will be invoked instead.

This simple and straight-forward approach has proven to be both flexible and fast.

However, because direct PHP theming is not ideal for everyone, we have implemented mechanisms on top of this: so-called template engines can act as intermediaries between Drupal and the template/theme. The template engine will override the theme_functions() and stick the appropriate content into user defined (X)HTML templates.
This way, no PHP knowledge is required and a lot of the complexity is hidden away. More information about this can be found in the Theme developer's guide, specifically the Theming overview.

Writing .info files (Drupal 5.x)

Starting with Drupal 5.x, the concept of a .info file has been introduced to give Drupal a little more information about your module. This file is used primarily by the modules administration system for display purposes as well as providing criteria to control activation and deactivation. This file is required for Drupal 5 to recognize the presence of a module.

The following is a sample .info file from the views_bonus module:

; $Id $
name = Bonus: panels, teasers, 2 col
description = "Show views as teasers in two columns."
dependencies = views panels
package = Views

The .info file should have the same name as the .module file and reside in the same directory. For example, if your module is named example.module then your .info file should be named example.info.

This file is in standard .ini file format, which places items in key/value pairs separated by an equal sign. You may include the value in quotes, and you must include the value in quotes if the value includes some punctuation:

description = "Fred's crazy, crazy module; use with care!"

.info files may contain comments. The comment character is the semi-colon and denotes a comment until the end of the line. A comment may begin at any point on the line, thus it is especially important you quote any string that contains a comment. It is typical to place the CVS Id at the top of a .info file using a comment:

; $Id $

The .info file can contain the following fields:

name (Required)
The displayed name of your module. It should follow the Drupal 5 capitalization standard: only the first letter of the first word is capitalized ("Example module", not "example module" or "Example Module").
name = "Forum"

description (Required)
A short, preferably one line description that will tell the administrator what this module does on the module administration page. Remember, overly long descriptions can make this page difficult to work with, so please try to be concise.
description = "Enables threaded discussions about general topics."

If your .info file's description (or other field) contains characters other than alphanumeric values then you must quote the string. If you need to use " in the string then you need to use the &quot; value to display the " character.

For example, this will display correctly:

description = "This is my &quot;crazy@email.com&quot; email address"

This is wrong and cause Drupal to display an error when going to the modules menu:

description = This is my "crazy@email.com" address <- DO NOT DO THIS

dependencies (Optional)
A space separated list of other modules that your module requires. If these modules are not present, your module can not be enabled. If these modules are present but not enabled, the administrator will be prompted with a list of additional modules to enable and may choose to enable the required modules as well, or cancel at that point.
dependencies = taxonomy comment

package (Optional)
If your module comes with other modules or is meant to be used exclusively with other modules, enter the name of the package here. If left blank, the module will be listed as 'Other'. 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 and the like. All other modules should leave this blank. As a guideline, four or more modules that depend on each other (or all on a single module) make a good candidate for a package. Fewer probably do not.

If used, the package string is used to group modules together on the module administration display; the string should therefore be the heading you would like your modules to appear under, and it needs to be consistent (in spelling and capitalization) in all .info files in which it appears. It should not use punctuation and it should follow the Drupal 5 capitalization standard as noted above.

package = Views

Suggested examples of appropriate items for the package field:

  • Audio
  • Bot
  • CCK
  • Chat
  • E-Commerce
  • Event
  • Feed parser
  • Organic groups
  • Station
  • Video
  • Views
  • Voting (if it uses/requires VotingAPI)
  • Location

The exception to this rule is the "Development" package, which should be used for any modules which are code development tool modules (Devel, Coder, Module Builder...).

version (Optional)
The version string will ordinarily be added by drupal.org when a release is created and a tarball packaged. However, if your module is not being hosted on the drupal.org infrastructure, you can give your module whatever version string makes sense.

Users getting their modules directly from CVS will not have a version string, since the .info files checked into CVS do not define a version. These users are encouraged to use the CVS deploy module to provide accurate version strings for the admin/build/modules page for modules in directories checked out directly from CVS.

In the past (before the CVS deploy module existed), it was recommended to use this:

version = "$Name$"

However, this led to much confusion and is no longer recommended. If you are a module developer, and you already have the above line in your .info files checked into the Drupal CVS repository, you should remove that line.

project (packaging use only)
Module maintainers should not use this at all. The packaging script on drupal.org will automatically place a string here to identify what project the module came from. This is primarily for the Update status module, so that Drupal installations can monitor versions of installed packages and notify administrators when new versions are available.

For more information on ini file formatting, see the PHP.net parse_ini_file documentation.

Updating your modules

As Drupal develops with each release it becomes necessary to update modules to take advantage of new features and stay functional with Drupal's API.

Please note that from Drupal 5.x on, the Code Review module has a mode that attempts to automate the process of checking modules for things that need to be updated between versions. While not perfect, it can greatly assist the process of updating modules.

Converting 5.x modules to 6.x

Overview of Drupal API changes in 6.x

  1. Entirely new menu system
  2. Major FormAPI improvements
  3. New Schema API
  4. New format for hook_install()
  5. New format for hook_uninstall()
  6. New format for hook_update_N()
  7. The arguments to url() and l() have changed
  8. Variable names can now be 128 characters long
  9. Taxonomy terms are now associated with node revisions, not just nodes
  10. format_plural() accepts replacements
  11. $form['#base'] is gone
  12. New drupal_alter() function for developers
  13. hook_form_alter() parameters have changed
  14. hook_link_alter() parameters have changed
  15. hook_profile_alter() parameters have changed
  16. hook_mail_alter() parameters have changed
  17. $locale become $language
  18. New hook_theme() registry
  19. node/add is now menu generated
  20. New watchdog hook, logging and alerts
  21. Parameters of watchdog() changed
  22. new hook_update_N naming convention
  23. New syntax for .info files
  24. Core compatibility now specified in .info files
  25. New db_column_exists() method
  26. cache_set parameter order has changed
  27. Cache set and get automatically (un)serialize complex data types
  28. node_revision_list() now returns keyed array
  29. New operation in image.inc: image_scale_and_crop()
  30. New user_mail_tokens() method
  31. New ip_address() function when working behind proxies
  32. file_check_upload() merged into file_save_upload()
  33. {file_revisions} table is now {upload}
  34. drupal_add_css() supports automatic RTL CSS discovery
  35. Node previews and adding form fields to the node form

The menu system has been completely re-hauled in 6.x. See the Menu system overview.

Major FormAPI improvements

A number of major improvements have been made to FormAPI in Drupal 6, most specifically intended to improve the consistency and reliability of the API. While each individual change is relatively easy to implement, they will affect ALL form processing code and should be noted by all Drupal developers. For a complete list of these changes and sample code, see drupal.org/node/144132.

New Schema API

The database schema (table creation) for modules has now been abstracted into a Schema API. This means that you no longer need to look up database-specific syntax for doing CREATE TABLE statements, and adding additional database backends is much easier. More detailed documentation is available here: http://drupal.org/node/146843.

This patch caused changes to the format of hook_install(), hook_uninstall(), and hook_update_N(). No longer are switch statements done on $GLOBALS['db_type']; instead, use the variety of schema API functions to perform table manipulation.

New format for hook_install()

5.x:

(in book.install)

<?php
/**
* Implementation of hook_install().
*/
function book_install() {
  switch (
$GLOBALS['db_type']) {
    case
'mysql':
    case
'mysqli':
     
db_query("CREATE TABLE {book} (
        vid int unsigned NOT NULL default '0',
        nid int unsigned NOT NULL default '0',
        parent int NOT NULL default '0',
        weight tinyint NOT NULL default '0',
        PRIMARY KEY (vid),
        KEY nid (nid),
        KEY parent (parent)
      ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "
);
      break;
    case
'pgsql':
     
db_query("CREATE TABLE {book} (
        vid int_unsigned NOT NULL default '0',
        nid int_unsigned NOT NULL default '0',
        parent int NOT NULL default '0',
        weight smallint NOT NULL default '0',
        PRIMARY KEY (vid)
      )"
);
     
db_query("CREATE INDEX {book}_nid_idx ON {book} (nid)");
     
db_query("CREATE INDEX {book}_parent_idx ON {book} (parent)");
      break;
  }
}
?>

6.x:
(in book.install)

<?php
/**
* Implementation of hook_install().
*/
function book_install() {
 
// Create tables.
 
drupal_install_schema('book');
}
?>

(in book.schema)

<?php
/**
* Implementation of hook_schema().
*/
function book_schema() {
 
$schema['book'] = array(
   
'fields' => array(
     
'vid'    => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
     
'nid'    => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
     
'parent' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
     
'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny')
    ),
   
'indexes' => array(
     
'nid'    => array('nid'),
     
'parent' => array('parent')
    ),
   
'primary key' => array('vid'),
  );

  return
$schema;
}
?>

New format for hook_uninstall()

5.x:

<?php
/**
* Implementation of hook_uninstall().
*/
function book_uninstall() {
 
db_query('DROP TABLE {book}');
}
?>

6.x:

<?php
/**
* Implementation of hook_uninstall().
*/
function book_uninstall() {
 
// Remove tables.
 
drupal_uninstall_schema('book');
}
?>

New format for hook_update_N()

5.x:

<?php
/**
* Update files tables to associate files to a uid by default instead of a nid.
* Rename file_revisions to upload since it should only be used by the upload
* module used by upload to link files to nodes.
*/
function system_update_6022() {
 
$ret = array();
  switch (
$GLOBALS['db_type']) {
    case
'mysql':
    case
'mysqli':
     
// Change owernship of files to users rather than nodes and add columns
      // for file status and timestamp.
     
$ret[] = update_sql("ALTER TABLE {files} DROP INDEX nid");
     
$ret[] = update_sql('ALTER TABLE {files} CHANGE COLUMN nid uid int unsigned NOT NULL default 0');
     
$ret[] = update_sql("ALTER TABLE {files} ADD COLUMN status int NOT NULL default 0 AFTER filesize");
     
$ret[] = update_sql("ALTER TABLE {files} ADD COLUMN timestamp int unsigned NOT NULL default 0 AFTER status");
     
$ret[] = update_sql("ALTER TABLE {files} ADD KEY uid (uid)");
     
$ret[] = update_sql("ALTER TABLE {files} ADD KEY status (status)");
     
$ret[] = update_sql("ALTER TABLE {files} ADD KEY timestamp (timestamp)");

     
// Rename the file_revisions table to upload then add nid column.
     
$ret[] = update_sql('ALTER TABLE {file_revisions} RENAME TO {upload}');
     
$ret[] = update_sql('ALTER TABLE {upload} ADD COLUMN nid int unsigned NOT NULL default 0 AFTER fid');
     
$ret[] = update_sql("ALTER TABLE {upload} ADD KEY nid (nid)");
      break;

    case
'pgsql':
     
// @todo test the pgsql queries
      // Change owernship of files to users rather than nodes and add columns
      // for file status and timestamp.
     
$ret[] = update_sql("DROP INDEX {files}_nid_idx");
     
db_change_column($ret, 'files', 'nid', 'uid', 'int_unsigned', array('default' => '0', 'not null' => TRUE));
     
db_add_column($ret, 'files', 'status', 'uid', 'int', array('default' => '0', 'not null' => TRUE));
     
db_add_column($ret, 'files', 'timestamp', 'uid', 'int_unsigned', array('default' => '0', 'not null' => TRUE));
     
$ret = update_sql("CREATE INDEX {files}_uid_idx ON {files} (uid)");
     
$ret = update_sql("CREATE INDEX {files}_status_idx ON {files} (status)");
     
$ret = update_sql("CREATE INDEX {files}_timestamp_idx ON {files} (timestamp)");    

     
// Rename the file_revisions table to upload then add nid column.
     
$ret[] = update_sql("DROP INDEX {file_revisions}_vid_idx");
     
$ret[] = update_sql('ALTER TABLE {file_revisions} RENAME TO {upload}');
     
db_add_column($ret, 'upload', 'nid', 'int unsigned', array('default' => 0, 'not null' => TRUE));
     
$ret[] = update_sql("CREATE INDEX {upload}_vid_idx ON {upload} (vid)");
     
$ret[] = update_sql("CREATE INDEX {upload}_nid_idx ON {upload} (nid)");
           

      break;
  }
 
// The nid column was renamed to uid. Use the old nid to find the node's uid.
 
$ret[] = update_sql('UPDATE {files} f JOIN {node} n ON f.uid = n.nid SET f.uid = n.uid');
 
// Use the existing vid to find the nid.
 
$ret[] = update_sql('UPDATE {upload} u JOIN {node_revisions} r ON u.vid = r.vid SET u.nid = r.nid');
 
  return
$ret;
}
?>

6.x:

<?php
/**
* Update files tables to associate files to a uid by default instead of a nid.
* Rename file_revisions to upload since it should only be used by the upload
* module used by upload to link files to nodes.
*/
function system_update_6022() {
 
$ret = array();

 
// Rename the nid field to vid, add status and timestamp fields, and indexes.
 
db_drop_index($ret, 'files', 'nid');
 
db_change_field($ret, 'files', 'nid', 'uid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0));
 
db_add_field($ret, 'files', 'status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
 
db_add_field($ret, 'files', 'timestamp', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0));
 
db_add_index($ret, 'files', 'uid', array('uid'));
 
db_add_index($ret, 'files', 'status', array('status'));
 
db_add_index($ret, 'files', 'timestamp', array('timestamp'));

 
// Rename the file_revisions table to upload then add nid column. Since we're
  // changing the table name we need to drop and re-add the vid index so both
  // pgsql ends up with the corect index name.
 
db_drop_index($ret, 'file_revisions', 'vid');
 
db_rename_table($ret, 'file_revisions', 'upload');
 
db_add_field($ret, 'upload', 'nid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0));
 
db_add_index($ret, 'upload', 'nid', array('nid'));
 
db_add_index($ret, 'upload', 'vid', array('vid'));

 
// The nid column was renamed to uid. Use the old nid to find the node's uid.
 
$ret[] = update_sql('UPDATE {files} f JOIN {node} n ON f.uid = n.nid SET f.uid = n.uid');
 
// Use the existing vid to find the nid.
 
$ret[] = update_sql('UPDATE {upload} u JOIN {node_revisions} r ON u.vid = r.vid SET u.nid = r.nid');

  return
$ret;
}
?>

Arguments changed for url() and l()

The arguments to url() and l() have changed. Instead of a long line of single arguments (the exact options and order of which were hard to remember), both of these functions now take an array to specify the optional arguments. The new function signatures are as follows:

<?php
l
($text, $path, $options = array());
url($path = NULL, $options = array());
?>

$options is an associative array that contains 'query', 'fragment', 'absolute', 'alias' for both, and 'html' and 'attributes' for l(). Thus, the $attributes array that was a parameter in 5.x will now be passed in as $options['attributes'].

Additionally, the 'query' argument can now be passed as either a serialized string (as before), or as an array. Treating the query arguments as an array allows these functions to automatically urlencode() and format them with drupal_query_string_encode(). Furthermore, the query elements are just name/value pairs, so an array is the most logical data structure to store and manipulate them with.

A basic example to demonstrate the new syntax. 5.x and before:

<?php
url
("user/$account->uid", NULL, NULL, TRUE)
?>

6.x:

<?php
url
("user/$account->uid", array('absolute' => TRUE)).
?>

See http://api.drupal.org/api/HEAD/function/url and http://api.drupal.org/api/HEAD/function/l for more information.

(Note: these links are evil and wrong, and will break. They should really be .../api/6/function/url or something, but that doesn't work (yet). Note2: the l() docs need to be updated to describe $options['attributes']).

Variables can have names 128 characters long

The names of the variables accessed via variable_set() and variable_get() can now be 128 characters long.

Taxonomy terms are associated with node revisions

Taxonomy terms are now associated with specific node revisions, not just the node itself. This allows modules to know if the taxonomy terms have changed from one revision to the next. The developer-visible changes as a result of this are:

  • The {term_node} table now has a vid (version id) column, and (tid, nid, vid) is now the primary key. Any direct queries against {term_node} should be checked, and probably modified to include the node revision (vid), as well.
  • taxonomy_node_get_terms() and taxonomy_node_get_terms_by_vocabulary() now take a full $node object, not just a nid (node id).
  • taxonomy_node_delete() takes a full $node object, not just a nid (node id) and now deletes all taxonomy terms for all revisions of the specified node.
  • taxonomy_node_delete_revision() has been added to delete all taxonomy terms from the current revision of the given $node object.

format_plural() accepts replacements

An argument for replacements has been added to format_plural(), escaping and/or theming the values just as done with t(). The new function signature is as follows:

<?php
format_plural
($count, $singular, $plural, $args = array());
?>

For example, 5.x and before:

<?php
strtr
(format_plural($num, 'There is currently 1 %type post on your site.', 'There are currently @count %type posts on your site.'), array('%type' => theme('placeholder', $type)));
?>

6.x:

<?php
format_plural
($num, 'There is currently 1 %type post on your site.', 'There are currently @count %type posts on your site.', array('%type' => $type));
?>

New drupal_alter() function for developers

Developers building and manipulating standard Drupal data structures in their modules can expose those structures for manipulation by other modules using the drupal_alter() function. For example:

<?php
$data
= array();
$data['#my_property']       = 'Some data!';
$data['#my_other_property'] = 'More data...';
drupal_alter('my_data', $data);

// Do your stuff here...
?>

The above code would allow any other module to implement a hook_my_data_alter() to manipulate the $data array before any further processing is done. The majority of Drupal's internal data structures are stored and altered in this fashion, including the familiar Form API arrays and hook_form_alter().

The order of the parameters for hook_link_alter() has changed for greater consistency with the rest of core, and compatibility with the new drupal_alter() function.

Drupal 5:

<?php
function my_module_link_alter($node, &$links) {
 
// Alter the links...
}
?>

Drupal 6:

<?php
function my_module_link_alter(&$links, $node) {
 
// Alter the links...
}
?>

hook_profile_alter() parameters have changed

The order of the parameters for hook_profile_alter() has changed for greater consistency with the rest of core, and compatibility with the new drupal_alter() function.

Drupal 5:

<?php
function my_module_profile_alter($account, &$fields) {
 
// Alter the fields...
}
?>

Drupal 6:

<?php
function my_module_profile_alter(&$fields, $account) {
 
// Alter the profile...
}
?>

hook_mail_alter() parameters have changed

The order and makeup of the parameters for hook_mail_alter() has changed for greater consistency with the rest of core, and compatibility with the new drupal_alter() function. Rather than being passed in as individual parameters, each piece of data is a separate property of a standard Drupal data array.

Drupal 5:

<?php
function my_module_mail_alter(&$mailkey, &$to, &$subject, &$body, &$from, &$headers) {
 
// Alter the individual params...
 
$to = 'mail@example.com';
}
?>

Drupal 6:

<?php
function my_module_profile_alter(&$message) {
 
// Alter the mail message...
 
$message['#to'] = 'mail@example.com';
}
?>

$locale become $language

With the improved language subsystem in Drupal, the global $locale variable (which contained a language code) is replaced with the global $language object (which contains properties for several language details). As with $locale, the new $language object gives information about the language chosen for the current page request. This change also affects themes, because Drupal know knows about the directionality (left to right or right to left) of the language used, and themes can make use of this information to output proper stylesheets.

New hook_theme registry

Modules need to register all their theme functions via the new hook_theme(). This hook returns an array of theme functions, and arguments.

The theme_something() functions remain mostly unchanged, but you need to add a something_hook() like so:

<?php
function something_theme() {
  return array(
   
'something_format' => array(
     
'arguments' => array('content'),
    ),
  );
}
?>

This registers a theme function called something_format, which can be implemented by a theme_something_format($content) or via having .tpl.php files.

Detailed documentation to follow.

node/add is now menu generated

This means that you can use hook_menu_alter to change the visibility of an item or change the access callback.

New watchdog hook, logging and alerts

There is now a new hook_watchdog in core. This means that contributed modules can implement hook_watchdog to log Drupal events to custom destinations. Two core modules are included, dblog.module (formerly known as watchdog.module), and syslog.module. Other modules in contrib include an emaillog.module, included in the logging_alerts module. See syslog or emaillog for an example on how to implement hook_watchdog.

In a nutshell, the watchdog hook takes one argument, which is a keyed array containing the log message, variables to replace placeholders with in the message, severity, the user object, message type, request URI, Referer, IP address, and timestamp. Messages are translateable with t(), passing on the message and variables, or strtr() can be used to replace placeholders with values from the variables array to get an English message.

Modules can route the messages to custom destination, based on severity, or other criteria.

Here is a hypothetical example:

<?php
function mysms_watchdog($log = array()) {
    if (
$log['severity'] == WATCHDOG_ALERT) {
     
mysms_send($log['user']->uid,
       
$log['type'],
       
$log['message'],
       
$log['variables'],
       
$log['severity'],
       
$log['referer'],
       
$log['ip'],
       
format_date($log['timestamp'])));
}
?>

As well, the severity levels have been expanded to confirm to RFC 3164. This means there are new severity levels that your module can log to. For example, alert and critical can go to a pager, or instant messaging, while debug can be dumped to a file.

The severity levels are as follows.

define('WATCHDOG_EMERG',    0); // Emergency: system is unusable
define('WATCHDOG_ALERT',    1); // Alert: action must be taken immediately
define('WATCHDOG_CRITICAL', 2); // Critical: critical conditions
define('WATCHDOG_ERROR',    3); // Error: error conditions
define('WATCHDOG_WARNING',  4); // Warning: warning conditions
define('WATCHDOG_NOTICE',   5); // Notice: normal but significant condition
define('WATCHDOG_INFO',     6); // Informational: informational messages
define('WATCHDOG_DEBUG',    7); // Debug: debug-level messages

As a module developer, take special care not to flood destinations with high priority messages, such as critical or alert. In other word, use sparingly.

Module authors who use the watchdog() function with $type = 'debug' are strongly encouraged to replace that with $severity WATCHDOG_DEBUG for consistency.

So instead of:

<?php
watchdog
('debug', 'My debug message here');
?>

You should use (see also below for the parameter changes of watchdog()):

<?php
watchdog
('modulename', 'My debug message here', array(), WATCHDOG_DEBUG);
?>

Parameters of watchdog() changed

The watchdog() function was improved to support translation of log messages later (or skip translation), depending on the logging method used (see the watchdog hook above). The core dblog.module translates log messages to the language used by the administrator when viewing the log messages, while the syslog.module passes on English log messages to the system log.

In Drupal 5 and before, you used to call watchdog with:

<?php
watchdog
('user', t('Removed %username user.', array('%username' => $user->name)));
?>

In Drupal 6 however, t() is called later (or not at all), so you should not call t() on the message, but keep the message and parameters separated. Just remove t() and leave the parameter order intact:

<?php
watchdog
('user', 'Removed %username user.', array('%username' => $user->name));
?>

If you have a message which has no placeholders to replace with values, pass on an empty array() - which is also the default value for the variable array, so you can omit it, if you have no more parameters to pass. If you have a dynamically generated message (a PHP error message, a message coming from a remote service or some other type of message, whose literal text cannot be included in the second parameter), please always pass on NULL in place of the variables parameter, so Drupal knows that the message is not for translation.

<?php
// Translatable message, no placeholders to replace, and default watchdog type used, so third parameter can be omitted.
watchdog('soap', 'Remote host seems to be slow.');

// Translatable message, no placeholders to replace, but watchdog type different from default
watchdog('soap', 'Soap message sent.', array(), WATCHDOG_DEBUG);

// A message that should not be translated. In this case, pass NULL in the third parameter!
watchdog('soap', $remote_soap_message, NULL);
?>

Please also take the time to review your watchdog() calls for the correct use of the first parameter. Some contributed modules used to have that wrapped in t(), but that was and still is improper use of watchdog()!

New hook_update_N naming convention

Starting in Drupal 6.x, hook_update_N follows a naming convention of updates starting with the Drupal core compatibility version number, major version number, and then a sequential number indicating which update is taking place. For example:

<?php
/**
* Change the severity column in the watchdog table to the new values.
*/
function system_update_6007() {
 
$ret = array();
 
$ret[] = update_sql("UPDATE {watchdog} SET severity = %d WHERE severity = 0", WATCHDOG_NOTICE);
 
$ret[] = update_sql("UPDATE {watchdog} SET severity = %d WHERE severity = 1", WATCHDOG_WARNING);
 
$ret[] = update_sql("UPDATE {watchdog} SET severity = %d WHERE severity = 2", WATCHDOG_ERROR);
  return
$ret;
}
?>

This indicates that this is the eighth update for system.module in version Drupal 6.

Version 1.5 of a contributed module compatible with Drupal 6.x would name its first update function like so:

<?php
function example_update_6100() {
// Stuff.
}
?>

Version 2.4 of this same contributed module would name its third update function like so:

<?php
function example_update_6204() {
 
// Stuff.
}
?>

New syntax for .info files

The syntax used for the .info files that contain metadata about modules (name, description, version, dependencies, etc) now supports nested data. This change was necessary when themes got .info files.

Arrays are created using a GET-like syntax:

key[] = "numeric array"
key[index] = "associative array"
key[index][] = "nested numeric array"
key[index][index] = "nested associative array"

For more details, see the comment at the top of drupal_parse_info_file() in incldues/common.inc.
http://api.drupal.org/api/head/function/drupal_parse_info_file

here, but the doxygen formatting is stripping all the whitespace and making it impossible to read -->

At this time, the only visible change for modules is how they specify the list of dependencies on other modules. Previously, the "dependencies" value had special-case handling to treat the value as a list. Now, the .info file just explicitly defines the dependencies as a list using the new syntax.

For example, in 5.x:

name = Forum
description = Enables threaded discussions about general topics.
dependencies = taxonomy comment
...

6.x:

name = Forum
description = Enables threaded discussions about general topics.
dependencies[] = taxonomy
dependencies[] = comment
...

Core compatibility now specified in .info files

As of version 6.x, Drupal core will refuse to enable or run modules and themes that aren't explicitly ported to the right version of core. For 5.x, this was implicitly true for modules, due to the existance of the .info files. For 6.x and beyond, the .info file must specify which Drupal core compatiblity any module or theme has been ported to. This is accomplished by means of the new core attribute in the .info files.

6.x:

core = 6.x

Please note that the drupal.org packaging script automatically sets this value based on the Drupal core compatibility setting on each release node, so people downloading packaged releases from drupal.org will always get the right thing. However, for sites that deploy Drupal directly from CVS, it helps if you commit this change to the .info files for your modules. This is also a good way to indicate to users of each module what version of core the HEAD of CVS is compatibile with at any given time.

New db_column_exists() method

The db_column_exists($table, $column) method was added to the database abstraction layer in 6.x core. This allows developers to find out if a certain column exists in a given table, regardless of the underlying database management system being used (MySQL, PostgreSQL, etc).

Changes to cache_set parameter order

The parameter order of cache_set has been changed to conform better to the Drupal coding standards, where optional variables should always come after all required variables. Since $data is required and has no default value, it should occur before $table.

5.x

<?php
cache_set
('example_cid', 'cache', $data);
?>

6.x

<?php
cache_set
('example_cid', $data);
?>

Cache set and get automatically (un)serialize complex data types

cache_set now automatically serializes arrays and objects passed to it before saving them to the cache table. When retrieving data from the cache, cache_get now automatically unserializes the data when necessary. Simple datatypes such as strings and integers do not need to and will not be serialized before being stored.

5.x:

<?php
$simple
= 'Simple text';
$complex = array( 'one', 'two' );

cache_set('simple_cid', 'cache', $simple);
cache_set('complex_cid', 'cache', serialize($complex));
//..
$simple = cache_get('simple_cid');
$complex = unserialize(cache_get('complex_cid'));
?>

6.x:

<?php
$simple
= 'Simple text';
$complex = array( 'one', 'two' );

cache_set('simple_cid', $simple);
cache_set('complex_cid', $complex);
//..
$simple = cache_get('simple_cid');
$complex = cache_get('complex_cid');
?>

node_revision_list() now returns keyed array

In previous versions of core, the node_revision_list() method returned an ordered array, but there were no meaningful keys to index the data. If you wanted to find information about a specific revision, you had to iterate through the whole array until you found the revision ID you were looking for. Now, the array is indexed by the revision ID (the vid column from the {node_revisions} table) so if you're looking for information about a specific revision, you can find it immediately.

New image.inc function: image_scale_and_crop()

image_scale_and_crop() scales an image to the exact width and height given. The required aspect ratio is maintained by cropping the image equally on both sides, or equally on the top and bottom. No image toolkit changes are required.

New user_mail_tokens() method

user.module now provides a user_mail_tokens() function to return an array of the tokens available for the email notification messages it sends when accounts are created, activated, blocked, etc. Contributed modules that wish to make use of the same tokens for their own needs are encouraged to use this function.

New ip_address() function when working behind proxies.

There is a new function, ip_address() that should be used instead of the superglobal $_SERVER['REMOTE_ADDR']. When Drupal is run behind a reverse proxy, the address of the proxy server will be in this superglobal for all users, and hence many parts of Drupal will log the wrong IP address for the client. This function makes deducing the client IP address transparent, whether a proxy is used or not. It is recommended that you replace all $_SERVER['REMOTE_ADDR'] with ip_address().

{files} table changed

The files table has been changed to make it easier to preview uploaded files. Two fields, status and timestamp, have been added so that temporary files can be stored and cleaned automatically removed during a cron job. Use file_set_status() to change a files status and make it a permanent file.

file_check_upload() merged into file_save_upload()

To simply the process of uploading files file_check_upload() has been merged into file_save_upload(). file_save_upload() now takes an array of callback functions to validate the upload and saves the files as temporary files in the {files} table. This cleans up a lot of duplicative code in core's upload processing and makes file previews much simpler.

  • file_validate_extensions() checks that the file extension in in the given list
  • file_validate_size() checks for maximum file sizes and against a user's quota
  • file_validate_is_image() checks that the upload is an image
  • file_validate_image_resolution() checks that images meets maximum and minimum resolutions requirements

Drupal 5:

<?php
 
if ($file = file_check_upload('picture_upload')) {
   
// A whole lot of code to check the image dimensions and file size goes here
    //  ...
   
$info = image_get_info($file->filepath);
   
$destination = 'files/picture.'. $info['extension'];
   
$file = file_save_upload('picture_upload', $destination, FILE_EXISTS_REPLACE);
  }
?>

Drupal 6:
<?php
  $validators
= array(
   
'file_validate_is_image' => array(),
   
'file_validate_image_resolution' => array('85x85')),
   
'file_validate_size' => array(30 * 1024),
  );
  if (
$file = file_save_upload('picture_upload', $validators)) {
   
// All that validation is taken care of... but image was saved using
    // file_save_upload() and was added to the files table as a
    // temporary file. We'll make a copy and let the garbage collector
    // delete the original upload.
   
$info = image_get_info($file->filepath);
   
$destination = 'files/picture.'. $info['extension'];
   
file_copy($file, $destination, FILE_EXISTS_REPLACE);
  }
?>

The {file_revisions} table is now {upload}

In order to reduce confusion about the role that the {file_revisions} table played in Drupal's file handling, it's been renamed to {upload} to make it clear that it is purely for the use of the upload module, and for modules that would like that module to manage their files.

If you're writing a module that links files to nodes you need to create a table to maintain this relation. Creating your own table gives you the ability to do store additional information about the file relation, and is much cleaner that than trying to repurpose the upload module's table.

drupal_add_css() supports automatic RTL CSS discovery

When displaying the page in a right to left language, the drupal_add_css() function now automatically searches for CSS files named with -rtl.css suffixes, and adds them to the list of CSS files used to display the page. Drupal core comes with a set of right to left CSS files. Contributed modules and themes have the possibility to include right to left CSS overrides. More information available in the theme update guide.

Node previews and adding form fields to the node form

The is a subtle but important difference in the was node previews (and other such operations) are carried out when adding or editing a node. With the new Forms API, the node form is handled as a multi-step form. When the node form is previewed, all the form values are submitted, and the form is rebuilt with those form values put into $form['#node']. Thus, form elements that are added to the node form will lose any user input unless they set their '#default_value' elements using this embedded node object.

Drupal 5.x to 6.x FormAPI changes

FormAPI now uses the $form_state variable.

Previous versions of FormAPI used a combination of $form_values, global variables, and custom flags in the form definition itself to capture information about the form's workflow and its current state during processing. In Drupal 6, a single array -- $form_state -- is passed by reference along through each stage of form processing. Several standard $form_state keys are used in FormAPI by default:

  • $form_state['values']
    Incoming $_POST data is first sanitized and checked against the structure of the form before being handed off to validate and submit handlers. The 'values' key is used to store this collection of data. This key replaces the old separate $form_values variable that was passed to submit and validate handlers.
  • The following three keys can be used to control the rendering and processing workflow of the form. Validation and submission handlers can modify the data in these three keys to alter the form workflow based on user input.
  • $form_state['redirect']
    The 'redirect' key controls what happens after a form's processing is complete. By default, the page with the form on it will reload, so the form's fields can be cleared out. If 'redirect' is set to a Drupal path (like user/edit), the user will be redirected to that path instead. If 'redirect' is set to FALSE, the user will not be redirected after the form is processed -- the values they entered into the form will remain in the fields.
  • $form_state['rebuild']
    The 'rebuild' key overrides the 'redirect' key: when it is set to TRUE, the form will be rebuilt from scratch and displayed on screen. This gives form construction code a chance to add additional fields or alter the structure of the form based on user input (For example, re-building the form with additional fields if the user clicks 'give me more choices'). If this flag is set by a validation handler, any 'submit' handlers will be skipped. If it's set by a 'submit' handler, the form will be rebuilt and displayed after all submit handlers have finished processing.
  • $form_state['storage']
    When building complex forms that require multiple steps for completion (for example, a three-page survey), it's necessary to preserve the data from all steps so that they can be processed together at the end. (And, on occasion, to vary the contents of one step based on the input from the previous one). Any data placed in the 'storage' bin of the $form_state collection will automatically be cached and re-loaded when the form is next submitted, allowing your code to accumulate data from step to step and process it in the final stage without any additional code. Developers who want more control can use their own caching mechanisms to store temporary form data (the user session and hidden form fields are two popular alternatives), but the 'storage' bin is automatically handled for you by FormAPI.
    Note that if $form_state['storage'] is populated, $form_state['rebuild'] is automatically set to TRUE.
  • $form_state['submitted']
    $form_state['submit_handlers']
    $form_state['validate_handlers']

    These three keys store information about the current processing state of the form. If 'submitted' is TRUE, user input is currently being processed. The 'submit_handlers' and 'validate_handlers' keys hold any custom validation or submission handlers that were attached to the specific button clicked by the user.

Validation and submission handlers can place additional data in custom bins in the $form_state.

Parameters for form validation and submission functions have changed

Previously, $form-id_validate and $form-id_submit() functions received the $form_id and $form_values parameters. Now, they receive $form and $form_state. If the form's ID is needed, it can still be retrieved from $form['form_id']['#value'], or $form_values['form_id']. In most cases, however, that ID is unneeded.

Drupal 5:

<?php
function my_form_validate($form_id, $form_values) {
 
// validation code goes here...
}
?>

Drupal 6:

<?php
function my_form_validate($form, &$form_state) {
 
// validation code goes here...
}
?>

Parameters for hook_form_alter() have changed

Similar syntax changes have been made to hook_form_alter(), though it still preserves the $form_id parameter given its use as a central function that operates on many different forms. In addition, $form is now the first parameter rather than the second, to preserve consistency with other hook_$foo_alter() functions throughout core.

Drupal 5:

<?php
function my_module_form_alter($form_id, &$form) {
  if (
$form_id == 'my_form') {
   
// Form modification code goes here.
 
}
}
?>

Drupal 6:

<?php
function my_module_form_alter(&$form, $form_state, $form_id) {
  if (
$form_id == 'my_form') {
   
// Form modification code goes here; $form_state cannot be modified,
    // but decisions can be made based on its contents.
 
}
}
?>

hook_form_alter() complimented by hook_form_$form-id_alter()

Optionally, modules can implement form-specific alteration functions rather than a single hook_form_alter() with many conditional switch statements. This is optional, and is most useful for tidying the code of modules that alter many forms to customize a site's operations.

Drupal 5:

<?php
function my_module_form_alter($form_id, &$form) {
  switch (
$form_id) {
    case
'my_form':
   
// Form modification code goes here.
   
break;

    case
'my_second_form':
   
// Form modification code goes here.
   
break;

    case
'my_third_form':
   
// Form modification code goes here.
   
break;
  }
}
?>

Drupal 6:

<?php
function my_module_my_form_form_alter(&$form, $form_state, $form_id) {
 
// Form modification code goes here.
}

function
my_module_form_my_second_form_alter(&$form, $form_state, $form_id) {
 
// Form modification code goes here.
}

function
my_module_my_third_form_form_alter(&$form, $form_state, $form_id) {
 
// Form modification code goes here.
}
?>

Submit handlers use $form_state rather than returning urls

In previous versions, a Drupal path was returned by a submit function in order to indicate that the user should be redirected to a new page after processing. In Drupal 6, the $form_state collection is used to control the processing of a form. Instead of returning a path, submit handlers can populate $form_state['redirect'] with a path.

In addition, developers using submit functions to persist data to the database are encouraged to return meaningful data about the results of the operation (ids of newly created nodes, etc.) in the $form_state. This allows developers calling forms using drupal_execute() to properly handle to the results of a form's successful submission.

Drupal 5:

<?php
function my_form_submit($form_id, $form_values) {
 
// processing code goes here...
 
return 'admin/my_page';
}
?>

Drupal 6:

<?php
function my_form_submit($form, &$form_state) {
 
// processing code goes here; we're creating a node in this example...
 
$form_state['redirect'] = 'admin/my_page';
 
$form_state['nid'] = $node->nid;
}
?>

$form['#submit'] and $form['#validate'] and $form['#process'] no longer support custom parameters

The #validate and #submit properties on a form can be used to add custom validation and submission handlers to a form, beyond the default $form-id_validate() and $form-id_submit() function names. Previously, it was possible to define custom arguments to pass into each of those parameters. Since the entire form array is now passed to each of those functions, additional parameters are unnecessary (same with #process).

Drupal 5:

<?php
function my_form() {
 
// form definition code goes here...
 
$form['#submit']['my_submit_function'] = array($param1, $param2);
  return
$form;
}
?>

Drupal 6:

<?php
function my_form() {
 
// form definition code goes here...
 
$form['#submit'][] = 'my_submit_function';
 
$form['#my_form_param1'] = $param1;
 
$form['#my_form_param2'] = $param2;
}
?>

When altering a form to add an additional submit handler, similar changes must be made:

Drupal 5:

<?php
function my_module_form_alter($form_id, &$form) {
  if (
$form_id == 'my_form') {
   
$form['#submit']['my_additional_submit_handler'] = array();
  }
}
?>

Drupal 6:

<?php
function my_module_form_alter(&$form, $form_state, $form_id) {
  if (
$form_id == 'my_form') {
   
$form['#submit'][] = 'my_additional_submit_handler';
  }
}
?>

form_set_value() parameters have changed

form_set_value(), like most other functions, now accepts the $form_state parameter. Adding this parameter to the end of every call to form_set_values() is required, as $form_state has replaced the internal global variables that were previously used to store form values.

Drupal 5:

<?php
form_set_value
($form_element, 'value');
?>

Drupal 6:

<?php
form_set_value
($form_element, 'value', $form_state);
?>

#multistep is gone, use $form_state instead

In previous versions of Drupal, forms that looped, repeatedly adding more fields or displaying different options based on previous user input, used a special #multistep property in the form definition. This behavior is no longer controlled by an explicit property, but by the contents of the $form_state collection.

When either a validation or a submission handler has completed their work, setting $form_state['rebuild'] to TRUE will cause the form to be rebuilt and rendered in a manner similar to that triggered by Drupal 5's #multistep property. The complete contents of $form_state are given to your form constructor function when the form is rebuilt, including $form_state['values'] for incoming form values and any other data stored in $form_state by your validation or submission handlers.

Also note that $form_values was previously passed in at the end of the list of parameters for your form constructor function, while $form_state is passed in before any additional parameters. Now that $form_state is passed into every form constructor function, it appears first in the list of parameters.

Drupal 5:

<?php
function my_form($param1, $form_values = NULL) {
 
// standard form definition code goes here...
 
$form['#multistep'] = TRUE;
 
$form['#redirect'] = FALSE;

  if (!empty(
$form_values)) {
   
// We're building the form a second time based on previous input...
    // Add additional fields, etc.
 
}
}


function
my_form_submit($form_id, $form_values) {
 
// process the form input
}
?>

Drupal 6:

<?php
function my_form($form_state, $param1) {
 
// standard form definition code goes here...

 
if (!empty($form_state)) {
   
$submitted_data = $form_state['values'];
   
$my_custom_stuff = $form_state['my_data'];

   
// We're building the form a second time based on previous input...
    // Add additional fields, etc.
 
}
}

function
my_form_submit($form, &$form_state) {
 
// process the form input...
 
$form_state['rebuild'] = TRUE;
 
$form_state['my_data'] =  'this will be passed back to the form constructor when the form is rebuilt...';
}
?>

In addition, a 'storage' element in the $form_state collection allows submission handlers to save temporary data between page loads, until a form's processing is completed. This allows forms that use multiple steps to build up a single data structure (like a complex survey, or data that must be persisted in a single batch) to accumulate information in $form_state['storage'], allowing FormAPI to keep track of it until the time comes to persist it properly in the submission handler during the form's final step.

If $form_state['storage'] is populated by any submit handlers, $form_state['rebuild'] is automatically set to true, and the data in $form_state['storage'] is cached and retrieved each time the page is re-loaded.

Validation for specific form elements now uses the #element_validate property

Previously, it was possible to set the #validate property on any form element, and the specified validation handlers would evaluate any data submitted in the element. This was most useful when defining custom form elements that needed special validation rules at all times (password validation fields, date fields, etc.).

However, #validate behaved differently if the property was set on a specific element rather than the entire form. While form-level validation received $form_id and $form_values as parameters, individual elements received $form_id and the entire $form array, without the $form_values collection. Due to the differences in behavior between these two modes, the functions have been separated into #validate and #element_validate.

When setting form-level validation functions, use #validate. When adding custom validation rules to specific form elements, use #element_validate.

Drupal 5:

<?php
function my_form() {
 
// standard form definition code goes here...
 
$form['#validate']['my_custom_validate'] = array(); 
}

function
my_module_elements() {
  return array(
   
'my_custom_form_element' => array(
      
'#custom_property' => 'foo',
      
'#validate' => 'my_element_validate',
     );
  );
}
?>

Drupal 6:

<?php
function my_form() {
 
// standard form definition code goes here...
 
$form['#validate'][] = 'my_custom_validate'
}

function
my_module_elements() {
  return array(
   
'my_custom_form_element' => array(
      
'#custom_property' => 'foo',
      
'#element_validate' => 'my_element_validate',
     );
  );
}
?>

$form['#base'] is gone

In FormAPI, many forms with different form_ids can share the same validate, submit, and theme handlers. This can be done by manually populating the $form['#submit'], $form['#validate'], and $form['#theme'] elements with the proper function names.

Previously, it was also possible to do this by setting $form['#base'] to a shared ID across all of the forms. In Drupal 6 and later, the use of #base has been eliminated for the sake of simplicity.

Drupal 4.7, 5:

<?php
function my_shared_form() {
 
$form['#base'] = 'my_shared_form_id';
 
// more form stuff here...
 
return $form;
}
?>

Drupal 6:

<?php
function my_shared_form() {
 
$form['#submit'][] = 'my_shared_form_submit'];
 
$form['#validate']['my_shared_form_validate'];
 
$form['#theme'] = 'my_shared_form'; // this will be called as theme('my_shared_form').
  // more form stuff here...
 
return $form;
}
?>

This is the same format used when modifying forms with hook_form_alter, and the clarity and consistency should help eliminate some confusion.

$form['#pre_render'] is gone

The #pre_render property has been removed from FormAPI; it was used solely to implement node previews, and is no longer necessary.

Form buttons can define custom #submit and #validate handlers

All forms can have #validate and #submit properties containing lists of validation and submission handlers to be executed when a user submits data. Previously, if a form featured multiple submission buttons to initiate different actions (updating a record versus deleting, for example), it was necessary to check the incoming $form_values['op'] for the name of the clicked button, then execute different code based on its value.

Now, it is possible to define #validate and #submit properties on each individual form button if desired.

When a specific button is used to submit a form, its validation and submission handlers will be used rather than the default form-level ones. If none are specified at the button level, the form-level handlers will be used instead.

Drupal 5:

<?php
function my_form() {
 
// standard form definition code goes here...
 
$form['delete'] = array(
   
'#type' => 'submit',
   
'#value' => t('Delete'),
  );
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Submit'),
  );
}


function
my_form_submit($form_id, $form_values) {
  if (
$form_values['op'] == t('Delete') {
   
// Delete the record
 
}
  else {
   
// Update the record
 
}
}
?>

Drupal 6:

<?php
function my_form() {
 
// standard form definition code goes here...
 
$form['delete'] = array(
   
'#type' => 'submit',
   
'#value' => t('Delete'),
   
'#submit' => array('my_delete_function'),
  );
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Submit'),
  );
}

function
my_form_submit($form, $form_state) {
   
// Update the record
}

function
my_delete_function($form, $form_state) {
   
// Delete the record
}
?>

Validation handlers can pass information to submit handlers

Validation handlers must sometimes perform complex, time-consuming, or resource-intensive operations to properly validate data. Repeating these expensive operations in the submission handlers is undesirable. Previously, it was necessary to embed these results in the $form_values to carry them over from validation to submission. Now, though, validation handlers can simply place them into $form_state.
This keeps $form_values clean, and ensures that it always represents the data submitted by the web user, rather than a general storage bin for temporary information. It also eliminates the need to create dummy 'hidden' fields in forms to serve as storage bins for such data.

Drupal 5:

<?php
function my_form_validate($form_id, $form_values, $form) {
 
// contact a web service that charges per-request!
 
if ($results) {
   
form_set_value($form['my_dummy_element'], $results);
  }
  else {
   
form_set_error('form', t('The results were incorrect!');
  }
}

function
my_form_submit($form_id, $form_values) {
 
$results = $form_values['my_dummy_element'];
 
// process the results
}
?>

Drupal 6:

<?php
function my_form_validate($form, &$form_state) {
 
// contact a web service that charges per-request!
 
if ($results) {
   
$form_state['expensive_results'] = $results;
  }
  else {
   
form_set_error('form', t('The results were incorrect!');
  }
}

function
my_form_submit($form, &$form_state) {
 
$results = $form_state['expensive_results'];
 
// process the results
}
?>
AJAX with 6.x FormAPI

by pwolanin - draft based on IRC conversation with Eaton

AJAX with 6.x FormAPI form (such as a form_alter'd piece of the node form) is, right now, is a theoretical possibility. It's not something that any code explicitly supports with helper functions or what not, but it is now more readily possible than with 5.x.

First, make sure that the form has $form['#cache'] = TRUE; because normally, Drupal doesn't cache the first time a form is shown, only when it's rebuilt for the first time, indicating that it'll probably be looping.

The form build id shows up in a hidden field on the client side. This is easily transformed into a cache id as $cid = 'form_'. $build_id. Then you can get the cached form in your callback, alter it, and save it back to the cache like:

$cache = cache_get($cid, 'cache_form');
$form = $cache->data;

// Alter the form
$form['my-select']['#options'] = $new_options;

$expire = max(ini_get('session.cookie_lifetime'), 86400);
cache_set($cid, $form, 'cache_form', $expire);

In terms of what you need to do to alter the form, the cached form is exactly what would be passed through a normal hook_form_alter function. It's actually cached right after that step.

Note- it is important that any server-side callback does not trust the user input. For example, you should never, ever, take from the client side a comma-delimited list for the new set of options for a select box! A hacker could easily re-write the JS to send options that will endanger your site security.

You'll almost certainly want to send back to the client side the HTML corresponding to the altered element, so that the visible form can be updated by the JS:

note - we're all still a little fuzzy on the best way to do this step. You need to process the form further so that the element you want is ready to be rendered.

$form_state = array();
$form['#post'] = array();
$form = form_builder($form['form_id']['#value'] , $form, $form_state);
$output = drupal_render($form['my-select']);
print(drupal_to_js($output));
exit();

The essential logic of the caching is all in function drupal_get_form(), so look there for guidance.

Converting 4.7.x modules to 5.x

This page describes changes to the module interface; a 5.x themes conversion guide is also available.

Overview of Drupal API changes in 5.x

  1. New module .info files must read
  2. New handling of links for hook_link()
  3. New hook_link_alter()
  4. Changed menu_primary_links(), and menu_secondary_links() return structured links
  5. Change user_mail() to drupal_mail()
  6. New hook_mail_alter()
  7. Change user_mail_wrapper() to drupal_mail_wrapper()
  8. Removed hook_settings()
  9. New hook_profile_alter()
  10. New message_na() removed
  11. New administration layout
  12. New drupal_add_css() - proper way to add css
  13. hook_taxonomy('form') has been removed. use hook_form_alter() instead
  14. Change form_render() to drupal_render()
  15. Change hook_view() and hook_nodeapi($op = 'view')
  16. New hook_nodeapi($op = 'alter') has been added
  17. Changes to hook_node_info() and the node type system.
  18. The node_get_names() and node_get_base() functions no longer exist.
  19. New hook_node_type()
  20. Changes to node type settings form
  21. New db_table_exists()
  22. New hook_node_operations(), hook_user_operations()
  23. Altered the behaviour of placeholders in t() calls
  24. Changed drupal_get_form() to take a $form_id, not a $form
  25. Changed - forms must be created in dedicated builder functions
  26. New hook_forms() optionally maps form_ids to builder functions
  27. New drupal_execute() function allows form data to be submitted programmatically
  28. module_exist() is now module_exists()
  29. format_plural() @count change
  30. Vastly extended drupal_add_js()
  31. Removed drupal_call_js()
  32. New drupal_add_feed() and drupal_get_feeds replaces theme_add_link()
  33. theme('page') may omit standard blocks
  34. New #disabled Form API property
  35. New changed cache API
  36. Uninstall hook
  37. Added jQuery to Drupal
  38. $_POST[op] deprecated in favor of $form_values[op]
  39. Change system_listing() to drupal_system_listing()
  40. Change menu item and node links to use Sentence capitalization instead of lowercase
  41. Major changes to node access system will affect all modules utilizing node_access table
  42. Changes to how confirm_form() works
  43. Changes to how #prefix and #suffix are rendered in form arrays
  44. Changes to how #options are represented in some form arrays (Especially the $form['taxonomy'] array).
  45. $node->moderate no longer used by core.

.info files

All modules now need to have a modulename.info file, containing meta information about the module. The format is:

; $Id$
name = Module Name
description = A description of what your module does.

Without this file, your module will not show up in the module listing!. You may remove this information from your hook_help() implementation.

There are also 2 optional lines that may appear in the .info file:

dependencies = module1 module2 module3
package = "Your arbitrary grouping string"

If you assign dependencies for your module, Drupal will not allow it to be activated until the required dependencies are met.

For compatibility with PHP versions earlier than 5.0, each value must be on a single line. It is not possible to include line breaks (e.g. neither newline nor return characters) within a single entry in this file.

If you assign a package string for your module, on the admin/build/modules page it will be listed with other modules with the same category. If you do not assign one, it will simply be listed as 'Other'. Not assigning a package for your module is perfectly ok; in general packages are best used for modules that are distributed together or are meant to be used together. If you have any doubt, leave this field blank.

Suggested examples of appropriate items for the package field:

  • Audio
  • Bot
  • CCK
  • Chat
  • E-Commerce
  • Event
  • Feed Parser
  • Organic groups
  • Station
  • Video
  • Views
  • Voting (if it uses/requires VotingAPI)

The files use the ini format and can include a ; $Id$ to have CVS insert the file ID information.

For more information on ini file formatting, see the PHP.net parse_ini_file documentation.

See http://drupal.org/node/101009 for more information on .info files.

hook_link() has changed in 5.x. No longer do you pass in an array of l()s, rather, you pass in a structured link array, much like forms api.

4.7.x:

<?php
function blog_link($type, $node = 0, $main = 0) {
 
$links = array();

  if (
$type == 'node' && $node->type == 'blog') {
    if (
arg(0) != 'blog' || arg(1) != $node->uid) {
     
$links[] = l(t("%username's blog"
                   array(
'%username' => $node->name)),
                  
"blog/$node->uid",
                   array(
'title' => t("Read %username's latest blog entries.",
                   array(
'%username' => $node->name))));
    }
  }

  return
$links;
}
?>

Now in 5.x:

<?php
function blog_link($type, $node = 0, $main = 0) {
 
$links = array();

  if (
$type == 'node' && $node->type == 'blog') {
    if (
arg(0) != 'blog' || arg(1) != $node->uid) {
     
$links['blog_usernames_blog'] = array(
       
'title' => t("%username's blog",
        array(
'%username' => $node->name)),
       
'href' => "blog/$node->uid",
       
'attributes' => array('title' => t("Read %username's latest blog entries.", array('%username' => $node->name)))
      );
    }
  }

  return
$links;
}
?>

To add links, the following variables can be used:

<?php
$links
['my_module_name_link_description'] = array(
  
'title' => t('Link Title'),
  
'href' => "node/$nid",
  
'attributes' => array('title' => t('Descriptive link title')),
  
'query' => 'some_query',
  
'fragment' => 'my_page_fragment',
  
'html' => FALSE,
);
?>

Note, be sure to specify 'my_module_name_link_description' in that order to be consistent and avoid namespace conflicts. Examples, 'menu-4-3-2', 'node-read-more', 'comment-add-new'.

The final variable, 'html', is used to specify if the link title is itself html code. If FALSE (the default), the title will be filtered for safe output. However, sometimes you need to have the title printed out without filtering (in which case it is your responsibility to ensure the input is safe). See the documentation for the l() function for more details. For example, to include an image in a link title, you would use something like this:

<?php
function station_listen_links($node, $short = FALSE)  {
 
$listen_url = 'station/archives/'. $node->nid;
 
$img_listen = drupal_get_path('module', 'station_schedule') .'/images/listen_tiny.gif';
 
  return array(
   
'station_archive_listen' => array(
     
'href' => $listen_url,
     
'title' => theme('image', $img_listen, t('Listen')) . t('Listen to previous'),
     
'attributes' => array('title' => t('Listen to previous broadcasts of this show')),
     
'html' => TRUE,
    ),
  );
}
?>

Additionally, for each link created, a class is automatically added to each link based on the name given to the link (e.g., my_module_name_link_description). You can optionally add more classes as well, but in most cases, this is probably not needed.

A new hook has been created for modules wishing to alter links of other modules: hook_link_alter().

To use this:

<?php
function forum_link_alter(&$node, &$links) {
  foreach (
$links AS $module => $link) {
    if (
strstr($module, 'taxonomy_term')) {
     
// Link back to the forum and not the taxonomy term page
     
$links[$module]['href'] = str_replace('taxonomy/term', 'forum', $link['href']);
    }
  }
}
?>

The $node object and array of $links are passed by reference for altering, much like hook_form_alter().

Note, this works for both node links and comment links.

menu_primary_links() and menu_secondary_links() now return structured links. No longer is a an array of l()s returned, but rather a structured array of links. When you loop through these links in $primary_links or $secondary_links in a phpTemplate theme (or menu_primary_links() and menu_secondary_links() respectively), you must build your l() by passing in the values.

4.7.x themes:

<?php
print '<ul>';
foreach (
$primary_links as $link) {
   print
'<li>'. $link .'</li>';
}
print
'</ul>';
?>

5.x themes:

<?php
print '<ul>';
foreach (
$primary_links as $link) {
  print
'<li>'. l($link['title'], $link['href'],
   
$link['attributes'], $link['query'],
   
$link['fragment'], FALSE, $link['html']) .'</li>';
}
print
'</ul>';
?>

It is recommended to pass these directly to theme('links') which will take care of this for you.

Example:

<?php
print theme('links', $primary_links);
?>

If you wish to have your links appear in a list as in above, simply override theme('links') in your theme and add this logic to print the UL and LI. This is the cleanest approach.

user_mail() is replaced by drupal_mail()

If your module uses user_mail() to send email, then you have to change that to use the new drupal_mail() function.

<?php
drupal_mail
($mailkey, $to, $subject, $body, $from, $headers);
?>

The new $mailkey is used to identify the email for hook_mail_alter (see below). $to is a string with one or more recipients, $subject

is the mail subject, $body is the message body. The optional $from sets From, Reply-To, Return-Path and Error-To headers to this same value (a common practice). The optional $headers is an associative array with header names and values. This function ensures that the header values are MIME encoded, and the newlines are correct.

New hook_mail_alter()

Your module can implement a hook_mail_alter() that takes the same arguments as the drupal_mail() function. You can use this to add a standard site footer to all outgoing emails, add special headers, or completely HTML-ize your mails. You should take the parameters by reference and change the values. hook_mail_alter() is not supposed to return anything.

user_mail_wrapper() changed to drupal_mail_wrapper()

The user_mail_wrapper() function used to support an alternate mail backend (instead of the PHP built-in mail() function) should now be called drupal_mail_wrapper(). It should take the same arguments as drupal_mail() does.

Removed hook_settings()

hook_settings() has been removed. Modules that want to add a settings page should now use the menu callback system. Settings pages are no longer a special case. Add a MENU_NORMAL_ITEM to your modules _menu() function and register your module's settings page(s) under ?q=admin/settings/<some-path>. In your callback, you need to define the form using the forms API. You don't have to implement a _validate or _submit hook. The system.module implements default validate and submit functions for simple setting forms. Just use the helper function system_settings_form() or return the form using the following snippet:

Here is an example:

The 4.7.x way:

<?php
function yourmodule_settings() {
$form['blah'] = ...

return
$form;
}
?>

In 5.x, this should be changed to:

<?php
function yourmodule_menu($may_cache) {
...
$items[] = array(
   
'path' => 'admin/settings/your-module',
   
'title' => t('your module name'),
   
'description' => t('Describes what the settings generally do.'),
   
'callback' => 'drupal_get_form',
   
'callback arguments' => array('yourmodule_admin_settings'),
   
'access' => user_access('administer site configuration'),
   
'type' => MENU_NORMAL_ITEM, // optional
  
);
...
}

function
yourmodule_admin_settings() {
...
$form['something'] = array(
  ...
  );
 
return
system_settings_form($form);
}
?>

New hook_profile_alter()

A new hook has been created for modules wishing to alter fields which appear on the user profile page: hook_profile_alter(). Modules may remove fields (e.g. a privacy control module for individuals) and/or change the order/organization of the fields.

To use this:

<?php
function mymodule_profile_alter(&$account, &$fields) {
  foreach (
$fields AS $key => $field) {
   
// do something
 
}
}
?>

All fields should have a unique key provided by the module which initially inserted the field. If no such key exists, please file an issue (and patch) against the offending module.

message_na() removed

The function message_na() was removed, remove it from your modules as well and replace it with t('n/a').

New administration layout

The layout of the administration pages have changed.

References to some common administration pages have changed; in particular, admin/modules is now admin/build/modules, admin/menu is now admin/build/menu and admin/block is now admin/build/block; for all existing links you may have into the admin/ tree, please check to see if the destination has moved, especially in your hook_help().

Particularly noteworthy, in your hook_help(), you need to remove the admin/modules#description section and, instead, put the description in the new module .info file. The .info file is required.

You should no longer put your module's administration links directly under admin/, but instead should identify an area for your module's administration links. Existing administration sections are:

  • admin/content -- for content related activities; modules that create a node type should put their administrative items here.
  • admin/build -- for site layout related activities; modules that modify the the structure of the site may put their administrative items here.
  • admin/log -- for logging and site status related pages and administrative items.
  • admin/user -- for items dealing with user or user access management.
  • admin/settings -- for pretty much everything else, considered general configuration.

Modules and module groups that may have many administration pages are allowed to create a block for themselves. For example, ecommerce might create an admin/ecommerce item. Administrative menu items that create these areas may set their callback to system_admin_menu_block_page(), or they can set it to a page that provides a general overview (but the page may not be visited often so do not put important information only on this page). The administrative overview allows these items to set 'position' => 'left' or 'right'; this doesn't need to be set, and it is expected some administrative themes will ignore this setting.

For example:

<?php
  $items
[] = array(
   
'path' => 'admin/my_settings',
   
'title' => t('My modules configuration'),
   
'description' => t('Adjust my modules configuration options.'),
   
'position' => 'right',
   
'weight' => -5,
   
'callback' => 'system_admin_menu_block_page',
   
'access' => $access);
?>

Finally, all administrative items should provide a 'description' in their menu item array that the administrative overview can provide to the user.

For example:

<?php
  $items
[] = array(
   
'path' => 'admin/my_settings/mymodule-information',
   
'title' => t('My module information'),
   
'description' => t('Change my module information, such the e-mail address.'),
   
'callback' => 'mymodule_information_settings',
   
'access' => user_access('administer site configuration'),
  );
?>

drupal_add_css() - the proper way to add CSS

Modules should now use:

<?php
drupal_add_css
(drupal_get_path('module', 'module-name') .'/module.css');
?>

To properly add CSS files to the theme. Read more about it at the theming level.

hook_view() and hook_nodeapi($op = 'view') have changed

Nodes are now prepared for display by assembling a structured array in $node->content, rather than directly manipulating $node->body and $node->teaser. The format of this array is the same used by FormAPI.

Modules that use hook_view() to prepare their custom node types for display should now use the following format:

<?php
function mymodule_view($node, $teaser, $page) {
 
$node = node_prepare($node, $teaser);
 
$node->content['myfield'] = array(
   
'#value' => theme('mymodule_myfield', $node->myfield),
   
'#weight' => 1,
  );
  return
$node;
}
?>

Modules that use hook_nodeapi() to alter the node's content should use a format :

<?php
function mymodule_nodeapi(&$node, $op) {
  if (
$op == 'view') {
   
$node->content['my_additional_field'] = array(
     
'#value' => theme('mymodule_my_additional_field', $additional_field),
     
'#weight' => 10,
    );
  }
}
?>

As with FormAPI arrays, the #weight property can be used to control the relative positions of added elements. This replaces the old method of appending or prepending text to $node->body. If for some reason you need to change the body or teaser returned by node_prepare(), you can modify $node->content['body']['#value']. Not that this will be the un-rendered content. To modify the rendered output, see hook_nodeapi($op = 'alter').

Other operations, like setting the breadcrumb trail, are unchanged.

hook_nodeapi($op = 'alter') has been added

The new NodeAPI 'alter' operation lets modules modify the fully-rendered node body or teaser. This works the way Drupal 4.7's 'view' operation did, and should only be used when text substitution, filtering, or other raw text operations are necessary. For example:

<?php
function mymodule_nodeapi(&$node, $op) {
  if (
$op == 'alter') {
   
$node->body = str_replace($node->body, 'Original word', 'Replacement word');
   
$node->teaser = str_replace($node->teaser, 'Original word', 'Replacement word');
  }
}
?>

Changes to hook_node_info() and the node type system

hook_node_info() now allows node modules to define more attributes for their node types. Node types are still defined by returning an array of arrays. The 'base' array sub-element has been renamed 'module'. There are now three required attributes for all node types defined through hook_node_info(): 'name', 'module', and 'description'.

Example:

<?php
function book_node_info() {
  return array(
   
'book' => array(
     
'name' => t('Book page'),
     
'module' => 'book',
     
'description' => t("A book is a collaborative writing effort: users
      can collaborate writing the pages of the book, positioning the
      pages in the right order, and reviewing or modifying pages previously
      written. So when you have some information to share or when you
      read a page of the book and you didn't like it, or if you think a certain
      page could have been written better, you can do something about it."
),
    )
  );
}
?>

Additionally, there are now seven optional attributes: 'help', 'has_title', 'title_label', 'has_body', 'body_label', 'min_word_count', and 'locked'. Previously, node modules could only define the machine-readable name (the key of each array element), the human-readable name (the 'name' array sub-element), and the base function name (the former 'base' array sub-element) of each node type.

hook_node_info() is now to be used only for defining module-provided node types. User-provided (or 'custom') node types are defined only in the new 'node_type' database table, and they should be maintained by using the node_type_save() and node_type_delete() functions. These node types should not be dynamically (re-)defined through hook_node_info().

The 'node_type' database table is now the authoritative source that defines all node types on a site. Previously, this table did not exist, and hook_node_info() was the authoritative source.

The node_get_names() function no longer exists. Please use node_get_types('name', $node) instead. Similarly, the node_get_base() function no longer exists. Please use node_get_types('module', $node) instead.

New hook_node_type()

As a result of the changes to the node type system (above), node types can now be modified by site administrators. The new hook_node_type() allows modules to respond to changes to a node type.

The hook has two parameters: $op, which defines the operation being performed on the node type (either insert, update, or delete); and $info, which is an object containing all of the attributes of the node type:

<?php
function node_node_type($op, $info) {
  if (!empty(
$info->old_type) && $info->old_type != $info->type) {
   
$update_count = node_type_update_nodes($info->old_type,
     
$info->type);

    if (
$update_count) {
     
$substr_pre = 'Changed the content type of ';
     
$substr_post = strtr(' from %old-type to %type.', array(
       
'%old-type' => theme('placeholder', $info->old_type),
       
'%type' => theme('placeholder', $info->type)));
     
drupal_set_message(format_plural($update_count, $substr_pre
       
.'@count post'. $substr_post, $substr_pre .'@count posts'.
       
$substr_post));
    }
  }
}
?>

As the node.module implementation of the hook (above) demonstrates, one of the most common uses of the hook is to update all database references when the machine-readable name of a node type changes. This is very important, because the machine-readable name is no longer a constant value: it can be edited by a site administrator (for many node types), just like most other fields of a node type can be edited. Any modules that use the machine-readable node type name as a reference field in their database tables need to implement hook_node_type(), in order to keep these references valid.

Changes to node type settings form

Modules can altered the 'default settings' form for any node type, adding an additional workflow option, for example. In 4.7.x, the form_id of that form changed for each node type. Now, the form_id is always 'node_type_settings'. In 4.7.x, fields added to this form were always saved to the settings table using the field name as a key. In 5.x, the node type is appended to the field name.

In Drupal 4.7.x:

<?php
function mymodule_form_alter($form_id, &$form) {
  if (
strpos($form_id, '_node_settings') !== FALSE) {
   
$node_type = str_replace('_node_settings', '', $form_id);
   
$form['workflow']['my_module_settings_'. $node_type] = array(
     
'#type' => 'checkbox',
     
'#title' => t('My module settings go here'),
     
'#default_value' => variable_get('my_module_settings_'. $node_type, 0),
    );
  }
}
?>

Now, in 5.x, that should look like:

<?php
function mymodule_form_alter($form_id, &$form) {
  if (
$form_id == 'node_type_form') {
   
$node_type = $form['old_type']['#value'];
   
$form['workflow']['my_module_settings'] = array(
     
'#type' => 'checkbox',
     
'#title' => t('My module settings go here'),
     
'#default_value' => variable_get('my_module_settings_'. $node_type, 0),
    );
  }
}
?>

New db_table_exists()

This function (which works under MySQL and PostgreSQL) will indicate if the given table exists in the database.

New hook_node_operations, hook_user_operations()

These hooks function identically, one for the mass operations found at admin/content/node, and the the other at admin/user/user. They allow for modules to inject custom operations into the dropdown menus, which are then executed via a declared callback function when the form is submitted (the user is subsequently directed back to the same url by default). Here's a short example of hook_node_operations in action:

  • The hook:
    <?php
     
    function node_node_operations() {
       
    $operations = array(
         
    'approve' => array(
           
    'label' => t('Approve the selected posts'),
           
    'callback' => 'node_operations_approve',
          ),
         
    'promote' => array(
           
    'label' => t('Promote the selected posts'),
           
    'callback' => 'node_operations_promote',
          ),
        );
        return
    $operations;
      }
    ?>
  • One of the callbacks:
    <?php
     
    function node_operations_approve($nodes) {
       
    db_query('UPDATE {node} SET status = 1 WHERE nid IN(%s)',
         
    implode(',', $nodes));
      }
    ?>

Altered the behaviour of placeholders in t() calls

The t() function was changed to be able to transparently escape and format its arguments to be safe for output. The idea is that where you previously called check_plain() or theme('placeholder') on an argument before passing it to t(), this is now done for you, based on the first character of the substitution key:

<?php
   
// Before:
   
print t('%type: %title was posted',  array('%type' => check_plain($node->type),  '%title' => theme('placeholder', $node->title)));
    print
t('Submitted by %name', array('%name' => theme('username', $node)));

   
// After:
   
print t('@type: %title was posted',  array('@type' => $node->type '%title' => $node->title));
    print
t('Submitted by !name', array('!name' => theme('username', $node)));
?>

Note that in the first print statement, we removed the calls to check_plain() and theme('placeholder') as they are now applied automatically to arguments whose key starts with respectively '@' and '%'. In the second case, there was no escaping before, so we use the '!' prefix to insert the argument as is.

Using the above code as a template for upgrading your module only works if your module already used check_plain() and theme('placeholder') where appropriate. Remember that it is important to use them in output to avoid XSS security problems.

If you are not sure when to use them, it is better to try '%' or '@' first, and only switch to '!' when the first two are causing problems. It is advised to read the book page about dealing with text in Drupal in a secure way as it contains coding guidelines and examples of good and bad code.

And obviously, you could just be lazy and replace all '%placeholders' with '!placeholders' to get the same behaviour as before. But that doesn't help you make your code more readable.

drupal_get_form() now takes a $form_id, not a $form

Forms must be created in dedicated builder functions

In Drupal 4.7.x, forms were built using a 'push' model: a form array was constructed and passed to the drupal_get_form() function. Now, in 5.x, each form is built in a dedicated function and identified by its form_id. drupal_get_form() only needs to know that form_id to properly load, process, and render the form.

By default, the Forms API will check for a function that shares the same name as the form's ID.

In Drupal 4.7.x:

<?php
function mymodule_edit_record($record) {
 
$output = t('This is my edit page!');
 
$form['my_field'] = array(
   
'#type' => 'textfield',
   
'#title' => 'Record name',
   
'#default_value' => $record->name,
  );
 
$output .= drupal_get_form('record_edit_form', $form);
  return
$output;
}
?>

In 5.x that code should look like:

<?php
function mymodule_edit_record($record) {
 
$output = t('This is my edit page!');
 
$output .= drupal_get_form('mymodule_edit_record_form', $record);
  return
$output;
}

function
mymodule_edit_record_form($record) {
 
$form['my_field'] = array(
   
'#type' => 'textfield',
   
'#title' => 'Record name',
   
'#default_value' => $record->name,
  );
  return
$form;
}
?>

hook_forms() optionally maps form_ids to builder functions

Modules can now implement hook_forms() to handle complex mapping of form IDs to builder functions. It's how node.module maps the id for each node type editing form to the central form-building function for the node edit screen. Here's an example:

<?php
function mymodule_forms() {
 
$forms['mymodule_first_form'] = array(
   
'callback' => 'mymodule_form_builder',
   
'callback arguments' => array('some parameter'),
  );
 
$forms['mymodule_second_form'] = array(
   
'callback' => 'mymodule_form_builder',
   
'callback arguments' => array('a different parameter'),
  );
  return
$forms;
}

function
mymodule_first_page() {
  return
drupal_get_form('mymodule_first_form');
}

function
mymodule_second_page() {
  return
drupal_get_form('mymodule_second_form');
}

function
mymodule_form_builder($param) {
 
$form = array()
 
// build the form here

 
if ($param == 'some parameter') {
   
// Add another field, change a default value...
 
}

 
// This is used the way $callback was in 4.7.x Forms API: it is used as the prefix for
  // _submit() and _validate() functions to process the form.
 
$form['#base'] = 'mymodule_form';

  return
$form;
}
?>

In the above code, the mymodule_first_page() and mymodule_second_page() functions display slight variations on the same form. Because no functions named 'mymodule_first_form' or 'mymodule_second_form' exist, Forms API looks to hook_forms() to find the builder function. Each entry in the array returned by hook_forms() can also define callback arguments that will be passed to the builder function.

In the mymodule_form_builder() function, the use of $form['#base'] is also important to note. It's a lot like the little-used but helpful $callback parameter that was used in the 4.7.x version of drupal_get_form(). If $form['#base'] is set, its value will be used to look up the proper submit, validate, and theme functions for the form rather than the form's ID.

New drupal_execute() function allows form data to be submitted programmatically

Forms can now be built and submitted programmatically without any user input using the drupal_execute() function. Pass in the id of the form, the values to submit to the form, and any parameters needed by the form's builder function. For example:

<?php
// register a new user
$values = array();
$values['name'] = 'robo-user';
$values['mail'] = 'robouser@example.com';
$values['pass'] = 'password';
drupal_execute('user_register', $values);

// Create a new node
$node = array('type' => 'story');
$values = array();
$values['title'] = 'My node';
$values['body'] = 'This is the body text!';
$values['name'] = 'robo-user';
drupal_execute('story_node_form', $values, $node);
?>

Calling form_get_errors() after execution will return an array of any validation errors encountered.

module_exist() is now module_exists()

For consistency with PHP function_exists, file_exists, etc. That's about all there is to say about that. :)

format_plural() @count change

This function used to substitute a number where you placed %count. Instead, use @count now. This is a result of the t() changes described above.

Vastly extended drupal_add_js()

The function to add JavaScript to a Drupal page has been reworked:

Definition

drupal_add_js($data = NULL, $type = 'module', $scope = 'header', $defer = FALSE, $cache = TRUE)
includes/common.inc, line 1329

Description

Add a JavaScript file, setting or inline code to the page.

The behavior of this function depends on the parameters it is called with. Generally, it handles the addition of JavaScript to the page, either as reference to an existing file or as inline code. The following actions can be performed using this function:

  • Add a file ('core', 'module' and 'theme'): Adds a reference to a JavaScript file to the page. JavaScript files are placed in a certain order, from 'core' first, to 'module' and finally 'theme' so that files, that are added later, can override previously added files with ease.
  • Add inline JavaScript code ('inline'): Executes a piece of JavaScript code on the current page by placing the code directly in the page. This can, for example, be useful to tell the user that a new message arrived, by opening a pop up, alert box etc.
  • Add settings ('setting'): Adds a setting to Drupal's global storage of JavaScript settings. Per-page settings are required by some modules to function properly. The settings will be accessible at Drupal.settings.

Parameters

$data (optional) If given, the value depends on the $type parameter:

  • 'core', 'module' or 'theme': Path to the file relative to base_path().
  • 'inline': The JavaScript code that should be placed in the given scope.
  • 'setting': An array with configuration options as associative array. The array is directly placed in Drupal.settings. You might want to wrap your actual configuration settings in another variable to prevent the pollution of the Drupal.settings namespace.

$type (optional) The type of JavaScript that should be added to the page. Allowed values are 'core', 'module', 'theme', 'inline' and 'setting'. You can, however, specify any value. It is treated as a reference to a JavaScript file. Defaults to 'module'.

$scope (optional) The location in which you want to place the script. Possible values are 'header' and 'footer' by default. If your theme implements different locations, however, you can also use these.

$defer (optional) If set to TRUE, the defer attribute is set on the <script> tag. Defaults to FALSE. This parameter is not used with $type == 'setting'.

$cache (optional) If set to FALSE, the JavaScript file is loaded anew on every page call, that means, it is not cached. Defaults to TRUE. Used only when $type references a JavaScript file.

Return value

If the first parameter is NULL, the JavaScript array that has been built so far for $scope is returned.

Removed drupal_call_js()

As it is now very easy to attach JavaScript to a page, this function is not needed anymore. Use this syntax instead:

<?php
drupal_add_js
('myCustomFunction(your, parameters, here)', 'inline');
?>

If you have more complex parameters that should be passed to the function, consider using drupal_to_js() to convert them to a JSON object before.

Note: It is recommended to not call functions in the page header directly. Instead use unobtrusive JavaScript that automatically runs when the page is loaded.

New drupal_add_feed() and drupal_get_feeds replaces theme_add_link()

There is a new function drupal_add_feed() which you should use to add feeds to a page. This function takes the URL and the title of the feed and automatically adds the link to the feed in the HEAD for autodiscovery, along with adding an appropriate feed icon to the page itself.

To get an array of these feeds, call drupal_get_feeds().

Appropriately, in your theme there is a new $feed_icons so you can move these about

theme('page') may omit standard blocks

Modules that want more control over their presentation can call theme('page', $content, FALSE) to avoid outputting standard blocks on the page. That third parameter is new.

New #disabled Form API property

Before, to mark an input field such as a textfield or select box "disabled" (greyed-out), you had to do the following:

<?php
$form
['textfield'] = array(
  ...
 
'#attributes' => array('disabled' => 'disabled'),
);
?>

Now, you can simply do:

<?php
$form
['textfield'] = array(
  ...
 
'#disabled' => TRUE,
);
?>

Changed cache API

The cache functions have changed. cache_set now requires a table name as the second parameter if you don't want to use the standard cache table. The same is true for cache_get. cache_clear_all called with no arguments will only invalidate the page cache in the new table cache_page. To clear expirable items from a specific table use cache_clear_all(NULL, 'tablename').

There are now four cache tables in core: cache, cache_page, cache_filter, cache_menu.

Uninstall hook

In module.install, you can now place an "uninstall" hook, which drops tables and deletes any variables that the module adds. For example:

<?php
/**
* Implementation of hook_uninstall().
*/
function profile_uninstall() {
 
db_query('DROP TABLE {profile_fields}');
 
db_query('DROP TABLE {profile_values}');
 
variable_del('profile_block_author_fields');
}
?>

Added jQuery to Drupal

The addition of jQuery has significantly altered drupal.js:

  1. All drupal.js functions are namespaced with Drupal. For example, if you called absolutePosition(...) before, you should now call Drupal.absolutePosition(...).
  2. The JsEnabled() killswitch has been changed from a function into a boolean. In practice, you just remove the () when doing the check: if (Drupal.JsEnabled) ....
  3. addLoadEvent() was removed in favor of jQuery's own $(document).ready() function.

Drupal includes the complete jQuery 1.0.1 library (without plug-ins). Check the jQuery website for its documentation.

$_POST[op] deprecated in favor of $form_values[op]

For better form api manageability, your validate and subbmit handlers should no longer inspect the value of $_POST['op'] to determine what button was pressed (for example). Instead, the $form_values array which is passed as the second parameter includes an op element.

Change menu item and node links to use Sentence capitalization instead of lowercase

Drupal 5 will require contrib modules to change menu item and links to use Sentence capitalization instead of lowercase. Here are some common places to look for capitalization changes to your modules:

  • Your module's hook_node_info() should now include 'name' to be t('My module') instead of t('my module')
  • Menu items defined in hook_menu()
  • Links defined in hook_link()
  • Module blocks defined in hook_block()

Major changes to node_access system

The node access system now requires that only node.module touch the node_access table directly. In the old method, a module wrote records to the node_access table at its discretion, meaning multiple modules couldn't successfully use the table. Instead, you must now use hook_node_access_records to tell Drupal what node_access records you want to write for a given node.

In addition, when you want to force the node module to rewrite the access records for a given node, use node_access_acquire_grants which will go through the process of collecing node access records to write. Also see the node access example module for a more complete description of how to deal with the node access system.

Changes to confirm_form()

The core confirm_form() function has been modified to use the new model of form builder functions. Not only have the arguments for confirm_form() changed, but how it must be invoked has changed as well. For example:

4.7.x:

<?php
function foo_delete_confirm() {
 
$node = node_load(arg(1));
 
$form['nid'] = array('#type' => 'value', '#value' => $node->nid);
 
$output = confirm_form('foo_delete_confirm', $form,
     
t('Are you sure you want to delete %title?', array('%title' => theme('placeholder', $node->title))),
     
'node/'. $node->nid, t('This action cannot be undone.'),
     
t('Delete'), t('Cancel')  );
  }
  return
$output;
}
?>

Now, in 5.x, this must be something like:

<?php
function foo_delete_confirm_page() {
  return
drupal_get_form('foo_delete_confirm', arg(1));
}

function
foo_delete_confirm($nid) {
 
$node = node_load($nid);
 
$form['nid'] = array('#type' => 'value', '#value' => $node->nid);
  return
confirm_form($form,
     
t('Are you sure you want to delete %title?', array('%title' => $node->title)),
     
'node/'. $node->nid, t('This action cannot be undone.'),
     
t('Delete'), t('Cancel')  );
  }
}
?>

(Note that this example also includes the change to the behavior of placeholders in t()). Also, keep in mind that the submit handler for your confirm form must be named to match the name of the form builder function. So, continuing the above example, the submit handler (the code to execute if the user confirms the operation), would be:

<?php
function foo_delete_confirm_submit() {
 
// Your logic here
}
?>

For more details, please see: http://api.drupal.org/api/head/function/confirm_form

Changes to how #prefix and #suffix are rendered in form arrays

In 4.7.x, if you put something in $form['#prefix'] it was included inside the <form> HTML that was rendered for your form. So, for example, if you used this in a node-type form:

$form['#prefix'] = '<div class="your-custom-node-type">

the resulting HTML gave you the <div class="node-form"> before your custom div from the #prefix. Now, the #prefix is always rendered outside the <form>. This could impact any custom CSS your module is relying on. For example, if you used to do this:

4.7.x:

.node-form .project ... {
  /* something interesting for project nodes */
}

you now need to change the order of the classes in your .css file, like this:

5.x:

.project .node-form ... {
  /* something interesting for project nodes */
}

Changes to how #options are represented in some form arrays

Due to a critical bug, select elements in form arrays that were trying to represent choices where there were multiple options with the same label (for example, inside multiple hierarchy taxonomies) would fail to work correctly. As a result, there is now a new Object-based way you can optionally represent the #options array you define for select form elements. This allows you to have unique select indexes (the keys in the #options array) and still have multiple options with the same label.

Here's how it works. Instead of just doing this:

<?php
foreach (array('foo', 'bar', 'foo') as $choice) {
 
$options[$choice] = $choice // kills the original 'foo' when handling the last one...
}
$form['baz']['#options'] = $options;
?>

You can now do this:

<?php
$options
= array();
foreach (array(
'foo', 'bar', 'foo') as $choice) {
 
$obj = new stdClass();
 
$obj->option = array($choice => $choice);
 
$options[] = $obj;
}
$form['baz']['#options'] = $options;
?>

That's all well and good if you're writing your own array for #options. However, if you're on the other end of this change, and trying to manipulate the #options array someone else has already created (for example, from inside hook_form_alter()), this change can create quite a headache.

For example, if you're trying to see if the key '17' exists in the array of options, you can no longer just use something like:

if (isset($form['baz']['#options'][17])) {
  ...
}

Luckily, there's a new helper method to ease your pain: form_get_option_key(). So, now you can do something like this:

<?php
$key
= form_get_option_key($form['baz'], 17);
if (
$key !== FALSE) {
 
// Do something interesting
}
?>

Unfortunately, this helper *only* works if the incoming #options array is using the crazy new object syntax. The $form['taxonomy'] array always uses the objects, so if your module is trying to manipulate that, you should use the new method. Otherwise, you can fall back to the old approach and just look in $form['baz']['#options'] directly.

$node->moderate no longer used by core

Unless your module specifically deals with moderation of nodes, all code referencing $node->moderate or the moderate column in node table should be removed. This column in the database is still present for modules who want to implement node moderation logic. See modr8.module for an example of a module which does this.

Mixing old and new links styles

Because this is a major change and many contributed modules haven't made this update yet, it is getting to be difficult to function in cvs. I made a patch to theme.inc that will accept links in both the old and new format so I can keep operating without errors. I haven't submitted this patch as an 'official' patch because I don't know if this is desirable for core, but it may be useful to others who are trying to use a variety of contributed modules in cvs without the need to patch them all.

I see that I can't attach a file to a book page, so I am just pasting my changed code for the theme_links function.

<?php
/**
* Return a themed set of links.
*
* @param $links
*   A keyed array of links to be themed.
* @param $delimiter
*   A string used to separate the links.
* @return
*   A string containing the themed links.
*/
function theme_links($links, $delimiter = ' | ') {
 
$output = array();
  if (
is_array($links)) {
    foreach (
$links as $key => $link) {
     
// trap old-format links and return them the old way
     
if (!is_array($link)) {
       
$output[] = $link;
      } else {
       
//Automatically add a class to each link and convert all _ to - for XHTML compliance
       
if (isset($link['#attributes']) && isset($link['#attributes']['class'])) {
         
$link['#attributes']['class'] .= ' '. str_replace('_', '-', $key);
        }
        else {
         
$link['#attributes']['class'] = str_replace('_', '-', $key);
        }

        if (
$link['#href']) {
         
$output[] = l($link['#title'], $link['#href'], $link['#attributes'], $link['#query'], $link['#fragment']);
        }
        else if (
$link['#title']) {
         
//Some links are actually not links, but we wrap these in <span> for adding title and class attributes
         
$output[] = '<span'. drupal_attributes($link['#attributes']) .'>'. $link['#title'] .'</span>';
        }
      }
    }
  }
  return
implode($delimiter, $output);
}
?>

Converting 4.7.x modules to 4.7.5

In addition to the steps mentioned in the Converting 4.7.x modules to 4.7.4 page, there has been another subtle change to the 4.7.x API in the 4.7.5 release that might affect modules (or themes). The change came as result of fixing a critical bug in the way that the Forms API renders the #prefix and #suffix values in form arrays. Unfortunately, this change might require modifications to the code of a certain modules, something we try to avoid in the middle of a stable series as much as possible.

In 4.7.x, if you put something in $form['#prefix'] it was included inside the <form> HTML that was rendered for your form. So, for example, if you used this in a node-type form:

$form['#prefix'] = '<div class="your-custom-node-type">

the resulting HTML gave you the <div class="node-form"> before your custom div from the #prefix. Now, the #prefix is always rendered outside the <form>. This is essential behavior for correctness, but some modules might be relying on the old behavior. For example, this change can impact any custom CSS your module is relying on. For example, if you used to do this:

4.7.x:

.node-form .project ... {
  /* something interesting for project nodes */
}

you now need to change the order of the classes in your .css file, like this:

4.7.5:

.project .node-form ... {
  /* something interesting for project nodes */
}

Since the <div class="node-form"> is always included inside the <form> tag, your prefix now comes first.

You can also create a CSS rule to apply to both:

.node-form .project ... ,
.project .node-form ... {
  /* something interesting for project nodes */
}

Converting 4.7.x modules to 4.7.4

Drupal 4.7.4 saw the addition of a new default form field; form_token, to protect against cross site request forgeries. The token ensures that forms submitted to the site are actually requested first.

There are a few potential issues surrounding the form_token.

Relying on specific, known form fields

Modules that rely on only known form fields to be present, have to account for the new form_token field when saving data or providing specific theme functions.

The following example theme function will result in a form that will always fail validation as the new form_token field is not included in the form that is presented to the user.

<?php
// This form will fail validation

function your_form() {
 
$form['field'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Example'),
   
'#default_value' => 'text',
  );
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Submit'),
  );
  return
drupal_get_form('your_form_id', $form);
}

function
theme_your_form_id($form) {
 
// Output fields in a specific order / with markup:
 
$output = form_render($form['field'])
 
$output .= form_render($form['form_id']);
 
$output .= form_render($form['submit']);
  return
$output;
}
?>

The solution is easy; be sure to output the form_token field by adapting your custom theme function.

<?php
function theme_your_form_id($form) {
 
// Output fields in a specific order / with markup:
 
$output = form_render($form['field'])
 
$output .= form_render($form['form_id']);
 
$output .= form_render($form['submit']);

 
// form_token necessary to pass validation
 
$output .= form_render($form['form_token']);
  return
$output;
}

// Or better

function theme_your_form_id($form) {
 
// Output fields in a specific order / with markup:
 
$output = form_render($form['field'])
 
$output .= form_render($form['submit']);
 
 
// Render the remainder of the form, including hidden fields.
 
$output .= form_render($form);
  return
$output;
}
?>

Accessing $_POST and taking actions at the form definition stage

Modules that do not use the forms api's definition => validation => submit pipeline will not be protected against cross site request forgeries.

As rewriting the form code may be daunting, the following code demonstrates an easier alternative; call drupal_valid_token() yourself:

<?php
// Not protected against cross site request forgeries!

function user_admin_access_add($mask = NULL, $type = NULL) {
  if (
$edit = $_POST['edit']) {
    if (!
$edit['mask']) {
     
form_set_error('mask', t('You must enter a mask.'));
    }
    else {
     
$aid = db_next_id('{access}_aid');
     
db_query("DO SOMETHING here");
     
drupal_set_message(t('The access rule has been added.'));
     
drupal_goto('admin/access/rules');
    }
  }
  else {
   
$edit['mask'] = $mask;
   
$edit['type'] = $type;
  }

 
$form = _user_admin_access_form($edit);
 
$form['submit'] = array('#type' => 'submit', '#value' => t('Add rule'));

  return
drupal_get_form('access_rule', $form);
}
?>

The form_token needs to be validated before taking action. To do so call drupal_valid_token with the arguments; the token ($_POST['edit']['form_token']), $form_id, TRUE.

The function drupal_valid_token returns true when the token is valid. When the third argument is true (as in the example) it returns true for anonymous users. While perhaps not particularly relevant for this example, that is necessary because anonymous users may receive a cached token that will always fail.

<?php
function user_admin_access_add($mask = NULL, $type = NULL) {
  if (
$edit = $_POST['edit']) {
    if (!
$edit['mask']) {
     
form_set_error('mask', t('You must enter a mask.'));
    }
    else if (
drupal_valid_token($_POST['edit']['form_token'], 'access_rule', TRUE)) {
     
$aid = db_next_id('{access}_aid');
     
db_query("DO SOMETHING here");     
     
drupal_set_message(t('The access rule has been added.'));
     
drupal_goto('admin/access/rules');
    }
    else {
     
form_set_error('form_token',
     
t('Validation error, please try again.  If this error persists, please contact the site administrator.'));
    }
  }
  else {
   
$edit['mask'] = $mask;
   
$edit['type'] = $type;
  }

 
$form = _user_admin_access_form($edit);
 
$form['submit'] = array('#type' => 'submit', '#value' => t('Add rule'));

  return
drupal_get_form('access_rule', $form);
}
?>

If you want your module to remain compatible with Drupal 4.7.0 - 4.7.3 check whether drupal_valid_token exists before calling the function.

For example:

<?php
if (function_exists('drupal_valid_token')) {
 
// Check the token here
}
?>

What if I don't want a token?

If you do not want a form token, set #token to false.

<?php
function your_form() {
 
$form['#token'] = FALSE;
 
//
  // other fields
  //
 
return drupal_get_form('your_form_id', $form);
}
?>

Converting 4.6.x modules to 4.7.x

Overview of Drupal API changes in 4.7

  1. new handling of return values from callbacks: must know.
  2. new node definition system: must know.
  3. node_load(): must know.
  4. node_save(): must know.
  5. node_list(): moderately used.
  6. node titles now handled by node modules: must know
  7. node_get_module_name() → node_get_base(): minor required change.
  8. format_name() → theme('username'): minor required change.
  9. theme_table(): minor required change.
  10. check_output() → check_markup(): infrequently used, security requirement.
  11. XML-RPC: infrequently used, backported to 4.6.3 for security reasons.
  12. taxonomy_save_vocabulary(), taxonomy_save_term(): advanced use, taxonomy specific.
  13. message_access() removed.
  14. Unicode string API: must know.
  15. conf_url_rewrite(): rarely used.
  16. revisions overhaul: important for node modules.
  17. Upgrading to forms API
  18. node_delete(): moderately used
  19. New order of node hooks
  20. hook_nodeapi('settings', ...) replaced by form api.
  21. hook_nodeapi('form', ...) replaced by form api.
  22. file_directory variables: moderately used, replaced by functions
  23. array2object moderately used: replace by native PHP conversion
  24. user_load moderately used: different return value when user not found.
  25. UTF-8 SQL conversion: required.
  26. We no longer use the <base> element.
  27. hook_onload replaced by addLoadEvent()
  28. database creation instructions, updates, and other module setup code go in .install files.
  29. hook_search_item was replaced by hook_search_page.
  30. node_validate_title was removed.
  31. tablesort_pager was removed.

New handling of return values from callbacks

Most (menu) callbacks used print theme('page', $output); to return HTML code. To enable the reuse of any callback in blocks, sub-pages, etc callbacks are no longer expected to call theme('page', ...):

  • If you return something (return $output;), then print('page', ...); is called.
  • If you do not return anything, it will assume you have handled your own output.

In general, there is no longer need to use theme('page', ...); in your module.

  • Impacts most modules.
  • Importance: recommended.
  • Compatibility: old code will still work, but it is deprecated -- you can expect it to break any time.
  • Affected code: any use of theme('page', ...).

Example:

<?php
// Drupal 4.6
function mymodule_admin_page(...) {
 
$output .= "some content";
  print
theme('page', $output);
}

// Drupal 4.7
function mymodule_admin_page(...) {
 
$output .= "some content";
  return
$output;
}
?>

node definition changes

In 4.6, you had hook_node_name and hook_node_types. Now you must implement hook_node_info if you want to create a module which defines node type(s). The implementation of the hook_node_info() needs to:

<?php

    
return array($type1 => array('name' => $name1, 'base' => $base1),
                 
$type2 => array('name' => $name2, 'base' => $base2));

?>

where $type is the node type, $name is the human readable name of the type and $base is used instead of
<hook>
for
<hook>
_load()
,
<hook>
_view()
, etc.

For example, the story module's node_info hook looks like this:

<?php

function story_node_info() {
  return array(
'story' => array('name' => t('story'), 'base' => 'story'));
}
?>

The page module's node_info hook looks like:
<?php
function page_node_info() {
  return array(
'page' => array('name' => t('page'), 'base' => 'page'));
}
?>

However, more complex node modules like the project module and the flexinode module can use the 'base' parameter to specify a different base.

The project module implements two node types, projects and issues, so it can do:

<?php
function project_node_info() {
  return array(
    array(
'project_project' => array('name' => t('project'), 'base' => 'project'),
    array(
'project_issue' => array('name' => t('issue'), 'base' => 'project_issue'));
}
?>

In the flexinode module's case there can only be one base.

Now you have node_get_name($type) to get the name of a node type and node_get_base($type) to get the base of a node type.

  • Impacts most modules.
  • Importance: absolute must.
  • Compatibility: old code will not work
  • Affected code: any use of node_name hook, node_types.

node_load() changes

You should write $node = node_load($nid); instead of $node = node_load(array('nid' => $nid));. The array syntax is for cases when you are not using $nid or need to add more fields.

  • Impacts many modules.
  • Importance: suggested.
  • Compatibility: old code will still work, but is not as efficient: by passing an array, the node_load() cache is not used resulting in lower performance.
  • Affected code: any use of node_load(array('nid' => $nid)); can be replaced by node_load($nid).
  • Docs: node_load().

The only use of node_load() that should be changed are the ones that only pass the $nid:

<?php
// Drupal 4.6
$node = node_load(array('nid' => $nid));

// Drupal 4.7
$node = node_load($nid);
?>

node_save() changes

node_save() now receives the $node parameter by reference, and modifies the object as needed. It has no return value anymore.

node_list() changes

node_list() became node_get_types now returns an associative array about node types. The types are now returned as the keys of this arrays (formerly they were returned as the values). The values are now the relevant human readable names.

  • Impacts some modules.
  • Importance: required.
  • Compatibility: old code will not work as the types are now the keys of the returned array and not the values (and the function name is changed, too).
  • Affected code: any use of node_list().
  • Docs: node_get_type().

The most typical use is:

<?php
// Drupal 4.6
foreach (node_list() as $type) {

// Drupal 4.7
foreach (node_get_types() as $type => $name) {
?>

Node titles now handled by node modules

Formerly hard-coded into the node.module, titles are now handled by the individual node modules.

In many cases, the only change needed is to add a title field at the beginning of the hook_form() hook, like this:

<?php
function modulename_form(&$node) {
 
$form['title'] = array('#type' => 'textfield',
   
'#title' => t('Subject'),
   
'#default_value' => $node->title,
   
'#size' => 60,
   
'#maxlength' => 128,
   
'#required' => TRUE);
...
?>
  • Impacts: all node modules.
  • Importance: required.
  • Compatibility: old code will not work.
  • Affected code: hook_form() need to be edited to add the title field.

module_get_node_name deprecated

Fetching a node type's module name is now handled by the node_get_base function.

Example:

<?php
// Drupal 4.6
$module = node_get_module_name($type);

// Drupal 4.7
$module = node_get_base($type);
?>

format_name() renamed

For consistency and enabling theming, format_name() was renamed to theme_username() and should be invoked using the theme('username', ...) API. Every use of format_name() must be replaced.

  • Impacts: few modules.
  • Importance: required.
  • Compatibility: old code will not work.
  • Affected code: any use of format_name(...) should be replaced by theme('username', ...).
  • Docs: format_name() (Drupal 4.6) and theme_username() (Drupal 4.7).

Example:

<?php
// Drupal 4.6
$output = t('by %username', array('%username' => format_name($node)));

// Drupal 4.7
$output = t('by %username', array('%username' => theme('username', $node)));
?>

theme_table() change

theme('table', ...); sometimes was called with arguments set to NULL or an empty string ('') to indicate that there were either no rows or no header. This is no longer allowed: the $rows and $header parameters now have to be an array. If you have no rows or no header to pass to theme('table', ...); you have to pass an empty array.

  • Impacts some modules.
  • Importance: required.
  • Compatibility: old code will not work if you pass NULL or an empty string.
  • Affected code: any use of theme('table', ...) where you pass no rows or no header.
  • Docs: theme_table().

Example:

<?php
// Drupal 4.6
theme('table', '', $rows);

// Drupal 4.7
theme('table', array(), $rows);
?>

check_output() change

Due to a security vulnerability discovered in the filter system, we have tightened security around the check_output() function. The format passed to check_output() is now checked for access by default. If you don't want this check, pass FALSE for the third parameter, $check. check_output() was renamed to check_markup() to enforce this change. The new syntax is:

<?php
function check_markup($text, $format = FILTER_FORMAT_DEFAULT, $check = TRUE) {
?>

Note that if you disable the check by passing FALSE, you need to make sure the $format value has been checked by filter_access() before. filter_access() checks the permissions of the current user, so it should be checked on submission, not on output.

In general you will want to use check_markup($text, $format, TRUE) prior to validating or saving the $text and use check_markup($text, $format, FALSE) when viewing the $text.

Please review any use of check_markup() carefully!

  • Impacts few modules.
  • Importance: required.
  • Compatibility: old code will not work.
  • Affected code: any use of check_output() should be checked (and double checked!) before replacing it with check_markup().
  • Docs: check_output() (Drupal 4.6), check_markup() (Drupal 4.7) and filter_access().

XML-RPC changes

As this happened between 4.6.2 and 4.6.3, there is a separate guide for this.

  • Impacts few modules.

Taxonomy API change

In order to provide more meaningful messages to the user, now you can provide your own messages when using the taxonomy APIs to create or modify terms and vocabularies. The taxonomy_save_vocabulary() and taxonomy_save_term() functions now return a status message which can be either SAVED_NEW, SAVED_UPDATED or SAVED_DELETED. The function will return NULL when there was an error. Also note that you must call these with an $edit array parameter which will be modified, and will include the 'vid' key.

  • Impacts few modules.
  • Importance: required.
  • Compatibility: old code will not work as expected: no feedback is given to the user anymore. If you used the return value of the functions, you should be aware that this return value has changed.
  • Affected code: any use of taxonomy_save_vocabulary() or taxonomy_save_term().
  • Docs: taxonomy_save_vocabulary() and taxonomy_save_term().

For example, this snippet shows you a typical handling of the returned status values:

<?php
// Drupal 4.6
taxonomy_save_vocabulary($edit);
?>

<?php
// Drupal 4.7
switch (taxonomy_save_vocabulary($edit)) {
  case
SAVED_NEW:
   
drupal_set_message(t('Created new vocabulary %name.', array('%name' => theme('placeholder', $edit['name']))));
    break;
  case
SAVED_UPDATED:
   
drupal_set_message(t('Updated vocabulary %name.', array('%name' => theme('placeholder', $edit['name']))));
    break;
  case
SAVED_DELETED:
   
drupal_set_message(t('Deleted vocabulary %name.', array('%name' => theme('placeholder', $deleted_name))));
    break;
}
?>

message_access() removed

The message_access() function was removed. Replace all occurrences with a nice case error message that is specific to the error that has been caught.

Unicode string API

Drupal now provides some wrappers for doing string handling in the most language-independent fashion. The following functions are available and behave exactly as their PHP counterparts, except that they count and work with Unicode characters in UTF-8 encoding and not literal bytes: drupal_strlen(), drupal_strtolower(), drupal_strtoupper(), drupal_substr(), drupal_ucfirst().

If you use any of the original functions in your own module, you should almost certainly replace them with their Drupal counterparts. Only use the plain PHP string API when you want to work with the literal bytes.

Note that strpos() is not mirrored, because it can still be safely used. If you want to chop off a string at a location found by strpos(), you should use substr() instead of drupal_substr().

For more information, see the API reference.

conf_url_rewrite() became custom_url_rewrite()

Watch out for changes in the URL rewriting process if you have a module or settings file, which implements mass URL aliasing. The conf_url_rewrite() you are expected to implement if you are about to provide mass URL aliasing in Drupal was renamed to custom_url_rewrite(). Now this function is always called on all URL aliasing request, so if you would not like to mangle already aliased URLs, you need to take action yourself.

<?php
// Drupal 4.6
function conf_url_rewrite($path, $mode = 'incoming') {
 
// $mode is either 'incoming' or 'outgoing' and function
  // is only called if there is no system alias found
}
?>

<?php
// Drupal 4.7
function custom_url_rewrite($type, $path, $original) {
 
// $type is either 'alias' or 'source', depending on the
  // desired resulting path type of the operation (to be
  // in line with drupal_lookup_path() operation),
  // $path is possibly already processed by Drupal,
  // $original is the originally passed path

 
if ($path == $original) {
   
// path is not yet aliased (this is the only case
    // conf_url_alias() was invoked in earlier versions)
 
}
}
?>

node_delete(): moderately used

The confirmation screen has been abstracted from the function. node_delete now strictly handles deletion of a node. It no longer takes an array as an argument--instead pass the nid of the node you wish to delete.

Instead of:

node_delete(array('nid' => $node->nid, 'confirm' => 1));

Now use:

node_delete($node->nid);

If a confirmation screen is desired, redirect to node/[nid]/delete, where [nid] is the nid of the node to delete.

New order of node hooks

There is a new order in the way node hooks are called in 4.7 vs. 4.6:

Drupal 4.6
1. validate
2. form pre
3. form post
4. if the form was submitted then validate (again)
5. insert/update

Drupal 4.7
1. prepare
2. form (return an array)
3. validate (you can only validate here, no changes possible)
4. submit (prepare the node for save)
5. insert/update

In short, Drupal 4.7 separates out the individual parts of the validation, rather than combining it together into one function.

hook_nodeapi('settings', ...) replaced by form api

Adding elements to the node type settings page is now done exclusively with the form api. Below is an example of the new way to add elements to this form from the node module.

<?php
function node_form_alter($form_id, &$form) {
  if (isset(
$form['type']) && $form['type']['#value'] .'_node_settings' == $form_id) {
   
$form['workflow']['node_options_'. $form['type']['#value']] = array(
     
'#type' => 'checkboxes', '#title' => t('Default options'), '#default_value' => variable_get('node_options_'. $form['type']['#value'], array('status', 'promote')),
     
'#options' => array('status' => t('Published'), 'moderate' => t('In moderation queue'), 'promote' => t('Promoted to front page'), 'sticky' => t('Sticky at top of lists'), 'revision' => t('Create new revision')),
     
'#description' => t('Users with the <em>administer nodes</em> permission will be able to override these options.'),
    );
  }
}
?>

hook_nodeapi('form x', ...) replaced by form api

nodeapi op 'form' is no more. Use hook_form_alter instead. This lets you not just add to the node form but change at will.

See the core modules for examples. The conversation process is the following: let's suppose we have foo.module , and it compiles its nodeapi form in _foo_form which takes one argument, $node. and the last command is return $form (core actually had such functions). Then we can rename _foo_form to foo_form_alter and wrap the code as follows:

<?php
function foo_form_alter($form_id, &$form) {
  if (isset(
$form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
   
$node = $form['#node'];
   
// _form_form code follows:
   
$form['foo']['myfield'] = array(...); 
   
// the final return is not needed because $form is a reference.
 
}
}
?>

file_directory variables replaced by functions

variable_get('file_directory_temp', ...) or variable_get('file_directory_path', ...) should be replaced with calls to file_directory_temp() and file_directory_path(). No arguments are necessary for either function.

array2object replaced by native PHP type conversion

$my_object = array2object($my_array); should be replaced with$my_object = (object) $my_array;.

user_load returns FALSE if a user cannot be loaded

user_load now returns FALSE if a user cannot be loaded, instead of an empty object.

MySQL tables are now always UTF-8 encoded

In order to ensure this, two changes need to be made.

First, all MySQL database tables created (e.g. in a my_module.mysql file) need to have a character set attribute appended to each CREATE TABLE statement:

Drupal 4.6

CREATE TABLE my_table (
...
) TYPE=MyISAM;

Drupal 4.7

CREATE TABLE my_table (
...
) TYPE=MyISAM /*!40100 DEFAULT CHARACTER SET utf8 */;

Use the snippet as is, including the comment markers. This ensures your .mysql file still works on MySQL 4.0 and below.

The second change is to upgrade all existing tables. Thanks to the upgrade system, this is very easy. Simple create a my_module.install file in the same directory as your my_module.module file, containing:

<?php
function my_module_update_1() {
  return
_system_update_utf8(array('table1', 'table2', 'table3'));
}
?>

Replace 'table1', 'table2', ... by the names of your module's table(s).

We no longer use the <base> element

In versions of Drupal prior to 4.7, we used the HTML BASE tag to indicate the base to which all relative links should be appended to. In Drupal 4.7, however, the BASE tag has been removed entirely. All Drupal functions that specifically return URLs (such as l() or url() or the various theme_add_style() features) have been updated to now return the base_path() prepended to any URLs. If, however, you are manually creating your own URLs and not using one of Drupal's internal functions, you'll need to do something like:

BEFORE:
print '<link rel="stylesheet" type="text/css" href="themes/chameleon/common.css" />";

AFTER (generic approach):
print '<link rel="stylesheet" type="text/css" href='. base_path() .'"themes/chameleon/common.css" />";

AFTER (strongest specific approach, using Drupal functions):

// this would only apply to styles, naturally
theme('stylesheet_import', base_path() . path_to_theme() ."/common.css");

hook_onload replaced by addLoadEvent()

If you need to add Javascript onLoad events from your modules you now need to use the Drupal custom Javascript function addLoadEvent(func). This was done to allow for new Javacript functions in the core.

<?php
// Drupal 4.6
function hook_onload() {
  return array(
'my_javascript_function()');
}
?>

// Drupal 4.7
// Note: this code is Javascript and should be included in a script tag or in a .js file.
if (isJsEnabled()) {
  addLoadEvent(yourCustomJSFunction);
}

hook_search_item replaced by hook_search_page

This was done to result in cleaner code. Modules must now implement hook_search_page and are expected to provide the necessary themable functions there.

node_validate_title() was removed

Simply specify the '#required' => TRUE attribute on the form item instead, and form API will validate for you.

tablesort_pager() was removed

This function was only ever used in combination with theme('pager'), as the last argument ($parameters). If you simply passed tablesort_pager() here, you can drop this argument altogether. Otherwise you should only specify your custom $parameters:

<?php
// Drupal 4.6
 
$output .= theme('pager', NULL, 50, 0, tablesort_pager());
 
$output .= theme('pager', NULL, 50, 0, $parameters + tablesort_pager());

// Drupal 4.7
 
$output .= theme('pager', NULL, 50, 0);
 
$output .= theme('pager', NULL, 50, 0, $parameters);
?>

chx's overview of menu system innards

This is a longish description of menu structures which are IMO way too big and rarely needed to stuff into a code comment of Drupal. At original creation, it's only chx's understanding of menu system innards.

For easier reference, let $menu = menu_get_menu();

Menu items can be defined at two places: in hook_menu and on the admin/menu interface. The latter items are stored in the database. However, there is the problem of modules changing from time to time and this changes the defined menu items. However, menu items defined on the UI must remain stable, hold their place in the menu tree, so we save some menu information to the menu table to 'pin it down'. There is another problem here, though -- hook_menu defines a ton more information than menu table store. Unless you want to store the PHP code there is no other choice. So, we should store information from the modules into a table and maintain this relationship.

menu IDs are negative for items defined in hook_menu and positive for those stored in the database. There is a special 0 menu ID which is the root element, it's children are the menus.

$menu['items'] will contain an associative array, the keys of which are menu IDs. The values are themselves associative arrays, with the following key-value pairs defined:

  • pid - the menu ID of the parent of the menu item or 0 for menus.
  • path - The Drupal path to the menu item.
  • title - The displayed title of the menu or menu item. It will already have been translated by the locale system.
  • weight - the weight of the menu or menu item.
  • access - whether the current item is accessible to the current user.
  • type - typical values are 22 (MENU_NORMAL_ITEM), 4 (MENU_CALLBACK). See the defines in the beginning of menu.inc
  • children - A linear list of the menu ID's of this item's children. While theoretically this is redundant information as the pid will contain the same information, it's a huge performance benefit to save these.

$menu['visible'] is already documented, it's a subset of $menu['items'] actually.

$menu['path index'] is an associative array, the keys are Drupal paths and the values are menu IDs. $menu['callbacks'] is again an associative array, the keys are Drupal paths and the values are an (surprise!) associative array which define the callback. This callback array always contains a 'callback' => 'name_of_callback_function' pair and optionally a 'callback arguments' => array(arg1, arg2, arg3).

Now that we are familiar with the menu structure, let's see what happens when we save a menu item (menu_edit_item_save): it's saved into the database, without much consideration about our structure. When you move an item it's possible that you put it into under an item which is not yet in the database and then we need to fix the situation.

That's what menu_rebuild does: first it collects all items not yet in the database into the queue. Then it walks the loop and if an element has a valid parent and is not yet in the menu tree then saves it and for new items we fix all its children mid to the new mid. Regardless of the item needed fixing or not, the item is dropped from the queue. As long as there are no recursive menu items, and there can't be, that's guaranteed by _menu_build, the queue will empty after a few loop.

Revisions overhaul

This change affects your module if you do direct SQL operations on node body or teaser fields, or if your module provides a node type and stores the node type's extra information in its own table.

Fields moved from node table to node_revisions table

Before Drupal 4.7 the node table included body and teaserfields. As of 4.7, these fields are moved into the node_revisions table. If you have SQL statements referencing these fields, you will have to rewrite your SQL to refer to the node_revisions table. Typically, you'll do an additional join of node on node_revisions. Here's an example from blog.module.

The 4.6 version:

<?php
  $result
= db_query_range(db_rewrite_sql("SELECT n.nid, n.title, n.teaser, n.created, u.name, u.uid FROM {node} n INNER JOIN {users} u ON n.uid = u.uid WHERE n.type = 'blog' AND u.uid = %d AND n.status = 1 ORDER BY n.created DESC"), $uid, 0, 15);
?>

And the 4.7:

<?php
  $result
= db_query_range(db_rewrite_sql("SELECT n.nid, n.title, r.teaser, n.created, u.name, u.uid FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid INNER JOIN {users} u ON n.uid = u.uid WHERE n.type = 'blog' AND u.uid = %d AND n.status = 1 ORDER BY n.created DESC"), $uid, 0, variable_get('feed_default_items', 10));
?>
Making your module revisions aware

This change has no influence on your node modules unless you want the information to be revisions aware.

Until Drupal 4.6 we used the node ID (nid) as a sole reference for any given node. As of Drupal 4.7 we have both a node ID and a version ID (vid). There can be several vids for each nid, one per revision. There is always at least one vid per nid. The vids are unique within one Drupal install.

Let us assume your module is named foo.module and provides the 'foo' node type. The extra information is stored in an extra table:

nid | foo

In order to take advantage of the new revisioning capabilities you need to change it to

vid | foo

The old nid values will become vid values; the actual value will not change. You, of course, need to change any JOINs that you perform in the nodeapi hook and other hooks. Instead of joining on nid you join on vid. The same applies to SELECTs.

If you do not want to take advantage of the revisioning system you can just leave everything as it is and the extra information provided by your module will be shared across all revisions of a given node.

If you already used revisions before this overhaul you will notice that they seem to be lost after you run the upgrade script. This is not the case. The old revisions are stored in the old_revisions table and will be made available again by a forthcoming update.

The development history of this revisions overhaul is found in this issue: http://drupal.org/node/7582

How to get the revisions system work for custom node fields?

Hi,
Following the node_example.module in drupaldocs, I've created a new node type with some custom fields.

In drupal 4.7 there is a new table in the database called node_revisions. But it seems to me that it only stores the revisions of "body" field of a node. Is that correct?

So my question is how can I enable my custom fields also have the revisions stored, using the new revisions system.

If it's not posible for a custom node-type, how about the for the flexinode module?

Thanks in advance.

Converting 4.6.x modules to 4.6.10

Drupal 4.6.10 saw the addition of a new form field; token, to protect against cross site request forgeries. The token ensures that forms submitted to the site are actually requested first.

The token will be added to all forms generated via the form function.

There is a potential issue surrounding the form_token for forms that are not defined via the form() function.

Raw HTML forms

Forms that are not created via the function form will (nearly) always fail validation. You need to manually add a form token.

<?php
// Fails validation
$output .= '<form method="post" action="'. url('comment') ."\"><div>\n";
$output .= theme('comment_controls', $threshold, $mode, $order, $comments_per_page);
$output .= form_hidden('nid', $nid);
$output .= '</div></form>';
?>

Call form_token() to insert the token right before the closing tag.

<?php
$output
.= '<form method="post" action="'. url('comment') ."\"><div>\n";
$output .= theme('comment_controls', $threshold, $mode, $order, $comments_per_page);
$output .= form_hidden('nid', $nid);

// Add a form token before the closing form tag.
$output .= '</div>' . form_token() . '</form>';
?>

If you want your module to keep functioning on earlier Drupal 4.6 versions, check whether form_token() exists with function_exists('form_token'):

<?php
$output
.= '<form method="post" action="'. url('comment') ."\"><div>\n";
$output .= theme('comment_controls', $threshold, $mode, $order, $comments_per_page);
$output .= form_hidden('nid', $nid);
$output .= '</div>';

// Add a form token before the closing form tag.
if (function_exists('form_token')) {
 
$output .= form_token();
}
$output .= '</form>';
?>

Converting legacy (4.5.4/4.6.2 and below) XML-RPC library to new implementation

The first Drupal versions to use the new library are 4.5.5, 4.6.3 and 4.7. If you have a custom written Drupal module for an older version, then the following applies.

In hook_xmlrpc

<?php
return array('foaf.getUrl' => array('function' => 'foaf_get_url'));
?>

becomes

<?php
return array('foaf.getUrl' => 'foaf_get_url');
?>

Now let's see the handler function itself. It's parameters are regular PHP variables now, there is nothing to process. So now you can write things like function blogapi_blogger_get_user_info($appkey, $username, $password) instead of doing parameter processing.

Return becomes a lot simpler, too:

<?php
return new xmlrpcresp(new xmlrpcval($string, 'string'));
?>

becomes
<?php
return $string;
?>

Client side:

<?php
// send an xmlrpc message to the server to get a full foaf url
$message = new xmlrpcmsg('foaf.getUrl', array(new xmlrpcval($name,
'string')));
$client = new xmlrpc_client('/xmlrpc.php', $server, 80);
if (
$result && !$result->faultCode()) {
 
$value = $result->value();
 
$user->foaf_url = $value->scalarval();
}
?>

this becomes
<?php
$result
= xmlrpc($server. '/xmlrpc.php', 'foaf.getUrl', $name);
if (
$result !== FALSE) {
 
$user->foaf_url = $result;
}
?>

Back to method handlers. All the efforts that are made to make the PHP --
XML-RPC conversations transparent can't handle date and base64 encoding.

So:

<?php
$node
->created = iso8601_decode($struct['dateCreated'], 1);
?>

becomes
<?php
$node
->created = mktime($struct['dateCreated']->hour,
$struct['dateCreated']->minute, $struct['dateCreated']->second,
$struct['dateCreated']->month, $struct['dateCreated']->day,
$struct['dateCreated']->year);
?>

And if you want to return a date (either a Unix timestamp or an ISO 8601
formatted one):
<?php
return xmlrpc_date($node->created);
?>

otherwise you'd get a return type of int for Unix timestamp and string for
ISO.

Likewise, to make the distinction between base64 and string, you need to
xmlrpc_base64($binary_data) your binary data.

Converting 4.5.x modules to 4.6.x

Block system

Every block now has a configuration page to control block-specific options. Modules which have configurations for their blocks should move those into hook_block().

The only required changes to modules implementing hook_block() is to be careful about what is returned. Do not return anything if $op is not 'list' or 'view'. Once this change is made, modules will still be compatible with Drupal 4.5.

If a specific block has configuration options, implement the additional $op options in your module. The implementation of 'configure' should return a string containing the configuration form for the block with the appropriate $delta. 'save' will have an additional $edit argument, which will contain the submitted form data for saving.

Search system

The search system got a significant overhaul.

Node indexing now uses the node's processed and filtered output, which means that any custom node fields will automatically be included in the index, as long as they are visible to normal users who view the node. Modules that implement hook_search() and hook_update_index() just to have extra node fields indexed no longer need to do this.

If you wish to have additional information indexed that is not visible in the node display at node/id, then you can do so using nodeapi('update index'). If you want to add extra information to the node results, use nodeapi('search result').

However, the standard search is still limited to a keyword search. Modules that implement custom, specific search forms (like project.module) can still do so. Custom search forms that do not use hook_search() should be located/moved to a local task under the /search page.

If you are unsure of what you need to do, please refer to the complete search documentation.

Module paths

The function module_get_path was renamed to drupal_get_path which now returns the path for all themes, theme engines and modules. Because of this abstraction you must pass an additional parameter identifying the type of item for which the path is requested. The following example compares retrieving the path to image module between Drupal 4.5 and 4.6.

<?php
// Drupal 4.5:
$path = module_get_path('image');

// Drupal 4.6:
$path = drupal_get_path('module', 'image');
?>

All instances of module_get_path should be renamed to drupal_get_path.

Database backend

The function check_query was renamed to db_escape_string and now has a database specific implementation. All instances of check_query should be renamed to db_escape_string.

Theme system

The function theme_page() no longer takes $title or $breadcrumb arguments. Set page titles using hook_menu() or, if the title must be dynamically determined, use drupal_set_title(). Set breadcrumb trails first using hook_menu(), which can be overridden with menu_set_location() and drupal_set_breadcrumb().

Watchdog messages

The watchdog() function now takes a severity attribute, so watchdog($type, $message, $link); becomes watchdog($type, $message, $severity, $link);. Specify a severity in case you are reporting a warning or error. Possible severity constants are: WATCHDOG_NOTICE, WATCHDOG_WARNING and WATCHDOG_ERROR. Also make sure that you provide the type as a literal string, so translation extraction can pick it up.

If you are unsure of which severity to use, remember these rules:

  • If the problem is caused by a definite fault and should be fixed as soon as possible, use an error message.
  • If the problem could point to a fault, but could also be harmless, use a warning message. This type should also be used whenever the problem could be caused by a remote server (example: ping timeout, failed to aggregate a feed, etc).
  • Normal messages should be notices.

Node markers

If you have a module calling theme('mark'), note that it is now possible to have different markers for different states of a node. The supported states are MARK_NEW, MARK_UPDATED and MARK_READ. You can get the marker state from node_mark(), which replaces the node_new() function available in previous Drupal versions.

Control over destination page after form processing

Occasionally a module might want to specify where a user should go after he submits a form. This is now possible by passing a querystring parameter &destination=<path>. For example, editing of nodes and comments from within the Admin pages now returns the user to those pages after he is done. For example usage, search drupal_get_destination() which can be found in path.module, node.module, comment.module, and user.module

Confirmation messages

Confirmations for dangerous actions should now be presented with the theme('confirm') function for consistency. Check the function's documentation or look at some of the core modules for examples.

Note that this is a themable function which should be invoked through theme('confirm') and not theme_confirm().

Inter module calls

New features are available -- it's not necessary to use them. Now you can really (and should) use module_invoke to call a function from another module. For example, taxonomy_get_tree should be called by module_invoke('taxonomy', 'get_tree') If you need to loop through the implementations of a hook, please check the new module_implements function.

Node queries

If you have a module which retrieves a list of nodes by issuing its own database query, then the following applies.

The functions node_access_join_sql() and node_access_where_sql() should not be used any more but the SELECT-queries should be wrapped in a db_rewrite_sql() call.

If you have used DISTINCT(nid) -- because of node_access_join_sql() -- you no longer need it, replace it simply with n.nid. If you have SELECT *, please replace it with SELECT n.nid, n.* -- and always make sure that n.nid field comes first in the SELECT statement -- this way the db_rewrite_sql() function can rewrite the query to use DISTINCT(nid) should there be a need for it. If the n.nid field is not first, the query will fail when node access modules are enabled. Also, at the moment db_rewrite_sql can not handle AS -- either leave it out or lowercase it.

Always use table name before the field names, especially before nid because other tables may be JOINed during the rewrite process.

Example:

<?php
// Drupal 4.5:
$nodes = db_query_range('SELECT DISTINCT(n.nid) FROM {node} n '. node_access_join_sql() .' WHERE '. node_access_where_sql() .' AND n.promote = 1 AND n.status = 1 ORDER BY n.created DESC', 0, 15);
// Drupal 4.6:
$nodes = db_query_range(db_rewrite_sql('SELECT n.nid FROM {node} n  WHERE n.promote = 1 AND n.status = 1 ORDER BY n.created DESC'), 0, 15);
?>

If you are not using the node table, then you shall pass the table name from which you SELECTing the nodes. For example

<?php
$result
= db_query(db_rewrite_sql("SELECT f.nid, f.* from {files} f WHERE filepath = '%s'", 'f'), $file);
?>

note the 'f' parameter of db_rewrite_sql().

Avoid USING because there could be JOINs before it, which will break the USING clause.

Text output

Drupal's text output was audited and several escaping bugs were found. For more info, see the check_plain patch.

You need to pay attention that all user-submitted plain-text in your module is escaped using check_plain() when you output it into HTML. No escaping should be done on data that is going into the database: only escape when outputting to HTML.

Check_plain() replaces drupal_specialchars() and check_form(), so if you are using any of those two, you should use check_plain() instead.

You should also wrap user-submitted text in messages with theme('placeholder', $text). For example for "created term %term".

Pay attention in particular to node and comment titles as their behaviour has been changed. They are now stored as plain-text, like other single-line fields in Drupal and should be escaped when output. However, the function l() now takes plain-text by default instead of HTML, which means that whenever $node->title is used as the caption for a link, it will automatically be escaped. When outputting titles literally, you still have to escape them yourself.

URLs also require attention, as the URL functions (url, request_uri, referer_uri, etc) were changed to output 'real' URLs rather than HTML-escaped URLs. When putting any of them inside an HTML tag attribute (e.g. <a href="...">), you need to pass it through check_url() first. When putting an URL into HTML outside of a tag or attribute, you can use check_url() or check_plain(), it doesn't matter. Don't use check_url() in situations where a real URL is expected (e.g. the HTTP "Location: ..." header).

The best test is to submit forms with HTML tags in the plain-text/single-line fields (e.g. "<u>test</u>"). If the underline tag is not interpreted, but displayed literally, your module is escaping the text correctly.

Nothing has changed for filtered/rich text, which still uses check_output() like before.

Converting 4.4.x modules to 4.5.x

Menu system

The Drupal menu system got a complete rewrite. The new features include:

  • The administrator may now customize the menu to reorder, remove, and add items.
  • Menu items may be classified as "local tasks," which will by default be displayed as tabs on the page content.
  • The menu API is much more consistent with the rest of Drupal's API.

The menu() function is no more. In its place, we have hook_menu(). The old hook_link() remains, but will no longer be called with the "system" argument. The hook reference in the Doxygen documentation details all the specifics of this new hook. In short, rather than making many calls to menu() in your hook_link() implementation, you will implement hook_menu() to return an array of the menu items you define.

As an example, the old pattern:

<?php
function blog_link($type, $node = 0, $main) {
  global
$user;
  if (
$type == 'system') {
   
menu('node/add/blog', t('blog entry'), user_access('maintain personal blog') ? MENU_FALLTHROUGH : MENU_DENIED, 0);
   
menu('blog', t('blogs'), user_access('access content') ? 'blog_page' : MENU_DENIED, 0, MENU_HIDE);
   
menu('blog/'. $user->uid, t('my blog'), MENU_FALLTHROUGH, 1, MENU_SHOW, MENU_LOCKED);
   
menu('blog/feed', t('RSS feed'), user_access('access content') ? 'blog_feed' : MENU_DENIED, 0, MENU_HIDE, MENU_LOCKED);
  }
}
?>

becomes:
<?php
function blog_menu($may_cache) {
  global
$user;

 
$items = array();
  if (
$may_cache) {
   
$items[] = array('path' => 'node/add/blog', 'title' => t('blog entry'),
     
'access' => user_access('maintain personal blog'));
   
$items[] = array('path' => 'blog', 'title' => t('blogs'),
     
'callback' => 'blog_page',
     
'access' => user_access('access content'),
     
'type' => MENU_SUGGESTED_ITEM);
   
$items[] = array('path' => 'blog/'. $user->uid, 'title' => t('my blog'),
     
'access' => user_access('maintain personal blog'),
     
'type' => MENU_DYNAMIC_ITEM);
   
$items[] = array('path' => 'blog/feed', 'title' => t('RSS feed'),
     
'callback' => 'blog_feed',
     
'access' => user_access('access content'),
     
'type' => MENU_CALLBACK);
  }
  return
$items;
}
?>

Drupal now distinguishes between 404 (Not Found) pages and 403 (Forbidden) pages. To accommodate this, modules should abandon the practice of not declaring menu items when access is denied to them. Instead, they should set the "access" attribute of their newly-declared menu item to FALSE. This will have the effect of the menu item being hidden, and also preventing the callback from being invoked by typing in the URL. Modules may also want to take advantage of the drupal_access_denied() function, which prints a 403 page (the analogue of drupal_not_found(), which prints a 404).

Path changes

Some internal URL paths have changed; check the links printed by your code. Most significant is that paths of the form "node/view/52" are now "node/52" instead, while "node/edit/52" becomes "node/52/edit".

Node changes

  • The database field static has been renamed to sticky.
  • Error handling of forms (such as node editing forms) is now done using form_set_error(). It simplifies the forms and validation code; however, it does change the node API slightly:
    • The _validate hook and the _nodeapi('validate') hook of the node API no longer take an "error" parameter, and should no longer return an error array. To set an error, call form_set_error().
    • Node modules' hook_form() implementations no longer take an "error" parameter and should not worry about displaying errors. The same applies to hook_nodeapi('form_post') and hook_nodeapi('form_pre').
    • All of the form_ family of functions can take a parameter that marks the field as required in a standard way. Use this instead of adding that information to the field description.
  • In order to allow modules such as book.module to inject HTML elements into the view of nodes safely, hook_nodeapi() was extended to respond to the 'view' operation. This operation needs to be invoked after the filtering of the node, so hook_view() was changed slightly to no longer require a return value. Instead of calling theme('node', $node) and returning the result as before, the hook can just modify $node as it sees fit (including running $node->body and $node->teaser through the filters, as before), and the calling code will take care of sending the result to the theme. Most modules will just work under the new semantics, as the return value from the hook is just discarded, but the $node parameter is now required to be passed by reference (this was common but optional before).
  • We have node-level access control now! This means that node modules need to make very small changes to their hook_access() implementations. The check for $node->status should be removed; the node module takes care of this check. A value should only be returned from this hook if the node module needs to override whatever access is granted by the node_access table. See the hook API for details.

    Node listing queries need to be changed as well, so that they properly check for whether the user has access to the node before listing it. Queries of the form

    <?php
    db_query
    ('SELECT n.nid, n.title FROM {node} n WHERE n.status = 1 AND foo');
    ?>

    become
    <?php
    db_query
    ('SELECT n.nid, n.title FROM {node} n '. node_access_join_sql() .' WHERE n.status = 1 AND '. node_access_where_sql() .' AND foo');
    ?>

    See node access rights in the Doxygen reference.

Filtering changes

This change affects non-filter modules as well! Please read on even if your module does not filter.

The filter system was changed to support multiple input formats. Each input format houses an entire filter configuration: which filters to use, in what order and with what settings. The filter system now supports multiple filters per module as well.

Check_output() changes

Because of the multiple input formats, a module which implements content has to take care of managing the format with each item. If your module uses the node system and passes content through check_output(), then you need to do two things:

  • Pass $node->format as the second parameter to check_output() whenever you use it.
  • Add a filter format selector to hook_form using a snippet like:
    <?php
    $output
    .= filter_form('format', $node->format);
    ?>

The node system will automatically save/load the format value for you.

If your module provides content outside of the node system, you can decide if you want to support multiple input formats or not. If you don't, the default format will always be used. However, if your module accepts input through the browser, it is strongly advised to support input formats!

To do this, you must:

  • Provide a selector for input formats on your forms, using filter_form().
  • Validate the chosen input format on submission, using filter_access().
  • Store the format ID with each content item (the format ID is a number).
  • Pass the format ID to check_output().

Check the API documentation for these functions for more information on how to use them.

Filter hook

The _filter hook was changed significantly. It's best to start with the following framework:

<?php
function hook_filter($op, $delta = 0, $format = -1, $text = '') {
  switch (
$op) {
    case
'list':
      return array(
0 => t('Filter name'));

    case
'description':
      return
t("Short description of the filter's actions.");
/*
    case 'no cache':
      return true;
*/
   
case 'prepare':
     
$text = ...
      return
$text;

    case
'process':
     
$text = ...
      return
$text;

    case
'settings':
     
$output = ...;
      return
$output;

    default:
      return
$text;
  }
}
?>

When converting a module to 4.5, you can normally ignore the $delta paramter: it is used to have multiple filters inside one module. The 'prepare', 'process' and 'settings' operations still work the same as before, with only small changes.

However, you should now include the $format parameter in the variable names for filter settings. If your filter has a setting "myfilter_something", it should be changed to "myfilter_something_$format". This allows the setting to be set separately for each input format. To check if it works correctly, add your filter to two different input formats and give each instance different settings. Verify that each input format retains its own settings.

Unlike before, the 'settings' operation should only be used to return actually useful settings, because there is now a separate overview of all enabled filters. A filter does not need its own on/off toggle. If a filter has no configurable settings, it should return nothing for the settings, rather than a message like we did before.

Finally, the filter system now includes caching. If your filter's output is dynamic and should not be cached, uncomment the 'no cache' snippet. Only do this when absolutely necessary, because this turns off caching for any input format your filter is used in. Beware of the filter cache when developing your module: it is advised to uncomment 'no cache' while developing, but be sure to remove it again if it's not needed.

Filter tips

Filter tips are now output through the format selector. Modules no longer need to call filter_tips_short() to display them.

A module's filter tips are returned through the filter_tips hook:

<?php
function hook_filter_tips($delta, $format, $long = false) {
  if (
$long) {
    return
t("Long tip");
  }
  else {
    return
t("Short tip");
  }
}
?>

As in the filter hook you can ignore the $delta parameter if you're upgrading an existing module. If your filter's tips depend on its settings, make sure you use $format to retrieve the setting for the current input format. $long tells you whether to return long or short tips.

Other changes

In addition to the above mentioned changes:

  • hook_user() was changed to allow multiple pages of user profile information. The new syntax of the hook is given in the API reference. Pay particular attention to the "categories", "form", and "view" operations.
  • When processing a form submission, you should use drupal_goto() to redirect to the result if the submission was accepted. This prevents a double post when people refresh their browser right after submitting. Messages set with drupal_set_message() will be saved across the redirect. If a submission was rejected, you should not use drupal_goto(), but simply print out the form along with error messages.

Converting 4.3.x modules to 4.4.x

Since Drupal 4.3, major changes have been made to the theme, menu, and node systems. Most themes and modules will require some changes.

Menu system

The Drupal menu system has been extended to drive all pages, not just administrative pages. This is continuing the work done for Drupal 4.3, which integrated the administrative menu with the user menu. We now have consistency between administrative and "normal" pages; when you learn to create one, you know how to create the other.

The flow of page generation now proceeds as follows:

  1. The _link hook in all modules is called, so that modules can use menu() to add items to the menu. For example, a module could define:
    <?php
    function example_link($type) {
      if (
    $type == "system") {
       
    menu("example", t("example"), "example_page");
       
    menu("example/foo", t("foo"), "example_foo");
      }
    }
    ?>
  2. The menu system examines the current URL, and finds the "best fit" for the URL in the menu. For example, if the current URL is example/foo/bar/12, the above menu() calls would cause example_foo("bar", 12) to get invoked.
  3. The callback may set the title or breadcrumb trail if the defaults are not satisfactory (more on this later).
  4. The callback is responsible for printing the requested page. This will usually involve preparing the content, and then printing the return value of theme("page"). For example:
    <?php
    function example_foo($theString, $theNumber) {
     
    $output = $theString. " - " .$theNumber;
      print
    theme("page", $output);
    }
    ?>

The following points should be considered when upgrading modules to use the new menu system:

  • The _page hook is obsolete. Pages will not be shown unless they are declared with a menu() call as discussed above. To convert former _page hooks to the new system as simply as possible, just declare that function as a "catchall" callback:
    <?php
      menu
    ("example", t("example"), "example_page", 0, MENU_HIDE);
    ?>

    The trailing MENU_HIDE argument in this call makes the menu item hidden, so the callback functions but the module does not clutter the user menu.
  • Old administrative callbacks returned their content. In the new system, administrative and normal callbacks alike are responsible for printing the entire page.
  • The title of the page is printed by the theme system, so page content does not need to be wrapped in a theme("box") to get a title printed. If the default title is not satisfactory, it can be changed by calling drupal_set_title($title) before theme("page") gets called, or by passing the title to theme("page") as a parameter.
  • The breadcrumb trail is also printed by the theme. If the default one needs to be overridden (to present things like forum hierarchies), this can be done by calling drupal_set_breadcrumb($breadcrumb) before theme("page") gets called, or by passing the breadcrumb to theme("page") as a parameter. $breadcrumb should be a list of links beginning with "Home" and proceeding up to, but not including, the current page.

Theme system

For full information on theme system changes, see converting 4.3 themes to CVS. The following points are directly relevant to module development:

  • All theme functions now return their output instead of printing them to the user. Old theme() usage:
    <?php
    theme
    ("box", $title, $output);
    ?>

    New usage:
    <?php
    print theme("box", $title, $output);
    ?>

    Modules that define their own theme functions should also return their output.
  • The naming of theme functions defined by modules has been standardized to theme_&lt;module&gt;_&lt;name&gt;. When using a theme function there is no need to include the theme_ part, as theme() will do this automatically. Example:
    <?php
    function theme_example_list($list) {
      return
    implode('<br />', $list);
    }

    print
    theme('example_list', array(1,2,3));
    ?>

    Theme functions must always be called using theme() to allow for the active theme to modify the output if necessary.
  • The theme("header") and theme("footer") functions are not available anymore. Module developers should use the theme("page") function which wraps the content in the site theme. The full syntax of this function is
    <?php
    theme
    ("page", $output, $title, $breadcrumb);
    ?>

    where $title and $breadcrumb will override any values set before for these properties.

Node system

The node system has been upgraded to allow a single module to define more than one type of node. This will allow some of the more convoluted code in, for example, project.module to be tidied up.

  • The _node() hook has been deprecated. In its place, modules that define nodes should use _node_name() and _help().
  • The _node_name() function should return a translated string containing the human-readable name of the node type.
  • The _help() function, when called with parameter "node/add#modulename", should return a translated string containing the description of the node type.

Filter system

  • The various filter hooks ('filter', 'conf_filters') have been merged into one 'filter' hook. A module that provides filtering functionality should implement:
    <?php
    function example_filter($op, $text = "") {
      switch (
    $op) {
        case
    "name":
          return
    t("Name of the filter");
        case
    "prepare":
         
    // Do preparing on $text
         
    return $text;
        case
    "process":
         
    // Do processing on $text
         
    return $text;
        case
    "settings":
         
    // Generate $output of settings
         
    return $output;
      }
    }
    ?>
  • "name" is new, and should return a friendly name for the filter.
  • "prepare" is also new. This is an extra step that is performed before the default HTML processing, if HTML tags are allowed. It is meant to give filters the chance to escape HTML-like data before it can get stripped. This means, to convert meaningful HTML characters like < and > into entities such as &lt; and &gt;.

    Common examples include filtering pieces of PHP code, mathematical formulas, etc. It is not allowed to do anything other than escaping in the "prepare" step.

    If your filter currently performs such a step in the main "process" step, it should be moved into "prepare" instead. If you don't need any escaping, your filter should simply return $text without processing in this case.

  • "process" is the equivalent of the old "filter" hook. Normal filtering is performed here, and the changed $text is returned.
  • "settings" is the equivalent of the old "conf_filters" hook. If your filter provides configurable options, you should return them here (using the standard form_* functions).
  • The filter handling code has been moved to a new required filter.module, and thus most of the filter function names changed, although none of those should have been called from modules. The check_output() function is still available with the same functionality.
  • Node filtering is optimized with the node_prepare() function now, which only runs the body through the filters if the node view page is displayed. Otherwise, only the teaser is filtered.
  • The _compose_tips hook (defined by the contrib compose_tips.module) is not supported anymore, but more advanced functionality exists in the core. You can emit extensive compose tips related to the filter you define via the _help hook with the 'filter#long-tip' section identifier. The compose_tips URL is thus changed to filter/tips. The form_allowed_tags_text() function is replaced with filter_tips_short(), which now supports short tips to be placed under textareas. Any module can inject short tips about the filter defined via the _help hook, with the 'filter#short-tip' section identifier.

Hook changes

Other than those mentioned above, the following hooks have changed:

  • The _view hook has been changed to return its content rather than printing it. It also has an extra parameter, $page, that indicates whether the node is being viewed as a standalone page or as part of a larger context. This is important because nodes may change the breadcrumb trail if they are being viewed as a page. Old usage:
    <?php
    function example_view($node, $main = 0) {
      if (
    $main) {
       
    theme("node", $node, $main);
      }
      else {

       
    $breadcrumb[] = l(t("Home"), "");
       
    $breadcrumb[] = l(t("foo"), "foo");
       
    $node->body = theme("breadcrumb", $breadcrumb) ."<br />". $node->body;
       
    theme("node", $node, $main);
      }
    }
    ?>

    New usage:
    <?php
    function example_view($node, $main = 0, $page = 0) {
      if (
    $main) {
        return
    theme("node", $node, $main, $page);
      }
      else {
        if (
    $page) {
         
    $breadcrumb[] = l(t("Home"), "");
         
    $breadcrumb[] = l(t("foo"), "foo");
         
    drupal_set_breadcrumb($breadcrumb);
        }
        return
    theme("node", $node, $main, $page);
      }
    }
    ?>
  • The _form hook used by node modules no longer takes 3 arguments. The second argument $help, typically used to print submission guidelines, has been removed. Instead, the help should be emitted using the module's _help hook. For examples, check the story, forum or blog module.
  • The _search hook was changed to not only return the result set array, but a two element array with the result group title and the result set array. This provides more precise control over result group titles.
  • The _head hook is eliminated and replaced with the drupal_set_html_head() and drupal_get_html_head() functions. You can add JavaScript code or CSS to the HTML head part with the drupal_set_html_head() function instead.
  • See also the description of the _compose_tips hook changes below.

Emitting links

  • The functions url() and l() take a new $fragment parameter. Calls to url() or l() that have '#' in the $url parameter need to be updated. If you don't update such calls, Drupal's path aliasing won't work for URLs with # in them.
  • Drupal now emits relative URLS instead of absolute URLs. Contributed modules must be updated whenere an absolute url is required. For example:
    • Any module that outputs an RSS feed without using node_feed() should be updated. Note: this is discouraged. please use node_feed() instead. Also modules using node_feed() should provide an absolute link in the 'link' key, if any.
    • Any module which send email should be updated so that links in the email have absolute urls instead of relative urls. You do this using a parameter in your call to l() or url()

Status and error messages

  • Modules that use theme('error', ...) to print error messages should be updated to use drupal_set_message(..., 'error') unless used to print an error message below a form item.
    <?php
    drupal_set_message
    (t('failed to update X', 'error'));  // set the second parameter to 'error'
    ?>
  • Modules that print status messages directly to the screen using status() should be updated to use drupal_set_message(). The status() function has been removed.
    <?php
    drupal_set_message
    (t('updated X'));
    ?>

Converting 4.2.x modules to 4.3.x

Database table prefix
On 2003 Jul 10, Dries committed Slavica's table prefix patch which allows for a configurable "prefix to each drupal mysql table to easily share one database for multiply applications on server with only one database allowed." This patch requires all table names in SQL-queries to be enclosed in {curly brackets}, eg.
-  db_query("DELETE FROM book WHERE nid = %d", $node->nid);
+  db_query("DELETE FROM {book} WHERE nid = %d", $node->nid);

so that the table prefix can be dynamically prepended to the table name. See the original feature request and the corresponding discussion at the mailing list for details.
New help system
From Michael Frankowski message:

There is a block of text placed at the top of each admin page by
the admin_page function. After 4.3.0 is out the door the function
menu_get_active_help() should probably be renamed/moved into the help module and be attached -- somehow -- to every _page hook (probably in the node module) so that we can use this system through out Drupal but for now, there is a block of text displayed at the top of every admin page. This is the active help block. (context sensitive help?)

If the URL of the admin page matches a URL in a _help hook then the text from that _help hook is displayed on the top of the admin page. If there is no match, the block it not displayed. Because Drupal matches URLs in order to stick "other" stuff in the _help hook we have taken to sticking descriptors after a "#" sign. So far, the following descriptors are recognised:

Descriptor Function
admin/system/modules#name The name of a module (unused, but there)
admin/system/modules#description The description found on the admin/system/modules page.
admin/help#modulename The module's help text, displayed on the admin/help page and through the module's individual help link.
user/help#modulename The help for a distrbuted authorization module

In the future we will probably recognise #block for the text needed
in a block displayed by the help system.

How to build up a _help hook

The following template can be used to build a _help hook.

<?php
function <modulename>_help($section){
 
$output = "";

  switch (
$section) {

  }
  return
$output;
}
?>

In the template replace modulename with the name of your module.

If you want to add help text to the overall administrative section. (admin/help) stick this inside the switch:

<?php
   
case 'admin/help#<modulename>':
     
$output = t('The text you want displayed');
      break;
?>

If you also want this same text displayed for an individual help link in your menu area. You have this kind of tree:

  + Administration
  |
  -> Your area
  |  |
  |  -> Your configuration
  |  -> help
  |
  -> Overall admin help.

Change the function line to this:

<?php
function <modulename>_help($section = 'admin/help#<modlename>') {
?>

Now that you have the template started place a case statement in for any URL you want a "context sesitive" help message in the admin section. An example, you have a page that individually configures your module, it is at admin/system/modules/, you want to add some text to the top help area.

<?php
   
case 'admin/system/modules/<modulename>':
     
$output = t('Your new help text');
      break;
?>

How to convert a _system hook

There are three things that can appear in a _system hook:

Field Function
$field == "name" The module name
$field == "description" The description placed in the module list
$field == "admin-help" The help text placed at the TOP of this module's individual configuration area.

Take the text for each one and move it into the _help hook. Replace the $system[<name>] that is normally at the front of each one with $output, now place a "break;" after the line and a case '<name>': before it where name is one of the following:

  • If $system is $system["name"] then the case is case 'admin/system/modules#name'
  • If $system is $system["description"] then case is case 'admin/system/modules#description'
  • If $system is $system["admin-help"] then the case is case 'admin/system/modules/<modulename>'

Now remove the _system function and you are done.

An example:

<?php
function example_system($field){
 
$system["description"] = t("This is my example _system hook to convert for
the help system I have spent a lot of time with."
);
 
$system["admin-help"] = t("Can you believe that I would actually write an
indivdual setup page on an EXAMPLE module??"
);
  return
$system[$field];
}
?>
<?php
function example_help($section) {
 
$output = "";

  switch (
$section) {
    case
'admin/system/modules#example':
     
$output = t("This is my example _system hook to convert for the help
system I have spent a lot of time with."
);
      break;
    case
'admin/system/modules/example':
     
$output = t("Can you believe that I would actually write an indivdual
setup page on an EXAMPLE module??"
);
      break;
  }
  return
$output;
}
?>

How to convert an _auth_help hook

Okay, you have written your Distributed Authorization module, and given us a great help text for it and I had to go and ruin it all by changing the help system. What a terrible thing for me to do. How do you convert it?

It is not that hard. There are two places you have to deal with:

  1. The text inside the _auth_help hook needs to be moved inside the _help hook under the section user/help#<modulename> and
  2. You have to change the _page hook, which normally displays that help text, to find your text in a new location by changing the function call <modulename>_auth_help() to <modulename>_help("user/help#<modulename>").

See, it is not THAT terrible.

An example:

<?php
function exampleda_page() {

 
theme("header");
 
theme("box", "Example DA", exampleda_auth_help());
 
theme("footer");
}

function
exampleda_auth_help() {
 
$site = variable_get("site_name", "this web site");
 
$html_output = "
  <p>This is my example Distributed Auth help. Using this example you cannot login to <i>%s</i> because it has no _auth hook.&</p>

<p><u>BUT</u> you should still use Drupal since it is a <b>GREAT</b> CMS and is only getting better.</p>

<p>To learn about about Drupal you can <a
href=\"www.drupal.org\">visit the site</a></p>"
;
  return
sprintf(t($html_output), $site);
}
?>
<?php
function exampleda_page() {

 
theme("header");
 
theme("box", "Example DA", exampleda_help('user/help#exampleda'));
 
theme("footer");
}

function
exampleda_help($section) {
 
$output = "";

  switch (
$section) {
    case
'user/help#exampleda':
     
$site = variable_get("site_name", "this web site");
     
$output .= "<p>This is my example Distributed Auth help. Using this example you cannot login to %site because it has no _auth hook.</p>";
     
$output .= "<p><u>BUT&</u> you should still use Drupal
since it is a <b>GREAT</b> CMS and is only getting better.</p>"
;
     
$output .= "<p>To learn about about Drupal you can
visit %drupal.</p>"
;
     
$output = t($output, array("%site" => "<i>$site</i>",
"%drupal" => "<a href=\"www.drupal.org\">visit the site</a>"));
      break;
  }
  return
$output
}
?>

Converting 4.1.x modules to 4.2.x

Some points posted by Axel on drupal-devel on migrating 4.1.0 modules to CVS [updated and added to by ax]:

  • drupal_url(array("mod" => "search", "op" => "bla"), "module"[, $anchor = ""])
      became
    url("search/bla"),
      with the first url part being the module, the second (typically) being the operation ($op); more arguments are handled differently per module convention.
  • l("view node", array("op" => "view", "id" => $nid), "node"[, $anchor = "", $attributes = array()])
      became
    l("view node", "node/view/$nid"[,$attributes = array(), $query = NULL])
  •   similar,
    lm(), which meant "module link" and used to be module.php?mod=bla&op=blub..., is now l("title", "bla/blub/..."); and
    la(), which meant "admin link" and used to be admin.php?mod=bla&op=blub..., is now l("title", "admin/bla/blub/..."
  • After fixing those functions, you'll need to edit your _page() function and possibly others so that they get their arguments using the arg() function (see includes/common.inc. These arguments used to be globals called "mod", "op", "id" etc. now these same arguments must be accessed as arg(1), arg(3), for example.
  • $theme->function() became theme("function"). see [drupal-devel] renaming 2 functions, [drupal-devel] theme("function") vs $theme->function() and [drupal-devel] [CVS] theme()
  • &lt;module&gt;_conf_options() became &lt;module&gt;_settings() - see [drupal-devel] renaming 2 functions. note that doesn't get an extra menu entry, but
    is accessed via "site configuration > modules > modules settings"
  • the administration pages got changed quite a lot to use a "database driven link system" and become more logical/intuitive - see [drupal-devel] X-mas commit: administration pages. this first try resulted in poor performance and a not-so-good api, so it got refactored - see [PATCH] menus. this, as of time ax is writing this, isn't really satisfying, neither (you cannot build arbitrary menu-trees, some forms don't work (taxonomy > add term), ...), so it probably will change again. and i won't write more about this here.

    well, this: you use menu() to add entries to the admin menu. menu("admin/node/nodes/0", "new or updated posts", "node_admin", "help", 0); adds a menu entry "new or updated posts" 1 level below "post overview" (admin/node/nodes) and 2 level below "node management" (admin/node) (ie. at the 3. level), with a weight of 0 in the 3. level, with a line "help" below the main heading. for the callback ("node_admin") ... ask dries or zbynek

    one more note, though: you do not add &lt;module&gt;_settings() to the menu (they automatically go to "site configuration > modules > module settings" - you only add &lt;module&gt;_admin...() ... things.

  • [from comment_is_new function lost]
    -  comment_is_new($comment)
    +  node_is_new($comment->nid, $comment->timestamp)
  • please add / update / correct!

Converting 4.0.x modules to 4.1.x

Drupal 4.1 changed the block hook function and taxonomy API. To convert a version 4.0 module to 4.1, the following changes must be made. First, the *_block() function must be re-written. Next, calls to taxonomy_get_tree() must be re-written to supply the parameters required by the new function. Finally, you may wish to take advantage of new functions added to the taxonomy API.

Required changes

Modified block hook:
Drupal 4.0:
function *_block() {
  $blocks[0]["info"] = "First block info";
  $blocks[0]["subject"] = "First block subject";
  $blocks[0]["content"] = "First block content";

  $blocks[1]["info"] = "Second block info";
  $blocks[1]["subject"] = "Second block subject";
  $blocks[1]["content"] = "Second block content";

  // return array of blocks
  return $blocks;
  }
}

Drupal 4.1:

function *_block($op = "list", $delta = 0) {
  if ($op == "list") {
    $blocks[0]["info"] = "First block info";
    $blocks[1]["info"] = "Second block info";
    return $blocks; // return array of block infos
  }
  else {
    switch($delta) {
      case 0:
        $block["subject"] = "First block subject";
        $block["content"] = "First block content";
        return $block; 
      case 1:
        $block["subject"] = "Second block subject";
        $block["content"] = "Second block content";
        return $block;
    }
  }
}

Modified taxonomy API:

Changes: in function taxonomy_get_tree()

  • there is no longer a "parent" property; rather "parents" is an array
  • the result tree is now returned instead of being passed by reference

Drupal 4.0:

function taxonomy_get_tree($vocabulary_id, &$tree, $parent = 0, $depth = -1, $key = "tid")

Drupal 4.1:

<b>$tree =</b> taxonomy_get_tree($vocabulary_id, <b>$parents</b> = 0, $depth = -1, $key = "tid")

Optional changes

  • Take advantage of new taxonomy functions taxonomy_get_vocabulary_by_name($name) and taxonomy_get_term_by_name($name)
  • Take advantage of pager functions
  • Move hardcoded markup from modules to themes, using theme_invoke

Converting 3.0.x modules to 4.0.x

Converting modules from version 3.0 to version 4.0 standards requires rewriting the form() function, as follows:

Drupal 3.0:

function form($action, <b>$form</b>, $method = "post", $options = 0)

// Example

global $REQUEST_URI;
$form = form_hidden("nid", $nid);
print form($REQUEST_URI, $form);

Drupal 4.0:

function form(<b>$form</b>, $method = "post", $action = 0, $options = 0)

// Example

$form = form_hidden("nid", $nid);
print form($form);

Drupal enhancement proposals (DEP)

This handbook section contains all the Proposals for enhancing Drupal. If you have a big project to overhaul a part of the core, a big infrastructural change, or big changes to a contribution, please write up a structured proposal first.
By no means is this required, we strongly encourage everyone to just add patches to the issues queue.
Also please discuss any proposals on the development mailinglist first.

A DEP should be structured using the following template

Title :
Abstract:
Author:
Dependencies:
Repository:
Status:

1. Introduction :
  Keep it short

2. Motivation :

3. Approach :

4. Security considerations: (optional, if it involves security related issues)

5. Excluding : (optional, what will NOT be covered in this DEP)

DEPS in progress

This chapter contains all the Drupal enhancement proposals that are in progress.

Enabling story module on Drupal.org

Title : Mapping API (map.module)
Abstract: Rationale for enabling story module on Drupal.org to streamline and improve the submission, monitoring, and organization of non-documentation style content to Drupal
Author: Boris Mann
Repository: N/A
Status: Proposal

Introduction

There is a lot of content in the handbook that could improve by being in a more consumable form. Using story nodes allows us to have a separate workflow....

Motivation

Approach

Excluding

The addition of other modules (e.g. voting, etc.) is currently out of scope. However, as rating/voting is developed for portions of Drupal.org (for example, contrib projects), then the same system should be applied to these story nodes.

Please email any comments on this to me, or add comments to the appropriate subpage. I will keep this DEP up to date based on comments and suggestions received. I would also welcome extensions or counter proposals.

Events Improvement

Drupal has a many event organizing modules avaliable which provide a range of functionality including scheduling, invitations, rsvp, and volunteer management. Our goal is to bring together the various module developers, funders, and site builders to work towards better integration and general improvements in event management modules.

The goal is three-fold:

  • Develop API's to integrate modules and create new functionality
  • Establish best practices and documentation
  • Fundraise to support development efforts
How this project is organized
Who's involved

Please add yourself to this list and a brief description of what events related work you have done so far and how you will be involved in this effort. If you don't have permission leave a comment and we will add you.

Areas of improvement

There are a number of different areas of improvement to events and event management that are covered in this proprosal. Each area of work will have its own subpage.

Improving And Simplifying Time Zone Support

Time zone support has been the bane of many developers who've worked with events and other time and date related information in Drupal. To make matters worse, the way time is handled in Core is different from (and not completely compatible with) the approach taken by the Event module.

Since there are a number of competing solutions that have been proposed, it makes sense to start with a survey of use cases where people care about time zones, a list of common problems, and a short critique of some of the things that have been tried up until now. I encourage anyone with a strong opinion about a particular solution (con or pro) to attach a child page to make your case.

Definitions Of Terms
While most or all of us have lived in places that observe "Day Light Savings Time" (DST) or "Summer Time", as it is called in parts of Europe, it's useful to define some terms that are commonly used in time and date libraries a bit more precisely to prevent common misunderstandings. Here are a few:

  • UTC: Coordinated Universal Time. I'm assuming that the order comes from the French. In any case, this is the CCITT's term for what people often call "GMT", i.e., the time in the UK with no change for summer time. Since the UK actually does observe summer time, the local time in Greenwich can be an hour off from GMT. Go figure.
  • Offset: The difference in seconds between local time and UTC. The offset is greater than 0 for zones to the east of Greenwich, and less than 0 for zones to the west of it. Offsets do not have be full hours. Parts of Australia and Canada, for example, are in zones that are a some number of hours and a half off from UTC. A few zones are 15 minutes off of a full hour.
  • Unix Time Stamp: the number of seconds since the Epoch, which is defined as 12AM on January 1st, 1970 UTC. Time stamps are always for UTC, by definition, and the standard time libraries always treat them this way. More on this point later.
  • Transition Rules: a "rule" that defines when to move from standard time to summer time ("2 AM local time on the last Sunday of the month of March"). Every country has to have their own damn rules.
  • Transition Time: the time, expressed as a Unix Time Stamp, when a time zone changes between standard time and summer time. The most commonly used time library, based upon code maintained by Arthur Olson of the US National Institute of Health, works by compiling Transition Rules into binary files containing arrays of transition times.
  • Time Zone: a time zone is a geographic area with a base offset from UTC, which may or may not have transition rules defined for DST. If a zone uses the same offsets but different transition rules, time zone libraries treat it as a distinct zone.
  • POSIX Time Zone Name: This is a string that can be used for the TZ environment variable on most Unix systems. Usually, it's of a form like "America/Los_Angeles". The Olson database has more than 500 different zone names.

These are a lot of definitions, but to discuss this, it helps in comparing different solutions to know how well they handle local time for different countries and localities.

We'll look at different aspects of the problem, including use cases, common problems and bugs, and some different solutions.

Common Use Cases

This is a list of some common use cases that I've seen coming up with time zones in Drupal. There are of course lots more, and anyone who wants to should pile 'em on.

There are 3 main use cases:

  1. Server And Users In the Same Region
  2. Users Come From Many Different Localities, But Generally Want to Post Events In Their Own Local Time.
  3. Users Come From Many Different Localities, But Need To Coordinate Between One Another

I discuss some of the complications and UI problems below.

Server And Users In the Same Region: This is the most common case -- your server has a clearly definable value for "local time", and most of your users also use it. All events are assumed to be in that local time. There are two important sub cases:

  • The local time zone does not observe DST.
  • The local time zone observes DST

The first case is very simple, and Drupal has always handled it well. The second has had a number of bugs that keep popping up:

  • A user is putting in future events for the Spring during February. In this case, in the Northern Hemisphere, during February local time is standard time, but for events in April and later, local time is an hour ahead. Drupal until recently made it very difficult to get the time right in this case.

Users Come From Many Different Localities, But Generally Want to Post Events In Their Own Local Time: This is the typical case in a national political campaign, where events are in "real space", so that people who go to the event will know what local time is. In this case, you want to keep the time zone of an event together with an event. This is less common than the first case, but still pretty common in applications many of us build. Events in this case need UI to set the local time zone.

Users Come From Many Different Localities, But Need To Coordinate Between One Another: This affects groups like us: when we call a conference on IRC, we all need to know when this is in our own local time. This case is a little exotic, but since most of us are developers around here, it's an important one. In this case, each individual user needs UI to set their own time zone, and it's useful to see the time of events in your own local time.

I'm sure there are lots more cases, but these are the ones that come up the most in the Event and related modules.

Design Considerations For Time Zone Libraries And UI

Some considerations to look at in comparing different solutions to handling time zones in Drupal:

  • Correctness: People really like it when the time set for an event is correct for their time zone, especially when their time zone observes DST. Since there are literally hundreds of different zones with different rules for going between standard time and daylight savings time/summer time, this means allowing people to choose the right zone.
  • Speed: Badly implemented time zone code can be very slow. Code size is important. Caching is usually necessary.
  • Simplicity of UI: This one can be really hard, and it's very hard to do it right, since getting people to somehow pick the right zone out of 500 or more is pretty daunting. Anything you can do to reduce the number of choices is really valuable. Picking from a map can help. Also: if you already know the user's country, a simple JavaScript test can reduce the number of choices to only one or two. And even if you don't know the user's country: the same JavaScript test (offset from UTC in January, offset from UTC in July) will work on all versions of javaScript you'll ever see, and can reduce the number of meaningful choices to around 10 even in the worse of cases.
  • Easy of Maintenance: Time zone data changes frequently; Olson of NIH comes up with updates of data at least once a year, and frequently more than once a year. Usually updates to any OS have to change the time zone tables. You want to make sure that you can update your libraries quickly, and with as little human intervention as possible. You also need to be able to debug them without going nuts.

There are certainly more than this, but this is a good basis for comparison.

Proposed Solutions For Core

We're likely to spend a fair amount of time discussing different solutions. One is the code now used in the Event module (event_timezones.inc), which covers some of the issues here, and is much more correct a solution than what I've seen done till now. But the best is the enemy of the good :-) So I propose a solution based upon Arthur Olson's data files, which you can find at ftp://elsie.nci.nih.gov/pub/. It works like this:

  • Use a program that's been posted to Olson's mailing list by John Cowan, which dumps the TZif binary files on your system (or that you can compile from Olson's source distribution) into an easy-to-parse text form.
  • Use a script to call Cowan's program for all of the zones you want to support, parse the data, and generate SQL tables that contain the data.
  • Load only the zones you need at run time, and cache the data in a form that makes it easy to find the right transition times by binary search.
  • Wrap the PHP gmt time functions so that you can specify the POSIX zone name, and look up the cached zone table. Store all times as Unix time stamps, and hide any games that you play with offsets inside of the wrappers. Developers only need to specify the right time zone to use, and don't need to play with offsets from UTC anymore. Users get their dates properly rendered for their notion of local time, and stop bothering the developers. Lions lie down with lambs, etc. etc.

I've already implemented most of this. While the solution covers more than 500 zones, the code is extremely small.

You can see my code up on my personal site, at http://torenware.com/bazaar/trunk/timezones/php_datetime/. The contents of the drupal/ directory contain the include file with the code, and a simple module to test it with. Just load the sql tables and the sql data, and you can try out the functions.

Enjoy.

Timezones and Daylight Savings Time

Better support for timezones and how to handle daylight savings time is a big issue. This is the stub page for this area of work... please add comments or subpages as needed. Just gathering a collection of links to relevent issues and forum posts about this would be a good start if anyone wants to help.

Thanks!

Unified support for managing relationships between users and events
The current state of affairs

Currently, there are a number of different contrib modules that manage the relationships between users and events:

Kieran made a nice table to compare event management modules

Unforunately, none of these modules works with the others, they all support slightly different means of representing the relationship between a user and the event, they all have slightly different features, and they're all (mostly) duplicate effort.

Our proposal

What we need is a unifying API that sits under all of these contrib modules that ties them together. In all cases, we just need to record the fact that there's a relationship between a given user (uid) and a given event (nid), and then store metadata about that relationship. Each contrib module can implement its own way to record a relationship (signup will create a form on the node view page, RSVP will use email, etc), and each one can implement its own way to display information about the relationship as it sees fit. But, at least most of the duplicate effort can be refactored into a common place, and all of the different methods for managing event attendance can work together (so if you reply to an RSVP email, your response will show up whenever the signup module is displaying attendance about an event).

Furthermore, the metadata that goes with the relationships must be flexible and admin-configurable. Different sites and different use cases demand different metadata. It's therefore a bad idea to attempt to make everyone agree on the same metadata and build that into the fundamental API and DB schema. However, there are some things that are fundamental to the relationship, which should be handled by this unifying API.

As an initial proposal, here's what I think this DB table needs to look like:

CREATE TABLE event_reply (
  uid int(10) unsigned NOT NULL default '0',
  nid int(10) unsigned NOT NULL default '0',
  reply_time int(10) unsigned NOT NULL default '0',
  reply int(2) unsigned NOT NULL default '0',
  UNIQUE KEY uid_nid (uid,nid)
);

I think all modules either already care (or should care) about when a user created their relationship to a given event (the "reply_time" field). I also believe all modules must care about what kind of relationship the user indicated. Support for yes, no, maybe, etc is very important for trying to manage attendance at any event. Certain modules attempt to provide functionality like noticing conflicts in a schedule (if you reply to multiple events happening at the same time and say you're coming to both), providing attendance limits, or even something basic like a printable attendance list. In all cases, the fundamental information about the reply is that a) the user replied at all and b) indicated whether they're planning to come or not. I believe a 2 digit int is better than a 1 bit bool, since it allows replies other than yes vs. no. At my site, I use a number from 0 to 10 (0 = no, 10 = definitely yes, anything else is something in between) and it seems to work great. For modules that don't care, or sites that don't care, they can only use 0 or 10, but it seems worth including something like this in the "core" event management API so that all contrib modules can agree on it and support it.

Other work to consider

Someone pointed out the User Related Content module. This might be a good basis for some of this user - event relationship management. I'll have to investigate that module and see if it's a suitable foundation, if there's chance of collaboration on that, etc.

There's a lot more to say, this is just an initial stab at generating some discussion and getting my ideas down.
-Derek

IRC discussion between dww and hunmonk

Chad (hunmonk) and Derek (dww) just had a nice discussion about this page. We generated a lot of new ideas. I'm posting the highlights here for further comment...

hunmonk: but i really don't know if the solution you proposed is workable
Derek: why not?
Derek: because each signup-related module is going to have diff metadata?
hunmonk: i think each module can have it's own kind of relationship
Derek: how so?
hunmonk: so you can't just say "this user is associated w/ this node"
hunmonk: or maybe i'm missing something
Derek: i'm proposing to think of it as "this user has a relationship to this node", where there are certain characteristics that are common to all kinds of relationships (the stuff in my event_reply table... needs a better name) and certain characteristics which can be module-specific (stored in another table, also keyed by uid + nid)
Derek: anything in the common table is shared, unified functionality across all such modules
Derek: i.e. the fact they replied at all, when they replied, and their basic response.
hunmonk: well there isn't much shared if both tables are keyed by uid/nid :)
hunmonk: plus, even times can be different, can't they? and status?
hunmonk: i don't see how this actually unifies anything
Derek: so, if you reply via an RSVP email, once the key fields go into event_reply, then the signup module would know about it, and would be able to display it on the event nodes, user profile, my fancy "user reply grid" (which you haven't seen yet), etc.
Derek: i think if you first reply via RSVP email, then change your status via signup, the entry in the shared table would be updated to reflect your most recent reply.
Derek: that's why it should be a shared, common table (imho)
Derek: (we could get crazy and get into the buisness of revisions for this, something i've wanted on my site... but that's another story)
Derek: even if it's all via signup, it'd be nice to see the initial reply, and then any modifications to the reply...
hunmonk: okay, so for workflows where mods want to work together, this would be helpful
Derek: yeah, that's one of the points... i think it's lame that if there are N different ways to say the same thing, you have to just choose 1 possible method and force everyone to use that.
Derek: or else none of the responses can be used by the event scheduler/organizer in a unified way.
hunmonk: i'm wondering about use cases where they might not work together, ie a user creating two different relationships to the node via two seperate mods
Derek: i maintain (perhaps incorrectly) that a user only has *one* possible relationship to a given event at any given point in time.
hunmonk: maybe.
Derek: if they change it, that's a new revision, sure, but there's no such thing as "i RSVP'ed maybe, but i signed up yes"...
Derek: if you did that, i think your *current* relationship should be "yes, as of [date_stamp]"
hunmonk: if we did revisions, i think we might want a different table structure
hunmonk: hm
Derek: yeah, i thought of revisions after i wrote that stuff...
hunmonk: event management is the _worst_.. :)
hunmonk: well, calendaring is the worst
Derek: that only came to me last weekend in the course of a 6 gigs in 3 days period where people kept changing their status which was driving me nuts...
Derek: i wanted a historical record of exactly who said what, when, who changed their mind, etc, etc, so i could do some historical blame-assignment. ;)
hunmonk: i actually won't make reply an int
hunmonk: just make it text
Derek: yeah, i was thinking of that.
hunmonk: if you want flexibility, that will give you more
Derek: i happen to dig my crazy mapping scheme, but i can see why not everyone would agree.
hunmonk: um
Derek: i suppose if it was text, i could do the reverse mapping in my own custom module to assign values and then provide these "attendance estimate" things.
hunmonk: so
Derek: however, i'm wondering about stuff like conflict resolution...
Derek: if it's just free-form text, it's harder to enforce any logic about comparing replies for the purpose of finding conflicts.
Derek: if there's an agreed-upon mapping of replies to what they mean, then there could be a conflict resolution module that knew what it was doing...
hunmonk: well it wouldn't be free form--you'd have states
...
hunmonk: events are nodes
hunmonk: wouldn't we be able to write the revision info?
hunmonk: meaning, use node revisions to track the history of the relationship
Derek: re: node revisions to track history... that'd only be if the *relationship* was itself a node (which might be a good idea)
hunmonk: yep
Derek: assuming there was a good way to view that node within other nodes (e.g. a table of "signups" when looking at a given event node.
Derek: yeah, if the relationship itself was a node, then we'd get all kinds of goodness for free...
Derek: it could be CCK, allowing people to easily define whatever metadata about the relationship they wanted
Derek: we'd get revisions for free
Derek: we'd get timestamps for free
hunmonk: so that feels like we're onto something
hunmonk: so that table would become "relate this event node to this relationship node to this user"
Derek: but, i'm a little leary of the viewing costs of such an approach...
Derek: but i guess that's where views come in. ;)
Derek: (i haven't played with those at all yet)
hunmonk: i haven't either
Derek: i also don't know how this fits in with broader node<->node relationship work...
hunmonk: unless there's a clear winner
hunmonk: i say we just pick an implementation and go with it :)
Derek: but, for example, i think it'd be slick as hell if there was a CCK field type for "relationship to another node" which allowed a flexible way to say what parts of that other node are displayed within the parent node...
Derek: currently, there's just a "make a link to some other node" field
Derek: which is better than nothing, but a little limited
Derek: and, it just assumes the "relationship" is a link, not itself a whole node full of metadata and other goodness
hunmonk: adrian mentioned per field permissions at one point as a CCK wish list item
hunmonk: hm. relationships as nodes.....
Derek: (should we move that part to #devel?)
hunmonk: i wonder if anyone has done that before
Derek: (get wisdom from others?)
...
Derek: let's unwind the stack...
Derek: replies as text, but not free-form...
Derek: what do you mean?
Derek: why is that better/different than ints (assuming we allow site admins to theme strings to go with the ints)
hunmonk: well
hunmonk: i see it as the drupal way of doing things for one
hunmonk: but in addition
hunmonk: i think it makes the code more readable
hunmonk: and i guess
hunmonk: it just seems to me that you get a good combination of flexibility and consistency, w/o some huge map of "1 means this, 2 means that..."
hunmonk: i'd rather see "confirmed", "unsure", etc
Derek: well, if it's truly flexible (admins can change the text however they want), then i don't see how any modules can agree on what the replies *mean*...
hunmonk: and i think any mod that wants to go outside the ten or so standard states can do so easily
Derek: (i personally don't care about scheduling conflict resolution stuff, but clearly you did when you wrote signup).
hunmonk: no, i didn't. i wrote that specifically for the vancouver conference site :)
Derek: ahh. ;)
hunmonk: ok
hunmonk: i see what you're saying
Derek: well, i can see why some people care about this as an important feature in scheduling functionality.
hunmonk: yeah
hunmonk: hm
Derek: so if i'm going to be blazing the field with a new, unified approach, i'd like to have a good answer for it, too...
hunmonk: i wasn't thinking that the states were admin determined
hunmonk: just text declared :)
Derek: i'm happy to go to 0-100, to allow more flexibility, but that just seems crazy.
hunmonk: still module determined, at least for the basic statets
Derek: also, keep in mind, i'm just talking about the 1 particular field that means "are you planning to be there?"
Derek: i can't honestly see how anyone needs more than 10 shades of grey for that. ;)
hunmonk: edge cases
hunmonk: it seems they always come up
Derek: all the other possible mojo about your relationship goes into other fields in your relationship node.
hunmonk: you brought some up you should know! :)
Derek: explain. ;)
hunmonk: there was some crazy thing you were wanting. i can't recall what it was
Derek: i see 1 edge as "i'm definitely not there", the other edge as "to the best of my knowledge, i'm definitely there", and having a few values in between for the wishy-washy people in the middle.
hunmonk: averaging out replies or something?
Derek: yeah, that's all accomplished by 0-10
Derek: for me, 0 = no, 10 = yes, 5 = maybe, etc.
Derek: and i just add this # up for all replies, and divide by 10
hunmonk: i'm not sure how many people would actually use that. hence the edge case designation :)
Derek: think of it as really being on a scale from 0 to 1 (maybe = 0.5), etc....
hunmonk: you're the only person who even suggested it
Derek: well, right, i don't know that people really need to use my "attendance estimate" average stuff...
Derek: however, i can definitely see value in 10 (err, 11) unique values a user can reply indicating the degree of attendance
Derek: then, for example, event_reply_conflicts.module can say if the reply is > [some config setting] i'll mark it as a conflict
hunmonk: sometimes this stuff makes my brain hurt :)
Derek: so, 0 is definitely not a conflict, and maybe 0-3 isn't, but anything 4-10 is considered a conflict
Derek: so, my int mapping scheme seems to solve (at least) two problems. ;)
hunmonk: as long as the only thing it represents about the attendance state is degree of certainty
Derek: plus, it'd be a way for the various modules to all speak the same language for this stuff... so if you replied "probably not" in the RSVP email, that'd still display as "probably not" in the signup table for the node.
hunmonk: i'm not sure if that's the only dimension we want to cover for attendance states
Derek: yeah, that's what i had in mind... attendance_certainty or something.
Derek: (should probably rename the field in the schema, if we can agree on this)
hunmonk: i don't agree
hunmonk: i think it's to specific
Derek: not to be a pain, but what's your alternative proposal, then? ;)
hunmonk: reply_state
hunmonk: textfield
hunmonk: look at project module
Derek: reply_state is fine. (i just said "reply" before, but sure, state is better).
hunmonk: you'd think it would be easy enough to come up w/ a standard list of issue states
hunmonk: but we just added three new ones not that long ago
Derek: right, but we're back to the orig problem... if there's no underlying mapping to something that can be handled in code, there's no way to, for example, do meaningful conflict resolution.
Derek: project issues are different...
Derek: they can be in a bunch of states, and those states have meaning
Derek: but those meanings are only used for selecting queries on the db to display the data
hunmonk: ok
hunmonk: so here's what i propose
hunmonk: it's a textfield
hunmonk: you have certain "standard" states that are documented
hunmonk: modules can talk between themselves using those
hunmonk: and you can still leave open the option of a module adding a new state if necessary
Derek: bold claim: the things you want to be able to add as new possible states are things that belong as new fields in the relationship metadata node, not as new possible values in the "reply_state" field.
hunmonk: perhaps
Derek: but, let's get concrete... got any examples of states people might want to set?
hunmonk: standard ones?
Derek: no, non-standard
hunmonk: hunmonk puts on creativity cap
•: "rain delay" :)
•: lol
Derek: here's the code from my custom version of signup.module i'm using: /** * Returns a string corresponding to the user's signup reply value * @ingroup signup_internal */ function _signup_reply_str($value) { switch ($value) { case '0' : return 'No'; break; case '3' : return 'Probably No'; break; case '5' : return 'Maybe'; break; case '7' : return 'Probably'; break; case '10' : return 'Yes'; break; default : return 'Unrecognized'; break; } }
Derek: (arg, that should have been more nicely formatted, sorry)
Derek: (it looks better in the code, i promise!) ;)
Derek: i don't even bother using all 11 states, since i don't really need them, but the math of 0-10 works so nicely, i decided to go for a sparse array...
hunmonk: i'm guessing there will be all kinds of creative ways that people will find to call something an 'event'
hunmonk: in which case standard states may not apply
hunmonk: i think that's the crux of it
Derek: (sort of off topic: i've flirted with the idea of having per-user mappings of these strings to ints, so that when the more unreliable people in my band say "probably", i only count that as a 3, not a 7, but that's another story...)
hunmonk: sheesh
hunmonk: see, the levels of complexity can be infinite :)
Derek: well, if they're not trying to schedule events, and they're trying to use our crap just as a way to do user->node relationships, they should just use a generic user->node solution, not all this (infinitely complex) crap. ;)
Derek: yeah, i know, it's crazy... i'm nuts about this stuff from years of dealing with flakey people in my band. ;)
hunmonk: no, i really mean that things can be seen as an event
Derek: (you might very well be onto something, but until you can produce an example for me to wrap my head around, i'm going to stick to my original position). ;)
hunmonk: what if people called a thunderstorm an event?
Derek: ahah! an example... phew!
hunmonk: that's certainly different than a rock concert :)
Derek: very true.
Derek: however, what does it mean that you're going to "signup" or "RSVP" for a thunderstorm?
hunmonk: anything that 'occurs' can be thought of as an event, really
Derek: true, but not anything that occurs can be scheduled.
hunmonk: that you're coming to my house to watch it?
Derek: then *that's* the event.
Derek: and that fits my model exactly.
Derek: it has a place, a time, and you have 0-10 possible "states" of if you're planning to attend.
hunmonk: okay
Derek: it's exactly like a rock concert. ;)
Derek: lol
hunmonk: how about signing up to take readings on it?
Derek: same deal: are you planning to go take measurements or not?
Derek: whole issue of trying to schedule events without a fixed time is a related question, that's the bigger issue i think you're trying to get at.
Derek: but that's just a whole new set of problems.
Derek: and i think that's more in the all-users-have-their-own-schedule, conflict resolution, voting on the best date/time kinda stuff.
hunmonk: if you go w/ attendance_certainty as an int, i'll bet you that we hit a limitation somewhere
hunmonk: but maybe it's enough for a start
Derek: call it reply_state if you like. ;) i just called it "reply" in my orig proposal.
hunmonk: i'd rather see it as a textfield
Derek: you've said that at least 3 times now, and each time you didn't have an answer to my concerns about that approach. ;)
hunmonk: i certainly did!
hunmonk: i said "documented standard states"
Derek: which users can't change?
hunmonk: that would allow modules to communicate at the code level
Derek: not in translation
hunmonk: ?
Derek: ints work better... let the code deal in ints (constants)
Derek: let the translation/theme folks worry about the strings displayed....
Derek: e.g. i can't strcmp("yes", $reply) if the email comes in with "si"
Derek: but, if the email gets processed and encoded as 10, then the code can compare $reply and 10, no sweat...
hunmonk: well
Derek: (i don't fully understand how drupal translation stuff works, but see what i'm getting at?)
hunmonk: well i'm not all that crazy about admins defining states
hunmonk: hm
hunmonk: hell i don't know
hunmonk: i need to think about it more
Derek: fair enough. i don't claim to have all the answers...
hunmonk: i think project uses int, and maps them to text strings
hunmonk: if you want user or admin created states, then i suppose int is the way to go
hunmonk: but we should get other opinions on this
hunmonk: i think we've made our mutual points clear enough
Derek: agreed, more opinions would be nice.

Workflow Proposals

We want to be able to provide a seamless user experience across the common functionality of event organizing in addition to tight integration of modules over API's. To do this we need to do interaction design for event modules across the domain of functionality. Workflow designs for event modules are posted as subpages here.

Workflow for event creation, invite, and rsvp

This workflow proposal was created as part of the "GoJoinGo" Group/Events development effort.

CivicSpace: Workflow event creation, searching, invitation, and rsvp

This diagram is a proposal for interaction design for a Drupal event system that can handle event creation, searching, invite, and rsvp.

  • The bubbles represent interface screens
  • The arrows represent paths the user can take through the different event components.
  • Each bubble links to a mockup of the interface designed in a way to lead the user through the different paths they can take when using the event system.

Can I party as well?

Folks,

Could you add me to the crew? I have a jones to begin coding....

Next generation event management based on views and the content creation kit

This is a stub to discuss next generation event modules based on views and content creation kit.

Use cases and motivation

This page will contain information about all the use cases and motiviation that's leading to this effort to improve event management in Drupal. Anyone with their own ideas and examples should create a subpage from here.

IRC Meetings

We periodically hold IRC discussions to plan event development work.

3/6/06 IRC Meeting: Re-Thinking Events in Drupal

Right now, Drupal has a plethora of available event options available, which range from modules which handle scheduling, to those which record attendance, to huge mega-modules that intend to do it all. This was also a topic of discussion at DrupalCon.

I'd like to setup a meeting on IRC Monday, March 6 at 12 PM PST (which is 3PM EST, 8PM GMT) in #drupal-events, to bring funders, developers, and site builders together to improve event management in Drupal.

The goal is three-fold:

  1. Finding funding
  2. Developing APIs and adding functionality
  3. Establishing best practices and documentation

There are lots of people thinking, developing, funding, and otherwise contributing in this space, including:

So let's get together and combine our efforts, and help make event management in Drupal shine!

Meeting Notes

Thank you very much to all who attended! The following is a summary of points raised:

  • Use the relationships API to keep track of relationships between users and events. This can then be used to track sign-ups, RSVPs, volunteers, possibly resources, and eliminate/reduce the need for these additional modules.
  • Capitalize on the work that's gone into CCK, by making 'events' a date/time field in CCK that can then be used to expand any type of node.
  • Use Views to tie in 'front-end' components to those on the 'back-end'.
  • Events need to be easier to theme. We also need a way to make add-ons to the event module. Killes welcomes any patches to the event patch queue. ;)
  • We need to draft a series of use cases, to show what events should do from the user's perspective. CivicSpace Labs/CivicActions already have done some work in this area.
  • Timezones are an issue -- a standard approach, such as the one used in events_timezone.inc, should be part of Drupal core.
  • Integration with Send module to handle invitations/responses via e-mail.
  • Generally, smaller, more focused modules is the desired approach, over huge 'do-it-all' modules, but it also needs to be easy for users to get something 'quick and easy' to satisfy certain common use-cases.

I've also uploaded a full log of the conversation (minus people entering/exiting) here:
http://drupal.org/files/issues/drupal-events.log

And a placeholder for the forth-coming DEP (Drupal Enhancement Proposal) can be found here:
http://drupal.org/node/52884

The next goals are:

1. Defining a DEP to describe our overall goals
2. Creating use cases to cover types of functionality users are looking for
3. Identify code that needs to be written vs. code that can be re-used
4. From the above, create a road-map document to illustrate the 'grand master plan' for Drupal events.

We need people to step up and volunteer to "own" some of these items. If you would like to help out, please post back here or feel free to contact me via my contact page! I'll be contacting those who already offered as well (and thanks!).

We will have a follow-up meeting to check progress on these issues within a couple weeks' time (I'm thinking around Tuesday, March 21 at a time that enables Australians to attend)

Events DEP Stub

Title : Event Improvements
Abstract:
Author: Event Special Interest Group
Dependencies:
Repository:
Status: planning stages

1. Introduction :
Keep it short

2. Motivation :

3. Approach :

4. Security considerations: (optional, if it involves security related issues)

5. Excluding : (optional, what will NOT be covered in this DEP)

Everything is a node

  • Users, comments, taxonomy terms and attachments become nodes
  • Why do this:
    • Simplicity, consistency and code reuse
    • Allow us to extend these content types as we do for nodes, without rewriting code
    • Allow relationships between these, without needing a mapping table
    • Why users:
      • Use CCK and taxonomy instead of profile.module
      • Use workflow for user signup/promotion
      • Location module does not need to deal with users separately, just location enable the users node type
      • Users birthdate can just be an event field. Birthdays are just repeating events
    • Why comments:
      • Use workflow to moderate comments
      • Use taxonomy to classify comments
      • Use relationships for threading
    • Why taxonomy:
      • The taxonomy builder becomes a streamlined interface to both build a tree of terms (nodes) and select an item from that tree to classify a node
      • Both the tree and each classification can be designated using relationships
      • All the taxonomy storage and retrieval functions can be encapsulated by the relationship API
    • Why attachments:
      • Less confusion about when to attach media directly to a node and when to add it using a media module node
      • The current attachment interface could then be a quick and easy UI to view/add/update attachments - uploading and adding attachment nodes, and relationships to those nodes (from the original node) on the fly. Expanding title/description fields that update the attached node could also be provided
      • A unified view of all attachments, whether they are added using the standard attachments control, or a more sophisticated media module
      • Potential to write generic gallery modules that don't need to understand different media module node types
  • Each of these already has a well defined APIs, meaning that changing the backend storage to nodes should not be overly hard
  • Existing API code would generally move to node hooks, and the old API functions would just become wrappers
  • Modules that are directly accessing affected tables (most commonly the users table) will be broken by this change, but if their own uid columns are updated things should still work fine, as user metadata would still be stored in the users table as it is now (the only duplication of data might be user name - stored in both the node title and the user table)
  • What will be significantly harder will be to upgrade existing sites, as all the id's will change.
    It should be possible to use some mapping to work around this:
    • Find the highest 'old-style' id in existence.
    • Ensure that the next nid is higher than this (bump it up if it is not). This value becomes the threshold.
    • Create nodes for each object, adding their old and new id's to a mapping table.
    • Update all fields in existing tables to use the new id's for each object (this could also be done automatically for contrib module tables, assuming that uid fields are for users etc).
    • When a request is made for an id below the threshold, map it transparently to the new id. This could be done at the function level for greater compatability, or (more simply) at the URL aliasing level.
    • This functionality could be wrapped in a module, which could be disabled for new sites as it is not needed.
  • Some notes on implementation:
    • As far as I am concerned, all the existing user interfaces and APIs should stay exactly the same. We have put a lot of work into making these as intuitive as possible and this is very important. Here is an example of one way this could look, using the user module as an example:
  • To avoid namespace conflicts with hook_load (renaming API functions like user_load would be nasty) the job of declaring, loading and saving these nodes could be handled by an external module (e.g. node_helper).
  • The current user_load() function would be renamed _user_load).
  • A new function user_load() (to keep the user API the same) would then just return node_load().
  • During the node load the node_helper module would call _user_load() and add it to the node object. (it could call "_$node->type_load" in fact)
  • The same thing would happen with user_save
  • Comments, taxonomy terms and attachments could get the same treatment.
  • The API and the database structure are identical, so other modules do not even need to know about the change. Optionally, node access could be used to hide the user node from users on the regular node pages, so this change is completely invisible.
  • By doing this however we have unified system ids - a uid is now also a nid, and so these can easily be related and categorised without needing ugly mapping tables/fields.
  • Even better, users can now be extended as nodes, rather than being a special case. The many modules that extend nodes can now interoperate with users and reduce help duplicate code.
  • Potential DEP Dependencies:
    • Relationship API. A lot of the benefits/simplicity of taxonomy and comments becoming nodes comes from the ability of relationships to provide a standardised way to store the node-relation metadata.

hook_roles($user)

Title : hook_roles($user)
Abstract: Add roles to array returned by $user->roles
Author: Somebody
Dependencies: none
Repository:
Status: Proposal

1. Introduction :

A hook_roles hook would add role IDs to the array of roles returned by the
existing Drupal operation: $user->roles.

2. Motivation :

I have a module I developed call og_user_roles: http://drupal.org/node/87679.
What this module does is assign roles to OG Group Members that are specific
to the group the member is in at the time the requested function is called.
Right now, for this to work, I have to hack the user_access function in
the user.module.

3. Approach :

Module developers would create a function in their module titled
"my_module_name_roles($user)" and this function would return the IDs of
additional roles that the module has determined the user should have
at the time $user->roles is called. These additional roles are added
to the results of $user->roles.

4. Security considerations: (optional, if it involves security related issues)

None that I can think of. Again, it would be the responsibility of the
hook code to return the additional roles the user should have based
upon the context it determines.

5. Excluding : (optional, what will NOT be covered in this DEP)

Links exchange checking module

Linking exchange - is a usual thing when making SEO. You always need to check whether your partner didn't remove your link from his Links page. In order not to perform such operation manually I propose the following solution to do it automatically:

1. Create a new node type "link" with necessary fields using any content enging (Flexinode, CCK). Each link will represent a link to a partner's page with all necessary info - description , logo, URL, title, etc. One of the field will be Reciprocal URL - an URL of the partern's page where our link should be.
2. This module will do 2 main things:
- displays a list of URLs using node_list and pager, outputs a block, etc. (can be implemented with Views module)
- automatically check for presence of all reciprocal links and outputs a report if any links were removed from partenrs' pages.

There is a now a working group for this on groups.drupal.org.

Mapping API Generalization

Title : Mapping API (map.module)
Abstract: A generalized mapping API
Author: webgeer
Repository: TBA
Status: Planning Stage

1. Introduction :

This will be a new module that will have a generalized mapping API for displaying manipulating and using maps within Drupal. Different types of maps (google maps, yahoo maps, open source mapping projects etc) would be implemented with specific .inc files for each type. Plug-ins will be used for a variety of mapping parsing inputs and outputs to allow gathering map data from other websites or sources, or to provide map feeds for other websites or applications.

2. Motivation :

There are a few separate modules that do some mapping. However, these are not well coordinated and create some conflicts with each other. This will make developing mapping information into drupal sites much easier and have a consistant approach.

3. Approach :

The map module would have generalized calls in the form of map_draw($mapvar) which would then call the applicable function from the include file based on the site administrators preferred map type (i.e. google_map_draw($mapvar) ). Different interfaces would have differences in exactly how it is implemented and what capabilities are available, but in general if the same $mapvar is used for yahoo_map_draw() or google_map_draw(), it should return roughly the same map with the same overlays. The $mapvar would be a standardized variable type. The map.module would also have some generalized map manipulation functions. Details on the specific map.module API functions and the $mapvar definitions can be found on the [link to subpage on mapping API]

The module will also have the ability to parse map data from files or external websites based on formats defined by plug-in files. It will also be able to output data from map information in drupal in various formats. Details on the plug-ins for inputting and outputting map files are available on the [link to subpage on map formats].

5. Excluding :

The base module should not require the installation of any other modules or tables. It will not include functionality for geolocating based on address.

Please email any comments on this to me, or add comments to the appropriate subpage. I will keep this DEP up to date based on comments and suggestions received.

Multilanguage support in Drupal core: i18n2core

Title : Multilanguage support in Drupal core: i18n2core
Abstract: Add support for basic multilingual content into Drupal core
Author: Jose A Reyero
Dependencies: locale module
Repository: Patches will be post to Drupal queue.
Status: WIP

1. Introduction

This will be a step by step effort to add some basic support for multilingual objects and content into Drupal core that will be achieved by some general patches and an additional lightweight module.
It will be based on current i18n module, implementing only some features which are basically language selection conditions and language attribute for some objects. The module proposed will be a trimmed down rewritten version of i18n [name of the module to be defined].

2. Motivation

While it is possible to achieve full multilanguage with Drupal + i18n module, the tight integration required by these features makes it difficult to implement it cleanly with a contributed module. In addition to that, some basic multilanguage support must be implemented at a very low level allowing the rest of Drupal and contributed modules to take advantage of these features without reimplementing them.

3. Approach

The features that will possibly need some core patching are:

  • Language management and selection. Language will be kept in path so some rework of the path processing and path aliasing mechanism will be needed.
  • Some minor changes for locale.module to reuse language definition and integration of the language selection.
  • Multilingual variables. This feature will need some changes in variable_get/set/init functions.

Implemented by i18n module:

  • Language selection block. This could also be included in locale module
  • Query rewriting applying language conditions. Some switching mechanism can be provided to enable/disable language conditions, i.e. for administration tasks.
  • Management of language field for the objects below using Forms API
  • Some basic api for language management and producing links for different languages
  • Some confgurable options for general language management

The list of objects that may be language dependent, thus may need a language attribute are:

  • Nodes. Option to enable language for a content type. [i18n]
  • Taxonomy vocabularies and terms. [i18n]
  • Path aliases. We should be able to define aliases for only one language or for all languages.
  • Menu items. Simple language field + query rewriting
  • Blocks. Simple language field + query rewriting
5. Excluding

The translation related features will not be included in this DEP. We'll only take care that additional data to keep translation relationships fits nicely in the proposed data model but all the translation interface -which may be quite big and complex- and translation links will be better for now in a contributed module.
Language icons. Including a complete set of language icons could make drupal tar.gz file much bigger and it seems more interesting to have icon sets provided as part of optional contributed modules. So better than including all this images in Drupal core it seems more reasonable to provide some plug-in support for collections of language icons.

Patch list

Note: This page is outdated. Now we have a drupal.org project for the follow up, http://drupal.org/project/corei18n

This will be maintanined as a set of core patches. List of open issues related with i18n patches in queue follows:

Use cases: site set ups

We may organize the stuff in modules following this groups of features.

  • Language-less site: content and interface in default language.
    This is a site in English with no more languages. No localization, no languages, nothing at all.
    Just default language -always 'en'- to show in HTML page headers.
  • Single language site
    Localization enabled: English with string translation or a site in any other language
    The only thing that needs configuration is which language to choose.
    Note: Why not translate English strings too when locale is enabled?
  • Multilingual interface, single language for content
    Localization enabled
    Language selection for registered users: Language in user profile
    Language selection for anonymous users: Language switcher block
    Multilingual variables
  • Full multilingual site
    Localization + Multilingual content enabled
    Language selection for all
    Content selection depending on language

Relationship API

  • Standard set of hooks to allow modules to express relationships and query them
  • Requirements (based on discussions at OSCMS Vancouver 2006):
    • Pluggable frontends (ways of entering and viewing relationships). Experience has shown that there are many different ways relationships can be created and presented to users. See the 'Relationship UI' child page for a simple frontend.
    • Pluggable backends (ways of storing/retrieving or deriving relationship data). Experience has shown that some modules will need direct control of the relationship storage space. See the 'Network Relationships', 'Hierachical Relationships' and 'CiviCRM Relationships' child pages for example backends.
    • Extensible
    • Works with both 1-1 and 1-many relationships
    • Object neutral - can relate nodes, users, comments, relationships, URIs
    • Can retrieve direct relationships (e.g. friend - L1) as well as as indirect relationships (e.g. friend-of-a-friend - L2, friend-of-a-friend-of-a-friend - L3 etc)
    • Can retrieve relationships taking into account direction (e.g. only parents or only children) or ignoring direction (e.g. relatives)

    Additional requirements for discussion:

    • Each relationship type can be a node - easily extensible with metadata explaining what this relationship 'means'. Additionally - or alternatively - we could allow a simple keyword to define the relationship type.
    • Split off the API from the 'standard' backends and frontend to make it easier to maintain and add to core.
    • Direct (L1) relationships could be cached with the node - we could even 'grow' the node relationship cache for higher level relationships as they are requested, possibly with certain limits.
    • Relationships can be directional or non-directional - we could track this with the relationship type (i.e. if the relationship type has an 'inverse' then it is directional).
  • Potential use cases for relationships in Drupal
    • Node authorship (multiple authors!)
    • Taxonomy (which is relating to a point in a taxonomy tree, which can also be created using relationships)
    • Media or other attachments to a node
    • Web Links (relating to an external URI) - these could optionally be picked up from the node body and/or tracked using weblinks.module
    • Event and volunteer signups
    • Comment threading
    • Metadata in general
    • Tracking users buddies
    • Tracking user content likes/dislikes
    • Adding AI-like capabilities 'If you liked this, you'll love...'
    • Lastly, but certainly not least - the semantic web (RDF, FOAF...) revolution!
  • Potential DEP Dependencies:
    • Users, comments, taxonomy terms and attachments become nodes. Without this DEP the RAPI would be significantly more complex, because of the need for either a table mapping 'object-ids' to node, comment and user ids, or tracking the tables in question directly with each relationship
CiviCRM Relationships
  • Backend to transparently store/retrieve relationships to/from CiviCRM
Hierachical Relationships
  • Backend focussing on strictly hierarchical relationships
  • Uses an optimized table structure (e.g. nested sets - that allows all children to be retrieved with a single query)
  • This leads to massively improved performance for certain use cases
  • There are several choices for the data structure here
Network Relationships
  • Backend focussing on non-hierarchical (network) relationships
  • Simple table structure like nid-rid-nid
  • One query per 'level' of removal from node
  • Could use temporary tables and joins to avoid creating queries with long lists of (nid = 123) OR...
Relationship UI
  • Generic front end for creating/browsing relationships
  • Add a relationship example workflow (from a node edit page):
    • Select relationship type from a dropdown, or create a new type
    • Select related node using an AJAXed textbox and/or a drop-down of the last 20 nodes viewed
    • Submit relation (use AJAX to avoid page reload if possible)
  • Users could click an 'Add link' button, or drag-n-drop relationships to the node body to easily create a link to the related node.
  • List of relationships could be displayed below the node, on an adjoining block, or not at all.
  • Relationship viewing/editing could be limited to just types created by rui users, or any relationship at all.
  • Interesting interface to visually display the 'web' of nodes, allowing you to click on a related node to recenter the chart on that node.

Finished DEPs

Drupal enhancement proposals that were finished or closed.

Translator's guide

Drupal translations can be obtained from the download page.

The interface translation of Drupal and contributed modules/themes are separated processes from development of these projects themselfs. Developers of Drupal and the modules/themes place specially marked text into the code, which signify text to be translated. An extractor program is required to collect these strings and generate a translation template, which only contains these strings, without the surrounding program code. Because there are existing industry standards in the translation area, we have choosen an established format with mature tools for these templates.

The GNU Gettext toolset provides solutions for most translation needs. Because Drupal uses custom markup for identifying text to translate, we provide our custom tools to generate Gettext translation templates, but from that point, any Gettext tool can be used to work with the templates and translations (a simple text editor is often enough). Drupal can import the translated Gettext templates. Drupal.org provides hosting of the translation files in separate places, depending on what you translate.

Translation processes

The process of creating translations is as follows (click on it for zoomed view):

Drupal translation processes

Translating Drupal core and contributed modules/themes (represented by 'module' on the figure) are similar processes, but there are important differences.

Drupal core translations

To offer a consistent base to work with, we generate the Drupal core templates for every Drupal release, so translators can download the pregenerated translation templates and work with their tools to finish the translation. There is no need to worry about generating the templates yourself in this case. Keep in mind that we only generate templates when a release is close. Otherwise there would be an unstable template set to work with, and you would need to redo your work every week a major change lands in Drupal. You can use any editor comfortable to you to translate these templates. Often you will find previous translations for earlier Drupal versions, which you can reuse with the existing Gettext toolset (read on for more information). At the end of your work, you will get a set of translated files Name them with .po extensions instead of the template's .pot extensions. Once you get the translations into our version control system and there is a translation release, they will get packaged for download.

If your language does not exists yet in our system, you can create a new translation project by adding a directory with your language code to contrib-cvs/translations, and then adding a project on the drupal.org web interface. If you don't have a CVS account, create an issue on the Translation templates project and upload your translation files there. Some helpful developer will hopefully come by and put them into CVS for you. In this case a project for your translation will be created, you will be made the maintainer. Finally your translation will become available on the download page.

Contributed Drupal module/theme translations

As the figure shows, contributed module/theme (called module from now on) translations are a bit more complex. Some developers generate translation templates for their module, and put them into their module's po/ directory. This is a nice gesture, but unfortunately these templates get outdated quickly. So it is advised that you generate fresh templates with the translation template extractor when you start translating. Translations of modules currently get to their module directory, into a 'po' subdirectory. If you have a CVS account, you can commit your file there, but if you don't have one, you need to submit an issue against that module, and let the module maintainer commit your translation.

Read more:

Translation templates for Drupal core

Translators should start by downloading the tarball and translating the files to their language of choice. You can find more information about the process in the Translator's guide (of which this page is also a subsection of).

The translated files should be stored in contrib-cvs/translations/id where id is an RFC 4646 language code. If you don't know your code, ask on the translations mailing list.

You should only put the individual translated files in this directory. A script will generate a merged id.po file. Make sure to fill out the header section of each file and rename them from .pot to .po. Also make sure to start with the file general.po. This file contains strings that appear in more than one place and therefore will give you best results for least effort. It also is required to create the downloadable files. There is also an installer.pot file which will not get merged into the id.po file. This is used by the installer, and should not be imported to the Drupal database.

If you do not have a CVS account, and a project already exists for your language, create an issue against that project, attaching your files to it. If there is no existing language project, create an issue for this (the translation templates) project and attach your files to it.

Note that the Drupal team will not check contributed translations for accuracy or errors.

If you have ideas or problems with how the translation template extractor works, this tool has its own project now, so feel free to submit issues there.

Programs to use for translation

Recommended PO file editors are (in no particular order):

  • XEmacs (with po-mode): runs on Unices with X
  • GNU Emacs (with po-mode): runs on Unices
  • KBabel: runs on KDE
  • poEdit: Linux, MacOSX, and Windows
    poEdit does support multiple plural forms since version 1.3. The 1.3.6 version is buggy on MacOSX, get the 1.3.5 version from sourceforge.
  • OmegaT is another translation tool that can translate PO files. It is written in Java so it is available for multiple platforms (including GNU/Linux and Windows). It can be downloaded from SourceForge.
  • For Mac OS X there is AquaEmacs and a port of GNU Emacs available using carbon for OS X. Also see the Emacs wiki for more usage help and tips. po-mode is not included, but is easy to add. Get it from the GNU gettext distribution.
  • Vim (Linux/Unix and Windows versions available) with PO ftplugin for easier editing of GNU gettext PO files.
  • LocFactory Editor (OS X).
  • gted is an eclipse plugin that turns eclipse into a gettext editor. Eclipse is written in Java, so this editor is platform independant.

Be sure to get a recent version for all editors, multiple plural forms are a recent addition to the gettext standard.

Issues using poEdit

poEdit for windows, version 1.3.1 (latest at the moment) seems to require some additional steps to recognize plural forms (if you try to edit a term which has plurals, even if you translate it, it doesn't appear in poedit when you move to an other term, as usual, and even if you save, it doesn't).

Plurals Solution #1

So, if you find a plural term, close poedit, open the file you were translating with a normal text editor (no, not Word...), and search for "plural" in it, you find something similar to this:

#: modules/comment.module:187 modules/node.module:89
msgid "1 comment"
msgid_plural "%count comments"
msgstr[0] "1 commento"
msgstr[1] "%count commenti"

simply tranlate the text in msgid (singular form) into msgstr[0], and the text in msgid_plural (plural form) into msgstr[1], save the file, close the editor and return to poedit. Even better, you can do this BEFORE start translating the rest of the file with poedit, translating every occurrance of plural in the same way, in every file, and THEN start using poedit: this way, you will find those strings already translated in poedit, and they don't bother you.
Plurals Solution #2

To use plurals in PO edit you can start with the catalog setting for english and then modify to suit. The syntax is:

nplurals=2; plural=(n != 1);

which gave me what I needed in Swedish translation of:

#: modules/aggregator.module:100;711;722
msgid "1 item"
msgid_plural "items"
msgstr[0] "1 inlägg"
msgstr[1] "%count inlägg"

I tested this in PO Edit 1.3.1 and got the proper GUI response and saved withut error.

Plurals Solution #3

The plural forms to use in PO edit under catalog-settings where you see

nplural=INTEGER; plural=EXPRESSION

Only one form:
Some languages only require one single form. There is no
distinction between the singular and plural form. An appropriate
header entry would look like this:

Plural-Forms: nplurals=1; plural=0;

Languages with this property include:

Finno-Ugric family
Hungarian

Asian family
Japanese, Korean

Turkic/Altaic family
Turkish

Two forms, singular used for one only
This is the form used in most existing programs since it is what
English is using. A header entry would look like this:

Plural-Forms: nplurals=2; plural=n != 1;

(Note: this uses the feature of C expressions that boolean
expressions have to value zero or one.)

Languages with this property include:

Germanic family
Danish, Dutch, English, German, Norwegian, Swedish

Finno-Ugric family
Estonian, Finnish

Latin/Greek family
Greek

Semitic family
Hebrew

Romanic family
Italian, Portuguese, Spanish

Artificial
Esperanto

Two forms, singular used for zero and one
Exceptional case in the language family. The header entry would
be:

Plural-Forms: nplurals=2; plural=n>1;

Languages with this property include:

Romanic family
French, Brazilian Portuguese

Three forms, special case for zero
The header entry would be:

Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;

Languages with this property include:

Baltic family
Latvian

Three forms, special cases for one and two
The header entry would be:

Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;

Languages with this property include:

Celtic
Gaeilge (Irish)

Three forms, special case for numbers ending in 1[2-9]
The header entry would look like this:

Plural-Forms: nplurals=3; \
               plural=n%10==1 && n%100!=11 ? 0 : \
                      n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2;

Languages with this property include:

Baltic family
Lithuanian

Three forms, special cases for numbers ending in 1 and 2, 3, 4, except those ending in 1[1-4]
The header entry would look like this:

Plural-Forms: nplurals=3; \
               plural=n%10==1 && n%100!=11 ? 0 : \
                      n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;

Languages with this property include:

Slavic family
Croatian, Czech, Russian, Slovak, Ukrainian

Three forms, special case for one and some numbers ending in 2, 3, or 4
The header entry would look like this:

Plural-Forms: nplurals=3; \

plural=n==1 ? 0 : \
                      n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;

Languages with this property include:

Slavic family
Polish

Four forms, special case for one and all numbers ending in 02, 03, or 04
The header entry would look like this:

Plural-Forms: nplurals=4; \
plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;

Languages with this property include:

Slavic family
Slovenian

Long plural formulae: Those should not be broken into several lines in the header of the PO file. Drupal expects the formula to be on one line. One could consider this a bug.

I don't think that you can use line breaks in POedit either. The text is fixed to keep from breaking the site layout. But this:

nplurals=1; plural=ar;

produces an error. The plural form "ar" is not recognized.

Setting up XEmacs with po-mode on Windows

XEmacs has been supported on Windows for a long time and can be downloaded from here: http://www.xemacs.org/Download/win32. The po-mode is bundled with XEmacs (no need to get the GNU gettext distribution).

However, you need a MULE-enabled XEmacs binary to edit the UTF-8-encoded PO files and I could not find such a binary for Windows on www.xemacs.org. What I did is this:

  1. Install XEmacs 21.4.13 using the Netinstall (http://www.xemacs.org/Download/win32/setup.exe)
  2. Replace the files installed in the C:\Program Files\XEmacs\XEmacs-21.4.13 directory with those from http://www.suiyokai.org/tomonori/xemacs/xemacs-i586-pc-win32-21.4.13-mul... (as the filename implies, this is a MULE-enabled XEmacs 21.4.13 binary for Windows)
  3. Install the MULE packages in the C:\Program Files\XEmacs\mule-packages directory (these can be downloaded from http://ftp.xemacs.org/xemacs/packages/xemacs-all-mule-packages.tar.gz).
  4. Set the environment variable EMACSPACKAGEPATH with this value:
    C:\Program Files\XEmacs\site-packages;C:\Program Files\XEmacs\mule-packages;C:\Program Files\XEmacs\xemacs-packages
  5. To ensure automatic Unicode detection when opening files, add these lines to your init file (init.el):
    (require 'un-define)
    (set-coding-priority-list '(utf-8))
    (set-coding-category-system 'utf-8 'utf-8)
  6. And finally, add this to automatically enable the po-mode:
    (require 'po-mode)

Yes, this is a bit complicated... Welcome to the wonderful world of XEmacs! :-) If you never used XEmacs before, prepare yourself for a steep learning curve.

BTW, your installation directory does not have to be C:\Program Files\XEmacs. I used this for simplicity in the above instructions.

Translated Drupal information

Some documentation about Drupal (outside of the Drupal interface itself) has also been translated into other languages. Links to such 3rd party translations of external Drupal documentation should live here.

Afrikaans

This page is for the translation of Drupal Core into Afrikaans. The rest of this text will be in Afrikaans, as that is the purpose of this document.

Vir diegene wat betrokke wil raak by die vertaling:

Stuur 'n e-pos aan Kobus en spesifiseer waarmee jy betrokke sou wou raak. Kobus sal dan met jou in verbinding tree indien jy die nodige besonderhede verskaf het.

Om 'n fout met die vertaling te rapporteer:

Besoek asb. die foutrapporteringsblad

Vir enige ander verwante redes waaroor jy wil kontak:

Stuur 'n e-pos aan Kobus en spesifiseer die presiese rede vir u skakeling.

Russian

Translations and original documentation for Russian Drupal users (still very incomplete, but work is in progress):

Visit drupal.ru/docs for a complete list of links to Russian documentation.

Join our efforts to translate Drupal docs into Russian!

Translation guidelines

To achieve translations that are consistent throughout a whole Drupal site, certain guidelines need to be agreed upon by the translator community for a particular language.

Such guidelines should include a wordlist for words that occur in Drupal's strings. The Ankur Bangla Developer's Guide provides a good example of how this is done on a project unrelated to Drupal. It will be helpfull to not set up a new word lists, but re-use existing ones from an existing translation project.

Other areas which need guidelines will differ from language to language. Please add those guidelines as child pages to this book page.

Translation of contributed modules

Translatable strings from contributed modules are not included in the Drupal core translation template files. Module authors can use the potx-cli.php script (or the potx.module) which can be obtained from the Translation template extractor project, to generate a POT file on their own. Instructions for running the extractor can be found in the README.txt that comes with the project. The generated POT file should be named as the module, but with a .pot extension, e.g. event.module gets an event-module.pot file. This file should be placed in a subdirectory po. Translations should be added to the same directory. E.g. the po subdirectory of event.module currently contains the following files: de.po, es.po, event-module.pot, he.po, hu.po.

Translators should take care to populate their started translation with the strings from the general.po file for their language using msgmerge. In this way they can avoid using different translations for terms that occur in both files.

Distributing the translation effort

To facilitate easier handling of a community translation effort, the Drupal POT file is split up into small files that do not contain doubly occurring strings.

All strings that occur more than once in the Drupal core distribution are put into the general.pot file. This ensures that those strings are translated to the same string. Also, files that have ten or less translatable strings will not get their own POT file, but those strings will be appended to the general.pot file.

Of course, some coordination among the project members is still needed to ensure the quality of the translation.

If a language has several options on how to translate some strings, then it is possible to create PO files that only change those strings. An example would be German where your can translate you either as Du or Sie depending on the audience of your site.

Status of the translations

This page used to present an overview of the status of each translation of Drupal core's interface. Unfortunately, recent changes to how Drupal contributions are released meant that the way this page was generated no longer works. There is an issue about getting this page working again under the new system, but until that is resolved, a summary table of translation status is not possible.

Checking your translation status

To see how many of the strings in the PO files you have already translated, you can try this:

for i in *.po; do echo -n "$i: " ; msgfmt --statistics $i ; done

Some PO editors already include this feature.

Make a single file from the loose .po files from CVS

If you want to make a single po file from a CVS folder containing all the small po files, the following commands will do (*nix only). You should execute this, while being in the folder with the .po files.

$ msgcat --use-first  general.po [^g]*.po | msgattrib  --no-fuzzy -o nl.po

Off course you should change nl into your own language code.

Recycling old translations

Drupal users with existing translations might want to add those to the translations download page. To do this they first need to export their translation from the localization manage languages screen (export subtab). Let us assume you have an Italian translation. The above mentioned process will create an it.po file for you. To use this file as a basis for a new translation, you treat it as a PO compendium, i.e. a library of pre-translated strings.

This guide assumes a Unix/Linux environment. If you use Windows, check if your PO editor doesn't have a function for this.

We will split the single, large PO file into the smaller files that the Drupal translation Project requires.

First, put the small PO files into a subdirectory drupal-pot and your it.po file into another one. Then create an empty directory where you want to keep your new small PO files.

Then go to the empty directory and execute the following command from the command line:

for i in /path/to/drupal-pot/*.pot ; do msgmerge  --compendium /path/to/it.po -o `basename $i .pot`.po /dev/null $i  ; done

After a while (yes this will take a few minutes) you should have a directory of small PO files that have the matching strings inserted.

You can also do without compendium with this:

for i in *.po; do msgmerge  --update $i ../drupal-pot/${i}t ; done

This will update all .po files in current directory with appropriate file from ../drupal-pot/ directory. This is usefull when updating for example from 4.6.1 to 4.6.2 (when there is small number of changes).

Partial translations
Let's say you have partially translated .po files for modules and someone sends you a LANG.po file which contains strings s/he translated himself.

You can merge translations using following approach:
0. Of course you already have a backup of your files :)
1. You need .pot files for all of your .po files you want to update, let's say they are in ../POTS dir
2. Concatenate .po files you want to update with LANG.po file:

msgcat -o ../update.po LANG.po file1.po file2.po ... fileN.po

3. Fix update.po's header, it'll be written into update .po files
4. The best is to check update.po with msgfmt and fix any errors:
msgfmt -o /dev/null -c --statistics ../update.po

5. Update .po files (this will update all .po files in current dir):
for i in *.po; do msgmerge -o $i ../update.po ../POTS/${i}t; done

6. Remove obsolete translations:
for i in *.po; do msgattrib -o $i.no-o  --no-obsolete $i && mv $i.no-o $i; done

You should have your .po files updated. Now you need fix fuzzy enteries and check translation for consistency. If a string was translated both in the LANG.po file you were sent and in your own .po file then both translations will be included in the updated .po file. You need to check it and decide which one is better.

Troubleshooting

When doing translations or importing them, several problems can occur. If you think you found a bug in either a translation or in Drupal's locale module, please file bug reports against the project in question.

If you have a more general question you can ask it in the translations forum.

Here we collect some of the more common issues found.

Some strings do not translate

Symptom: After importing a translation, some strings on your site are translated, some are not.

Possible causes (and solutions):

  1. The imported translation is not complete. Consider finishing it and contributing your update by filing an issue against the translation.
  2. The actual source strings have been modified after the translation file was created. You might need to update the PO file from a fresh POT file (translation template). The latest core POT files are always available for download, but you might need to generate fresh POT files yourself.
  3. There are end-of-line inconsistencies between the actual source strings and the source strings from the PO file. All Drupal files have Unix-style end-of-lines (\n), but these sometimes get messed up on Windows (which normally uses \r\n). WinZip users beware: Make sure the "TAR file smart CR/LF conversion" option is not checked before extracting any Drupal tarball.

Weird characters or question marks

Symptom: After importing a translation you find all kind of weird characters or question marks on your site.

Solution 1: The translator did not use UTF-8. Drupal is fully UTF-8 aware and expects translations to be supplied in that character set as well.
You can change the charset of a .po file using GNU msgconv. Or with XEmacs you can use this command:

C-x <RET> f utf-8 <RET>

Please also file a bug against the translation in question asking them to change the character set to UTF-8.

Solution 2: You do not have the correct font installed to display the language in question, this is an issue wit your browser or operating system.

If you want to check the character set of a .po file, you can use the "file" command.

file *.po

Example output:
ca.po:    UTF-8 Unicode PO (gettext message catalogue) text
de.po:    UTF-8 Unicode PO (gettext message catalogue) text, with very long lines
fr.po:    UTF-8 Unicode PO (gettext message catalogue) text
it.po:    UTF-8 Unicode PO (gettext message catalogue) text

Bazaar-NG

Bazaar-NG is a decentralized revision control system. The key point of decentralized revision control systems is that third parties can branch off of the official drupal branches and perform work under revision control without requiring permission from the core drupal team.

Installation

Bazaar-NG is nearly as portable as CVS, though installation is a little trickier. The general rule of thumb is that the further one gets from a GNU/Linux installation, the more effort the installation.

Installation on Ubuntu & Debian based systems

This platform is by far the easiest system to install Bazaar-NG on. Installing on this platform essentially involves adding a source line for the nightly built packages, updating the packages list and installing Bazar-NG and a couple related tools. Once the sources has been done any distro tool -- whether apt-get, aptitude, or synaptic can be used to install and keep current an installation of Bazaar-NG. This document will use apt, since its command line based and can be easily expressed in book format. Feel free to use the one of your choice and follow along conceptually! I'll also assume that you have used su to become root.

One of the members of the Bazaar-NG is kind enough to make and distributes nightly debs of Bazaar-NG and related packages. You can gain access to these packages via apt or synaptic by adding the following line to /etc/apt/sources.list:
deb http://people.ubuntu.com/~jbailey/snapshot/bzr ./

Now you'll need to update your packages list. In apt we perform this step by running "apt-get update". This will update the packages that are available on your system.

Finally, perform the actual installation by running the command "apt-get install bzr bzrtools".

Congratulations, you now have Bazaar-NG installed!

Installation on Cygwin

The second most challenging platforms to install bazaar-NG on is Cygwin. Though each step involved in installation isn't difficult, a lot of them exist.

The first thing that you'll want to do is to ensure that python 2.4 installed. You can check this by running "python -V". If the python that you have is earlier than this, or if you don't have python at all, then use the cygwin package manager to install the newer one. Install openssh and rsync as well, while you're at it.

The second thing that you'll do is manually download and install a few python packages. No worries; this process is easy. First, download the latest package for each of these modules:
1. cElementTree
2. pyCrypto
3. Paramiko

The third thing you need to do is perform two steps for each downloaded module:
1. unpack the latest version
2. cd into the unpacked directory and run "python setup.py install"

Now, its time to download a copy of bzr. Since you've installed rsync, we'll use that here:

$ cd /usr/local/lib
$ rsync -av bazaar-ng.org::bazaar-ng/bzr/bzr.dev bzr.dev
$ ln -s /usr/local/lib/bzr.dev/bzr /usr/local/bin/bzr

Congratulations! You now have Bazaar-NG installed. You're not done quite yet. As the last part of installation, we're going to use Bazaar-NG to install a plugin for itself called bzrtools. We do that this way:

$ cd ~
$ mkdir -p .bazaar/plugins
$ cd .bazaar/plugins
$ bzr branch http://panoramicfeedback.com/opensource/bzr/bzrtools/ bzrtools

Installation on OS X

While the Apple Developer Tools come with python, you will need upgrade to python 2.4 in order to install the latest version of Bazaar-NG.

You can get the python binaries from http://undefined.org/python/

If you are running 10.4 (Tiger), you will need to also install the TigerPython24Fix, also available from http://undefined.org/python/

If you want to do pushes or pulls over SFTP you will need to get and install:

  1. pyCrypto
  2. Paramiko

Directions are included with the tar.gz files.

Finally, you can install bzr. Download the source and away you go.

The windows installation guide also has a convenient write up on bzrtools.

You can also get Bazaar-NG via the DarwinPorts system.

As of this writing the the Fink project is a little behind the times for Bazaar-NG.

Setting up Bazaar-NG

When one commits to a branch in Bazaar-NG several things are saved. Some of these things include the modifications you made and the message that you gave when you comitted. Another thing that is saved is the email address of the committer. This way, when somebody wants to chase down the person that hacked on some code, they know who to contact! Bazaar-NG is happy to make a guess, though the guess is usually not as accurate as setting it yourself.

Several methods exist for telling Bazaar-NG what your email address is. All of these methods are listed on the Setting Email in Bzr web page. The easiest method among those listed is setting one in a special file in your home directory. This is done by making a directory to hold a Bazaar-NG config file. In pure windows this file is %APPDATA%\bazaar\2.0\bazaar.conf. In cygwin and other unixlike operating systems the file is ~/.bazaar/bazaar.conf.

Inside of this file you'll want to add two lines:

[DEFAULT]
email=Your Name <name@isp.com>

Now, every time you commit, you will be credited with the patches that you made. Now, whenever somebody merges your code, you will get credit for your hard work.

Hacking your local Drupal

Bazaar-NG is not like CVS. If you have a copy of a branch in Bazaar-NG then you can commit to it without getting permission from anyone. This kind of makes sense. If the branch is on your hard drive, then you should be able to commit to it!

There are a few rules to follow to making local commits:

* You will want to commit your work from time to time. This is performed with 'bzr commit -m"A description of your changes"'
* If you add a new file or directory to your drupal branch then you'll want to run "bzr add ".
* If you want to take a file or directory out of revision control, then you do so with "bzr remove ". Doing this will not delete the file, but will tell Bazaar-NG to stop tracking it for you.
* You can ignore files by running "bzr ignore "
* Once you have committed to your drupal branch "bzr pull" will no longer work. This is because the branch is no longer strictly a copy of the official development drupal branch. Now, instead of pulling new changes, you will have to merge them.

When you commit to a branch then those changes will stay in your branch until somebody else merges them. This is because Bazaar-NG does not check into a central server like cvs does. This means that if you want somebody to merge your code, that you'll have to put a copy of a branch in a place where other people can reach it. They in turn can either branch or merge from you. We'll cover this in a different chapter.

Database revisions

Tracking database changes can be hard. Here's an example of a script for such purposes. (It may be a good idea to keep it in revision control as well):

#!/bin/sh
rm -f database/tabs/*
mysqldump -uUSER -pPASSWORD DATABASE --opt -r database/currentdb.myqsl
mysqldump -uUSER -pPASSWORD DATABASE --skip-opt -T database/tabs
bzr add database/tabs
bzr commit

You will want to create a fold "tabs" in the database directory. Make sure that the mySQL user can edit it (with the nuclear option being "chmod 777 tabs" - a better way would be to chown it to the right user/group).

Every time you want to checkpoint your code (put the code and db in sync in version control), run this script to give yourself a picture of the entire database AND individual tables. If tables are dropped, the 'rm' takes care of that.

If you use postgreSQL, you will need to adapt this to for your system.

WARNING

Be careful. If you're serving drupal from a bzr checkout that bzr checkout can be branched from (since everything is a branch).

Try: bzr info http://mydrupalsite.com/
If it responds with anything other than:
bzr: ERROR: Not a branch: http://mydrupalsite.com/
then anyone can check out your code. Not a problem unless you happened to check in your changes to sites/default/settings.php - ie: your database password.

Not a problem though, just toss in a little .htaccess file in your .bzr/:

Order deny,allow
Deny from all

Good as new.

Updating your Branch with Official Drupal Dev

A branch that is downloaded from the drupal bzr server is just a copy of a branch. These sorts of branches, as we've already covered, are updated simply by running "bzr pull". This only works, though, if your branch is _just_ a copy. If you have committed to a branch, then a slightly different approach needs to be taken. A branch that has been committed to is a "diverged branch". These branches are different, but equally valid, from the branch they came from.

One merges changes between diverged branches with the "bzr merge" command. In your case, your branch will have diverged from the official drupal.dev branch. You can merge from drupal.dev in this way:

$ bzr merge

This will figue out what changes you are missing from official drupal.dev and apply them to your version of drupal. You're still not done, however. You should also review the changes in your branch and decide whether or not you want them. You can see what changed by running "bzr st" and how they changed by "bzr diff". Presumably you'll want to save the changes. You can do this the same way that you've always saved changes by
bzr commit -m 'merged from mainline'

Getting Merged

When your drupal branch is ready for merging you'll have to put it in a public place so that other people can get it. Publically exposing one of your branches involves "pushing the branch". One can push a branch via sftp or rsync if BzrTools is installed.

One pushes a branch in the following manager:

$ cd my-drupal-fix
$ bzr push sftp://hostname.com/public_html/my-drupal-fix

Once you have pushed your branch you'll want other people to know about it so that they can merge you. You can do this by registering your branch on Drupal Launchpad page. New branches should be marked as "NEW".

The person responsible for merging Drupal branches will merge your code in the same way that you merge Drupal code. He or she will then review your code and will email you. Two possible reactions are likely: either your hacks were accepted and merged, or your hacks need some more work. Don't get discouraged if you need to do more work; just fix the outstanding problems, commit, push again and ask that your branch is reviewed.

*NOTE* There is currently no person responsible for merging. Please see the next chapter about generating a diff.

Getting a diff against core/head

Another way that one can submit changes to Drupal with Bazaar-NG is by generating a diff. Diffs are a little bit more awkward to work with, but do fit well within the current system.

You can generate a diff against another branch at any time by using the bzr diff command. For example, to generate a diff against the official Drupal development tree, one would run:

$ cd my-drupal-branch
$ bzr diff -r branch:http://bzr.drupal.org > ~/mypatch

One important note though! Make sure that you merge from the official drupal branch and commit before you generate your diff! Otherwise, bzr will think that you mean for the changes in official drupal development should be reverted!

If you find it a pain in the rear to constantly have to type so much whenever you want a diff, then consider putting the following line in your ~/.bashrc. The next time that you run in, you can compare the differences in your branches against official drupal whenever you want by running "ddiff": alias ddiff='bzr diff -r branch:http://drupal.revisioncontrol.net/core/head"

To get function names into your diff, use --diff-options -F^f. If you are lazy/efficient, you might alias the 'b' command to bzr diff --diff-options -F^f . > patch

Getting Drupal Head via Bazaar-NG

Once Bazaar-NG is installed and set up you can use Bazaar-NG to download and then keep updated the current development version of drupal.

The Bazaar-NG mirror of the development version of Drupal is at http://bazaar.launchpad.net/~vcs-imports/drupal/main/

However, since it has lots of revisions in it, it could take a long time to get Drupal from it (up to 10 minutes).

Later on, a simple command will keep you up to date:

bzr pull

We'll cover howo update a drupal branch that you have modified in a later chapter. Don't worry; the process is almost as easy.

Drupal test suite

Drupal is currently lacking some test suite to be run by developers before submitting important patches. The simpletest module shows some great promise but it is unfortunately not widely adopted yet and there aren't many tests written. See here for a tutorial on how to write tests for your module.

The following setup isn't really a test suite but it is a start to avoid the most embarrassing errors.

Proceed as follows:

  1. Enable the menu module and disable the 'log out' link.
  2. If you have the image module installed, enable the image module and set permissions so images can be written for the userid running the link check. This will avoid image creation errors when crawling with image enabled.

HOWTO: Write an installation profile for the Drupal installer

An installation profile is a simple file that tells the Drupal installer to enable a set of core and/or contributed modules as well as configure some default settings. Using installation profiles, the Drupal 5 installer can be customized, allowing for special set-ups for specific user groups. For example, an installation profile for "Drupal band sites" might automatically install modules for e-mail newsletters, events and downloadable audio files.

The following is based on the initial documentation by CivicSpace Labs on how to create an install profile.

Anatomy of an installation profile

Install profiles are located in the 'profiles' directory of a Drupal installation, and named like example.profile. A typical profile file contains the following functions:

Note: I use profilename here as an example profile name. This is whatever your .profile file is named, some examples include default, my_blog_site, corporate, etc.

1. profilename_profile_modules() - REQUIRED

<?php
/**
* Return an array of the modules to be enabled when this profile is installed.
*
* @return
*  An array of modules to be enabled.
*/
function profilename_profile_modules() {
  return array(
   
// Enable required core modules first.
   
'block', 'filter', 'node', 'system', 'user', 'watchdog',
   
   
// Enable optional core modules next.
   
'blog', 'color', 'comment', 'forum', 'help', 'menu', 'taxonomy',

   
// Then, enable any contributed modules here.
   
'og', 'views', 'views_ui', 'views_rss',
  );
}
?>

Each module specified is enabled in order (important when you want to make database changes which affect other modules), which in turn fires its modulename.install file, if present. Note that you need to enable stuff like 'system', 'blocks', etc. in addition to extra modules like 'cart' and 'product.' Probably the best thing to do is copy/paste from the default.profile file's profile_modules() hook and go from there.

2. profilename_profile_details() - REQUIRED

<?php
/**
* Return a description of the profile for the initial installation screen.
*
* @return
*   An array with keys 'name' and 'description' describing this profile.
*/
function profilename_profile_details() {
  return array(
   
'name' => 'Example profile',
   
'description' => 'This example profile will install some commonly used contrib modules.',
  );
}
?>

3. profilename_profile_final() - optional

<?php
/**
* Perform any final installation tasks for this profile.
*
* @return
*   An optional HTML string to display to the user on the final installation
*   screen.
*/
function profilename_profile_final() {
 
// Insert default user-defined node types into the database.
 
$types = array(
    array(
     
'type' => 'page',
     
'name' => t('Page'),
     
'module' => 'node',
     
'description' => t('If you want to add a static page, like a contact page or an about page, use a page.'),
     
'custom' => TRUE,
     
'modified' => TRUE,
     
'locked' => FALSE,
    ),
    array(
     
'type' => 'story',
     
'name' => t('Story'),
     
'module' => 'node',
     
'description' => t('Stories are articles in their simplest form: they have a title, a teaser and a body, but can be extended by other modules. The teaser is part of the body too. Stories may be used as a personal blog or for news articles.'),
     
'custom' => TRUE,
     
'modified' => TRUE,
     
'locked' => FALSE,
    ),
  );

  foreach (
$types as $type) {
   
$type = (object) _node_type_set_defaults($type);
   
node_type_save($type);
  }

 
// Default page to not be promoted and have comments disabled.
 
variable_set('node_options_page', array('status'));
 
variable_set('comment_page', COMMENT_NODE_DISABLED);

 
// Don't display date and author information for page nodes by default.
 
$theme_settings = variable_get('theme_settings', array());
 
$theme_settings['toggle_node_info_page'] = FALSE;
 
variable_set('theme_settings', $theme_settings);

 
// The return message is optional, if you omit it the default will be used.
 
return '<p>'. (drupal_set_message() ? t('Please review the messages above before continuing on to <a href="@url">your new Profile Name site</a>.', array('@url' => url(''))) : t('You may now visit <a href="@url">your new Profile Name site</a>.', array('@url' => url('')))) .'</p>';
}
?>

In the profilename_profile_final() implementation, you have the opportunity to do anything extra AFTER the modules specified in profilename_profile_modules() have been installed. In this function you have access to the full Drupal API so you could define any custom content types, create vocabularies and terms, change variable settings to your liking, etc.

General strategy for setting options in your profile's hook_profile_final()

Below I've tried to itemize some of the most common tasks. But here is a general strategy that worked for me when I was developing the GJG install profile:

1. Take a dump of the database, using mysqldump or PHPMyAdmin
2. Change something on a form or whatever
3. Take another dump (of the database :P)
4. Diff the two
5. Take those lines that are different and stick them in db_query(), replacing table_name with {table_name}

Common stuff you need to do: store settings/variables

Anytime you submit a form under administer >> settings, it's put into the variable table. I find it helpful to take a dump of the variable table before and after visiting a settings page and then diff to find the differences. Another approach might be to view source to find out field names, and then check the database to see what value it input.

Variable defaults usually do not exist in the variable table when you first install. A routine way to to capture as many variables as possible is to open every link on the /admin page, (and then look for more links called "settings") and then submit every form, even if you do not want to change the value. This will force every default into the variables table.

A quick way to view all existing variables is to use devel and enable the devel block. This block contains a link to view all variables.

To then override a variable in your profile, we use variable_set. For example, to enable user locations from location module:

<?php
  variable_set
('location_user', 1);
?>

The only place this can get tricky is with checkboxes/mutiselects, because those will get stored in a serialized array, so the db record will look like:

node_options_event:
a:2:{i:0;s:6:"status";i:1;s:7:"promote";}

The "a:2" means it's an array with 2 elements. The way to insert this is like:

<?php
  variable_set
('node_options_event', array('status', 'promote'));
?>

Common tasks you need to do: enable blocks

For this, I just take a dump of the blocks and boxes (for custom blocks) tables and then figure out which ones are custom vs. which ones come with Drupal by default (these will be in 'modules/system/system.install'). Then just insert the SQL directly in db_query statements, like:

<?php
  db_query
("INSERT INTO {blocks} VALUES ('gjg', '0', 1, 0, 1, 0, 0, 1, 'my*', '')");
?>

Common stuff you need to do: configure roles/permissions

Again, for this I take a dump of the role, users_roles, and permissions tables, and just make sure not to include definitions for the built-in roles (basically, if it's in database.mysql, you don't want to take it because it will cause an error because of a duplicate record).

<?php
// Make an 'administrator' role
db_query("INSERT INTO {role} (rid, name) VALUES (3, 'admin user')");

// Add user 1 to the 'admin user' role
db_query("INSERT INTO {users_roles} VALUES (1, 3)");

// Change anonymous user's permissions - this is UPDATE rather than INSERT
db_query("UPDATE {permission} SET perm = 'access comments, can send feedback, access content, search content, view uploaded files' WHERE rid = 1");

// Insert new role's permissions
db_query("INSERT INTO {permission} (rid, perm, tid) VALUES (3, 'administer blocks, edit own blog,....', 0)");
?>

Sample .profile file - gojoingo.profile

<?php
/**
* The modules that are enabled when this profile is installed.
*
* @return
*  An array of modules to be enabled.
*/
function gojoingo_profile_modules() {
 
$core = array('system', 'block', 'blog', 'comment', 'contact', 'filter', 'forum', 'help', 'menu', 'node', 'page', 'path', 'profile', 'search', 'story', 'taxonomy', 'upload', 'user', 'watchdog');
 
$contrib = array('buddylist', 'front', 'content', 'text', 'jstools', 'location', 'location_views', 'event' 'rsvp', 'signup', 'signup conflicts', 'image', 'image_attach', 'image_gallery', 'invite', 'logintoboggan', 'og', 'og_basic', 'privatemsg', 'urlfilter', 'views', 'views_ui');
 
// TODO: How to deal w/ spam requirement?
  // TODO: How will this deal with image_attach, which is a contrib module inside image?
 
return array_merge($core, $contrib);
}

/**
* Implementation of hook_profile_details().
*
* This contains an array of profile details for display from the main selection screen.
*/
function gojoingo_profile_details() {
  return array(
   
'name' => 'GoJoinGo',
   
'description' => 'A social networking website with groups, events, friends, etc.'
 
);
}

/**
* Implementation of hook_profile_final().
*
* GoJoinGo platform installation.
*/
function gojoingo_profile_final() {

 
// Enable user locations
 
variable_set('location_user', 1);

 
// Turn on LoginToboggan features
 
variable_set('login_with_mail', 1);
 
variable_set('email_reg_confirm', 1);
 
variable_set('reg_passwd_set', 1);
 
variable_set('toboggan_immed_login', 1);
 
variable_set('toboggan_role', 4);
 
variable_set('toboggan_hijack', 1);

 
// Enable Organic Group access control
 
variable_set('og_enabled', 1);
 
db_query("DELETE FROM {node_access}");

 
// Make post visibility selectable by author - default to Public
 
variable_set('og_visibility', 2);

 
// Omit page types from OG
 
variable_set('og_omitted', array('page'));

 
// Enable Urlfilter
 
db_query("INSERT INTO {filters} (format, module, delta, weight) VALUES (1, 'urlfilter', 0, 10)");

 
/** CONFIGURATION SETTINGS */

  // Change front page to my/home
 
variable_set('site_frontpage', 'my/home');
 
 
// Turn on user pictures
 
variable_set('user_pictures', 1);

 
// Set default primary links
 
variable_set('phptemplate_secondary_links', array(
   
'text' => array('my home', 'my blog', 'my groups', 'my events', 'my friends'),
   
'link' => array('my/home', 'my/blog', 'my/groups', 'my/events', 'my/friends'),
   
'description' => array('', '', '', '', ''),
  ));

 
// Set welcome message for anonymous users
 
variable_set('front_page', 'Welcome to '. variable_get('site_name', 'GoJoinGo') .'!');

 
// Change welcome email to include validation URL
 
variable_set('user_mail_welcome_body', "

%username,

Thank you for registering at %site.

IMPORTANT:
For full site access, you will need to click on this link or copy and paste it in your browser:

%login_url

This will verify your account and log you into the site. In the future you will be able to log in using the username and password that you created during registration.

Your new %site membership also enables to you to login to other Drupal powered websites (e.g. <a href="
http://www.drupal.org/" title="http://www.drupal.org/" rel="nofollow">http://www.drupal.org/</a>) without registering. Just use the following Drupal ID along with the password you've chosen:

Drupal ID: %username@%uri_brief


--  %site team

");

  // Remove default line break filter for the FULL HTML filter
  db_query("
DELETE FROM {filters} WHERE format = 3");

  /** BLOCK CONFIGURATION **/
  // Recommendations block - only show on my*
  db_query("
INSERT INTO {blocks} VALUES ('block', '1', 1, 0, 1, 0, 0, 1, 'my*', '')");
  db_query("
INSERT INTO {boxes} VALUES (1, 'Recommendations', '<?php\r\n  /* Edit the following variables if you''d like to change the text for this block */\r\n  \$groups = ''Groups in your Area'';\r\n  \$events = ''Events in your area'';\r\n  \$people = ''People in your area'';\r\n  \$popular = ''Popular Groups'';\r\n  \$new = ''New Groups'';\r\n  \r\n  /* Below is PHP code, only edit if you feel comfortable with PHP and the Drupal API.  */\r\n  global \$user;\r\n    \$items = array(l(t(\$groups), ''gsearch/og''),\r\n                   l(t(\$events), ''gsearch/gjg_event''),\r\n                   l(t(\$people), ''gsearch/user''),\r\n                   l(t(\$popular), ''groups/popular''),\r\n                   l(t(\$new), ''groups/new''));\r\n   \$output = theme(''gjg_menu'', \$items);\r\n   print \$output;', 'Recommendations', 2)");

  // My friends block - only show on my*
  db_query("
INSERT INTO {blocks} VALUES ('gjg', '0', 1, 0, 1, 0, 0, 1, 'my*', '')");

  // Recommendations block
  db_query("
INSERT INTO {blocks} VALUES ('gjg', '1', 0, 0, 0, 0, 0, 0, '', '')");

  // Group profile block - only show on og types
  db_query("
INSERT INTO {blocks} VALUES ('group_block', '0', 1, 0, 0, 0, 0, 0, '', 'og')");

  // Group actions block - only show on og types
  db_query("
INSERT INTO {blocks} VALUES ('group_block', '1', 1, 1, 0, 0, 0, 0, '', 'og')");

  // LoginToboggan login block
  // NOTE: The following lines are commented out until I get LT working
  //db_query("
INSERT INTO {blocks} VALUES ('logintoboggan', '0', 1, 0, 0, 0, 0, 0, '', '')");

  // Hide normal user login block
  //db_query("
UPDATE {blocks} SET status = 0 WHERE module = 'user' AND delta = 0");

  // Move navigation block down
  db_query("
UPDATE {blocks} SET weight = 1 WHERE module = 'user' AND delta = 1");

  // User actions block - show only on my* and tracker*
  db_query("
INSERT INTO {blocks} VALUES ('user_block', '0', 1, 0, 0, 0, 0, 1, 'my*\r\ntracker*', '')");

  // User personal actions block - show only on my* and tracker*
  db_query("
INSERT INTO {blocks} VALUES ('user_block', '1', 1, 1, 0, 0, 0, 1, 'my*\r\ntracker*', '')");

  /** DEFAULT CONTENT TYPE SETTINGS **/

  // Generally, all nodes default to _not_ promoted to front page and
  // attachments disabled
  foreach(node_list() as $node) {
    variable_set("
node_options_$node", array('status'));
    variable_set("
upload_$node", 0);
  }

  // File: enable attachments
  variable_set('upload_file', 1);

  // GJG Event: enable events
  variable_set('event_nodeapi_gjg_event', 'all');

  // OG: turn off comments, enable locations
  variable_set('comment_og', 0);
  variable_set('location_og', 1);
  variable_set('location_name_og', 1);
  variable_set('location_street_og', 1);
  variable_set('location_city_og', 1);
  variable_set('location_province_og', 1);
  variable_set('location_postal_code_og', 1);
  variable_set('location_country_og', 2);

  // Page: turn off comments
  variable_set('comment_page', 0);

  // Venue: enable locations
  variable_set('location_venue', 1);
  variable_set('location_name_venue', 1);
  variable_set('location_street_venue', 1);
  variable_set('location_city_venue', 1);
  variable_set('location_province_venue', 1);
  variable_set('location_postal_code_venue', 1);
  variable_set('location_country_venue', 2);

  /** ROLES AND PERMISSIONS **/
  // Administrator user
  db_query("
INSERT INTO {role} (rid, name) VALUES (3, 'admin user')");

  // Pre-authorized user (for LoginToboggan)
  db_query("
INSERT INTO {role} (rid, name) VALUES (4, 'pre-authorized user')");

  // Add user 1 to authenticated and admin roles
  db_query("
INSERT INTO {users_roles} VALUES (1, 2)");
  db_query("
INSERT INTO {users_roles} VALUES (1, 3)");

  // Configure default permissions for each role
  db_query("
UPDATE {permission} SET perm = 'access comments, can send feedback, access content, search content, view uploaded files' WHERE rid = 1");
  db_query("
UPDATE {permission} SET perm = 'edit own blog, access comments, post comments, post comments without approval, can send feedback, create files, edit own files, create forum topics, edit own forum topics, create events, edit own events, create images, submit latitude/longitude, view location section, access content, create groups, access private messages, search content, report spam, create stories, edit own stories, upload files, view uploaded files, access user profiles, create venue, edit own venues' WHERE rid = 2");
  db_query("
INSERT INTO {permission} (rid, perm, tid) VALUES (3, 'administer blocks, edit own blog, access comments, administer comments, administer moderation, moderate comments, post comments, post comments without approval, can send feedback, create files, edit own files, administer filters, administer forums, create forum topics, edit own forum topics, create events, edit own events, administer images, create images, submit latitude/longitude, view location section, administer menu, access content, administer nodes, administer organic groups, create groups, create pages, edit own pages, administer url aliases, create url aliases, access private messages, administer search, search content, access spam, administer spam, bypass filter, report spam, create stories, edit own stories, access administration pages, administer site configuration, administer taxonomy, upload files, view uploaded files, access user profiles, administer users, create venue, edit own venues, administer watchdog', 0)");
  db_query("
INSERT INTO {permission} (rid, perm, tid) VALUES (4, 'access comments, post comments, can send feedback, create forum topics, create events, submit latitude/longitude, view location section, access content, search content, create stories, view uploaded files, access user profiles, create venue', 0)");

  /** THEME SETUP **/
  // Disable bluemarine
  //db_query("
UPDATE {system} SET status = 0 WHERE name = 'bluemarine'");

  // Enable PHPTemplate theme engine
  db_query("
INSERT INTO {system} VALUES ('themes/engines/phptemplate/phptemplate.engine', 'phptemplate', 'theme_engine', '', 1, 0, 0)");

  // Enable default theme
  drupal_system_enable('theme', 'gojoingo');
  variable_set('theme_default', 'gojoingo');

  // Disable default logo and enter new logo path
  variable_set('theme_gojoingo_settings', array(
    'default_logo' => 0,
    'logo_path' => 'themes/gojoingo/images/gojoingo-header.png',
    'toggle_name' => 1,
    'toggle_slogan' => 0,
    'toggle_mission' => 1,
    'toggle_primary_links' => 1,
    'toggle_secondary_links' => 1,
    'toggle_node_user_picture' => 0,
    'toggle_comment_user_picture' => 0,
    'toggle_search' => 0,
  ));

}
?>

This initial documentation was presented by CivicSpace Labs.

E-Commerce Installation Profile

E-Commerce is a collection of several dozen modules for creating an online store. However, it requires some advanced configuration. As an example we have produced a simple install profile for e-commerce. In this profile we provide first a list of modules that should be enabled, then a profile description. In each of the .install files we have copied important pieces of settings forms to be added to the installation system forms.

/**
* The modules that are enabled when this profile is installed.
*
* @return
*  An array of modules to be enabled.
*/
function ecommerce_profile_modules() {
  return array('system', 'apparel', 'audio', 'block', 'cart', 'civicrm', 'cod', 'comment', 'content', 'coupon', 'custom', 'donate', 'ecivicrm', 'file', 'filter', 'help', 'image', 'image_attach', 'menu', 'node', 'page', 'path', 'payment', 'product', 'recommend', 'role_discount', 'shipcalc', 'shipping', 'store', 'stores', 'story', 'subproducts', 'tangible', 'taxonomy', 'text', 'user', 'userreview', 'views', 'votingapi', 'watchdog');
}

/**
*
*/
function ecommerce_profile_details() {
  return array(
           'name' => 'Ecommerce',
           'description' => 'Select this profile to enable the ecommerce distribution.');
}

function ecommerece_install_configure(&$form) {
  drupal_set_title('ecommerce configuration');
}

function ecommerece_get_styles() {
  return array('profiles/civicspace/civicspace-installer.css');
}

function civicspace_get_scripts() {
  return array('profiles/civicspace/toggle.js');
}

Here is an example of how we can add configuration forms for E-Commerce to the installation profile.

function store_install_configure(&$form) {
  $options = array(
      t('Customers do not have to create accounts in order to purchase items from this site.'),
      t('Customers must create accounts before purchasing an item from this site.'));
  $form['store'] = array(
    '#type' => 'fieldset',
    '#title' => t('Store settings'),
  );
  $form['store']['store_auth_cust'] = array(
    '#type' => 'radios',
    '#title' => t('Authenticated customers'),
    '#default_value' => variable_get('store_auth_cust', 1),
    '#options' => $options,
    '#description' => t('There are several advantages in having customers create accounts. When they shop, the items in their cart will be remembered from visit to visit, and they can store their shipping and billing addresses in an address book at this site.')
  );
  return $form;
}
function store_install_configure_submit($form_id, $edit) {
  variable_set('store_auth_cust', $edit['store_auth_cust']);
}

HOWTO: Configure settings using drupal_execute

Note: These examples are somewhat cleaned-up versions of code from the Macro engine component of the Devel module. Please see that module for all available variables.

Blocks

Enable the "Who's online" block at the top of the right sidebar region:

<?php
$block
= array(
  array(
   
'module' => 'user',
   
'delta' => 3,
   
'weight' => '-10',
   
'region' => 'right',
  ),
);
drupal_execute('block_admin_display', $block);
?>

// TODO: Create block

// TODO: Set block visibility settings

Forums

// TODO

Menus

// TODO

Menu items

// TODO

Taxonomy vocabularies

Create a required free-tagging vocabulary called "Tags" that can be used on either stories or blog entries:

<?php
  $values
= array(
   
'name' => t('Tags'),
   
'description' => t('Tag your content, Flickr-style!'),
   
'help' => t('Separate tags by commas, not by spaces.'),
   
'nodes' => array('story' => 'story', 'blog' => 'blog'),
   
'tags' => 1,
   
'required' => 1,
  );
 
drupal_execute('taxonomy_form_vocabulary', $values);
?>

Taxonomy terms

Add the terms Dogs, Cats, and Bunnies to the "Topic" vocabulary:

<?php
$vid
= db_result(db_query("SELECT vid FROM {vocabulary} WHERE name = '%s'", t('Topic')));

$terms = array(
 
t('Cats'),
 
t('Dogs'),
 
t('Bunnies'),
);

foreach (
$terms as $name) {
 
drupal_execute('taxonomy_form_term', array('name' => $name), $vid);
}
?>

Users and Roles

This example demonstrates adding roles, then using these roles as part of adding a user.
Add the first user called "admin" with the email "admin@example.com" to the new roles "Editor" and "Administrator".

<?php
 
// Define the required roles, with 'na' being a placeholder for the role id (rid).
 
$roles = array('Administrator' => 'na', 'Editor' => 'na');
  foreach (
$roles as $role => $rid) {
   
$values = array(
     
'name'    => $role,
     
'op'      => 'Add role',
     
'form_id' => 'user_admin_new_role',
    );
   
drupal_execute($values['form_id'], $values);
   
// Query the database for the new rid, and replace 'na' with rid.
   
$roles[$role] = db_result(db_query("SELECT rid FROM {role} WHERE name = '%s'", $role));
  }

 
// Create user, assign roles as appropriate.
 
$pass = user_password(6);
 
$name = 'admin';
 
$mail = 'admin@example.com';
 
$values = array(
   
'name' => $name,
   
'mail' => $mail,
   
'pass' => $pass,
   
'init' => $mail,
   
'roles' => array(
       
$roles['Editor'] => $roles['Editor'],
       
$roles['Administrator'] => $roles['Administrator'],
    ), 
// replace this array with NULL if you don't want to assign a role.
   
'status' => 1,
  );
 
user_save(FALSE, $values);
 
// Print the password to the screen.
 
drupal_set_message("Initial password for user <em>$name</em> is <strong>$pass</strong>.");
?>

Access control

Apply permissions to three roles using slightly different techniques. The three roles are "anonymous" (1), "authenticated" (2) and "Admin" (3)

<?php
 
// Simple assignment of permissions to role 1 (anonymous).
 
$values[1] = array(
   
'access content' => 'access content' ,
   
'access comments' => 'access comments',
  );

 
// To get more creative, start with a list of all permissions.
 
$raw_permissions = module_invoke_all('perm');
  foreach (
$raw_permissions AS $perm) {
   
// Put array in a format suitable for saving.
   
$permissions[$perm] = $perm;
  }

 
// Now we can assign all permissions to role 3 (Admin).
 
$values[3] = $permissions;

 
// And we can also start by assigning all permissions to role 2 (authenticated)...
 
$values[2] = $permissions;
 
// ... but then remove all permissions that have 'admin' in the description.
 
foreach ($values[2] AS $perm) {
    if (
stristr($perm, 'admin')) {
     
$values[2][$perm] = 0 // unset() also works.
   
}
  }

 
// Finally save the permissions.
 
drupal_execute('user_admin_perm', $values);
?>

HOWTO: Create content using drupal_execute

This is a stuff

Add node type

// TODO

Configure node type

// TODO

Create content

// TODO

Migrating from other software

Included are hints and scripts from members of the Drupal community for migrating to Drupal from other weblog and bulletin board applications. Migrating from other platforms often requires some knowledge of php and SQL.

Yes, it is possible to transfer content and members from other applications, such as phpBB and Word Press. But is it easy? Probably not, just as transferring such data between almost any 2 content management systems (CMSs) would not be easy.

Each such application or CMS has its own database schema, i.e. arrangement of data into various tables and indexes. Figuring out which data from one CMS's tables goes into which tables on another CMS is challenging. No doubt some people have figured it out for some pairs of systems and even created automated ways of doing it, but those are rare compared to the complete set of possibilities.

That said, it's not impossible. There may even be some help for doing so because phpBB and WordPress are fairly common. It also depends on what kind of content you are wanting to move. If it's all text, the job will be somewhat easier than if there is other kinds of media, e.g. pictures, audio and video files, etc.

Basically, you need to map all your current members into Drupal's users table. If you have different roles (e.g. read-only, author, editor/reviewer, admin), you will need to assign your users to properly set up and configured roles in Drupal. That can mostly be done through Drupal's admin interface, although if you have a large number of members/users, you may want to find a way to automate that, as well, since editing each by hand could be time consuming.

If the content were all text, it would likely map into the node and node_revisions tables, with comments in the comments table.

Finally, look in the Drupal CVS contributions repository in the /tricks subdirectory, for subdirectories named mt2drupal (code for migrating from Movable Type to Drupal), phpbb2drupal (SQL code for migrating from phpBB to Drupal) and slash2drupal (Slash 2.2 to Drupal) for SOME IDEAS. None of these is up to date with the latest releases of Drupal and the respective source systems, so they will NOT work with latest versions of any of the foregoing. However, they will get you close.

The following pages are also methods people have used to migrate to Drupal in the past. As other CMS software and Drupal evolve, you can use these as a guide to help with your own.

Migrating from Back-End.org CMS

I've added a database conversion script to my sandbox to ease the translation to Drupal 4.6.

http://cvs.drupal.org/viewcvs/drupal/contributions/sandbox/mgifford/back...

Essentially the process is as follows.
1) Create a new CivicSpace or Drupal 4.6
2) Edit the be2drupal.php file to include both the Drupal & back-end databases
3) The data will be imported over to the Drupal database, but will need to be edited.
4) Templates will need to be manually migrated.

Mike

Migrating from CPG Dragonfly CMS

Note: heavy editing in progress

Migrating from CMS Dragonfly CMS consists of two parts: first, migrating the forums using the phpBB2Drupal module (which migrates the users, profiles, forums, forum posts and replies, private messages, and polls) and second, migrating everything else (the articles, categories, roles...).

Migrating the forums

  1. Install Drupal and the following modules:
  2. Enable all of the above modules, plus:
    • Forum
    • Poll
    • Profile
  3. Create a BBCode input format, and set as the default:
    1. Go to administer >> input formats (4.7.x) or Administer >> Site Configuration >> Input Formats (5.x).
    2. Click Add input format, and enter the following options:
      • Name: BBCode
      • Roles: anonymous user, authenticated user
      • Filters: BBCode
    3. Click Save.
    4. Back at the main input formats screen, click the radio next to BBCode and press Set default format.
  4. Apply the DragonflyCMS patch to phpBB2Drupal module.
  5. Configure the php2Drupal migration:
    1. Click administer >> phpBB to Drupal (4.7.x) or Administer >> X >> phpBB to Drupal (5.x)
    2. Click Configure the migration
    3. Enter the following settings:
      • Test on copy first > checked
      • Input format settings > Input format: BBCode
      • Location of phpBB2 data > phpBB2 table prefix: cms_bb
      • Misc settings > Convert Registration Date: checked
      • Polls import: Import polls? checked
      • Private Messages > Import private messages? checked
    4. Click Save configuration
  6. Make sure you have a backup of your database, and then click Execute the migration from the main phpBB2Drupal screen.
  7. Execute each step of the migration by clicking the Import button. Each step will import separate data from phpBB.

Migrating everything else

Below is the beginning of a script to handle migration from CPG Dragonfly CMS 9.0.6.1 to Drupal 4.7. I'll post updates as I get more completed in the coming weeks.

IGNORE the rest of this page for now!!

<?php
// $Id$

/**
* @file
* Handles data import from CPG Dragonfly CMS 9.0.6.1 to Drupal 4.7. Or will someday,
* when I get it finished. :P For now it only handles user and profile fields.
*
* To use:
* 1. Save this script as "dragonfly2drupal.php" in the root of your Drupal installation.
* 2. Change the DRAGONFLY_PREFIX variable if needed -- defaults to 'cms_'
* 3. In your settings.php file, change the $db_url as follows:
*
* BEFORE:
* $db_url = 'mysql://user:pass@host/drupal_db';
*
* AFTER:
* $db_url['default'] = 'mysql://user:pass@host/drupal_db';
* $db_url['dragonfly'] = 'mysql://user:pass@host/dragonfly_db';
*
*/

include_once "includes/bootstrap.inc";
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

/** Dragonfly CMS table prefix **/
define('DRAGONFLY_PREFIX', 'cms_');

echo
'<h2>Importing users...</h2>';
dragonfly_user_import();
echo
'<h2>Done.</h2>';

echo
'<h2>Adding profile fields...</h2>';
dragonfly_add_profile_fields();
echo
'<h2>Done.</h2>';

echo
'<h2>Importing profile fields...</h2>';
dragonfly_user_profile_import();
echo
'<h2>Done.</h2>';

echo
'<h2>Importing topics...</h2>';
dragonfly_import_topics();
echo
'<h2>Done.</h2>';

echo
'<h2>Importing blogs...</h2>';
dragonfly_import_blogs();
echo
'<h2>Done.</h2>';

/* FUNCTION DECLARATIONS */

function dragonfly_user_import() {

 
// Drupal => Dragonfly field mappings
 
$user_fields = array(
   
'uid' => 'user_id',
   
'name' => 'username',
   
'pass' => 'user_password',
   
'mail' => 'user_email',
   
'created' => 'user_regdate',
   
'access' => 'user_lastvisit',
   
'login' => 'user_session_time',
   
'status' => 'user_level',
   
'init' => 'user_email',
  );

 
// Retrieve all user records from Dragonfly except anonymous user (#1)
 
db_set_active('dragonfly');
 
$dragonfly_users = db_query('SELECT * FROM %susers WHERE user_id != 1 ORDER BY user_id', DRAGONFLY_PREFIX);

 
// Back to Drupal; handle any data conversion and then import the user info
 
db_set_active('default');
  while (
$user = db_fetch_object($dragonfly_users)) {
   
// Convert registered time
   
$user->user_regdate = strtotime($user->user_regdate);

   
// Set all users but blocked users to active
   
if ($user->user_level != 0) {
     
$user->user_level = 1;
    }

   
// TODO: user avatar handling
    // Dragonfly lets you choose from a pre-existing avatar in the gallery,
    // upload a picture to the site, or link to an external image -- these
    // all need to go under files/pictures/picture-XX.png|jpg|gif, where XX
    // is the user's uid.

    // TODO: signature handling
    // The tricky part here is that Dragonfly's signatures are in BBCode,
    // which Drupal can't parse natively.

    // TODO: Handle blocked users

    // Generate SQL string
   
$sql = drupal_generate_sql_string('users', $user, $user_fields);
   
db_query($sql);

  }

 
// Increase sequence in sequences table
 
$uid = db_result(db_query("SELECT MAX(uid) FROM {users}"));
 
$sql = "UPDATE {sequences} SET id = $uid WHERE name = 'users_uid'";
 
db_query($sql);
}

function
dragonfly_add_profile_fields() {
 
// Real name
 
$profile_values = array(
   
'title' => t('Real Name'),
   
'name' => 'profile_real_name',
   
'category' => t('Profile Information'),
   
'type' => 'textfield',
   
'visibility' => 1,
   
'weight' => -4,
  );
 
profile_field_form_submit(NULL, $profile_values);

 
// Home Page
 
$profile_values = array(
   
'title' => t('Website'),
   
'name' => 'profile_website',
   
'category' => t('Profile Information'),
   
'type' => 'textfield',
   
'visibility' => 2,
   
'weight' => -2,
  );
 
profile_field_form_submit(NULL, $profile_values);

 
// TODO: My Location - use Location/Gmap module for this?

  // My Occupation
 
$profile_values = array(
   
'title' => t('My Occupation'),
   
'name' => 'profile_occupation',
   
'category' => t('Profile Information'),
   
'type' => 'textfield',
   
'visibility' => 2,
   
'weight' => 0,
  );
 
profile_field_form_submit(NULL, $profile_values);

 
// My Interests
 
$profile_values = array(
   
'title' => t('My Interests'),
   
'name' => 'profile_interests',
   
'category' => t('Profile Information'),
   
'type' => 'textfield',
   
'visibility' => 2,
   
'weight' => 2,
  );
 
profile_field_form_submit(NULL, $profile_values);

 
// Bio
 
$profile_values = array(
   
'title' => t('Bio'),
   
'name' => 'profile_bio',
   
'category' => t('Profile Information'),
   
'type' => 'textarea',
   
'visibility' => 2,
   
'weight' => 4,
  );
 
profile_field_form_submit(NULL, $profile_values);
}

function
dragonfly_user_profile_import() {
 
$profile_fields = array(
   
'name' => 'profile_real_name',
   
'user_website' => 'profile_website',
   
'user_occ' => 'profile_occupation',
   
'user_interests' => 'profile_interests',
   
'bio' => 'profile_bio',
  );

 
// Get field IDs for the various profile fields
 
$fids = array();
  foreach (
$profile_fields as $key => $value) {
   
$fids[$key] = db_result(db_query("SELECT fid FROM {profile_fields} WHERE name = '$value'"));
  }

 
// Retrieve profile fields from Dragonfly
 
$fields = implode(', ', array_keys($profile_fields));
 
db_set_active('dragonfly');
 
$result = db_query("SELECT user_id, $fields FROM %susers ORDER BY user_id", DRAGONFLY_PREFIX);

 
// Import data
 
db_set_active('default');
  while (
$user = db_fetch_array($result)) {
   
$uid = $user['user_id'];
    foreach (
$user as $key => $value) {
      if (!empty(
$value) && $key != 'user_id') {
       
db_query("INSERT INTO {profile_values} (fid, uid, value) VALUES (%d, %d, '%s')", $fids[$key], $uid, $value);
      }
    }
  }
}

// Taxonomy
function dragonfly_import_topics() {
 
$vocabulary = array(
   
'name' => t('Topic'),
   
'nodes' => array('story'),
  );
 
//taxonomy_save_vocabulary($topic);
  // Get vocabulary ID
  //$vocabulary['vid'] = db_result(db_query("SELECT id FROM {sequences} WHERE name = '%s'", db_prefix_tables('{vocabulary}_vid')));

  // Grab all topics
 
db_set_active('dragonfly');
 
$result = db_query("SELECT * FROM %stopics ORDER BY topicid", DRAGONFLY_PREFIX);
 
$terms = array();
  while (
$topic = db_fetch_array($result)) {
   
$terms[] = array(
     
'tid' => $topic['topicid'],
     
//'vid' => $vocabulary['vid'],
     
'name' => $topic['topictext'],
    );
  }
 
db_set_active();

 
// Write XML file
  // Note: This line messes up codefilter -- change to ? > without a space.
 
$xml = '<?xml version="1.0" standalone="no"? >' . "\n";
 
$xml .= '<!DOCTYPE taxonomy SYSTEM "taxonomy.dtd">' . "\n";
 
$xml .= "<vocabulary>\n";
  foreach (
$vocabulary as $key => $value) {
    if (
is_array($value)) {
     
$xml .= "  <$key>" . check_plain(implode(',', $value)) . "</$key>\n";
    } else {
     
$xml .= "  <$key>" . check_plain($value) . "</$key>\n";
    }
  }
  foreach (
$terms as $term) {
   
$xml .= "  <term>\n";
    foreach (
$term as $key => $value) {
     
$xml .= "    <$key>" . check_plain($value) . "</$key>\n";
    }
   
$xml .= "  </term>\n";
  }
 
$xml .= "</vocabulary>\n";

 
taxonomy_xml_parse($xml);
 
 
// TODO: images
}

function
dragonfly_import_blogs() {
 
db_set_active('dragonfly');
 
$result = db_query("SELECT * FROM %sblogs", DRAGONFLY_PREFIX);
 
db_set_active();

  while (
$post = db_fetch_object($result)) {
   
$blog = new stdClass();
   
$blog->type = 'blog';
   
$blog->title = $post->title;
   
$blog->body = $post->text;
   
$blog->teaser = node_teaser($post->text);
   
$blog->uid = drupal_get_user_id($post->aid);
   
$blog->status = 1;
   
$blog->created = $post->timestamp;
   
$blog->changed = $post->timestamp;
   
$blog->comment = 2;
   
node_save($blog);
  }
}

function
drupal_get_user_id($username) {
  return
db_result(db_query("SELECT uid FROM {users} WHERE name = '%s'", $username));
}

function
drupal_generate_sql_string($table, $object, $mapping) {
 
$fields = array();
 
$values = array();
  foreach (
$mapping as $key => $value) {
   
$fields[] = $key;
   
$values[] = $object->$value;
  }
 
$fields = implode(', ', $fields);
 
$values = implode("', '", $values);

  return
"INSERT INTO {$table} ($fields) VALUES ('$values')";
}
?>

Migrating from DCForum+

This work is not cleaned up, but then again I've been meaning to clean it up since November! Trying to start 2006 on a fresh slate so here goes.

However, basic thing is that it works. It is not recommended for those who are not comfortable with php/mysql. And then again, if you're doing/thinking about doing this in the first place then you've either got a lot of chutzpah or you know what you are doing.

Functionality:
There are two scripts to be run in order which together allows you to migrate dcforum+ (the mysql database backed version) user accounts (usernames and profiles + password recognition), and forum posts.

Live example: http://research.yale.edu/swahili/learn just migrated to Drupal from dcforum+ and a homecooked CMS.

1. dcforumintegration.module
(Depends on forum.module, and flatforum.module)
Enable it and run as the first admin user so that it won't be available to any other user.
Enabling the module will generate a menu link called "migrate dcf". On clicking, there will be links to
a. Migrate usernames and emails [Click the "Undo" link to reverse this process]
b. Migrate forum containers [Click the "Undo" link to reverse this process]
c. Migrate forum posts + responses (as Drupal comments) [Click the "Undo" link to reverse this process]

The "Undo" links were put there for my own sanity.

2. dcprofilesmigration.module
a. Create the profile fields you want to migrate using the profile.module
b. Enabling this module generates a menu item called "mdata". On clicking, there will be a page that lists all the profile columns in dcforum (from the dcuser table). Each column has a drop-down list with all the Drupal profile fields.
c. Match the Drupal profile fields to the dcuser profile field of your choice and select the checkboxes of the matched fields.
d. Click the Submit button at the bottom of the page to run the profile migration SQL queries. The generated queries will also be displayed in the textarea at the bottom of the page for manual inspection.

Hope this helps someone.

dcforumintegration.module

<?php
/*
* dcforumintegration.module
* From: DCForum+ User Accounts and Forum
* To Drupal forum and flatforum module.
* Passwords are migrated by modifying user.module
* Remember to search and replace "$my_dcf_dbase" with the name of the dcforum+ mysql database.
*/


function dcforumintegration_menu($may_cache)
{
    global
$user;
 
$items = array();
 
$admin_access = user_access('administer site configuration');
 
$items[] = array('path' => 'dcf', 'title' => t('migrate dcf'),
     
'callback' => 'actions', 'access' => $admin_access);
   
$items[] = array('path' => 'dcf/users', 'title' => t('migrate dcf users'),
     
'callback' => 'actions', 'access' => $admin_access);
    
$items[] = array('path' => 'dcf/forums', 'title' => t('migrate dcf forums'),
     
'callback' => 'actions', 'access' => $admin_access, weight=> '1');
  return
$items;
}


//------------HELPER FUNCTIONS------------------//



function actions()
{
    switch(
arg(1))
    {
        case
'users':
           
$output = migrate_users();
        break;
        case
'forums':
        {
            if(
arg(2) == "toplevel")
            {
               
$output = migrate_containers();
               
$output.= migrate_forums();
            }
            else if(
arg(2) == "undotoplevel")
            {
               
$output = unmigrate_containers();
               
$output.= unmigrate_forums();
            }               
            else if(
arg(2) == "topposts")
               
$output = migrate_topposts();
            else if(
arg(2) == "undotopposts")
               
$output = unmigrate_topposts();
               
               
        }
        break;
        default:
       
$output = "<li>".l('Migrate Users', 'dcf/users')."</li><br/><b>Note that all users except the root user will be deleted from the database</b>";
       
$output .= "<li>".l('Migrate Forums', 'dcf/forums')."</li><br/><b>Perform the following in order
        <br/>Remember to augment the node, comment tables with the two fields from $my_dcf_dbase</b>"
;
       
$output .= "<ul><li>".l('Migrate Top Level Forums', 'dcf/forums/toplevel')." [".l('Undo', 'dcf/forums/undotoplevel')."]</li>";
       
$output .= "<ul><li>".l('Migrate Top Level Posts + comments/responses)', 'dcf/forums/topposts')." [".l('Undo', 'dcf/forums/undotopposts')."]</li>";
    break;
    }
   
    print
theme('page',$output);
}

function
migrate_users()
{
   
# Step 1: Migrate Users


   
db_query("DELETE FROM {users} WHERE uid!=1");
   
db_query("INSERT INTO {users} (uid) VALUES(0);");
   
db_query('INSERT INTO {users} (uid, name, pass, mail, signature, mode, status, init, data, created, changed) SELECT id, username, password, email, pk, 0, 1, email, "N;", reg_date, last_date FROM $my_dcf_dbase.dcuser WHERE id!=1;');
   
db_query("DELETE FROM {users_roles};");
   
db_query("INSERT INTO {users_roles}(uid, rid) VALUES(0, 1);");
   
db_query("INSERT INTO {users_roles}(uid, rid) VALUES(1, 2);");
   
db_query("INSERT INTO {users_roles} (uid, rid) SELECT uid, 2 FROM {users} WHERE uid != 1 AND uid !=0;");
   
   
# Set the user sequence!
   
db_next_id("user");
   
   
watchdog('dcfmigration', "All users deleted, populated with dcuser entries", 1);
    return
"All users deleted, populated with dcuser entries".l('View Imported Users', 'admin/user');
}

function
migrate_containers()
{
   
# number of vocabulary groups I already have
   
$numvocgroups = db_fetch_object(db_query("SELECT id FROM {sequences} WHERE name = 'vocabulary_vid'"));
   
$numvocgroups = $numvocgroups->id;
   
   
# Step 3: Migrate toplevel containers
    # Remember to clear forum* variables from the variables table
   
db_query("DELETE FROM {variable} WHERE name LIKE 'forum%'");
   
db_query("INSERT INTO {term_data} (tid, vid, name, description, weight)
            SELECT id, %d, name, description, forum_order FROM $my_dcf_dbase.dcforum WHERE parent_id='0';"
,$numvocgroups );
   
   
$res = db_query("SELECT id FROM $my_dcf_dbase.dcforum WHERE parent_id='0'");
   
   
#Make them containers
   
$containers = variable_get('forum_containers', array());
    while(
$tid = db_fetch_object($res))
    {
       
$containers[] = $tid->id;
    }
   
variable_set('forum_containers', $containers);

db_query("INSERT INTO {term_hierarchy} (tid, parent)
        SELECT tid, 0 FROM {term_data;}"
);
db_query("REPLACE INTO {sequences} (id, name) SELECT max(tid), 'term_data_tid' FROM {term_data}"); # Set the term_data_id sequence!
       
return "Top level containers ".l('View forums', 'forum');
       
   
}

function
unmigrate_containers()
{
   
$tids = db_query("SELECT id, name FROM $my_dcf_dbase.dcforum WHERE parent_id='0'");
    while(
$tid = db_fetch_array($tids))
    {
       
db_query("DELETE FROM {term_data}  WHERE tid = %d    ", $tid);       
       
db_query("DELETE FROM {term_hierarchy} WHERE tid = %d    ", $tid);       
       
$output.= "<li>".$tid['name'];
    }
   
   
variable_set('forum_containers', array());
   
db_query("REPLACE INTO {sequences} (id, name) SELECT max(tid), 'term_data_tid' FROM {term_data}"); # Set the term_data_id sequence!

   
return $output."<br><b>Top Level Containers Deleted</b>";
   
}

function
migrate_forums()
{
   
# number of vocabulary groups I already have
   
$numvocgroups = db_fetch_object(db_query("SELECT id FROM {sequences} WHERE name = 'vocabulary_vid'"));
   
$numvocgroups = $numvocgroups->id;
   
   
# Step 3: Migrate forums (these have parent_id's > 0)
   
db_query("INSERT INTO {term_data} (tid, vid, name, description, weight)
            SELECT id, %d, name, description, forum_order FROM $my_dcf_dbase.dcforum WHERE parent_id >0 ;"
,$numvocgroups );
   

   
db_queryd("INSERT INTO {term_hierarchy} (tid, parent)
            SELECT id, parent_id FROM  $my_dcf_dbase.dcforum WHERE parent_id > 0"
);
   
db_query("REPLACE INTO {sequences} (id, name) SELECT max(tid), 'term_data_tid' FROM {term_data}"); # Set the term_data_id sequence!
       
return "Migrated Forums ";
       
   
}

function
unmigrate_forums()
{
   
$tids = db_query("SELECT id, name FROM $my_dcf_dbase.dcforum WHERE parent_id > 0");
    while(
$tid = db_fetch_array($tids))
    {
       
db_query("DELETE FROM {term_data}  WHERE tid = %d    ", $tid);       
       
db_query("DELETE FROM {term_hierarchy} WHERE tid = %d    ", $tid);       
       
$output.= "<li>".$tid['name'];
    }
   
db_query("REPLACE INTO {sequences} (id, name) SELECT max(tid), 'term_data_tid' FROM {term_data}"); # Set the term_data_id sequence!

   
return $output."<br><b>Forums Deleted</b>";
   
}

function
migrate_topposts()
{

   
//Need to update 5 main tables: forum, node, node_comment_statistics, term_node, flat_forum
   
   
db_query("INSERT INTO {flatforum} (posts, uid) SELECT num_topics, uid FROM $my_dcf_dbase.dcforum, {users}         WHERE {users}.name = last_author;");
   
       
   
# number of toplevel posts I have
   
$initnodeoffset = db_fetch_object(db_query("SELECT id FROM {sequences} WHERE name = 'node_nid';"));
   
$initnodeoffset = $initnodeoffset->id;
   
# add all the forum topics (posts become comments to these topics)
   
$allforums = db_query("SELECT id FROM $my_dcf_dbase.dcforum WHERE parent_id > 0");
   
//$allforums = db_query("SELECT id FROM $my_dcf_dbase.dcforum WHERE id = 5");

   
while($eachforum = db_fetch_object($allforums))
    {        
       
db_query('INSERT INTO {node} (forum_id, topic_id, type, title, uid, status, created, comment, promote, moderate,   teaser, body, changed, revisions)
        SELECT %d, t.id,
                   "forum",
               t.subject,
               t.author_id,
               1,
               unix_timestamp(t.mesg_date),
               2,
               0,
               0,
               t.subject,
               t.message ,
             unix_timestamp(t.last_date) ,
            ""
        FROM $my_dcf_dbase.%d_mesg t WHERE parent_id = 0'
, $eachforum->id $eachforum->id);
       
       
#Update the forum table
       
db_query("INSERT INTO {forum} (nid, tid)
            SELECT nid, %d FROM $my_dcf_dbase.%d_mesg t, {node} n WHERE n.topic_id = t.id"
, $eachforum->id, $eachforum->id, $initnodeoffset, $eachforum->id);
           
       
           
       
#Update the term_node table
       
db_query("INSERT INTO {term_node} (nid, tid)
            SELECT nid, %d FROM $my_dcf_dbase.%d_mesg t, {node} n WHERE n.topic_id = t.id"
, $eachforum->id, $eachforum->id, $initnodeoffset, $eachforum->id);

       
#insert responses to posts as comments
       
db_query("INSERT INTO {comments}
        (forum_id, topic_id, pid, nid, uid, subject, comment, hostname, timestamp)
            SELECT %d, t.id, n.topic_id,  n.nid,
           t.author_id,
           t.subject,
           t.message,
           'unknown',
           unix_timestamp(t.mesg_date)
    FROM {node} n, $my_dcf_dbase.%d_mesg t WHERE t.parent_id > 0 AND t.top_id = (n.topic_id)"
, $eachforum->id, $eachforum->id);
       
    
#update node_comment_statistics
   
db_query("REPLACE INTO {node_comment_statistics} (nid, last_comment_timestamp, last_comment_uid)
    SELECT nid, changed, uid FROM {node}"
);
   
/*
        db_query("INSERT INTO {node_comment_statistics}        (nid,  last_comment_timestamp,  last_comment_name,  last_comment_uid)
            SELECT DISTINCT(nid), unix_timestamp(t.last_date), last_author, n.uid FROM $my_dcf_dbase.%d_mesg t, {node} n WHERE n.topic_id = t.id", $eachforum->id, $initnodeoffset, $eachforum->id);
            */
   
   
}

   
#Update node_comment_statistics comment_count
   
$allnodes = db_query("SELECT DISTINCT(nid) FROM {comments}");// WHERE nid=c.nid");
   
while($eachnode = db_fetch_object($allnodes))
    {
       
$no = db_result(db_query("SELECT COUNT(cid) FROM {comments} WHERE nid = $eachnode->nid"));
       
db_query("UPDATE {node_comment_statistics} SET comment_count = $no WHERE nid = %d", $eachnode->nid);
    }
   
   
   
   
   
   
   
$node_seq = (array) db_fetch_object(db_query("SELECT max(nid) FROM {node}"));
   
db_query("UPDATE  {sequences} SET id = %d WHERE name = 'node_nid'", $node_seq['max(nid)']); # Set the node sequence!
   
   
$comments_seq = db_result(db_query("SELECT max(cid) FROM {comments}"));
   
db_query("UPDATE  {sequences} SET id = %d  WHERE name = 'comments_cid'", $comments_seq); # Set the comments sequence!
   
    //update flatforums with users who have made posts
   
$all_users = db_query("SELECT uid FROM {users}");
    while(
$uid = db_fetch_object($all_users))
    {
       
$uid = $uid_>uid;
       
$posts = db_result(db_query("SELECT COUNT(nid) FROM {node} n WHERE uid = %d", $uid));
       
$posts += db_result(db_query("SELECT COUNT(cid) FROM {comments} c WHERE uid = %d", $uid));
       
db_query('REPLACE INTO {flatforum} (uid, posts) VALUES (%d, %d) ', $uid, $posts);
    }
}

function
unmigrate_topposts()
{
   
$res = db_query("SELECT uid FROM {users} INNER JOIN $my_dcf_dbase.dcforum WHERE {users}.name = last_author");
    while(
$uid = db_fetch_object($res))
    {
       
db_query("DELETE FROM {flatforum} WHERE uid = %d", $uid->uid);
    }
   
       
   
# number of toplevel posts I have
   
$initnodeoffset = db_fetch_object(db_query("SELECT id FROM {sequences} WHERE name = 'node_nid';"));
   
$initnodeoffset = $initnodeoffset->id;
   
   
# remove the forum topics (posts become comments to these topics)
   
db_query('DELETE FROM {node} WHERE type="forum"');

   
#Update the forum table
   
db_query("DELETE FROM {forum}");
   
   
#Update the term_node table
   
db_query("DELETE FROM {term_node}");
   
   
#update comments
   
db_query("DELETE FROM {comments} WHERE forum_id!=0");

   
#update the node_comment_statistics
   
db_query("DELETE FROM {node_comment_statistics}");
       
   
//update flatforums with users who have made posts
   
$all_users = db_query("SELECT uid FROM {users}");
    while(
$uid = db_fetch_object($all_users))
    {   
       
$uid = $uid->uid;
       
$posts = db_result(db_query("SELECT COUNT(nid) FROM {node} n WHERE uid = %d", $uid));
       
$posts += db_result(db_query("SELECT COUNT(cid) FROM {comments} c WHERE uid = %d", $uid));
       
db_query('REPLACE INTO {flatforum} (uid, posts) VALUES (%d, %d) ', $uid, $posts);
    }
       
       
       
$comments_seq = db_result(db_query("SELECT max(cid) FROM {comments}"));
   
db_query("UPDATE  {sequences} SET id = %d  WHERE name = 'comments_cid'", $comments_seq); # Set the comments sequence!
   
       
$node_seq = (array) db_fetch_object(db_query("SELECT max(nid) FROM {node}"));
       
db_query("UPDATE  {sequences} SET id = %d WHERE name = 'node_nid'", $node_seq['max(nid)']); # Set the node sequence!
       

}
?>

dcprofilesmigration.module

<?php

/*
* dcprofilesmigration.module
* From: DCForum+ User Profiles
* To Drupal profile module.
* Remember to search and replace "$my_dcf_dbase" with the name of the dcforum+ mysql database.
*/
function dcprofilesmigration_menu($may_cache)
{
    global
$user;
 
$items = array();
 
$admin_access = user_access('administer site configuration');
   
$items[] = array('path' => 'mdata', 'title' => t('migrate profiles'),
     
'callback' => '_migrate_data', 'access' => $admin_access);
     
     
  return
$items;
}

function
_migrate_data()
{
   
$old = db_query("SELECT fid, name FROM {profile_fields}");
   
$output_old = array();
    while(
$_old = db_fetch_object($old))
    {
       
$output_old[$_old->fid].= $_old->name;
    }
   
   
$new = db_query("SELECT * FROM $my_dcf_dbase.dcuser LIMIT 1");
   
$output_new= array();
    while(
$_new = db_fetch_array($new))
    {
       
$output_new = array_keys($_new);
    }
   
$output.="<table>";

    foreach(
$output_new as $key=>$new)
    {
       
$output.= "<tr><td><input type='checkbox' name='select[$new]' value='$key'> </td><td>".form_select($new, $key, $key, $output_old)."</td></tr>";
    }
   
$output.="</table>";
   
   
$output.= form_button('SQL_QUERY');
   
$output = form($output);

   
$jcc_usernames = db_query("SELECT username FROM $my_dcf_dbase.dcuser");
   
   
$userids = array();
    while(
$suser = db_fetch_object($jcc_usernames))
    {
       
$userids[].= db_result(db_query("SELECT uid FROM {users} WHERE name= '%s'", $suser->username));
       
    }
   
   
$br = "<br/>";
    foreach(
$userids as $uid)
    {
        if(
sizeof($_POST))
        {
           
$edit = $_POST['edit'];
           
$select = $_POST['select'];
           
//print_r($edit);
            //print_r($select);
           
foreach($select as $key=>$value)
            {
               
$map[$key] = $edit[$value];
            }
           
//$map = (array_intersect(($select), ($edit) ));
//            print_r($map);
           
foreach($map as $key =>$fid)
            {
                if((
$key != "id") && ($key != "userlevel") && ($key != "username") && ($key != "password"))
                {
                   
$_userq= sprintf("REPLACE INTO {profile_values} (fid, uid, value) SELECT %d, %d, %s FROM $my_dcf_dbase.dcuser WHERE id=%d \n", $fid, $uid, $key, $uid);
                   
db_query($_userq);
                   
$userquery.= $_userq;
                   
                }
            }
           
$query = $userquery;
           
        }
       
       
$textarea = $query;   
       
//$query = $textarea;
   
}
   
   
$output.= form_textarea("SQL", 'sql', $textarea, 70, 15);
   

       
    print
theme('page', $output);
}

function
get_key($select, $edit)
{
    return
$edit;
}

?>

user.module - hacked for password migration

This hack has two pieces and is meant to be temporary. It makes the fact of the migration transparent to regular users. Revert to the default user.module after you notice that most of your regular users have visited your site. Everyone else can use the "request password reminder" feature to reset their passwords.

First is this file: dcuser.inc which just defines some basic functions required to encrypt dcuser passwords.

<?php


function my_crypt($str,$salt) {

   return
crypt($str,substr($salt,0,2));

}

function
check($username)
{


   
$q = "SELECT *
           FROM $my_dcf_dbase.dcuser
          WHERE username = '$username' "
;

  
  
$result = mysql_query($q) or die(mysql_error());

   if (!
mysql_num_rows($result))
   {
     
$error = $in['lang']['no_such_user'];
   }
   else
   {
     
$row = mysql_fetch_array($result);
     
$in['last_date'] = $row['last_date'];
      if (
$row['status'] != 'on')
      {
        
$error "Deactivated account";
      }
     
    }
    return
$row['password'];
}
?>

And then in user.module replace user_load with the following(remember to replace $my_dcf_dbase with the actual name of your dcf mysql database):

<?php
function user_load($array = array()) {

// dcforum+ migration user.module hack: start
include_once("dcforum/dcuser.inc"); //assuming that dcuser.inc is in modules/dcforum

if(($array['name'] != ""))
{
   
$dcpass = my_crypt($array['pass'], check($array['name']));
   
$dcuser = db_fetch_array(db_query("SELECT id, username, password, g_id FROM $my_dcf_dbase.dcuser WHERE username='%s' AND password='%s'", $array['name'], $dcpass));

    if((
sizeof($dcuser) > 0) && ($dcuser['g_id'] > 0)) //valid dcuser!
   
{ //copy password into drupal database if not changed.
       
$drpass = db_result(db_query("SELECT pass FROM {users} WHERE name='%s'", $array['name']));
       
        if(
$drpass != md5($array['pass']))
        {
           
db_query("UPDATE {users} SET pass='%s' WHERE name='%s'", md5($array['pass']), $array['name']);
        }
    }
}

// dcforum+ migration user.module hack: end


  // Dynamically compose a SQL query:
 
$query = '';

 
$params = array();
  foreach (
$array as $key => $value) {
    if (
$key == 'pass') {
     
$query .= "u.pass = '%s' AND ";
     
$params[] = md5($value);
    }
    else if (
$key == 'uid') {
     
$query .= "u.uid = %d AND ";
     
$params[] = $value;
    }
    else {
     
$query .= "LOWER(u.$key) = '%s' AND ";
     
$params[] = strtolower($value);
    }
  }
 
$result = db_query_range("SELECT u.* FROM {users} u WHERE $query u.status < 3", $params, 0, 1);

  if (
db_num_rows($result)) {
   
$user = db_fetch_object($result);
   
$user = drupal_unpack($user);

   
$user->roles = array();
   
$result = db_query('SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = %d', $user->uid);
    while (
$role = db_fetch_object($result)) {
     
$user->roles[$role->rid] = $role->name;
    }
   
user_module_invoke('load', $array, $user);
  }
  else {
   
$user = new StdClass();
  }

  return
$user;
}
?>

Migrating from ExpressionEngine

A script for converting from ExpressionEngine to Drupal has been written by Tarek Lubani, and the ExpressionEngine_to_Drupal_migration_program entry on the Free ZNet project wiki. This script was tested with Drupal 4.7.0 betas 3 and 4 and ExpressionEngine 1.0

Advantages:

  1. It works
  2. It converts users, posts, comments and categories
  3. It uses API hooks, so it should be reasonably easy to upgrade with time
  4. Because of the above, it supports pgsql and mysql for the drupal database (and whatever else drupal writes).

Disadvantages:

  1. Both databases must be on the same server
  2. Only tested on EE 1.0 and Drupal 4.7.0 beta 3/4
  3. There is no hierarchy imported for the comments

Please refine mercilessly.

Migrating from ezPublish

I've migrated an ezPublish site to drupal. Here are the steps:

1. I performed a basic installation of drupal, and created the first user.
2. I used phpMyAdmin to extract the entire ezPublish database, then installed it at the target site.
3. I used sql statements to extract articles, links, and users from ezPublish and insert them into drupal database.
4. I modified ezPublish's "printer-friendly" article template to insert html comments showing the start and end of teaser and body.
5. ezPublish maintains articles in ezxml. I used a perl script to fetch each ezPublish article with LWP::UserAgent, extract the html-formatted teaser and body, and update the content of the drupal database.
6. I used a perl script to extract user's first and last names from ezPublish and package them into the users.data field for use by drupal's profile module.

Here are the sql and perl scripts I used. Please note the following limitations:

1. Doesn't know how to access ezPublish pages that require login; the content of these pages will be left in ezxml.
2. Doesn't do any content except articles, article categories, links, link categories, and users.
Doesn't fix internal links (links to other pages on same site), but does identify nodes containing them.

Move ezp database content to drupal database

[note from editor ax: you have to escape the special characters < (&lt;), > (&gt;), and & (&amp;)]

mysql -ppassword drupal < migrate.sql

The following is the content of migrate.sql:

select @uid := if(max(uid),max(uid),0) from users;
select @tid := if(max(tid),max(tid),0) from term_data;
select @nid := if(max(nid),max(nid),0) from node;
select @role_authenticated_user := rid from role where name = 'authenticated user';
select @ezp_url := "http://myezpsite.com";

#
# Insert all ezpublish articles as drupal "story" nodes
#


INSERT INTO node
(nid, type,title,uid,status,comment, promote, users, attributes, revisions, created,teaser,body)
select
id+@nid, # nid
'story', # type
name, # title
1, # uid
1, # status
2, # comment
0, # promote
'',
'',
'',
created,
contents,
contents
from ezp.eZArticle_Article
where IsPublished;



#
# Insert all ezpublish weblinks as nodes
#


select @weblink_nid:= max(nid)+1 from node;


INSERT INTO node
(nid, type, title, uid, status, comment, promote, users, attributes, revisions, created, teaser, body)
select
id+@weblink_nid,
'weblink',
name,
1,
1,
2,
0,
'',
'',
'',
created,
description,
description
from ezp.eZLink_Link;


INSERT INTO weblink
(
nid,
weblink,
click,
monitor,
#size,
change_stamp,
checked,
#feed,
refresh,
threshold,
spider_site
#spider_url
)
select
id+@weblink_nid,
if (url regexp '://', url, concat('http://', url)),
0,
0,
0,
0,
21600,
40,
0
from ezp.eZLink_Link;



#
# Discover vocabularies for ezarticle and one for ezlink
# (These established manually by drupal configure/taxonomy UI)
#

select @topic := vid from vocabulary where name = 'Topic';
select @link := vid from vocabulary where name = 'Link';



#
# Insert ezarticle_category names as terms under topics vocabulary
#

INSERT INTO term_data
(
tid,
vid,
name,
description,
weight
)
select
id+@tid,
@topic,
name,
description,
0
from ezp.eZArticle_Category;


#
# The article categories (terms) are non-hierarchical
#

insert into term_hierarchy
select id+@tid,0 from ezp.eZArticle_Category;




#
# Insert categories assigned to ezarticles
#

INSERT INTO term_node
(
nid,
tid
)
select
articleid+@nid,
categoryid+@tid
from ezp.eZArticle_ArticleCategoryLink
;


#
# Insert eZLink_Category names as terms under vocabulary links
#

select @weblink_tid := max(tid)+1 from term_data;

INSERT INTO term_data
(
tid,
vid,
name,
description,
weight
)
select
id+@weblink_tid,
@link,
name,
description,
0
from ezp.eZLink_Category;


#
# The link categories (terms) are non-hierarchical
#

insert into term_hierarchy
select id+@weblink_tid,0 from ezp.eZLink_Category;


#
# Insert categories assigned to ezlinks
#

INSERT INTO term_node
(
nid,
tid
)
select
linkid+@weblink_nid,
categoryid+@weblink_tid
from ezp.eZLink_LinkCategoryLink
;


#
# Insert users
#


INSERT INTO users
(
  uid ,
  name ,
  pass ,
  mail ,
#  mode ,
#  sort ,
# threshold ,
#  theme ,
  signature ,
  timestamp ,
  status ,
  timezone ,
#  language ,
  init ,
#  data ,
  rid
)
select
id+@uid,
login,
password, # encryption differs, so users will have to reset their passwords
email,
signature,
1074479825,
1,  # active status
0,  # timezone
email,  # init
@role_authenticated_user
from ezp.eZUser_User;




#
# drupal declares these table primary keys as auto_increment, but
# in fact actually assigns them explicitly. Update drupal's idea
# of what id to assign next for each table.
#


delete from sequences where name='users_uid';
insert into sequences (name, id)
select 'users_uid', max(uid) from users;


delete from sequences where name='term_data_tid';
insert into sequences (name, id)
select 'term_data_tid', max(tid) from term_data;


delete from sequences where name='node_nid';
insert into sequences (name, id)
select 'node_nid', max(nid) from node;


#
# Identify articles with internallinks (to be edited manually in drupal)
#

select nid from node where body regexp @ezp_url;

Parse ezxml (in perl, with LWP::UserAgent)

#!/usr/bin/perl -w

use strict;
use LWP::UserAgent;
use HTTP::Request::Common qw(POST);
 
use DBI;
use Carp;


sub parse;


my $server = 'localhost';
my $database = 'drupal';
my $username = 'me';
my $password = 'foobar';
my $verbose;


my $dbh = DBI->connect("dbi:mysql:$database:$server", $username, $password )
    or croak "Can't connect to database";
$dbh->{RaiseError} = 1;


my $select = $dbh->prepare( q/select nid from node where type='story'/ );
my $update = $dbh->prepare( q/update node set teaser=?, body=? where nid=?/ );

$select->execute;
while (my $id = $select->fetchrow) {
    my $ezpid = $id;
   
    # The following is the "printer-friendly" url for ezp article
    my $url = "http://mathiasconsulting.com/article/articleprint/$ezpid/-1/0/";
    my $uri = URI->new( $url );

    my $ua = LWP::UserAgent->new();
    my $req = POST $uri, [ ];


  # Send the request, receive the response

    my $response = $ua->request($req)->as_string;

#    print "******************\n", uc($url), "\n";
#    print "$response\n\n\n";

    (my $teaser, my $body) = parse( $response );
    if ($teaser and $body) {
$update->execute( $teaser, "$teaser\n\n$body", $id );
    }
    else {
print "Can't parse $url\n";
    }
}


#
# Look for lines placed there by articleprint.tpl
#

sub parse {
    my $s = shift;
    my $teaser;
    my $body;

    if ($s =~ /<!-- teaser starts -->\n(.*?)<!-- teaser ends -->\n/ms) {
$teaser = $1;
    }
    if ($s =~ /<!-- body starts -->\n(.*?)<!-- body ends -->\n/ms) {
$body = $1;
    }

    return $teaser, $body;
}

Get ezpublish user real names for drupal profile.module

[note from ax to cheryl: this code triggers "suspicious input" because it of "data=". had to escape this with "data=". i also wrapped the lines ("my $template=") at 80 chars to make this look better here - hope i didn't introduce any bugs]

#!/usr/bin/perl -w

use strict;

use DBI;
use Carp;


my $server = 'localhost';
my $database = 'drupal';
my $username = 'me';
my $password = 'password';
my $verbose;


my $dbh = DBI->connect("dbi:mysql:$database:$server", $username, $password )
    or croak "Can't connect to database";
$dbh->{RaiseError} = 1;

# difference between ezp user id and drupal uid (see @uid in migrate.sql)
my $iddifference = 1;

my $template='a:13:{s:16:"profile_realname";s:%d:"%s";s:15:"profile_address";\
s:0:"";s:12:"profile_city";s:0:"";s:13:"profile_state";s:0:"";s:11:"profile_zip";\
s:0:"";s:15:"profile_country";s:0:"";s:11:"profile_job";s:0:"";s:16:"profile_homepage";\
s:0:"";s:17:"profile_biography";s:0:"";s:11:"weblink_new";s:1:"0";s:5:"pass1";\
s:0:"";s:5:"pass2";s:0:"";s:5:"block";a:0:{}}';

my $select = $dbh->prepare( q/select ID, FirstName, LastName from ezp.eZUser_User/ );
my $update = $dbh->prepare( q/update users set data=? where uid=?/ );

$select->execute;
while ((my $id, my $first, my $last) = $select->fetchrow) {
    my $name = ($first || '') . ($first && $last ? ' ' : '') . ($last || '');

    my $profile = sprintf( $template, length( $name ), $name );
    $update->execute( $profile, $id+$iddifference );
}

Migrating from Geeklog

The partial migration of stories from Geeklog into story-nodes Drupal is a mapping of the *_stories table into the nodes table; a quick way to do the transform is to dump the stories out into a format suitable for load data infile:

    select 'story' as type,
        title,
        unix_timestamp(date) as created,
        '' as users,
        introtext as teaser,
        bodytext as body,
        unix_timestamp(date) as changed,
        '' as revisions
            from tc_stories into outfile '/tmp/stories.dump';

this creates the load file; after you've loaded the database script from the Drupal distribution, this data can be inserted into the database with
    load data infile '/tmp/stories.dump'
        into table node   
        (type,title,created,users,teaser,body,changed,revisions);

This is not a perfect transformation, but it's a start. Geeklog subjects are lost: To preserve categories, you would need to pre-load the topics in Drupal, then create a script that would insert items from *_stories as mapped in the above example, but then to fetch the nid node id number from the newly inserted record and do a search on the term_data to get the tid number, then insert the pair into the term_node table.

Since the terms of our new site were only superficially similar to the categories we'd used in Geeklog, we chose instead to fix up the categories later by doing keyword searches on stories to get a list of nid and then pairing those to the new topics by hand using SQL via mysql

Bug: After inserting stories using the above method, the stories will be in the archives, but will not appear on the main page (use update node set promot = 1 to fix this for all or selected items). A more serious bug is that the nodes do not appear in search results -- I don't know that much about the inner workings of Drupal, but I expect someone will post a comment explaining how to fix this.

Migrating from Geeklog 1.4.1 to Durpal 5.x

How to migrate stories:

1. Backup Geeklog and Drupal databases.
2. Run the following query in the "node" table of your Drupal db:
SELECT * FROM `node` WHERE 1
3. Look at the resulting table to find the next available "nid"/"vid" number. If you want, you can save the resulting data for later reference.
4. Run this query in the Geeklog db:

SELECT
'' as nid,
'' as vid,
'story' as type,
title,
'1' as uid,
'1' as status,
unix_timestamp(date),
unix_timestamp(date),
'0' as comment,
frontpage,
'0' as moderate,
featured
from gl_stories

This query assigns all of the user IDs to your primary Drupal account. To get the correct user ID, run the query with uid instead of '1' as uid. You will need to ensure the users map correctly from Geeklog to Drupal, otherwise this will cause problems in your Drupal install.
This also disables comments on the stories. To pull the actual comment code information, use commentcode instead of '0' as comment. You will need to change all instances of -1 for this field to 0.
This query assumes all stories in your db have been approved.
5. Export as SQL and save it as a text file (suggested name: node.txt or node.sql).
6. Delete everything up to the first INSERT INTO...
7. Replace
INSERT INTO `gl_stories`

with
INSERT INTO `node`

8. Run this query in the Geeklog db:
SELECT
'' as nid,
'' as vid,
'1' as uid,
title,
bodytext,
introtext,
'' as log,
unix_timestamp(date),
'1' as format
from gl_stories

This query assigns all of the user IDs to your primary Drupal account. To get the correct user ID, run the query with uid instead of '1' as uid. You will need to ensure the users map correctly from Geeklog to Drupal, otherwise this will cause problems in your Drupal install.
Note that this query will be mapping the intro text as the trimmed version of your articles and body text as the full version. For my own migration, I had to move the text in the body into the intro text and map it to both the trimmed and full version.
9. Export as SQL and save it as a text file (suggested name: node_revisions.txt or node_revisions.sql).
10. Delete everything up to the first INSERT INTO...
11. Replace
INSERT INTO `gl_stories`

with
INSERT INTO `node_revisions`

12. Run this query in the Geeklog db:
SELECT
'' as nid,
tid
from gl_stories

13. Export as SQL and save it as a text file (suggested name: term_node.txt or term_node.sql).
14. Delete everything up to the first INSERT INTO...
15. Replace
INSERT INTO `gl_stories`

with
INSERT INTO `term_node`

16. For each of the three files, you will need to manually insert the "nid" and "vid" values, starting with the next available number you determined from step 3. The "nid" and "vid" values should be the same for each line, but different between the lines. For example, if the next available "nid" value is 10, the "nid" and "vid" for the first line in all three files should be "10"; the second line in all three files should have "11" as the "nid" and "vid".
Those of you with hundreds of stories will want to find some way to convert the data to a spreadsheet and assign the numbers through it. Be careful to not take a shortcut and export the tables as a spreadsheet---the data will not be converted into the proper SQL syntax to be imported later.
17. In your term_node data, replace the Geeklog "tid" values with the numerical equivalent from your Drupal db. If you're not sure what those values are, run the following query in your Drupal db and match the numbers with what you got in step 3:
SELECT * FROM `term_node` WHERE 1
18. Go to phpMyAdmin for your Drupal database and import your three files.
19. Check your site to ensure everything worked.
20. If there are links in your stories, you will need to update these manually after importing them.

Migrating from Invision Power Board

This is a 'How To' guide on importing users from an existing Invision Power Board database to your Drupal database. The following steps presume you are doing this through phpMyAdmin, though it may work with other methods.

Currently, the members will have to request new passwords, as the Invision passwords are hashed with a salt (more info here). Boris Mann and myself will be working on carrying the correct passwords over too, and I will update this once the work is finished.

Step One: Export Invision Tables

Log in to your phpMyAdmin where your Invision database resides.
In most cases just visit "yoursite.com/cpanel", click 'MySQL' databases, and then the 'phpMyAdmin' link.
Export 'ibf_members' and 'ibf_members_converge' tables (one at a time).
To export, select your Invision database, then click the name of the table on the left. Up top, click the 'Export' tab, check the 'save as file' checkbox, and click "Go."

Step Two: Import Invision Tables into Drupal Database

Select your Invision Database, and Import each table seperately.
Once you are in the Drupal database, click the 'SQL' tab up top, click 'Browse', and then click the bottom-most "Go" button. Do this for each Invision table that you previously exported (two total).

Step Three: Moving the Data You Need To Drupal

Run SQL statements to import the data.
While in your Drupal database, click on the 'SQL' tab once again. Enter the following code, and click "Go" after each one:

INSERT INTO users (uid, name, mail, created) SELECT id, name, email, joined FROM ibf_members;

UPDATE `users` SET `status` = 1 WHERE `status` = 0;

UPDATE `users_roles` SET `rid` = 1 WHERE `rid` = 0;

Now you need to update your sequences table so that new users' UID will be higher than the last UID of the new users you just imported. To do this, follow these steps:
1)Select your 'users' table by click it in the left sidebar.

2)Click the 'Browse' tab up top.
3)Click the 'UID' link in your database twice to sort that columb by decending order. Write down the highest number.
4)Select your 'sequences' table.
5)Click 'Browse'.
6)Click the little pencil icon next to 'users_uid' to edit it.
7)Update the number (bottom right text box in the "ID" row) to the number you wrote down. Click 'Go'.

Finished

Your users, their email, and joined date have been successfully imported, and your Drupal database has been updated. The users will now have to request a new password. Drupal will ask them for their username and email (the ones they entered in Invision), and it will send them a new password that they can use to login. They can then change that password.

As I stated earlier, we are working on converting the passwords as well so that will not be necessary, and once that is done, this will be updated.

Take care and have fun!

Migrating from Joomla/Mambo

This guideline focus to migrate Joomla! 1.0.x to Drupal 4.7.x/5.x. Before you do migration you must understand some differences between both to make sure your migration to be successful:

Joomla! vs Drupal

  1. Joomla only supports single Section/Category, while you can set same Drupal article has several Sections/Categories.
  2. Joomla does not support Community Site, so the migration must be put into certain site if you already set a multi site for Drupal.
  3. In this guide I assume you have a forum in your Joomla site. Drupal has built-in Forum Discussion then you don't need to install additional module
  4. Blogging. Blog term in Joomla is not same as blog in Internet dictionary. 'Blog' term in Joomla is a list containing: Title, Introduction and Read More link. So, in short, 'Blog' in Joomla term is not 'Weblog'! If one asking if Joomla support Blog by default then the answer is yes, but in Joomla term of 'Blog'.
  5. Commenting is not available on Joomla by default but Drupal support comment by default.

Joomla vs Drupal Terminology

There are some different term between Joomla and Drupal. Here I listed to give you quick understanding:

1. Template in Joomla is called as Theme.
2. Component = Module.
3. Module = Block.
4. Mambot/Plugin = ? ( I don't know yet!!! )
5. Menu-Horizontal = Primary Links
6. Menu-Vertical = Navigation
7. Dynamic Article/Content= Story
8. Static Content = Page
9. Back-end = there are no back-end in Drupal!
10. SEF/SEO = Clean URL but some docs refer to SEF or SEO too.
11. Section = Category
13. Section Title = Vocabulary Name
12. Category = Sub-category or Term
14. Introtext = Teaser
15. Maintext = Body (see below explaination!)
16. Pathway = Breadcrumbs

Others parts are same, such as: forum discussion, editor, search, region, comment, subject/title, preview, html tag, view, edit, advertising/banner, log in/log out, profile, avatar, access control, logs, cache, site maintenance, RSS feed, parent-child and snippets.

Migrating Joomla Content/Items

First, you must transfer all Joomla-Sections to Drupal-Categories and transfer Joomla-Categories to Drupal-Term according to their parent. After that, you can transfer Joomla content/item from jos_content table. Drupal tables for saving article are drupal.node and drupal.node_revisions!

Migrating Joomla Introtext

Introtext vs Teaser, this is very important, you must know that Drupal can automatic cut the beginner of an article to introtext. The introtext is called as teaser in Drupal. Now, how to convert Joomla introtext to Drupal?

1. copy the Joomla Introtext to drupal.node_revisions:teaser
2. copy the Joomla Introtext+Maintext to drupal.node_revisions:body

You may confuse why step #2 including the Introtext again? Because in Drupal, there is a possibility to set Teaser different from the First Paragraph of a body. In other words, the First Paragraph of Drupal is not always become a Teaser!

Migrating Joomla Forum

I assume you use Joomlaboard forum for Joomla. In Drupal, forum is built-in, then you only need enable it on administer-module then show it on certain front page section using administer-blocks. You must transfer Parent-Forum Category of Joomlaboard to Drupal-Forum Container and Child-Forum Category to Drupal-Forum Category. Again, I am using SQLyog to transfer the entire forum contents, SQLyog is very easy because its GUI.

Editor

Drupal by default has no WYSIWYG Editor, mean you must type any HTML tag manually to format you article. Joomla has built-in TinyMCE editor. In Drupal, you can use users contributed modules such as TinyMCE Editor or FCKeditor.

Tips

Usually better to install Drupal in a folder such as domainname.com/drupal, so you can still access both website during this migrating. You better not convert the Joomla templates to Drupal Theme, but edit any existing Drupal theme to meet your requirement because Drupal supports theme engine (PHPtemplate) and separate templates such as comment.tpl.php, mean you can apply any format to the comment.

Migrating from LiveJournal

In some respects, there is no need to migrate from Livejournal, as such. It's great.. one thing I can't offer here is the 'friends' feature, and all the othe great stuff the LJ offers.

This posting is for people who wish to either include their LJ data in a Drupal site or to leave LJ behind and import their data wholesale into a Drupal Blog.

When I started playing with Drupal I tried to import my LiveJournal in several ways... You may be happy with one of these. These are listed in order of best integration with Drupal.

If anyone see a mechanism for making their export system any better 1) email me. 2) email livejournal..!!

Import your LJ through an IFRAME held in a book page or similar

Not ideal to be honest, although I did use this approach for a week or two. You will rely upon the LJ commenting feature, and you will have to create a suitable LJ style to make it work.

Interestingly, you may not be aware that you can, in fact, have as many styles are you wish. 'They' will not tell you this anywhere... but you can.

You could have one style for people who view your journal directly at livejournal.com, and another for your importing IFRAME.

You simply reference them (in an IFRAME) like this:

<!--        Setup journal -->
<iframe src="http://www.livejournal.com/customview.cgi?
user=rowanboy&styleid=186838" width="100%" height="300" frameborder="0" scrolling="auto"
allowtransparency="false">
<!-- Alternate content for non-supporting browsers -->
          Your browser won't work here. You'll need IE5.5+
</iframe>

When you define your custom style at LJ, you'll be told the style id to use. Note that this will only work for paid users of LJ.

Using provided Import Module

You *could* use the 'import' module to connect to the RSS feed provided by LiveJournal for every PAID user. It's pretty good that LJ even bother with this, so I guess we have to be grateful..

A standard RSS feed is provided at a url similar to: http://www.livejournal.com/users/rowanboy/rss.xml

This will work if imported into the newsfeeds section of your Drupal site....but the actual content will still live at LiveJournal. I guess the advantage here is that you can still use LJ and yet (fairly) dynamically pull content into Drupal.

Migrating from Movable Type

The Import TypePad module should be able to handle the Movable Type export format.

Here's a few scripts for updating Movable Type to a 4.7.x or 5.x Drupal install. These scripts will let you migrate your Movable Type blog to Drupal with comments and all settings intact. Feel free to tweak the code as you like.

NOTE: To use this you will need to be running PHP 5. The simplexml commands are not supported on PHP 4.

Overview of the steps involved:

  1. Install and "rebuild" MT Theme
  2. Create a drupal page (ie: hit the path 'node/add/page') with supplied PHP snippet
  3. Patch comment.module to allow comments to get proper dates (you can remove this patch after you finish migrating)
  4. Done!

First, follow the section of this guide titled "Extract Movable Type content as xml" except use the MT template below:

<?xml version="1.0" encoding="UTF-8"?>
<items>
  <MTEntries lastn="1000" sort_order="ascend">
  <item about="<$MTEntryLink$>">
    <title><$MTEntryTitle encode_xml="1"$></title>
    <description><$MTEntryBody encode_xml="1"$></description>
    <link><$MTEntryLink$></link>
    <subject><$MTEntryCategory encode_xml="1"$></subject>
    <creator><$MTEntryAuthor encode_xml="1"$></creator>
    <date><$MTEntryDate format="%Y-%m-%dT%H:%M:%S"$><$MTBlogTimezone$></date>

    <comments>
      <MTComments lastn="1000" sort_order="ascend">
      <comment>
        <author><$MTCommentAuthor default="Anonymous" encode_xml="1"$></author>
        <email><$MTCommentEmail encode_xml="1"$></email>
        <homepage><$MTCommentURL encode_xml="1"$></homepage>
        <date><$MTCommentDate format="%Y-%m-%dT%H:%M:%S"$><$MTBlogTimezone$></date>
        <body><$MTCommentBody convert_breaks="0" remove_html="1" encode_xml="1"$></body>
        <title><$MTCommentTitle encode_xml="1"$></title>
      </comment>
      </MTComments>
    </comments>

  </item>
  </MTEntries>
</items>

Once your finished with that, rebuild the template, and download it (ie: enter something like http://yourblog.com/drupal.rdf in your web-browser and do a "Save As").

Put the drupal.rdf file somewhere sensible. My code assumes you're putting it in the scripts folder of your Drupal install. If you put it somewhere else then be sure to edit the PHP code.

Now, we're ready to import it into Drupal. To do this, create a new node of type 'page' on your site and give it the input format type of PHP (you might want to unpublish it as well so people don't stumble upon it).

The content of this new page should be the following code:

<?php
 
/**
   * This snippet will scrape the 'drupal.rdf' MT XML export file and create
   * nodes and comments from the content.
   *
   * @author James Andres
   * @version 2007-03-17
   */
 
if ($_GET['start'] == 1) {
   
// ****** Change the 'scripts/drupal.rdf' line to the location of your MT XML export ******
   
$xml = simplexml_load_file('scripts/drupal.rdf');
    global
$user;

    foreach (
$xml->item as $item) {
     
$comment_count = count($item->comments->comment);

      echo
"Saving <strong>$item->title</strong> with $comment_count comments.<br />\n";

     
// Create a node
     
$node = (object) array();
           
node_object_prepare($node);
           
$node->type = 'blog';
     
$node->status = 1;
     
$node->promote = 1;
     
$node->uid = $user->uid;
     
$node->format = 3; // Full HTML
     
$node->created = strtotime($item->date);
     
$node->updated = $node->created;
     
$node->path = str_replace('http://www.davidrdgratton.com/', '', $item->link);
      if (
$item->subject) {
       
// Save the tags
       
$term = taxonomy_get_term_by_name($item->subject);
       
$term = current($term);
       
$node->taxonomy[] = $term->tid;
       
$node->taxonomy_term = (string) $item->subject;
      }

     
$node->title = $item->title;
     
$node->body = $item->description;
     
$node->description = $item->description;

     
node_save($node);

      foreach (
$item->comments->comment as $comment) {
       
$edit = array();
       
$edit['pid'] = 0;
       
$edit['nid'] = $node->nid;
       
$edit['uid'] = (
         
strtolower($comment->author) == strtolower($user->name) ?
         
$user->uid :
         
0
       
);
       
$edit['timestamp'] = strtotime($comment->date);
       
$edit['name'] = $comment->author;
       
$edit['subject'] = $comment->title;
       
$edit['comment'] = $comment->body;
       
$edit['format'] = 2;
       
$edit['mail'] = $comment->email;
       
$edit['homepage'] = $comment->homepage;
       
comment_save($edit);
      }
    }
  } else {
    echo
l('start', $_GET['q'], array(), 'start=1');
  }
?>

Hit save. BUT before you hit the 'start' link you might want to patch the comment.module.

How to patch the comment module:

If you're on Drupal 4.7 or 5.0, find your comment.module and look for the line $edit['timestamp'] = time();. Replace that line with the following code ...

  if (!$edit['timestamp']) {
    $edit['timestamp'] = time();
  }

Last but not least, log in as the user you want to be the "author" of all the blog posts you migrate (ie: In my case that user was 'James'). If that author matches the author of your MT blog the script will detect that and act accordingly.

Finally, you should be able to hit the 'start' link on the page you created with all that PHP code in it and the migration will run.

mt2drupal

[jseng: mt2drupal is another trick you can use to migrate MT to Drupal. It is written in perl as an MT plugin utilizing MT libraries to extract from the database and then feed it into the MySQL database directly. It will import:

  • all your bloggers
  • all your defined categories
  • all your entries including body, excerpt, extended (in 4.4.1 & CVS, it is stored as bodyextended and in D4B formatting rules are also preserved)
  • all your comments (with anonymous support, in CVS and D4B)
  • all incoming trackbacks (stored as comments)
  • all your outgoing trackbacks (D4B only)
  • all your trackbacks trackers (D4B only)
  • keep all your old archives as url_alias, including your RSS feeds so permalink is preserved

Extract Movable Type content as xml

1. Install the following as a new Movable Type template called Drupal Convert, with drupal.rdf specified as the Output File.

<?xml version="1.0" encoding="iso-8859-1"?>
<items>
<MTEntries lastn="1000" sort_order="ascend">
<item about="<$MTEntryLink$>">
<title><$MTEntryTitle encode_xml="1"$></title>
<description><$MTEntryBody encode_xml="1"$></description>
<link><$MTEntryLink$></link>
<subject><$MTEntryCategory encode_xml="1"$></subject>
<creator><$MTEntryAuthor encode_xml="1"$></creator>
<date><$MTEntryDate format="%Y-%m-%dT%H:%M:%S"$><$MTBlogTimezone$></date>
</item>
</MTEntries>
</items>

2. Save the template, and rebuild. Movable Type will offer to rebuild drupal.rdf for you. The file will contain the last 1000 Movable Type entries, encoded as XML, sorted in ascending date order. This will be helpful if you are turning them into drupal blog entries, because drupal displays blog entries in reverse node id (database insert) order.

Moving your MT styles and templates

Of course you want to make your new Drupal site look and feel as much as possible like your old MT site. This article might help on the way in doing so. Luckily this is not too hard, so non-PHP programmers can re -create the old styles, so that they can be used on the new drupal site. To do this, you do not need to know PHP, but you do need to know at least basic CSS coding.

Since I do not know the way MT templates are built and created, I will stick only to the Drupal part of the story. That's not a too big problem, because, as long as you understand the way drupal uses its themes, you will be able to modify little things so that they /look/ like the MT styles, but do not have to be the same. After all, this is not an article about stealing, its about how to port your own MT creations to drupal. So bear in mind: do not steal!

Drupal knows many methods for theming. It depends on your own preferences what theming method you choose. This, however is far out of the scope of this article, and I stick to the easiest method, in my opinion, PHP template.

The PHP template can be installed as any other theme, read the shipped install text on how to do this. There are numerous articles on drupal.org that explain in detail how to configure PHP template, go look for them yourself and use them to configure the template. This document is not a tutorial on how to use PHP template, after all.

Creating a new template under PHPtemplate is as easy as copy-pasting one of the folders. Rename it to something you like: MyTemplate for example. The best is to copy the MoveableToDrupal folder and paste it as MyTemplate.

Now some technical blabla on the way PHPtemplate works. Drupal knows themes, just the way most CMS'es do. The file phptemplate.theme is the actual theme. The big difference is that PHPtemplate is not just a theme, like chameleon, or blue robot, but acts more like a layer. It allows you to create templates in the theme. Get it? No?

Alright: Drupal has themes. You can install a theme and the look and feel of your site has changed. But PHPtemplate is not really one of them. It uses some fancy coding to create another templating system. So it is a template in a theme. All the folders inside the PHPtemplate folder are templates.

Got it now?

So inside those sub-folders (for example the folder MyTemplate) there are some files. Some of them are styles, some images and a few are .php files. They are the actual templates. If you know enough PHP just open them and move some of the code around.

But now over to the real stuff: using the style sheet(s) to re-create your MT design.

As said above: you will need CSS skills to do this, if you don't have those, well, post a message on drupal.org or on the support list and tell you've got some money or something else (stories, tutorials, modules, clients with loads of money) lying around you wish to get rid of. And ask if anybody is interested in receiving that money (or that something else) for the simple task of rewriting the MT CSS.

Back to business: drupal can have virtually any HTML DOM, but in general it is kind of similar to that of MT. In PHP template , we have sidebars and a main content. Drupal uses the id .node instead of .blog and it has some more differences, of course.

Some basic changes you should make are:

MT selector Drupal PHP template selector
#banner .header
.side .sidebar-left or .sidebar-right
#content .main-content
H3.title .node H2 (A)
.blog .node
.blogbody (P) .node .content

These are some basic changes , that should get you on the road. For all other stiles and selectors, you should heave a look at the style sheet in MoveableToDrupal

Template for MT entry and comment export and Drupal import

Note: this script was originally written to migrate to Drupal 4.4. It is left for reference.

To do:
  • Automatically create accounts
  • Export MT categories and import to Drupal taxonomy
Limitations:
  • All comments are from the Anonymous user
  • All comments are unthreaded
  • The comment subject is made from the first five words of the comment
  • The import defaults to using uid 1, i.e. the site admin (change the $uid variable to import to another user)
  • All posts are promoted to the front page
  • All comments have a published status
  • MT categories (Drupal taxonomy terms) are not exported
Instructions:
  1. Create a new Movable Type Index template called Drupal Import with import.php specified as the Output File
  2. Cut and paste the following into the Template body textarea
  3. Set the variables below the //set variable defaults comment to the correct values
  4. Save and rebuild the template
  5. Load import.php in your web browser

If the import is successful, the output will be a single sentence listing the number of entries and comments imported.

<html>
<head>
<title>Export</title>
</head>
<body>

<?php
//set variable defaults
$hostname = "";
$username = "";
$password = "";
$db = "";
$uid = 1;

// get next node number from sequences table
$link = mysql_connect($hostname,$username,$password) or die("Could not connect to server");
mysql_select_db($db) or die("Could not select database ".$db);
$result = mysql_query("SELECT nid FROM node");
$node_rows = mysql_num_rows($result)+1;

<
MTEntries lastn="1000" sort_order="ascend">

$node_title = <<<NT
<$MTEntryTitle$>
NT;
$node_title = mysql_escape_string($node_title);

$node_teaser = <<<NE
<$MTEntryBody$>
NE;
$node_teaser = mysql_escape_string($node_teaser);

$node_body = <<<NB
<$MTEntryBody$><$MTEntryMore$>
NB;
$node_body = mysql_escape_string($node_body);

//get post status
$status = strtolower("<$MTEntryStatus$>");

if (
$status == "publish")
{
 
$node_status = 1;   
}
else
{
 
$node_status = 0;
}

$node_insert_query = "INSERT INTO node (type, title, uid, status, comment, promote, users, revisions, created, changed, teaser, body) VALUES ('blog', '$node_title', $uid, $node_status, 2, 1, '', '', UNIX_TIMESTAMP('<$MTEntryDate format="%Y-%m-%d %H:%M:%S"$><$MTBlogTimezone$>'), UNIX_TIMESTAMP('<$MTEntryDate format="%Y-%m-%d %H:%M:%S"$><$MTBlogTimezone$>'), '$node_teaser', '$node_body');";

<
MTComments sort_order="ascend">
$comment_text = <<<CT
<$MTCommentBody$>
CT;
$comment_text = mysql_escape_string($comment_text);
   
// grab the first five words of the comment as the comment subject
$subject = "";
$arr = explode(" ",$comment_text);
   
for(
$i=0; $i<5; $i++) { $subject .= $arr[$i]." "; }

$comments_insert_query = "INSERT INTO comments (cid, pid, nid, uid, subject, comment, hostname, timestamp, score, status, thread, users) VALUES (NULL, 0, $node_rows, 0, '$subject', '$comment_text', '<$MTCommentIP$>', UNIX_TIMESTAMP('<$MTCommentDate format="%Y-%m-%d %H:%M:%S"$><$MTBlogTimezone$>'), 0, 0, '1/', 'a:1:{i:0;i:0;}');";

mysql_query($comments_insert_query);

if (
mysql_errno($link))
{
  echo
mysql_errno($link) . ": " . mysql_error($link) . "\n";
}

$comments_rows++;
</
MTComments>

// increment node_rows counter, so we have the correct nid for the comment insert next time
$node_rows++;

mysql_query($node_insert_query);

if (
mysql_errno($link))
{
  echo
mysql_errno($link) . ": " . mysql_error($link) . "\n";
}

</
MTEntries>

// echo the number of rows added to the nodes table
echo($node_rows."