Commit cddbc91f authored by bgm's avatar bgm Committed by Aegir user

init platform, based on civicrm-website-org but without site-specific files/themes/custom-modules

parents
name = CCRM Extension Validation
description = Replaces ccrm_validateextrelease. Validates a CiviCRM native module XML descriptor and sets node values to match XML file. Also validates Extension and Release Maintainers.
package = civicrm.org custom modules
version = 7.x-1.0
core = 7.x
files[] = ccrm_extensionvalidation.module
files[] = ccrm_extensionvalidation.version.inc
files[] = ccrm_extensionvalidation.test
dependencies[] = taxonomy
dependencies[] = list
php = 5.2
<?php
define('CCRM_EXTVALID_XMLFILE_FIELD', 'field_extension_release_xml');
/**
* Loads and Validates fields from the XML info file:
* Release version == <version> (required)
* Release Date == <releaseDate>
* Release Status == <develStage> (required)
* Release CiviCRM Compatibility == <compatibility><ver> (required)(multiple ok)
* Parent Extension FQN == <extension key="xx.yy.zz"> (required)
* Documentation Link == <url desc="http://example.com">
* (Postponed) Download URL > downloadUrl (required)
* TODO: Fully validate the XML document format against a DOCtype
*/
/**
*
* implements hook_node_validate
*/
function ccrm_extensionvalidation_node_validate($node, $form, &$form_state) {
// Make sure author of extension is in maintainers
if ('extension' == $node->type) {
$authornid = $node->uid; $delta = 0; $foundauthor = FALSE;
foreach ($form_state['values']['field_maintainers'][LANGUAGE_NONE] as $index => $ref) {
if (is_numeric($ref['target_id'])
&& $form_state['values']['field_maintainers'][LANGUAGE_NONE][$index]['target_id'] == $authornid) {
$foundauthor = TRUE;
}
$delta++;
}
if (FALSE == $foundauthor) {
$form_state['values']['field_maintainers'][LANGUAGE_NONE][$delta]['target_id'] = $authornid;
$form_state['values']['field_maintainers'][LANGUAGE_NONE][$delta]['_weight'] = $delta;
}
}
// Set maintainers on releases from parent extension
if ($node->type == 'extension_release_civicrm') {
$parentnid = $node->field_extension_nr_crm[LANGUAGE_NONE][0]['nid'];
}
if ($node->type == 'extension_release_cms') {
$parentnid = $node->field_extension_nr_cms[LANGUAGE_NONE][0]['nid'];
}
if ($node->type == 'extension_release_civicrm' || $node->type == 'extension_release_cms') {
$parent_node = is_numeric($parentnid) ? node_load($parentnid) : NULL;
if (NULL == $parent_node) {
_ccrmextvalid_formseterror(array(
'title' => t('Could not determine parent extension. Please add via extension page.'),
));
return;
}
}
if ($node->type == 'extension_release_cms') {
// Delete current maintainers
foreach ($form_state['values']['field_extension_release_na_mains'][LANGUAGE_NONE] as $index => $ref) {
if (is_numeric($ref['target_id'])) {
$form_state['values']['field_extension_release_na_mains'][LANGUAGE_NONE][$index]['target_id'] = NULL;
}
}
// Check parent node has maintainers set and add maintainers from parent
$delta = 0;
foreach ($parent_node->field_maintainers[LANGUAGE_NONE] as $index => $ref) {
if (is_numeric($ref['target_id'])) {
$form_state['values']['field_extension_release_na_mains'][LANGUAGE_NONE][$index]['target_id'] = $ref['target_id'];
$form_state['values']['field_extension_release_na_mains'][LANGUAGE_NONE][$index]['_weight'] = $delta++;
}
}
}
if ($node->type == 'extension_release_civicrm') {
// Delete current maintainers
foreach ($form_state['values']['field_extension_release_ci_mains'][LANGUAGE_NONE] as $index => $ref) {
if (is_numeric($ref['target_id'])) {
$form_state['values']['field_extension_release_ci_mains'][LANGUAGE_NONE][$index]['target_id'] = NULL;
}
}
// Check parent node has maintainers set and add maintainers from parent
$delta = 0;
foreach ($parent_node->field_maintainers[LANGUAGE_NONE] as $index => $ref) {
if (is_numeric($ref['target_id'])) {
$form_state['values']['field_extension_release_ci_mains'][LANGUAGE_NONE][$index]['target_id'] = $ref['target_id'];
$form_state['values']['field_extension_release_ci_mains'][LANGUAGE_NONE][$index]['_weight'] = $delta++;
}
}
}
if ($node->type == 'extension_release_civicrm') {
$errors = _ccrmextvalid_validatexml($node, $form, $form_state, $parent_node);
if (!empty($errors)) {
_ccrmextvalid_formseterror($errors);
}
}
}
function _ccrmextvalid_validatexml($node, $form, &$form_state, $parent_node) {
$keeperrors = array();
$file_id = $form_state['values'][CCRM_EXTVALID_XMLFILE_FIELD][LANGUAGE_NONE][0]['fid'];
if ($file_id != 0) {
$file = file_load($file_id);
if ($file) {
libxml_use_internal_errors(TRUE);
$doc = new DOMDocument();
$doc->recover = TRUE;
$doc->strictErrorChecking = FALSE;
if ($doc->load(drupal_realpath($file->uri))) {
// Initialize variables to be read
$got = array('version' => FALSE, 'releaseDate' => FALSE, 'develStage' => FALSE,
'ver' => FALSE, 'extension' => FALSE, 'documentation' => FALSE); //'downloadUrl' =>FALSE (postponed)
foreach ($got as $tag => $tagcontents) {
$error = _ccrmextvalid_validateTag($doc, $tag, $tagcontents, $parent_node);
$got[$tag] = $tagcontents;
if (empty($error)) {
_ccrmextvalid_assignTag($form, $form_state, $tag, $tagcontents);
} else {
_ccrmextvalid_seterror( 'title', $error, $keeperrors);
drupal_set_message(t('Invalid or empty tag in the extension release XML: @tag', array('@tag' => $tag)));
}
}
}
else {
_ccrmextvalid_seterror( 'title', t('The XML file could not be read'), $keeperrors);
}
}
}
else {
_ccrmextvalid_seterror( CCRM_EXTVALID_XMLFILE_FIELD, t('Please upload an XML file descriptor'), $keeperrors);
}
return $keeperrors;
}
/**
* Checks the validity of an XML tag in an extension release XML file
* @param $doc XML as a DOMDocument() class
* @param $tag tag that needs validation
* @param $tagcontents value of the tag (will be returned)
* @param $parent_node Drupal node for the parent extension
* @return error message for the validation or empty string for success
* NOTE: this function is also used in the extdir module during automated release creation
*/
function _ccrmextvalid_validateTag($doc, $tag, &$tagcontents, $parent_node) {
$gotit = FALSE; $tagcount = 0; $contents = '';
switch ($tag) {
case 'version':
case 'releaseDate':
case 'develStage':
case 'downloadUrl':
$tagcount = _ccrmextvalid_getOneReqdByTag($doc, $tag, $tagcontents);
break;
case 'ver':
$tagcount = _ccrmextvalid_getByTag($doc, $tag, $tagcontents);
break;
case 'documentation':
$domnodes = $doc->getElementsByTagName('url');
$tagcount = $domnodes->length;
foreach ($domnodes as $node) {
if ($node->hasAttributes()) {
$attrib = $node->attributes->getNamedItem('desc');
if (strtolower($attrib->value) == "documentation") {
$tagcontents = $node->nodeValue;
}
}
}
break;
case 'extension':
$domnodes = $doc->getElementsByTagName('extension');
$tagcount = $domnodes->length;
if ($tagcount == 1) {
$elmt = $domnodes->item(0);
if ($elmt->hasAttributes()) {
$tagcontents = $elmt->getAttribute('key');
}
}
break;
default:
return t("The Extension Release XML file contained an invalid tag: '@tag'", array('@tag' => $tag));
}
if ($tagcount > 0) {
switch ($tag) {
case 'version':
module_load_include('version.inc', 'ccrm_extensionvalidation');
if (ccrm_extensionvalidation_version_isValid($tagcontents)) {
$contents = $tagcontents;
$gotit = TRUE;
}
break;
case 'releaseDate':
$tmp = filter_var($tagcontents, FILTER_SANITIZE_STRING);
if (preg_match('/\d{4}-\d{2}-\d{2}/', $tmp)) {
//$contents = new DateObject($tagcontents, NULL);
$contents = $tmp;
$gotit = TRUE;
}
break;
case 'develStage':
$contents = _ccrmextvalid_checkallowedval($tagcontents, 'field_extension_release_status');
if ($contents && (count($contents) == 1)) {
$gotit = TRUE;
}
$contents = $contents[0];
break;
case 'ver':
if ($tagcount == 1) {
$tagcontents = array($tagcontents);
}
$vers = array_map('_ccrmextvalid_mapversionval', $tagcontents);
$okvers = _ccrmextvalid_checkallowedval($vers, 'field_extension_release_civicrm');
if (count($okvers) == count($vers)) {
$gotit = TRUE;
$contents = array_map('_ccrmextvalid_mapversionvaltid', $okvers);
} else {
return t("The Version Compatibility tag contains an incorrect value");
}
break;
case 'documentation':
$contents = filter_var($tagcontents, FILTER_VALIDATE_URL);
$gotit = TRUE;
break;
case 'extension':
$parentkey = isset($parent_node) ? $parent_node->field_extension_fq_name[LANGUAGE_NONE][0]['safe_value'] : NULL;
$contents = $parentkey . ' vs. ' . $tagcontents;
if ($tagcontents == $parentkey) {
$gotit = TRUE;
} else {
return t("The Fully Qualified Name did not match parent extension: @tagcont", array('@tagcont' => $tagcontents));
}
break;
}
$tagcontents = $contents;
}
if ($gotit == false) {
// There is an error but we do not have all the details - come out with a generic message
return t("In info.xml, the element (\"@tag\") is missing or invalid.", array('@tag' => $tag));
}
// All is good, we have no error messages to return
return null;
}
function _ccrmextvalid_assignTag($form, &$form_state, $tag, &$tagcontents) {
// Add $tagcontents to $form_state
switch ($tag) {
case 'version':
module_load_include('version.inc', 'ccrm_extensionvalidation');
$form_state['values']['field_extension_release_version'][LANGUAGE_NONE][0]['value'] = t('Version @ver', array('@ver' => $tagcontents));
$form_state['values']['field_extension_release_vc'][LANGUAGE_NONE][0]['value'] = ccrm_extensionvalidation_version_normalize($tagcontents);
break;
case 'releaseDate':
$form_state['values']['field_extension_release_date'][LANGUAGE_NONE][0]['value'] = $tagcontents;
break;
case 'develStage':
$form_state['values']['field_extension_release_status'][LANGUAGE_NONE][0]['value'] = $tagcontents;
break;
case 'downloadUrl':
$form_state['values']['field_extension_release_url'][LANGUAGE_NONE][0]['url'] = $tagcontents;
break;
case 'ver':
$form_state['values']['field_extension_release_civicrm'][LANGUAGE_NONE] = $tagcontents;
break;
case 'documentation':
$form_state['values']['field_documentation'][LANGUAGE_NONE][0]['url'] = $tagcontents;
break;
}
}
// Get the value of a single tag from the XML that should only have one value
function _ccrmextvalid_getOneReqdByTag($doc, $tagname, &$values) {
$defaultval = $values;
$tagcount = _ccrmextvalid_getByTag($doc, $tagname, $values);
if ($tagcount > 1) {
form_set_error(CCRM_EXTVALID_XMLFILE_FIELD,
t("XML file must contain only one '@tagname' element"), array('tagname' => $tagname));
}
return $tagcount;
}
// Get all tags by value from the XML
function _ccrmextvalid_getByTag($doc, $tagname, &$values) {
$domnodes = $doc->getElementsByTagName($tagname);
$nodecount = $domnodes->length;
if ($nodecount > 0) {
if ($nodecount > 1) {
$values = array();
for ($i = 0; $i < $domnodes->length; $i++) {
$values[] = $domnodes->item($i)->nodeValue;
}
}
else {
$values = $domnodes->item(0)->nodeValue;
}
}
return $nodecount;
}
// Compare a value with the allowed values (lower case)
function _ccrmextvalid_checkallowedval($vals, $fieldname) {
$retvals = array();
$vals = !is_array($vals) ? array($vals) : $vals;
$fld = field_info_field($fieldname);
if ($fld['type'] == 'taxonomy_term_reference') {
$allowedvals1 = taxonomy_allowed_values($fld);
}
elseif ($fld['type'] == 'list_text') {
$allowedvals1 = list_allowed_values(field_info_field($fieldname));
}
else {
// No other field types handled
return $retvals;
}
// Taxonomy tags search in values, list search in keys
$allowedvals = array_map('strtolower', $allowedvals1);
foreach ($vals as $key => $val) {
if ($fld['type'] == 'list_text') {
if (array_key_exists($val, $allowedvals)) {
$retvals[] = $val;
}
}
else {
$retvals = array_merge($retvals, array_keys($allowedvals, $val));
}
}
return $retvals;
}
// Little helper function for array_map
function _ccrmextvalid_mapversionval($val) {
return "civicrm {$val}";
}
// Little helper function for array_map
function _ccrmextvalid_mapversionvaltid($val) {
return array( 'tid' => $val);
}
function _ccrmextvalid_seterror( $name, $message, &$keeperrors) {
// Form_set_error can only happen once on an element so collect the errors
$keeperrors[$name] = empty($keeperrors[$name]) ? $message : $keeperrors[$name] . " " . $message;
}
function _ccrmextvalid_formseterror($errors) {
foreach ($errors as $name => $err) {
// No other field is working reliably, tried the file upload button & others so usually use title
form_set_error(check_plain($name), check_plain($err));
}
}
<?php
/**
* @file
* Tests for ccrm_extensionvalidation.module.
*/
/**
* Tests version validation/formatting logic
*/
class CcrmExtensionValidatationTestCase extends DrupalUnitTestCase {
public static function getInfo() {
return array(
'name' => 'Version Formatting',
'description' => 'Tests version validation/formatting logic',
'group' => 'ccrm_extensionvalidation',
);
}
public function setUp() {
parent::setUp();
module_load_include('version.inc', 'ccrm_extensionvalidation');
}
public function testIsValid() {
$valids = array(
'1',
'1.1',
'1.2.3.4',
'1.2-3',
'1.2.alpha2',
'1.2.rc2',
'2012-01-01-1',
'2012-01-01',
'r456',
'r5000',
);
$invalids = array(
'Four',
'1.2 alpha',
'1 2',
'1.2-a',
'1/2',
'1,2',
'1.2-generalrelease',
'123456789', // exceeds padding limit
'20120101', // exceeds padding limit
'20120101124556', // exceeds padding limit
'XIV',
'1.pre10',
);
foreach ($valids as $valid) {
$this->assertEqual(
TRUE,
ccrm_extensionvalidation_version_isValid($valid),
"Expected that '$valid' is valid"
);
}
foreach ($invalids as $invalid) {
$this->assertEqual(
FALSE,
ccrm_extensionvalidation_version_isValid($invalid),
"Expected that '$invalid' is invalid"
);
}
}
public function testSortNormalizedValues() {
$cases = array(
array('1.alpha1', '1.alpha2', '1.beta', '1.0', '1.0.1', '1.1'),
array('1.alpha.1', '1.beta2','1.rc1'),
array('4.2.alpha1', '4.2.alpha2', '4.2.beta1', '4.2.beta2', '4.2.0'),
array('1.9', '1.9.1', '1.10', '1.10.1'),
array('r1', 'r9', 'r10', 'r900', 'r1001'),
array('1', '2', '9', '10', '11'),
array('1.0', '1.1', '2.0', '2.1', '3.0', '3.1'),
);
foreach ($cases as $case) {
$expectedOrder = array();
foreach ($case as $ver) {
$expectedOrder[] = ccrm_extensionvalidation_version_normalize($ver);
}
// shuffle and sort using MySQL ordering
$actualOrder = $expectedOrder;
shuffle($actualOrder);
//$actualOrder = self::sqlSort($actualOrder);
usort($actualOrder, array(__CLASS__, 'cmpSql'));
// did we get it right?
$this->assertEqual($expectedOrder, $actualOrder);
}
}
public function testSplit() {
$cases = array(
'1' => array('z0000001'),
'1.1' => array('z0000001', 'z0000001'),
'1.2.3.4' => array('z0000001', 'z0000002', 'z0000003', 'z0000004'),
'1.2-3' => array('z0000001', 'z0000002', 'z0000003'),
'1.2.alpha2' => array('z0000001', 'z0000002', 'pre010', 'z0000002'),
'1.2beta2' => array('z0000001', 'z0000002', 'pre020', 'z0000002'),
'2012-01-01-1' => array('z0002012', 'z0000001', 'z0000001', 'z0000001'),
'20120101' => array('invalid'),
'r456' => array('z0000456'),
'r5000' => array('z0005000'),
'whiz' => array('whiz'),
);
foreach ($cases as $input => $expect) {
$actual = ccrm_extensionvalidation_version_split($input, CCRM_VALIDATEEXTRELEASE_PAD);
$this->assertEqual($actual, $expect, sprintf("input=[%s] expect=[%s] actual=[%s]",
$input,
implode('/', $expect),
implode('/', $actual)
));
}
}
/**
* Sort a list using MySQL's default sort for VARCHARs
*
static protected function sqlSort($values) {
db_query("CREATE TEMPORARY TABLE tmp_sort (value VARCHAR(128)) ENGINE=MEMORY")->execute();
foreach ($values as $value) {
db_query("INSERT INTO tmp_sort (value) VALUES ('$value')");
//db_query("INSERT INTO tmp_sort (value) VALUES (:value)", array(
// ':value' => $value,
//));
}
$result = array();
$q = db_query("SELECT value FROM tmp_sort ORDER BY value");
$q->execute();
while ($row = $q->fetchAssoc()) {
$result[] = $row['value'];
}
db_query("DROP TEMPORARY TABLE tmp_sort")->execute();
return $result;
} */
/**
* Compare two values using MySQL string comparison
*
* Note: SQL injection because WTF but doesn't matter because inputs are controlled test data
*/
static protected function cmpSql($a, $b) {
// Note: SQL injection from test data
$q = db_query("SELECT IF('$a' < '$b', 'lt','gte') as lt, IF('$a' = '$b', 'eq','ne') as eq");
/*
$q = db_query("SELECT IF(:a < :b,'lt','gte') as lt, IF(:a = :b,'eq','ne') as eq", array(
':a' => $a,
':b' => $b,
));
*/
$q->execute();
$res = $q->fetchAssoc();
if ($res['eq'] == 'eq') {
return 0;
} elseif ($res['lt'] == 'lt') {
return -1;
} else {
return 1;
}
}
}
<?php
define(CCRM_EXTENSIONVALIDATION_PAD, 7);
/**
* Determine if a version is valid
*
* @return bool
*/
function ccrm_extensionvalidation_version_isValid($ver) {
$parts = ccrm_extensionvalidation_version_split($ver);
foreach ($parts as $part) {
if (!preg_match('/^(z[0-9]+|pre[0-9]+)$/', $part)) {
return FALSE;
}
}
return TRUE;
}
/**
* Normalize version, producing a code that can be correclty
* sorted in SQL.
*
* @return string
*/
function ccrm_extensionvalidation_version_normalize($ver) {
return implode('-', ccrm_extensionvalidation_version_split($ver));
}
/**
* Clean version number:
* - Use consistent delimiter
* - Change revision #s (r456) to straight numbers (456)
* - Explode into numeric and alphabetic parts
* - Make numerals higher than alphas (1.2.0 > 1.2.alpha)
*
* @return array of version parts
*/
function ccrm_extensionvalidation_version_split($ver, $pad = CCRM_EXTENSIONVALIDATION_PAD) {
$ver = strtolower($ver);
$ver = preg_replace('/-/', '.', $ver);
$ver = preg_replace('/^r/', '', $ver);
$ver .= '.';
$parts = array();
$len = strlen($ver);
$buf = '';
$state = 'NEW';
for ($i = 0; $i < $len; $i++) {
if ($ver{$i} == '.') {
$newCharType = 'SEP';
} elseif (is_numeric($ver{$i})) {
$newCharType = 'NUM';
} else {
$newCharType = 'ALPHA';
}
// printf("ver=[%s] state=%-5s newCharType=%-5s char=[%s] parts=[%-12s] buf=[%s]\n", $ver, $state, $newCharType, $ver{$i}, implode('/', $parts), $buf);
switch ($state) {
case 'NEW':
$buf .= $ver{$i};
$state = $newCharType;
break;
case 'NUM':
case 'ALPHA':
default:
if ($state == $newCharType) {
$buf .= $ver{$i};
} elseif ($newCharType == 'SEP') {
$parts[] = $buf;
$buf = '';
$state = 'NEW';
} elseif ($newCharType == 'NUM') {
$parts[] = $buf;
$buf = $ver{$i};
$state = $newCharType;
} elseif ($newCharType == 'ALPHA') {
$parts[] = $buf;
$buf = $ver{$i};
$state = $newCharType;
}
break;
}
}
$codes = array(
'alpha' => 'pre010',
'beta' => 'pre020',
'rc' => 'pre030',
);
foreach ($parts as $i => &$part) {
if (is_numeric($part)) {
if (strlen($part) > $pad) {
$part = 'invalid';
} else {
$part = sprintf("z%0${pad}s", $part);
}
} else {
if (isset($codes[$part])) {
$part = $codes[$part];
}
}
}
return $parts;
}
name = CiviCRM.org membership
description = Code to support the CiviCRM member program
package = civicrm.org custom modules
core = 7.x
<?php
/**
* Implements hook_civicrm_buildForm.
*
* Alters contribution forms to hide "site id" field and populate it from the url.
*
* @param string $formName
* @param CRM_Core_Form $form
*/
function civicrm_org_members_civicrm_buildForm($formName, &$form) {
if ($formName == 'CRM_Contribute_Form_Contribution_Main' && $form->elementExists('onbehalf[custom_193]')) {
$form->removeElement('onbehalf[custom_193]');
$form->addElement('hidden', 'onbehalf[custom_193]');
$sid = CRM_Utils_Array::value('sid', $_GET);
if ($sid && preg_match('/^[a-zA-Z0-9]{32}$/', $sid)) {
$form->setDefaults(array('onbehalf[custom_193]' => $sid));
}
}
}
/**
* Implements hook_civicrm_sumfields_definitions()
*
* Adds summary fields to calculate total contributions to the project
*
* @param $custom
*/
function civicrm_org_members_civicrm_sumfields_definitions(&$custom) {
$custom['fields']['partner_total_twelve_months'] = array(
'label' => 'Finanancial and In-Kind Contributions (12 months)',
'data_type' => 'Money',
'html_type' => 'Text',
'weight' => '15',
'text_length' => '32',
// Tally all financial contributions, code contributions (type 18) get multiplied
'trigger_sql' => '(
SELECT COALESCE(SUM(IF(cont1.financial_type_id = 18, cont1.total_amount*10, cont1.total_amount)),0)
FROM civicrm_contribution cont1
LEFT JOIN civicrm_contribution_soft soft
ON soft.contribution_id = cont1.id
WHERE (cont1.contact_id = NEW.contact_id OR soft.contact_id = NEW.contact_id)
AND CAST(cont1.receive_date AS DATE) BETWEEN DATE_SUB(NOW(), INTERVAL 12 MONTH) AND NOW()
AND cont1.contribution_status_id = 1 AND cont1.financial_type_id IN (1,2,10,12,19,11,13,5,6,18)
)',
'trigger_table' => 'civicrm_contribution',