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
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.
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".
Changes to the Drupal core are generally of three types:
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.
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)".
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.
Summary
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.
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.
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.
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?
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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
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.
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:
(adapted from http://drupal.org/node/118045)
The following criteria are used by core developers in reviewing and approving
proposed changes:
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.
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.
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.

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.
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.
These are bugs that hold back a release
These are bugs that do not hold a release:
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:
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:
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.
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/.
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.
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.
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
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).
The following tips might improve the chances of your contributions being accepted. Supplying patches is how to make changes to core or contributed code.
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
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.
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.
Use an indent of 2 spaces, with no tabs.
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;
}
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 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 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.
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.
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.
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.
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 $
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:
Use "example.com" for all example URLs, per RFC 2606.
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 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.
If you need to define global variables, their name should start with a single underscore followed by the module/theme name and another underscore.
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 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.
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.
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.
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.
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.
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.
This describes how to configure Eclipse to play nicely with Drupal.
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
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
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.
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
If you enjoy syntax highlighting, it is may be worth remembering that many of Drupal's php files are *.module or *.inc, among others.
vim -u ~/.vimrc-drupalTo make this easier (using bash on Linux), you could create an alias by typing:
alias vid="vim -u ~/.vimrc-drupal"
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
This article is a stub, please help expand and correct this
Using empty() is reported to be about 20% faster than comparison with '' or "" (zero-length strings).
So
<?php
if ($var == "") {
}
?>
<?php
if (empty($var)) {
}
?>
Use an indent of 2 spaces, with no tabs. No trailing spaces.
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:
http://drupal.org/node/2497/
-->Some commonly misused keywords: TIMESTAMP, TYPE, TYPES, MODULE, DATA, DATE, TIME, ...
See also [bug] SQL Reserved Words.
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
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 users_sid_idx.References:
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.
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';
?>
<?php
$string = "Foo $bar";
?>
When you include icons, take a few guidelines into consideration:
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.
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.
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:
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.
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) {}
?>
<?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.
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)) {
?>
<?php
if ($errno & (E_ALL )) {
?>
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 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.
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;
}
?>
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;
}
?>
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 $
// $Id$.
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:
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'])
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.
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.
Please provide us with a detailled report. As a minimum we need:
If you report a previously unknown vulnerability to the Drupal security team, you will be credited in the security announcement.
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);
?>
SELECT cust_id, cust_name, cust_email FROM customers WHERE category = ''; DROP TABLE customers; --'
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 " 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 />';
}
?>
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);
?>
$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/*'
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);
?>
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);
?>
[This section is work in progress]
Allowing users to manage files is a potentially dangerous operation.
You need to make sure that users cannot
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);
}
?>
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:
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:
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).theme('placeholder'): the placeholder text is plain-text.Some places require HTML which might not be obvious:
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.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.
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.
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:
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.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.
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) .'">'; ?>
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().
[This section is under construction].
A few general guidelines:
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.
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.
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.
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.
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).
An alternative method exists:
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.
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.
Drupal has two repositories which can be checked out (downloaded to your local computer):
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.
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.
One of the most important and powerful features of CVS is the ability to create tags and branches.
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.
The following content is in part sourced from contributions/README.txt in the Drupal CVS repository.
Jump to:
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.
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.
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.
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.
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.
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)
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.
Here is a step-by-step guide to installing and setting up CVL.
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).
The CVL package and complete installation instructions are available at http://www.sente.ch/software/cvl/
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.
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'.
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.
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.
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.
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.
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:
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.
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.
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.
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.
First you need a copy of eclipse. The Eclipse Homepage is http://www.eclipse.org.
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.
The easiest way to get Drupal CVS into your Eclipse Workbench is the "Import from CVS" feature.
Start Eclipse and open the File/Import... Dialog.
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 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.
Eclipse is a very capable open-source development platform that is available for Windows. This guide should help you get started.
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.
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.
You can checkout code from the Drupal repository as an anonymous user:

This has been a rough beginner's guide to setting up Eclipse. Additional information about the PHP plugin and making patches will follow.
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 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.
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), orcontributions/modules/event) to retrieve a specific module, theme, etc.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
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:
DRUPAL-4-5 and Drupal 4.6.3 would be DRUPAL-4-6-3.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.
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.
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.
You can now work with WinCVS to manage your local copy of the files, updating and committing as necessary.
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:
$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout drupal
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).
$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d my_drupal drupal
$ cvs update -dP
-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).
$ cvs -q update -dP
-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.
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:
$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout contributions
$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout contributions/directory/of/contrib
$ cd sites/mysite.com/modules
$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -d modulenamehere contributions/modules/modulenamehere
$ cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -r <version tag> contributions
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.
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-z7-d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupalpserver = password authenticating server)anonymous)anonymous)cvs.drupal.org)/cvs/drupal)checkoutupdate or diff.-d drupal5-r DRUPAL-5drupalTo 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.
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.
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.
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
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.
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 are always tagged in CVS, and the naming convention is:
DRUPAL-[Version]-BETA-[X]
.) are replaced with hyphens (-).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.
The CVS tags for release candidates have the form:
DRUPAL-[Version]-RC-[X]
.) are replaced with hyphens (-).For example, Drupal 4.7.0 release candidate 1 is tagged with DRUPAL-4-7-0-RC-1.
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:
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>
DRUPAL-4-7 or DRUPAL-5 (any value from the complete list of available Drupal core branches).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.0 and subsequent releases increase the patch level.-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-0DRUPAL-4-7--2-1DRUPAL-5--0-1DRUPAL-5--1-0Once 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.
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]
DRUPAL-4-7 or DRUPAL-5.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--2DRUPAL-4-7--3The branches currently available in Drupal core are:
The tags currently available in Drupal core are:
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.
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.
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:-
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.
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.
Here are some guidelines and hints on using Drupal's contrib repository:
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.
Provide at least a quick summary of what is contained in the commit.
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.
If others have contributed to the change you're committing, take the time to give them credit.
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.
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.
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.
There are two main types of releases:
In addition, you may categorize any release by giving it a Release type.
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.
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.
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.
For a detailed discussion on version numbers, please see the Branches and tags for contributions page.
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:
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
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.
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.
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.
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.
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.
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.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.
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".
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.
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:
modules directory itself, and run the following command:cvs log -rDRUPAL-4-7 node.modulecvs log -rTRUNK node/node.modulenode 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.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.
Answer: you can't.
There are only 2 ways to "remove" a directory from a cvs repository when people go to check something out:
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:
so, either you have to:
sorry,
-derek
If you get errors such as:
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.
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.
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.
cvs checkout -r DRUPAL-4-7 modules/mymodulecd modules/mymodulecvs tag -d DRUPAL-4-7cvs tag -b DRUPAL-4-7cvs update -r DRUPAL-4-7Verify 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)".
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.
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.
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!
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."
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."
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
Note: Before you can branch/tag a module, it needs to have a project created.
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
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.
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
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.
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.
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.
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.
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...
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.
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.
Maintaining a site from CVS helps in two main areas:
Note: For detailed explanation and further examples of CVS commands, see the Main repository and Contributions repository pages.
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
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
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.
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.
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...
#!/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
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.
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)
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:
[HOST:~/drupal]$ ls -1
contributions
drupal
imports
[MP:~/drupal/contributions/modules]$ cp -r event ../../imports
[MP:~/drupal/contributions/modules]$ cd ../../imports/
[MP:~/drupal/imports]$ ls -1a
.
..
event
[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
[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".
[MP:~/projects/drupalcustomA]$ cvs -q ci -m "adding event module"
Checking in modules/event/...
...
done
[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)
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)
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:
Reading the following additional resources is highly recommended.
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
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.
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 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, 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
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
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 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.
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.
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 directorycd .. -- 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 directory13. 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.
If you only need patch, download the standalone program from Sourceforge.
Note: patch on Windows requires CRLF line-endings; see also Common problems.
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.
When posting your issue follow-up, it is important to think critically and speak positively
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:
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.
cd /path/to/web/root/
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:
mkdir sites/example.com
cp sites/default/settings.php sites/example.com/settings.php
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:
cvs update -dP
wget http://drupal.org/files/issues/patch-name.patch
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
Might seem a lot of work, but once I got used to it it works very fast.
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.
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.
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.
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.
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.
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.
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
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
cd) to the Drupal root directory and run the update command: cvs update -dP.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.cd) to the Drupal root directory.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.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.
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
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.
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.
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!
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.
If you keep drupal or selected modules under version control with CVS, you can create patches using the built in diff command.
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
-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.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:
cvs diff [options] file_to_diff | unix2dos -u > file_to_diff.patchcvs diff [options] file_to_diff > file_to_diff.patchfile_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.(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)
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
-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. if you send a patch, make sure it has the proper line endingsNotes:
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:
cvs diff [options] file_to_diff | unix2dos -u > file_to_diff.patchcvs diff [options] file_to_diff > file_to_diff.patchfile_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.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.
c:\unxutilsdiffup.bat in c:\unxutils containing the commands@echo off
c:\unxutils\user\local\wbin\diff.exe -u -F^function %*
%SystemDrive%\Documents and Settings\[Username]\Application Data\Subversion\config (usually on c:)[Helpers] and add the linediff-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.

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 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.
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 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.
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.
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 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.
Reviewers are overloaded reviewing and testing patch submissions. Please make their lives easier by assuring the following:
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.

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.
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? ;)
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:
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.
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.
Here is a list of general resources for applying and testing patches.
Here are some tips and tricks (scripts, etc. to make patching easier).
(Reposted with permission from http://acko.net/blog/handy-drupal-core-development)
Some quick tips for better productivity when developing Drupal core:
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 ;).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).
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.
c (clean) command to clean a CVS tree:
cvs up -C -d .
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 - is useful to find and replace text in single multiple files.
sed -i 's/foo/foo_bar/g' somefile.module
sed -i 's/foo/foo_bar/g' *.module
cvs diff -up > yourpatchfile.patch to create a patch.sed is available on the Win32 platform by installing GNU utilities for Win32
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.
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.
cache_set(): this function is well documented and provides some more, useful documentation.cache_get() and cache_clear_all() are two more, useful functions to know and use.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.
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.
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.
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.
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.
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.
For an introduction to how the forms API works, check out the
Forms API Quickstart Guide.
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();
//..
}
?>
For a comprehensive listing of form elements and their associated attributes, please see the Forms API Reference.
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.
For the purposes of this article, we'll be looking at three specific kinds of dynamic forms:
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...
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.
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:
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.
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.
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?
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!
}
}
?>
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 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.
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!
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.
hook_form.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:
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):
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:
#pre_renderLet'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.
#pre_render function.#required in these declarations either.#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.
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.
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.
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.
For this article's purposes we are interested in the third function, the one ending in _submit.
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.
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:
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.
Yeah, me too, at least until finding a small bug in user.module, maybe that's your case.
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:
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:
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.
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:
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"
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

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

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.
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:
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.
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

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.
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:
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.
Save project.module at this point and reload the settings page.
Project settings page after replacing form values

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.
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.
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;
}
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 ###
This example illustrates the creation of a simple form with a few fields and a submit button.
<?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
}
?>
<?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);
}
?>
This rather lengthy example introduces how fieldsets are handled, and also shows some additional field types.
<?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;
}
?>
<?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;
}
?>
This example demonstrates how to make use of the API's validation and execution functions.
<?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;
}
?>
<?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');
}
?>
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.
<?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;
}
?>
<?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');
}
?>
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:
<?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;
}
}
?>
<?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;
}
?>
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.
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);
}
?>
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();
}
?>
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)
Until they are written for core, you would need to write a valid_url, valid_email and valid_integer function.
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)
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.
#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.
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!
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.
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.
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().
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>'
);
}
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 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;
}
?>
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.
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.
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.
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>';
}
?>
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;
}
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.
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.
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));
?>
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.
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. 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.
}
?>
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;
}
?>
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));
?>
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
}
?>
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.
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:
<?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);
}
?>
<?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.
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.
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!
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...');
?>
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:
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.
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 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:
Using Schema API, you perform the same two steps:
<?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:
<?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().
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:
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().
Placeholder.
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:
TO DO: Add a page defining all type:size maps for each supported database.
Note that type 'text' and 'blob' fields cannot have default values.
All parameters apart from 'type' are optional except that type 'numeric' columns must specify 'precision' and 'scale'.
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.
See http://drupal.org/node/28913
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.
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.
If you choose to develop a custom module which utilitizes hook_xmlrpc, consult the 'blogapi' module for some great examples.
Services and amfphp modules do this as well.
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:
Step 2: Installation
Step 3: Indexing
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.
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);
Step 4: Finishing touches
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.
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. ;)
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.
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:
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.
make_users(50, $domain); to make_users(2000, $domain).$output .= create_terms(50, $vocs); to $output .= create_terms(250, $vocs);create_nodes(50, $users); to create_nodes(5000, $users); and create_comments(500, $users, $nodes, $comments); to create_comments(10000, $users, $nodes, $comments); (note: this one takes awhile)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...).
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:
curl -LO http://drupal.org/files/issues/patch_file.patch
patch -p0 < patch_file.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)
Some things to watch out for ... // todo
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.
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.
Below you will find a list of useful tools for use when developing Drupal sites.
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)
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.
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)
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
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.
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.
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.
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).
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.
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.
This is a place to post original usability research for Drupal. There are many ways to help with usability.
This survey was conducted September to October 2006.
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.
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.
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.

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.
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.
Survey Monkey does record IP addresses; we did not find any irregularities and do not believe anyone tried to unfairly influence the results.













The source data for the survey analysis and design.
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, |
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.
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:
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.
Watch 7 min 5 sec
The following tasks were to be completed and evaluated by the participants:
While Frank went over these tasks, I used Drupal 5's new installer to set up a new site.
Start of group review, the interesting part
Watch 12 min 20 sec
Watch 22 min 35 sec
Watch 25 min 35 sec
Watch 7 min 54 sec
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:
We have some idea of who uses Drupal:
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.
Status: incomplete draft
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.
Developer documentation can be found at http://api.drupal.org and in the remainder of the Drupal developer's guide below.
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.
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.
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:
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.
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.
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.
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.
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
}
}
?>
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;
}
}
?>
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 orhttp://.../?q=admin/system/modules
Note: You'll see one of three things for the 'onthisdate' module at this point:
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.
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.
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);
}
?>
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.
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.
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.
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:
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.
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.
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.
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
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.
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.
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 orhttp://.../?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.
Some modules need to be tested with large loads. Here are some situations where your module might slow down a large or busy site.
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.
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;
?>
<?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.
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.
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.
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.
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.
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:
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.
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.
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:
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.
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
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
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.
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 orhttp://.../?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.
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)
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;
?>
<?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);
?>
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
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.
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.
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
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.
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 <code> and <?php ?> 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 <code>...</code> tags. For PHP code, you can use <?php ... ?>, which will also colour it based on syntax.');
}
else {
return t('You may post code using <code>...</code> (generic) or <?php ... ?> (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() {}
?>
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.
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;
}
?>
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.
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:
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.
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.
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.
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.
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.
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.
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.
// TODO: how to create an install file
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).
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.
Here is a list of the objects and properties that Drupal offers to functions.
[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
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.
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...
As an example... say you want a simple press release node type.
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."
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.
- 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.
- this hook does not appear in node_example but is a good idea to use. This example will implement this hook.
Note: it is considered a good practice to include the following comment before each hook
/**
* Implementation of hook_{hook name here}().
*/
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...
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.
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 $
?>
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
Before each hook is implemented you should you will see a php comment
for hook_help...
<?php
/**
* Implementation of hook_help().
*/
?>
<?php
/**
* Implementation of hook_node_name().
*/
?>
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) {
?>
<?php
function story_node_name($node) {
?>
In the function calls change story to your technical name, in our example release
for hook_help...
<?php
function story_help($section) {
?>
<?php
function release_help($section) {
?>
and for hook_node_name
<?php
function story_node_name($node) {
?>
<?php
function release_node_name($node) {
?>
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.');
}
}
?>
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');
}
?>
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');
}
?>
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;
}
}
}
?>
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;
}
?>
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;
}
?>
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
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...
As an example... say you want a simple press release node type.
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.
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 $
The next couple lines of code should be
/**
* @file
* Enables users to submit <strong>stories, articles or similar content</strong>.
*/
/**
* @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
Before each hook is implemented you should you will see a php comment
for hook_help...
/**
* Implementation of hook_help().
*/
/**
* Implementation of hook_node_info().
*/
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) {
function story_node_info() {
In the function calls change story to your technical-name, in our example release
for hook_help...
function <strong>story</strong>_help($section) {
function <strong>release</strong>_help($section) {
and for hook_node_info
function <strong>story</strong>_node_info() {
function <strong>release</strong>_node_info() {
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 >> <strong>story</strong></a>.</li>
<li>configure <strong>story</strong> at <a href="%admin-node-configure-types">
administer >> content >> configure types >>
<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>');
}
}
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 >> <strong>user-friendly-name</strong></a>.</li>
<li>configure <strong>user-friendly-name</strong> at <a href="%admin-node-configure-types">
administer >> content >> configure types >>
<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>');
}
}
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 >> <strong>press release</strong></a>.</li>
<li>configure <strong>press release</strong> at <a href="%admin-node-configure-types">
administer >> content >> configure types >>
<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>'));
}
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>'));
}
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>');
}
function <strong>technical-name</strong>_perm() {
return array('create <strong>user-friendly-plural</strong>',
'edit own <strong>user-friendly-plural</strong>');
}
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;
}
}
}
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;
}
}
}
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;
}
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;
}
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;
}
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;
}
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
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.
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 >> **user-friendly-name**</a>.</li>
<li>configure **user-friendly-name** at <a href="%admin-settings-content-types-**technical-name**">administer >> settings >> content types >> 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;
}
For my particular application I noted a few pros/cons.
Pros
Cons
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.// $Id: book.module because it isn't needed.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.
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
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:
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 4.7 javascript toolkit has three basic components:
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.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.
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);
}
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.
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.
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.
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).
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.
<fieldset></fieldset> object.<script type="text/javascript" src="/misc/drupal.js"></script>
<script type="text/javascript" src="/misc/collapse.js"></script>
class=" collapsible" within the fieldset brackets (see example below)class=" collapsible collapsed" instead< ! --break-- > tag above the form so it doesn't muck up your template when the page is in 'teaser' view.<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>
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.
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.
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);
}
}
?>
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).
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.
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:
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.
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()user/autocomplete.taxonomy_autocomplete()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.").'));
?>
If you want to make your own autocomplete function to answer a need not already met, there are a couple of additional steps.
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).
user_menu() lines are:<?php
$items[] = array('path' => 'user/autocomplete', 'title' => t('user autocomplete'),
'callback' => 'user_autocomplete', 'access' => $view_access, 'type' => MENU_CALLBACK);
?>
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.
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.
At their most basic, AJAX widgets will have three components.
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.
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.
Step 2 is writing Javascript to post information to the server, and to interpret the response.
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);
}
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:
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!
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.
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.
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.
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>
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:
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.
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.
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.
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 Drupal 5 version of this page is here. It is already more complete than this page.
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:
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 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.
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>
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.
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>
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)
Several contrib modules listed in the Utility section offer some more javascript and jQuery functionality.
Here are some:
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
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());
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.)
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:
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:
jQuery is currently incorporated into Drupal HEAD (to be 5.0) and work remains to remove dependencies on drupal.js and friends.
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.
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>
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>
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>'
);
?>
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.
$( with $id(You should now have jquery.js output in your webpages, without sacrificing the existing Drupal javascript effects.
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.
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.
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:
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.
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.
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.."
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.
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.
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 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.
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.
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.).
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 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;
}
}
?>
.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.
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.
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:
For Drupal 4.7, modules need to perform a special update. Check the relevant section in the module upgrading guide for that.
Placeholder for Schema API documentation. Coming soon.
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.

