I use a lot of open source software for day-to-day jobs and in the same spirit as the software, I'd like to share some of my knowledge with the community.
So far I'll be covering drupal, sugarCRM and civiCRM among others. I seem to write hundreds of modules a year. Some for a specific purpose which would probably not help much but others extend the standard software in some pretty weird and wonderful ways.
If there's something specific that anyone needs, just shout and I'll have a look in the module library (see: collection of messy folders in hundreds of project files).
I'm currently working on a project that is heavily involved with the inner workings of drupal and CiviCRM. In the course of this I am increasingly needing to write my own hooks and calls to CiviCRM from within a drupal module. I agree that the api could get bloated and full of unnecessary functions but the fact remains, I need the data. So begins my quest to extend the tentacles of a drupal module into CiviCRM.
Features that are currently either in progress or at a point where they do the job they should and are waiting for refinement. None of this code is security tested although I do my best to keep things safe from the script kiddies.
Membership signup / Contribution block - displays a link to the contribution page.
Upcoming events block - displays a simple list of upcoming events
Events additions:
To come are:
Once the module is ready you will be able to download it below.
While I'm not overly competent with CiviCRM I recently needed to work on the content of a page from within a drupal module. The hook system seems to be in active development but as of v2.1.2 it hasn't got to the point of exposing the template to the hook api. I added a few lines of code to enable this. I also files a feature request for version 2.2 so hopefully it'll be added for that.
During the meanwhilst here's my effort:
edit CRM/Core/Page.php
first add
<?php
require_once 'CRM/Utils/Hook.php';
?><?php
function run( )
{
if ( $this->_embedded ) {
return;
}
self::$_template->assign( 'mode' , $this->_mode );
self::$_template->assign( 'tplFile', $this->getTemplateFileName() );
if ( $this->_print ) {
if ( $this->_print == CRM_Core_Smarty::PRINT_SNIPPET ||
$this->_print == CRM_Core_Smarty::PRINT_PDF ) {
$content = self::$_template->fetch( 'CRM/common/snippet.tpl' );
} else {
$content = self::$_template->fetch( 'CRM/common/print.tpl' );
}
if ( $this->_print == CRM_Core_Smarty::PRINT_PDF ) {
require_once 'CRM/Utils/PDF/Utils.php';
CRM_Utils_PDF_Utils::domlib( $content, "{$this->_name}.pdf" );
} else {
echo $content;
}
exit( );
}
$config =& CRM_Core_Config::singleton();
// Adding hook to drupal
CRM_Utils_Hook::pageProcess(self::$_template);
$content = self::$_template->fetch( 'CRM/common/'. strtolower($config->userFramework) .'.tpl' );
echo CRM_Utils_System::theme( 'page', $content, true, $this->_print );
return;
}
?>Now we add a corresponding hook function in CRM/Utils/Hook.php
Add this before the end of the class.
<?php
static function pageProcess( &$page ) {
$config =& CRM_Core_Config::singleton( );
require_once( str_replace( '_', DIRECTORY_SEPARATOR, $config->userHookClass ) . '.php' );
$null =& CRM_Core_DAO::$_nullObject;
return
eval( 'return ' .
$config->userHookClass .
'::invoke( 1, $page, $null, $null, $null, $null, \'civicrm_page\' );' );
}
?>And now in your module you can write your hook.
<?php
function MODULE_NAME_civicrm_page(&$page) {
$someVar = "Hello World";
$page->assign("someVar", $someValue);
}
?>The only thing remaining is to add this to the template which will look something like {$someVar}
IMPORTANT:
At this point we are not checking what page we are using. I suggest looking at the smartyDebug=1 info to find out what makes your page unique and check for it OR just use the arg() from drupal to get your url. This method will add the var to all pages of your site although it will not be displayed unless it is added to you template.
Drupal is a fantastic CMS which I have been actively developing for, for some time now. Yesterday was the first time I had really ventured into the Ajax territory but I learnt a lot and hopefully this guide should help others.
Currently this example is Drupal 5.x only. I will update it for Drupal 6 in another post in due course.
I'm going to create a simple module to demonstrate the communication between client and server. There is already a module out there that does more or less exactly what this demo will do so I'd advise using that if you require the same functionality.
The demo module will be a simple username checker for new registrants. We will add a button to the registration form which will query the server for an existing user of that name. If it already exists we put up a warning otherwise we display a confirmation that they can safely use that name.
So, to begin, we will need the .info file which looks something like this:
; $Id$
name = Jquery Example
core = "5.x"
description = Quick example JQuery module. Adds a name checker to the register page.
version = "5.x-1.00"Okay so now we want to create the .module file. We need 3 things: we need to hook into the registration page, a menu callback for our jquery and a function for it to call.
We'll start with the menu system.
<?php
/**
* Implementation of hook_menu()
*/
function jquery_example_menu($may_cache) {
$items = array();
if($may_cache) {
$items[] = array(
'path' => 'usercheck',
'callback' => 'jquery_example_usercheck',
'access' => 1,
'type' => MENU_CALLBACK,
);
}
return $items;
}
?>This should be fairly straightforward if you're familiar with writing modules.
Next stop, the callback function we just defined.
<?php
function jquery_example_usercheck() {
$name = check_plain($_POST['theName']);
// Query the database.
$result = db_num_rows(db_query("SELECT uid FROM {users} WHERE name = '%s'", $name));
$reply = array();
// different replies based on result.
if($result > 0) {
// user exists.
$reply['exists'] = true;
$reply['replyString'] = t('Sorry, username %theName has already been taken', array('%theName' => $name));
} else {
$reply['exists'] = false;
$reply['replyString'] = t('Username %theName is available.', array('%theName' => $name));
}
print drupal_to_js($reply);
exit();
}
?>So what have we done here? We grab the username from the POST array and make sure it's safe to use with our db query by using check_plain(). Then we do a very simple query to see if the name exists and return the number of rows of that name. We should never get more than one but a simple check to see if it is greater than zero will suffice.
The last call before the exit() is the one we are really interested in. This handy method will convert the array we sent it into a JSON formatted string. This is very important as we will need to undo this action on the client's machine to reconstitute the data. For more information on JSON and why we use it see here
If you're happy (and you know it) that you understand everything so far, the next step will be to hook into the registration form.
<?php
/**
* Implementation of hook_form_alter()
*/
function jquery_example_form_alter($form_id, &$form) {
drupal_add_js(drupal_get_path('module', 'jquery_example'). '/jquery_example.js');
drupal_add_js(array(
'username_check' => array(
'ajaxUrl' => url('usercheck', null, null, true),
'msgWait' => t('Checking username availability ...')
),
), 'setting');
if($form_id == 'user_register') {
// we want to alter the form to add button and a placeholder for our reply
$form['checkUser'] = array(
'#type' => 'button',
'#value' => t('Check availability'),
);
$form['replyMessage'] = array(
'#type' => 'markup',
'#prefix' => '<div id="reply-message">',
'#value' => t("Feel free to check your username before proceeding"),
'#suffix' => '</div>'
);
}
}
?>At the top if this function we are adding our as-yet non-existent javascript file plus a couple of settings. These settings have been lifted almost directly from the username_check module
There's not much else to this other than a quick mention that we are passing $form by reference so any changes we make to $form will be passed back to the originator.
We can now move on to the exciting bit; the javascript. JQuery is built in to the core of Drupal but I can recommend getting hold of the jquery_update module to make sure you have a more recent version. In Drupal 6 it is up to date at the time of writing.
Create a new file called jquery_example.js and add this code:
if (Drupal.jsEnabled) {
$(document).ready(function() {
$("#edit-checkUser").bind("click", function() {
var username = $("#edit-name").val();
$.post(Drupal.settings.jquery_example.ajaxUrl,
{theName: username},
function (data) {
var result = Drupal.parseJson(data);
var message = $("#replyMessage");
message.html(result['replyString']);
message.show();
});
return false;
});
$("#replyMessage").hide();
});
}Lets go through this. Firstly we find out if javascript is enabled for the site with the line:
if (Drupal.jsEnabled) {
Then we ask JQuery to perform our function when the document is loaded and ready.
$(document).ready(function() {
$("#edit-checkUser").bind("click", function() {The second line of this will tell JQuery to find the item with ID of 'edit-checkUser' (remember that Drupal adds 'edit-' to form elements) and then bind the click event to a function declared straight after.
var username = $("#edit-name").val();
$.post(Drupal.settings.username_check.ajaxUrl,
{theName: username},
function (data) {
var result = Drupal.parseJson(data);
var message = $("#replyMessage");
message.html(result['replyString']);
message.show();
});
return false;So what are we doing? We grab the name that has been typed into the username textfield 'edit-name' by using the JQuery val() function.
We now do a call to the server with $.post(). On the JQuery website www.jquery.com the signature of this function is:
jQuery.post( url, [data], [callback], [type] )
Items in [] brackets are optional. If you look at this and apply it to our call you will see that we are reading the url from the Drupal.settings object. After this we send some data as an object {theName: username} as key-value pairs and then our callback will be the next function declaration.
function (data) {
var result = Drupal.parseJson(data);
var message = $("#replyMessage");
message.html(result['replyString']);
message.show();
}Very simply this will parse the reply using Drupal's JSON parser helper and we can do what we want once we have the information back. In this case we find the message div we set up in the form and was previously hidden with $("#replyMessage").hide(); declared further down the page just inside the $(document).ready() function. Once we have the message box we set the value and tell it to unhide itself with .show();
So there you have it. A simple ajax call to the server. Perhaps as an exercise you might like to see the effects of this javascript code:
if (Drupal.jsEnabled) {
$(document).ready(function() {
$("#edit-checkUser").bind("click", function() {
var username = $("#edit-name").val();
$.post(Drupal.settings.username_check.ajaxUrl,
{theName: username},
function (data) {
var result = Drupal.parseJson(data);
var message = $("#replyMessage");
message.html(result['replyString']);
message.removeClass('username-message-progress');
if(result.exists === true){
message.removeClass('username-accepted');
message.addClass('username-rejected');
}
else{
message.removeClass('username-rejected');
message.addClass('username-accepted');
}
message.show();
});
return false;
});
$("#replyMessage").ajaxStart(function(){
$(this).html(Drupal.settings.jquery_example.msgWait);
$(this).removeClass('username-accepted');
$(this).removeClass('username-rejected');
$(this).addClass('username-message-progress');
$(this).show();
});
$("#replyMessage").hide();
});
}Note the new classes being added. You'll need to add these to your css file for your theme. Also the .ajaxStart() function is useful for doing things once a call to the server is being made. Disabling buttons and altering messages is a good way to inform the user that something is going on.
That's all for now. Any questions please leave a comment or contact me from the contact page.
A while ago we were charged with the task of using drupal with SugarCRM. Unlike CiviCRM, these 2 systems have never been designed to work together nicely. Rather than hack into core code to share a user table we decided to take advantage of the existing SOAP functionality that SugarCRM affords us.
Unfortunately we had to alter about 2 lines of SugarCRM's core code to get this to work. Also I don't think the code is as secure as we would like it. There is a point at which the password is sent (although encrypted) via a GET request. This would not be our preferred choice.
As a result of these imperfections I am not posting the full module here. If you would like a copy or would like to help the continuing development of this project, please get in touch and I'll see what I can do.
Where to begin? There's a lot of fundamental knowledge which helps the Drupal learning curve but I've found some rules that I stick as close as possible to, to help me through the day.
There's a good reason to not alter the core files. Its called maintenance. Drupal allows a developer to keep custom code in only one or two locations which makes replacing core files that much easier.
If you have to alter the core files, DOCUMENT IT. A small text file for your own purposes in the root of the site could really help. Call it something obvious like README.txt or MODIFICATIONS.txt or whatever you choose, then have a clear readable structure to the doc to help you out. Here's an example:
Notes on modifications to core and standard modules
=============
CONTENTS
=============
1) Taxonomy.module (core)
2) Profile.module (core)
3) Video.module
Appendix
A) Sites Affected
B) Misc Notes
=====================
1) Taxonomy.module
We changed this module so that the taxonomy ID shows up in the list terms page for categories table.
2) Profile.module
Changed this module to automatically filter out the neurotic ravings of teenage schoolgirls.
3) Video.module
Code has been altered to prevent random deletion of the converted video files. See line 890.
=============
APPENDIX
=============
A) Sites affected
www.example.com
www.notonyournelly.com
B) Misc Notes
Some sites have had ECT to prevent them working on IE 6 it seems. Must sort this out and get the themes some counselling.There are so many script kiddies around now that we constantly have to keep our guard up. In the past we had several Joomla sites compromised because the clients did not keep up maintenance or want to have any kind of support package in place so we would do it. By documenting any core module changes you can update to the latest, most secure version at a moment's notice.
When writing your code. Never output submitted text directly to the screen and never use it without checking it first in an SQL query. Let me show you a non-drupal specific example.
You are creating a login box of your own for some reason. The user has the usual username and password fields. The values of these fields go straight into a db query.
Normal user
username: bob
password: halibutWhen we get to the query:
SELECT uid FROM users WHERE name = 'bob' AND password = 'halibut'Now for the malicious user
username: admin
password: ' OR 'a' = 'aIt may look odd but see what happens
SELECT uid FROM users WHERE name = 'admin' AND password = '' OR 'a' = 'a'And there you have it. An instant admin login.
That is, things a drupal developer might benefit knowing or doing.
I started writing this post a few weeks ago and when clicking submit, my internet connection dropped off and I subsequently lost it all. I'll try and capture the essence of my first post but with added readability.
As a rule I try and comment every function as I go along. It is of course possible to reach the point of overkill with comments. If I am implementing a hook I say so with the simple line Implementing hook_something()
Another thing I like to do is section my files up. Like this:
//===================================================
// DRUPAL HOOKS
//===================================================
//===================================================
// DRUPAL CALLBACKS
//===================================================
//===================================================
// PROCESSING FUNCTIONS
//===================================================
//===================================================
// THEMING
//===================================================
//===================================================
// HELPER FUNCTIONS
//===================================================I know that sometimes I slip up but generally I like to keep a simple structure to the function names and variables. Firstly I tend to keep my helper functions prefixed with an underscore: e.g. _my_helper_function()
Next, camel-case or underscores? I seem to have ended up using underscores but the choice is yours. If you pick one and stick to it, at least for that whole module, you should be able to easily remember your function names.
I always keep my constants defined right at the top of my document and in upper case. This is something that I am very strict on. It's not hard to do but can make things so much easier.
I use my constants to define the items in the hook_perm array. This means I can assign a simple name to the constant and then a human readable string for the value shown on the permissions page.
Even if you are a bloke
Make sure you have an easy to reach bookmark of the api pages (http://api.drupal.com) You'll find them invaluable. There are changes you might not be expecting between versions 5 and 6 and I think radical changes when we get to version 7. You'll need to use the api to find these differences. There is also a section in the main drupal developer handbook online that highlights some of the key changes (see: http://drupal.org/node/114774)
I use subversion (see: http://subversion.tigris.org) to keep all my sites in check. There are often multiple developers on the same project and trying to keep the code base in sync was a pain before I started using it. Basically I never lose anything now. I use the subversion server as the master copy, the working copies that people have are used to develop on and then we have our live/test servers. Using this method, even if someone decides they want to work on the same file at the same time you can easily combine changes and prevent losing one dev's changes. I recommend it.
If you are beginning drupal I recommend getting a good book on the subject. I have a few but I keep referring to the "Pro Drupal Development" book by John K. VanDyk published by apress. It covers a lot and will help you to get your head around the concepts of what happens under the skin of drupal.
the other thing I would suggest is to get on to the IRC channel (see: http://en.wikipedia.org/wiki/Irc) which is based on irc.freenode.net. Join the #drupal channel and ask away. Please bear in mind that there is an unwritten etiquette in the channel. Try not to annoy people and be patient, most users are holding down their day job and might not have time right away. If someone knows, they will answer.