# PLUGIN PREVIEW BY TEXTPATTERN.INFO
/*
adi_contact - Form field validation, spam prevention, email header/body extras, database update, MailChimp subscription
Written by Adi Gilbert
Released under the GNU General Public License
Version history:
0.6 - fixed: problem with validate type="equal" error message
- enhancement: database update functionality
- enhancement: MailChimp subscription
- TXP 4.6 compatibility
- new tags: adi_contact_headers & adi_contact_body
- ZCR 4.5+ compatibility: "errorElement" class used in addition to "zemRequirederrorElement"
0.5 - fixed: javascript doesn't find contact <form> if zcr input & error forms are separate (thanks jakob)
- fixed: error message if no fields filled in when using "value" attribute
- fixed: errors in checkbox processing (thanks jakob)
- enhancement: extended "name"+"value" dependency to "restrict" & "number" (for jakob)
0.4 - change: new adi_contact_checkboxes tag (adi_contact_combo 'checkbox' attribute withdrawn)
- enhancement: adi_contact_combo if "value" then "required" (for sacripant)
- fix: internationalised 'Yes' & 'No' in checkboxes (thanks sacripant)
- help section formatting errors (thanks net-carver) !!!!!!!! HAVEN'T ACTUALLY FIXED ANYTHING !!!!!!!!!
- fix: no more duplicate javascript insertion
- class action: 'adi_contact_combo_error', 'adi_contact_validate_error', 'adi_contact_spam_error' (for net-carver)
- new adi_contact_validate type 'base64' (for net-carver)
- javascript code rewrite (& bug fixes: <select>, multiple forms)
0.3 - enhancement: new adi_contact_validate option type="equal" (for kevinpotts)
- fix: adi_contact_combo now processes an unticked checkbox correctly (as not being a field of interest) (thanks Logoleptic)
- fix: adi_contact_validate coding error (thanks planeth)
0.2 - enhancement: new adi_contact_combo attribute 'required'; equivalent to restrict="equal" number="1", enables custom error messages (inspired by Bloke)
- enhancement: new adi_contact_validate attribute & option type="custom" pattern="pattern" (for Bloke)
- enhancement: new adi_contact_validate attribute option type="url" (inspired by Bloke)
- enhancement: new adi_contact_spam attribute 'exclude' (e.g. use in conjunction with adi_contact_validate type="url")
- fix: adi_contact_combo fails to cope with checkboxes (new attribute 'checkbox') (thanks valpc)
0.1.1 - fix: adi_contact_spam error message field lists incorrectly generated (both standard & custom)
0.1 - initial release
*/
// TXP 4.6 tag registration
if (class_exists('\Textpattern\Tag\Registry')) {
Txp::get('\Textpattern\Tag\Registry')
->register('adi_contact_body')
->register('adi_contact_checkboxes')
->register('adi_contact_combo')
->register('adi_contact_db_data')
->register('adi_contact_db')
->register('adi_contact_debug')
->register('adi_contact_headers')
->register('adi_contact_javascript')
->register('adi_contact_labels')
->register('adi_contact_mailchimp_data')
->register('adi_contact_mailchimp_email')
->register('adi_contact_mailchimp')
->register('adi_contact_spam')
->register('adi_contact_validate')
;
}
global $prefs,$zem_contact_form,$zem_contact_submit,$zem_contact_error,$zem_contact_labels,$zem_contact_values,$adi_contact_js_added,$txp_using_svn;
if (@txpinterface == 'admin') {
register_callback('adi_contact_lifecycle','plugin_lifecycle.adi_contact');
}
if (txpinterface === 'public') {
register_callback('adi_contact_db_update','zemcontact.submit');
register_callback('adi_contact_mailchimp_update','zemcontact.submit');
register_callback('adi_contact_deliver','zemcontact.deliver');
}
function adi_contact_lifecycle($event,$step) {
// enforce the load order
if ($step == 'installed')
$result = safe_update('txp_plugin',"load_order = 9","name = 'adi_contact'");
}
function adi_contact_javascript() {
// Javascript function for injecting error classes
$javascript = <<<END_SCRIPT
<script type="text/javascript">
function addErrorClass(list,thisFunction) {
var zemRequiredClass = 'zemRequirederrorElement errorElement', zemErrorElementClass = 'errorElement' , adiContactClass = 'adi_contact_error';
if (thisFunction === undefined)
thisClass = '';
else
thisClass = thisFunction + '_error';
thisClass = thisClass + ' ' + adiContactClass;
if (thisFunction == 'adi_contact_combo')
thisClass = thisClass + ' ' + zemRequiredClass;
if (thisFunction == 'adi_contact_validate')
thisClass = thisClass + ' ' + zemErrorElementClass;
if (thisFunction == 'adi_contact_spam')
thisClass = thisClass + ' ' + zemErrorElementClass;
// former method of identifying form don't work all the time - if zcr input & error forms are separate then the <form> tags lose their id!
// var formId = window.location.hash.substr(1); // convert #zcr... to zcr...
// var thisForm = document.getElementById(formId);
// new method
// get #zcr... anchor from URL
var anchor = window.location.hash;
// find <form> that has the action="" that matches the #zcr
var formsArray = document.getElementsByTagName("form");
for (i=0; i<formsArray.length; i++) {
var action = formsArray[i].getAttribute('action');
var pattern = RegExp(anchor,'i'); // i.e. /#zcr.../i
if (pattern.test(action)) // is #zcr... in form action="blah blah"?
thisForm = formsArray[i];
}
// apply error class to list
var labels = thisForm.getElementsByTagName('label'); // find all <label> in this form
var types = ["input","textarea","select"]; // tag types we're interested in
for (var x = 0; (thisType = types[x]); x++) { // for each tag type
var tags = thisForm.getElementsByTagName(thisType); // get all instances of tag type in this form
for (var i = 0; (thisTag = tags[i]); i++) { // for each tag of this type
if (list[thisTag.name]) { // name is on the list
for (var j = 0; (label = labels[j]); j++) {
if (label.htmlFor == thisTag.id) { // label 'for' matches input id
label.className += ' ' + thisClass;
thisTag.className += ' ' + thisClass;
// console.log("type=",thisTag.type,"name=",thisTag.name,"id=",thisTag.id,"label for=",label.htmlFor,"classes=",thisClass);
}
}
}
}
}
}
</script>
END_SCRIPT;
return $javascript;
}
function adi_contact_insert_javascript() {
// insert javascript function
global $adi_contact_js_added;
// only want to insert javascript function once
if (isset($adi_contact_js_added))
return '';
else {
$adi_contact_js_added = '';
return adi_contact_javascript();
}
}
function adi_contact_literal($list) {
// create string in form of an object literal {"field1":1,"field2":1 ... }
$list = explode(',',$list); // convert comma separated list to array
foreach ($list as $index => $value) // create literal as array
$literal[] = '"'.$value.'":1';
return '{'.join(',',$literal).'}'; // return as {comma separated list}
}
function adi_contact_message($default_msg,$user_msg) {
// generate zem_contact error message
// ZCR will strip out duplicate error messages in $zem_contact_error
global $zem_contact_error,$label_list,$number,$require_label_list;
if ($user_msg) {
$user_msg = str_replace('[NAMES]',$label_list,$user_msg);
$user_msg = str_replace('[REQUIRE]',$require_label_list,$user_msg);
$user_msg = str_replace('[NUMBER]',$number,$user_msg);
$zem_contact_error[] = $user_msg;
}
else
$zem_contact_error[] = $default_msg;
}
function adi_contact_get_labels($name_list) {
// translate list of names into list of labels
global $adi_contact_labels,$zem_contact_labels;
$label_list = array();
if (!isset($adi_contact_labels)) // initialise to empty array (<txp:adi_contact_labels not used or not placed before <txp:adi_contact_combo /> etc.
$adi_contact_labels = array();
foreach ($name_list as $index => $value) // try three options
if (array_key_exists($value,$adi_contact_labels)) // 1. user supplied label via adi_contact_labels tag
$label_list[] = $adi_contact_labels[$value];
else if (isset($zem_contact_labels)) // check for presence of label supplied by submitted form
if (array_key_exists($value,$zem_contact_labels)) // 2. use label supplied by submitted form
$label_list[] = $zem_contact_labels[$value];
else // 3. use the name itself, but pretty it up
$label_list[] = ucfirst(strtolower($value));
else // 3. use the name itself, but pretty it up
$label_list[] = ucfirst(strtolower($value));
return join(', ',$label_list);
}
function adi_contact_spam($atts) {
// tag to check form fields for mail headers and link tags
global $zem_contact_form,$zem_contact_labels,$zem_contact_values,$zem_contact_error,$adi_contact_labels,$label_list;
extract(lAtts(array(
'names' => '', // list of form fields we're interested in (default = all submitted fields)
'exclude' => '', // list of form fields to be ignored (default = none)
'message' => '', // alternative error message (with [NAMES] & [NUMBER])
'detect_mail_headers' => '1',
'detect_link_tags' => '1',
'allow_www' => '0', // allow presence of simple www.... links
'debug' => '0'
), $atts));
$return = adi_contact_insert_javascript();
if ($names) {
$name_list = explode(',',$names); // convert comma separated list to array
foreach ($name_list as $index => $field) // remove extraneous spaces
$name_list[$index] = trim($field);
}
else // get list of all submitted fields
isset($zem_contact_values) ?
$name_list = array_keys($zem_contact_values) :
$name_list = array();
if ($exclude) {
$exclude_list = explode(',',$exclude); // convert comma separated list to array
foreach ($exclude_list as $index => $field) // remove extraneous spaces
$exclude_list[$index] = trim($field);
}
else
$exclude_list = array();
$original_name_list = $name_list; // remember original list of fields
$name_list = array_diff($name_list,$exclude_list); // include minus exclude
if ($name_list) { // there's work to do
// link tags
$allow_www ?
$link_tags = '/(\[url|http|href)/i' :
$link_tags = '/(\[url|http|www|href)/i';
$link_tag_list = str_replace('/(\\','',$link_tags);
$link_tag_list = str_replace(')/i','',$link_tag_list);
$link_tag_list = str_replace('|',', ',$link_tag_list);
// mail headers
$mail_headers = '/(to\:|from\:|bcc\:|multipart|cc\:|Content\-Transfer\-Encoding\:|Content\-Type\:|Mime\-Version\:)/i';
$mail_headers_list = str_replace('/(','',$mail_headers);
$mail_headers_list = str_replace(')/i','',$mail_headers_list);
$mail_headers_list = str_replace('\\','',$mail_headers_list);
$mail_headers_list = str_replace('|',', ',$mail_headers_list);
$mail_header_error = FALSE;
$link_tag_error = FALSE;
$error_list = array();
foreach ($name_list as $field => $value) { // scan fields for spam
if ($detect_mail_headers)
if (preg_match($mail_headers,$zem_contact_values[$value])) {
$mail_header_error = TRUE;
$error_list[] = $value; // add field name to list
}
if ($detect_link_tags)
if (preg_match($link_tags,$zem_contact_values[$value])) {
$link_tag_error = TRUE;
$error_list[] = $value; // add field name to list
}
}
$label_list = adi_contact_get_labels($error_list); // create list of labels for spam fields
if ($mail_header_error)
adi_contact_message("Mail header injection detected in “<strong>$label_list</strong>” ($mail_headers_list not allowed).",$message);
if ($link_tag_error)
adi_contact_message("Link detected in “<strong>$label_list</strong>” ($link_tag_list not allowed).",$message);
$error_fields = adi_contact_literal(join(',',$error_list)); // don't want spaces in list
if ($mail_header_error || $link_tag_error)
$return .= '<script type="text/javascript">addErrorClass('.$error_fields.',"'.__FUNCTION__.'")</script>';
}
if ($debug) {
adi_contact_debug(__FUNCTION__,$atts,$name_list);
echo "FIELD LIST:<br/>";
dmp($original_name_list);
echo "EXCLUDE LIST:<br/>";
dmp($exclude_list);
if (isset($error_list)) {
echo "FIELD NAMES WITH SPAM ERRORS:<br/>";
dmp($error_list);
echo "SPAM FIELD LABELS:<br/>";
dmp($label_list);
}
}
return $return;
}
function adi_contact_labels($atts) {
// tag to record name/label mappings
global $adi_contact_labels;
$adi_contact_labels = $atts;
}
function adi_contact_checkboxes($atts) {
// tag to manually define which fields are checkboxes
global $zem_contact_submit,$adi_contact_is_checkbox,$zem_contact_values;
extract(lAtts(array(
'names' => '', // list of form fields that are checkboxes
'debug' => '0'
), $atts));
$adi_contact_is_checkbox = array();
if ($names) {
$name_list = explode(',',$names); // convert comma separated list to array
foreach ($name_list as $index => $field) // remove extraneous spaces
$name_list[$index] = trim($field);
foreach ($name_list as $index => $field_name)
$adi_contact_is_checkbox[$field_name] = TRUE;
}
if ($debug)
adi_contact_debug(__FUNCTION__,$atts,$name_list);
}
function adi_contact_error($atts) {
// insert an extra <span>item</span> at the beginning of $zem_contact_error array - to masquerade as a introductory message (will be output as the first <li>)
global $zem_contact_error;
extract(lAtts(array(
'preamble' => '', // text to precede error messages
), $atts));
if ($preamble && $zem_contact_error)
array_unshift($zem_contact_error,'<span>'.htmlentities($preamble).'</span');
}
function adi_contact_sed_is_base64_encoded($text) {
$text = strtr($text, array("\n"=>'', "\r"=>'', "\t"=>'', ' '=>''));
$b64_all = 'A-Za-z0-9+/';
$b64_mini = 'AEIMQUYcgkosw048';
$b64_micro = 'AQgw';
$b64_pat = "@
^ # Start of text followed by...
(?: [$b64_all]{4})* # Any number of groups of 4 chars from all chars.
(?: # ...optionally (see ? at end of this subpattern) followed by...
[$b64_all]{2} [$b64_mini] = # Any two chars from all, then one from mini, then '='
| # -- or --
[$b64_all] [$b64_micro] == # one from all, then one from micro then two '='s.
)? # ...followed by...
\$ # End of text
@x";
$b64 = (bool)preg_match($b64_pat, $text);
return $b64;
}
function adi_contact_validate($atts) {
// tag to validate form field contents
global $zem_contact_form,$zem_contact_submit,$zem_contact_error,$zem_contact_values,$adi_contact_js_added;
extract(lAtts(array(
'names' => '', // list of form fields we're interested in
'type' => '', // 'alpha', 'alphanum', 'integer', 'numeric', 'equal', 'base64' etc
'values' => '', // list of acceptable values
'allow_whitespace' => '0', // allow spaces as well as 'type'
'pattern' => '', // custom pattern used with type="custom'
'message' => '', // alternative error message (with [NAMES])
'debug' => '0'
), $atts));
$return = adi_contact_insert_javascript();
// tidy up supplied attributes
$name_list = explode(',',$names); // convert comma separated list to array
foreach ($name_list as $index => $field) // remove extraneous spaces
$name_list[$index] = trim($field);
$invalids = array();
$valid_values = array();
if ($zem_contact_submit) { // form has been submitted
$ok = TRUE; // benefit of the doubt
if (isset($zem_contact_form))
if ($type == 'equal') {
$undefined_field_value = 'thechancesofsomeonetypingthisinisextremelyremote...bim';
$ok = (count($name_list) > 1); // must be at least two fields supplied
if ($ok) {
// remember first field value
if (array_key_exists($name_list[0], $zem_contact_values))
$first_field_value = $zem_contact_values[$name_list[0]];
else
$first_field_value = $undefined_field_value;
// compare all field values with the first
foreach ($name_list as $index => $field) {
if (array_key_exists($field, $zem_contact_values))
$this_field_value = $zem_contact_values[$field];
else
$this_field_value = $undefined_field_value;
$ok &= strcmp($this_field_value,$first_field_value) == 0;
}
if (!$ok) { // add to list of invalids ??? TEST THIS - ORIGINALLY BODGIED UP IN OBI
$invalids[] = $name_list[0]; // first field
$invalids[] = $field; // this field
}
}
}
else if ($values != '') {
$type = '';
$valid_values = explode(',',$values);
foreach ($name_list as $field) {
if (array_key_exists($field, $zem_contact_values))
$this_field_value = $zem_contact_values[$field];
else
$this_field_value = '';
$ok = (array_search($this_field_value,$valid_values) !== FALSE);
if (!$ok) { // add to list of invalids
$invalids[] = $field; // this field
}
}
}
else
foreach ($name_list as $index => $field) {
if (array_key_exists($field, $zem_contact_values)) {
$field_value = $zem_contact_values[$field];
if ($allow_whitespace)
$field_value = preg_replace('/[\s]/','', $field_value);
switch ($type) {
case 'alpha':
$ok &= ctype_alpha($field_value);
break;
case 'alphanum':
$ok &= ctype_alnum($field_value);
break;
case 'integer':
$ok &= ctype_digit($field_value);
break;
case 'numeric':
$ok &= is_numeric($field_value);
break;
case 'phone':
$ok &= preg_match("/^[0-9x\ \+\-\(\)\.,]*$/i",$field_value);
break;
case 'url':
$ok &= preg_match("/^(?:http:\/\/)?(?:[\w-]+\.)+[a-z]{2,6}$/i",$field_value);
break;
case 'base64':
$ok &= adi_contact_sed_is_base64_encoded($field_value);
break;
case 'custom':
if ($pattern)
$ok &= preg_match($pattern,$field_value);
break;
default:
break;
}
if (!$ok)
$invalids[] = $field; // add to list of invalids
}
}
if (!$ok) {
$label_list = adi_contact_get_labels($invalids); // create list of labels
switch ($type) {
case 'alpha':
adi_contact_message("Non-alphabetical characters found in “<strong>$label_list</strong>”",$message);
break;
case 'alphanum':
adi_contact_message("Non-alphanumeric characters found in “<strong>$label_list</strong>”",$message);
break;
case 'integer':
adi_contact_message("Non-digits found in “<strong>$label_list</strong>”",$message);
break;
case 'numeric':
adi_contact_message("Non-numeric characters found in “<strong>$label_list</strong>”",$message);
break;
case 'phone':
adi_contact_message("Only 0-9 + - . , () xX allowed in “<strong>$label_list</strong>”",$message);
break;
case 'url':
adi_contact_message("Invalid URL in “<strong>$label_list</strong>”",$message);
break;
case 'base64':
adi_contact_message("Invalid Base64 encoded characters found in “<strong>$label_list</strong>”",$message);
break;
case 'custom':
adi_contact_message("Invalid data in “<strong>$label_list</strong>”",$message);
break;
case 'equal':
adi_contact_message("“<strong>$label_list</strong>” are not the same",$message);
break;
default:
adi_contact_message("Data in “<strong>$label_list</strong>” is not recognised",$message); //??? IMPROVE THIS
break;
}
$invalid_list = adi_contact_literal(join(',',$invalids));
$return .= '<script type="text/javascript">addErrorClass('.$invalid_list.',"'.__FUNCTION__.'")</script>';
}
if ($debug) {
adi_contact_debug(__FUNCTION__,$atts,$name_list);
if ($valid_values) {
echo 'VALID VALUES:<br/>';
dmp($valid_values);
}
echo 'INVALID LIST:<br/>';
dmp($invalids);
}
}
return $return;
}
function adi_contact_combo($atts) {
// tag to check for specified field combinations
global $zem_contact_form,$zem_contact_submit,$zem_contact_labels,$zem_contact_values,$zem_contact_error,$label_list,$number,$require_label_list,$adi_contact_labels,$adi_contact_js_added,$adi_contact_is_checkbox;
extract(lAtts(array(
'names' => '', // list of form fields we're interested in
'require' => '', // list of form fields required
'restrict' => '', // 'min', 'max' or 'equal'
'number' => '0', // number of fields required
'value' => '', // pertinent value to trigger a 'require'
'case_sensitive' => '0', // when checking value
'message' => '', // alternative error message (with [NAMES],[NUMBER],[REQUIRE])
'required' => '0', // forces restrict="equal" number="1", e.g. for custom error messages or checkboxes that must be ticked
'checkbox' => '0', // indicates that fields are checkboxes (i.e. check for 'Yes' or 'No' - in the appropriate language) - DEPRECATED
'debug' => '0'
), $atts));
$return = adi_contact_insert_javascript();
// just in case adi_contact_checkboxes hasn't been called
if (!isset($adi_contact_is_checkbox))
$adi_contact_is_checkbox = array();
// tidy up supplied attributes
$name_list = explode(',',$names); // convert comma separated list to array
foreach ($name_list as $index => $field) // process fields of interest
$name_list[$index] = trim($field); // remove extraneous spaces
$label_list = adi_contact_get_labels($name_list); // put field list back together tidily spaced out: field1, field2, field3
$require_list = explode(',',$require); // convert comma separated list to array
foreach ($require_list as $index => $field) // remove extraneous spaces
$require_list[$index] = trim($field);
$require_label_list = adi_contact_get_labels($require_list); // put field list back together tidily spaced out: field1, field2, field3
$restrict = strtolower($restrict);
if ($required) { // set other attribute values
$restrict = 'equal';
$number = '1';
}
if ($checkbox) // old checkbox attribute is no longer used, so force the issue
echo "<b>adi_contact_combo tag error:</b> The checkbox attribute is no longer valid (use adi_contact_checkboxes tag instead).";
// initial settings
$field_count = 0;
$restrict_error = FALSE;
$require_error = FALSE;
$switch_name_require_list = FALSE;
$missing_required = array();
if ($zem_contact_submit) { // form has been submitted
foreach ($name_list as $index => $name) { // go through supplied fields of interest
$flagged = FALSE; // flag will be set if field filled in or it matches supplied value
if (isset($zem_contact_values)) // NULL if all fields empty on submit!
if (array_key_exists($name, $zem_contact_values)) { // see if field has been submitted
if ($value == '') { // anything (or a tick) will do
if (array_key_exists($name,$adi_contact_is_checkbox)) { // it's a checkbox
if (strcasecmp($zem_contact_values[$name],gTxt('yes')) == 0) // it's ticked
$flagged = TRUE;
}
else
if ($zem_contact_values[$name] != '') // field filled in
$flagged = TRUE;
}
else { // specific value required
if ($case_sensitive)
$flagged = ($zem_contact_values[$name] == $value);
else
$flagged = (strtolower($zem_contact_values[$name]) == strtolower($value));
}
}
if ($flagged)
if ($require && $restrict) { // SPECIAL CASE: require list contains "secondary" fields of interest
foreach ($require_list as $index => $required_field) { // see if they're filled
if (array_key_exists($required_field,$adi_contact_is_checkbox)) {
if (strcasecmp($zem_contact_values[$required_field],gTxt('yes')) == 0) // "filled in" if set to 'Yes', so that's OK then
$field_count += 1;
}
else if (array_key_exists($required_field, $zem_contact_values))
$field_count += 1;
}
$switch_name_require_list = TRUE; // will need to change list later for error message
}
else if ($require) { // SIMPLE REQUIREMENT
foreach ($require_list as $index => $required_field) {
if (array_key_exists($required_field,$adi_contact_is_checkbox)) {
if (strcasecmp($zem_contact_values[$required_field],gTxt('yes')) == 0) // "filled in" if set to 'Yes', so that's OK then
continue;
}
else if (array_key_exists($required_field, $zem_contact_values))
continue;
// not filled in then!
$require_error = TRUE;
$missing_required[] = $required_field; // list the missing
}
}
else if ($restrict) { // SIMPLE RESTRICTION
$field_count += 1;
}
}
// SPECIAL CASE: require list contains "secondary" fields of interest
if ($require && $restrict)
if ($switch_name_require_list) { // "secondary" fields of interest have come into play
$name_list = $require_list;
$label_list = adi_contact_get_labels($require_list);
}
else // forget about restriction, coz it don't apply i.e. no flags raised
$restrict = '';
// 'restrict' mode error messages
switch ($restrict) {
case 'min':
if ($field_count < $number) {
$restrict_error = TRUE;
adi_contact_message("At least $number of “<strong>$label_list</strong>” required.",$message);
}
break;
case 'max':
if ($field_count > $number) {
$restrict_error = TRUE;
adi_contact_message("Maximum $number of “<strong>$label_list</strong>” required.",$message);
}
break;
case 'equal':
if ($field_count != $number) {
$restrict_error = TRUE;
adi_contact_message("Exactly $number of “<strong>$label_list</strong>” required.",$message);
}
break;
default:
break;
}
if ($debug) {
adi_contact_debug(__FUNCTION__,$atts,$name_list);
echo "LABELS OF INTEREST:<br/>";
dmp($label_list);
if ($restrict) {
if ($number) {
echo "VALID FIELD COUNT:<br/>";
echo '<pre>'.$field_count.'</pre>';
}
}
if ($require) {
echo "REQUIRED FIELDS:<br/>";
dmp($require_list);
if (isset($missing_required))
{
echo "MISSING REQUIRED FIELDS:<br/>";
dmp($missing_required);
}
}
}
// finally some javascript action
if ($restrict_error) {
$missing_list = adi_contact_literal(join(',',$name_list)); // don't want spaces in list
if ($debug)
echo "MISSING LIST:<br/><pre>$missing_list</pre>";
$return .= '<script type="text/javascript">addErrorClass('.$missing_list.',"'.__FUNCTION__.'")</script>';
}
else if ($require_error) { // 'require' error messages
adi_contact_message("If “<strong>$label_list</strong>” supplied then “<strong>$require_label_list</strong>” required.",$message);
$missing_list = adi_contact_literal(join(',',$missing_required)); // don't want spaces in list
$return .= '<script type="text/javascript">addErrorClass('.$missing_list.',"'.__FUNCTION__.'")</script>';
}
}
return $return;
}
function adi_contact_headers($atts) {
// tag for extra headers, e.g. CC or BCC
// adi_contact_deliver() does the actual doing
global $adi_contact_headers;
extract(lAtts(array(
'cc' => '', // CC email address
'bcc' => '', // BCC email address
), $atts));
if (!isset($adi_contact_headers))
$adi_contact_headers = array();
$adi_contact_headers['cc'] = $cc;
$adi_contact_headers['bcc'] = $bcc;
return;
}
function adi_contact_body($atts) {
// for appending extra information onto end of email
// accepts string (internal function use) OR array (tag mode)
// adi_contact_deliver() does the actual doing
global $adi_contact_body,$zem_contact_submit;
if (!isset($adi_contact_body))
$adi_contact_body = array(''); // start a new line otherwise extra body tacked onto end of last field/value line
if (is_array($atts))
extract(lAtts(array(
'line' => '', // a line to add to the body
), $atts));
else
$line =$atts;
if($zem_contact_submit) // don't add unless form submitted (to avoid combining lines from separate forms on same page)
$adi_contact_body[] = $line;
return;
}
function adi_contact_deliver($event,$step,&$payload) {
// modify the email $payload (event="zemcontact.deliver",step="send/copysender")
global $adi_contact_headers,$adi_contact_body;
// extra headers - provided by adi_contact_headers()
if ($adi_contact_headers['cc'] != '')
$payload['headers']['Cc'] = $adi_contact_headers['cc'];
if ($adi_contact_headers['bcc'] != '')
$payload['headers']['Bcc'] = $adi_contact_headers['bcc'];
if ($step !== 'copysender') { // making big assumption that copy of email going to form submitter shouldn't contain extra body (e.g. MailChimp diags)
// extra body (message text already formed at this point, so simply adding extra lines to email)
if ($adi_contact_body) {
foreach ($adi_contact_body as $line)
$payload['body'] .= $line."\n";
}
}
return 'zemcontact.send'; // I command thee to send thy email
}
function adi_contact_db($atts,$thing='') {
// database mechanism enclosing tag
global $zem_contact_submit,$zem_contact_error,$adi_contact_db_table,$production_status;
// CREATE TABLE contacts (`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,`timestamp` datetime NOT NULL default '0000-00-00 00:00:00',`fullname` VARCHAR(255) NOT NULL,`email` VARCHAR(255) NOT NULL,`mailout` VARCHAR(32) NOT NULL default '',`general` VARCHAR(16) NOT NULL default '',`sales` VARCHAR(16) NOT NULL default '',`technical` VARCHAR(16) NOT NULL default '',`feedback` VARCHAR(16) NOT NULL default '',`region` VARCHAR(32) NOT NULL default '');
extract(lAtts(array(
'table' => '', // the database table
'form' => '', // the adi_contact_db form
// 'mode' => 'insert', // insert or update
), $atts));
$diags = array();
$adi_contact_db_data = array(); // array indexed by db_field name: ['contact_field'] => contact field name, ['default'] => default value (if supplied)
$adi_contact_db_table = $table;
// validate supplied table
if (($production_status === 'debug') && ($table != '')) {
$rs = safe_query("SHOW TABLES LIKE '".safe_pfx($table)."'");
$a = nextRow($rs);
if ($a)
$diags[] = "table - '$table' found in database";
else
$diags[] = "table - '$table' NOT FOUND in database";
}
if ($table == '')
$diags[] = 'table not set';
// form submit status
if ($zem_contact_submit)
$diags[] = 'form - submitted';
else
$diags[] = 'form - not submitted';
if ($zem_contact_error)
$diags[] = 'form - submit errors';
adi_contact_trace($diags);
// do the doing
$form = ($form) ? fetch_form($form) : $thing; // only get tag errors about invalid DB fields if using form (i.e. not enclosing tag)?
return parse($form);
}
function adi_contact_db_data($atts) {
// contact form/database field mapping tag - updates $adi_contact_db_data
global $zem_contact_submit,$zem_contact_labels,$production_status,$adi_contact_db_table,$adi_contact_db_data;
extract(lAtts(array(
'db_field' => '', // the database field (required)
'contact_field' => '', // the contact form input field (default same name as db_field)
'default' => '', // optional default value
'timestamp' => '' // database field is a timestamp (datetime, date or timestamp)
), $atts));
$diags = array();
// contact field same as database field?
if (($contact_field == '') && !$timestamp)
$contact_field = $db_field;
// validate contact form field
if ($zem_contact_submit) {
if (isset($zem_contact_labels[$contact_field]))
$diags[] = "field - '$contact_field' found in contact form";
else
$diags[] = "field - '$contact_field' NOT FOUND OR BLANK in contact form";
}
// check required attributes
if ($db_field == '')
$diags[] = "db_field - ATTRIBUTE MISSING";
else {
// db column -> contact field mapping
$adi_contact_db_data[$db_field]['contact_field'] = $contact_field;
// default values
if (isset($atts['default'])) // default value supplied
$adi_contact_db_data[$db_field]['default'] = $default;
if (isset($atts['timestamp'])) { // set timestamp according to MySQL formats: DATETIME, DATE, TIMESTAMP
$timestamp = strtolower($timestamp);
switch ($timestamp) {
case 'date': // yyyy-mm-dd
$adi_contact_db_data[$db_field]['default'] = date("Y-m-d",time()+tz_offset());
break;
case 'timestamp': // UTC yyyy-mm-dd hh:mm:ss
$adi_contact_db_data[$db_field]['default'] = gmstrftime("%Y-%m-%d %H:%M:%S");
break;
default: // default to "datetime" - yyyy-mm-dd hh:mm:ss
$adi_contact_db_data[$db_field]['default'] = date("Y-m-d H:i:s",time()+tz_offset());
break;
}
}
$diags[] = "map - '".$adi_contact_db_data[$db_field]['contact_field']."' -> db '$db_field'".(isset($adi_contact_db_data[$db_field]['default']) ? ' (default = "'.$adi_contact_db_data[$db_field]['default'].'")' : '');
// validate database column
if ($production_status === 'debug') {
if (isset($adi_contact_db_table)) {
$rs = safe_query('SHOW COLUMNS FROM `'.safe_pfx($adi_contact_db_table)."` WHERE FIELD = '$db_field'");
if ($rs !== FALSE) { // table exists
$a = nextRow($rs);
if ($a)
$diags[] = "column - '$db_field' found in table '$adi_contact_db_table'";
else
$diags[] = "column - '$db_field' NOT FOUND in table '$adi_contact_db_table'";
}
}
}
}
adi_contact_trace($diags);
}
function adi_contact_db_update() {
// update database - called back during form submit
global $zem_contact_values,$adi_contact_db_table,$adi_contact_db_data;
$diags = array();
if (!isset($adi_contact_db_table)) return; // adi_contact_db hasn't been called so bail out
// check spammage
$evaluation =& get_zemcontact_evaluator();
if ($evaluation->get_zemcontact_status()) {
$diags[] = "spam evaluation - DIRTY";
return;
}
// build set
$set = array();
if ($adi_contact_db_data) {
foreach ($adi_contact_db_data as $db_field => $map) {
if (isset($zem_contact_values[$map['contact_field']]))
$set[]= "`$db_field`='".doSlash($zem_contact_values[$map['contact_field']])."'"; // use value from contact form
else if (isset($map['default']))
$set[]= "`$db_field`='".doSlash($map['default'])."'"; // use supplied default value
// else
// let the database fill in its own default
}
}
else
$diags[] = "map - EMPTY";
// update the database
if ($set && $adi_contact_db_table) {
$diags[] = 'set - '.implode(', ',$set);
if (!safe_insert($adi_contact_db_table,implode(',',$set)))
$diags[] = "safe_insert: FAIL";
}
adi_contact_trace($diags);
}
function adi_contact_mailchimp($atts,$thing=NULL) {
// MailChimp subscription container tag
global $zem_contact_submit,$zem_contact_error,$adi_contact_mailchimp_data,$adi_contact_mailchimp_info;
extract(lAtts(array(
'api_key' => '', // MailChimp API key (required)
'list_id' => '', // MailChimp list ID (required)
'status' => 'pending', // 'subscribed', 'unsubscribed', 'pending', 'cleaned'
'opt_in_field' => 'mailing_list', // contact form field name, used to trigger MailChimp action
'opt_in_value' => gTxt('yes'), // contact form field value, used to trigger MailChimp action (defaults to checkbox value)
'form' => '', // adi_contact_mailchimp form
), $atts));
// status values
// 'pending' - confirmation email will be sent to user
// 'subscribed' - subscribed immediately, no confirmation sent/required
$diags = array();
$default_form = '<txp:adi_contact_mailchimp_email />';
// array containing data required for MailChimp connect (api_key, list_id, email, status, merge_fields) as well as opt-in info
$adi_contact_mailchimp_info = array('api_key'=>$api_key,'list_id'=>$list_id,'status'=>$status,'opt_in_field'=>$opt_in_field,'opt_in_value'=>$opt_in_value);
// array indexed by MailChimp field (email,FNAME,LNAME): ['contact_field'] => contact field name, ['default'] => default value (DEFAULT VALUES NOT CURRENTLY USED)
$adi_contact_mailchimp_data = array();
// check attributes
if ($api_key == '')
$diags[] = 'API key missing';
if ($list_id == '')
$diags[] = 'list ID missing';
if ($status == '')
$diags[] = 'status missing';
// form submit status
if ($zem_contact_submit)
$diags[] = 'form - submitted';
else
$diags[] = 'form - not submitted';
if ($zem_contact_error)
$diags[] = 'form - submit errors';
adi_contact_trace($diags);
// form guide
$form = ($form) ? fetch_form($form) : ($thing === NULL ? $default_form : $thing);
return parse($form);
}
function adi_contact_mailchimp_data($atts) {
// contact form/mailchimp field mapping tag - updates $adi_contact_mailchimp_data
global $zem_contact_submit,$zem_contact_labels,$adi_contact_mailchimp_data;
extract(lAtts(array(
'mailchimp_field' => '', // MailChimp field, e.g. FNAME, LNAME (required)
'contact_field' => '', // the contact form input field (required)
), $atts));
$diags = array();
// validate contact form field
if ($zem_contact_submit) {
if (isset($zem_contact_labels[$contact_field]))
$diags[] = "field - '$contact_field' found in contact form";
else
$diags[] = "field - '$contact_field' NOT FOUND OR EMPTY in contact form";
}
// check required attributes
if ($mailchimp_field == '')
$diags[] = "mailchimp_field - ATTRIBUTE MISSING";
else if ($contact_field == '')
$diags[] = "contact_field - ATTRIBUTE MISSING";
else {
// mailchimp column -> contact field mapping
$adi_contact_mailchimp_data[$mailchimp_field]['contact_field'] = $contact_field;
// default values (NOT CURRENTLY USED)
if (isset($atts['default'])) // default value supplied
$adi_contact_mailchimp_data[$mailchimp_field]['default'] = $default;
$diags[] = "map - '".$adi_contact_mailchimp_data[$mailchimp_field]['contact_field']."' -> mailchimp '$mailchimp_field'".(isset($adi_contact_mailchimp_data[$mailchimp_field]['default']) ? ' (default = "'.$adi_contact_mailchimp_data[$mailchimp_field]['default'].'")' : '');
}
adi_contact_trace($diags);
}
function adi_contact_mailchimp_email($atts) {
// mailchimp email field mapping tag
extract(lAtts(array(
'contact_field' => 'email', // the contact form input field
), $atts));
adi_contact_mailchimp_data(array('mailchimp_field'=>'email', 'contact_field'=>$contact_field));
}
function adi_contact_mailchimp_update() {
// update MailChimp list - called back during form submit
global $zem_contact_values,$adi_contact_mailchimp_info,$adi_contact_mailchimp_data;
$diags = array();
if (!isset($adi_contact_mailchimp_info)) return; // adi_contact_mailchimp hasn't been called so bail out
// check spammage
$evaluation =& get_zemcontact_evaluator();
if ($evaluation->get_zemcontact_status()) {
adi_contact_trace("FAIL - spam evaluation, DIRTY");
return;
}
// check opt-in
$opt_in_field = $adi_contact_mailchimp_info['opt_in_field'];
$opt_in_value = $adi_contact_mailchimp_info['opt_in_value'];
if ($opt_in_field != '') { // if opt_in_field attribute set, check it exists in contact form
if (!isset($zem_contact_values[$opt_in_field])) {
adi_contact_trace("FAIL - opt-in field '$opt_in_field' NOT FOUND");
return;
}
if (strcasecmp($zem_contact_values[$opt_in_field],$opt_in_value) != 0) { // opt_in_field value matches opt_in_value attribute?
$opt_in_field_value = $zem_contact_values[$opt_in_field];
adi_contact_trace("DISABLED - opt-in field '$opt_in_field' value '$opt_in_field_value', doesn't match opt_in_value '$opt_in_value'");
return;
}
}
// build up the chimp
$adi_contact_mailchimp_info['email'] = '';
$adi_contact_mailchimp_info['merge_fields'] = array();
if ($adi_contact_mailchimp_data) {
foreach ($adi_contact_mailchimp_data as $mailchimp_field => $map) {
if ($mailchimp_field == 'email') { // the email
if (isset($zem_contact_values[$map['contact_field']]))
$adi_contact_mailchimp_info['email'] = $zem_contact_values[$map['contact_field']]; // use email from contact form;
}
else { // merge fields
$value = (isset($zem_contact_values[$map['contact_field']]) ? $zem_contact_values[$map['contact_field']] : '');
$adi_contact_mailchimp_info['merge_fields'][$mailchimp_field] = $value;
}
}
}
else
$diags[] = "map - EMPTY";
// update the chimp
if ($adi_contact_mailchimp_info['email']) { // need an email address as bare minimum
$diag_info = $adi_contact_mailchimp_info;
unset($diag_info['merge_fields']);
$diags[] = 'info - '.implode(', ',$diag_info);
$diags[] = 'merge_fields - '.implode(', ',$adi_contact_mailchimp_info['merge_fields']);
$mc_result = adi_contact_mailchimp_connect($adi_contact_mailchimp_info);
$email_msg = "MailChimp:\n";
// see adi_contact_mailchimp_connect for returns
if ($mc_result === FALSE) {
$reason = 'unable to connect';
$diags[] = 'FAIL - '.$reason;
$email_msg .= "- subscribe failed ($reason)\n";
}
if ($mc_result === NULL) {
$reason = 'software error';
$diags[] = 'FAIL - '.$reason;
$email_msg .= "- subscribe failed ($reason)\n";
}
if (is_array($mc_result))
if (isset($mc_result['title'])) {
$reason = 'status '.$mc_result['status'].', '.$mc_result['title'];
$diags[] = 'FAIL - '.$reason;
$email_msg .= "- subscribe failed ($reason)\n";
$diags[] = 'FAIL - '.trim($mc_result['detail']);
$email_msg .= '- '.trim($mc_result['detail'])."\n";
foreach ($mc_result['errors'] as $error) {
$email_msg .= "- ";
$error_msg = '';
foreach ($error as $index => $value)
$error_msg .= "$index: $value; ";
$diags[] = "ERROR - $error_msg";
$email_msg .= "$error_msg\n";
}
}
else {
$reason = 'status '.$mc_result['status'];
$diags[] = 'success - '.$mc_result['status'];
$email_msg .= "- subscribed ($reason)\n";
}
// maybe attributed option to include mc diags in email or not?
adi_contact_body("\n"); // two blank lines
adi_contact_body($email_msg);
}
else
$diags[] = 'FAIL - no contact field to MailChimp "email" field mapping';
adi_contact_trace($diags);
}
function adi_contact_mailchimp_connect($mailchimp_info) {
// connect to MailChimp & update mailing list
// - much code taken from stackoverflow.com/questions/30481979/adding-subscribers-to-a-list-using-mailchimps-api-v3
//
// - success
// - return array('status')
// status = 'pending' or 'subscribed' etc
// - failure
// - return array('status','title')
// status = 400, 401, 404 etc
// title = informative summary
// 400 - Member Exists
// 401 - API Key Invalid
// 404 - Resource Not Found (dodgy URL)
// detail = further explanation
// errors = optional array of field/error message
// - return FALSE
// connection failure (e.g. timeout)
// - return NULL
// software problem (e.g. curl functions not available)
extract($mailchimp_info); // i.e. $api_key, $list_id, $status, $email, $merge_fields[...]
list(,$datacentre) = explode('-',$api_key); // datacentre is the "us" bit at end of API key
$url = 'https://'.$datacentre.'.api.mailchimp.com/3.0/lists/'.$list_id.'/members/';
$data = array(
'apikey' => $api_key,
'email_address' => $email,
'status' => $status,
'merge_fields' => $merge_fields
);
// there seems to be an intermittant issue with MailChimp API v3.0
// scenario: when merge_fields is an empty array (i.e. there are no adi_contact_mailchimp_data mappings other than "email")
// symptoms/diags:
// subscribe failed (status 400, Invalid Resource)
// "The resource submitted could not be validated. For field-specific details, see the 'errors' array."
// field: merge_fields; message: Schema describes object, array found instead;
// intermittant: may only occur on certain servers (e.g. local & staging OK, live COL fails), possibly a PHP version/server config thing
// discussed here: https://github.com/drewm/mailchimp-api/issues/54#issuecomment-171263356
// fix: omit 'merge_fields' item from $data if empty
if (empty($merge_fields)) unset($data['merge_fields']);
// echo 'adi_contact_mailchimp_connect:';
// dmp($data);
$json_data = json_encode($data);
// curly wurly check
//??? IMPLEMENT ALT CONNECTION METHOD using file_get_contents() - see smd_mailchimp
if (!function_exists('curl_init') || !function_exists('curl_setopt')) {
adi_contact_trace("FAIL - curl_init(), curl_setopt() not available");
return NULL;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_USERPWD, "user:" . $api_key);
curl_setopt($ch, CURLOPT_USERAGENT, 'PHP-MCAPI/2.0');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data);
$result = curl_exec($ch); // FALSE = connection error, JSON data if contact made with MAilChimp
$arr = json_decode($result,TRUE); // json_decode into array
// echo __FUNCTION__.' response/status:';
// dmp($arr);
// not using extract() coz there's a shed load of variously named indexes in $arr (especially in a successful subscribe)
$status = '';
if (isset($arr['status'])) $status = $arr['status'];
$title = '';
if (isset($arr['title'])) $title = $arr['title'];
$detail = '';
if (isset($arr['detail'])) $detail = $arr['detail'];
$errors = array();
if (isset($arr['errors'])) $errors = $arr['errors'];
// echo __FUNCTION__.' errors:';
// dmp($errors);
// final analysis of results
if ($result === FALSE) // connection error, return FALSE
return $result;
else
if (is_int($status)) // status is a number, return descriptive error
return array('status'=>$status,'title'=>$title,'detail'=>$detail,'errors'=>$errors);
else // return status (string)
return array('status'=>$status);
}
function adi_contact_debug($function='',$atts=array(),$name_list=array(),$diags=array()) {
// debug function for debug="1" attributes or can be used as a standalone tag (<txp:adi_contact_debug />) to output general diags
global $zem_contact_form,$zem_contact_submit,$zem_contact_labels,$zem_contact_values,$zem_contact_error,$label_list,$number,$require_label_list,$adi_contact_labels,$prefs,$adi_contact_is_checkbox,$adi_contact_zcr_4500;
if ($function) {
echo '********** '.strtoupper($function).' **********'.br.br;
if ($atts) {
echo 'SUPPLIED ATTRIBUTES:'.br;
dmp($atts);
}
if ($name_list) {
echo 'FIELDS OF INTEREST:'.br;
dmp($name_list);
}
if ($diags) {
echo 'DIAGNOSTICS:'.br;
echo tag(implode(n,$diags),'pre');
}
}
else {
echo '********** ADI_CONTACT DEBUG **********'.br.br;
echo 'VERSIONS:'.br;
echo '<pre>';
$plugin_list = array('adi_contact','zem_contact_reborn','pap_contact_cleaner');
foreach ($plugin_list as $index => $plugin) {
$row = safe_row('version,status,load_order','txp_plugin',"name='$plugin'");
if ($row) {
extract($row);
echo "$plugin: $version".($status?' (active)':' (not active)').", load order $load_order".br;
}
if (!$row || !$status) {
$func_list = array('adi_contact_combo','zem_contact','pap_zemcontact_form');
if (function_exists($func_list[$index]))
echo $plugin.': in plugin cache, load order 0'.br;
else
echo $plugin.': not installed'.br;
}
}
echo 'TXP: '.$prefs['version'].br;
echo 'PHP: '.phpversion().br;
echo 'MySQL: '.mysql_get_server_info().br;
echo 'Language: yes = '.gTxt('yes').br;
echo 'TXP date/time = '.date("Y-m-d H:i:s",time()+tz_offset()); // this is actual (TXP adjusted) local time (incl DST) regardless of server timezone
echo '</pre>';
echo 'ZEM CONTACT FORM:'.br;
echo '<pre>';
if (isset($_POST['zem_contact_form_id']))
echo 'id = '.$_POST['zem_contact_form_id'].br;
if (isset($_POST['zem_contact_nonce']))
echo 'nonce = '.$_POST['zem_contact_nonce'].br;
echo '</pre>';
echo 'SUBMITTED FORM LABEL/VALUE ($zem_contact_form):'.br;
dmp($zem_contact_form); // array keyed by labels (null if no fields filled in or not submitted)
echo 'SUBMITTED FORM NAME/LABEL ($zem_contact_labels):'.br;
dmp($zem_contact_labels); // array keyed by names (null if no fields filled in or not submitted)
echo 'SUBMITTED FORM NAME/VALUE ($zem_contact_values):'.br;
dmp($zem_contact_values); // array keyed by names (null if no fields filled in or not submitted)
if (isset($zem_contact_error)) {
echo 'ERROR LIST ($zem_contact_error):'.br;
dmp($zem_contact_error);
}
if (isset($adi_contact_labels)) {
echo 'SUPPLIED LABELS FROM adi_contact_labels:'.br;
dmp($adi_contact_labels);
}
echo 'CHECKBOXES ($adi_contact_is_checkbox):'.br;
dmp($adi_contact_is_checkbox);
if ($diags) {
echo 'DIAGNOSTICS:'.br;
echo tag(implode(n,$diags),'pre');
}
}
}
function adi_contact_trace($diags) {
// captain's log
global $trace;
if (is_string($diags))
$diags = str_split($diags,9999); // convert string to array
// get meaningful calling function
if (is_callable('debug_backtrace')) {
$backtrace = debug_backtrace();
$function = isset($backtrace[1]['function']) ? $backtrace[1]['function'] : __FUNCTION__;
}
else
$function = __FUNCTION__;
if ($function == "include") $function = __FUNCTION__; // plugin cache
if ($function == "eval") $function = __FUNCTION__; // plugin installed
if (is_object($trace)) // TXP 4.6
foreach ($diags as $diag)
$trace->log('['.$function.': '.$diag.']');
else
foreach ($diags as $diag)
trace_add('['.$function.': '.$diag.']');
}