(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.)
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.
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.
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/editnode/12345/%node/%/editnode/%/%node/12345node/%nodeThese we call the ancestors of node/12345/edit.
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.
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').
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).
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 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:
The above arguments interact in the following ways (with pseudo code) to compute the title:
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
}
?>
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
SELECT * from {menu_links} WHERE plid in (15, 7, 0) ORDER BY p1 ASC, p2 ASC, p3 ASC
5.0.0
7.0.0
7.10.0
7.13.0
7.15.0
7.15.16
7.15.23
12.0.0
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
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
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.
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:
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.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:
path is the new index for $items.callback becomes page callbackcallback arguments becomes page argumentsaccess 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.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.
(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.
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'));
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.
This section is intended as a handy reference, collecting things which you may need to look up as you code to Drupal.
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
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') |
|
| after | load | hook_user'('load') | hook_load() hook_nodeapi('load') |
|||
| before | update |
-
hook_profile_alter() |
- - - |
hook_access('update')
- |
||
| 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 | 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.
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.

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.

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.

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.

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.

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.

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.

Here are the auxiliary system tables in the standard (core) Drupal database that are not defined elsewhere.

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.

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
Long-term

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.

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
COMMENTS
This section collects various 'How-to' articles of interest to module writers and hackers.
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.
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');
$output = theme('table', $header, $rows, array('class' => 'some-class'), NULL, 'user');
For the node table:
...
$node_sort = tablesort_sql($header, NULL, 'node');
$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.
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.
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) { ... }
?>
This information is superseded by the Doxygen documentation. In particular, its example node module is a good tutorial.
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
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.
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.
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 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.
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);
}
?>
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.
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.
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.
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.
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.
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:
<?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:
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);
?>
Here is a place where you can find various tips and hints which will help you in writing tests
Use $this->showSource() to output the source that the simpletest browser is receiving. Useful for debugging during development. Comes from simpletest/simpletest/webtester.php.
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 mean NULL.db_next_id(<tablename_fieldname>).SELECT * FROM {accesslog} WHERE ... instead of SELECT * FROM accesslog WHERE ....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.
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.
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'");
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 |
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);
?>
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.
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 = "Forum"
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 " value to display the " character.
For example, this will display correctly:
description = "This is my "crazy@email.com" email address"
description = This is my "crazy@email.com" address <- DO NOT DO THIS
dependencies = taxonomy comment
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:
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...).
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.
For more information on ini file formatting, see the PHP.net parse_ini_file documentation.
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.
url() and l() have changedformat_plural() accepts replacements$form['#base'] is goneimage_scale_and_crop()The menu system has been completely re-hauled in 6.x. See the Menu system overview.
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.
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.
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;
}
?>
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');
}
?>
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;
}
?>
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']).
The names of the variables accessed via variable_set() and variable_get() can now be 128 characters long.
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:
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 replacementsAn 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));
?>
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...
}
?>
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...
}
?>
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';
}
?>
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.
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.
This means that you can use hook_menu_alter to change the visibility of an item or change the access callback.
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
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);
?>
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()!
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.
}
?>
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
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
...
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.
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).
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 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');
?>
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.
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.
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.
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().
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.
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.
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);
}
?>
<?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);
}
?>
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.
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.
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.
hook_form_alter() have changedhook_form_alter() complimented by hook_form_$form-id_alter()$form_state rather than returning urls$form['#submit'] and $form['#validate'] and $form['#process'] no longer support custom parametersform_set_value() parameters have changed#multistep is gone, use $form_state instead$form['#base'] is gone$form['#pre_render'] is gonePrevious 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:
Validation and submission handlers can place additional data in custom bins in the $form_state.
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...
}
?>
hook_form_alter() have changedSimilar 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.
}
?>
$form_state rather than returning urlsIn 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 parametersThe #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 changedform_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 insteadIn 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.
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 goneIn 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 goneThe #pre_render property has been removed from FormAPI; it was used solely to implement node previews, and is no longer necessary.
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 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
}
?>
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.
This page describes changes to the module interface; a 5.x themes conversion guide is also available.
hook_link()hook_link_alter()menu_primary_links(), and menu_secondary_links() return structured linksuser_mail() to drupal_mail()hook_mail_alter()user_mail_wrapper() to drupal_mail_wrapper()hook_settings()hook_profile_alter()message_na() removeddrupal_add_css() - proper way to add csshook_taxonomy('form') has been removed. use hook_form_alter() insteadform_render() to drupal_render()hook_view() and hook_nodeapi($op = 'view')hook_nodeapi($op = 'alter') has been addedhook_node_info() and the node type system.node_get_names() and node_get_base() functions no longer exist.hook_node_type()db_table_exists()hook_node_operations(), hook_user_operations()t() callsdrupal_get_form() to take a $form_id, not a $formhook_forms() optionally maps form_ids to builder functionsdrupal_execute() function allows form data to be submitted programmaticallymodule_exist() is now module_exists()format_plural() @count changedrupal_add_js()drupal_call_js()drupal_add_feed() and drupal_get_feeds replaces theme_add_link()theme('page') may omit standard blocks$_POST[op] deprecated in favor of $form_values[op]system_listing() to drupal_system_listing()$form['taxonomy'] array).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:
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_linkhook_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.
hook_link_alter()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 linksmenu_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.
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
$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.
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.
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);
}
?>
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() removedThe function message_na() was removed, remove it from your modules as well and replace it with t('n/a').
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);
?>
'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 CSSModules 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 changedNodes 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.
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');
}
}
?>
hook_node_info() and the node type systemhook_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.
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.
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),
);
}
}
?>
db_table_exists()This function (which works under MySQL and PostgreSQL) will indicate if the given table exists in the database.
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:
<?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;
}
?>
<?php
function node_operations_approve($nodes) {
db_query('UPDATE {node} SET status = 1 WHERE nid IN(%s)',
implode(',', $nodes));
}
?>
t() callsThe 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 $formIn 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 functionsModules 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.
drupal_execute() function allows form data to be submitted programmaticallyForms 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 changeThis function used to substitute a number where you placed %count. Instead, use @count now. This is a result of the t() changes described above.
drupal_add_js()The function to add JavaScript to a Drupal page has been reworked:
drupal_add_js($data = NULL, $type = 'module', $scope = 'header', $defer = FALSE, $cache = TRUE)
includes/common.inc, line 1329
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:
$data (optional) If given, the value depends on the $type parameter:
$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.
If the first parameter is NULL, the JavaScript array that has been built so far for $scope is returned.
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.
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 blocksModules 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.
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,
);
?>
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.
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');
}
?>
The addition of jQuery has significantly altered drupal.js:
Drupal. For example, if you called absolutePosition(...) before, you should now call Drupal.absolutePosition(...).() when doing the check: if (Drupal.JsEnabled) ....$(document).ready() function.Drupal includes the complete jQuery 1.0.1 library (without plug-ins). Check the jQuery website for its documentation.
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.
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:
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.
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
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 */
}
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.
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.
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);
}
?>
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 */
}
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.
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;
}
?>
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
}
?>
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);
}
?>
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', ...):
return $output;), then print('page', ...); is called.In general, there is no longer need to use theme('page', ...); in your module.
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;
}
?>
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));
?>
$type is the node type, $name is the human readable name of the type and $base is used instead of
<hook>
<hook>
_load()
<hook>
_view()
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'));
}
?>
<?php
function page_node_info() {
return array('page' => array('name' => t('page'), 'base' => 'page'));
}
?>
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'));
}
?>
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.
node_name hook, node_types.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.
node_load() cache is not used resulting in lower performance.node_load(array('nid' => $nid)); can be replaced by node_load($nid).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() now receives the $node parameter by reference, and modifies the object as needed. It has no return value anymore.
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.
node_list().The most typical use is:
<?php
// Drupal 4.6
foreach (node_list() as $type) {
// Drupal 4.7
foreach (node_get_types() as $type => $name) {
?>
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);
...
?>
hook_form() need to be edited to add the title field.module_get_node_name deprecatedFetching 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);
?>
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.
format_name(...) should be replaced by theme('username', ...).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', ...); 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.
NULL or an empty string.theme('table', ...) where you pass no rows or no header.Example:
<?php
// Drupal 4.6
theme('table', '', $rows);
// Drupal 4.7
theme('table', array(), $rows);
?>
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) {
?>
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!
check_output() should be checked (and double checked!) before replacing it with check_markup().As this happened between 4.6.2 and 4.6.3, there is a separate guide for this.
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.
taxonomy_save_vocabulary() or 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;
}
?>
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.
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.
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)
}
}
?>
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.
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.
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.'),
);
}
}
?>
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.
}
}
?>
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.
$my_object = array2object($my_array); should be replaced with$my_object = (object) $my_array;.
user_load now returns FALSE if a user cannot be loaded, instead of an empty object.
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 */;
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'));
}
?>
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");
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);
}
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.
Simply specify the '#required' => TRUE attribute on the form item instead, and form API will validate for you.
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);
?>
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:
$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.
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.
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));
?>
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
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.
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.
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>';
?>
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'));
?>
<?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();
}
?>
<?php
$result = xmlrpc($server. '/xmlrpc.php', 'foaf.getUrl', $name);
if ($result !== FALSE) {
$user->foaf_url = $result;
}
?>
So:
<?php
$node->created = iso8601_decode($struct['dateCreated'], 1);
?>
<?php
$node->created = mktime($struct['dateCreated']->hour,
$struct['dateCreated']->minute, $struct['dateCreated']->second,
$struct['dateCreated']->month, $struct['dateCreated']->day,
$struct['dateCreated']->year);
?>
<?php
return xmlrpc_date($node->created);
?>
Likewise, to make the distinction between base64 and string, you need to
xmlrpc_base64($binary_data) your binary data.
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.
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.
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.
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.
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().
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 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.
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
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().
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.
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.
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.
The Drupal menu system got a complete rewrite. The new features include:
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);
}
}
?>
<?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).
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".
static has been renamed to sticky.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');
?>
<?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');
?>
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.
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:
check_output() whenever you use it.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:
Check the API documentation for these functions for more information on how to use them.
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;
}
}
?>
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 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");
}
}
?>
In addition to the above mentioned changes:
Since Drupal 4.3, major changes have been made to the theme, menu, and node systems. Most themes and modules will require some changes.
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:
_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");
}
}
?>
example/foo/bar/12, the above menu() calls would cause example_foo("bar", 12) to get invoked.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:
_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);
?>
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.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.For full information on theme system changes, see converting 4.3 themes to CVS. The following points are directly relevant to module development:
theme() usage:<?php
theme("box", $title, $output);
?>
<?php
print theme("box", $title, $output);
?>
theme_<module>_<name>. 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() to allow for the active theme to modify the output if necessary.
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);
?>
$title and $breadcrumb will override any values set before for these properties.
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.
_node() hook has been deprecated. In its place, modules that define nodes should use _node_name() and _help()._node_name() function should return a translated string containing the human-readable name of the node type._help() function, when called with parameter "node/add#modulename", should return a translated string containing the description of the node type.<?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;
}
}
?>
"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 < and >.
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.
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_prepare() function now, which only runs the body through the filters if the node view page is displayed. Otherwise, only the teaser is filtered._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.Other than those mentioned above, the following hooks have changed:
_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);
}
}
?>
<?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);
}
}
?>
_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._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._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._compose_tips hook changes below.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.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.l() or url()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'
?>
status() should be updated to use drupal_set_message(). The status() function has been removed.<?php
drupal_set_message(t('updated X'));
?>
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);
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.
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;
?>
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:
$system is $system["name"] then the case is case 'admin/system/modules#name'case 'admin/system/modules#description'$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;
}
?>
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:
user/help#<modulename> and<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
}
?>
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 = ""])url("search/bla"),l("view node", array("op" => "view", "id" => $nid), "node"[, $anchor = "", $attributes = array()])l("view node", "node/view/$nid"[,$attributes = array(), $query = NULL])lm(), which meant "module link" and used to be module.php?mod=bla&op=blub..., is now l("title", "bla/blub/..."); andla(), which meant "admin link" and used to be admin.php?mod=bla&op=blub..., is now l("title", "admin/bla/blub/..."$theme->function() became theme("function"). see [drupal-devel] renaming 2 functions, [drupal-devel] theme("function") vs $theme->function() and [drupal-devel] [CVS] theme()<module>_conf_options() became <module>_settings() - see [drupal-devel] renaming 2 functions. note that doesn't get an extra menu entry, butthe 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 <module>_settings() to the menu (they automatically go to "site configuration > modules > module settings" - you only add <module>_admin...() ... things.
- comment_is_new($comment)
+ node_is_new($comment->nid, $comment->timestamp)
please add / update / correct!
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.
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;
}
}
}
Changes: in function taxonomy_get_tree()
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")
taxonomy_get_vocabulary_by_name($name) and taxonomy_get_term_by_name($name)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);
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)
This chapter contains all the Drupal enhancement proposals that are in progress.
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.
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:
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.
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.
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:
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.
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:
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 first case is very simple, and Drupal has always handled it well. The second has had a number of bugs that keep popping up:
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.
Some considerations to look at in comparing different solutions to handling time zones in Drupal:
There are certainly more than this, but this is a good basis for comparison.
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:
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.
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!
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.
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.
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
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.
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.
This workflow proposal was created as part of the "GoJoinGo" Group/Events development effort.

