#!/usr/bin/php DRUPAL_ROOT, 'SITE_NAME' => SITE_NAME, ); $fatal_err = FALSE; foreach ($vars as $name => $val) { if (empty($val)) { print "ERROR: \"$name\" constant not defined, aborting\n"; $fatal_err = TRUE; } } if ($fatal_err) { exit(1); } $script_name = $argv[0]; // See if we're being restricted to a single project. $project_id = 0; if (!empty($argv[1])) { $project_id = $argv[1]; } // Setup variables for Drupal bootstrap $_SERVER['HTTP_HOST'] = SITE_NAME; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['REQUEST_URI'] = '/' . $script_name; $_SERVER['SCRIPT_NAME'] = '/' . $script_name; $_SERVER['PHP_SELF'] = '/' . $script_name; $_SERVER['SCRIPT_FILENAME'] = $_SERVER['PWD'] .'/'. $script_name; $_SERVER['PATH_TRANSLATED'] = $_SERVER['SCRIPT_FILENAME']; if (!chdir(DRUPAL_ROOT)) { print "ERROR: Can't chdir(DRUPAL_ROOT), aborting.\n"; exit(1); } // Make sure our umask is sane for generating directories and files. umask(022); require_once 'includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); if (!is_dir(HISTORY_ROOT)) { if (!mkdir(HISTORY_ROOT)) { wd_err(array('message' => "ERROR: Could not create history directory (%directory).\n", 'args' => array('%directory' => HISTORY_ROOT))); exit(1); } } project_release_history_generate_all($project_id); if (empty($project_id)) { // If we're operating on all projects, generate the huge list, too. project_list_generate(); } // ------------------------------------------------------------ // Functions: main work // ------------------------------------------------------------ /** * Figure out what project and API terms to generate the history for. */ function project_release_history_generate_all($project_id = 0) { if (!empty($project_id)) { if (is_numeric($project_id)) { $project_nid = $project_id; } else { $project_nid = db_result(db_query("SELECT nid FROM {project_projects} WHERE uri = '%s'", $project_id)); } if (empty($project_nid)) { wd_err(array('message' => 'Project ID %id not found', 'args' => array('%id' => $project_id))); return FALSE; } wd_msg(array('message' => 'Generating XML release history files for project: %id.', 'args' => array('%id' => $project_id))); } else { wd_msg(array('message' => 'Generating XML release history files for all projects.', 'args' => array())); } $api_terms = project_release_compatibility_list(); $i = 0; if (empty($project_nid)) { // Generate all.xml files for projects with releases. $query = db_query("SELECT DISTINCT(pid) FROM {project_release_nodes}"); while ($project = db_fetch_object($query)) { project_release_history_generate_project_xml($project->pid); $i++; } } else { project_release_history_generate_project_xml($project_nid); $i++; } if ($i == 1) { wd_msg(array('message' => 'Generated an XML release history summary for a project.')); } else { wd_msg(array('message' => 'Generated XML release history summaries for @count projects.', 'args' => array('@count' => $i))); } // Generate XML files based on API compatibility. $i = 0; $args = array_keys($api_terms); $placeholders = db_placeholders($args); $where = ''; if (!empty($project_nid)) { $args[] = $project_nid; $where = 'AND pid = %d'; } $query = db_query("SELECT DISTINCT(pid), version_api_tid FROM {project_release_nodes} WHERE version_api_tid IN ($placeholders) $where", $args); while ($project = db_fetch_object($query)) { project_release_history_generate_project_xml($project->pid, $project->version_api_tid); $i++; } if ($i == 1) { wd_msg(array('message' => 'Completed XML release history files for 1 project/version pair')); } else { wd_msg(array('message' => 'Completed XML release history files for @count project/version pairs', 'args' => array('@count' => $i))); } } /** * Generate the XML history file for a given project name and API * compatibility term. * * @todo If a history file already exists for this combination, this * function will generate a new history and atomically replace the old * one (currently, just logs to watchdog for debugging). * * @todo If there's no subdirectory in the directory tree for this * project yet, this function creates one. * * @param $project_nid * Project ID (node id of the project node) to generate history for. * @param $api_tid * Taxonomy id (tid) of the API compatibility term to use, or NULL if * all terms are considered. */ function project_release_history_generate_project_xml($project_nid, $api_tid = NULL) { $api_vid = _project_release_get_api_vid(); /// @todo: This is a drupal.org-specific hack. /// @see http://drupal.org/node/1003764 $is_profile = FALSE; if (isset($api_tid)) { // Restrict output to a specific API compatibility term. $api_terms = project_release_compatibility_list(); if (!isset($api_terms[$api_tid])) { wd_err(array('message' => 'API compatibility term %tid not found.', 'args' => array('%tid' => $api_tid))); return FALSE; } $api_version = $api_terms[$api_tid]; // Get project-wide data: $sql = "SELECT DISTINCT n.title, n.nid, n.vid, n.status, p.uri, u.name AS username FROM {node} n INNER JOIN {project_projects} p ON n.nid = p.nid INNER JOIN {project_release_supported_versions} prsv ON prsv.nid = n.nid INNER JOIN {users} u ON n.uid = u.uid WHERE prsv.tid = %d AND prsv.nid = %d"; $query = db_query($sql, $api_tid, $project_nid); } else { // Consider all API compatibility terms. $api_version = 'all'; $sql = "SELECT n.title, n.nid, n.vid, n.status, p.uri, u.name AS username FROM {node} n INNER JOIN {project_projects} p ON n.nid = p.nid INNER JOIN {users} u ON n.uid = u.uid WHERE p.nid = %d"; $query = db_query($sql, $project_nid); } $project = db_fetch_object($query); if (empty($project)) { if (empty($api_tid)) { wd_err(array('message' => 'Project ID @pid not found', 'args' => array('@pid' => $project_nid))); } else { wd_err(array('message' => 'Project ID @pid has no supported releases for API term ID @api_tid', 'args' => array('@pid' => $project_nid, '@api_tid' => $api_tid))); return FALSE; } } $xml = '
' . check_plain($full_xml), 'args' => array('@file' => $tmp_filename)));
return FALSE;
}
// We have to close this handle before we can rename().
fclose($hist_fd);
// Now we can atomically rename the .new into place in the "live" spot.
if (!_rename($tmp_filename, $filename)) {
wd_err($errors['rename']);
return FALSE;
}
return TRUE;
}
/**
* Generate a list of all projects available on this server.
*/
function project_list_generate() {
$api_vid = _project_release_get_api_vid();
$query = db_query("SELECT n.title, n.nid, n.vid, n.status, p.uri, u.name AS username FROM {node} n INNER JOIN {project_projects} p ON n.nid = p.nid INNER JOIN {users} u ON n.uid = u.uid");
$xml = '';
while ($project = db_fetch_object($query)) {
$xml .= " \n";
$xml .= ' '. check_plain($project->title) ." \n";
$xml .= ' '. check_plain($project->uri) ." \n";
$xml .= ' '. prch_url("node/$project->nid") ."\n";
$xml .= ' '. check_plain($project->username). " \n";
$term_query = db_query("SELECT v.name AS vocab_name, v.vid, td.name AS term_name, td.tid FROM {term_node} tn INNER JOIN {term_data} td ON tn.tid = td.tid INNER JOIN {vocabulary} v ON td.vid = v.vid WHERE tn.vid = %d", $project->vid);
$xml_terms = '';
while ($term = db_fetch_object($term_query)) {
$xml_terms .= ' '. check_plain($term->vocab_name) .' ';
$xml_terms .= ''. check_plain($term->term_name) ." \n";
}
if (!empty($xml_terms)) {
$xml .= " \n". $xml_terms ." \n";
}
if (!$project->status) {
// If it's not published, we can skip the rest for this project.
$xml .= " unpublished \n";
}
else {
$xml .= " published \n";
// Include a list of API terms if available.
$term_query = db_query("SELECT DISTINCT(td.tid), td.name AS term_name FROM {project_release_nodes} prn INNER JOIN {term_data} td ON prn.version_api_tid = td.tid WHERE prn.pid = %d AND td.vid = %d ORDER BY td.weight ASC", $project->nid, $api_vid);
$xml_api_terms = '';
while ($api_term = db_fetch_object($term_query)) {
$xml_api_terms .= ' '. check_plain($api_term->term_name) ." \n";
}
if (!empty($xml_api_terms)) {
$xml .= " \n". $xml_api_terms ." \n";
}
}
$xml .= " \n";
}
if (empty($xml)) {
wd_err(array('message' => 'No projects found on this server.'));
return FALSE;
}
project_release_history_write_xml($xml);
}
// ------------------------------------------------------------
// Functions: utility methods
// ------------------------------------------------------------
/**
* Wrapper function for watchdog() to log notice messages.
*
* @param $notice
* An associative array with 'message' and 'args' keys for the notice message
* and any arguments respectively.
* @param $link
* A link to associate with the message.
*/
function wd_msg($notice, $link = NULL) {
watchdog('release_history', $notice['message'], $notice['args'], WATCHDOG_NOTICE, $link);
}
/**
* Wrapper function for watchdog() to log error messages.
*
* @param $error
* An associative array with 'message' and 'args' keys for the error message
* and any arguments respectively.
* @param $link
* A link to associate with the message.
*/
function wd_err($error, $link = NULL) {
watchdog('release_hist_err', $error['message'], $error['args'], WATCHDOG_ERROR, $link);
}
/**
* Rename on Windows isn't atomic like it is on *nix systems.
* See http://www.php.net/rename about this bug.
*/
function _rename($oldfile, $newfile) {
if (substr(PHP_OS, 0, 3) == 'WIN') {
if (copy($oldfile, $newfile)) {
unlink($oldfile);
return TRUE;
}
return FALSE;
}
else {
return rename($oldfile, $newfile);
}
}
/**
* Sorting function to ensure releases are in the right order in the XML file.
*
* Loop over the fields in the release node we care about, and the first field
* that differs between the two releases determines the order.
*
* We first check the 'weight' (of the API version term) for when we're
* building a single list of all versions, not a per-API version listing. In
* this case, lower numbers should float to the top.
*
* We also need to special-case the 'rebuild' field, which is how we know if
* it's a dev snapshot or official release. Rebuild == 1 should always come
* last within a given major version, since that's how update_status expects
* the ordering to ensure that we never recommend a -dev release if there's an
* official release available. So, like weight, the lower number for 'rebuild'
* should float to the top.
*
* For every other field, we want the bigger numbers come first.
*
* @see project_release_history_generate_project_xml()
* @see usort()
*/
function _release_sort($a, $b) {
// This array maps fields in the release node to the sort order, where -1
// means to sort like Drupal weights, and +1 means the bigger numbers are
// higher in the listing.
$fields = array(
'weight' => -1,
'version_major' => 1,
'rebuild' => -1,
'version_minor' => 1,
'version_patch' => 1,
'version_extra_weight' => 1,
'version_extra_delta' => 1,
);
foreach ($fields as $field => $sign) {
if (!isset($a->$field) && !isset($b->$field)) {
continue;
}
if ($a->$field == $b->$field) {
continue;
}
return ($a->$field < $b->$field) ? $sign : (-1 * $sign);
}
}
/**
* Helper function to generate clean absolute links for the XML files.
*
* Relying on core's url() gives us crazy results when the script is invoked
* with a full path, since the construction of $base_url during bootstrapping
* is all wrong.
*
* @todo This doesn't handle sites installed in subdirectories that actually
* define their own $base_url in settings.php...
*/
function prch_url($path) {
static $base_url = NULL;
static $clean_url = NULL;
if (!isset($clean_url)) {
$clean_url = (bool)variable_get('clean_url', '0');
}
if (!isset($base_url)) {
$base_root = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';
$base_url = $base_root .= '://'. $_SERVER['HTTP_HOST'] .'/';
}
$path = drupal_get_path_alias($path, '');
if ($clean_url) {
return $base_url . $path;
}
else {
return $base_url .'?q='. $path;
}
}