HOWTO: Write an installation profile for the Drupal installer
The following is based on the initial documentation by CivicSpace Labs on how to create an install profile.
Anatomy of an installation profile
Install profiles are located in the 'profiles' directory of a Drupal installation, and named like example.profile. A typical profile file contains the following functions:
Note: I use profilename here as an example profile name. This is whatever your .profile file is named, some examples include default, my_blog_site, corporate, etc.
1. profilename_profile_modules() - REQUIRED
<?php
/**
* Return an array of the modules to be enabled when this profile is installed.
*
* @return
* An array of modules to be enabled.
*/
function profilename_profile_modules() {
return array(
// Enable required core modules first.
'block', 'filter', 'node', 'system', 'user', 'watchdog',
// Enable optional core modules next.
'blog', 'color', 'comment', 'forum', 'help', 'menu', 'taxonomy',
// Then, enable any contributed modules here.
'og', 'views', 'views_ui', 'views_rss',
);
}
?>Each module specified is enabled in order (important when you want to make database changes which affect other modules), which in turn fires its modulename.install file, if present. Note that you need to enable stuff like 'system', 'blocks', etc. in addition to extra modules like 'cart' and 'product.' Probably the best thing to do is copy/paste from the default.profile file's profile_modules() hook and go from there.
Warning: Not all modules can be enabled here. When this hook is run, none of the modules is actually included, not even the required core ones. Thus any module using a function call within its .install file, will cause this hook to fail. Thus, such modules can only be included within hook_profile_final(). This buggy behaviour will probably endure until Drupal 7.
2. profilename_profile_details() - REQUIRED
<?php
/**
* Return a description of the profile for the initial installation screen.
*
* @return
* An array with keys 'name' and 'description' describing this profile.
*/
function profilename_profile_details() {
return array(
'name' => 'Example profile',
'description' => 'This example profile will install some commonly used contrib modules.',
);
}
?>3. profilename_profile_final() - optional
<?php
/**
* Perform any final installation tasks for this profile.
*
* @return
* An optional HTML string to display to the user on the final installation
* screen.
*/
function profilename_profile_final() {
// Insert default user-defined node types into the database.
$types = array(
array(
'type' => 'page',
'name' => t('Page'),
'module' => 'node',
'description' => t('If you want to add a static page, like a contact page or an about page, use a page.'),
'custom' => TRUE,
'modified' => TRUE,
'locked' => FALSE,
),
array(
'type' => 'story',
'name' => t('Story'),
'module' => 'node',
'description' => t('Stories are articles in their simplest form: they have a title, a teaser and a body, but can be extended by other modules. The teaser is part of the body too. Stories may be used as a personal blog or for news articles.'),
'custom' => TRUE,
'modified' => TRUE,
'locked' => FALSE,
),
);
foreach ($types as $type) {
$type = (object) _node_type_set_defaults($type);
node_type_save($type);
}
// Default page to not be promoted and have comments disabled.
variable_set('node_options_page', array('status'));
variable_set('comment_page', COMMENT_NODE_DISABLED);
// Don't display date and author information for page nodes by default.
$theme_settings = variable_get('theme_settings', array());
$theme_settings['toggle_node_info_page'] = FALSE;
variable_set('theme_settings', $theme_settings);
// The return message is optional, if you omit it the default will be used.
return '<p>'. (drupal_set_message() ? t('Please review the messages above before continuing on to <a href="@url">your new Profile Name site</a>.', array('@url' => url(''))) : t('You may now visit <a href="@url">your new Profile Name site</a>.', array('@url' => url('')))) .'</p>';
}
?>In the profilename_profile_final() implementation, you have the opportunity to do anything extra AFTER the modules specified in profilename_profile_modules() have been installed. In this function you have access to the full Drupal API so you could define any custom content types, create vocabularies and terms, change variable settings to your liking, etc.
General strategy for setting options in your profile's hook_profile_final()
Below I've tried to itemize some of the most common tasks. But here is a general strategy that worked for me when I was developing the GJG install profile:
1. Take a dump of the database, using mysqldump or PHPMyAdmin
2. Change something on a form or whatever
3. Take another database dump
4. Diff the two
5. Take those lines that are different and stick them in db_query(), replacing table_name with {table_name}
Common stuff you need to do: store settings/variables
Anytime you submit a form under administer >> settings, it's put into the variable table. I find it helpful to take a dump of the variable table before and after visiting a settings page and then diff to find the differences. Another approach might be to view source to find out field names, and then check the database to see what value it input.
Variable defaults usually do not exist in the variable table when you first install. A routine way to to capture as many variables as possible is to open every link on the /admin page, (and then look for more links called "settings") and then submit every form, even if you do not want to change the value. This will force every default into the variables table.
A quick way to view all existing variables is to use devel and enable the devel block. This block contains a link to view all variables.
To then override a variable in your profile, we use variable_set. For example, to enable user locations from location module:
<?php
variable_set('location_user', 1);
?>The only place this can get tricky is with checkboxes/mutiselects, because those will get stored in a serialized array, so the db record will look like:
node_options_event:
a:2:{i:0;s:6:"status";i:1;s:7:"promote";}The "a:2" means it's an array with 2 elements. The way to insert this is like:
<?php
variable_set('node_options_event', array('status', 'promote'));
?>Common tasks you need to do: enable blocks
For this, I just take a dump of the blocks and boxes (for custom blocks) tables and then figure out which ones are custom vs. which ones come with Drupal by default (these will be in 'modules/system/system.install'). Then just insert the SQL directly in db_query statements, like:
<?php
db_query("INSERT INTO {blocks} VALUES ('gjg', '0', 1, 0, 1, 0, 0, 1, 'my*', '')");
?>Common stuff you need to do: configure roles/permissions
Again, for this I take a dump of the role, users_roles, and permissions tables, and just make sure not to include definitions for the built-in roles (basically, if it's in database.mysql, you don't want to take it because it will cause an error because of a duplicate record).
<?php
// Make an 'administrator' role
db_query("INSERT INTO {role} (rid, name) VALUES (3, 'admin user')");
// Add user 1 to the 'admin user' role
db_query("INSERT INTO {users_roles} VALUES (1, 3)");
// Change anonymous user's permissions - this is UPDATE rather than INSERT
db_query("UPDATE {permission} SET perm = 'access comments, can send feedback, access content, search content, view uploaded files' WHERE rid = 1");
// Insert new role's permissions
db_query("INSERT INTO {permission} (rid, perm, tid) VALUES (3, 'administer blocks, edit own blog,....', 0)");
?>Sample .profile file - gojoingo.profile
<?php
/**
* The modules that are enabled when this profile is installed.
*
* @return
* An array of modules to be enabled.
*/
function gojoingo_profile_modules() {
$core = array('system', 'block', 'blog', 'comment', 'contact', 'filter', 'forum', 'help', 'menu', 'node', 'page', 'path', 'profile', 'search', 'story', 'taxonomy', 'upload', 'user', 'watchdog');
$contrib = array('buddylist', 'front', 'content', 'text', 'jstools', 'location', 'location_views', 'event', 'rsvp', 'signup', 'signup conflicts', 'image', 'image_attach', 'image_gallery', 'invite', 'logintoboggan', 'og', 'og_basic', 'privatemsg', 'urlfilter', 'views', 'views_ui');
// TODO: How to deal w/ spam requirement?
// TODO: How will this deal with image_attach, which is a contrib module inside image?
return array_merge($core, $contrib);
}
/**
* Implementation of hook_profile_details().
*
* This contains an array of profile details for display from the main selection screen.
*/
function gojoingo_profile_details() {
return array(
'name' => 'GoJoinGo',
'description' => 'A social networking website with groups, events, friends, etc.'
);
}
/**
* Implementation of hook_profile_final().
*
* GoJoinGo platform installation.
*/
function gojoingo_profile_final() {
// Enable user locations
variable_set('location_user', 1);
// Turn on LoginToboggan features
variable_set('login_with_mail', 1);
variable_set('email_reg_confirm', 1);
variable_set('reg_passwd_set', 1);
variable_set('toboggan_immed_login', 1);
variable_set('toboggan_role', 4);
variable_set('toboggan_hijack', 1);
// Enable Organic Group access control
variable_set('og_enabled', 1);
db_query("DELETE FROM {node_access}");
// Make post visibility selectable by author - default to Public
variable_set('og_visibility', 2);
// Omit page types from OG
variable_set('og_omitted', array('page'));
// Enable Urlfilter
db_query("INSERT INTO {filters} (format, module, delta, weight) VALUES (1, 'urlfilter', 0, 10)");
/** CONFIGURATION SETTINGS */
// Change front page to my/home
variable_set('site_frontpage', 'my/home');
// Turn on user pictures
variable_set('user_pictures', 1);
// Set default primary links
variable_set('phptemplate_secondary_links', array(
'text' => array('my home', 'my blog', 'my groups', 'my events', 'my friends'),
'link' => array('my/home', 'my/blog', 'my/groups', 'my/events', 'my/friends'),
'description' => array('', '', '', '', ''),
));
// Set welcome message for anonymous users
variable_set('front_page', 'Welcome to '. variable_get('site_name', 'GoJoinGo') .'!');
// Change welcome email to include validation URL
variable_set('user_mail_welcome_body', "
%username,
Thank you for registering at %site.
IMPORTANT:
For full site access, you will need to click on this link or copy and paste it in your browser:
%login_url
This will verify your account and log you into the site. In the future you will be able to log in using the username and password that you created during registration.
Your new %site membership also enables to you to login to other Drupal powered websites (e.g. drupal.org) 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.

Docu needs much work
The complete part after Sample .profile file - gojoingo.profile should be revised. i worked on this and there is nearly in every "second" line a bug. most of this code isn't working at all!
i wonder how the following parts can ever work:
<?php
// 1. Page and Story isn't created
// 2. comment: secondary and primary links are not inside variables table, isn't it? this isn't working at all.
// 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 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('', '', '', '', ''),
));
// 3. comment: non-existing function
drupal_system_enable('theme', 'gojoingo');
// 4. comment: user1 already have ALL permissions
// 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)");
// 5. comment: is already inside, noone need this
// Enable PHPTemplate theme engine
db_query("INSERT INTO {system} VALUES ('themes/engines/phptemplate/phptemplate.engine', 'phptemplate', 'theme_engine', '', 1, 0, 0)");
// 6. comment: garland is the default theme!
// Disable bluemarine
//db_query("UPDATE {system} SET status = 0 WHERE name = 'bluemarine'");
// 7. to activate other themes, this one needs an insert like:
db_query("INSERT INTO {system} VALUES ('sites/all/themes/mytheme/page.tpl.php', 'mytheme', 'theme', 'themes/engines/phptemplate/phptemplate.engine', 1, 0, 0, -1, 0)");
?>
Activate a custom theme
When hook_profile_final() is fired only Garland theme is presented in a database. We need to call system_theme_data() to update it with all available themes:
<?php$themes = system_theme_data();
$preferred_themes = array('my_theme', 'my_another_theme', 'zen');
// In preference descending order
foreach ($preferred_themes as $theme) {
if (array_key_exists($theme, $themes)) {
variable_set('theme_default', $theme);
break;
}
}
?>
in 5.6
system_theme_data();system_initialize_theme_blocks('theme_name');
db_query("UPDATE {system} SET status = 1 WHERE type = 'theme' and name = 'theme_name'");
variable_set('theme_default', 'theme_name');
translation import... a hack.
Translation import can by done with
<?php
/* Import current install language files
* TODO This is only a HACK until autolocale module becomes available...
*
* copy the small "[isocode]/install.po" into profiles/myprofile/[isocode].po
* copy the bigger "[isocode]/de.po" into profiles/myprofile/po/[isocode].po
*/
global $profile, $install_locale;
static $mode = 'overwrite';
// Add language, if not yet supported
$languages = locale_supported_languages(TRUE, TRUE);
if (!isset($languages['name'][$install_locale])) {
$isocodes = _locale_get_iso639_list();
_locale_add_language($install_locale, $isocodes[$install_locale][0], FALSE);
}
$filename = './profiles/' . $profile . '/po/' . $install_locale . '.po';
if (file_exists($filename)) {
$file = (object) array('filepath' => $filename);
if ($ret = _locale_import_po($file, $install_locale, $mode) == FALSE) {
$message = t('The translation import of %filename failed.', array('%filename' => $file->filename));
}
else {
// import successfull, enable selected language and make standard
db_query("UPDATE {locales_meta} SET isdefault = 0 WHERE locale = '%s'", 'en');
db_query("UPDATE {locales_meta} SET enabled = 1, isdefault = 1 WHERE locale = '%s'", $install_locale);
}
}
?>
This is nuts...
Why not have a way to copy an existing site as a new profile? Seriously. Creating a profile could be as simple as
cp /var/lib/mysql/drupal_existing $drupal-path/profiles/new-profile.profile
And applying it to a new site would be just as straightforward. Or, since this approach assumes separate databases for each site, you could do the equivalent in SQL backups.
I agree. Here's help?
I agree that the installation routines are complicated. My workaround was as follows:
1) Set up a drupal site on my local server how I wanted it, noting especially the modules I planned to use. (Note: I learned the hard way to put non-core modules into sites/all/modules rather than in /modules)
2) Used PHPMyAdmin to dump the data for the entire site to an SQL text file, stored in my custom profile folder.
3) Through trial and error, decided which of the tables could be stripped out (the caches, for example) and removed them from the SQL file.
4) Created a new profile based on the Drupal default.
5) Modified the list of modules to include my additions.
6) Replaced the profile_final code with the code I offered in this link: http://drupal.org/node/147720 This code simply populates an empty Drupal database with all the data from the original site--content, settings, accounts, etc.
7) Wrapped everything up.
This will accomplish your goal.
HTH,
David
Script to download modules
See here for a script to include that will automatically download the latest version of any missing modules for you:
http://groups.drupal.org/node/10810
----
Drupal Micro-Blogging at Twitter
variable_set: unserialize
when trying to recreate a serialized variable like this from above:
node_options_event:a:2:{i:0;s:6:"status";i:1;s:7:"promote";}
Rather than laboring to recreate these as an array I like to just do this:
variable_set('node_options_event', unserialize(a:2:{i:0;s:6:"status";i:1;s:7:"promote";}));