This diagram is a proposal for interaction design for a Drupal event system that can handle event creation, searching, invite, and rsvp.
Folks,
Could you add me to the crew? I have a jones to begin coding....
This is a stub to discuss next generation event modules based on views and content creation kit.
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.
We periodically hold IRC discussions to plan event development work.
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:
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!
Thank you very much to all who attended! The following is a summary of points raised:
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)
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)
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)
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.
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.
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
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].
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.
The features that will possibly need some core patching are:
Implemented by i18n module:
The list of objects that may be language dependent, thus may need a language attribute are:
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.
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:
Enable Drupal for full multilanguage: i18n2core big patch, http://drupal.org/node/77866
This is a big patch intended to try and see the general approach. Smaller ones will follow.
We may organize the stuff in modules following this groups of features.
Additional requirements for discussion:
Drupal enhancement proposals that were finished or closed.
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.
The process of creating translations is as follows (click on it for zoomed view):
Translating Drupal core and contributed modules/themes (represented by 'module' on the figure) are similar processes, but there are important differences.
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.
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:
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.
Recommended PO file editors are (in no particular order):
Be sure to get a recent version for all editors, multiple plural forms are a recent addition to the gettext standard.
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).
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"
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.
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.
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:
C:\Program Files\XEmacs\site-packages;C:\Program Files\XEmacs\mule-packages;C:\Program Files\XEmacs\xemacs-packages
(require 'un-define)
(set-coding-priority-list '(utf-8))
(set-coding-category-system 'utf-8 'utf-8)
(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.
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.
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.
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.
Besoek asb. die foutrapporteringsblad
Stuur 'n e-pos aan Kobus en spesifiseer die presiese rede vir u skakeling.
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.
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.
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.
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.
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.
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.
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.
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
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
msgfmt -o /dev/null -c --statistics ../update.po
for i in *.po; do msgmerge -o $i ../update.po ../POTS/${i}t; done
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.
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.
Symptom: After importing a translation, some strings on your site are translated, some are not.
Possible causes (and solutions):
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>
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
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 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.
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.
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!
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
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:
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.
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.
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.
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.
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
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
bzr commit -m 'merged from mainline'
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.
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
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
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 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:
wget --mirror --delete-after http://example.com/
wget --mirror --delete-after --load-cookies=/path/to/cookies.txt http://example.com/
Note that this can take some time. wget will access every Drupal page linked from anywhere on your site. You can later have a look at the error logs and find out if any errors where caused. This will not test submitting forms. You can test submitting forms through simple test module.
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.
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.
<?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.
<?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.',
);
}
?>
<?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.
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}
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'));
?>
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*', '')");
?>
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)");
?>
<?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 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']);
}
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.
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
// TODO
// TODO
// TODO
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);
?>
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);
}
?>
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>.");
?>
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);
?>
This is a stuff
// TODO
// TODO
// TODO
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.
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
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...).
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')";
}
?>
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.
<?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!
}
?>
<?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;
}
?>
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;
}
?>
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
Please refine mercilessly.
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.
[note from editor ax: you have to escape the special characters < (<), > (>), and & (&)]
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;
#!/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;
}
[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 );
}
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';
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.
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
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.commentcode instead of '0' as comment. You will need to change all instances of -1 for this field to 0.INSERT INTO...INSERT INTO `gl_stories`
INSERT INTO `node`
SELECT
'' as nid,
'' as vid,
'1' as uid,
title,
bodytext,
introtext,
'' as log,
unix_timestamp(date),
'1' as format
from gl_stories
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.INSERT INTO...INSERT INTO `gl_stories`
INSERT INTO `node_revisions`
SELECT
'' as nid,
tid
from gl_stories
INSERT INTO...INSERT INTO `gl_stories`
INSERT INTO `term_node`
SELECT * FROM `term_node` WHERE 1This 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.
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;
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'.
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!
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:
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.
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!
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!
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.
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.
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.
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..!!
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.
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.
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:
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.
[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:
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.
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
Note: this script was originally written to migrate to Drupal 4.4. It is left for reference.
uid1, i.e. the site admin (change the
$uid variable to import to another user)Drupal Importwith
import.phpspecified as the Output File
Template bodytextarea
//set variable defaultscomment to the correct values
import.phpin 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."