diff --git a/assets/templates/civicrm.shortcode.php b/assets/templates/civicrm.shortcode.php index 02f45e08df0447d01c21206d6d0ccf2f03bb3bcd..c590390b01a72437875c92bd261993dddbe6abad 100644 --- a/assets/templates/civicrm.shortcode.php +++ b/assets/templates/civicrm.shortcode.php @@ -2,7 +2,7 @@ /* +--------------------------------------------------------------------+ - | CiviCRM version 5 | + | CiviCRM version 4.7 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2018 | +--------------------------------------------------------------------+ diff --git a/civicrm.php b/civicrm.php index 9f5d00f363fd3683e3a02892cee2d476929b0ab0..05a481b92ff60a2bb55465ba3a355f3ce0133679 100644 --- a/civicrm.php +++ b/civicrm.php @@ -2,11 +2,10 @@ /* Plugin Name: CiviCRM Description: CiviCRM - Growing and Sustaining Relationships -Version: 5.3.2 +Version: 5.4.0 Author: CiviCRM LLC Author URI: https://civicrm.org/ Plugin URI: https://wiki.civicrm.org/confluence/display/CRMDOC/Installing+CiviCRM+for+WordPress -GitLab Plugin URI: https://develop.tadpole.cc/plugins/civicrm License: AGPL3 Text Domain: civicrm Domain Path: /languages @@ -15,7 +14,7 @@ Domain Path: /languages /* +--------------------------------------------------------------------+ - | CiviCRM version 5 | + | CiviCRM version 4.7 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2018 | +--------------------------------------------------------------------+ @@ -71,7 +70,7 @@ if ( ! defined( 'ABSPATH' ) ) exit; // set version here: when it changes, will force JS to reload -define( 'CIVICRM_PLUGIN_VERSION', '5' ); +define( 'CIVICRM_PLUGIN_VERSION', '4.7' ); // store reference to this file if (!defined('CIVICRM_PLUGIN_FILE')) { @@ -309,9 +308,7 @@ class CiviCRM_For_WordPress { if ( $this->civicrm_in_wordpress() ) { // this is required for AJAX calls in WordPress admin $_GET['noheader'] = TRUE; - } - - if ( !CIVICRM_INSTALLED && !$this->civicrm_in_wordpress() ) { + } else { $_GET['civicrm_install_type'] = 'wordpress'; } diff --git a/civicrm/CONTRIBUTORS.txt b/civicrm/CONTRIBUTORS.txt index 60a8f1ac4e325eb1ceb0d511acf37563bf79e95d..6af5bf8853ca5a881aa645b290a1a82f0f115456 100644 --- a/civicrm/CONTRIBUTORS.txt +++ b/civicrm/CONTRIBUTORS.txt @@ -9,17 +9,28 @@ CiviCRM - Coleman Watts, Tim Otten AGH Strategies - Alice Frumin, Andrew Hunt, Eli Lisseck Agileware - Alok Patel, Francis Whittle, Justin Freeman Andrew Thompson +applicado Australian Greens - Seamus Lee -CiviDesk - Yashodha Chaku -CompuCorp - Michael Devery, Mukesh Ram, Omar Abu Hussein, René Olivo, Vinu - Varshith Sekar -Coop SymbioTIC - Samuel Vanhove +Bastien Ho +Blackfly Solutions - Alan Dixon +Caltha - Tomasz Pietrzkowski +CEDC - Laryn Kragt Bakker +Chris Burgess +CiviCoop - Jaap Jansma +CiviDesk - Sunil Pawar, Yashodha Chaku +CompuCorp - Camilo Rodriguez, Davi Alexandre, Debarshi Bhaumik, Michael Devery, + Mukesh Ram, Omar Abu Hussein, René Olivo, Vinu Varshith Sekar +Coop SymbioTIC - Mathieu Lutfy, Samuel Vanhove Davis Media Access - Darrick Servis +Electronic Frontier Foundation - Mark Burdett Fuzion - Jitendra Purohit Ginkgo Street Labs - Frank Gómez +Hossein Amin JMA Consulting - Monish Deb +Johan Vervloet John Kingsnorth Joinery - Allen Shaw +Kanzu Code - Carl Andrew Lema Kompetenzzentrum Technik-Diversity-Chancengleichheit - Niels Heinemann Left Join Labs - Sean Madsen Lighthouse Design and Consulting - Brian Shaughnessy @@ -30,11 +41,17 @@ myDropWizard - David Snopek Naomi Rosenberg Olivier Tétard Oxfam Germany - Thomas Schüttler, Yuliyana Liyana +Pradeep Nayak Progressive Technology Project - Jamie McClelland +Romain Thouvenin +Squiffle Consulting - Aidan Saunders Systopia - Björn Endres Tadpole Collective - Kevin Cristiano Third Sector Design - Michael McAndrew +Tom Bloor Wikimedia Foundation - Eileen McNaughton +Wildsight - Lars Sanders-Green +Will Long ************************************************ Key Contributors and Sponsors for 4.7 diff --git a/civicrm/CRM/ACL/BAO/ACL.php b/civicrm/CRM/ACL/BAO/ACL.php index cd9c25dfeee764dfad9dae2447bcbf84cb6ac632..15221f74565ead4bc8a0177ce0147f34df3c4c6a 100644 --- a/civicrm/CRM/ACL/BAO/ACL.php +++ b/civicrm/CRM/ACL/BAO/ACL.php @@ -877,7 +877,7 @@ SELECT g.* $aclKeys = array_keys($acls); $aclKeys = implode(',', $aclKeys); - $cacheKey = "$tableName-$aclKeys"; + $cacheKey = CRM_Core_BAO_Cache::cleanKey("$tableName-$aclKeys"); $cache = CRM_Utils_Cache::singleton(); $ids = $cache->get($cacheKey); if (!$ids) { diff --git a/civicrm/CRM/Activity/BAO/Activity.php b/civicrm/CRM/Activity/BAO/Activity.php index a0a6b8e4999bb75b41bb62d10ca8f0a5bb9401dc..31c3c253d9328b511a0c761210ddcf5ea3791400 100644 --- a/civicrm/CRM/Activity/BAO/Activity.php +++ b/civicrm/CRM/Activity/BAO/Activity.php @@ -920,8 +920,7 @@ class CRM_Activity_BAO_Activity extends CRM_Activity_DAO_Activity { $config = CRM_Core_Config::singleton(); - $randomNum = md5(uniqid()); - $activityTempTable = "civicrm_temp_activity_details_{$randomNum}"; + $activityTempTable = CRM_Utils_SQL_TempTable::build()->setCategory('actdetail')->getName(); $tableFields = array( 'activity_id' => 'int unsigned', @@ -1012,7 +1011,7 @@ LEFT JOIN civicrm_case_activity ON ( civicrm_case_activity.activity_id = tbl.ac // step 2: Get target and assignee contacts for above activities // create temp table for target contacts - $activityContactTempTable = "civicrm_temp_activity_contact_{$randomNum}"; + $activityContactTempTable = CRM_Utils_SQL_TempTable::build()->setCategory('actcontact')->getName(); $query = "CREATE TEMPORARY TABLE {$activityContactTempTable} ( activity_id int unsigned, contact_id int unsigned, record_type_id varchar(16), contact_name varchar(255), is_deleted int unsigned, counter int unsigned, INDEX index_activity_id( activity_id ) ) @@ -2537,7 +2536,10 @@ AND cl.modified_id = c.id // my case hence we have defined fields as case_* if ($name == 'Activity') { $exportableFields = CRM_Activity_DAO_Activity::export(); - $exportableFields['source_contact_id']['title'] = ts('Source Contact ID'); + $exportableFields['source_contact_id'] = [ + 'title' => ts('Source Contact ID'), + 'type' => CRM_Utils_Type::T_INT, + ]; $exportableFields['source_contact'] = array( 'title' => ts('Source Contact'), 'type' => CRM_Utils_Type::T_STRING, diff --git a/civicrm/CRM/Activity/BAO/Query.php b/civicrm/CRM/Activity/BAO/Query.php index 724a30642a649e3ec93152d0628759fdb83c63da..ddfb01aedd1d7c9bbaac3ff17d9f208e6e55a790 100644 --- a/civicrm/CRM/Activity/BAO/Query.php +++ b/civicrm/CRM/Activity/BAO/Query.php @@ -136,7 +136,13 @@ class CRM_Activity_BAO_Query { if (!empty($query->_returnProperties['source_contact'])) { $query->_select['source_contact'] = 'source_contact.sort_name as source_contact'; $query->_element['source_contact'] = 1; - $query->_tables['source_contact'] = $query->_whereTables['source_contact'] = 1; + $query->_tables['civicrm_activity'] = $query->_tables['source_contact'] = $query->_whereTables['source_contact'] = 1; + } + + if (!empty($query->_returnProperties['source_contact_id'])) { + $query->_select['source_contact_id'] = 'source_contact.id as source_contact_id'; + $query->_element['source_contact_id'] = 1; + $query->_tables['civicrm_activity'] = $query->_tables['source_contact'] = $query->_whereTables['source_contact'] = 1; } if (!empty($query->_returnProperties['activity_result'])) { @@ -348,6 +354,14 @@ class CRM_Activity_BAO_Query { $query->_qill[$grouping][] = ts('Activities which are not Followup Activities'); } break; + + case 'source_contact': + case 'source_contact_id': + $columnName = strstr($name, '_id') ? 'id' : 'sort_name'; + $query->_where[$grouping][] = CRM_Contact_BAO_Query::buildClause("source_contact.{$columnName}", $op, $value, CRM_Utils_Type::typeToString($fields[$name]['type'])); + list($op, $value) = CRM_Contact_BAO_Query::buildQillForFieldValue('CRM_Contact_DAO_Contact', $columnName, $value, $op); + $query->_qill[$grouping][] = ts('%1 %2 %3', array(1 => $fields[$name]['title'], 2 => $op, 3 => $value)); + break; } } @@ -402,9 +416,8 @@ class CRM_Activity_BAO_Query { $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate'); $sourceID = CRM_Utils_Array::key('Activity Source', $activityContacts); $from = " - LEFT JOIN civicrm_activity_contact ac - ON ( ac.activity_id = civicrm_activity_contact.activity_id AND ac.record_type_id = {$sourceID}) - INNER JOIN civicrm_contact source_contact ON (ac.contact_id = source_contact.id)"; + INNER JOIN civicrm_contact source_contact ON + (civicrm_activity_contact.contact_id = source_contact.id) AND civicrm_activity_contact.record_type_id = {$sourceID}"; break; case 'parent_id': diff --git a/civicrm/CRM/Activity/Form/Task.php b/civicrm/CRM/Activity/Form/Task.php index 254579876b015c1af5a4e974d2391cea0d8c6c4d..3a052beec467105460210501ced3edefbe120892 100644 --- a/civicrm/CRM/Activity/Form/Task.php +++ b/civicrm/CRM/Activity/Form/Task.php @@ -32,37 +32,10 @@ */ /** - * Class for activity task actions. + * Class for activity form task actions. + * FIXME: This needs refactoring to properly inherit from CRM_Core_Form_Task and share more functions. */ -class CRM_Activity_Form_Task extends CRM_Core_Form { - - /** - * The task being performed. - * - * @var int - */ - protected $_task; - - /** - * The additional clause that we restrict the search with. - * - * @var string - */ - protected $_componentClause = NULL; - - /** - * The array that holds all the component ids. - * - * @var array - */ - protected $_componentIds; - - /** - * The array that holds all the contact ids. - * - * @var array - */ - public $_contactIds; +class CRM_Activity_Form_Task extends CRM_Core_Form_Task { /** * The array that holds all the member ids. @@ -82,9 +55,8 @@ class CRM_Activity_Form_Task extends CRM_Core_Form { * Common pre-process function. * * @param CRM_Core_Form $form - * @param bool $useTable */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $form->_activityHolderIds = array(); $values = $form->controller->exportValues($form->get('searchFormName')); diff --git a/civicrm/CRM/Admin/Form/Job.php b/civicrm/CRM/Admin/Form/Job.php index 74776dd0d41e5bbb8a2d226cd117418aae7b9032..e319f498ca6790e250e9a614af1a918e6b1ae384 100644 --- a/civicrm/CRM/Admin/Form/Job.php +++ b/civicrm/CRM/Admin/Form/Job.php @@ -180,7 +180,7 @@ class CRM_Admin_Form_Job extends CRM_Admin_Form { */ public function postProcess() { - CRM_Utils_System::flushCache('CRM_Core_DAO_Job'); + CRM_Utils_System::flushCache(); if ($this->_action & CRM_Core_Action::DELETE) { CRM_Core_BAO_Job::del($this->_id); diff --git a/civicrm/CRM/Admin/Form/LocationType.php b/civicrm/CRM/Admin/Form/LocationType.php index 7979db2dabd229a3dbc099393cc19191a930fdc4..b1e1a97763600802ffb783b6b61f2a0a937773fe 100644 --- a/civicrm/CRM/Admin/Form/LocationType.php +++ b/civicrm/CRM/Admin/Form/LocationType.php @@ -86,7 +86,7 @@ class CRM_Admin_Form_LocationType extends CRM_Admin_Form { * Process the form submission. */ public function postProcess() { - CRM_Utils_System::flushCache('CRM_Core_DAO_LocationType'); + CRM_Utils_System::flushCache(); if ($this->_action & CRM_Core_Action::DELETE) { CRM_Core_BAO_LocationType::del($this->_id); diff --git a/civicrm/CRM/Admin/Form/OptionGroup.php b/civicrm/CRM/Admin/Form/OptionGroup.php index 059a5bf878604fab3bc2c7cac65c7e145490efae..c1216fcd83c2294b8d88f289009b8114161ac15b 100644 --- a/civicrm/CRM/Admin/Form/OptionGroup.php +++ b/civicrm/CRM/Admin/Form/OptionGroup.php @@ -110,22 +110,24 @@ class CRM_Admin_Form_OptionGroup extends CRM_Admin_Form { public function postProcess() { CRM_Utils_System::flushCache(); - $params = $this->exportValues(); if ($this->_action & CRM_Core_Action::DELETE) { CRM_Core_BAO_OptionGroup::del($this->_id); CRM_Core_Session::setStatus(ts('Selected option group has been deleted.'), ts('Record Deleted'), 'success'); } else { - - $params = $ids = array(); // store the submitted values in an array $params = $this->exportValues(); + // If we are adding option group via UI it should not be marked reserved. + if (!isset($params['is_reserved'])) { + $params['is_reserved'] = 0; + } + if ($this->_action & CRM_Core_Action::UPDATE) { - $ids['optionGroup'] = $this->_id; + $params['id'] = $this->_id; } - $optionGroup = CRM_Core_BAO_OptionGroup::add($params, $ids); + $optionGroup = CRM_Core_BAO_OptionGroup::add($params); CRM_Core_Session::setStatus(ts('The Option Group \'%1\' has been saved.', array(1 => $optionGroup->name)), ts('Saved'), 'success'); } } diff --git a/civicrm/CRM/Admin/Form/PaymentProcessorType.php b/civicrm/CRM/Admin/Form/PaymentProcessorType.php index f81617056744c6bfb0844b292607f808437a40d6..6dfdaf7647cda13235d9208a11b64165d7ed7117 100644 --- a/civicrm/CRM/Admin/Form/PaymentProcessorType.php +++ b/civicrm/CRM/Admin/Form/PaymentProcessorType.php @@ -203,7 +203,7 @@ class CRM_Admin_Form_PaymentProcessorType extends CRM_Admin_Form { * Process the form submission. */ public function postProcess() { - CRM_Utils_System::flushCache('CRM_Financial_DAO_PaymentProcessorType'); + CRM_Utils_System::flushCache(); if ($this->_action & CRM_Core_Action::DELETE) { CRM_Financial_BAO_PaymentProcessorType::del($this->_id); diff --git a/civicrm/CRM/Admin/Form/ScheduleReminders.php b/civicrm/CRM/Admin/Form/ScheduleReminders.php index 106487c5e32b71880f3bfa5dd17443c396df86f3..1067dad2c534e422db20d9687479cb02285e5f4b 100644 --- a/civicrm/CRM/Admin/Form/ScheduleReminders.php +++ b/civicrm/CRM/Admin/Form/ScheduleReminders.php @@ -43,6 +43,22 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { public $_freqUnits; + protected $_compId; + + /** + * @return mixed + */ + public function getComponentID() { + return $this->_compId; + } + + /** + * @param mixed $compId + */ + public function setComponentID($compId) { + $this->_compId = $compId; + } + /** * Build the form object. */ @@ -50,57 +66,46 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { parent::buildQuickForm(); $this->_mappingID = $mappingID = NULL; $providersCount = CRM_SMS_BAO_Provider::activeProviderCount(); - $this->_context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this); + $this->setContext(); + $isEvent = $this->getContext() == 'event'; - //CRM-16777: Don't provide access to administer schedule reminder page, with user that does not have 'administer CiviCRM' permission - if (empty($this->_context) && !CRM_Core_Permission::check('administer CiviCRM')) { - CRM_Core_Error::fatal(ts('You do not have permission to access this page.')); - } - //CRM-16777: When user have ACLs 'edit' permission for specific event, do not give access to add, delete & updtae - //schedule reminder for other events. - else { - $this->_compId = CRM_Utils_Request::retrieve('compId', 'Integer', $this); - if (!CRM_Event_BAO_Event::checkPermission($this->_compId, CRM_Core_Permission::EDIT)) { - CRM_Core_Error::fatal(ts('You do not have permission to access this page.')); + if ($isEvent) { + $this->setComponentID(CRM_Utils_Request::retrieve('compId', 'Integer', $this)); + if (!CRM_Event_BAO_Event::checkPermission($this->getComponentID(), CRM_Core_Permission::EDIT)) { + throw new CRM_Core_Exception(ts('You do not have permission to access this page.')); } } + elseif (!CRM_Core_Permission::check('administer CiviCRM')) { + throw new CRM_Core_Exception(ts('You do not have permission to access this page.')); + } if ($this->_action & (CRM_Core_Action::DELETE)) { $reminderName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_ActionSchedule', $this->_id, 'title'); - if ($this->_context == 'event') { - $this->_compId = CRM_Utils_Request::retrieve('compId', 'Integer', $this); - } $this->assign('reminderName', $reminderName); return; } elseif ($this->_action & (CRM_Core_Action::UPDATE)) { $this->_mappingID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_ActionSchedule', $this->_id, 'mapping_id'); - if ($this->_context == 'event') { - $this->_compId = CRM_Utils_Request::retrieve('compId', 'Integer', $this); - } } - elseif (!empty($this->_context)) { - if ($this->_context == 'event') { - $this->_compId = CRM_Utils_Request::retrieve('compId', 'Integer', $this); - $isTemplate = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $this->_compId, 'is_template'); - $mapping = CRM_Utils_Array::first(CRM_Core_BAO_ActionSchedule::getMappings(array( - 'id' => $isTemplate ? CRM_Event_ActionMapping::EVENT_TPL_MAPPING_ID : CRM_Event_ActionMapping::EVENT_NAME_MAPPING_ID, - ))); - if ($mapping) { - $this->_mappingID = $mapping->getId(); - } - else { - CRM_Core_Error::fatal('Could not find mapping for event scheduled reminders.'); - } + if ($isEvent) { + $isTemplate = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $this->getComponentID(), 'is_template'); + $mapping = CRM_Utils_Array::first(CRM_Core_BAO_ActionSchedule::getMappings(array( + 'id' => $isTemplate ? CRM_Event_ActionMapping::EVENT_TPL_MAPPING_ID : CRM_Event_ActionMapping::EVENT_NAME_MAPPING_ID, + ))); + if ($mapping) { + $this->_mappingID = $mapping->getId(); + } + else { + throw new CRM_Core_Exception('Could not find mapping for event scheduled reminders.'); } } - if (!empty($_POST) && !empty($_POST['entity']) && empty($this->_context)) { + if (!empty($_POST) && !empty($_POST['entity']) && empty($this->getContext())) { $mappingID = $_POST['entity'][0]; } elseif ($this->_mappingID) { $mappingID = $this->_mappingID; - if ($this->_context == 'event') { + if ($isEvent) { $this->add('hidden', 'mappingID', $mappingID); } } @@ -123,7 +128,7 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { array_combine(array_keys($entityRecipientLabels), array_keys($entityRecipientLabels)) )); - if (empty($this->_context)) { + if (!$this->getContext()) { $sel = &$this->add( 'hierselect', 'entity', @@ -158,7 +163,7 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { $attributes = array('multiple' => 'multiple', 'class' => 'crm-select2 huge', 'placeholder' => $options[0]); unset($options[0]); $this->add('select', 'entity', ts('Recipient(s)'), $options, TRUE, $attributes); - $this->assign('context', $this->_context); + $this->assign('context', $this->getContext()); } //get the frequency units. @@ -243,7 +248,7 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { ); if (!empty($this->_submitValues['recipient_listing'])) { - if (!empty($this->_context)) { + if ($this->getContext()) { $recipientListingOptions = CRM_Core_BAO_ActionSchedule::getRecipientListing($this->_mappingID, $this->_submitValues['recipient']); } else { @@ -325,7 +330,7 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { $errors['html_message'] = ts('The HTML message is a required field.'); } - if (empty($self->_context) && CRM_Utils_System::isNull(CRM_Utils_Array::value(1, $fields['entity']))) { + if (empty($self->getContext()) && CRM_Utils_System::isNull(CRM_Utils_Array::value(1, $fields['entity']))) { $errors['entity'] = ts('Please select entity value'); } @@ -384,7 +389,7 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { $defaults = $this->_values; $entityValue = explode(CRM_Core_DAO::VALUE_SEPARATOR, CRM_Utils_Array::value('entity_value', $defaults)); $entityStatus = explode(CRM_Core_DAO::VALUE_SEPARATOR, CRM_Utils_Array::value('entity_status', $defaults)); - if (empty($this->_context)) { + if (empty($this->getContext())) { $defaults['entity'][0] = CRM_Utils_Array::value('mapping_id', $defaults); $defaults['entity'][1] = $entityValue; $defaults['entity'][2] = $entityStatus; @@ -430,9 +435,9 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { // delete reminder CRM_Core_BAO_ActionSchedule::del($this->_id); CRM_Core_Session::setStatus(ts('Selected Reminder has been deleted.'), ts('Record Deleted'), 'success'); - if ($this->_context == 'event' && $this->_compId) { + if ($this->getContext() == 'event' && $this->getComponentID()) { $url = CRM_Utils_System::url('civicrm/event/manage/reminder', - "reset=1&action=browse&id={$this->_compId}&component={$this->_context}&setTab=1" + "reset=1&action=browse&id=" . $this->getComponentID() . "&component=" . $this->getContext() . "&setTab=1" ); $session = CRM_Core_Session::singleton(); $session->pushUserContext($url); @@ -457,8 +462,8 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { ); } - if ($this->_context == 'event' && $this->_compId) { - $url = CRM_Utils_System::url('civicrm/event/manage/reminder', "reset=1&action=browse&id={$this->_compId}&component={$this->_context}&setTab=1"); + if ($this->getContext() == 'event' && $this->getComponentID()) { + $url = CRM_Utils_System::url('civicrm/event/manage/reminder', "reset=1&action=browse&id=" . $this->getComponentID() . "&component=" . $this->getContext() . "&setTab=1"); $session = CRM_Core_Session::singleton(); $session->pushUserContext($url); } @@ -543,9 +548,9 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { $params['group_id'] = $params['recipient_manual'] = $params['recipient_listing'] = 'null'; } - if (!empty($this->_mappingID) && !empty($this->_compId)) { + if (!empty($this->_mappingID) && !empty($this->getComponentID())) { $params['mapping_id'] = $this->_mappingID; - $params['entity_value'] = $this->_compId; + $params['entity_value'] = $this->getComponentID(); $params['entity_status'] = implode(CRM_Core_DAO::VALUE_SEPARATOR, $values['entity']); } else { diff --git a/civicrm/CRM/Admin/Form/Setting.php b/civicrm/CRM/Admin/Form/Setting.php index e19ff5128651bf947469ff469ecc61044e0d8f02..695f6f097df4bcf63c494d270d9bafe460934f46 100644 --- a/civicrm/CRM/Admin/Form/Setting.php +++ b/civicrm/CRM/Admin/Form/Setting.php @@ -233,6 +233,7 @@ class CRM_Admin_Form_Setting extends CRM_Core_Form { } CRM_Core_Config::clearDBCache(); + Civi::cache('session')->clear(); // This doesn't make a lot of sense to me, but it maintains pre-existing behavior. CRM_Utils_System::flushCache(); CRM_Core_Resources::singleton()->resetCacheCode(); diff --git a/civicrm/CRM/Admin/Form/Setting/UpdateConfigBackend.php b/civicrm/CRM/Admin/Form/Setting/UpdateConfigBackend.php index 68711ff42aa69d2413052c28239f27c7e18c4508..b5c7bb8c57b89612c3aa06888d6e98f7a031ba05 100644 --- a/civicrm/CRM/Admin/Form/Setting/UpdateConfigBackend.php +++ b/civicrm/CRM/Admin/Form/Setting/UpdateConfigBackend.php @@ -65,6 +65,7 @@ class CRM_Admin_Form_Setting_UpdateConfigBackend extends CRM_Admin_Form_Setting // clear all caches CRM_Core_Config::clearDBCache(); + Civi::cache('session')->clear(); CRM_Utils_System::flushCache(); parent::rebuildMenu(); diff --git a/civicrm/CRM/Admin/Page/Job.php b/civicrm/CRM/Admin/Page/Job.php index 92237cccda27b7af37f83c1bcd602866742a7cef..9d9d31699d7a987e40e44b6044da6189597b5aff 100644 --- a/civicrm/CRM/Admin/Page/Job.php +++ b/civicrm/CRM/Admin/Page/Job.php @@ -96,6 +96,12 @@ class CRM_Admin_Page_Job extends CRM_Core_Page_Basic { 'qs' => 'action=delete&id=%%id%%', 'title' => ts('Delete Scheduled Job'), ), + CRM_Core_Action::COPY => array( + 'name' => ts('Copy'), + 'url' => 'civicrm/admin/job', + 'qs' => 'action=copy&id=%%id%%', + 'title' => ts('Copy Scheduled Job'), + ), ); } return self::$_links; @@ -128,11 +134,25 @@ class CRM_Admin_Page_Job extends CRM_Core_Page_Basic { $this, FALSE, 0 ); + // FIXME: Why are we comparing an integer with a string here? if ($this->_action == 'export') { $session = CRM_Core_Session::singleton(); $session->pushUserContext(CRM_Utils_System::url('civicrm/admin/job', 'reset=1')); } + if (($this->_action & CRM_Core_Action::COPY) && (!empty($this->_id))) { + try { + $jobResult = civicrm_api3('Job', 'clone', array('id' => $this->_id)); + if ($jobResult['count'] > 0) { + CRM_Core_Session::setStatus($jobResult['values'][$jobResult['id']]['name'], ts('Job copied successfully'), 'success'); + } + CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/admin/job', 'reset=1')); + } + catch (Exception $e) { + CRM_Core_Session::setStatus(ts('Failed to copy job'), 'Error'); + } + } + return parent::run(); } diff --git a/civicrm/CRM/Batch/BAO/Batch.php b/civicrm/CRM/Batch/BAO/Batch.php index ceae07312299e8fd686be54309fe480c168c49a8..ee73a515f07c069c825ed794d57b65c7b64d1c56 100644 --- a/civicrm/CRM/Batch/BAO/Batch.php +++ b/civicrm/CRM/Batch/BAO/Batch.php @@ -719,6 +719,8 @@ LEFT JOIN civicrm_contribution_soft ON civicrm_contribution_soft.contribution_id 'contribution_date_low', 'contribution_check_number', 'contribution_status_id', + 'financial_trxn_card_type_id', + 'financial_trxn_pan_truncation', ); $values = array(); foreach ($searchFields as $field) { diff --git a/civicrm/CRM/Campaign/BAO/Petition.php b/civicrm/CRM/Campaign/BAO/Petition.php index 43246cc66f65eb038a5e28262c0d1472e5aedf77..f8b0500ca277bb0e4cdff8b32bd7cfd89592a778 100644 --- a/civicrm/CRM/Campaign/BAO/Petition.php +++ b/civicrm/CRM/Campaign/BAO/Petition.php @@ -586,7 +586,7 @@ AND tag_id = ( SELECT id FROM civicrm_tag WHERE name = %2 )"; $toName = CRM_Contact_BAO_Contact::displayName($params['contactId']); - $replyTo = "do-not-reply@$emailDomain"; + $replyTo = CRM_Core_BAO_Domain::getNoReplyEmailAddress(); // set additional general message template params (custom tokens to use in email msg templates) // tokens then available in msg template as {$petition.title}, etc diff --git a/civicrm/CRM/Campaign/Form/Task.php b/civicrm/CRM/Campaign/Form/Task.php index 5d7bbd6a9bd93057d36dab54e230267a37a4a46c..a7e464ad83a7b6d7547cabed44f25b618e29516b 100644 --- a/civicrm/CRM/Campaign/Form/Task.php +++ b/civicrm/CRM/Campaign/Form/Task.php @@ -34,35 +34,7 @@ /** * This class generates form components for relationship. */ -class CRM_Campaign_Form_Task extends CRM_Core_Form { - - /** - * The additional clause that we restrict the search. - * - * @var string - */ - protected $_componentClause = NULL; - - /** - * The task being performed - * - * @var int - */ - protected $_task; - - /** - * The array that holds all the contact ids - * - * @var array - */ - public $_contactIds; - - /** - * The array that holds all the component ids - * - * @var array - */ - protected $_componentIds; +class CRM_Campaign_Form_Task extends CRM_Core_Form_Task { /** * The array that holds all the voter ids diff --git a/civicrm/CRM/Case/BAO/Case.php b/civicrm/CRM/Case/BAO/Case.php index 8048c8ff75b0780abecf333d40c99bd16740b29f..0fa87b459e92f0910c38cfc3f885a539934ffea0 100644 --- a/civicrm/CRM/Case/BAO/Case.php +++ b/civicrm/CRM/Case/BAO/Case.php @@ -274,16 +274,16 @@ WHERE civicrm_case.id = %1"; * ID of the case. * * @param int $contactID + * @param int $startArrayAt This is to support legacy calls to Case.Get API which may rely on the first array index being set to 1 * * @return array */ - public static function retrieveContactIdsByCaseId($caseId, $contactID = NULL) { + public static function retrieveContactIdsByCaseId($caseId, $contactID = NULL, $startArrayAt = 0) { $caseContact = new CRM_Case_DAO_CaseContact(); $caseContact->case_id = $caseId; $caseContact->find(); $contactArray = array(); - // FIXME: Why does this return a 1-based array? - $count = 1; + $count = $startArrayAt; while ($caseContact->fetch()) { if ($contactID != $caseContact->contact_id) { $contactArray[$count] = $caseContact->contact_id; @@ -840,12 +840,13 @@ SELECT case_status.label AS case_status, status_id, civicrm_case_type.title AS c * @param int $caseID * Case id. * @param int $relationshipID + * @param bool $activeOnly * * @return array * case role / relationships * */ - public static function getCaseRoles($contactID, $caseID, $relationshipID = NULL) { + public static function getCaseRoles($contactID, $caseID, $relationshipID = NULL, $activeOnly = TRUE) { $query = ' SELECT rel.id as civicrm_relationship_id, con.sort_name as sort_name, @@ -861,7 +862,11 @@ SELECT case_status.label AS case_status, status_id, civicrm_case_type.title AS c LEFT JOIN civicrm_phone ON (civicrm_phone.contact_id = con.id AND civicrm_phone.is_primary = 1) LEFT JOIN civicrm_email ON (civicrm_email.contact_id = con.id AND civicrm_email.is_primary = 1) WHERE (rel.contact_id_a = %1 OR rel.contact_id_b = %1) AND rel.case_id = %2 - AND rel.is_active = 1 AND con.is_deleted = 0 AND (rel.end_date IS NULL OR rel.end_date > NOW())'; + AND con.is_deleted = 0'; + + if ($activeOnly) { + $query .= ' AND rel.is_active = 1 AND (rel.end_date IS NULL OR rel.end_date > NOW())'; + } $params = array( 1 => array($contactID, 'Positive'), @@ -1085,7 +1090,6 @@ SELECT case_status.label AS case_status, status_id, civicrm_case_type.title AS c $contactViewUrl = CRM_Utils_System::url("civicrm/contact/view", "reset=1&cid=", FALSE, NULL, FALSE); $hasViewContact = CRM_Core_Permission::giveMeAllACLs(); - $clientIds = self::retrieveContactIdsByCaseId($caseID); if (!$userID) { $session = CRM_Core_Session::singleton(); diff --git a/civicrm/CRM/Case/BAO/CaseType.php b/civicrm/CRM/Case/BAO/CaseType.php index 42cc1baedde825e5ec2075a6dc75f2b749780d04..89baec6098e0224babbbac96f9508b18857481d6 100644 --- a/civicrm/CRM/Case/BAO/CaseType.php +++ b/civicrm/CRM/Case/BAO/CaseType.php @@ -242,6 +242,8 @@ class CRM_Case_BAO_CaseType extends CRM_Case_DAO_CaseType { // set activity sets if (isset($xml->ActivitySets)) { $definition['activitySets'] = array(); + $definition['timelineActivityTypes'] = array(); + foreach ($xml->ActivitySets->ActivitySet as $activitySetXML) { // parse basic properties $activitySet = array(); @@ -257,7 +259,11 @@ class CRM_Case_BAO_CaseType extends CRM_Case_DAO_CaseType { if (isset($activitySetXML->ActivityTypes)) { $activitySet['activityTypes'] = array(); foreach ($activitySetXML->ActivityTypes->ActivityType as $activityTypeXML) { - $activitySet['activityTypes'][] = json_decode(json_encode($activityTypeXML), TRUE); + $activityType = json_decode(json_encode($activityTypeXML), TRUE); + $activitySet['activityTypes'][] = $activityType; + if ($activitySetXML->timeline) { + $definition['timelineActivityTypes'][] = $activityType; + } } } $definition['activitySets'][] = $activitySet; diff --git a/civicrm/CRM/Case/Form/Activity/ChangeCaseStatus.php b/civicrm/CRM/Case/Form/Activity/ChangeCaseStatus.php index c00901c9abc5dfe99d455b121d7d0b0a69ec4d0c..3452a51ea79e88e691be3a8df7a1af0567836399 100644 --- a/civicrm/CRM/Case/Form/Activity/ChangeCaseStatus.php +++ b/civicrm/CRM/Case/Form/Activity/ChangeCaseStatus.php @@ -194,7 +194,7 @@ class CRM_Case_Form_Activity_ChangeCaseStatus { // Reopen case-specific relationships (roles) foreach ($params['target_contact_id'] as $cid) { - $rels = CRM_Case_BAO_Case::getCaseRoles($cid, $params['case_id']); + $rels = CRM_Case_BAO_Case::getCaseRoles($cid, $params['case_id'], NULL, FALSE); // FIXME: Is there an existing function? $query = 'UPDATE civicrm_relationship SET end_date=NULL WHERE id=%1'; foreach ($rels as $relId => $relData) { diff --git a/civicrm/CRM/Case/Form/ActivityView.php b/civicrm/CRM/Case/Form/ActivityView.php index e18b086d1909c3f95d3308e64ae819e365601045..16247cda225f1e4c7619aec3ca9268331476c530 100644 --- a/civicrm/CRM/Case/Form/ActivityView.php +++ b/civicrm/CRM/Case/Form/ActivityView.php @@ -172,6 +172,38 @@ class CRM_Case_Form_ActivityView extends CRM_Core_Form { $recentContactDisplay, $recentOther ); + + // Set breadcrumb to take the user back to the case being viewed + $caseTypeId = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_Case', $caseID, 'case_type_id'); + $caseType = CRM_Core_PseudoConstant::getLabel('CRM_Case_BAO_Case', 'case_type_id', $caseTypeId); + $caseContact = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseContact', $caseID, 'contact_id', 'case_id'); + + CRM_Utils_System::resetBreadCrumb(); + $breadcrumb = [ + [ + 'title' => ts('Home'), + 'url' => CRM_Utils_System::url(), + ], + [ + 'title' => ts('CiviCRM'), + 'url' => CRM_Utils_System::url('civicrm', 'reset=1'), + ], + [ + 'title' => ts('CiviCase Dashboard'), + 'url' => CRM_Utils_System::url('civicrm/case', 'reset=1'), + ], + [ + 'title' => $caseType, + 'url' => CRM_Utils_System::url('civicrm/contact/view/case', [ + 'reset' => 1, + 'id' => $caseID, + 'context' => 'case', + 'action' => 'view', + 'cid' => $caseContact, + ]), + ], + ]; + CRM_Utils_System::appendBreadCrumb($breadcrumb); } } diff --git a/civicrm/CRM/Case/Form/Task.php b/civicrm/CRM/Case/Form/Task.php index b9d05e68758cb3a7419117fcbc53d5b20386617e..513ee62e0502e8ff977078f5a2210063df4391aa 100644 --- a/civicrm/CRM/Case/Form/Task.php +++ b/civicrm/CRM/Case/Form/Task.php @@ -49,4 +49,13 @@ class CRM_Case_Form_Task extends CRM_Core_Form_Task { ); } + /** + * Get the query mode (eg. CRM_Core_BAO_Query::MODE_CASE) + * + * @return int + */ + public function getQueryMode() { + return CRM_Contact_BAO_Query::MODE_CASE; + } + } diff --git a/civicrm/CRM/Case/Task.php b/civicrm/CRM/Case/Task.php index 956ff16662ed15480baba932c8aebb4badb1d1dd..954786cde557e2d614763031d2ab613805f6d388 100644 --- a/civicrm/CRM/Case/Task.php +++ b/civicrm/CRM/Case/Task.php @@ -67,7 +67,7 @@ class CRM_Case_Task extends CRM_Core_Task { self::TASK_EXPORT => array( 'title' => ts('Export cases'), 'class' => array( - 'CRM_Export_Form_Select', + 'CRM_Export_Form_Select_Case', 'CRM_Export_Form_Map', ), 'result' => FALSE, diff --git a/civicrm/CRM/Case/XMLProcessor/Process.php b/civicrm/CRM/Case/XMLProcessor/Process.php index f125355ec9367e5095d043fabb6206c193f1139c..1929c38fb7293bcb328c3cb6df8239d7b670167c 100644 --- a/civicrm/CRM/Case/XMLProcessor/Process.php +++ b/civicrm/CRM/Case/XMLProcessor/Process.php @@ -31,6 +31,8 @@ * @copyright CiviCRM LLC (c) 2004-2018 */ class CRM_Case_XMLProcessor_Process extends CRM_Case_XMLProcessor { + protected $defaultAssigneeOptionsValues = []; + /** * Run. * @@ -314,6 +316,7 @@ class CRM_Case_XMLProcessor_Process extends CRM_Case_XMLProcessor { /** * @param SimpleXMLElement $caseTypeXML + * * @return array<string> symbolic activity-type names */ public function getDeclaredActivityTypes($caseTypeXML) { @@ -342,6 +345,7 @@ class CRM_Case_XMLProcessor_Process extends CRM_Case_XMLProcessor { /** * @param SimpleXMLElement $caseTypeXML + * * @return array<string> symbolic relationship-type names */ public function getDeclaredRelationshipTypes($caseTypeXML) { @@ -474,6 +478,8 @@ AND a.is_deleted = 0 ); } + $activityParams['assignee_contact_id'] = $this->getDefaultAssigneeForActivity($activityParams, $activityTypeXML); + //parsing date to default preference format $params['activity_date_time'] = CRM_Utils_Date::processDate($params['activity_date_time']); @@ -568,6 +574,155 @@ AND a.is_deleted = 0 return TRUE; } + /** + * Return the default assignee contact for the activity. + * + * @param array $activityParams + * @param object $activityTypeXML + * + * @return int|null the ID of the default assignee contact or null if none. + */ + protected function getDefaultAssigneeForActivity($activityParams, $activityTypeXML) { + if (!isset($activityTypeXML->default_assignee_type)) { + return NULL; + } + + $defaultAssigneeOptionsValues = $this->getDefaultAssigneeOptionValues(); + + switch ($activityTypeXML->default_assignee_type) { + case $defaultAssigneeOptionsValues['BY_RELATIONSHIP']: + return $this->getDefaultAssigneeByRelationship($activityParams, $activityTypeXML); + + break; + case $defaultAssigneeOptionsValues['SPECIFIC_CONTACT']: + return $this->getDefaultAssigneeBySpecificContact($activityTypeXML); + + break; + case $defaultAssigneeOptionsValues['USER_CREATING_THE_CASE']: + return $activityParams['source_contact_id']; + + break; + case $defaultAssigneeOptionsValues['NONE']: + default: + return NULL; + } + } + + /** + * Fetches and caches the activity's default assignee options. + * + * @return array + */ + protected function getDefaultAssigneeOptionValues() { + if (!empty($this->defaultAssigneeOptionsValues)) { + return $this->defaultAssigneeOptionsValues; + } + + $defaultAssigneeOptions = civicrm_api3('OptionValue', 'get', [ + 'option_group_id' => 'activity_default_assignee', + 'options' => [ 'limit' => 0 ] + ]); + + foreach ($defaultAssigneeOptions['values'] as $option) { + $this->defaultAssigneeOptionsValues[$option['name']] = $option['value']; + } + + return $this->defaultAssigneeOptionsValues; + } + + /** + * Returns the default assignee for the activity by searching for the target's + * contact relationship type defined in the activity's details. + * + * @param array $activityParams + * @param object $activityTypeXML + * + * @return int|null the ID of the default assignee contact or null if none. + */ + protected function getDefaultAssigneeByRelationship($activityParams, $activityTypeXML) { + $isDefaultRelationshipDefined = isset($activityTypeXML->default_assignee_relationship) + && preg_match('/\d+_[ab]_[ab]/', $activityTypeXML->default_assignee_relationship); + + if (!$isDefaultRelationshipDefined) { + return NULL; + } + + $targetContactId = is_array($activityParams['target_contact_id']) + ? CRM_Utils_Array::first($activityParams['target_contact_id']) + : $activityParams['target_contact_id']; + list($relTypeId, $a, $b) = explode('_', $activityTypeXML->default_assignee_relationship); + + $params = [ + 'relationship_type_id' => $relTypeId, + "contact_id_$b" => $targetContactId, + 'is_active' => 1, + ]; + + if ($this->isBidirectionalRelationshipType($relTypeId)) { + $params["contact_id_$a"] = $targetContactId; + $params['options']['or'] = [['contact_id_a', 'contact_id_b']]; + } + + $relationships = civicrm_api3('Relationship', 'get', $params); + + if ($relationships['count']) { + $relationship = CRM_Utils_Array::first($relationships['values']); + + // returns the contact id on the other side of the relationship: + return (int) $relationship['contact_id_a'] === (int) $targetContactId + ? $relationship['contact_id_b'] + : $relationship['contact_id_a']; + } + else { + return NULL; + } + } + + /** + * Determines if the given relationship type is bidirectional or not by + * comparing their labels. + * + * @return bool + */ + protected function isBidirectionalRelationshipType($relationshipTypeId) { + $relationshipTypeResult = civicrm_api3('RelationshipType', 'get', [ + 'id' => $relationshipTypeId, + 'options' => ['limit' => 1] + ]); + + if ($relationshipTypeResult['count'] === 0) { + return FALSE; + } + + $relationshipType = CRM_Utils_Array::first($relationshipTypeResult['values']); + + return $relationshipType['label_b_a'] === $relationshipType['label_a_b']; + } + + /** + * Returns the activity's default assignee for a specific contact if the contact exists, + * otherwise returns null. + * + * @param object $activityTypeXML + * + * @return int|null + */ + protected function getDefaultAssigneeBySpecificContact($activityTypeXML) { + if (!$activityTypeXML->default_assignee_contact) { + return NULL; + } + + $contact = civicrm_api3('Contact', 'get', [ + 'id' => $activityTypeXML->default_assignee_contact + ]); + + if ($contact['count'] == 1) { + return $activityTypeXML->default_assignee_contact; + } + + return NULL; + } + /** * @param $activitySetsXML * @@ -617,6 +772,7 @@ AND a.is_deleted = 0 /** * @param string $caseType + * * @return array<\Civi\CCase\CaseChangeListener> */ public function getListeners($caseType) { @@ -662,6 +818,7 @@ AND a.is_deleted = 0 * @param string $settingKey * @param string $xmlTag * @param mixed $default + * * @return int */ private function getBoolSetting($settingKey, $xmlTag, $default = 0) { diff --git a/civicrm/CRM/Contact/BAO/Contact.php b/civicrm/CRM/Contact/BAO/Contact.php index 01499868630a2a9a0aa678be672e3f3a20b261da..f222e19594a8f976f2e5586bcbbab2f45aa45af4 100644 --- a/civicrm/CRM/Contact/BAO/Contact.php +++ b/civicrm/CRM/Contact/BAO/Contact.php @@ -1022,10 +1022,6 @@ WHERE civicrm_contact.id = " . CRM_Utils_Type::escape($id, 'Integer'); CRM_Utils_Hook::post('delete', $contactType, $contact->id, $contact); } - // also reset the DB_DO global array so we can reuse the memory - // http://issues.civicrm.org/jira/browse/CRM-4387 - CRM_Core_DAO::freeResult(); - return TRUE; } diff --git a/civicrm/CRM/Contact/BAO/ContactType.php b/civicrm/CRM/Contact/BAO/ContactType.php index a299e9e59115790e58d3211a55e89f39fa009794..cb13346f556a1bd91fa98d7b7010b056757c2b17 100644 --- a/civicrm/CRM/Contact/BAO/ContactType.php +++ b/civicrm/CRM/Contact/BAO/ContactType.php @@ -387,6 +387,7 @@ WHERE type.name IS NOT NULL $argString = $all ? 'CRM_CT_GSE_1' : 'CRM_CT_GSE_0'; $argString .= $isSeparator ? '_1' : '_0'; $argString .= $separator; + $argString = CRM_Core_BAO_Cache::cleanKey($argString); if (!array_key_exists($argString, $_cache)) { $cache = CRM_Utils_Cache::singleton(); $_cache[$argString] = $cache->get($argString); diff --git a/civicrm/CRM/Contact/BAO/Group.php b/civicrm/CRM/Contact/BAO/Group.php index 759a6b3eaced3bfd9e966fd5bb0a0257e1bc9db6..8c08654256d78acd477d7017d4c70506a31a3d0b 100644 --- a/civicrm/CRM/Contact/BAO/Group.php +++ b/civicrm/CRM/Contact/BAO/Group.php @@ -882,7 +882,7 @@ class CRM_Contact_BAO_Group extends CRM_Contact_DAO_Group { // CRM-9936 $reservedPermission = CRM_Core_Permission::check('administer reserved groups'); - $links = self::actionLinks(); + $links = self::actionLinks($params); $allTypes = CRM_Core_OptionGroup::values('group_type'); $values = array(); @@ -1254,12 +1254,17 @@ WHERE {$whereClause}"; * @return array * array of action links */ - public static function actionLinks() { + public static function actionLinks($params) { + // If component_mode is set we change the "View" link to match the requested component type + if (!isset($params['component_mode'])) { + $params['component_mode'] = CRM_Contact_BAO_Query::MODE_CONTACTS; + } + $modeValue = CRM_Contact_Form_Search::getModeValue($params['component_mode']); $links = array( CRM_Core_Action::VIEW => array( - 'name' => ts('Contacts'), + 'name' => $modeValue['selectorLabel'], 'url' => 'civicrm/group/search', - 'qs' => 'reset=1&force=1&context=smog&gid=%%id%%', + 'qs' => 'reset=1&force=1&context=smog&gid=%%id%%&component_mode=' . $params['component_mode'], 'title' => ts('Group Contacts'), ), CRM_Core_Action::UPDATE => array( diff --git a/civicrm/CRM/Contact/BAO/Query.php b/civicrm/CRM/Contact/BAO/Query.php index 4e02c364dde3d56018d3c4f6d127c6b6b6716867..eda7792aa7d315e4c17307b001562a4b3acf2dd9 100644 --- a/civicrm/CRM/Contact/BAO/Query.php +++ b/civicrm/CRM/Contact/BAO/Query.php @@ -4066,7 +4066,7 @@ WHERE $smartGroupClause $relationType[2] = (array) $relationType[2]; foreach ($relationType[2] as $relType) { $rel = explode('_', $relType); - self::$_relType .= $rel[1]; + self::$_relType = $rel[1]; $params = array('id' => $rel[0]); $typeValues = array(); $rTypeValue = CRM_Contact_BAO_RelationshipType::retrieve($params, $typeValues); @@ -5662,11 +5662,11 @@ SELECT COUNT( conts.total_amount ) as cancel_count, return $clause; case 'IS EMPTY': - $clause = " (NULLIF($field, '') IS NULL) "; + $clause = ($dataType == 'Date') ? " $field IS NULL " : " (NULLIF($field, '') IS NULL) "; return $clause; case 'IS NOT EMPTY': - $clause = " (NULLIF($field, '') IS NOT NULL) "; + $clause = ($dataType == 'Date') ? " $field IS NOT NULL " : " (NULLIF($field, '') IS NOT NULL) "; return $clause; case 'IN': @@ -5677,7 +5677,7 @@ SELECT COUNT( conts.total_amount ) as cancel_count, } default: - if (empty($dataType)) { + if (empty($dataType) || $dataType == 'Date') { $dataType = 'String'; } if (is_array($value)) { diff --git a/civicrm/CRM/Contact/Form/Contact.php b/civicrm/CRM/Contact/Form/Contact.php index 9239530190acff16c604f26d49c1f3d7744bc620..1deb5ea20a14ac32125b21f077c7a72150cfa867 100644 --- a/civicrm/CRM/Contact/Form/Contact.php +++ b/civicrm/CRM/Contact/Form/Contact.php @@ -1461,7 +1461,7 @@ class CRM_Contact_Form_Contact extends CRM_Core_Form { 'max_related' => $dao->max_related, ); - CRM_Member_BAO_MembershipLog::add($membershipLog, CRM_Core_DAO::$_nullArray); + CRM_Member_BAO_MembershipLog::add($membershipLog); //create activity when membership status is changed $activityParam = array( diff --git a/civicrm/CRM/Contact/Form/Search/Builder.php b/civicrm/CRM/Contact/Form/Search/Builder.php index 2a5dd79351b47a673a22bb98d082df6fcaf8f324..4b5f0ddc29c84ba29f922fc5435d75d7b5063366 100644 --- a/civicrm/CRM/Contact/Form/Search/Builder.php +++ b/civicrm/CRM/Contact/Form/Search/Builder.php @@ -95,18 +95,10 @@ class CRM_Contact_Form_Search_Builder extends CRM_Contact_Form_Search { // This array contain list of available fields and their corresponding data type, // later assigned as json string, to be used to filter list of mysql operators $fieldNameTypes = []; - $dataType = [ - CRM_Utils_Type::T_STRING => 'String', - CRM_Utils_Type::T_TEXT => 'String', - CRM_Utils_Type::T_LONGTEXT => 'String', - CRM_Utils_Type::T_BOOLEAN => 'Boolean', - CRM_Utils_Type::T_DATE => 'Date', - CRM_Utils_Type::T_TIMESTAMP => 'Date', - ]; foreach ($fields as $name => $field) { // Assign date type to respective field name, which will be later used to modify operator list - if (isset($field['type']) && array_key_exists($field['type'], $dataType)) { - $fieldNameTypes[$name] = $dataType[$field['type']]; + if ($type = CRM_Utils_Array::key(CRM_Utils_Array::value('type', $field), CRM_Utils_Type::getValidTypes())) { + $fieldNameTypes[$name] = $type; } // it's necessary to know which of the fields are searchable by label if (isset($field['searchByLabel']) && $field['searchByLabel']) { @@ -477,8 +469,10 @@ class CRM_Contact_Form_Search_Builder extends CRM_Contact_Form_Search { $options[substr($field, 0, -3)] = $entity; } } - elseif (!empty($info['data_type']) && in_array($info['data_type'], array('StateProvince', 'Country'))) { - $options[$field] = $entity; + elseif (!empty($info['data_type'])) { + if (in_array($info['data_type'], array('StateProvince', 'Country'))) { + $options[$field] = $entity; + } } elseif (in_array(substr($field, 0, 3), array( 'is_', diff --git a/civicrm/CRM/Contact/Form/Search/Criteria.php b/civicrm/CRM/Contact/Form/Search/Criteria.php index 3c1b3e7ea9fca751a366756b879fe29c7096cea4..52d7a100168d5b6ce39baf69e9271424c622a7c9 100644 --- a/civicrm/CRM/Contact/Form/Search/Criteria.php +++ b/civicrm/CRM/Contact/Form/Search/Criteria.php @@ -35,6 +35,7 @@ class CRM_Contact_Form_Search_Criteria { * @param CRM_Core_Form $form */ public static function basic(&$form) { + self::setBasicSearchFields($form); $form->addElement('hidden', 'hidden_basic', 1); if ($form->_searchOptions['contactType']) { @@ -69,7 +70,7 @@ class CRM_Contact_Form_Search_Criteria { $contactTags = CRM_Core_BAO_Tag::getTags(); if ($contactTags) { - $form->add('select', 'contact_tags', ts('Tags'), $contactTags, FALSE, + $form->add('select', 'contact_tags', ts('Select Tag(s)'), $contactTags, FALSE, array('id' => 'contact_tags', 'multiple' => 'multiple', 'class' => 'crm-select2', 'style' => 'width: 100%;') ); } @@ -99,10 +100,10 @@ class CRM_Contact_Form_Search_Criteria { } // add text box for last name, first name, street name, city - $form->addElement('text', 'sort_name', ts('Find...'), CRM_Core_DAO::getAttribute('CRM_Contact_DAO_Contact', 'sort_name')); + $form->addElement('text', 'sort_name', ts('Complete OR Partial Name'), CRM_Core_DAO::getAttribute('CRM_Contact_DAO_Contact', 'sort_name')); // add text box for last name, first name, street name, city - $form->add('text', 'email', ts('Contact Email'), CRM_Core_DAO::getAttribute('CRM_Contact_DAO_Contact', 'sort_name')); + $form->add('text', 'email', ts('Complete OR Partial Email'), CRM_Core_DAO::getAttribute('CRM_Contact_DAO_Contact', 'sort_name')); //added contact source $form->add('text', 'contact_source', ts('Contact Source'), CRM_Core_DAO::getAttribute('CRM_Contact_DAO_Contact', 'contact_source')); @@ -248,6 +249,74 @@ class CRM_Contact_Form_Search_Criteria { $form->add('select', 'phone_phone_type_id', ts('Phone Type'), array('' => ts('- any -')) + $phoneType, FALSE, array('class' => 'crm-select2')); } + /** + * Defines the fields that can be displayed for the basic search section. + * + * @param CRM_Core_Form $form + */ + protected static function setBasicSearchFields($form) { + $userFramework = CRM_Core_Config::singleton()->userFramework; + + $form->assign('basicSearchFields', [ + 'sort_name' => ['name' => 'sort_name'], + 'email' => ['name' => 'email'], + 'contact_type' => ['name' => 'contact_type'], + 'group' => [ + 'name' => 'group', + 'template' => 'CRM/Contact/Form/Search/Criteria/Fields/group.tpl', + ], + 'contact_tags' => ['name' => 'contact_tags'], + 'tag_types_text' => ['name' => 'tag_types_text'], + 'tag_search' => [ + 'name' => 'tag_search', + 'help' => ['id' => 'id-all-tags'], + ], + 'tag_set' => [ + 'name' => 'tag_set', + 'is_custom' => TRUE, + 'template' => 'CRM/Contact/Form/Search/Criteria/Fields/tag_set.tpl', + ], + 'all_tag_types' => [ + 'name' => 'all_tag_types', + 'class' => 'search-field__span-3 search-field__checkbox', + 'help' => ['id' => 'id-all-tag-types'] + ], + 'phone_numeric' => [ + 'name' => 'phone_numeric', + 'description' => ts('Punctuation and spaces are ignored.'), + ], + 'phone_location_type_id' => ['name' => 'phone_location_type_id'], + 'phone_phone_type_id' => ['name' => 'phone_phone_type_id'], + 'privacy_toggle' => [ + 'name' => 'privacy_toggle', + 'class' => 'search-field__span-2', + 'template' => 'CRM/Contact/Form/Search/Criteria/Fields/privacy_toggle.tpl', + ], + 'preferred_communication_method' => [ + 'name' => 'preferred_communication_method', + 'template' => 'CRM/Contact/Form/Search/Criteria/Fields/preferred_communication_method.tpl', + ], + 'contact_source' => [ + 'name' => 'contact_source', + 'help' => ['id' => 'id-source', 'file' => 'CRM/Contact/Form/Contact'], + ], + 'job_title' => ['name' => 'job_title'], + 'preferred_language' => ['name' => 'preferred_language'], + 'contact_id' => [ + 'name' => 'contact_id', + 'help' => ['id' => 'id-contact-id', 'file' => 'CRM/Contact/Form/Contact'], + ], + 'external_identifier' => [ + 'name' => 'external_identifier', + 'help' => ['id' => 'id-external-id', 'file' => 'CRM/Contact/Form/Contact'], + ], + 'uf_user' => [ + 'name' => 'uf_user', + 'description' => ts('Does the contact have a %1 Account?', [$userFramework]), + ], + ]); + } + /** * @param CRM_Core_Form $form diff --git a/civicrm/CRM/Contact/Form/Search/Custom/ContribSYBNT.php b/civicrm/CRM/Contact/Form/Search/Custom/ContribSYBNT.php index bfd961b1c275bf44b9a59107929da9237fcd2c24..cf2b1e950be59178278235c38f703c3025eb27f8 100644 --- a/civicrm/CRM/Contact/Form/Search/Custom/ContribSYBNT.php +++ b/civicrm/CRM/Contact/Form/Search/Custom/ContribSYBNT.php @@ -192,10 +192,8 @@ ORDER BY donation_amount desc "; if ($justIDs) { - CRM_Core_DAO::executeQuery("DROP TEMPORARY TABLE IF EXISTS CustomSearch_SYBNT_temp"); - $query = "CREATE TEMPORARY TABLE CustomSearch_SYBNT_temp AS ({$sql})"; - CRM_Core_DAO::executeQuery($query); - $sql = "SELECT contact_a.id as contact_id FROM CustomSearch_SYBNT_temp as contact_a"; + $tempTable = CRM_Utils_SQL_TempTable::build()->createWithQuery($sql); + $sql = "SELECT contact_a.id as contact_id FROM {$tempTable->getName()} as contact_a"; } return $sql; } diff --git a/civicrm/CRM/Contact/Form/Search/Custom/DateAdded.php b/civicrm/CRM/Contact/Form/Search/Custom/DateAdded.php index 3ec22422e3db323af7da5952bb76efed92ae988d..34ec772f3c76b49bdeba68c73275bb7f122ec016 100644 --- a/civicrm/CRM/Contact/Form/Search/Custom/DateAdded.php +++ b/civicrm/CRM/Contact/Form/Search/Custom/DateAdded.php @@ -36,6 +36,8 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C protected $_aclFrom = NULL; protected $_aclWhere = NULL; + protected $_datesTable = NULL, $_xgTable = NULL, $_igTable = NULL; + /** * Class constructor. * @@ -177,11 +179,12 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C */ public function from() { //define table name - $randomNum = md5(uniqid()); - $this->_tableName = "civicrm_temp_custom_{$randomNum}"; + $this->_datesTable = CRM_Utils_SQL_TempTable::build()->setCategory('dates')->getName(); + $this->_xgTable = CRM_Utils_SQL_TempTable::build()->setCategory('xg')->getName(); + $this->_igTable = CRM_Utils_SQL_TempTable::build()->setCategory('ig')->getName(); //grab the contacts added in the date range first - $sql = "CREATE TEMPORARY TABLE dates_{$this->_tableName} ( id int primary key, date_added date ) ENGINE=HEAP"; + $sql = "CREATE TEMPORARY TABLE {$this->_datesTable} ( id int primary key, date_added date ) ENGINE=HEAP"; if ($this->_debug > 0) { print "-- Date range query: <pre>"; print "$sql;"; @@ -197,7 +200,7 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C $endDateFix = "AND date_added <= '" . substr($endDate, 0, 10) . " 23:59:00'"; } - $dateRange = "INSERT INTO dates_{$this->_tableName} ( id, date_added ) + $dateRange = "INSERT INTO {$this->_datesTable} ( id, date_added ) SELECT civicrm_contact.id, min(civicrm_log.modified_date) AS date_added @@ -249,16 +252,16 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C $xGroups = 0; } - $sql = "DROP TEMPORARY TABLE IF EXISTS Xg_{$this->_tableName}"; + $sql = "DROP TEMPORARY TABLE IF EXISTS {$this->_xgTable}"; CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray); - $sql = "CREATE TEMPORARY TABLE Xg_{$this->_tableName} ( contact_id int primary key) ENGINE=HEAP"; + $sql = "CREATE TEMPORARY TABLE {$this->_xgTable} ( contact_id int primary key) ENGINE=HEAP"; CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray); //used only when exclude group is selected if ($xGroups != 0) { - $excludeGroup = "INSERT INTO Xg_{$this->_tableName} ( contact_id ) + $excludeGroup = "INSERT INTO {$this->_xgTable} ( contact_id ) SELECT DISTINCT civicrm_group_contact.contact_id - FROM civicrm_group_contact, dates_{$this->_tableName} AS d + FROM civicrm_group_contact, {$this->_datesTable} AS d WHERE d.id = civicrm_group_contact.contact_id AND civicrm_group_contact.status = 'Added' AND @@ -277,16 +280,16 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C SELECT contact_id FROM civicrm_group_contact WHERE civicrm_group_contact.group_id = {$values} AND civicrm_group_contact.status = 'Removed')"; - $smartGroupQuery = " INSERT IGNORE INTO Xg_{$this->_tableName}(contact_id) $smartSql"; + $smartGroupQuery = " INSERT IGNORE INTO {$this->_xgTable}(contact_id) $smartSql"; CRM_Core_DAO::executeQuery($smartGroupQuery, CRM_Core_DAO::$_nullArray); } } } - $sql = "DROP TEMPORARY TABLE IF EXISTS Ig_{$this->_tableName}"; + $sql = "DROP TEMPORARY TABLE IF EXISTS {$this->_igTable}"; CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray); - $sql = "CREATE TEMPORARY TABLE Ig_{$this->_tableName} + $sql = "CREATE TEMPORARY TABLE {$this->_igTable} ( id int PRIMARY KEY AUTO_INCREMENT, contact_id int, group_names varchar(64)) ENGINE=HEAP"; @@ -299,9 +302,9 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray); - $includeGroup = "INSERT INTO Ig_{$this->_tableName} (contact_id, group_names) + $includeGroup = "INSERT INTO {$this->_igTable} (contact_id, group_names) SELECT d.id as contact_id, civicrm_group.name as group_name - FROM dates_{$this->_tableName} AS d + FROM {$this->_datesTable} AS d INNER JOIN civicrm_group_contact ON civicrm_group_contact.contact_id = d.id LEFT JOIN civicrm_group @@ -309,8 +312,8 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C //used only when exclude group is selected if ($xGroups != 0) { - $includeGroup .= " LEFT JOIN Xg_{$this->_tableName} - ON d.id = Xg_{$this->_tableName}.contact_id"; + $includeGroup .= " LEFT JOIN {$this->_xgTable} + ON d.id = {$this->_xgTable}.contact_id"; } $includeGroup .= " WHERE civicrm_group_contact.status = 'Added' AND @@ -318,7 +321,7 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C //used only when exclude group is selected if ($xGroups != 0) { - $includeGroup .= " AND Xg_{$this->_tableName}.contact_id IS null"; + $includeGroup .= " AND {$this->_xgTable}.contact_id IS null"; } if ($this->_debug > 0) { @@ -339,7 +342,7 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C $smartSql .= " AND contact_a.id IN ( SELECT id AS contact_id - FROM dates_{$this->_tableName} )"; + FROM {$this->_datesTable} )"; $smartSql .= " AND contact_a.id NOT IN ( SELECT contact_id FROM civicrm_group_contact @@ -347,11 +350,11 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C //used only when exclude group is selected if ($xGroups != 0) { - $smartSql .= " AND contact_a.id NOT IN (SELECT contact_id FROM Xg_{$this->_tableName})"; + $smartSql .= " AND contact_a.id NOT IN (SELECT contact_id FROM {$this->_xgTable})"; } $smartGroupQuery = " INSERT IGNORE INTO - Ig_{$this->_tableName}(contact_id) + {$this->_igTable}(contact_id) $smartSql"; CRM_Core_DAO::executeQuery($smartGroupQuery, CRM_Core_DAO::$_nullArray); @@ -360,11 +363,11 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C print "$smartGroupQuery;"; print "</pre>"; } - $insertGroupNameQuery = "UPDATE IGNORE Ig_{$this->_tableName} + $insertGroupNameQuery = "UPDATE IGNORE {$this->_igTable} SET group_names = (SELECT title FROM civicrm_group WHERE civicrm_group.id = $values) - WHERE Ig_{$this->_tableName}.contact_id IS NOT NULL - AND Ig_{$this->_tableName}.group_names IS NULL"; + WHERE {$this->_igTable}.contact_id IS NOT NULL + AND {$this->_igTable}.group_names IS NULL"; CRM_Core_DAO::executeQuery($insertGroupNameQuery, CRM_Core_DAO::$_nullArray); if ($this->_debug > 0) { print "-- Smart group query: <pre>"; @@ -380,12 +383,12 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C /* We need to join to this again to get the date_added value */ - $from .= " INNER JOIN dates_{$this->_tableName} d ON (contact_a.id = d.id) {$this->_aclFrom}"; + $from .= " INNER JOIN {$this->_datesTable} d ON (contact_a.id = d.id) {$this->_aclFrom}"; // Only include groups in the search query of one or more Include OR Exclude groups has been selected. // CRM-6356 if ($this->_groups) { - $from .= " INNER JOIN Ig_{$this->_tableName} temptable1 ON (contact_a.id = temptable1.contact_id)"; + $from .= " INNER JOIN {$this->_igTable} temptable1 ON (contact_a.id = temptable1.contact_id)"; } return $from; @@ -437,13 +440,13 @@ class CRM_Contact_Form_Search_Custom_DateAdded extends CRM_Contact_Form_Search_C public function __destruct() { //drop the temp. tables if they exist - if (!empty($this->_includeGroups)) { - $sql = "DROP TEMPORARY TABLE IF EXISTS Ig_{$this->_tableName}"; + if ($this->_igTable && !empty($this->_includeGroups)) { + $sql = "DROP TEMPORARY TABLE IF EXISTS {$this->_igTable}"; CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray); } - if (!empty($this->_excludeGroups)) { - $sql = "DROP TEMPORARY TABLE IF EXISTS Xg_{$this->_tableName}"; + if ($this->_xgTable && !empty($this->_excludeGroups)) { + $sql = "DROP TEMPORARY TABLE IF EXISTS {$this->_xgTable}"; CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray); } } diff --git a/civicrm/CRM/Contact/Form/Task.php b/civicrm/CRM/Contact/Form/Task.php index 5136641e3b70ee2def7aa92f76e76f650f5c203e..2d6e7637bf7a46a75acda0ea7cd8d3eaf0c5f462 100644 --- a/civicrm/CRM/Contact/Form/Task.php +++ b/civicrm/CRM/Contact/Form/Task.php @@ -34,7 +34,7 @@ /** * This class generates form components for search-result tasks. */ -class CRM_Contact_Form_Task extends CRM_Core_Form { +class CRM_Contact_Form_Task extends CRM_Core_Form_Task { /** * The task being performed @@ -94,12 +94,15 @@ class CRM_Contact_Form_Task extends CRM_Core_Form { * Common pre-processing function. * * @param CRM_Core_Form $form - * @param bool $useTable + * + * @throws \CRM_Core_Exception */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $form->_contactIds = array(); $form->_contactTypes = array(); + $useTable = (CRM_Utils_System::getClassName($form->controller->getStateMachine()) == 'CRM_Export_StateMachine_Standalone'); + $isStandAlone = in_array('task', $form->urlPath) || in_array('standalone', $form->urlPath); if ($isStandAlone) { list($form->_task, $title) = CRM_Contact_Task::getTaskAndTitleByClass(get_class($form)); @@ -150,7 +153,7 @@ class CRM_Contact_Form_Task extends CRM_Core_Form { $form->assign('taskName', CRM_Utils_Array::value($form->_task, $crmContactTaskTasks)); if ($useTable) { - $form->_componentTable = CRM_Core_DAO::createTempTableName('civicrm_task_action', TRUE, $qfKey); + $form->_componentTable = CRM_Utils_SQL_TempTable::build()->setCategory('tskact')->setDurable()->setId($qfKey)->getName(); $sql = " DROP TABLE IF EXISTS {$form->_componentTable}"; CRM_Core_DAO::executeQuery($sql); @@ -172,7 +175,7 @@ class CRM_Contact_Form_Task extends CRM_Core_Form { $allCids = CRM_Core_BAO_PrevNextCache::getSelection($cacheKey, "getall"); } else { - $allCids[$cacheKey] = $form->getContactIds(); + $allCids[$cacheKey] = self::getContactIds($form); } $form->_contactIds = array(); @@ -283,36 +286,41 @@ class CRM_Contact_Form_Task extends CRM_Core_Form { } /** - * Get the contact id for custom search. + * Get the contact ids for: + * - "Select Records: All xx records" + * - custom search (FIXME: does this still apply to custom search?). + * When we call this function we are not using the prev/next cache + * + * @param $form CRM_Core_Form * - * we are not using prev/next table in case of custom search + * @return array $contactIds */ - public function getContactIds() { + public static function getContactIds($form) { // need to perform action on all contacts // fire the query again and get the contact id's + display name $sortID = NULL; - if ($this->get(CRM_Utils_Sort::SORT_ID)) { - $sortID = CRM_Utils_Sort::sortIDValue($this->get(CRM_Utils_Sort::SORT_ID), - $this->get(CRM_Utils_Sort::SORT_DIRECTION) + if ($form->get(CRM_Utils_Sort::SORT_ID)) { + $sortID = CRM_Utils_Sort::sortIDValue($form->get(CRM_Utils_Sort::SORT_ID), + $form->get(CRM_Utils_Sort::SORT_DIRECTION) ); } - $selectorName = $this->controller->selectorName(); + $selectorName = $form->controller->selectorName(); - $fv = $this->get('formValues'); - $customClass = $this->get('customSearchClass'); + $fv = $form->get('formValues'); + $customClass = $form->get('customSearchClass'); $returnProperties = CRM_Core_BAO_Mapping::returnProperties(self::$_searchFormValues); $selector = new $selectorName($customClass, $fv, NULL, $returnProperties); - $params = $this->get('queryParams'); + $params = $form->get('queryParams'); // fix for CRM-5165 - $sortByCharacter = $this->get('sortByCharacter'); + $sortByCharacter = $form->get('sortByCharacter'); if ($sortByCharacter && $sortByCharacter != 1) { $params[] = array('sortByCharacter', '=', $sortByCharacter, 0, 0); } - $queryOperator = $this->get('queryOperator'); + $queryOperator = $form->get('queryOperator'); if (!$queryOperator) { $queryOperator = 'AND'; } diff --git a/civicrm/CRM/Contact/Import/Parser.php b/civicrm/CRM/Contact/Import/Parser.php index 552ec453f618997be0b1b739aa90fe809b707614..6a40dea23ff94e32898bbea78b99aa9e642ce715 100644 --- a/civicrm/CRM/Contact/Import/Parser.php +++ b/civicrm/CRM/Contact/Import/Parser.php @@ -275,9 +275,6 @@ abstract class CRM_Contact_Import_Parser extends CRM_Import_Parser { break; } - // clean up memory from dao's - CRM_Core_DAO::freeResult(); - // see if we've hit our timeout yet /* if ( $the_thing_with_the_stuff ) { do_something( ); diff --git a/civicrm/CRM/Contact/Task.php b/civicrm/CRM/Contact/Task.php index 71cf992bdab553885112ddac440619b82f541d7f..f314b028493b956b72454047da892683ec9c0a64 100644 --- a/civicrm/CRM/Contact/Task.php +++ b/civicrm/CRM/Contact/Task.php @@ -297,18 +297,17 @@ class CRM_Contact_Task extends CRM_Core_Task { self::LABEL_CONTACTS => self::$_tasks[self::LABEL_CONTACTS]['title'], ); - if (isset(self::$_tasks[self::MAP_CONTACTS]) && - !empty(self::$_tasks[self::MAP_CONTACTS]['title']) - ) { - $tasks[self::MAP_CONTACTS] = self::$_tasks[self::MAP_CONTACTS]['title']; - } - - if (isset(self::$_tasks[self::CREATE_MAILING]) && - !empty(self::$_tasks[self::CREATE_MAILING]['title']) - ) { - $tasks[self::CREATE_MAILING] = self::$_tasks[self::CREATE_MAILING]['title']; + foreach ([ + self::MAP_CONTACTS, + self::CREATE_MAILING, + self::TASK_SMS + ] as $task) { + if (isset(self::$_tasks[$task]) && + !empty(self::$_tasks[$task]['title']) + ) { + $tasks[$task] = self::$_tasks[$task]['title']; + } } - } $tasks = parent::corePermissionedTaskTitles($tasks, $permission, $params); diff --git a/civicrm/CRM/Contribute/BAO/Contribution.php b/civicrm/CRM/Contribute/BAO/Contribution.php index f58bd9f9eaadc40c4ee859a511c81d54f36034b2..5682ae6fea556e788ff07c798f52ea1fe6572097 100644 --- a/civicrm/CRM/Contribute/BAO/Contribution.php +++ b/civicrm/CRM/Contribute/BAO/Contribution.php @@ -1967,7 +1967,7 @@ LEFT JOIN civicrm_contribution contribution ON ( componentPayment.contribution_ $membershipLog['modified_date'] = date('Ymd'); $membershipLog['membership_type_id'] = $membership->membership_type_id; - CRM_Member_BAO_MembershipLog::add($membershipLog, CRM_Core_DAO::$_nullArray); + CRM_Member_BAO_MembershipLog::add($membershipLog); //update related Memberships. CRM_Member_BAO_Membership::updateRelatedMemberships($membership->id, $formattedParams); @@ -2441,7 +2441,7 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac } } - $this->loadRelatedMembershipObjects($ids); + $ids = $this->loadRelatedMembershipObjects($ids); if ($this->_component != 'contribute') { // we are in event mode @@ -3840,7 +3840,6 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac * @return null|object */ public static function recordAdditionalPayment($contributionId, $trxnsData, $paymentType = 'owed', $participantId = NULL, $updateStatus = TRUE) { - $statusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); $getInfoOf['id'] = $contributionId; $defaults = array(); $contributionDAO = CRM_Contribute_BAO_Contribution::retrieve($getInfoOf, $defaults, CRM_Core_DAO::$_nullArray); @@ -3849,8 +3848,7 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac } // load related memberships on basis of $contributionDAO object - $membershipIDs = array(); - $contributionDAO->loadRelatedMembershipObjects($membershipIDs); + $contributionDAO->loadRelatedMembershipObjects(); // build params for recording financial trxn entry $params['contribution'] = $contributionDAO; @@ -3859,7 +3857,7 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac $trxnsData['trxn_date'] = !empty($trxnsData['trxn_date']) ? $trxnsData['trxn_date'] : date('YmdHis'); $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($contributionDAO->financial_type_id, 'Accounts Receivable Account is'); - // get the paid status id + $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); $paidStatus = CRM_Core_PseudoConstant::getKey('CRM_Financial_DAO_FinancialItem', 'status_id', 'Paid'); if ($paymentType == 'owed') { @@ -3894,7 +3892,7 @@ LEFT JOIN civicrm_entity_financial_trxn eft WHERE eft.entity_table = 'civicrm_contribution' AND eft.entity_id = {$contributionId} AND ft.to_financial_account_id != {$toFinancialAccount} - AND ft.status_id = {$statusId} + AND ft.status_id = {$completedStatusId} "; $query = CRM_Core_DAO::executeQuery($sql); $query->fetch(); @@ -3904,7 +3902,7 @@ WHERE eft.entity_table = 'civicrm_contribution' if ($contributionDAO->total_amount == $sumOfPayments) { // update contribution status and // clean cancel info (if any) if prev. contribution was updated in case of 'Refunded' => 'Completed' - $contributionDAO->contribution_status_id = $statusId; + $contributionDAO->contribution_status_id = $completedStatusId; $contributionDAO->cancel_date = 'null'; $contributionDAO->cancel_reason = NULL; $netAmount = !empty($trxnsData['net_amount']) ? NULL : $trxnsData['total_amount']; @@ -3913,7 +3911,7 @@ WHERE eft.entity_table = 'civicrm_contribution' $contributionDAO->save(); //Change status of financial record too - $financialTrxn->status_id = $statusId; + $financialTrxn->status_id = $completedStatusId; $financialTrxn->save(); // note : not using the self::add method, @@ -3932,15 +3930,11 @@ WHERE eft.entity_table = 'civicrm_contribution' } } - // update membership details - if (!empty($contributionDAO->_relatedObjects['membership'])) { - self::updateMembershipBasedOnCompletionOfContribution( - $contributionDAO, - $contributionDAO->_relatedObjects['membership'], - $contributionId, - $trxnsData['trxn_date'] - ); - } + self::updateMembershipBasedOnCompletionOfContribution( + $contributionDAO, + $contributionId, + $trxnsData['trxn_date'] + ); // update financial item statuses $baseTrxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contributionId); @@ -3966,7 +3960,7 @@ WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']}) // which in 'Pending Refund' => 'Completed' is not useful, instead specific financial record updates // are coded below i.e. just updating financial_item status to 'Paid' if ($updateStatus) { - $contributionDetails = CRM_Core_DAO::setFieldValue('CRM_Contribute_BAO_Contribution', $contributionId, 'contribution_status_id', $statusId); + CRM_Core_DAO::setFieldValue('CRM_Contribute_BAO_Contribution', $contributionId, 'contribution_status_id', $completedStatusId); } // add financial item entry $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($contributionDAO->id); @@ -4553,7 +4547,6 @@ WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']}) } $participant = CRM_Utils_Array::value('participant', $objects); - $memberships = CRM_Utils_Array::value('membership', $objects); $recurContrib = CRM_Utils_Array::value('contributionRecur', $objects); $recurringContributionID = (empty($recurContrib->id)) ? NULL : $recurContrib->id; $event = CRM_Utils_Array::value('event', $objects); @@ -4601,10 +4594,6 @@ WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']}) self::repeatTransaction($contribution, $input, $contributionParams, $paymentProcessorId); $contributionParams['financial_type_id'] = $contribution->financial_type_id; - if (is_numeric($memberships)) { - $memberships = array($objects['membership']); - } - $values = array(); if (isset($input['is_email_receipt'])) { $values['is_email_receipt'] = $input['is_email_receipt']; @@ -4632,15 +4621,12 @@ WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']}) $values['is_email_receipt'] = $recurContrib->is_email_receipt; } - if (!empty($memberships)) { - self::updateMembershipBasedOnCompletionOfContribution( - $contribution, - $memberships, - $primaryContributionID, - $changeDate, - CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_Contribution', 'contribution_status_id', CRM_Utils_Array::value('contribution_status_id', $input)) - ); - } + self::updateMembershipBasedOnCompletionOfContribution( + $contribution, + $primaryContributionID, + $changeDate, + CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_Contribution', 'contribution_status_id', CRM_Utils_Array::value('contribution_status_id', $input)) + ); } else { if (empty($input['IAmAHorribleNastyBeyondExcusableHackInTheCRMEventFORMTaskClassThatNeedsToBERemoved'])) { @@ -4823,9 +4809,11 @@ WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']}) * * @param array $ids * + * @return array $ids + * * @throws Exception */ - public function loadRelatedMembershipObjects(&$ids) { + public function loadRelatedMembershipObjects($ids = []) { $query = " SELECT membership_id FROM civicrm_membership_payment @@ -4856,6 +4844,7 @@ WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']}) } } } + return $ids; } /** @@ -5400,7 +5389,6 @@ LEFT JOIN civicrm_contribution on (civicrm_contribution.contact_id = civicrm_co * load them in this function. Code clean up would compensate for any minor performance implication. * * @param \CRM_Contribute_BAO_Contribution $contribution - * @param array $memberships * @param int $primaryContributionID * @param string $changeDate * @param string $contributionStatus @@ -5408,7 +5396,12 @@ LEFT JOIN civicrm_contribution on (civicrm_contribution.contact_id = civicrm_co * * @todo investigate completely bypassing this function if $contributionStatus != Completed. */ - protected static function updateMembershipBasedOnCompletionOfContribution($contribution, $memberships, $primaryContributionID, $changeDate, $contributionStatus = 'Completed') { + protected static function updateMembershipBasedOnCompletionOfContribution($contribution, $primaryContributionID, $changeDate, $contributionStatus = 'Completed') { + $contribution->loadRelatedMembershipObjects(); + if (empty($contribution->_relatedObjects['membership'])) { + return; + } + $memberships = $contribution->_relatedObjects['membership']; foreach ($memberships as $membershipTypeIdKey => $membership) { if ($membership) { $membershipParams = array( diff --git a/civicrm/CRM/Contribute/BAO/Contribution/Utils.php b/civicrm/CRM/Contribute/BAO/Contribution/Utils.php index 2b99789923aa5e552e4fabe7174604c8fc25159c..0b2c99b898e9b99c8c7db1e4ad53c7c29ff85976 100644 --- a/civicrm/CRM/Contribute/BAO/Contribution/Utils.php +++ b/civicrm/CRM/Contribute/BAO/Contribution/Utils.php @@ -74,6 +74,8 @@ class CRM_Contribute_BAO_Contribution_Utils { // add some financial type details to the params list // if folks need to use it + //CRM-15297 deprecate contributionTypeID + $paymentParams['financial_type_id'] = $paymentParams['financialTypeID'] = $paymentParams['contributionTypeID'] = $financialType->id; //CRM-15297 - contributionType is obsolete - pass financial type as well so people can deprecate it $paymentParams['financialType_name'] = $paymentParams['contributionType_name'] = $form->_params['contributionType_name'] = $financialType->name; //CRM-11456 @@ -154,8 +156,6 @@ class CRM_Contribute_BAO_Contribution_Utils { } $paymentParams['contributionID'] = $contribution->id; - //CRM-15297 deprecate contributionTypeID - $paymentParams['financialTypeID'] = $paymentParams['contributionTypeID'] = $contribution->financial_type_id; $paymentParams['contributionPageID'] = $contribution->contribution_page_id; if (isset($paymentParams['contribution_source'])) { $paymentParams['source'] = $paymentParams['contribution_source']; diff --git a/civicrm/CRM/Contribute/BAO/ContributionRecur.php b/civicrm/CRM/Contribute/BAO/ContributionRecur.php index aa1d02b5ab2229576cc0d4d9a7f8241cc852b902..a5c76722c4339cca4753afcb0dd3edf417270dbc 100644 --- a/civicrm/CRM/Contribute/BAO/ContributionRecur.php +++ b/civicrm/CRM/Contribute/BAO/ContributionRecur.php @@ -32,6 +32,13 @@ */ class CRM_Contribute_BAO_ContributionRecur extends CRM_Contribute_DAO_ContributionRecur { + /** + * Array with statuses that mark a recurring contribution as inactive. + * + * @var array + */ + private static $inactiveStatuses = array('Cancelled', 'Chargeback', 'Refunded', 'Completed'); + /** * Create recurring contribution. * @@ -233,15 +240,12 @@ SELECT r.payment_processor_id * * @param int $recurId * Recur contribution id. - * @param array $objects - * An array of objects that is to be cancelled like. - * contribution, membership, event. At least contribution object is a must. * * @param array $activityParams * * @return bool */ - public static function cancelRecurContribution($recurId, $objects, $activityParams = array()) { + public static function cancelRecurContribution($recurId, $activityParams = array()) { if (!$recurId) { return FALSE; } @@ -282,7 +286,7 @@ SELECT r.payment_processor_id } $activityParams = array( 'source_contact_id' => $dao->contact_id, - 'source_record_id' => CRM_Utils_Array::value('source_record_id', $activityParams), + 'source_record_id' => $dao->recur_id, 'activity_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Cancel Recurring Contribution'), 'subject' => CRM_Utils_Array::value('subject', $activityParams, ts('Recurring contribution cancelled')), 'details' => $details, @@ -299,16 +303,8 @@ SELECT r.payment_processor_id CRM_Activity_BAO_Activity::create($activityParams); } - // if there are associated objects, cancel them as well - if (!$objects) { - $transaction->commit(); - return TRUE; - } - else { - // @todo - this is bad! Get the function out of the ipn. - $baseIPN = new CRM_Core_Payment_BaseIPN(); - return $baseIPN->cancelled($objects, $transaction); - } + $transaction->commit(); + return TRUE; } else { // if already cancelled, return true @@ -938,4 +934,14 @@ INNER JOIN civicrm_contribution con ON ( con.id = mp.contribution_id ) return $lineSets; } + /** + * Returns array with statuses that are considered to make a recurring + * contribution inacteve. + * + * @return array + */ + public static function getInactiveStatuses() { + return self::$inactiveStatuses; + } + } diff --git a/civicrm/CRM/Contribute/Exception/FutureContributionPageException.php b/civicrm/CRM/Contribute/Exception/FutureContributionPageException.php new file mode 100644 index 0000000000000000000000000000000000000000..5deeb178f493be16ffdc0fc75e064bfec522a0d0 --- /dev/null +++ b/civicrm/CRM/Contribute/Exception/FutureContributionPageException.php @@ -0,0 +1,25 @@ +<?php + +class CRM_Contribute_Exception_FutureContributionPageException extends Exception { + private $id; + + /** + * @param string $message + * @param int $id + */ + public function __construct($message, $id) { + parent::__construct(ts($message)); + $this->id = $id; + CRM_Core_Error::debug_log_message('Access to contribution page with start date in future attempted - page number ' . $id); + } + + /** + * Get Contribution page ID. + * + * @return int + */ + public function getID() { + return $this->id; + } + +} diff --git a/civicrm/CRM/Contribute/Exception/PastContributionPageException.php b/civicrm/CRM/Contribute/Exception/PastContributionPageException.php new file mode 100644 index 0000000000000000000000000000000000000000..fc7c8b3182eae60a81f63de38daa77b5e11da837 --- /dev/null +++ b/civicrm/CRM/Contribute/Exception/PastContributionPageException.php @@ -0,0 +1,25 @@ +<?php + +class CRM_Contribute_Exception_PastContributionPageException extends Exception { + private $id; + + /** + * @param string $message + * @param int $id + */ + public function __construct($message, $id) { + parent::__construct(ts($message)); + $this->id = $id; + CRM_Core_Error::debug_log_message('Access to contribution page with past end date attempted - page number ' . $id); + } + + /** + * Get Contribution page ID. + * + * @return int + */ + public function getID() { + return $this->id; + } + +} diff --git a/civicrm/CRM/Contribute/Form/CancelSubscription.php b/civicrm/CRM/Contribute/Form/CancelSubscription.php index 06941d9035bf3b4dc88c3c17ace08bfcdf8fb147..ec4aabd93beb3eb5a7b0923bb1fb73d8d514f1ac 100644 --- a/civicrm/CRM/Contribute/Form/CancelSubscription.php +++ b/civicrm/CRM/Contribute/Form/CancelSubscription.php @@ -224,7 +224,6 @@ class CRM_Contribute_Form_CancelSubscription extends CRM_Core_Form { ); $cancelStatus = CRM_Contribute_BAO_ContributionRecur::cancelRecurContribution( $this->_subscriptionDetails->recur_id, - NULL, $activityParams ); diff --git a/civicrm/CRM/Contribute/Form/Contribution/Confirm.php b/civicrm/CRM/Contribute/Form/Contribution/Confirm.php index 17a0bd2f92bbd99c4d34af27924ccf03209e82b6..a8b19f13e04ff52b36a2ae67470eb159a6713071 100644 --- a/civicrm/CRM/Contribute/Form/Contribution/Confirm.php +++ b/civicrm/CRM/Contribute/Form/Contribution/Confirm.php @@ -740,21 +740,18 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr * * Comments from previous refactor indicate doubt as to what was going on. * - * @param int $contributionTypeId + * @param int $financialTypeID * * @return null|string */ - public function wrangleFinancialTypeID($contributionTypeId) { - if (isset($paymentParams['financial_type'])) { - $contributionTypeId = $paymentParams['financial_type']; - } - elseif (!empty($this->_values['pledge_id'])) { - $contributionTypeId = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', + public function wrangleFinancialTypeID($financialTypeID) { + if (empty($financialTypeID) && !empty($this->_values['pledge_id'])) { + $financialTypeID = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $this->_values['pledge_id'], 'financial_type_id' ); } - return $contributionTypeId; + return $financialTypeID; } /** @@ -1997,6 +1994,7 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr } } $form->set('memberPriceFieldIDS', $membershipPriceFieldIDs); + $form->setRecurringMembershipParams(); $form->processFormSubmission(CRM_Utils_Array::value('contact_id', $params)); } @@ -2395,26 +2393,19 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr $priceFieldIds = $this->get('memberPriceFieldIDS'); if (!empty($priceFieldIds)) { - $financialTypeID = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $priceFieldIds['id'], 'financial_type_id'); + $membershipParams['financial_type_id'] = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $priceFieldIds['id'], 'financial_type_id'); unset($priceFieldIds['id']); $membershipTypeIds = array(); $membershipTypeTerms = array(); foreach ($priceFieldIds as $priceFieldId) { - if ($id = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceFieldValue', $priceFieldId, 'membership_type_id')) { - $membershipTypeIds[] = $id; - //@todo the value for $term is immediately overwritten. It is unclear from the code whether it was intentional to - // do this or a double = was intended (this ambiguity is the reason many IDEs complain about 'assignment in condition' - $term = 1; - if ($term = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceFieldValue', $priceFieldId, 'membership_num_terms')) { - $membershipTypeTerms[$id] = ($term > 1) ? $term : 1; - } - else { - $membershipTypeTerms[$id] = 1; - } + $membershipTypeId = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceFieldValue', $priceFieldId, 'membership_type_id'); + if ($membershipTypeId) { + $membershipTypeIds[] = $membershipTypeId; + $term = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceFieldValue', $priceFieldId, 'membership_num_terms') ?: 1; + $membershipTypeTerms[$membershipTypeId] = ($term > 1) ? $term : 1; } } $membershipParams['selectMembership'] = $membershipTypeIds; - $membershipParams['financial_type_id'] = $financialTypeID; $membershipParams['types_terms'] = $membershipTypeTerms; } if (!empty($membershipParams['selectMembership'])) { diff --git a/civicrm/CRM/Contribute/Form/Contribution/Main.php b/civicrm/CRM/Contribute/Form/Contribution/Main.php index 9d7ed7c59b4bce1644644daddaa3a094a34c2080..89b92fbf3e98710b2fdce7061f66ad4be69f0ce5 100644 --- a/civicrm/CRM/Contribute/Form/Contribution/Main.php +++ b/civicrm/CRM/Contribute/Form/Contribution/Main.php @@ -1300,6 +1300,9 @@ class CRM_Contribute_Form_Contribution_Main extends CRM_Contribute_Form_Contribu if (empty($this->_ccid)) { return; } + if (!$this->getContactID()) { + CRM_Core_Error::statusBounce(ts("Returning since there is no contact attached to this contribution id.")); + } $payment = CRM_Contribute_BAO_Contribution::getPaymentInfo($this->_ccid, 'contribution'); //bounce if the contribution is not pending. diff --git a/civicrm/CRM/Contribute/Form/ContributionBase.php b/civicrm/CRM/Contribute/Form/ContributionBase.php index 97a06e145ce4f0c404de754dc6e69920dd8ff68c..561adfe00d4d0c61d90ce67c8da0452f54ded010 100644 --- a/civicrm/CRM/Contribute/Form/ContributionBase.php +++ b/civicrm/CRM/Contribute/Form/ContributionBase.php @@ -313,6 +313,17 @@ class CRM_Contribute_Form_ContributionBase extends CRM_Core_Form { throw new CRM_Contribute_Exception_InactiveContributionPageException(ts('The page you requested is currently unavailable.'), $this->_id); } + $endDate = CRM_Utils_Date::processDate(CRM_Utils_Array::value('end_date', $this->_values)); + $now = date('YmdHis'); + if ($endDate && $endDate < $now) { + throw new CRM_Contribute_Exception_PastContributionPageException(ts('The page you requested has past its end date on ' . CRM_Utils_Date::customFormat($endDate)), $this->_id); + } + + $startDate = CRM_Utils_Date::processDate(CRM_Utils_Array::value('start_date', $this->_values)); + if ($startDate && $startDate > $now) { + throw new CRM_Contribute_Exception_FutureContributionPageException(ts('The page you requested will be active from ' . CRM_Utils_Date::customFormat($startDate)), $this->_id); + } + $this->assignBillingType(); // check for is_monetary status @@ -603,11 +614,7 @@ class CRM_Contribute_Form_ContributionBase extends CRM_Core_Form { $this->assign($paymentField, $this->_params[$paymentField]); } } - $paymentFieldsetLabel = ts('%1 Information', array($paymentProcessorObject->getPaymentTypeLabel())); - if (empty($paymentFields)) { - $paymentFieldsetLabel = ''; - } - $this->assign('paymentFieldsetLabel', $paymentFieldsetLabel); + $this->assign('paymentFieldsetLabel', CRM_Core_Payment_Form::getPaymentLabel($paymentProcessorObject)); $this->assign('paymentFields', $paymentFields); } diff --git a/civicrm/CRM/Contribute/Form/ContributionPage.php b/civicrm/CRM/Contribute/Form/ContributionPage.php index 238353ca077d2d350f858326475e5e03f895da14..bc3f23d03ea6ce17a98a6db21de713b9ab2a48d8 100644 --- a/civicrm/CRM/Contribute/Form/ContributionPage.php +++ b/civicrm/CRM/Contribute/Form/ContributionPage.php @@ -332,18 +332,13 @@ class CRM_Contribute_Form_ContributionPage extends CRM_Core_Form { if ($this->_priceSetID) { $defaults['price_set_id'] = $this->_priceSetID; } - - if (!empty($defaults['end_date'])) { - list($defaults['end_date'], $defaults['end_date_time']) = CRM_Utils_Date::setDateDefaults($defaults['end_date']); - } - - if (!empty($defaults['start_date'])) { - list($defaults['start_date'], $defaults['start_date_time']) = CRM_Utils_Date::setDateDefaults($defaults['start_date']); - } } else { $defaults['is_active'] = 1; // set current date as start date + // @todo look to change to $defaults['start_date'] = date('Ymd His'); + // main settings form overrides this to implement above but this is left here + // 'in case' another extending form uses start_date - for now list($defaults['start_date'], $defaults['start_date_time']) = CRM_Utils_Date::setDateDefaults(); } @@ -353,7 +348,7 @@ class CRM_Contribute_Form_ContributionPage extends CRM_Core_Form { ), '1'); } else { - # CRM 10860 + // CRM-10860 $defaults['recur_frequency_unit'] = array('month' => 1); } diff --git a/civicrm/CRM/Contribute/Form/ContributionPage/Settings.php b/civicrm/CRM/Contribute/Form/ContributionPage/Settings.php index 49e595bfec0a967e5d6205e239d53395a9893df7..7e94089ef4ebe4a88df51148e2e82e687c78c806 100644 --- a/civicrm/CRM/Contribute/Form/ContributionPage/Settings.php +++ b/civicrm/CRM/Contribute/Form/ContributionPage/Settings.php @@ -44,6 +44,11 @@ class CRM_Contribute_Form_ContributionPage_Settings extends CRM_Contribute_Form_ */ public function setDefaultValues() { $defaults = parent::setDefaultValues(); + // @todo handle properly on parent. + if (!$this->_id) { + $defaults['start_date'] = date('Y-m-d H:i:s'); + unset($defaults['start_time']); + } $soft_credit_types = CRM_Core_OptionGroup::values('soft_credit_type', TRUE, FALSE, FALSE, NULL, 'name'); if ($this->_id) { @@ -223,8 +228,8 @@ class CRM_Contribute_Form_ContributionPage_Settings extends CRM_Contribute_Form_ } // add optional start and end dates - $this->addDateTime('start_date', ts('Start Date')); - $this->addDateTime('end_date', ts('End Date')); + $this->add('datepicker', 'start_date', ts('Start Date')); + $this->add('datepicker', 'end_date', ts('End Date')); $this->addFormRule(array('CRM_Contribute_Form_ContributionPage_Settings', 'formRule'), $this); @@ -334,10 +339,6 @@ class CRM_Contribute_Form_ContributionPage_Settings extends CRM_Contribute_Form_ $params['is_credit_card_only'] = CRM_Utils_Array::value('is_credit_card_only', $params, FALSE); $params['honor_block_is_active'] = CRM_Utils_Array::value('honor_block_is_active', $params, FALSE); $params['is_for_organization'] = !empty($params['is_organization']) ? CRM_Utils_Array::value('is_for_organization', $params, FALSE) : 0; - - $params['start_date'] = CRM_Utils_Date::processDate($params['start_date'], $params['start_date_time'], TRUE); - $params['end_date'] = CRM_Utils_Date::processDate($params['end_date'], $params['end_date_time'], TRUE); - $params['goal_amount'] = CRM_Utils_Rule::cleanMoney($params['goal_amount']); if (!$params['honor_block_is_active']) { diff --git a/civicrm/CRM/Contribute/Form/ContributionPage/Widget.php b/civicrm/CRM/Contribute/Form/ContributionPage/Widget.php index df8905af0af200bc94ebe9fb20eba394b6e169b0..9d8a621354b847c0625e1148db4880dee11d3c9c 100644 --- a/civicrm/CRM/Contribute/Form/ContributionPage/Widget.php +++ b/civicrm/CRM/Contribute/Form/ContributionPage/Widget.php @@ -85,55 +85,55 @@ class CRM_Contribute_Form_ContributionPage_Widget extends CRM_Contribute_Form_Co $this->_colorFields = array( 'color_title' => array( ts('Title Text Color'), - 'text', + 'color', FALSE, '#2786C2', ), 'color_bar' => array( ts('Progress Bar Color'), - 'text', + 'color', FALSE, '#2786C2', ), 'color_main_text' => array( ts('Additional Text Color'), - 'text', + 'color', FALSE, '#FFFFFF', ), 'color_main' => array( ts('Background Color'), - 'text', + 'color', FALSE, '#96C0E7', ), 'color_main_bg' => array( ts('Background Color Top Area'), - 'text', + 'color', FALSE, '#B7E2FF', ), 'color_bg' => array( ts('Border Color'), - 'text', + 'color', FALSE, '#96C0E7', ), 'color_about_link' => array( ts('Button Text Color'), - 'text', + 'color', FALSE, '#556C82', ), 'color_button' => array( ts('Button Background Color'), - 'text', + 'color', FALSE, '#FFFFFF', ), 'color_homepage_link' => array( ts('Homepage Link Color'), - 'text', + 'color', FALSE, '#FFFFFF', ), diff --git a/civicrm/CRM/Contribute/Form/Task.php b/civicrm/CRM/Contribute/Form/Task.php index 296401626407cb367dd9f7ce5492b7dfb20b60e8..f089448db49c37ab3ebf243e8b2d7a91cf6caf1d 100644 --- a/civicrm/CRM/Contribute/Form/Task.php +++ b/civicrm/CRM/Contribute/Form/Task.php @@ -32,30 +32,10 @@ */ /** - * This class generates form components for relationship. + * Class for contribute form task actions. + * FIXME: This needs refactoring to properly inherit from CRM_Core_Form_Task and share more functions. */ -class CRM_Contribute_Form_Task extends CRM_Core_Form { - - /** - * The task being performed. - * - * @var int - */ - protected $_task; - - /** - * The additional clause that we restrict the search with. - * - * @var string - */ - protected $_componentClause = NULL; - - /** - * The array that holds all the component ids. - * - * @var array - */ - protected $_componentIds; +class CRM_Contribute_Form_Task extends CRM_Core_Form_Task { /** * The array that holds all the contribution ids. @@ -64,13 +44,6 @@ class CRM_Contribute_Form_Task extends CRM_Core_Form { */ protected $_contributionIds; - /** - * The array that holds all the contact ids. - * - * @var array - */ - public $_contactIds; - /** * The array that holds all the mapping contribution and contact ids. * @@ -94,9 +67,8 @@ class CRM_Contribute_Form_Task extends CRM_Core_Form { /** * @param CRM_Core_Form $form - * @param bool $useTable */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $form->_contributionIds = array(); $values = $form->controller->exportValues($form->get('searchFormName')); @@ -217,7 +189,7 @@ class CRM_Contribute_Form_Task extends CRM_Core_Form { */ public function setContactIDs() { if (!$this->_includesSoftCredits) { - $this->_contactIds = &CRM_Core_DAO::getContactIDsFromComponent( + $this->_contactIds = CRM_Core_DAO::getContactIDsFromComponent( $this->_contributionIds, 'civicrm_contribution' ); diff --git a/civicrm/CRM/Contribute/Page/Tab.php b/civicrm/CRM/Contribute/Page/Tab.php index c8260fad816be2de54fce82dbcac2b1cc92ea2d6..b5c20a9e0c57449eff850f1ec1d539143d154db0 100644 --- a/civicrm/CRM/Contribute/Page/Tab.php +++ b/civicrm/CRM/Contribute/Page/Tab.php @@ -166,59 +166,106 @@ class CRM_Contribute_Page_Tab extends CRM_Core_Page { * Get all the recurring contribution information and assign to the template */ private function addRecurringContributionsBlock() { + $activeContributions = $this->getActiveRecurringContributions(); + $inactiveRecurringContributions = $this->getInactiveRecurringContributions(); + + if (!empty($activeContributions) || !empty($inactiveRecurringContributions)) { + // assign vars to templates + $this->assign('action', $this->_action); + $this->assign('activeRecurRows', $activeContributions); + $this->assign('inactiveRecurRows', $inactiveRecurringContributions); + $this->assign('recur', TRUE); + } + } + + /** + * Loads active recurring contributions for the current contact and formats + * them to be used on the form. + * + * @return array; + */ + private function getActiveRecurringContributions() { try { $contributionRecurResult = civicrm_api3('ContributionRecur', 'get', array( 'contact_id' => $this->_contactId, + 'contribution_status_id' => array('NOT IN' => CRM_Contribute_BAO_ContributionRecur::getInactiveStatuses()), 'options' => array('limit' => 0, 'sort' => 'start_date ASC'), )); $recurContributions = CRM_Utils_Array::value('values', $contributionRecurResult); } catch (Exception $e) { - $recurContributions = NULL; + $recurContributions = array(); } - if (!empty($recurContributions)) { - foreach ($recurContributions as $recurId => $recurDetail) { - $action = array_sum(array_keys($this->recurLinks($recurId))); - // no action allowed if it's not active - $recurContributions[$recurId]['is_active'] = (!CRM_Contribute_BAO_Contribution::isContributionStatusNegative($recurDetail['contribution_status_id'])); + return $this->buildRecurringContributionsArray($recurContributions); + } - // Get the name of the payment processor - if (!empty($recurDetail['payment_processor_id'])) { - $recurContributions[$recurId]['payment_processor'] = CRM_Financial_BAO_PaymentProcessor::getPaymentProcessorName($recurDetail['payment_processor_id']); - } - // Get the label for the contribution status - if (!empty($recurDetail['contribution_status_id'])) { - $recurContributions[$recurId]['contribution_status'] = CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_ContributionRecur', 'contribution_status_id', $recurDetail['contribution_status_id']); - } + /** + * Loads inactive recurring contributions for the current contact and formats + * them to be used on the form. + * + * @return array; + */ + private function getInactiveRecurringContributions() { + try { + $contributionRecurResult = civicrm_api3('ContributionRecur', 'get', array( + 'contact_id' => $this->_contactId, + 'contribution_status_id' => array('IN' => CRM_Contribute_BAO_ContributionRecur::getInactiveStatuses()), + 'options' => array('limit' => 0, 'sort' => 'start_date ASC'), + )); + $recurContributions = CRM_Utils_Array::value('values', $contributionRecurResult); + } + catch (Exception $e) { + $recurContributions = NULL; + } - if ($recurContributions[$recurId]['is_active']) { - $details = CRM_Contribute_BAO_ContributionRecur::getSubscriptionDetails($recurContributions[$recurId]['id'], 'recur'); - $hideUpdate = $details->membership_id & $details->auto_renew; + return $this->buildRecurringContributionsArray($recurContributions); + } - if ($hideUpdate) { - $action -= CRM_Core_Action::UPDATE; - } + /** + * @param $recurContributions + * + * @return mixed + */ + private function buildRecurringContributionsArray($recurContributions) { + foreach ($recurContributions as $recurId => $recurDetail) { + $action = array_sum(array_keys($this->recurLinks($recurId))); + // no action allowed if it's not active + $recurContributions[$recurId]['is_active'] = (!CRM_Contribute_BAO_Contribution::isContributionStatusNegative($recurDetail['contribution_status_id'])); + + // Get the name of the payment processor + if (!empty($recurDetail['payment_processor_id'])) { + $recurContributions[$recurId]['payment_processor'] = CRM_Financial_BAO_PaymentProcessor::getPaymentProcessorName($recurDetail['payment_processor_id']); + } + // Get the label for the contribution status + if (!empty($recurDetail['contribution_status_id'])) { + $recurContributions[$recurId]['contribution_status'] = CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_ContributionRecur', 'contribution_status_id', $recurDetail['contribution_status_id']); + } - $recurContributions[$recurId]['action'] = CRM_Core_Action::formLink(self::recurLinks($recurId), $action, - array( - 'cid' => $this->_contactId, - 'crid' => $recurId, - 'cxt' => 'contribution', - ), - ts('more'), - FALSE, - 'contribution.selector.recurring', - 'Contribution', - $recurId - ); + if ($recurContributions[$recurId]['is_active']) { + $details = CRM_Contribute_BAO_ContributionRecur::getSubscriptionDetails($recurContributions[$recurId]['id'], 'recur'); + $hideUpdate = $details->membership_id & $details->auto_renew; + + if ($hideUpdate) { + $action -= CRM_Core_Action::UPDATE; } + + $recurContributions[$recurId]['action'] = CRM_Core_Action::formLink(self::recurLinks($recurId), $action, + array( + 'cid' => $this->_contactId, + 'crid' => $recurId, + 'cxt' => 'contribution', + ), + ts('more'), + FALSE, + 'contribution.selector.recurring', + 'Contribution', + $recurId + ); } - // assign vars to templates - $this->assign('action', $this->_action); - $this->assign('recurRows', $recurContributions); - $this->assign('recur', TRUE); } + + return $recurContributions; } /** diff --git a/civicrm/CRM/Core/BAO/Cache.php b/civicrm/CRM/Core/BAO/Cache.php index 0e619143e33a830c4623ac49657158bff82a32c7..ce1d63ede35a9a3106ea2f10f13963330fc5645d 100644 --- a/civicrm/CRM/Core/BAO/Cache.php +++ b/civicrm/CRM/Core/BAO/Cache.php @@ -40,6 +40,13 @@ */ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { + /** + * When store session/form state, how long should the data be retained? + * + * @var int, number of second + */ + const DEFAULT_SESSION_TTL = 172800; // Two days: 2*24*60*60 + /** * @var array ($cacheKey => $cacheValue) */ @@ -66,15 +73,15 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { $argString = "CRM_CT_{$group}_{$path}_{$componentID}"; if (!array_key_exists($argString, self::$_cache)) { $cache = CRM_Utils_Cache::singleton(); - self::$_cache[$argString] = $cache->get($argString); + self::$_cache[$argString] = $cache->get(self::cleanKey($argString)); if (!self::$_cache[$argString]) { $table = self::getTableName(); $where = self::whereCache($group, $path, $componentID); $rawData = CRM_Core_DAO::singleValueQuery("SELECT data FROM $table WHERE $where"); - $data = $rawData ? unserialize($rawData) : NULL; + $data = $rawData ? self::decode($rawData) : NULL; self::$_cache[$argString] = $data; - $cache->set($argString, self::$_cache[$argString]); + $cache->set(self::cleanKey($argString), self::$_cache[$argString]); } } return self::$_cache[$argString]; @@ -99,7 +106,7 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { $argString = "CRM_CT_CI_{$group}_{$componentID}"; if (!array_key_exists($argString, self::$_cache)) { $cache = CRM_Utils_Cache::singleton(); - self::$_cache[$argString] = $cache->get($argString); + self::$_cache[$argString] = $cache->get(self::cleanKey($argString)); if (!self::$_cache[$argString]) { $table = self::getTableName(); $where = self::whereCache($group, NULL, $componentID); @@ -107,12 +114,12 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { $result = array(); while ($dao->fetch()) { - $result[$dao->path] = unserialize($dao->data); + $result[$dao->path] = self::decode($dao->data); } $dao->free(); self::$_cache[$argString] = $result; - $cache->set($argString, self::$_cache[$argString]); + $cache->set(self::cleanKey($argString), self::$_cache[$argString]); } } @@ -148,7 +155,7 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { $where = self::whereCache($group, $path, $componentID); $dataExists = CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM $table WHERE {$where}"); $now = date('Y-m-d H:i:s'); // FIXME - Use SQL NOW() or CRM_Utils_Time? - $dataSerialized = serialize($data); + $dataSerialized = self::encode($data); // This table has a wonky index, so we cannot use REPLACE or // "INSERT ... ON DUPE". Instead, use SELECT+(INSERT|UPDATE). @@ -180,13 +187,13 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { $argString = "CRM_CT_{$group}_{$path}_{$componentID}"; $cache = CRM_Utils_Cache::singleton(); - $data = unserialize($dataSerialized); + $data = self::decode($dataSerialized); self::$_cache[$argString] = $data; - $cache->set($argString, $data); + $cache->set(self::cleanKey($argString), $data); $argString = "CRM_CT_CI_{$group}_{$componentID}"; unset(self::$_cache[$argString]); - $cache->delete($argString); + $cache->delete(self::cleanKey($argString)); } /** @@ -237,7 +244,8 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { if (!empty($_SESSION[$sessionName[0]][$sessionName[1]])) { $value = $_SESSION[$sessionName[0]][$sessionName[1]]; } - self::setItem($value, 'CiviCRM Session', "{$sessionName[0]}_{$sessionName[1]}"); + $key = "{$sessionName[0]}_{$sessionName[1]}"; + Civi::cache('session')->set($key, $value, self::pickSessionTtl($key)); if ($resetSession) { $_SESSION[$sessionName[0]][$sessionName[1]] = NULL; unset($_SESSION[$sessionName[0]][$sessionName[1]]); @@ -248,7 +256,7 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { if (!empty($_SESSION[$sessionName])) { $value = $_SESSION[$sessionName]; } - self::setItem($value, 'CiviCRM Session', $sessionName); + Civi::cache('session')->set($sessionName, $value, self::pickSessionTtl($sessionName)); if ($resetSession) { $_SESSION[$sessionName] = NULL; unset($_SESSION[$sessionName]); @@ -275,17 +283,13 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { public static function restoreSessionFromCache($names) { foreach ($names as $key => $sessionName) { if (is_array($sessionName)) { - $value = self::getItem('CiviCRM Session', - "{$sessionName[0]}_{$sessionName[1]}" - ); + $value = Civi::cache('session')->get("{$sessionName[0]}_{$sessionName[1]}"); if ($value) { $_SESSION[$sessionName[0]][$sessionName[1]] = $value; } } else { - $value = self::getItem('CiviCRM Session', - $sessionName - ); + $value = Civi::cache('session')->get($sessionName); if ($value) { $_SESSION[$sessionName] = $value; } @@ -293,6 +297,32 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { } } + /** + * Determine how long session-state should be retained. + * + * @param string $sessionKey + * Ex: '_CRM_Admin_Form_Preferences_Display_f1a5f232e3d850a29a7a4d4079d7c37b_4654_container' + * Ex: 'CiviCRM_CRM_Admin_Form_Preferences_Display_f1a5f232e3d850a29a7a4d4079d7c37b_4654' + * @return int + * Number of seconds. + */ + protected static function pickSessionTtl($sessionKey) { + $secureSessionTimeoutMinutes = (int) Civi::settings()->get('secure_cache_timeout_minutes'); + if ($secureSessionTimeoutMinutes) { + $transactionPages = array( + 'CRM_Contribute_Controller_Contribution', + 'CRM_Event_Controller_Registration', + ); + foreach ($transactionPages as $transactionPage) { + if (strpos($sessionKey, $transactionPage) !== FALSE) { + return $secureSessionTimeoutMinutes * 60; + } + } + } + + return self::DEFAULT_SESSION_TTL; + } + /** * Do periodic cleanup of the CiviCRM session table. * @@ -304,33 +334,7 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache { * @param bool $table * @param bool $prevNext */ - public static function cleanup($session = FALSE, $table = FALSE, $prevNext = FALSE) { - // first delete all sessions more than 20 minutes old which are related to any potential transaction - $timeIntervalMins = (int) Civi::settings()->get('secure_cache_timeout_minutes'); - if ($timeIntervalMins && $session) { - $transactionPages = array( - 'CRM_Contribute_Controller_Contribution', - 'CRM_Event_Controller_Registration', - ); - - $params = array( - 1 => array( - date('Y-m-d H:i:s', time() - $timeIntervalMins * 60), - 'String', - ), - ); - foreach ($transactionPages as $trPage) { - $params[] = array("%${trPage}%", 'String'); - $where[] = 'path LIKE %' . count($params); - } - - $sql = " -DELETE FROM civicrm_cache -WHERE group_name = 'CiviCRM Session' -AND created_date <= %1 -AND (" . implode(' OR ', $where) . ")"; - CRM_Core_DAO::executeQuery($sql, $params); - } + public static function cleanup($session = FALSE, $table = FALSE, $prevNext = FALSE, $expired = FALSE) { // clean up the session cache every $cacheCleanUpNumber probabilistically $cleanUpNumber = 757; @@ -338,10 +342,10 @@ AND (" . implode(' OR ', $where) . ")"; $timeIntervalDays = 2; if (mt_rand(1, 100000) % $cleanUpNumber == 0) { - $session = $table = $prevNext = TRUE; + $expired = $session = $table = $prevNext = TRUE; } - if (!$session && !$table && !$prevNext) { + if (!$session && !$table && !$prevNext && !$expired) { return; } @@ -355,13 +359,43 @@ AND (" . implode(' OR ', $where) . ")"; } if ($session) { + // Session caches are just regular caches, so they expire naturally per TTL. + $expired = TRUE; + } - $sql = " -DELETE FROM civicrm_cache -WHERE group_name = 'CiviCRM Session' -AND created_date < date_sub( NOW( ), INTERVAL $timeIntervalDays DAY ) -"; - CRM_Core_DAO::executeQuery($sql); + if ($expired) { + $sql = "DELETE FROM civicrm_cache WHERE expired_date < %1"; + $params = [ + 1 => [date(CRM_Utils_Cache_SqlGroup::TS_FMT, CRM_Utils_Time::getTimeRaw()), 'String'], + ]; + CRM_Core_DAO::executeQuery($sql, $params); + } + } + + /** + * (Quasi-private) Encode an object/array/string/int as a string. + * + * @param $mixed + * @return string + */ + public static function encode($mixed) { + return base64_encode(serialize($mixed)); + } + + /** + * (Quasi-private) Decode an object/array/string/int from a string. + * + * @param $string + * @return mixed + */ + public static function decode($string) { + // Upgrade support -- old records (serialize) always have this punctuation, + // and new records (base64) never do. + if (strpos($string, ':') !== FALSE || strpos($string, ';') !== FALSE) { + return unserialize($string); + } + else { + return unserialize(base64_decode($string)); } } @@ -390,4 +424,35 @@ AND created_date < date_sub( NOW( ), INTERVAL $timeIntervalDays DAY ) return $clauses ? implode(' AND ', $clauses) : '(1)'; } + /** + * Normalize a cache key. + * + * This bridges an impedance mismatch between our traditional caching + * and PSR-16 -- PSR-16 accepts a narrower range of cache keys. + * + * @param string $key + * Ex: 'ab/cd:ef' + * @return string + * Ex: '_abcd1234abcd1234' or 'ab_xx/cd_xxef'. + * A similar key, but suitable for use with PSR-16-compliant cache providers. + */ + public static function cleanKey($key) { + if (!is_string($key) && !is_int($key)) { + throw new \RuntimeException("Malformed cache key"); + } + + $maxLen = 64; + $escape = '-'; + + if (strlen($key) >= $maxLen) { + return $escape . md5($key); + } + + $r = preg_replace_callback(';[^A-Za-z0-9_\. ];', function($m) use ($escape) { + return $escape . dechex(ord($m[0])); + }, $key); + + return strlen($r) >= $maxLen ? $escape . md5($key) : $r; + } + } diff --git a/civicrm/CRM/Core/BAO/ConfigSetting.php b/civicrm/CRM/Core/BAO/ConfigSetting.php index 8cc765328614251e238f1584ea598fd56b2c3fcf..bb5340bbec4ab108cc6bd7687128cfe81121e25d 100644 --- a/civicrm/CRM/Core/BAO/ConfigSetting.php +++ b/civicrm/CRM/Core/BAO/ConfigSetting.php @@ -260,6 +260,7 @@ class CRM_Core_BAO_ConfigSetting { // clear all caches CRM_Core_Config::clearDBCache(); + Civi::cache('session')->clear(); $moveStatus .= ts('Database cache tables cleared.') . '<br />'; $resetSessionTable = CRM_Utils_Request::retrieve('resetSessionTable', diff --git a/civicrm/CRM/Core/BAO/CustomField.php b/civicrm/CRM/Core/BAO/CustomField.php index 41a71643196b6df7343e8832a16d766424229f1c..554eb94dc9ea141e0178b67a996f8da24a37452a 100644 --- a/civicrm/CRM/Core/BAO/CustomField.php +++ b/civicrm/CRM/Core/BAO/CustomField.php @@ -83,6 +83,30 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { return self::$_dataType; } + /** + * Build the map of custom field's data types and there respective Util type + * + * @return array + * Data data-type => CRM_Utils_Type + */ + public static function dataToType() { + return [ + 'String' => CRM_Utils_Type::T_STRING, + 'Int' => CRM_Utils_Type::T_INT, + 'Money' => CRM_Utils_Type::T_MONEY, + 'Memo' => CRM_Utils_Type::T_LONGTEXT, + 'Float' => CRM_Utils_Type::T_FLOAT, + 'Date' => CRM_Utils_Type::T_DATE, + 'DateTime' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, + 'Boolean' => CRM_Utils_Type::T_BOOLEAN, + 'StateProvince' => CRM_Utils_Type::T_INT, + 'File' => CRM_Utils_Type::T_STRING, + 'Link' => CRM_Utils_Type::T_STRING, + 'ContactReference' => CRM_Utils_Type::T_INT, + 'Country' => CRM_Utils_Type::T_INT, + ]; + } + /** * Get data to html array. * @@ -691,6 +715,7 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { $regexp = preg_replace('/[.,;:!?]/', '', CRM_Utils_Array::value(0, $values)); $importableFields[$key] = array( 'name' => $key, + 'type' => CRM_Utils_Array::value(CRM_Utils_Array::value('data_type', $values), self::dataToType()), 'title' => CRM_Utils_Array::value('label', $values), 'headerPattern' => '/' . preg_quote($regexp, '/') . '/', 'import' => 1, @@ -2189,46 +2214,45 @@ ORDER BY html_type"; } /** + * Get custom field ID from field/group name/title. * - */ - - /** - * Get custom field ID. - * - * @param string $fieldLabel - * @param null $groupTitle + * @param string $fieldName Field name or label + * @param string|null $groupTitle (Optional) Group name or label + * @param bool $fullString Whether to return "custom_123" or "123" * - * @return int|null + * @return string|int|null + * @throws \CiviCRM_API3_Exception */ - public static function getCustomFieldID($fieldLabel, $groupTitle = NULL) { - $params = array(1 => array($fieldLabel, 'String')); - if ($groupTitle) { - $params[2] = array($groupTitle, 'String'); - $sql = " -SELECT f.id -FROM civicrm_custom_field f -INNER JOIN civicrm_custom_group g ON f.custom_group_id = g.id -WHERE ( f.label = %1 OR f.name = %1 ) -AND ( g.title = %2 OR g.name = %2 ) -"; - } - else { - $sql = " -SELECT f.id -FROM civicrm_custom_field f -WHERE ( f.label = %1 OR f.name = %1 ) -"; - } + public static function getCustomFieldID($fieldName, $groupTitle = NULL, $fullString = FALSE) { + if (!isset(Civi::$statics['CRM_Core_BAO_CustomField'][$fieldName])) { + $customFieldParams = [ + 'name' => $fieldName, + 'label' => $fieldName, + 'options' => ['or' => [["name", "label"]]], + ]; + + if ($groupTitle) { + $customFieldParams['custom_group_id.name'] = $groupTitle; + $customFieldParams['custom_group_id.title'] = $groupTitle; + $customFieldParams['options'] = ['or' => [["name", "label"], ["custom_group_id.name", "custom_group_id.title"]]]; + } - $dao = CRM_Core_DAO::executeQuery($sql, $params); - if ($dao->fetch() && - $dao->N == 1 - ) { - return $dao->id; + $field = civicrm_api3('CustomField', 'get', $customFieldParams); + + if (empty($field['id'])) { + Civi::$statics['CRM_Core_BAO_CustomField'][$fieldName]['id'] = NULL; + Civi::$statics['CRM_Core_BAO_CustomField'][$fieldName]['string'] = NULL; + } + else { + Civi::$statics['CRM_Core_BAO_CustomField'][$fieldName]['id'] = $field['id']; + Civi::$statics['CRM_Core_BAO_CustomField'][$fieldName]['string'] = 'custom_' . $field['id']; + } } - else { - return NULL; + + if ($fullString) { + return Civi::$statics['CRM_Core_BAO_CustomField'][$fieldName]['string']; } + return Civi::$statics['CRM_Core_BAO_CustomField'][$fieldName]['id']; } /** diff --git a/civicrm/CRM/Core/BAO/CustomQuery.php b/civicrm/CRM/Core/BAO/CustomQuery.php index bc082255a3d635cf49569f27e46f709f6ccc7b4b..b71acb1081ce6a18c7e4400587eb1d6dc3934fb9 100644 --- a/civicrm/CRM/Core/BAO/CustomQuery.php +++ b/civicrm/CRM/Core/BAO/CustomQuery.php @@ -433,7 +433,7 @@ SELECT f.id, f.label, f.data_type, break; case 'Date': - $this->_where[$grouping][] = CRM_Contact_BAO_Query::buildClause($fieldName, $op, $value, 'String'); + $this->_where[$grouping][] = CRM_Contact_BAO_Query::buildClause($fieldName, $op, $value, 'Date'); list($qillOp, $qillVal) = CRM_Contact_BAO_Query::buildQillForFieldValue(NULL, $field['label'], $value, $op, array(), CRM_Utils_Type::T_DATE); $this->_qill[$grouping][] = "{$field['label']} $qillOp '$qillVal'"; break; diff --git a/civicrm/CRM/Core/BAO/CustomValue.php b/civicrm/CRM/Core/BAO/CustomValue.php index 516eb9fe6dc01a656070b05c34659d261bd45831..ce72448de28a950fde4af63e24ca2320e4778895 100644 --- a/civicrm/CRM/Core/BAO/CustomValue.php +++ b/civicrm/CRM/Core/BAO/CustomValue.php @@ -207,13 +207,18 @@ class CRM_Core_BAO_CustomValue extends CRM_Core_DAO { // first we need to find custom value table, from custom group ID $tableName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupID, 'table_name'); + // Retrieve the $entityId so we can pass that to the hook. + $entityID = CRM_Core_DAO::singleValueQuery("SELECT entity_id FROM {$tableName} WHERE id = %1", array( + 1 => array($customValueID, 'Integer'), + )); + // delete custom value from corresponding custom value table $sql = "DELETE FROM {$tableName} WHERE id = {$customValueID}"; CRM_Core_DAO::executeQuery($sql); CRM_Utils_Hook::custom('delete', $customGroupID, - NULL, + $entityID, $customValueID ); } diff --git a/civicrm/CRM/Core/BAO/Domain.php b/civicrm/CRM/Core/BAO/Domain.php index c3a9094d6f44f3e7d4c58303a5a410517adb1234..58a06e16d1620700b22fa986078b05349ac92c32 100644 --- a/civicrm/CRM/Core/BAO/Domain.php +++ b/civicrm/CRM/Core/BAO/Domain.php @@ -319,4 +319,12 @@ class CRM_Core_BAO_Domain extends CRM_Core_DAO_Domain { return array($userName, $userEmail); } + /** + * Get address to be used for system from addresses when a reply is not expected. + */ + public static function getNoReplyEmailAddress() { + $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain(); + return "do-not-reply@$emailDomain"; + } + } diff --git a/civicrm/CRM/Core/BAO/Job.php b/civicrm/CRM/Core/BAO/Job.php index fcb749a2c791536af995ca76848ee0bd7a54dc6f..a67ffaa5b07f852b00472ad51a2d54f9b1b7680f 100644 --- a/civicrm/CRM/Core/BAO/Job.php +++ b/civicrm/CRM/Core/BAO/Job.php @@ -146,4 +146,25 @@ class CRM_Core_BAO_Job extends CRM_Core_DAO_Job { CRM_Core_DAO::executeQuery($query); } + /** + * Make a copy of a Job. + * + * @param int $id The job id to copy. + * + * @return CRM_Core_DAO + */ + public static function copy($id, $params = array()) { + $fieldsFix = array( + 'suffix' => array( + 'name' => ' - ' . ts('Copy'), + ), + 'replace' => $params, + ); + $copy = &CRM_Core_DAO::copyGeneric('CRM_Core_DAO_Job', array('id' => $id), NULL, $fieldsFix); + $copy->save(); + CRM_Utils_Hook::copy('Job', $copy); + + return $copy; + } + } diff --git a/civicrm/CRM/Core/BAO/OptionGroup.php b/civicrm/CRM/Core/BAO/OptionGroup.php index 85b2e5203de0b00ba48c7caa1bb3fc423e05ddb9..fdc415b5cab5df557d057de5d33c57f22310d6d0 100644 --- a/civicrm/CRM/Core/BAO/OptionGroup.php +++ b/civicrm/CRM/Core/BAO/OptionGroup.php @@ -93,17 +93,11 @@ class CRM_Core_BAO_OptionGroup extends CRM_Core_DAO_OptionGroup { } $params['is_active'] = CRM_Utils_Array::value('is_active', $params, FALSE); - $params['is_default'] = CRM_Utils_Array::value('is_default', $params, FALSE); // action is taken depending upon the mode $optionGroup = new CRM_Core_DAO_OptionGroup(); $optionGroup->copyValues($params);; - if ($params['is_default']) { - $query = "UPDATE civicrm_option_group SET is_default = 0"; - CRM_Core_DAO::executeQuery($query); - } - $optionGroup->save(); return $optionGroup; } diff --git a/civicrm/CRM/Core/BAO/OptionValue.php b/civicrm/CRM/Core/BAO/OptionValue.php index 2e621cf073ccafcb09c2e2c3fd48dc911db761d1..cfb4b45dce48838c9d9b53720e6db4b26fd80fc7 100644 --- a/civicrm/CRM/Core/BAO/OptionValue.php +++ b/civicrm/CRM/Core/BAO/OptionValue.php @@ -547,16 +547,23 @@ class CRM_Core_BAO_OptionValue extends CRM_Core_DAO_OptionValue { * that an option value exists, without hitting an error if it already exists. * * This is sympathetic to sites who might pre-add it. + * + * @param array $params the option value attributes. + * @return array the option value attributes. */ public static function ensureOptionValueExists($params) { - $existingValues = civicrm_api3('OptionValue', 'get', array( + $result = civicrm_api3('OptionValue', 'get', array( 'option_group_id' => $params['option_group_id'], 'name' => $params['name'], - 'return' => 'id', + 'return' => ['id', 'value'], + 'sequential' => 1, )); - if (!$existingValues['count']) { - civicrm_api3('OptionValue', 'create', $params); + + if (!$result['count']) { + $result = civicrm_api3('OptionValue', 'create', $params); } + + return CRM_Utils_Array::first($result['values']); } } diff --git a/civicrm/CRM/Core/BAO/RecurringEntity.php b/civicrm/CRM/Core/BAO/RecurringEntity.php index fb73a78cf2cf1c21296de29ec276a7d32ceec975..a5ab757b7b8cecd8eaadb42abf84c7463f5cddc5 100644 --- a/civicrm/CRM/Core/BAO/RecurringEntity.php +++ b/civicrm/CRM/Core/BAO/RecurringEntity.php @@ -660,7 +660,6 @@ class CRM_Core_BAO_RecurringEntity extends CRM_Core_DAO_RecurringEntity { } $updateDAO = CRM_Core_DAO::cascadeUpdate($daoName, $obj->id, $entityID, $skipData); - CRM_Core_DAO::freeResult(); } else { CRM_Core_Error::fatal("DAO Mapper missing for $entityTable."); diff --git a/civicrm/CRM/Core/Config.php b/civicrm/CRM/Core/Config.php index 349c672b83de8990f98e1ea3dffeea1908eb7951..265532c67d8b07d711c30925656f9a47682acf97 100644 --- a/civicrm/CRM/Core/Config.php +++ b/civicrm/CRM/Core/Config.php @@ -295,6 +295,7 @@ class CRM_Core_Config extends CRM_Core_Config_MagicMerge { // clear all caches self::clearDBCache(); + Civi::cache('session')->clear(); CRM_Utils_System::flushCache(); if ($sessionReset) { @@ -356,7 +357,6 @@ class CRM_Core_Config extends CRM_Core_Config_MagicMerge { 'TRUNCATE TABLE civicrm_group_contact_cache', 'TRUNCATE TABLE civicrm_menu', 'UPDATE civicrm_setting SET value = NULL WHERE name="navigation" AND contact_id IS NOT NULL', - 'DELETE FROM civicrm_setting WHERE name="modulePaths"', // CRM-10543 ); foreach ($queries as $query) { @@ -389,11 +389,12 @@ class CRM_Core_Config extends CRM_Core_Config_MagicMerge { WHERE TABLE_SCHEMA = %1 AND ( TABLE_NAME LIKE 'civicrm_import_job_%' - OR TABLE_NAME LIKE 'civicrm_export_temp%' - OR TABLE_NAME LIKE 'civicrm_task_action_temp%' OR TABLE_NAME LIKE 'civicrm_report_temp%' + OR TABLE_NAME LIKE 'civicrm_tmp_d%' ) "; + // NOTE: Cannot find use-cases where "civicrm_report_temp" would be durable. Could probably remove. + if ($timeInterval) { $query .= " AND CREATE_TIME < DATE_SUB(NOW(), INTERVAL {$timeInterval})"; } diff --git a/civicrm/CRM/Core/DAO.php b/civicrm/CRM/Core/DAO.php index 6df94eb19ad1694f07d99ca3eae790d7a825cb44..b4d07e75adcc492ef031b68b10ec5142603a7104 100644 --- a/civicrm/CRM/Core/DAO.php +++ b/civicrm/CRM/Core/DAO.php @@ -48,6 +48,13 @@ require_once 'CRM/Core/I18n.php'; */ class CRM_Core_DAO extends DB_DataObject { + /** + * How many times has this instance been cloned. + * + * @var int + */ + protected $resultCopies = 0; + /** * @var null * @deprecated @@ -119,6 +126,22 @@ class CRM_Core_DAO extends DB_DataObject { $this->__table = $this->getTableName(); } + public function __clone() { + if (!empty($this->_DB_resultid)) { + $this->resultCopies++; + } + } + + /** + * Class destructor. + */ + public function __destruct() { + if ($this->resultCopies === 0) { + $this->free(); + } + $this->resultCopies--; + } + /** * Empty definition for virtual function. */ @@ -1995,6 +2018,8 @@ SELECT contact_id * @param null $string * * @return string + * @deprecated + * @see CRM_Utils_SQL_TempTable */ public static function createTempTableName($prefix = 'civicrm', $addRandomString = TRUE, $string = NULL) { $tableName = $prefix . "_temp"; @@ -2243,6 +2268,54 @@ SELECT contact_id return $refsFound; } + /** + * Get all references to contact table. + * + * This includes core tables, custom group tables, tables added by the merge + * hook and the entity_tag table. + * + * Refer to CRM-17454 for information on the danger of querying the information + * schema to derive this. + */ + public static function getReferencesToContactTable() { + if (isset(\Civi::$statics[__CLASS__]) && isset(\Civi::$statics[__CLASS__]['contact_references'])) { + return \Civi::$statics[__CLASS__]['contact_references']; + } + $contactReferences = []; + $coreReferences = CRM_Core_DAO::getReferencesToTable('civicrm_contact'); + foreach ($coreReferences as $coreReference) { + if (!is_a($coreReference, 'CRM_Core_Reference_Dynamic')) { + $contactReferences[$coreReference->getReferenceTable()][] = $coreReference->getReferenceKey(); + } + } + self::appendCustomTablesExtendingContacts($contactReferences); + + // FixME for time being adding below line statically as no Foreign key constraint defined for table 'civicrm_entity_tag' + $contactReferences['civicrm_entity_tag'][] = 'entity_id'; + \Civi::$statics[__CLASS__]['contact_references'] = $contactReferences; + return \Civi::$statics[__CLASS__]['contact_references']; + } + + /** + * Add custom tables that extend contacts to the list of contact references. + * + * CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity seems like a safe-ish + * function to be sure all are retrieved & we don't miss subtypes or inactive or multiples + * - the down side is it is not cached. + * + * Further changes should be include tests in the CRM_Core_MergerTest class + * to ensure that disabled, subtype, multiple etc groups are still captured. + * + * @param array $cidRefs + */ + public static function appendCustomTablesExtendingContacts(&$cidRefs) { + $customValueTables = CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity('Contact'); + $customValueTables->find(); + while ($customValueTables->fetch()) { + $cidRefs[$customValueTables->table_name] = array('entity_id'); + } + } + /** * Lookup the value of a MySQL global configuration variable. * diff --git a/civicrm/CRM/Core/DAO/CustomField.php b/civicrm/CRM/Core/DAO/CustomField.php index 1f48b8858fb3a3370804f161dc5dc76ef471e021..24b86133fe260882d83008ba0271b36f1feb0896 100644 --- a/civicrm/CRM/Core/DAO/CustomField.php +++ b/civicrm/CRM/Core/DAO/CustomField.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Core/CustomField.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:dcb494bf3990ce63b66ef13ee47a2d15) + * (GenCodeChecksum:7f096c92af68ef9564675e3d708fbbe1) */ /** @@ -594,6 +594,11 @@ class CRM_Core_DAO_CustomField extends CRM_Core_DAO { 'entity' => 'CustomField', 'bao' => 'CRM_Core_BAO_CustomField', 'localizable' => 0, + 'pseudoconstant' => [ + 'table' => 'civicrm_option_group', + 'keyColumn' => 'id', + 'labelColumn' => 'title', + ] ], 'filter' => [ 'name' => 'filter', diff --git a/civicrm/CRM/Core/DAO/UFGroup.php b/civicrm/CRM/Core/DAO/UFGroup.php index c0b5d43b477e224aa73e76e9d0eb439f28e0fca0..53e1c18927dff6a9e826239e64fa5bfcb2a3056f 100644 --- a/civicrm/CRM/Core/DAO/UFGroup.php +++ b/civicrm/CRM/Core/DAO/UFGroup.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Core/UFGroup.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:a6776694df1be240b3f7be798792175d) + * (GenCodeChecksum:a48f9522d0bd2e1d485064ebfc66f9a2) */ /** @@ -208,6 +208,13 @@ class CRM_Core_DAO_UFGroup extends CRM_Core_DAO { */ public $submit_button_text; + /** + * Should a Cancel button be included in this Profile form. + * + * @var boolean + */ + public $add_cancel_button; + /** * Class constructor. */ @@ -545,6 +552,17 @@ class CRM_Core_DAO_UFGroup extends CRM_Core_DAO { 'bao' => 'CRM_Core_BAO_UFGroup', 'localizable' => 1, ], + 'add_cancel_button' => [ + 'name' => 'add_cancel_button', + 'type' => CRM_Utils_Type::T_BOOLEAN, + 'title' => ts('Include Cancel Button'), + 'description' => 'Should a Cancel button be included in this Profile form.', + 'default' => '1', + 'table_name' => 'civicrm_uf_group', + 'entity' => 'UFGroup', + 'bao' => 'CRM_Core_BAO_UFGroup', + 'localizable' => 0, + ], ]; CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } diff --git a/civicrm/CRM/Core/Form.php b/civicrm/CRM/Core/Form.php index 113411d7ff83c9ddb81115c849b3147f2a9d748f..abfbcdfcd44236ebd000ba53e94106db9beea29a 100644 --- a/civicrm/CRM/Core/Form.php +++ b/civicrm/CRM/Core/Form.php @@ -179,6 +179,29 @@ class CRM_Core_Form extends HTML_QuickForm_Page { */ public $urlPath = array(); + /** + * Context of the form being loaded. + * + * 'event' or null + * + * @var string + */ + protected $context; + + /** + * @return string + */ + public function getContext() { + return $this->context; + } + + /** + * Set context variable. + */ + public function setContext() { + $this->context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this); + } + /** * @var CRM_Core_Controller */ @@ -1845,7 +1868,7 @@ class CRM_Core_Form extends HTML_QuickForm_Page { $setDefaultCurrency = TRUE ) { $currencies = CRM_Core_OptionGroup::values('currencies_enabled'); - if (!array_key_exists($defaultCurrency, $currencies)) { + if (!empty($defaultCurrency) && !array_key_exists($defaultCurrency, $currencies)) { Civi::log()->warning('addCurrency: Currency ' . $defaultCurrency . ' is disabled but still in use!'); $currencies[$defaultCurrency] = $defaultCurrency; } diff --git a/civicrm/CRM/Core/Form/Task.php b/civicrm/CRM/Core/Form/Task.php index 2a487ba70f1d0f705e68d85e208ea7884508dd4e..7eea0555c5902e9bb8757f5ed33c0a92744ce129 100644 --- a/civicrm/CRM/Core/Form/Task.php +++ b/civicrm/CRM/Core/Form/Task.php @@ -56,6 +56,11 @@ abstract class CRM_Core_Form_Task extends CRM_Core_Form { */ protected $_componentIds; + /** + * @var int + */ + protected $queryMode; + /** * The array that holds all the case ids * @@ -70,9 +75,18 @@ abstract class CRM_Core_Form_Task extends CRM_Core_Form { */ public $_contactIds; - // Must be set to entity table name (eg. civicrm_participant) by child class + /** + * Must be set to entity table name (eg. civicrm_participant) by child class + * + * @var string + */ static $tableName = NULL; - // Must be set to entity shortname (eg. event) + + /** + * Must be set to entity shortname (eg. event) + * + * @var string + */ static $entityShortname = NULL; /** @@ -87,26 +101,25 @@ abstract class CRM_Core_Form_Task extends CRM_Core_Form { /** * Common pre-processing function. * - * @param CRM_Core_Form $form - * @param bool $useTable FIXME This parameter could probably be deprecated as it's not used here + * @param CRM_Core_Form_Task $form * * @throws \CRM_Core_Exception */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $form->_entityIds = array(); - $values = $form->controller->exportValues($form->get('searchFormName')); + $searchFormValues = $form->controller->exportValues($form->get('searchFormName')); - $form->_task = $values['task']; + $form->_task = $searchFormValues['task']; $className = 'CRM_' . ucfirst($form::$entityShortname) . '_Task'; $entityTasks = $className::tasks(); $form->assign('taskName', $entityTasks[$form->_task]); - $ids = array(); - if ($values['radio_ts'] == 'ts_sel') { - foreach ($values as $name => $value) { + $entityIds = array(); + if ($searchFormValues['radio_ts'] == 'ts_sel') { + foreach ($searchFormValues as $name => $value) { if (substr($name, 0, CRM_Core_Form::CB_PREFIX_LEN) == CRM_Core_Form::CB_PREFIX) { - $ids[] = substr($name, CRM_Core_Form::CB_PREFIX_LEN); + $entityIds[] = substr($name, CRM_Core_Form::CB_PREFIX_LEN); } } } @@ -117,24 +130,22 @@ abstract class CRM_Core_Form_Task extends CRM_Core_Form { $sortOrder = $form->get(CRM_Utils_Sort::SORT_ORDER); } - $query = new CRM_Contact_BAO_Query($queryParams, NULL, NULL, FALSE, FALSE, - CRM_Contact_BAO_Query::MODE_CASE - ); + $query = new CRM_Contact_BAO_Query($queryParams, NULL, NULL, FALSE, FALSE, $form->getQueryMode()); $query->_distinctComponentClause = " ( " . $form::$tableName . ".id )"; $query->_groupByComponentClause = " GROUP BY " . $form::$tableName . ".id "; $result = $query->searchQuery(0, 0, $sortOrder); $selector = $form::$entityShortname . '_id'; while ($result->fetch()) { - $ids[] = $result->$selector; + $entityIds[] = $result->$selector; } } - if (!empty($ids)) { - $form->_componentClause = ' ' . $form::$tableName . '.id IN ( ' . implode(',', $ids) . ' ) '; - $form->assign('totalSelected' . ucfirst($form::$entityShortname) . 's', count($ids)); + if (!empty($entityIds)) { + $form->_componentClause = ' ' . $form::$tableName . '.id IN ( ' . implode(',', $entityIds) . ' ) '; + $form->assign('totalSelected' . ucfirst($form::$entityShortname) . 's', count($entityIds)); } - $form->_entityIds = $form->_componentIds = $ids; + $form->_entityIds = $form->_componentIds = $entityIds; // Some functions (eg. PDF letter tokens) rely on Ids being in specific fields rather than the generic $form->_entityIds // So we set that specific field here (eg. for cases $form->_caseIds = $form->_entityIds). @@ -197,4 +208,14 @@ abstract class CRM_Core_Form_Task extends CRM_Core_Form { ); } + /** + * Get the query mode (eg. CRM_Core_BAO_Query::MODE_CASE) + * Should be overridden by child classes in most cases + * + * @return int + */ + public function getQueryMode() { + return $this->queryMode ?: CRM_Contact_BAO_Query::MODE_CONTACTS; + } + } diff --git a/civicrm/CRM/Core/Invoke.php b/civicrm/CRM/Core/Invoke.php index e5edcd82c95608fe0fc71d00580392a0f92d412c..eb2bef51af7f0f6c715323aa9c702aa9e8e97924 100644 --- a/civicrm/CRM/Core/Invoke.php +++ b/civicrm/CRM/Core/Invoke.php @@ -350,7 +350,7 @@ class CRM_Core_Invoke { return; } // always use cached results - they will be refreshed by the session timer - $status = Civi::settings()->get('systemStatusCheckResult'); + $status = Civi::cache('checks')->get('systemStatusCheckResult'); $template->assign('footer_status_severity', $status); $template->assign('footer_status_message', CRM_Utils_Check::toStatusLabel($status)); } diff --git a/civicrm/CRM/Core/JobManager.php b/civicrm/CRM/Core/JobManager.php index e7008ea64517f7801bb4b467af2d018d28afc536..fccfc18b8cf804567359ba4cf3eb57704a942124 100644 --- a/civicrm/CRM/Core/JobManager.php +++ b/civicrm/CRM/Core/JobManager.php @@ -141,12 +141,15 @@ class CRM_Core_JobManager { $params = $job->apiParams; } + CRM_Utils_Hook::preJob($job, $params); try { $result = civicrm_api($job->api_entity, $job->api_action, $params); } catch (Exception$e) { $this->logEntry('Error while executing ' . $job->name . ': ' . $e->getMessage()); + $result = $e; } + CRM_Utils_Hook::postJob($job, $params, $result); $this->logEntry('Finished execution of ' . $job->name . ' with result: ' . $this->_apiResultToMessage($result)); $this->currentJob = FALSE; diff --git a/civicrm/CRM/Core/OptionGroup.php b/civicrm/CRM/Core/OptionGroup.php index bc1f7ad2ac8124e538fa01d5e940ed0392c58945..c7e07e67d613ce2af1cc6f7edcba0c43129229e5 100644 --- a/civicrm/CRM/Core/OptionGroup.php +++ b/civicrm/CRM/Core/OptionGroup.php @@ -200,8 +200,8 @@ WHERE v.option_group_id = g.id /** * @return string */ - protected static function createCacheKey() { - $cacheKey = "CRM_OG_" . serialize(func_get_args()); + protected static function createCacheKey($id) { + $cacheKey = "CRM_OG_" . preg_replace('/[^a-zA-Z0-9]/', '', $id) . '_' . md5(serialize(func_get_args())); return $cacheKey; } diff --git a/civicrm/CRM/Core/Payment.php b/civicrm/CRM/Core/Payment.php index 8626cd790de353f4170e1304d647c732da91b256..b686cd8501a36f70b3b0e90f1e6566abda1749be 100644 --- a/civicrm/CRM/Core/Payment.php +++ b/civicrm/CRM/Core/Payment.php @@ -682,7 +682,6 @@ abstract class CRM_Core_Payment { 'htmlType' => 'text', 'name' => 'credit_card_number', 'title' => ts('Card Number'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 20, @@ -695,7 +694,6 @@ abstract class CRM_Core_Payment { 'htmlType' => 'text', 'name' => 'cvv2', 'title' => ts('Security Code'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 5, 'maxlength' => 10, @@ -714,7 +712,6 @@ abstract class CRM_Core_Payment { 'htmlType' => 'date', 'name' => 'credit_card_exp_date', 'title' => ts('Expiration Date'), - 'cc_field' => TRUE, 'attributes' => CRM_Core_SelectValues::date('creditCard'), 'is_required' => TRUE, 'rules' => array( @@ -729,7 +726,6 @@ abstract class CRM_Core_Payment { 'htmlType' => 'select', 'name' => 'credit_card_type', 'title' => ts('Card Type'), - 'cc_field' => TRUE, 'attributes' => $creditCardType, 'is_required' => FALSE, ), @@ -737,7 +733,6 @@ abstract class CRM_Core_Payment { 'htmlType' => 'text', 'name' => 'account_holder', 'title' => ts('Account Holder'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 34, @@ -750,7 +745,6 @@ abstract class CRM_Core_Payment { 'htmlType' => 'text', 'name' => 'bank_account_number', 'title' => ts('Bank Account Number'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 34, @@ -770,7 +764,6 @@ abstract class CRM_Core_Payment { 'htmlType' => 'text', 'name' => 'bank_identification_number', 'title' => ts('Bank Identification Number'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 11, @@ -789,7 +782,6 @@ abstract class CRM_Core_Payment { 'htmlType' => 'text', 'name' => 'bank_name', 'title' => ts('Bank Name'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 64, @@ -803,7 +795,6 @@ abstract class CRM_Core_Payment { 'name' => 'check_number', 'title' => ts('Check Number'), 'is_required' => FALSE, - 'cc_field' => TRUE, 'attributes' => NULL, ), 'pan_truncation' => array( @@ -811,7 +802,6 @@ abstract class CRM_Core_Payment { 'name' => 'pan_truncation', 'title' => ts('Last 4 digits of the card'), 'is_required' => FALSE, - 'cc_field' => TRUE, 'attributes' => array( 'size' => 4, 'maxlength' => 4, @@ -826,6 +816,13 @@ abstract class CRM_Core_Payment { ), ), ), + 'payment_token' => array( + 'htmlType' => 'hidden', + 'name' => 'payment_token', + 'title' => ts('Authorization token'), + 'is_required' => FALSE, + 'attributes' => ['size' => 10, 'autocomplete' => 'off', 'id' => 'payment_token'], + ), ); } diff --git a/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php b/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php index 2696e2a1abb9f07e24f64c22ddddfd937b2c0224..e350195886a3e6f41581d6adecf392af68fbd8b9 100644 --- a/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php +++ b/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php @@ -77,7 +77,7 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN { // processor id & the handleNotification function (which should call the completetransaction api & by-pass this // entirely). The only thing the IPN class should really do is extract data from the request, validate it // & call completetransaction or call fail? (which may not exist yet). - Civi::log()->warning('Unreliable method used for AuthNet IPN - this will cause problems if you have more than one instance'); + Civi::log()->warning('Unreliable method used to get payment_processor_id for AuthNet IPN - this will cause problems if you have more than one instance'); $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'AuthNet', 'id', 'name' ); diff --git a/civicrm/CRM/Core/Payment/Form.php b/civicrm/CRM/Core/Payment/Form.php index 05de148e8248308ddb8c12d44a53c9b2c63b1a19..60e946d7dd0283edbad023982c0f62215ec8427c 100644 --- a/civicrm/CRM/Core/Payment/Form.php +++ b/civicrm/CRM/Core/Payment/Form.php @@ -68,8 +68,7 @@ class CRM_Core_Payment_Form { $processor['object']->setPaymentInstrumentID($paymentInstrumentID); $paymentTypeName = self::getPaymentTypeName($processor); $form->assign('paymentTypeName', $paymentTypeName); - $paymentTypeLabel = self::getPaymentTypeLabel($processor); - $form->assign('paymentTypeLabel', $paymentTypeLabel); + $form->assign('paymentTypeLabel', self::getPaymentLabel($processor['object'])); $form->assign('isBackOffice', $isBackOffice); $form->_paymentFields = $form->billingFieldSets[$paymentTypeName]['fields'] = self::getPaymentFieldMetadata($processor); $form->_paymentFields = array_merge($form->_paymentFields, self::getBillingAddressMetadata($processor, $form->_bltID)); @@ -116,24 +115,22 @@ class CRM_Core_Payment_Form { protected static function addCommonFields(&$form, $paymentFields) { $requiredPaymentFields = array(); foreach ($paymentFields as $name => $field) { - // @todo - remove the cc_field check - no longer useful. - if (!empty($field['cc_field'])) { - if ($field['htmlType'] == 'chainSelect') { - $form->addChainSelect($field['name'], array('required' => FALSE)); - } - else { - $form->add($field['htmlType'], - $field['name'], - $field['title'], - $field['attributes'], - FALSE - ); - } + if ($field['htmlType'] == 'chainSelect') { + $form->addChainSelect($field['name'], array('required' => FALSE)); + } + else { + $form->add($field['htmlType'], + $field['name'], + $field['title'], + $field['attributes'], + FALSE + ); } // This will cause the fields to be marked as required - but it is up to the payment processor to // validate it. $requiredPaymentFields[$field['name']] = $field['is_required']; } + $form->assign('requiredPaymentFields', $requiredPaymentFields); } @@ -207,7 +204,7 @@ class CRM_Core_Payment_Form { * @return string */ public static function getPaymentTypeLabel($paymentProcessor) { - return ts(($paymentProcessor['object']->getPaymentTypeLabel()) . ' Information'); + return ts('%1 Information', [$paymentProcessor->getPaymentTypeLabel()]); } /** @@ -426,4 +423,25 @@ class CRM_Core_Payment_Form { return CRM_Utils_Array::value('Y', $src['credit_card_exp_date']); } + /** + * Get the label for the processor. + * + * We do not use a label if there are no enterable fields. + * + * @param \CRM_Core_Payment $processor + * + * @return string + */ + public static function getPaymentLabel($processor) { + $isVisible = FALSE; + $paymentTypeLabel = self::getPaymentTypeLabel($processor); + foreach (self::getPaymentFieldMetadata(['object' => $processor]) as $paymentField) { + if ($paymentField['htmlType'] !== 'hidden') { + $isVisible = TRUE; + } + } + return $isVisible ? $paymentTypeLabel : ''; + + } + } diff --git a/civicrm/CRM/Core/Payment/PayPalIPN.php b/civicrm/CRM/Core/Payment/PayPalIPN.php index 0100ca27d54228aa2f5e125188904094e0907d8f..b058eb63f6c7e21aa6ce95bc1dac364904119d65 100644 --- a/civicrm/CRM/Core/Payment/PayPalIPN.php +++ b/civicrm/CRM/Core/Payment/PayPalIPN.php @@ -29,7 +29,6 @@ * * @package CRM * @copyright CiviCRM LLC (c) 2004-2018 - * $Id$ * */ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { @@ -52,7 +51,7 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { * @throws CRM_Core_Exception */ public function __construct($inputData) { - //CRM-19676 + // CRM-19676 $params = (!empty($inputData['custom'])) ? array_merge($inputData, json_decode($inputData['custom'], TRUE)) : $inputData; @@ -62,47 +61,46 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { /** * @param string $name - * @param $type + * @param string $type * @param bool $abort * * @return mixed + * @throws \CRM_Core_Exception */ public function retrieve($name, $type, $abort = TRUE) { - static $store = NULL; - $value = CRM_Utils_Type::validate( - CRM_Utils_Array::value($name, $this->_inputParameters), - $type, - FALSE - ); + $value = CRM_Utils_Type::validate(CRM_Utils_Array::value($name, $this->_inputParameters), $type, FALSE); if ($abort && $value === NULL) { - CRM_Core_Error::debug_log_message("Could not find an entry for $name"); + Civi::log()->debug("PayPalIPN: Could not find an entry for $name"); echo "Failure: Missing Parameter<p>" . CRM_Utils_Type::escape($name, 'String'); - exit(); + throw new CRM_Core_Exception("PayPalIPN: Could not find an entry for $name"); } return $value; } /** - * @param $input - * @param $ids - * @param $objects - * @param $first + * @param array $input + * @param array $ids + * @param array $objects + * @param bool $first + * + * @return void * - * @return bool + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception */ public function recur(&$input, &$ids, &$objects, $first) { if (!isset($input['txnType'])) { - CRM_Core_Error::debug_log_message("Could not find txn_type in input request"); + Civi::log()->debug('PayPalIPN: Could not find txn_type in input request'); echo "Failure: Invalid parameters<p>"; - return FALSE; + return; } if ($input['txnType'] == 'subscr_payment' && $input['paymentStatus'] != 'Completed' ) { - CRM_Core_Error::debug_log_message("Ignore all IPN payments that are not completed"); + Civi::log()->debug('PayPalIPN: Ignore all IPN payments that are not completed'); echo "Failure: Invalid parameters<p>"; - return FALSE; + return; } $recur = &$objects['contributionRecur']; @@ -110,9 +108,9 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { // make sure the invoice ids match // make sure the invoice is valid and matches what we have in the contribution record if ($recur->invoice_id != $input['invoice']) { - CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request"); + Civi::log()->debug('PayPalIPN: Invoice values dont match between database and IPN request (RecurID: ' . $recur->id . ').'); echo "Failure: Invoice values dont match between database and IPN request<p>"; - return FALSE; + return; } $now = date('YmdHis'); @@ -127,18 +125,19 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { } $sendNotification = FALSE; $subscriptionPaymentStatus = NULL; - //set transaction type + // set transaction type $txnType = $this->retrieve('txn_type', 'String'); + $contributionStatuses = array_flip(CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'validate')); switch ($txnType) { case 'subscr_signup': $recur->create_date = $now; - //some times subscr_signup response come after the - //subscr_payment and set to pending mode. + // sometimes subscr_signup response come after the subscr_payment and set to pending mode. + $statusID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_ContributionRecur', $recur->id, 'contribution_status_id' ); - if ($statusID != 5) { - $recur->contribution_status_id = 2; + if ($statusID != $contributionStatuses['In Progress']) { + $recur->contribution_status_id = $contributionStatuses['Pending']; } $recur->processor_id = $this->retrieve('subscr_id', 'String'); $recur->trxn_id = $recur->processor_id; @@ -147,8 +146,8 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { break; case 'subscr_eot': - if ($recur->contribution_status_id != 3) { - $recur->contribution_status_id = 1; + if ($recur->contribution_status_id != $contributionStatuses['Cancelled']) { + $recur->contribution_status_id = $contributionStatuses['Completed']; } $recur->end_date = $now; $sendNotification = TRUE; @@ -156,19 +155,19 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { break; case 'subscr_cancel': - $recur->contribution_status_id = 3; + $recur->contribution_status_id = $contributionStatuses['Cancelled']; $recur->cancel_date = $now; break; case 'subscr_failed': - $recur->contribution_status_id = 4; + $recur->contribution_status_id = $contributionStatuses['Failed']; $recur->modified_date = $now; break; case 'subscr_modify': - CRM_Core_Error::debug_log_message("We do not handle modifications to subscriptions right now"); + Civi::log()->debug('PayPalIPN: We do not handle modifications to subscriptions right now (RecurID: ' . $recur->id . ').'); echo "Failure: We do not handle modifications to subscriptions right now<p>"; - return FALSE; + return; case 'subscr_payment': if ($first) { @@ -180,8 +179,8 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { // make sure the contribution status is not done // since order of ipn's is unknown - if ($recur->contribution_status_id != 1) { - $recur->contribution_status_id = 5; + if ($recur->contribution_status_id != $contributionStatuses['Completed']) { + $recur->contribution_status_id = $contributionStatuses['In Progress']; } break; } @@ -189,7 +188,6 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { $recur->save(); if ($sendNotification) { - $autoRenewMembership = FALSE; if ($recur->id && isset($ids['membership']) && $ids['membership'] @@ -211,14 +209,18 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { } if (!$first) { - //check if this contribution transaction is already processed - //if not create a contribution and then get it processed + // check if this contribution transaction is already processed + // if not create a contribution and then get it processed $contribution = new CRM_Contribute_BAO_Contribution(); $contribution->trxn_id = $input['trxn_id']; if ($contribution->trxn_id && $contribution->find()) { - CRM_Core_Error::debug_log_message("returning since contribution has already been handled"); + Civi::log()->debug('PayPalIPN: Returning since contribution has already been handled (trxn_id: ' . $contribution->trxn_id . ')'); echo "Success: Contribution has already been handled<p>"; - return TRUE; + return; + } + + if ($input['paymentStatus'] != 'Completed') { + throw new CRM_Core_Exception("Ignore all IPN payments that are not completed"); } $contribution->contact_id = $ids['contact']; @@ -240,27 +242,23 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { } /** - * @param $input - * @param $ids - * @param $objects + * @param array $input + * @param array $ids + * @param array $objects * @param bool $recur * @param bool $first * - * @return bool + * @return void */ - public function single( - &$input, &$ids, &$objects, - $recur = FALSE, - $first = FALSE - ) { + public function single(&$input, &$ids, &$objects, $recur = FALSE, $first = FALSE) { $contribution = &$objects['contribution']; // make sure the invoice is valid and matches what we have in the contribution record if ((!$recur) || ($recur && $first)) { if ($contribution->invoice_id != $input['invoice']) { - CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request"); + Civi::log()->debug('PayPalIPN: Invoice values dont match between database and IPN request. (ID: ' . $contribution->id . ').'); echo "Failure: Invoice values dont match between database and IPN request<p>"; - return FALSE; + return; } } else { @@ -269,9 +267,9 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { if (!$recur) { if ($contribution->total_amount != $input['amount']) { - CRM_Core_Error::debug_log_message("Amount values dont match between database and IPN request"); + Civi::log()->debug('PayPalIPN: Amount values dont match between database and IPN request. (ID: ' . $contribution->id . ').'); echo "Failure: Amount values dont match between database and IPN request<p>"; - return FALSE; + return; } } else { @@ -280,9 +278,6 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { $transaction = new CRM_Core_Transaction(); - $participant = &$objects['participant']; - $membership = &$objects['membership']; - $status = $input['paymentStatus']; if ($status == 'Denied' || $status == 'Failed' || $status == 'Voided') { return $this->failed($objects, $transaction); @@ -298,11 +293,12 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { } // check if contribution is already completed, if so we ignore this ipn - if ($contribution->contribution_status_id == 1) { + $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($contribution->contribution_status_id == $completedStatusId) { $transaction->commit(); - CRM_Core_Error::debug_log_message("returning since contribution has already been handled"); + Civi::log()->debug('PayPalIPN: Returning since contribution has already been handled. (ID: ' . $contribution->id . ').'); echo "Success: Contribution has already been handled<p>"; - return TRUE; + return; } $this->completeTransaction($input, $ids, $objects, $transaction, $recur); @@ -311,10 +307,10 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { /** * Main function. * - * @return bool + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception */ public function main() { - $objects = $ids = $input = array(); $component = $this->retrieve('module', 'String'); $input['component'] = $component; @@ -361,29 +357,26 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { if ($ids['contributionRecur']) { // check if first contribution is completed, else complete first contribution $first = TRUE; - if ($objects['contribution']->contribution_status_id == 1) { + $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($objects['contribution']->contribution_status_id == $completedStatusId) { $first = FALSE; } - return $this->recur($input, $ids, $objects, $first); - } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); + $this->recur($input, $ids, $objects, $first); + return; } } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); - } + $this->single($input, $ids, $objects, FALSE, FALSE); } /** - * @param $input - * @param $ids + * @param array $input + * @param array $ids * - * @return bool + * @throws \CRM_Core_Exception */ public function getInput(&$input, &$ids) { if (!$this->getBillingID($ids)) { - return FALSE; + return; } $input['txnType'] = $this->retrieve('txn_type', 'String', FALSE); diff --git a/civicrm/CRM/Core/Payment/PayPalImpl.php b/civicrm/CRM/Core/Payment/PayPalImpl.php index 3fa006689248a7883b6a9ed5d3a12c6a950be223..8ab1bbec3a72056eb575540aabfd24a98813578a 100644 --- a/civicrm/CRM/Core/Payment/PayPalImpl.php +++ b/civicrm/CRM/Core/Payment/PayPalImpl.php @@ -412,7 +412,6 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment { */ public function createRecurringPayments(&$params) { $args = array(); - // @todo this function is riddled with enotices - perhaps use $this->mapPaypalParamsToCivicrmParams($fieldMap, $result) $this->initialize($args, 'CreateRecurringPaymentsProfile'); $start_time = strtotime(date('m/d/Y')); @@ -424,15 +423,12 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment { $args['currencyCode'] = $params['currencyID']; $args['payerID'] = $params['payer_id']; $args['invnum'] = $params['invoiceID']; - $args['returnURL'] = $params['returnURL']; - $args['cancelURL'] = $params['cancelURL']; $args['profilestartdate'] = $start_date; $args['method'] = 'CreateRecurringPaymentsProfile'; $args['billingfrequency'] = $params['frequency_interval']; $args['billingperiod'] = ucwords($params['frequency_unit']); $args['desc'] = $params['amount'] . " Per " . $params['frequency_interval'] . " " . $params['frequency_unit']; - //$args['desc'] = 'Recurring Contribution'; - $args['totalbillingcycles'] = $params['installments']; + $args['totalbillingcycles'] = CRM_Utils_Array::value('installments', $params); $args['version'] = '56.0'; $args['profilereference'] = "i={$params['invoiceID']}" . "&m=" . @@ -450,16 +446,18 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment { return $result; } - /* Success */ - $params['trxn_id'] = $result['transactionid']; - $params['gross_amount'] = $result['amt']; - $params['fee_amount'] = $result['feeamt']; - $params['net_amount'] = $result['settleamt']; - if ($params['net_amount'] == 0 && $params['fee_amount'] != 0) { - $params['net_amount'] = number_format(($params['gross_amount'] - $params['fee_amount']), 2); - } - $params['payment_status'] = $result['paymentstatus']; - $params['pending_reason'] = $result['pendingreason']; + /* Success - result looks like" + * array ( + * 'profileid' => 'I-CP1U0PLG91R2', + * 'profilestatus' => 'ActiveProfile', + * 'timestamp' => '2018-05-07T03:55:52Z', + * 'correlationid' => 'e717999e9bf62', + * 'ack' => 'Success', + * 'version' => '56.0', + * 'build' => '39949200',) + */ + $params['trxn_id'] = $result['profileid']; + $params['payment_status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'); return $params; } @@ -721,7 +719,7 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment { * @throws \Civi\Payment\Exception\PaymentProcessorException */ public function cancelSubscription(&$message = '', $params = array()) { - if ($this->isPayPalType($this::PAYPAL_PRO)) { + if ($this->isPayPalType($this::PAYPAL_PRO) || $this->isPayPalType($this::PAYPAL_EXPRESS)) { $args = array(); $this->initialize($args, 'ManageRecurringPaymentsProfileStatus'); diff --git a/civicrm/CRM/Core/Payment/PayPalProIPN.php b/civicrm/CRM/Core/Payment/PayPalProIPN.php index d230762395c7a94f7a6e26c8c746dc89276c31ae..f16c7ceae824b85d82c6b251f885847474edccf8 100644 --- a/civicrm/CRM/Core/Payment/PayPalProIPN.php +++ b/civicrm/CRM/Core/Payment/PayPalProIPN.php @@ -161,13 +161,13 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { * @param array $ids * @param array $objects * @param bool $first - * @return bool + * @return void */ public function recur(&$input, &$ids, &$objects, $first) { if (!isset($input['txnType'])) { - CRM_Core_Error::debug_log_message("Could not find txn_type in input request"); + Civi::log()->debug('PayPalProIPN: Could not find txn_type in input request.'); echo "Failure: Invalid parameters<p>"; - return FALSE; + return; } $recur = &$objects['contributionRecur']; @@ -176,9 +176,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // make sure the invoice is valid and matches what we have in // the contribution record if ($recur->invoice_id != $input['invoice']) { - CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request recur is " . $recur->invoice_id . " input is " . $input['invoice']); + Civi::log()->debug('PayPalProIPN: Invoice values dont match between database and IPN request recur is ' . $recur->invoice_id . ' input is ' . $input['invoice']); echo "Failure: Invoice values dont match between database and IPN request recur is " . $recur->invoice_id . " input is " . $input['invoice']; - return FALSE; + return; } $now = date('YmdHis'); @@ -211,21 +211,20 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { //set transaction type $txnType = $this->retrieve('txn_type', 'String'); //Changes for paypal pro recurring payment - $contributionStatuses = civicrm_api3('contribution', 'getoptions', array('field' => 'contribution_status_id')); - $contributionStatuses = $contributionStatuses['values']; + $contributionStatuses = array_flip(CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'validate')); switch ($txnType) { case 'recurring_payment_profile_created': if (in_array($recur->contribution_status_id, array( - array_search('Pending', $contributionStatuses), - array_search('In Progress', $contributionStatuses), + $contributionStatuses['Pending'], + $contributionStatuses['In Progress'], )) && !empty($recur->processor_id) ) { echo "already handled"; - return FALSE; + return; } $recur->create_date = $now; - $recur->contribution_status_id = 2; + $recur->contribution_status_id = $contributionStatuses['Pending']; $recur->processor_id = $this->retrieve('recurring_payment_id', 'String'); $recur->trxn_id = $recur->processor_id; $subscriptionPaymentStatus = CRM_Core_Payment::RECURRING_PAYMENT_START; @@ -256,9 +255,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { if ($this->retrieve('profile_status', 'String') == 'Expired') { if (!empty($recur->end_date)) { echo "already handled"; - return FALSE; + return; } - $recur->contribution_status_id = 1; + $recur->contribution_status_id = $contributionStatuses['Completed']; $recur->end_date = $now; $sendNotification = TRUE; $subscriptionPaymentStatus = CRM_Core_Payment::RECURRING_PAYMENT_END; @@ -266,8 +265,8 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // make sure the contribution status is not done // since order of ipn's is unknown - if ($recur->contribution_status_id != 1) { - $recur->contribution_status_id = 5; + if ($recur->contribution_status_id != $contributionStatuses['Completed']) { + $recur->contribution_status_id = $contributionStatuses['In Progress']; } break; } @@ -291,7 +290,7 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { } if ($txnType != 'recurring_payment') { - return TRUE; + return; } if (!$first) { @@ -300,9 +299,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { $contribution = new CRM_Contribute_BAO_Contribution(); $contribution->trxn_id = $input['trxn_id']; if ($contribution->trxn_id && $contribution->find()) { - CRM_Core_Error::debug_log_message("returning since contribution has already been handled"); + Civi::log()->debug('PayPalProIPN: Returning since contribution has already been handled.'); echo "Success: Contribution has already been handled<p>"; - return TRUE; + return; } $contribution->contact_id = $recur->contact_id; @@ -319,19 +318,17 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // CRM-13737 - am not aware of any reason why payment_date would not be set - this if is a belt & braces $objects['contribution']->receive_date = !empty($input['payment_date']) ? date('YmdHis', strtotime($input['payment_date'])) : $now; - $this->single($input, $ids, $objects, - TRUE, $first - ); + $this->single($input, $ids, $objects, TRUE, $first); } /** - * @param $input - * @param $ids - * @param $objects + * @param array $input + * @param array $ids + * @param array $objects * @param bool $recur * @param bool $first * - * @return bool + * @return void */ public function single(&$input, &$ids, &$objects, $recur = FALSE, $first = FALSE) { $contribution = &$objects['contribution']; @@ -339,9 +336,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // make sure the invoice is valid and matches what we have in the contribution record if ((!$recur) || ($recur && $first)) { if ($contribution->invoice_id != $input['invoice']) { - CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request"); + Civi::log()->debug('PayPalProIPN: Invoice values dont match between database and IPN request.'); echo "Failure: Invoice values dont match between database and IPN request<p>contribution is" . $contribution->invoice_id . " and input is " . $input['invoice']; - return FALSE; + return; } } else { @@ -350,9 +347,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { if (!$recur) { if ($contribution->total_amount != $input['amount']) { - CRM_Core_Error::debug_log_message("Amount values dont match between database and IPN request"); + Civi::log()->debug('PayPalProIPN: Amount values dont match between database and IPN request.'); echo "Failure: Amount values dont match between database and IPN request<p>"; - return FALSE; + return; } } else { @@ -363,24 +360,29 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { $status = $input['paymentStatus']; if ($status == 'Denied' || $status == 'Failed' || $status == 'Voided') { - return $this->failed($objects, $transaction); + $this->failed($objects, $transaction); + return; } elseif ($status == 'Pending') { - return $this->pending($objects, $transaction); + $this->pending($objects, $transaction); + return; } elseif ($status == 'Refunded' || $status == 'Reversed') { - return $this->cancelled($objects, $transaction); + $this->cancelled($objects, $transaction); + return; } elseif ($status != 'Completed') { - return $this->unhandled($objects, $transaction); + $this->unhandled($objects, $transaction); + return; } // check if contribution is already completed, if so we ignore this ipn - if ($contribution->contribution_status_id == 1) { + $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($contribution->contribution_status_id == $completedStatusId) { $transaction->commit(); - CRM_Core_Error::debug_log_message("returning since contribution has already been handled"); + Civi::log()->debug('PayPalProIPN: Returning since contribution has already been handled.'); echo "Success: Contribution has already been handled<p>"; - return TRUE; + return; } $this->completeTransaction($input, $ids, $objects, $transaction, $recur); @@ -397,6 +399,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // processor id & the handleNotification function (which should call the completetransaction api & by-pass this // entirely). The only thing the IPN class should really do is extract data from the request, validate it // & call completetransaction or call fail? (which may not exist yet). + + Civi::log()->warning('Unreliable method used to get payment_processor_id for PayPal Pro IPN - this will cause problems if you have more than one instance'); + $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'PayPal', 'id', 'name' ); @@ -414,7 +419,7 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { * (with the input parameters) & call this & all will be done * * @todo the references to POST throughout this class need to be removed - * @return bool + * @return void */ public function main() { CRM_Core_Error::debug_var('GET', $_GET, TRUE, TRUE); @@ -464,16 +469,10 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr } } - // This is an unreliable method as there could be more than one instance. - // Recommended approach is to use the civicrm/payment/ipn/xx url where xx is the payment - // processor id & the handleNotification function (which should call the completetransaction api & by-pass this - // entirely). The only thing the IPN class should really do is extract data from the request, validate it - // & call completetransaction or call fail? (which may not exist yet). - $paymentProcessorID = self::getPayPalPaymentProcessorID(); if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) { - return FALSE; + return; } self::$_paymentProcessor = &$objects['paymentProcessor']; @@ -484,31 +483,27 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr if ($ids['contributionRecur']) { // check if first contribution is completed, else complete first contribution $first = TRUE; - if ($objects['contribution']->contribution_status_id == 1) { + $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($objects['contribution']->contribution_status_id == $completedStatusId) { $first = FALSE; } - return $this->recur($input, $ids, $objects, $first); - } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); + $this->recur($input, $ids, $objects, $first); + return; } } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); - } + $this->single($input, $ids, $objects, FALSE, FALSE); } /** - * @param $input - * @param $ids + * @param array $input + * @param array $ids * - * @return bool + * @return void * @throws CRM_Core_Exception */ public function getInput(&$input, &$ids) { - if (!$this->getBillingID($ids)) { - return FALSE; + return; } $input['txnType'] = self::retrieve('txn_type', 'String', 'POST', FALSE); @@ -542,6 +537,7 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr /** * Handle payment express IPNs. + * * For one off IPNS no actual response is required * Recurring is more difficult as we have limited confirmation material * lets look up invoice id in recur_contribution & rely on the unique transaction id to ensure no @@ -560,27 +556,30 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr // as membership id etc can be derived by the load objects fn $objects = $ids = $input = array(); $isFirst = FALSE; + $input['invoice'] = self::getValue('i', FALSE); $input['txnType'] = $this->retrieve('txn_type', 'String'); - if ($input['txnType'] != 'recurring_payment') { + $contributionRecur = civicrm_api3('contribution_recur', 'getsingle', array( + 'return' => 'contact_id, id, payment_processor_id', + 'invoice_id' => $input['invoice'], + )); + + if ($input['txnType'] !== 'recurring_payment' && $input['txnType'] !== 'recurring_payment_profile_created') { throw new CRM_Core_Exception('Paypal IPNS not handled other than recurring_payments'); } - $input['invoice'] = self::getValue('i', FALSE); + $this->getInput($input, $ids); - if ($this->transactionExists($input['trxn_id'])) { + if ($input['txnType'] === 'recurring_payment' && $this->transactionExists($input['trxn_id'])) { throw new CRM_Core_Exception('This transaction has already been processed'); } - $contributionRecur = civicrm_api3('contribution_recur', 'getsingle', array( - 'return' => 'contact_id, id', - 'invoice_id' => $input['invoice'], - )); $ids['contact'] = $contributionRecur['contact_id']; $ids['contributionRecur'] = $contributionRecur['id']; - $result = civicrm_api3('contribution', 'getsingle', array('invoice_id' => $input['invoice'])); + $result = civicrm_api3('contribution', 'getsingle', ['invoice_id' => $input['invoice'], 'contribution_test' => '']); $ids['contribution'] = $result['id']; - //@todo hard - coding 'pending' for now - if ($result['contribution_status_id'] == 2) { + //@todo hardcoding 'pending' for now + $pendingStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'); + if ($result['contribution_status_id'] == $pendingStatusId) { $isFirst = TRUE; } // arg api won't get this - fix it @@ -595,12 +594,12 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr // membership would be an easy add - but not relevant to my customer... $this->_component = $input['component'] = 'contribute'; $input['trxn_date'] = date('Y-m-d-H-i-s', strtotime(self::retrieve('time_created', 'String'))); - $paymentProcessorID = self::getPayPalPaymentProcessorID(); + $paymentProcessorID = $contributionRecur['payment_processor_id']; if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) { throw new CRM_Core_Exception('Data did not validate'); } - return $this->recur($input, $ids, $objects, $isFirst); + $this->recur($input, $ids, $objects, $isFirst); } /** diff --git a/civicrm/CRM/Core/Payment/PayflowPro.php b/civicrm/CRM/Core/Payment/PayflowPro.php index 2fcdf724084bed9138a4353755af8aef66ea2609..2020b2a8cd81d9ae53c3d33c68b8b37fe2eaefc8 100644 --- a/civicrm/CRM/Core/Payment/PayflowPro.php +++ b/civicrm/CRM/Core/Payment/PayflowPro.php @@ -567,85 +567,4 @@ class CRM_Core_Payment_PayflowPro extends CRM_Core_Payment { return $responseData; } - /** - * @param int $recurringProfileID - * @param int $processorID - * - * @throws Exception - */ - public function getRecurringTransactionStatus($recurringProfileID, $processorID) { - if (!defined('CURLOPT_SSLCERT')) { - CRM_Core_Error::fatal(ts('Payflow Pro requires curl with SSL support')); - } - - /* - * define variables for connecting with the gateway - */ - - //if you have not set up a separate user account the vendor name is used as the username - if (!$this->_paymentProcessor['subject']) { - $user = $this->_paymentProcessor['user_name']; - } - else { - $user = $this->_paymentProcessor['subject']; - } - //$recurringProfileID = "RT0000000001"; - // c $trythis = $this->getRecurringTransactionStatus($recurringProfileID,17); - - /* - *Create the array of variables to be sent to the processor from the $params array - * passed into this function - * - */ - - $payflow_query_array = array( - 'USER' => $user, - 'VENDOR' => $this->_paymentProcessor['user_name'], - 'PARTNER' => $this->_paymentProcessor['signature'], - 'PWD' => $this->_paymentProcessor['password'], - // C - Direct Payment using credit card - 'TENDER' => 'C', - // A - Authorization, S - Sale - 'TRXTYPE' => 'R', - 'ACTION' => 'I', - //A for add recurring - //(M-modify,C-cancel,R-reactivate, - //I-inquiry,P-payment - 'ORIGPROFILEID' => $recurringProfileID, - 'PAYMENTHISTORY' => 'Y', - ); - - $payflow_query = $this->convert_to_nvp($payflow_query_array); - echo $payflow_query; - $submiturl = $this->_paymentProcessor['url_site']; - //ie. url at payment processor to submit to. - $responseData = self::submit_transaction($submiturl, $payflow_query); - /* - * Payment successfully sent to gateway - process the response now - */ - - $result = strstr($responseData, "RESULT"); - $nvpArray = array(); - while (strlen($result)) { - // name - $keypos = strpos($result, '='); - $keyval = substr($result, 0, $keypos); - // value - $valuepos = strpos($result, '&') ? strpos($result, '&') : strlen($result); - $valval = substr($result, $keypos + 1, $valuepos - $keypos - 1); - // decoding the respose - $nvpArray[$keyval] = $valval; - $result = substr($result, $valuepos + 1, strlen($result)); - } - - // @TODO Function is named getRecurringTransactionStatus() which - // suggests it returns a result. It sets a $result_code but doesn't return - // it, printing output instead? - $result_code = $nvpArray['RESULT']; - print_r($responseData); - - //RESPMSG=Invalid Profile ID: Invalid recurring profile ID - //RT0000000001 - } - } diff --git a/civicrm/CRM/Core/Permission.php b/civicrm/CRM/Core/Permission.php index ee7b49f69d6fe2cc11fc7b84806da5d39c136da7..ff732084aa3efa97d215f9bb4be9c19b35065a81 100644 --- a/civicrm/CRM/Core/Permission.php +++ b/civicrm/CRM/Core/Permission.php @@ -1470,6 +1470,9 @@ class CRM_Core_Permission { 'create' => array('edit message templates', 'edit user-driven message templates', 'edit system workflow message templates'), 'update' => array('edit message templates', 'edit user-driven message templates', 'edit system workflow message templates'), ); + + $permissions['report_template']['update'] = 'save Report Criteria'; + $permissions['report_template']['create'] = 'save Report Criteria'; return $permissions; } diff --git a/civicrm/CRM/Core/PseudoConstant.php b/civicrm/CRM/Core/PseudoConstant.php index 81bdb6f232eb264a21ebf2f532fa3a1b75c7a2b0..3392a8e371e968977ab504b40d5cdb80130430fd 100644 --- a/civicrm/CRM/Core/PseudoConstant.php +++ b/civicrm/CRM/Core/PseudoConstant.php @@ -536,7 +536,7 @@ class CRM_Core_PseudoConstant { $key = 'id', $force = NULL ) { - $cacheKey = "CRM_PC_{$name}_{$all}_{$key}_{$retrieve}_{$filter}_{$condition}_{$orderby}"; + $cacheKey = CRM_Core_BAO_Cache::cleanKey("CRM_PC_{$name}_{$all}_{$key}_{$retrieve}_{$filter}_{$condition}_{$orderby}"); $cache = CRM_Utils_Cache::singleton(); $var = $cache->get($cacheKey); if ($var && empty($force)) { diff --git a/civicrm/CRM/Core/Session.php b/civicrm/CRM/Core/Session.php index 7c1c1a42a1a3f3922712c94cf92ea47c73157822..632a9e14ab9a632b767d571f5cdff72ff2167ef9 100644 --- a/civicrm/CRM/Core/Session.php +++ b/civicrm/CRM/Core/Session.php @@ -286,7 +286,7 @@ class CRM_Core_Session { $values = &$this->_session[$this->_key]; } else { - $values = CRM_Core_BAO_Cache::getItem('CiviCRM Session', "CiviCRM_{$prefix}"); + $values = Civi::cache('session')->get("CiviCRM_{$prefix}"); } if ($values) { diff --git a/civicrm/CRM/Cxn/CiviCxnHttp.php b/civicrm/CRM/Cxn/CiviCxnHttp.php index f2b2c6357c8c1038b844817d7585473d2d756809..0d6024812cc0c40441ab0acd4a49dd157eedf06c 100644 --- a/civicrm/CRM/Cxn/CiviCxnHttp.php +++ b/civicrm/CRM/Cxn/CiviCxnHttp.php @@ -54,7 +54,7 @@ class CRM_Cxn_CiviCxnHttp extends \Civi\Cxn\Rpc\Http\PhpHttp { $lowVerb = strtolower($verb); if ($lowVerb === 'get' && $this->cache) { - $cachePath = 'get/' . md5($url); + $cachePath = 'get_' . md5($url); $cacheLine = $this->cache->get($cachePath); if ($cacheLine && $cacheLine['expires'] > CRM_Utils_Time::getTimeRaw()) { return $cacheLine['data']; @@ -66,7 +66,7 @@ class CRM_Cxn_CiviCxnHttp extends \Civi\Cxn\Rpc\Http\PhpHttp { if ($lowVerb === 'get' && $this->cache) { $expires = CRM_Utils_Http::parseExpiration($result[0]); if ($expires !== NULL) { - $cachePath = 'get/' . md5($url); + $cachePath = 'get_' . md5($url); $cacheLine = array( 'url' => $url, 'expires' => $expires, @@ -106,4 +106,11 @@ class CRM_Cxn_CiviCxnHttp extends \Civi\Cxn\Rpc\Http\PhpHttp { return $result; } + /** + * @return \CRM_Utils_Cache_Interface|null + */ + public function getCache() { + return $this->cache; + } + } diff --git a/civicrm/CRM/Dedupe/Merger.php b/civicrm/CRM/Dedupe/Merger.php index a2fe7e7187480d44c9af7e7012bac6cf8df1fb07..afb53df5f545cf88b5a0fc4ec3b46c6f7d9b71c0 100644 --- a/civicrm/CRM/Dedupe/Merger.php +++ b/civicrm/CRM/Dedupe/Merger.php @@ -203,35 +203,23 @@ class CRM_Dedupe_Merger { /** * Get array tables and fields that reference civicrm_contact.id. * - * This includes core tables, custom group tables, tables added by the merge - * hook and (somewhat randomly) the entity_tag table. + * This function calls the merge hook and only exists to wrap the DAO function to support that deprecated call. + * The entityTypes hook is the recommended way to add tables to this result. * - * Refer to CRM-17454 for information on the danger of querying the information - * schema to derive this. - * - * This function calls the merge hook but the entityTypes hook is the recommended - * way to add tables to this result. + * I thought about adding another hook to alter tableReferences but decided it was unclear if there + * are use cases not covered by entityTables and instead we should wait & see. */ public static function cidRefs() { if (isset(\Civi::$statics[__CLASS__]) && isset(\Civi::$statics[__CLASS__]['contact_references'])) { return \Civi::$statics[__CLASS__]['contact_references']; } - $contactReferences = array(); - $coreReferences = CRM_Core_DAO::getReferencesToTable('civicrm_contact'); - foreach ($coreReferences as $coreReference) { - if (!is_a($coreReference, 'CRM_Core_Reference_Dynamic')) { - $contactReferences[$coreReference->getReferenceTable()][] = $coreReference->getReferenceKey(); - } - } - self::addCustomTablesExtendingContactsToCidRefs($contactReferences); - // FixME for time being adding below line statically as no Foreign key constraint defined for table 'civicrm_entity_tag' - $contactReferences['civicrm_entity_tag'][] = 'entity_id'; + $contactReferences = $coreReferences = CRM_Core_DAO::getReferencesToContactTable(); - // Allow hook_civicrm_merge() to adjust $cidRefs. - // Note that if entities are registered using the entityTypes hook there - // is no need to use this hook. CRM_Utils_Hook::merge('cidRefs', $contactReferences); + if ($contactReferences !== $coreReferences) { + Civi::log()->warning("Deprecated hook ::merge in context of 'cidRefs. Use entityTypes instead.", array('civi.tag' => 'deprecated')); + } \Civi::$statics[__CLASS__]['contact_references'] = $contactReferences; return \Civi::$statics[__CLASS__]['contact_references']; } @@ -486,7 +474,8 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m // getting all custom tables $customTables = array(); if ($customTableToCopyFrom !== NULL) { - self::addCustomTablesExtendingContactsToCidRefs($customTables); + // @todo this duplicates cidRefs? + CRM_Core_DAO::appendCustomTablesExtendingContacts($customTables); $customTables = array_keys($customTables); } @@ -1457,7 +1446,6 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m $otherTree = CRM_Core_BAO_CustomGroup::getTree($main['contact_type'], NULL, $otherId, -1, CRM_Utils_Array::value('contact_sub_type', $other), NULL, TRUE, NULL, TRUE, $checkPermissions ); - CRM_Core_DAO::freeResult(); foreach ($otherTree as $gid => $group) { $foundField = FALSE; @@ -1926,26 +1914,6 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m } } - /** - * Add custom tables that extend contacts to the list of contact references. - * - * CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity seems like a safe-ish - * function to be sure all are retrieved & we don't miss subtypes or inactive or multiples - * - the down side is it is not cached. - * - * Further changes should be include tests in the CRM_Core_MergerTest class - * to ensure that disabled, subtype, multiple etc groups are still captured. - * - * @param array $cidRefs - */ - public static function addCustomTablesExtendingContactsToCidRefs(&$cidRefs) { - $customValueTables = CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity('Contact'); - $customValueTables->find(); - while ($customValueTables->fetch()) { - $cidRefs[$customValueTables->table_name] = array('entity_id'); - } - } - /** * Create activities tracking the merge on affected contacts. * @@ -2329,8 +2297,6 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m // pair may have been flipped, so make sure we delete using both orders CRM_Core_BAO_PrevNextCache::deletePair($mainId, $otherId, $cacheKeyString, TRUE); } - - CRM_Core_DAO::freeResult(); } } diff --git a/civicrm/CRM/Event/Form/Registration/Confirm.php b/civicrm/CRM/Event/Form/Registration/Confirm.php index da1a8ee522d420454695f1cd357adb3c51103a84..29b5c5a2795101aee1807f226db7061e0661074c 100644 --- a/civicrm/CRM/Event/Form/Registration/Confirm.php +++ b/civicrm/CRM/Event/Form/Registration/Confirm.php @@ -208,7 +208,8 @@ class CRM_Event_Form_Registration_Confirm extends CRM_Event_Form_Registration { $this->assignToTemplate(); if ($this->_values['event']['is_monetary'] && - ($this->_params[0]['amount'] || $this->_params[0]['amount'] == 0) + ($this->_params[0]['amount'] || $this->_params[0]['amount'] == 0) && + !$this->_requireApproval ) { $this->_amount = array(); diff --git a/civicrm/CRM/Event/Form/Task.php b/civicrm/CRM/Event/Form/Task.php index 263a644cba8cf6e2151b74bda96cc8f5c35a938c..cc7df4d31d5f8673e4bd4fb61733395faa3d505c 100644 --- a/civicrm/CRM/Event/Form/Task.php +++ b/civicrm/CRM/Event/Form/Task.php @@ -34,31 +34,10 @@ */ /** - * This class generates task actions for CiviEvent - * + * Class for event form task actions. + * FIXME: This needs refactoring to properly inherit from CRM_Core_Form_Task and share more functions. */ -class CRM_Event_Form_Task extends CRM_Core_Form { - - /** - * The task being performed. - * - * @var int - */ - protected $_task; - - /** - * The additional clause that we restrict the search with. - * - * @var string - */ - protected $_componentClause = NULL; - - /** - * The array that holds all the component ids. - * - * @var array - */ - protected $_componentIds; +class CRM_Event_Form_Task extends CRM_Core_Form_Task { /** * The array that holds all the participant ids. @@ -80,9 +59,8 @@ class CRM_Event_Form_Task extends CRM_Core_Form { /** * @param CRM_Core_Form $form - * @param bool $useTable */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $form->_participantIds = array(); $values = $form->controller->exportValues($form->get('searchFormName')); diff --git a/civicrm/CRM/Export/BAO/Export.php b/civicrm/CRM/Export/BAO/Export.php index b86bbe8bd83dabd69a68a594814230ea92291065..dd6fb35db9bdaefe135beb530c1612d5e2f5048b 100644 --- a/civicrm/CRM/Export/BAO/Export.php +++ b/civicrm/CRM/Export/BAO/Export.php @@ -41,6 +41,68 @@ class CRM_Export_BAO_Export { // CRM-7675 const EXPORT_ROW_COUNT = 100000; + protected static $relationshipReturnProperties = []; + + /** + * @param $value + * @param $locationTypeFields + * @param $relationshipTypes + * + * @return array + */ + protected static function setRelationshipReturnProperties($value, $locationTypeFields, $relationshipTypes) { + $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id'); + $relPhoneTypeId = $relIMProviderId = NULL; + if (!empty($value[2])) { + $relationField = CRM_Utils_Array::value(2, $value); + if (trim(CRM_Utils_Array::value(3, $value))) { + $relLocTypeId = CRM_Utils_Array::value(3, $value); + } + else { + $relLocTypeId = 'Primary'; + } + + if ($relationField == 'phone') { + $relPhoneTypeId = CRM_Utils_Array::value(4, $value); + } + elseif ($relationField == 'im') { + $relIMProviderId = CRM_Utils_Array::value(4, $value); + } + } + elseif (!empty($value[4])) { + $relationField = CRM_Utils_Array::value(4, $value); + $relLocTypeId = CRM_Utils_Array::value(5, $value); + if ($relationField == 'phone') { + $relPhoneTypeId = CRM_Utils_Array::value(6, $value); + } + elseif ($relationField == 'im') { + $relIMProviderId = CRM_Utils_Array::value(6, $value); + } + } + if (in_array($relationField, $locationTypeFields) && is_numeric($relLocTypeId)) { + if ($relPhoneTypeId) { + self::$relationshipReturnProperties[$relationshipTypes]['location'][$locationTypes[$relLocTypeId]]['phone-' . $relPhoneTypeId] = 1; + } + elseif ($relIMProviderId) { + self::$relationshipReturnProperties[$relationshipTypes]['location'][$locationTypes[$relLocTypeId]]['im-' . $relIMProviderId] = 1; + } + else { + self::$relationshipReturnProperties[$relationshipTypes]['location'][$locationTypes[$relLocTypeId]][$relationField] = 1; + } + } + else { + self::$relationshipReturnProperties[$relationshipTypes][$relationField] = 1; + } + return array($relationField); + } + + /** + * @return array + */ + public static function getRelationshipReturnProperties() { + return self::relationshipReturnProperties; + } + /** * Get Querymode based on ExportMode * @@ -348,58 +410,15 @@ class CRM_Export_BAO_Export { ); foreach ($fields as $key => $value) { - $relationField = NULL; - $relationshipTypes = $fieldName = CRM_Utils_Array::value(1, $value); + $fieldName = CRM_Utils_Array::value(1, $value); if (!$fieldName) { continue; } - if (array_key_exists($relationshipTypes, $contactRelationshipTypes) && (!empty($value[2]) || !empty($value[4]))) { - $relPhoneTypeId = $relIMProviderId = NULL; - if (!empty($value[2])) { - $relationField = CRM_Utils_Array::value(2, $value); - if (trim(CRM_Utils_Array::value(3, $value))) { - $relLocTypeId = CRM_Utils_Array::value(3, $value); - } - else { - $relLocTypeId = 'Primary'; - } - - if ($relationField == 'phone') { - $relPhoneTypeId = CRM_Utils_Array::value(4, $value); - } - elseif ($relationField == 'im') { - $relIMProviderId = CRM_Utils_Array::value(4, $value); - } - } - elseif (!empty($value[4])) { - $relationField = CRM_Utils_Array::value(4, $value); - $relLocTypeId = CRM_Utils_Array::value(5, $value); - if ($relationField == 'phone') { - $relPhoneTypeId = CRM_Utils_Array::value(6, $value); - } - elseif ($relationField == 'im') { - $relIMProviderId = CRM_Utils_Array::value(6, $value); - } - } - if (in_array($relationField, $locationTypeFields) && is_numeric($relLocTypeId)) { - if ($relPhoneTypeId) { - $returnProperties[$relationshipTypes]['location'][$locationTypes[$relLocTypeId]]['phone-' . $relPhoneTypeId] = 1; - } - elseif ($relIMProviderId) { - $returnProperties[$relationshipTypes]['location'][$locationTypes[$relLocTypeId]]['im-' . $relIMProviderId] = 1; - } - else { - $returnProperties[$relationshipTypes]['location'][$locationTypes[$relLocTypeId]][$relationField] = 1; - } - } - else { - $returnProperties[$relationshipTypes][$relationField] = 1; - } - } - - if ($relationField) { - // already handled. + if (array_key_exists($fieldName, $contactRelationshipTypes) && (!empty($value[2]) || !empty($value[4]))) { + self::setRelationshipReturnProperties($value, $locationTypeFields, $fieldName); + // @todo we can later not add this to this array but maintain a separate array. + $returnProperties = array_merge($returnProperties, self::$relationshipReturnProperties); } elseif (is_numeric(CRM_Utils_Array::value(2, $value))) { $locTypeId = $value[2]; @@ -582,81 +601,7 @@ INSERT INTO {$componentTable} SELECT distinct gc.contact_id FROM civicrm_group_c unset($returnProperties[$relationKeyHOH]['im_provider']); } - $allRelContactArray = $relationQuery = array(); - - foreach ($contactRelationshipTypes as $rel => $dnt) { - if ($relationReturnProperties = CRM_Utils_Array::value($rel, $returnProperties)) { - $allRelContactArray[$rel] = array(); - // build Query for each relationship - $relationQuery[$rel] = new CRM_Contact_BAO_Query(NULL, $relationReturnProperties, - NULL, FALSE, FALSE, $queryMode - ); - list($relationSelect, $relationFrom, $relationWhere, $relationHaving) = $relationQuery[$rel]->query(); - - list($id, $direction) = explode('_', $rel, 2); - // identify the relationship direction - $contactA = 'contact_id_a'; - $contactB = 'contact_id_b'; - if ($direction == 'b_a') { - $contactA = 'contact_id_b'; - $contactB = 'contact_id_a'; - } - if ($exportMode == CRM_Export_Form_Select::CONTACT_EXPORT) { - $relIDs = $ids; - } - elseif ($exportMode == CRM_Export_Form_Select::ACTIVITY_EXPORT) { - $sourceID = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_ActivityContact', 'record_type_id', 'Activity Source'); - $dao = CRM_Core_DAO::executeQuery(" - SELECT contact_id FROM civicrm_activity_contact - WHERE activity_id IN ( " . implode(',', $ids) . ") AND - record_type_id = {$sourceID} - "); - - while ($dao->fetch()) { - $relIDs[] = $dao->contact_id; - } - } - else { - $component = self::exportComponent($exportMode); - - if ($exportMode == CRM_Export_Form_Select::CASE_EXPORT) { - $relIDs = CRM_Case_BAO_Case::retrieveContactIdsByCaseId($ids); - } - else { - $relIDs = CRM_Core_DAO::getContactIDsFromComponent($ids, $component); - } - } - - $relationshipJoin = $relationshipClause = ''; - if (!$selectAll && $componentTable) { - $relationshipJoin = " INNER JOIN {$componentTable} ctTable ON ctTable.contact_id = {$contactA}"; - } - elseif (!empty($relIDs)) { - $relID = implode(',', $relIDs); - $relationshipClause = " AND crel.{$contactA} IN ( {$relID} )"; - } - - $relationFrom = " {$relationFrom} - INNER JOIN civicrm_relationship crel ON crel.{$contactB} = contact_a.id AND crel.relationship_type_id = {$id} - {$relationshipJoin} "; - - //check for active relationship status only - $today = date('Ymd'); - $relationActive = " AND (crel.is_active = 1 AND ( crel.end_date is NULL OR crel.end_date >= {$today} ) )"; - $relationWhere = " WHERE contact_a.is_deleted = 0 {$relationshipClause} {$relationActive}"; - $relationGroupBy = CRM_Contact_BAO_Query::getGroupByFromSelectColumns($relationQuery[$rel]->_select, "crel.{$contactA}"); - $relationSelect = "{$relationSelect}, {$contactA} as refContact "; - $relationQueryString = "$relationSelect $relationFrom $relationWhere $relationHaving $relationGroupBy"; - - $allRelContactDAO = CRM_Core_DAO::executeQuery($relationQueryString); - while ($allRelContactDAO->fetch()) { - //FIX Me: Migrate this to table rather than array - // build the array of all related contacts - $allRelContactArray[$rel][$allRelContactDAO->refContact] = clone($allRelContactDAO); - } - $allRelContactDAO->free(); - } - } + list($relationQuery, $allRelContactArray) = self::buildRelatedContactArray($selectAll, $ids, $exportMode, $componentTable, $contactRelationshipTypes, $returnProperties, $queryMode); // make sure the groups stuff is included only if specifically specified // by the fields param (CRM-1969), else we limit the contacts outputted to only @@ -1314,7 +1259,7 @@ VALUES $sqlValueString */ public static function createTempTable(&$sqlColumns) { //creating a temporary table for the search result that need be exported - $exportTempTable = CRM_Core_DAO::createTempTableName('civicrm_export', TRUE); + $exportTempTable = CRM_Utils_SQL_TempTable::build()->setDurable()->setCategory('export')->getName(); // also create the sql table $sql = "DROP TABLE IF EXISTS {$exportTempTable}"; @@ -2167,4 +2112,105 @@ WHERE {$whereClause}"; } } + /** + * Get the ids that we want to get related contact details for. + * + * @param array $ids + * @param int $exportMode + * + * @return array + */ + protected static function getIDsForRelatedContact($ids, $exportMode) { + if ($exportMode == CRM_Export_Form_Select::CONTACT_EXPORT) { + return $ids; + } + if ($exportMode == CRM_Export_Form_Select::ACTIVITY_EXPORT) { + $relIDs = []; + $sourceID = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_ActivityContact', 'record_type_id', 'Activity Source'); + $dao = CRM_Core_DAO::executeQuery(" + SELECT contact_id FROM civicrm_activity_contact + WHERE activity_id IN ( " . implode(',', $ids) . ") AND + record_type_id = {$sourceID} + "); + + while ($dao->fetch()) { + $relIDs[] = $dao->contact_id; + } + return $relIDs; + } + $component = self::exportComponent($exportMode); + + if ($exportMode == CRM_Export_Form_Select::CASE_EXPORT) { + return CRM_Case_BAO_Case::retrieveContactIdsByCaseId($ids); + } + else { + return CRM_Core_DAO::getContactIDsFromComponent($ids, $component); + } + } + + /** + * @param $selectAll + * @param $ids + * @param $exportMode + * @param $componentTable + * @param $contactRelationshipTypes + * @param $returnProperties + * @param $queryMode + * @return array + */ + protected static function buildRelatedContactArray($selectAll, $ids, $exportMode, $componentTable, $contactRelationshipTypes, $returnProperties, $queryMode) { + $allRelContactArray = $relationQuery = array(); + + foreach ($contactRelationshipTypes as $rel => $dnt) { + if ($relationReturnProperties = CRM_Utils_Array::value($rel, $returnProperties)) { + $allRelContactArray[$rel] = array(); + // build Query for each relationship + $relationQuery[$rel] = new CRM_Contact_BAO_Query(NULL, $relationReturnProperties, + NULL, FALSE, FALSE, $queryMode + ); + list($relationSelect, $relationFrom, $relationWhere, $relationHaving) = $relationQuery[$rel]->query(); + + list($id, $direction) = explode('_', $rel, 2); + // identify the relationship direction + $contactA = 'contact_id_a'; + $contactB = 'contact_id_b'; + if ($direction == 'b_a') { + $contactA = 'contact_id_b'; + $contactB = 'contact_id_a'; + } + $relIDs = self::getIDsForRelatedContact($ids, $exportMode); + + $relationshipJoin = $relationshipClause = ''; + if (!$selectAll && $componentTable) { + $relationshipJoin = " INNER JOIN {$componentTable} ctTable ON ctTable.contact_id = {$contactA}"; + } + elseif (!empty($relIDs)) { + $relID = implode(',', $relIDs); + $relationshipClause = " AND crel.{$contactA} IN ( {$relID} )"; + } + + $relationFrom = " {$relationFrom} + INNER JOIN civicrm_relationship crel ON crel.{$contactB} = contact_a.id AND crel.relationship_type_id = {$id} + {$relationshipJoin} "; + + //check for active relationship status only + $today = date('Ymd'); + $relationActive = " AND (crel.is_active = 1 AND ( crel.end_date is NULL OR crel.end_date >= {$today} ) )"; + $relationWhere = " WHERE contact_a.is_deleted = 0 {$relationshipClause} {$relationActive}"; + $relationGroupBy = CRM_Contact_BAO_Query::getGroupByFromSelectColumns($relationQuery[$rel]->_select, "crel.{$contactA}"); + $relationSelect = "{$relationSelect}, {$contactA} as refContact "; + $relationQueryString = "$relationSelect $relationFrom $relationWhere $relationHaving $relationGroupBy"; + + $allRelContactDAO = CRM_Core_DAO::executeQuery($relationQueryString); + while ($allRelContactDAO->fetch()) { + //FIX Me: Migrate this to table rather than array + // build the array of all related contacts + $allRelContactArray[$rel][$allRelContactDAO->refContact] = clone($allRelContactDAO); + } + $allRelContactDAO->free(); + } + } + return array($relationQuery, $allRelContactArray); + } + } diff --git a/civicrm/CRM/Export/Form/Select.php b/civicrm/CRM/Export/Form/Select.php index 3e36b238ca931b620c43648fa25d868c1e27c802..af478d4adfad066729eaf4ff6f9e745426a60920 100644 --- a/civicrm/CRM/Export/Form/Select.php +++ b/civicrm/CRM/Export/Form/Select.php @@ -36,7 +36,7 @@ /** * This class gets the name of the file to upload */ -class CRM_Export_Form_Select extends CRM_Core_Form { +class CRM_Export_Form_Select extends CRM_Core_Form_Task { /** * Various Contact types. @@ -70,20 +70,6 @@ class CRM_Export_Form_Select extends CRM_Core_Form { public $_componentTable; - /** - * Must be set to entity table name (eg. civicrm_participant) by child class - * - * @var string - */ - static $tableName = NULL; - - /** - * Must be set to entity shortname (eg. event) - * - * @var string - */ - static $entityShortname = NULL; - /** * Build all the data structures needed to build the form. * @@ -108,23 +94,14 @@ class CRM_Export_Form_Select extends CRM_Core_Form { $this->_componentIds = array(); $this->_componentClause = NULL; - $stateMachine = $this->controller->getStateMachine(); - $formName = CRM_Utils_System::getClassName($stateMachine); - $isStandalone = $formName == 'CRM_Export_StateMachine_Standalone'; - // we need to determine component export - $componentName = explode('_', $formName); $components = array('Contact', 'Contribute', 'Member', 'Event', 'Pledge', 'Case', 'Grant', 'Activity'); - if ($isStandalone) { - $componentName = array('CRM', $this->controller->get('entity')); - } - - $componentMode = $this->controller->get('component_mode'); // FIXME: This should use a modified version of CRM_Contact_Form_Search::getModeValue but it doesn't have all the contexts - switch ($componentMode) { + switch ($this->getQueryMode()) { case CRM_Contact_BAO_Query::MODE_CONTRIBUTE: $entityShortname = 'Contribute'; + $entityDAOName = $entityShortname; break; case CRM_Contact_BAO_Query::MODE_MEMBER: @@ -134,33 +111,41 @@ class CRM_Export_Form_Select extends CRM_Core_Form { case CRM_Contact_BAO_Query::MODE_EVENT: $entityShortname = 'Event'; + $entityDAOName = $entityShortname; break; case CRM_Contact_BAO_Query::MODE_PLEDGE: $entityShortname = 'Pledge'; + $entityDAOName = $entityShortname; break; case CRM_Contact_BAO_Query::MODE_CASE: $entityShortname = 'Case'; + $entityDAOName = $entityShortname; break; case CRM_Contact_BAO_Query::MODE_GRANT: $entityShortname = 'Grant'; + $entityDAOName = $entityShortname; break; case CRM_Contact_BAO_Query::MODE_ACTIVITY: $entityShortname = 'Activity'; + $entityDAOName = $entityShortname; break; default: + // FIXME: Code cleanup, we may not need to do this $componentName code here. + $formName = CRM_Utils_System::getClassName($this->controller->getStateMachine()); + $componentName = explode('_', $formName); + if ($formName == 'CRM_Export_StateMachine_Standalone') { + $componentName = array('CRM', $this->controller->get('entity')); + } $entityShortname = $componentName[1]; // Contact + $entityDAOName = $entityShortname; break; } - if (empty($entityDAOName)) { - $entityDAOName = $entityShortname; - } - if (in_array($entityShortname, $components)) { $this->_exportMode = constant('CRM_Export_Form_Select::' . strtoupper($entityShortname) . '_EXPORT'); $formTaskClassName = "CRM_{$entityShortname}_Form_Task"; @@ -210,7 +195,7 @@ class CRM_Export_Form_Select extends CRM_Core_Form { } } - $formTaskClassName::preProcessCommon($this, !$isStandalone); + $formTaskClassName::preProcessCommon($this); // $component is used on CRM/Export/Form/Select.tpl to display extra information for contact export ($this->_exportMode == self::CONTACT_EXPORT) ? $component = FALSE : $component = TRUE; @@ -358,7 +343,7 @@ FROM {$this->_componentTable} * @return bool|array * mixed true or array of errors */ - static public function formRule($params, $files, $self) { + public static function formRule($params, $files, $self) { $errors = array(); if (CRM_Utils_Array::value('mergeOption', $params) == self::EXPORT_MERGE_SAME_ADDRESS && @@ -386,7 +371,7 @@ FROM {$this->_componentTable} /** * Process the uploaded file. * - * @return void + * @throws \CRM_Core_Exception */ public function postProcess() { $params = $this->controller->exportValues($this->_name); @@ -535,4 +520,13 @@ FROM {$this->_componentTable} return $options; } + /** + * Get the query mode (eg. CRM_Core_BAO_Query::MODE_CASE) + * + * @return int + */ + public function getQueryMode() { + return (int) ($this->queryMode ?: $this->controller->get('component_mode')); + } + } diff --git a/civicrm/CRM/Export/Form/Select/Case.php b/civicrm/CRM/Export/Form/Select/Case.php new file mode 100644 index 0000000000000000000000000000000000000000..c95aba984ae8a14a9c90aa69261397db9abdd624 --- /dev/null +++ b/civicrm/CRM/Export/Form/Select/Case.php @@ -0,0 +1,53 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 5 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2018 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License and the CiviCRM Licensing Exception along | + | with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ + */ + +/** + * + * @package CRM + * @copyright CiviCRM LLC (c) 2004-2018 + */ + +/** + * This class gets the name of the file to upload + */ +class CRM_Export_Form_Select_Case extends CRM_Export_Form_Select { + + /** + * @var int + */ + protected $queryMode = CRM_Contact_BAO_Query::MODE_CASE; + + /** + * Use the form name to create the tpl file name. + * + * @return string + */ + public function getTemplateFileName() { + return 'CRM/Export/Form/Select.tpl'; + } + +} diff --git a/civicrm/CRM/Extension/Mapper.php b/civicrm/CRM/Extension/Mapper.php index 8615513566bbc5fa9e738fca291139cce5849c39..38ff7d1951b78812ad7790f25547ecffd6c691b9 100644 --- a/civicrm/CRM/Extension/Mapper.php +++ b/civicrm/CRM/Extension/Mapper.php @@ -282,7 +282,7 @@ class CRM_Extension_Mapper { $moduleExtensions = NULL; if ($this->cache && !$fresh) { - $moduleExtensions = $this->cache->get($this->cacheKey . '/moduleFiles'); + $moduleExtensions = $this->cache->get($this->cacheKey . '_moduleFiles'); } if (!is_array($moduleExtensions)) { @@ -315,7 +315,7 @@ class CRM_Extension_Mapper { } if ($this->cache) { - $this->cache->set($this->cacheKey . '/moduleFiles', $moduleExtensions); + $this->cache->set($this->cacheKey . '_moduleFiles', $moduleExtensions); } } return $moduleExtensions; @@ -461,7 +461,7 @@ class CRM_Extension_Mapper { $this->infos = array(); $this->moduleExtensions = NULL; if ($this->cache) { - $this->cache->delete($this->cacheKey . '/moduleFiles'); + $this->cache->delete($this->cacheKey . '_moduleFiles'); } // FIXME: How can code so code wrong be so right? CRM_Extension_System::singleton()->getClassLoader()->refresh(); diff --git a/civicrm/CRM/Financial/Page/AJAX.php b/civicrm/CRM/Financial/Page/AJAX.php index 1c3a9bc266396185de2ed4ee1e2aeee99d68b8da..c5a51b5f9937b8144ea2433fdcc4ff323bc9fb79 100644 --- a/civicrm/CRM/Financial/Page/AJAX.php +++ b/civicrm/CRM/Financial/Page/AJAX.php @@ -293,6 +293,8 @@ class CRM_Financial_Page_AJAX { 'civicrm_financial_trxn.currency as currency', 'civicrm_financial_trxn.status_id as status', 'civicrm_financial_trxn.check_number as check_number', + 'civicrm_financial_trxn.card_type_id', + 'civicrm_financial_trxn.pan_truncation', ); $columnHeader = array( diff --git a/civicrm/CRM/Friend/BAO/Friend.php b/civicrm/CRM/Friend/BAO/Friend.php index 2d650fd8d7e3c5286af9d8a9704a5e373a8d38bd..75e65ba21652abe4a277dabeba5a83319ba7dfc0 100644 --- a/civicrm/CRM/Friend/BAO/Friend.php +++ b/civicrm/CRM/Friend/BAO/Friend.php @@ -29,15 +29,22 @@ * * @package CRM * @copyright CiviCRM LLC (c) 2004-2018 - * $Id$ * */ /** - * This class contains the funtions for Friend + * This class contains the functions for Friend * */ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { + + /** + * Tell a friend id in db. + * + * @var int + */ + public $_friendId; + /** */ public function __construct() { @@ -74,13 +81,9 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { */ public static function retrieve(&$params, &$values) { $friend = new CRM_Friend_DAO_Friend(); - $friend->copyValues($params); - $friend->find(TRUE); - CRM_Core_DAO::storeValues($friend, $values); - return $values; } @@ -88,15 +91,17 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { * Takes an associative array and creates a friend object. * * @param array $params - * (reference ) an assoc array of name/value pairs. + * (reference) an assoc array of name/value pairs. * - * @return void + * @throws \CRM_Core_Exception */ public static function create(&$params) { $transaction = new CRM_Core_Transaction(); $mailParams = array(); - //create contact corresponding to each friend + $contactParams = array(); + + // create contact corresponding to each friend foreach ($params['friend'] as $key => $details) { if ($details["first_name"]) { $contactParams[$key] = array( @@ -111,14 +116,15 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { } } - $frndParams = array(); - $frndParams['entity_id'] = $params['entity_id']; - $frndParams['entity_table'] = $params['entity_table']; - self::getValues($frndParams); + $friendParams = [ + 'entity_id' => $params['entity_id'], + 'entity_table' => $params['entity_table'], + ]; + self::getValues($friendParams); $activityTypeId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionValue', 'Tell a Friend', 'value', 'name'); - //create activity + // create activity $activityParams = array( 'source_contact_id' => $params['source_contact_id'], 'source_record_id' => NULL, @@ -127,20 +133,19 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { 'activity_date_time' => date("YmdHis"), 'subject' => ts('Tell a Friend') . ": {$params['title']}", 'details' => $params['suggested_message'], - 'status_id' => 2, + 'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_status_id', 'Completed'), 'is_test' => $params['is_test'], 'campaign_id' => CRM_Utils_Array::value('campaign_id', $params), ); - //activity creation + // activity creation $activity = CRM_Activity_BAO_Activity::create($activityParams); $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate'); $targetID = CRM_Utils_Array::key('Activity Targets', $activityContacts); - //friend contacts creation + // friend contacts creation foreach ($contactParams as $key => $value) { - - //create contact only if it does not exits in db + // create contact only if it does not exits in db $value['email'] = $value['email-Primary']; $contactID = CRM_Contact_BAO_Contact::getFirstDuplicateContact($value, 'Individual', 'Supervised', array(), FALSE); @@ -167,28 +172,28 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { $transaction->commit(); - //process sending of mails + // Process sending of mails $mailParams['title'] = CRM_Utils_Array::value('title', $params); - $mailParams['general_link'] = CRM_Utils_Array::value('general_link', $frndParams); + $mailParams['general_link'] = CRM_Utils_Array::value('general_link', $friendParams); $mailParams['message'] = CRM_Utils_Array::value('suggested_message', $params); - // get domain - $domainDetails = CRM_Core_BAO_Domain::getNameAndEmail(); - list($username, $mailParams['domain']) = explode('@', $domainDetails[1]); + // Default "from email address" is default domain address. + // This is normally overridden by one of the if statements below + list($_, $mailParams['email_from']) = CRM_Core_BAO_Domain::getNameAndEmail(); + list($username, $mailParams['domain']) = explode('@', $mailParams['email_from']); $default = array(); $findProperties = array('id' => $params['entity_id']); if ($params['entity_table'] == 'civicrm_contribution_page') { - $returnProperties = array('receipt_from_email', 'is_email_receipt'); CRM_Core_DAO::commonRetrieve('CRM_Contribute_DAO_ContributionPage', $findProperties, $default, $returnProperties ); - //if is_email_receipt is set then take receipt_from_email - //as from_email + + // if is_email_receipt is set then take receipt_from_email as from_email if (!empty($default['is_email_receipt']) && !empty($default['receipt_from_email'])) { $mailParams['email_from'] = $default['receipt_from_email']; } @@ -197,7 +202,6 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { $mailParams['module'] = 'contribute'; } elseif ($params['entity_table'] == 'civicrm_event') { - $returnProperties = array('confirm_from_email', 'is_email_confirm'); CRM_Core_DAO::commonRetrieve('CRM_Event_DAO_Event', $findProperties, @@ -205,10 +209,7 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { $returnProperties ); - $mailParams['email_from'] = $domainDetails['1']; - - //if is_email_confirm is set then take confirm_from_email - //as from_email + // if is_email_confirm is set then take confirm_from_email as from_email if (!empty($default['is_email_confirm']) && !empty($default['confirm_from_email'])) { $mailParams['email_from'] = $default['confirm_from_email']; } @@ -226,14 +227,14 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { $mailParams['page_url'] = CRM_Utils_System::url($urlPath, "reset=1&id={$params['entity_id']}", TRUE, NULL, FALSE, TRUE); - //send mail + // Send the email self::sendMail($params['source_contact_id'], $mailParams); } /** * Build the form object. * - * @param CRM_Core_Form $form + * @param CRM_Friend_Form $form * Form object. * * @return void @@ -263,12 +264,12 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { } /** - * The function sets the deafult values of the form. + * The function sets the default values of the form. * * @param array $defaults * (reference) the default values. * - * @return booelan + * @return bool * whether anything was found */ public static function getValues(&$defaults) { @@ -283,7 +284,7 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { } /** - * Process that send tell a friend e-mails + * Process that sends tell a friend e-mails * * @param int $contactID * @param array $values @@ -302,28 +303,27 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { $values['email_from'] = $email; } + $templateParams = array( + 'groupName' => 'msg_tpl_workflow_friend', + 'valueName' => 'friend', + 'contactId' => $contactID, + 'tplParams' => array( + $values['module'] => $values['module'], + 'senderContactName' => $fromName, + 'title' => $values['title'], + 'generalLink' => $values['general_link'], + 'pageURL' => $values['page_url'], + 'senderMessage' => $values['message'], + ), + 'from' => "$fromName (via {$values['domain']}) <{$values['email_from']}>", + 'replyTo' => $email, + ); + foreach ($values['email'] as $displayName => $emailTo) { if ($emailTo) { - // FIXME: factor the below out of the foreach loop - CRM_Core_BAO_MessageTemplate::sendTemplate( - array( - 'groupName' => 'msg_tpl_workflow_friend', - 'valueName' => 'friend', - 'contactId' => $contactID, - 'tplParams' => array( - $values['module'] => $values['module'], - 'senderContactName' => $fromName, - 'title' => $values['title'], - 'generalLink' => $values['general_link'], - 'pageURL' => $values['page_url'], - 'senderMessage' => $values['message'], - ), - 'from' => "$fromName (via {$values['domain']}) <{$values['email_from']}>", - 'toName' => $displayName, - 'toEmail' => $emailTo, - 'replyTo' => $email, - ) - ); + $templateParams['toName'] = $displayName; + $templateParams['toEmail'] = $emailTo; + CRM_Core_BAO_MessageTemplate::sendTemplate($templateParams); } } } @@ -336,16 +336,14 @@ class CRM_Friend_BAO_Friend extends CRM_Friend_DAO_Friend { * pairs * * @param array $params - * (reference ) an assoc array of name/value pairs. + * (reference) an assoc array of name/value pairs. * - * @return CRM_Friend_BAO_Friend + * @return CRM_Friend_DAO_Friend */ public static function addTellAFriend(&$params) { $friendDAO = new CRM_Friend_DAO_Friend(); - $friendDAO->copyValues($params); $friendDAO->is_active = CRM_Utils_Array::value('is_active', $params, FALSE); - $friendDAO->save(); return $friendDAO; diff --git a/civicrm/CRM/Friend/Form.php b/civicrm/CRM/Friend/Form.php index 7f0329ac49a125208a43ed2d654e8e1d126f2d2e..9b2a9c9f765540efcb5a4c9a63e36c8eb216eadc 100644 --- a/civicrm/CRM/Friend/Form.php +++ b/civicrm/CRM/Friend/Form.php @@ -45,14 +45,21 @@ class CRM_Friend_Form extends CRM_Core_Form { const NUM_OPTION = 3; /** - * The id of the entity that we are proceessing. + * The id of the entity that we are processing. * * @var int */ protected $_entityId; /** - * The table name of the entity that we are proceessing. + * Tell a friend id in db. + * + * @var int + */ + public $_friendId; + + /** + * The table name of the entity that we are processing. * * @var string */ diff --git a/civicrm/CRM/Grant/Form/Task.php b/civicrm/CRM/Grant/Form/Task.php index 51f505428ee2b13183ec9f0171556f32b5fadb2b..58fd8f9acffa5d5b89aeed1ca610cebed0824108 100644 --- a/civicrm/CRM/Grant/Form/Task.php +++ b/civicrm/CRM/Grant/Form/Task.php @@ -34,31 +34,10 @@ */ /** - * This class generates task actions for CiviEvent - * + * Class for grant form task actions. + * FIXME: This needs refactoring to properly inherit from CRM_Core_Form_Task and share more functions. */ -class CRM_Grant_Form_Task extends CRM_Core_Form { - - /** - * The task being performed. - * - * @var int - */ - protected $_task; - - /** - * The additional clause that we restrict the search with. - * - * @var string - */ - protected $_componentClause = NULL; - - /** - * The array that holds all the component ids. - * - * @var array - */ - protected $_componentIds; +class CRM_Grant_Form_Task extends CRM_Core_Form_Task { /** * The array that holds all the grant ids. @@ -80,9 +59,8 @@ class CRM_Grant_Form_Task extends CRM_Core_Form { /** * @param CRM_Core_Form $form - * @param bool $useTable */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $form->_grantIds = array(); $values = $form->controller->exportValues('Search'); @@ -127,7 +105,7 @@ class CRM_Grant_Form_Task extends CRM_Core_Form { $form->_grantIds = $form->_componentIds = $ids; //set the context for redirection for any task actions - $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $this); + $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form); $urlParams = 'force=1'; if (CRM_Utils_Rule::qfKey($qfKey)) { $urlParams .= "&qfKey=$qfKey"; diff --git a/civicrm/CRM/Group/Form/Edit.php b/civicrm/CRM/Group/Form/Edit.php index a4d944e18f7be0b96feb45d97b5e2f7eb2d35b93..810ca19d1b804053ea275678d0c771e782c4a206 100644 --- a/civicrm/CRM/Group/Form/Edit.php +++ b/civicrm/CRM/Group/Form/Edit.php @@ -364,7 +364,7 @@ WHERE title = %1 * Process the form when submitted. */ public function postProcess() { - CRM_Utils_System::flushCache('CRM_Core_DAO_Group'); + CRM_Utils_System::flushCache(); $updateNestingCache = FALSE; if ($this->_action & CRM_Core_Action::DELETE) { @@ -458,7 +458,7 @@ WHERE title = %1 $potentialParentGroupIds = array_keys($groupNames); } - $parentGroupSelectValues = array('' => '- ' . ts('select group') . ' -'); + $parentGroupSelectValues = array(); foreach ($potentialParentGroupIds as $potentialParentGroupId) { if (array_key_exists($potentialParentGroupId, $groupNames)) { $parentGroupSelectValues[$potentialParentGroupId] = $groupNames[$potentialParentGroupId]; @@ -472,7 +472,7 @@ WHERE title = %1 else { $required = FALSE; } - $form->add('select', 'parents', ts('Add Parent'), $parentGroupSelectValues, $required, array('class' => 'crm-select2')); + $form->add('select', 'parents', ts('Add Parent'), $parentGroupSelectValues, $required, array('class' => 'crm-select2', 'multiple' => TRUE)); } return $parentGroups; diff --git a/civicrm/CRM/Group/Form/Search.php b/civicrm/CRM/Group/Form/Search.php index f05dfdcb830dac242da11adde82d29dcac72c9ed..a8edc042fde030f50750f9def9f3c8e51118e73d 100644 --- a/civicrm/CRM/Group/Form/Search.php +++ b/civicrm/CRM/Group/Form/Search.php @@ -81,6 +81,17 @@ class CRM_Group_Form_Search extends CRM_Core_Form { NULL, NULL, NULL, NULL, ' ' ); + $componentModes = CRM_Contact_Form_Search::getModeSelect(); + if (count($componentModes) > 1) { + $this->add('select', + 'component_mode', + ts('View Results As'), + $componentModes, + FALSE, + array('class' => 'crm-select2') + ); + } + $this->addButtons(array( array( 'type' => 'refresh', @@ -97,7 +108,7 @@ class CRM_Group_Form_Search extends CRM_Core_Form { $params = $this->controller->exportValues($this->_name); $parent = $this->controller->getParent(); if (!empty($params)) { - $fields = array('title', 'created_by', 'group_type', 'visibility', 'active_status', 'inactive_status'); + $fields = array('title', 'created_by', 'group_type', 'visibility', 'active_status', 'inactive_status', 'component_mode'); foreach ($fields as $field) { if (isset($params[$field]) && !CRM_Utils_System::isNull($params[$field]) diff --git a/civicrm/CRM/Group/Page/AJAX.php b/civicrm/CRM/Group/Page/AJAX.php index a1d9211f9f06bc262b10335fff835ebb03de6d57..da2e8f1ea65dcc40bb996930532584ee54accb9c 100644 --- a/civicrm/CRM/Group/Page/AJAX.php +++ b/civicrm/CRM/Group/Page/AJAX.php @@ -54,6 +54,7 @@ class CRM_Group_Page_AJAX { 'created_by' => 'String', 'group_type' => 'String', 'visibility' => 'String', + 'component_mode' => 'String', 'status' => 'Integer', 'parentsOnly' => 'Integer', 'showOrgInfo' => 'Boolean', diff --git a/civicrm/CRM/Logging/Schema.php b/civicrm/CRM/Logging/Schema.php index 996653ce6cd48523e24b998bf3ab0243b3f65279..a24906ef91b1e79ca374af2d5c63847170126e52 100644 --- a/civicrm/CRM/Logging/Schema.php +++ b/civicrm/CRM/Logging/Schema.php @@ -945,7 +945,7 @@ COLS; * but this is the only entity currently available... */ public function getLogTablesForContact() { - $tables = array_keys(CRM_Dedupe_Merger::cidRefs()); + $tables = array_keys(CRM_Core_DAO::getReferencesToContactTable()); return array_intersect($tables, $this->tables); } diff --git a/civicrm/CRM/Mailing/BAO/Mailing.php b/civicrm/CRM/Mailing/BAO/Mailing.php index 8e34d917e12a2350bffe1727c070c04a8560b887..27a46fd693200a7f6b863f5000402840db9351f6 100644 --- a/civicrm/CRM/Mailing/BAO/Mailing.php +++ b/civicrm/CRM/Mailing/BAO/Mailing.php @@ -1453,7 +1453,7 @@ ORDER BY civicrm_email.is_bulkmail DESC } $mailing->domain_id = CRM_Utils_Array::value('domain_id', $params, CRM_Core_Config::domainID()); - if (!isset($params['replyto_email']) && + if (((!$id && empty($params['replyto_email'])) || !isset($params['replyto_email'])) && isset($params['from_email']) ) { $params['replyto_email'] = $params['from_email']; diff --git a/civicrm/CRM/Mailing/BAO/MailingJob.php b/civicrm/CRM/Mailing/BAO/MailingJob.php index 9b2a80b2079c4b10b5e21bfb3bd9176dabe6bcc7..398afeeaa202fab05190c800bcd6d54a8d49eb91 100644 --- a/civicrm/CRM/Mailing/BAO/MailingJob.php +++ b/civicrm/CRM/Mailing/BAO/MailingJob.php @@ -598,6 +598,7 @@ VALUES (%1, %2, %3, %4, %5, %6, %7) $returnProperties = $mailing->getReturnProperties(); $params = $targetParams = $deliveredParams = array(); $count = 0; + $retryGroup = FALSE; // CRM-15702: Sending bulk sms to contacts without e-mail address fails. // Solution is to skip checking for on hold @@ -684,9 +685,11 @@ VALUES (%1, %2, %3, %4, %5, %6, %7) CRM_Core_Error::debug_log_message("SMTP Socket Error or failed to set sender error. Message: $message, Code: $code"); // these are socket write errors which most likely means smtp connection errors - // lets skip them + // lets skip them and reconnect. $smtpConnectionErrors++; if ($smtpConnectionErrors <= 5) { + $mailer->disconnect(); + $retryGroup = TRUE; continue; } @@ -776,6 +779,10 @@ VALUES (%1, %2, %3, %4, %5, %6, %7) $job_date ); + if ($retryGroup) { + return FALSE; + } + return $result; } @@ -817,11 +824,54 @@ AND status IN ( 'Scheduled', 'Running', 'Paused' ) 2 => array(date('YmdHis'), 'Timestamp'), ); CRM_Core_DAO::executeQuery($sql, $params); - - CRM_Core_Session::setStatus(ts('The mailing has been canceled.'), ts('Canceled'), 'success'); } } + /** + * Pause a mailing + * + * @param int $mailingID + * The id of the mailing to be paused. + */ + public static function pause($mailingID) { + $sql = " + UPDATE civicrm_mailing_job + SET status = 'Paused' + WHERE mailing_id = %1 + AND is_test = 0 + AND status IN ('Scheduled', 'Running') + "; + CRM_Core_DAO::executeQuery($sql, array(1 => array($mailingID, 'Integer'))); + } + + /** + * Resume a mailing + * + * @param int $mailingID + * The id of the mailing to be resumed. + */ + public static function resume($mailingID) { + $sql = " + UPDATE civicrm_mailing_job + SET status = 'Scheduled' + WHERE mailing_id = %1 + AND is_test = 0 + AND start_date IS NULL + AND status = 'Paused' + "; + CRM_Core_DAO::executeQuery($sql, array(1 => array($mailingID, 'Integer'))); + + $sql = " + UPDATE civicrm_mailing_job + SET status = 'Running' + WHERE mailing_id = %1 + AND is_test = 0 + AND start_date IS NOT NULL + AND status = 'Paused' + "; + CRM_Core_DAO::executeQuery($sql, array(1 => array($mailingID, 'Integer'))); + } + /** * Return a translated status enum string. * diff --git a/civicrm/CRM/Mailing/Event/BAO/Confirm.php b/civicrm/CRM/Mailing/Event/BAO/Confirm.php index f01e188d8abbeec204e2515664c436d6665b3781..1d3c2d4d18d34bb68d1f70ee20da5ae670494e49 100644 --- a/civicrm/CRM/Mailing/Event/BAO/Confirm.php +++ b/civicrm/CRM/Mailing/Event/BAO/Confirm.php @@ -118,8 +118,6 @@ class CRM_Mailing_Event_BAO_Confirm extends CRM_Mailing_Event_DAO_Confirm { $component->find(TRUE); - $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain(); - $html = $component->body_html; if ($component->body_text) { @@ -143,11 +141,11 @@ class CRM_Mailing_Event_BAO_Confirm extends CRM_Mailing_Event_DAO_Confirm { $mailParams = array( 'groupName' => 'Mailing Event ' . $component->component_type, 'subject' => $component->subject, - 'from' => "\"$domainEmailName\" <do-not-reply@$emailDomain>", + 'from' => "\"$domainEmailName\" <" . CRM_Core_BAO_Domain::getNoReplyEmailAddress() . '>', 'toEmail' => $email, 'toName' => $display_name, - 'replyTo' => "do-not-reply@$emailDomain", - 'returnPath' => "do-not-reply@$emailDomain", + 'replyTo' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), + 'returnPath' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), 'html' => $html, 'text' => $text, ); diff --git a/civicrm/CRM/Mailing/Event/BAO/Reply.php b/civicrm/CRM/Mailing/Event/BAO/Reply.php index 00d8cd3eac0877e84f70d4ce1911df589bff6928..679822ffe024dc8f04803e22a13420e15a0878d5 100644 --- a/civicrm/CRM/Mailing/Event/BAO/Reply.php +++ b/civicrm/CRM/Mailing/Event/BAO/Reply.php @@ -173,8 +173,6 @@ class CRM_Mailing_Event_BAO_Reply extends CRM_Mailing_Event_DAO_Reply { } } else { - $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain(); - if (empty($eq->display_name)) { $from = $eq->email; } @@ -189,7 +187,7 @@ class CRM_Mailing_Event_BAO_Reply extends CRM_Mailing_Event_DAO_Reply { 'To' => $mailing->replyto_email, 'From' => $from, 'Reply-To' => empty($replyto) ? $eq->email : $replyto, - 'Return-Path' => "do-not-reply@{$emailDomain}", + 'Return-Path' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), // CRM-17754 Include re-sent headers to indicate that we have forwarded on the email 'Resent-From' => $domainValues['values'][0]['from_email'], 'Resent-Date' => date('r'), @@ -253,14 +251,12 @@ class CRM_Mailing_Event_BAO_Reply extends CRM_Mailing_Event_DAO_Reply { $domain = CRM_Core_BAO_Domain::getDomain(); list($domainEmailName, $_) = CRM_Core_BAO_Domain::getNameAndEmail(); - $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain(); - $headers = array( 'Subject' => $component->subject, 'To' => $to, - 'From' => "\"$domainEmailName\" <do-not-reply@$emailDomain>", - 'Reply-To' => "do-not-reply@$emailDomain", - 'Return-Path' => "do-not-reply@$emailDomain", + 'From' => "\"$domainEmailName\" <" . CRM_Core_BAO_Domain::getNoReplyEmailAddress() . '>', + 'Reply-To' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), + 'Return-Path' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), ); // TODO: do we need reply tokens? diff --git a/civicrm/CRM/Mailing/Event/BAO/Resubscribe.php b/civicrm/CRM/Mailing/Event/BAO/Resubscribe.php index 483f35641d2d40507fefba4a048524afdf9a0023..360bd36bfec2f85a978dd4543875aae78ecfa77f 100644 --- a/civicrm/CRM/Mailing/Event/BAO/Resubscribe.php +++ b/civicrm/CRM/Mailing/Event/BAO/Resubscribe.php @@ -267,14 +267,12 @@ class CRM_Mailing_Event_BAO_Resubscribe { $message->setTxtBody($text); } - $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain(); - $headers = array( 'Subject' => $component->subject, - 'From' => "\"$domainEmailName\" <do-not-reply@$emailDomain>", + 'From' => "\"$domainEmailName\" <" . CRM_Core_BAO_Domain::getNoReplyEmailAddress() . '>', 'To' => $eq->email, - 'Reply-To' => "do-not-reply@$emailDomain", - 'Return-Path' => "do-not-reply@$emailDomain", + 'Reply-To' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), + 'Return-Path' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), ); CRM_Mailing_BAO_Mailing::addMessageIdHeader($headers, 'e', $job, $queue_id, $eq->hash); $b = CRM_Utils_Mail::setMimeParams($message); diff --git a/civicrm/CRM/Mailing/Event/BAO/Subscribe.php b/civicrm/CRM/Mailing/Event/BAO/Subscribe.php index d3257557b7597bc677829fbcfa97cbf28b50a68e..d82776413b4c2cfe13914b8a551e14cce528c90f 100644 --- a/civicrm/CRM/Mailing/Event/BAO/Subscribe.php +++ b/civicrm/CRM/Mailing/Event/BAO/Subscribe.php @@ -227,7 +227,7 @@ SELECT civicrm_email.id as email_id 'From' => "\"{$domainEmailName}\" <{$domainEmailAddress}>", 'To' => $email, 'Reply-To' => $confirm, - 'Return-Path' => "do-not-reply@$emailDomain", + 'Return-Path' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), ); $url = CRM_Utils_System::url('civicrm/mailing/confirm', diff --git a/civicrm/CRM/Mailing/Event/BAO/Unsubscribe.php b/civicrm/CRM/Mailing/Event/BAO/Unsubscribe.php index 76f3fef7fb6aefa4930e8e87e175926187e0f065..aa6950cb8c8d8be61cb0179636ceb8afb3993aab 100644 --- a/civicrm/CRM/Mailing/Event/BAO/Unsubscribe.php +++ b/civicrm/CRM/Mailing/Event/BAO/Unsubscribe.php @@ -418,10 +418,10 @@ WHERE email = %2 $headers = array( 'Subject' => $component->subject, - 'From' => "\"$domainEmailName\" <do-not-reply@$emailDomain>", + 'From' => "\"$domainEmailName\" <" . CRM_Core_BAO_Domain::getNoReplyEmailAddress() . '>', 'To' => $eq->email, - 'Reply-To' => "do-not-reply@$emailDomain", - 'Return-Path' => "do-not-reply@$emailDomain", + 'Reply-To' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), + 'Return-Path' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(), ); CRM_Mailing_BAO_Mailing::addMessageIdHeader($headers, 'u', $job, $queue_id, $eq->hash); diff --git a/civicrm/CRM/Mailing/Form/Browse.php b/civicrm/CRM/Mailing/Form/Browse.php index c5db18c6aec6f33784d8455eabbc8c153f729787..dad45758b2f9fb457a374dd5f68200fa192c6cd6 100644 --- a/civicrm/CRM/Mailing/Form/Browse.php +++ b/civicrm/CRM/Mailing/Form/Browse.php @@ -82,6 +82,7 @@ class CRM_Mailing_Form_Browse extends CRM_Core_Form { } elseif ($this->_action & CRM_Core_Action::DISABLE) { CRM_Mailing_BAO_MailingJob::cancel($this->_mailingId); + CRM_Core_Session::setStatus(ts('The mailing has been canceled.'), ts('Canceled'), 'success'); } elseif ($this->_action & CRM_Core_Action::RENEW) { //set is_archived to 1 diff --git a/civicrm/CRM/Mailing/Form/Search.php b/civicrm/CRM/Mailing/Form/Search.php index ce959101007017c55911ea1de990ba466752400e..f64d36dd90d79960f0ccd2045b0a499b73dedc74 100644 --- a/civicrm/CRM/Mailing/Form/Search.php +++ b/civicrm/CRM/Mailing/Form/Search.php @@ -92,7 +92,7 @@ class CRM_Mailing_Form_Search extends CRM_Core_Form { $defaults['status_unscheduled'] = 1; } if ($parent->get('scheduled')) { - $statusVals = array('Scheduled', 'Complete', 'Running', 'Canceled'); + $statusVals = array_keys(CRM_Core_SelectValues::getMailingJobStatus()); $defaults['is_archived'] = 0; } if ($parent->get('archived')) { diff --git a/civicrm/CRM/Mailing/Form/Task.php b/civicrm/CRM/Mailing/Form/Task.php index 68ff661c5656152d1d378687f5a6ec1253578f3e..dc9290be3fccbc53436f99f3900fefeb0068d9e8 100644 --- a/civicrm/CRM/Mailing/Form/Task.php +++ b/civicrm/CRM/Mailing/Form/Task.php @@ -32,30 +32,10 @@ */ /** - * This class generates form components for relationship + * Class for mailing form task actions. + * FIXME: This needs refactoring to properly inherit from CRM_Core_Form_Task and share more functions. */ -class CRM_Mailing_Form_Task extends CRM_Core_Form { - - /** - * The task being performed. - * - * @var int - */ - protected $_task; - - /** - * The additional clause that we restrict the search with. - * - * @var string - */ - protected $_componentClause = NULL; - - /** - * The array that holds all the component ids. - * - * @var array - */ - protected $_componentIds; +class CRM_Mailing_Form_Task extends CRM_Core_Form_Task { /** * Build all the data structures needed to build the form. @@ -66,9 +46,8 @@ class CRM_Mailing_Form_Task extends CRM_Core_Form { /** * @param CRM_Core_Form $form - * @param bool $useTable */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $values = $form->controller->exportValues($form->get('searchFormName')); $form->_task = CRM_Utils_Array::value('task', $values); diff --git a/civicrm/CRM/Mailing/Page/Browse.php b/civicrm/CRM/Mailing/Page/Browse.php index d5e2ac1a514c76b938db3b5346bce0f8fc6d7e6b..bb801f45fe4e1db8c1b1d1b07a03255ed9cfd0f5 100644 --- a/civicrm/CRM/Mailing/Page/Browse.php +++ b/civicrm/CRM/Mailing/Page/Browse.php @@ -181,6 +181,7 @@ class CRM_Mailing_Page_Browse extends CRM_Core_Page { if ($this->_action & CRM_Core_Action::DISABLE) { if (CRM_Utils_Request::retrieve('confirmed', 'Boolean', $this)) { CRM_Mailing_BAO_MailingJob::cancel($this->_mailingId); + CRM_Core_Session::setStatus(ts('The mailing has been canceled.'), ts('Canceled'), 'success'); CRM_Utils_System::redirect($context); } else { @@ -192,6 +193,22 @@ class CRM_Mailing_Page_Browse extends CRM_Core_Page { $controller->run(); } } + elseif ($this->_action & CRM_Core_Action::CLOSE) { + if (!CRM_Core_Permission::checkActionPermission('CiviMail', CRM_Core_Action::CLOSE)) { + CRM_Core_Error::fatal(ts('You do not have permission to access this page.')); + } + CRM_Mailing_BAO_MailingJob::pause($this->_mailingId); + CRM_Core_Session::setStatus(ts('The mailing has been paused. Active message deliveries may continue for a few minutes, but CiviMail will not begin delivery of any more batches.'), ts('Paused'), 'success'); + CRM_Utils_System::redirect($context); + } + elseif ($this->_action & CRM_Core_Action::REOPEN) { + if (!CRM_Core_Permission::checkActionPermission('CiviMail', CRM_Core_Action::CLOSE)) { + CRM_Core_Error::fatal(ts('You do not have permission to access this page.')); + } + CRM_Mailing_BAO_MailingJob::resume($this->_mailingId); + CRM_Core_Session::setStatus(ts('The mailing has been resumed.'), ts('Resumed'), 'success'); + CRM_Utils_System::redirect($context); + } elseif ($this->_action & CRM_Core_Action::DELETE) { if (CRM_Utils_Request::retrieve('confirmed', 'Boolean', $this)) { diff --git a/civicrm/CRM/Mailing/Selector/Browse.php b/civicrm/CRM/Mailing/Selector/Browse.php index 4668cf2938f4e0fa0eb070ae05c84670a28db225..8af4a7f00c11013dc3929b15c657fc2738052a96 100644 --- a/civicrm/CRM/Mailing/Selector/Browse.php +++ b/civicrm/CRM/Mailing/Selector/Browse.php @@ -294,6 +294,18 @@ LEFT JOIN civicrm_contact scheduledContact ON ( $mailing.scheduled_id = schedul 'extra' => 'onclick="if (confirm(\'' . $archiveExtra . '\')) this.href+=\'&confirmed=1\'; else return false;"', 'title' => ts('Archive Mailing'), ), + CRM_Core_Action::REOPEN => array( + 'name' => ts('Resume'), + 'url' => 'civicrm/mailing/browse', + 'qs' => 'action=reopen&mid=%%mid%%&reset=1', + 'title' => ts('Resume mailing'), + ), + CRM_Core_Action::CLOSE => array( + 'name' => ts('Pause'), + 'url' => 'civicrm/mailing/browse', + 'qs' => 'action=close&mid=%%mid%%&reset=1', + 'title' => ts('Pause mailing'), + ), ); } @@ -389,6 +401,12 @@ LEFT JOIN civicrm_contact scheduledContact ON ( $mailing.scheduled_id = schedul ) { $actionMask |= CRM_Core_Action::DISABLE; + if ($row['status'] == "Paused") { + $actionMask |= CRM_Core_Action::REOPEN; + } + else { + $actionMask |= CRM_Core_Action::CLOSE; + } } if ($row['status'] == 'Scheduled' && empty($row['approval_status_id']) @@ -550,7 +568,7 @@ LEFT JOIN civicrm_contact scheduledContact ON ( $mailing.scheduled_id = schedul if (!$isFormSubmitted && $this->_parent->get('scheduled')) { // mimic default behavior for scheduled screen $isArchived = 0; - $mailingStatus = array('Scheduled' => 1, 'Complete' => 1, 'Running' => 1, 'Canceled' => 1); + $mailingStatus = array('Scheduled' => 1, 'Complete' => 1, 'Running' => 1, 'Paused' => 1, 'Canceled' => 1); } if (!$isFormSubmitted && $this->_parent->get('archived')) { // mimic default behavior for archived screen diff --git a/civicrm/CRM/Member/BAO/Membership.php b/civicrm/CRM/Member/BAO/Membership.php index ce6df5a60591540670789f03dc7667d3b424ca10..1826901429c0c729392492391a4e8c5ec0ab11fb 100644 --- a/civicrm/CRM/Member/BAO/Membership.php +++ b/civicrm/CRM/Member/BAO/Membership.php @@ -134,7 +134,7 @@ class CRM_Member_BAO_Membership extends CRM_Member_DAO_Membership { $membershipLog['modified_id'] = $membership->contact_id; } - CRM_Member_BAO_MembershipLog::add($membershipLog, CRM_Core_DAO::$_nullArray); + CRM_Member_BAO_MembershipLog::add($membershipLog); // reset the group contact cache since smart groups might be affected due to this CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush(); @@ -1233,7 +1233,7 @@ AND civicrm_membership.is_test = %2"; ) ); - CRM_Member_BAO_MembershipLog::add($logParams, CRM_Core_DAO::$_nullArray); + CRM_Member_BAO_MembershipLog::add($logParams); } } diff --git a/civicrm/CRM/Member/BAO/MembershipType.php b/civicrm/CRM/Member/BAO/MembershipType.php index 66c9cd2a8e9787d72096d645889e70fef5e22df0..796e047a0731b3b474d446b7240f668795a69e39 100644 --- a/civicrm/CRM/Member/BAO/MembershipType.php +++ b/civicrm/CRM/Member/BAO/MembershipType.php @@ -807,41 +807,39 @@ class CRM_Member_BAO_MembershipType extends CRM_Member_DAO_MembershipType { * @param array $params */ public static function updateAllPriceFieldValue($membershipTypeId, $params) { - $updateFields = array(); - if (!empty($params['minimum_fee'])) { - $amount = $params['minimum_fee']; - } - else { - $amount = 0; - } - - $updateValues = array( - 2 => array('financial_type_id', 'financial_type_id', 'Integer'), - 3 => array('label', 'name', 'String'), - 4 => array('amount', 'minimum_fee', 'Float'), - 5 => array('description', 'description', 'String'), + $defaults = array(); + $fieldsToUpdate = array( + 'financial_type_id' => 'financial_type_id', + 'name' => 'label', + 'minimum_fee' => 'amount', + 'description' => 'description', + 'visibility' => 'visibility_id', ); - - $queryParams = array(1 => array($membershipTypeId, 'Integer')); - foreach ($updateValues as $key => $value) { - if (array_key_exists($value[1], $params)) { - $updateFields[] = "cpfv." . $value[0] . " = %$key"; - if ($value[1] == 'minimum_fee') { - $fieldValue = $amount; - } - else { - $fieldValue = $params[$value[1]]; + $priceFieldValueBAO = new CRM_Price_BAO_PriceFieldValue(); + $priceFieldValueBAO->membership_type_id = $membershipTypeId; + $priceFieldValueBAO->find(); + while ($priceFieldValueBAO->fetch()) { + $updateParams = array( + 'id' => $priceFieldValueBAO->id, + 'price_field_id' => $priceFieldValueBAO->price_field_id, + ); + //Get priceset details. + $fieldParams = array('fid' => $priceFieldValueBAO->price_field_id); + $setID = CRM_Price_BAO_PriceSet::getSetId($fieldParams); + $setParams = array('id' => $setID); + $setValues = CRM_Price_BAO_PriceSet::retrieve($setParams, $defaults); + if (!empty($setValues->is_quick_config) && $setValues->name != 'default_membership_type_amount') { + foreach ($fieldsToUpdate as $key => $value) { + if ($value == 'visibility_id' && !empty($params['visibility'])) { + $updateParams['visibility_id'] = CRM_Price_BAO_PriceField::getVisibilityOptionID(strtolower($params['visibility'])); + } + else { + $updateParams[$value] = CRM_Utils_Array::value($key, $params); + } } - $queryParams[$key] = array($fieldValue, $value[2]); + CRM_Price_BAO_PriceFieldValue::add($updateParams); } } - - $query = "UPDATE `civicrm_price_field_value` cpfv -INNER JOIN civicrm_price_field cpf on cpf.id = cpfv.price_field_id -INNER JOIN civicrm_price_set cps on cps.id = cpf.price_set_id -SET " . implode(' , ', $updateFields) . " WHERE cpfv.membership_type_id = %1 -AND cps.is_quick_config = 1 AND cps.name != 'default_membership_type_amount'"; - CRM_Core_DAO::executeQuery($query, $queryParams); } } diff --git a/civicrm/CRM/Member/Form/Task.php b/civicrm/CRM/Member/Form/Task.php index 009b67f201f7c6914c2850c31b0cf961be068710..e8c6e9bfe11c5316be1eb8095dec030bcd1fce99 100644 --- a/civicrm/CRM/Member/Form/Task.php +++ b/civicrm/CRM/Member/Form/Task.php @@ -34,38 +34,10 @@ */ /** - * Class for civimember task actions - * + * Class for member form task actions. + * FIXME: This needs refactoring to properly inherit from CRM_Core_Form_Task and share more functions. */ -class CRM_Member_Form_Task extends CRM_Core_Form { - - /** - * The task being performed. - * - * @var int - */ - protected $_task; - - /** - * The additional clause that we restrict the search with. - * - * @var string - */ - protected $_componentClause = NULL; - - /** - * The array that holds all the component ids. - * - * @var array - */ - protected $_componentIds; - - /** - * The array that holds all the contact ids. - * - * @var array - */ - public $_contactIds; +class CRM_Member_Form_Task extends CRM_Core_Form_Task { /** * The array that holds all the member ids. @@ -87,9 +59,8 @@ class CRM_Member_Form_Task extends CRM_Core_Form { /** * @param CRM_Core_Form $form - * @param bool $useTable */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $form->_memberIds = array(); $values = $form->controller->exportValues($form->get('searchFormName')); diff --git a/civicrm/CRM/Member/Page/RecurringContributions.php b/civicrm/CRM/Member/Page/RecurringContributions.php index 67ef43d1d2ccb82774bd158f705e99620b82557d..67b2b93938145ab593879302bde2485075ddf8f9 100644 --- a/civicrm/CRM/Member/Page/RecurringContributions.php +++ b/civicrm/CRM/Member/Page/RecurringContributions.php @@ -112,14 +112,18 @@ class CRM_Member_Page_RecurringContributions extends CRM_Core_Page { */ private function setActionsForRecurringContribution($recurID, &$recurringContribution) { $action = array_sum(array_keys($this->recurLinks($recurID))); + // no action allowed if it's not active $recurringContribution['is_active'] = ($recurringContribution['contribution_status_id'] != 3); + if ($recurringContribution['is_active']) { $details = CRM_Contribute_BAO_ContributionRecur::getSubscriptionDetails($recurringContribution['id'], 'recur'); $hideUpdate = $details->membership_id & $details->auto_renew; - if ($hideUpdate || empty($details->processor_id)) { + + if ($hideUpdate) { $action -= CRM_Core_Action::UPDATE; } + $recurringContribution['action'] = CRM_Core_Action::formLink( $this->recurLinks($recurID), $action, diff --git a/civicrm/CRM/Member/Task.php b/civicrm/CRM/Member/Task.php index a8843f6e6c91202149766a9a28b75f890d91fb12..572dbae0114dca2ae92a1b171d91be870cb50a77 100644 --- a/civicrm/CRM/Member/Task.php +++ b/civicrm/CRM/Member/Task.php @@ -102,6 +102,16 @@ class CRM_Member_Task extends CRM_Core_Task { 'class' => 'CRM_Member_Form_Task_PDFLetter', 'result' => FALSE, ), + self::SAVE_SEARCH => array( + 'title' => ts('Group - create smart group'), + 'class' => 'CRM_Contact_Form_Task_SaveSearch', + 'result' => TRUE, + ), + self::SAVE_SEARCH_UPDATE => array( + 'title' => ts('Group - update smart group'), + 'class' => 'CRM_Contact_Form_Task_SaveSearch_Update', + 'result' => TRUE, + ), ); //CRM-4418, check for delete diff --git a/civicrm/CRM/Pledge/Form/Task.php b/civicrm/CRM/Pledge/Form/Task.php index 092fece3a3e03124f9c934b41ea47eb0f5a5ca4b..4a8b85561449a409c5fc3beac7be79bbe4698658 100644 --- a/civicrm/CRM/Pledge/Form/Task.php +++ b/civicrm/CRM/Pledge/Form/Task.php @@ -32,30 +32,10 @@ */ /** - * This class generates task actions for CiviEvent. + * Class for pledge form task actions. + * FIXME: This needs refactoring to properly inherit from CRM_Core_Form_Task and share more functions. */ -class CRM_Pledge_Form_Task extends CRM_Core_Form { - - /** - * The task being performed. - * - * @var int - */ - protected $_task; - - /** - * The additional clause that we restrict the search with. - * - * @var string - */ - protected $_componentClause = NULL; - - /** - * The array that holds all the component ids. - * - * @var array - */ - protected $_componentIds; +class CRM_Pledge_Form_Task extends CRM_Core_Form_Task { /** * The array that holds all the pledge ids. @@ -75,9 +55,8 @@ class CRM_Pledge_Form_Task extends CRM_Core_Form { * Common pre-processing. * * @param CRM_Core_Form $form - * @param bool $useTable */ - public static function preProcessCommon(&$form, $useTable = FALSE) { + public static function preProcessCommon(&$form) { $form->_pledgeIds = array(); $values = $form->controller->exportValues('Search'); diff --git a/civicrm/CRM/Profile/Form/Edit.php b/civicrm/CRM/Profile/Form/Edit.php index 601283c1e6f647e02479793a6bdf0424b1e4ca3e..ada0354e0adca562cfc6881e96ca8331df0c7941 100644 --- a/civicrm/CRM/Profile/Form/Edit.php +++ b/civicrm/CRM/Profile/Form/Edit.php @@ -222,6 +222,7 @@ SELECT module,is_reserved $cancelButtonValue = !empty($this->_ufGroup['cancel_button_text']) ? $this->_ufGroup['cancel_button_text'] : ts('Cancel'); $this->assign('cancelButtonText', $cancelButtonValue); + $this->assign('includeCancelButton', CRM_Utils_Array::value('add_cancel_button', $this->_ufGroup)); if (($this->_multiRecord & CRM_Core_Action::DELETE) && $this->_recordExists) { $this->_deleteButtonName = $this->getButtonName('upload', 'delete'); diff --git a/civicrm/CRM/Report/BAO/ReportInstance.php b/civicrm/CRM/Report/BAO/ReportInstance.php index 0b254ee73cb239b696ff3f0aeb6fd57d2fff7402..a7dc5733cb44b26fb43adbe2e376f67caaef8480 100644 --- a/civicrm/CRM/Report/BAO/ReportInstance.php +++ b/civicrm/CRM/Report/BAO/ReportInstance.php @@ -364,24 +364,35 @@ class CRM_Report_BAO_ReportInstance extends CRM_Report_DAO_ReportInstance { * - general script-add. */ public static function getActionMetadata() { - $actions = array( - 'report_instance.save' => array('title' => ts('Save')), - 'report_instance.copy' => array( + $actions = array(); + if (CRM_Core_Permission::check('save Report Criteria')) { + $actions['report_instance.save'] = array('title' => ts('Save')); + $actions['report_instance.copy'] = array( 'title' => ts('Save a Copy'), 'data' => array( 'is_confirm' => TRUE, 'confirm_title' => ts('Save a copy...'), 'confirm_refresh_fields' => json_encode(array( - 'title' => array('selector' => '.crm-report-instanceForm-form-block-title', 'prepend' => ts('(Copy) ')), - 'description' => array('selector' => '.crm-report-instanceForm-form-block-description', 'prepend' => ''), - 'parent_id' => array('selector' => '.crm-report-instanceForm-form-block-parent_id', 'prepend' => ''), + 'title' => array( + 'selector' => '.crm-report-instanceForm-form-block-title', + 'prepend' => ts('(Copy) '), + ), + 'description' => array( + 'selector' => '.crm-report-instanceForm-form-block-description', + 'prepend' => '', + ), + 'parent_id' => array( + 'selector' => '.crm-report-instanceForm-form-block-parent_id', + 'prepend' => '', + ), )), ), - ), - 'report_instance.print' => array('title' => ts('Print Report')), - 'report_instance.pdf' => array('title' => ts('Print to PDF')), - 'report_instance.csv' => array('title' => ts('Export as CSV')), - ); + ); + } + $actions['report_instance.print'] = array('title' => ts('Print Report')); + $actions['report_instance.pdf'] = array('title' => ts('Print to PDF')); + $actions['report_instance.csv'] = array('title' => ts('Export as CSV')); + if (CRM_Core_Permission::check('administer Reports')) { $actions['report_instance.delete'] = array( 'title' => ts('Delete report'), diff --git a/civicrm/CRM/Report/Form.php b/civicrm/CRM/Report/Form.php index 97f2e78d0f34066d08ed752b27093369ba75df04..3d1ebc34562439e7b4829c98362c97dde192d74a 100644 --- a/civicrm/CRM/Report/Form.php +++ b/civicrm/CRM/Report/Form.php @@ -488,6 +488,14 @@ class CRM_Report_Form extends CRM_Core_Form { protected $sqlArray; + /** + * Tables created for the report that need removal afterwards. + * + * ['civicrm_temp_report_x' => ['temporary' => TRUE, 'name' => 'civicrm_temp_report_x'] + * @var array + */ + protected $temporaryTables = []; + /** * Can this report use the sql mode ONLY_FULL_GROUP_BY. * @var bool @@ -1114,6 +1122,15 @@ class CRM_Report_Form extends CRM_Core_Form { return $this->_defaults; } + /** + * Remove any temporary tables. + */ + public function cleanUpTemporaryTables() { + foreach ($this->temporaryTables as $temporaryTable) { + CRM_Core_DAO::executeQuery('DROP ' . ($temporaryTable['temporary'] ? 'TEMPORARY' : '') . ' TABLE IF EXISTS ' . $temporaryTable['name']); + } + } + /** * Add columns to report. */ @@ -3672,7 +3689,7 @@ WHERE cg.extends IN ('" . implode("','", $this->_customGroupExtends) . "') AND WHERE smartgroup_contact.group_id IN ({$smartGroups}) "; } - $this->groupTempTable = 'civicrm_report_temp_group_' . date('Ymd_') . uniqid(); + $this->groupTempTable = CRM_Utils_SQL_TempTable::build()->setCategory('rptgrp')->setId(date('Ymd_') . uniqid())->getName(); $this->executeReportQuery(" CREATE TEMPORARY TABLE $this->groupTempTable $this->_databaseAttributes $query @@ -5357,6 +5374,22 @@ LEFT JOIN civicrm_contact {$field['alias']} ON {$field['alias']}.id = {$this->_a 'title' => $options['prefix_label'] . ts('Nick Name'), 'is_fields' => TRUE, ), + $options['prefix'] . 'prefix_id' => array( + 'name' => 'prefix_id', + 'title' => $options['prefix_label'] . ts('Prefix'), + 'options' => CRM_Contact_BAO_Contact::buildOptions('prefix_id'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'is_fields' => TRUE, + 'is_filters' => TRUE, + ), + $options['prefix'] . 'suffix_id' => array( + 'name' => 'suffix_id', + 'title' => $options['prefix_label'] . ts('Suffix'), + 'options' => CRM_Contact_BAO_Contact::buildOptions('suffix_id'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'is_fields' => TRUE, + 'is_filters' => TRUE, + ), $options['prefix'] . 'gender_id' => array( 'name' => 'gender_id', 'title' => $options['prefix_label'] . ts('Gender'), diff --git a/civicrm/CRM/Report/Form/Contribute/Bookkeeping.php b/civicrm/CRM/Report/Form/Contribute/Bookkeeping.php index 088e2d730e85975ef1e7305e83e25915982953e4..8e4c25d1e3ddc6af814d04007b890892837b074b 100644 --- a/civicrm/CRM/Report/Form/Contribute/Bookkeeping.php +++ b/civicrm/CRM/Report/Form/Contribute/Bookkeeping.php @@ -301,6 +301,10 @@ class CRM_Report_Form_Contribute_Bookkeeping extends CRM_Report_Form { 'title' => ts('Contribution Status'), 'default' => TRUE, ), + 'contribution_source' => array( + 'title' => ts('Source'), + 'name' => 'source', + ), 'id' => array( 'title' => ts('Contribution ID'), 'default' => TRUE, @@ -314,6 +318,11 @@ class CRM_Report_Form_Contribute_Bookkeeping extends CRM_Report_Form { 'type' => CRM_Utils_Type::T_INT, ), 'receive_date' => array('operatorType' => CRM_Report_Form::OP_DATE), + 'contribution_source' => array( + 'title' => ts('Source'), + 'name' => 'source', + 'type' => CRM_Utils_Type::T_STRING, + ), 'contribution_status_id' => array( 'title' => ts('Contribution Status'), 'operatorType' => CRM_Report_Form::OP_MULTISELECT, diff --git a/civicrm/CRM/Report/Form/Contribute/Detail.php b/civicrm/CRM/Report/Form/Contribute/Detail.php index 7910fa1453c5ea0a025e29c6a97ce209d62507da..3619a474093ee77d4ce811d0fedc4bb17219838c 100644 --- a/civicrm/CRM/Report/Form/Contribute/Detail.php +++ b/civicrm/CRM/Report/Form/Contribute/Detail.php @@ -46,6 +46,32 @@ class CRM_Report_Form_Contribute_Detail extends CRM_Report_Form { protected $groupConcatTested = TRUE; + protected $isTempTableBuilt = FALSE; + + /** + * Query mode. + * + * This can be 'Main' or 'SoftCredit' to denote which query we are building. + * + * @var string + */ + protected $queryMode = 'Main'; + + /** + * Is this report being run on contributions as the base entity. + * + * The report structure is generally designed around a base entity but + * depending on input it can be run in a sort of hybrid way that causes a lot + * of complexity. + * + * If it is in isContributionsOnlyMode we can simplify. + * + * (arguably there should be 2 separate report templates, not one doing double duty.) + * + * @var bool + */ + protected $isContributionBaseMode = FALSE; + /** * This report has been optimised for group filtering. * @@ -465,34 +491,59 @@ GROUP BY {$this->_aliases['civicrm_contribution']}.currency"; } /** - * This function appears to have been overrriden for the purposes of facilitating soft credits in the report. + * Build the report query. * - * The report appears to have 2 different functions: - * 1) contribution report - * 2) soft credit report - showing a row per 'payment engagement' (payment or soft credit). There is a separate - * soft credit report as well. + * @param bool $applyLimit * - * Somewhat confusingly this report returns multiple rows per contribution when soft credits are included. It feels - * like there is a case to split it into 2 separate reports. - * - * Soft credit functionality is not currently unit tested for this report. + * @return string */ - public function postProcess() { - // @todo in order to make this report testable we need to remove this function override in favour of the - // functions called by the reportTemplate.getrows api - this requires a bit of tidy up! - // get the acl clauses built before we assemble the query - $this->buildACLClause($this->_aliases['civicrm_contact']); + public function buildQuery($applyLimit = TRUE) { + if ($this->isTempTableBuilt) { + return "SELECT * FROM civireport_contribution_detail_temp3 $this->_orderBy"; + } + return parent::buildQuery($applyLimit); + } - $this->beginPostProcess(); + /** + * Shared function for preliminary processing. + * + * This is called by the api / unit tests and the form layer and is + * the right place to do 'initial analysis of input'. + */ + public function beginPostProcessCommon() { + // CRM-18312 - display soft_credits and soft_credits_for column + // when 'Contribution or Soft Credit?' column is not selected + if (empty($this->_params['fields']['contribution_or_soft'])) { + $this->_params['fields']['contribution_or_soft'] = 1; + $this->noDisplayContributionOrSoftColumn = TRUE; + } + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == 'contributions_only') { + $this->isContributionBaseMode = TRUE; + } + if ($this->isContributionBaseMode && + (!empty($this->_params['fields']['soft_credit_type_id']) + || !empty($this->_params['soft_credit_type_id_value'])) + ) { + unset($this->_params['fields']['soft_credit_type_id']); + if (!empty($this->_params['soft_credit_type_id_value'])) { + $this->_params['soft_credit_type_id_value'] = array(); + CRM_Core_Session::setStatus(ts('Is it not possible to filter on soft contribution type when not including soft credits.')); + } + } // 1. use main contribution query to build temp table 1 $sql = $this->buildQuery(); $tempQuery = "CREATE TEMPORARY TABLE civireport_contribution_detail_temp1 {$this->_databaseAttributes} AS {$sql}"; - $this->addToDeveloperTab($tempQuery); - CRM_Core_DAO::executeQuery($tempQuery); + $this->temporaryTables['civireport_contribution_detail_temp1'] = ['name' => 'civireport_contribution_detail_temp1', 'temporary' => TRUE]; + $this->executeReportQuery($tempQuery); $this->setPager(); // 2. customize main contribution query for soft credit, and build temp table 2 with soft credit contributions only + $this->queryMode = 'SoftCredit'; + // Rebuild select with no groupby. Do not let column headers change. + $headers = $this->_columnHeaders; + $this->select(); + $this->_columnHeaders = $headers; $this->softCreditFrom(); // also include custom group from if included // since this might be included in select @@ -504,11 +555,13 @@ GROUP BY {$this->_aliases['civicrm_contribution']}.currency"; if (!empty($this->_groupBy) && !$this->noDisplayContributionOrSoftColumn) { $this->_groupBy .= ', contribution_soft_civireport.amount'; } - // we inner join with temp1 to restrict soft contributions to those in temp1 table - $sql = "{$select} {$this->_from} {$this->_where} {$this->_groupBy}"; + // we inner join with temp1 to restrict soft contributions to those in temp1 table. + // no group by here as we want to display as many soft credit rows as actually exist. + $sql = "{$select} {$this->_from} {$this->_where}"; $tempQuery = "CREATE TEMPORARY TABLE civireport_contribution_detail_temp2 {$this->_databaseAttributes} AS {$sql}"; - $this->addToDeveloperTab($tempQuery); - CRM_Core_DAO::executeQuery($tempQuery); + $this->executeReportQuery($tempQuery); + $this->temporaryTables['civireport_contribution_detail_temp2'] = ['name' => 'civireport_contribution_detail_temp2', 'temporary' => TRUE]; + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == 'soft_credits_only' ) { @@ -527,66 +580,41 @@ GROUP BY {$this->_aliases['civicrm_contribution']}.currency"; $this->customDataFrom(); // 3. Decide where to populate temp3 table from - if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == - 'contributions_only' + if ($this->isContributionBaseMode ) { - $tempQuery = "(SELECT * FROM civireport_contribution_detail_temp1)"; + $this->executeReportQuery( + "CREATE TEMPORARY TABLE civireport_contribution_detail_temp3 {$this->_databaseAttributes} AS (SELECT * FROM civireport_contribution_detail_temp1)" + ); } elseif (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == 'soft_credits_only' ) { - $tempQuery = "(SELECT * FROM civireport_contribution_detail_temp2)"; + $this->executeReportQuery( + "CREATE TEMPORARY TABLE civireport_contribution_detail_temp3 {$this->_databaseAttributes} AS (SELECT * FROM civireport_contribution_detail_temp2)" + ); } else { - $tempQuery = " + $this->executeReportQuery("CREATE TEMPORARY TABLE civireport_contribution_detail_temp3 {$this->_databaseAttributes} (SELECT * FROM civireport_contribution_detail_temp1) UNION ALL -(SELECT * FROM civireport_contribution_detail_temp2)"; +(SELECT * FROM civireport_contribution_detail_temp2)"); } - - // 4. build temp table 3 - $sql = "CREATE TEMPORARY TABLE civireport_contribution_detail_temp3 {$this->_databaseAttributes} AS {$tempQuery}"; - $this->addToDeveloperTab($sql); - CRM_Core_DAO::executeQuery($sql); - - // 6. show result set from temp table 3 - $rows = array(); - $sql = "SELECT * FROM civireport_contribution_detail_temp3 $this->_orderBy"; - $this->buildRows($sql, $rows); - - // format result set. - $this->formatDisplay($rows, FALSE); - - // assign variables to templates - $this->doTemplateAssignment($rows); - // do print / pdf / instance stuff if needed - $this->endPostProcess($rows); + $this->temporaryTables['civireport_contribution_detail_temp3'] = ['name' => 'civireport_contribution_detail_temp3', 'temporary' => TRUE]; + $this->isTempTableBuilt = TRUE; } /** - * Shared function for preliminary processing. + * Store group bys into array - so we can check elsewhere what is grouped. * - * This is called by the api / unit tests and the form layer and is - * the right place to do 'initial analysis of input'. + * If we are generating a table of soft credits we do not want to be using + * group by. */ - public function beginPostProcessCommon() { - // CRM-18312 - display soft_credits and soft_credits_for column - // when 'Contribution or Soft Credit?' column is not selected - if (empty($this->_params['fields']['contribution_or_soft'])) { - $this->_params['fields']['contribution_or_soft'] = 1; - $this->noDisplayContributionOrSoftColumn = TRUE; + protected function storeGroupByArray() { + if ($this->queryMode === 'SoftCredit') { + $this->_groupByArray = []; } - - if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == - 'contributions_only' && - (!empty($this->_params['fields']['soft_credit_type_id']) - || !empty($this->_params['soft_credit_type_id_value'])) - ) { - unset($this->_params['fields']['soft_credit_type_id']); - if (!empty($this->_params['soft_credit_type_id_value'])) { - $this->_params['soft_credit_type_id_value'] = array(); - CRM_Core_Session::setStatus(ts('Is it not possible to filter on soft contribution type when not including soft credits.')); - } + else { + parent::storeGroupByArray(); } } @@ -600,7 +628,6 @@ UNION ALL * Rows generated by SQL, with an array for each row. */ public function alterDisplay(&$rows) { - $checkList = array(); $entryFound = FALSE; $display_flag = $prev_cid = $cid = 0; $contributionTypes = CRM_Contribute_PseudoConstant::financialType(); @@ -717,7 +744,7 @@ UNION ALL array_key_exists('civicrm_contribution_contribution_id', $row) ) { $query = " -SELECT civicrm_contact_id, civicrm_contact_sort_name, civicrm_contribution_total_amount_sum, civicrm_contribution_currency +SELECT civicrm_contact_id, civicrm_contact_sort_name, civicrm_contribution_total_amount, civicrm_contribution_currency FROM civireport_contribution_detail_temp2 WHERE civicrm_contribution_contribution_id={$row['civicrm_contribution_contribution_id']}"; $this->addToDeveloperTab($query); @@ -729,7 +756,7 @@ WHERE civicrm_contribution_contribution_id={$row['civicrm_contribution_contribu $dao->civicrm_contact_id); $string = $string . ($string ? $separator : '') . "<a href='{$url}'>{$dao->civicrm_contact_sort_name}</a> " . - CRM_Utils_Money::format($dao->civicrm_contribution_total_amount_sum, $dao->civicrm_contribution_currency); + CRM_Utils_Money::format($dao->civicrm_contribution_total_amount, $dao->civicrm_contribution_currency); } $rows[$rowNum]['civicrm_contribution_soft_credits'] = $string; } diff --git a/civicrm/CRM/Report/Form/Contribute/History.php b/civicrm/CRM/Report/Form/Contribute/History.php index 117ece48366bc7606b11834112bc1dd3aaed0f96..ac23fd4bfd46be21b1821fe5608c775f00af8e95 100644 --- a/civicrm/CRM/Report/Form/Contribute/History.php +++ b/civicrm/CRM/Report/Form/Contribute/History.php @@ -867,6 +867,8 @@ class CRM_Report_Form_Contribute_History extends CRM_Report_Form { $entryFound = TRUE; } + $entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, NULL, NULL) ? TRUE : $entryFound; + } } diff --git a/civicrm/CRM/Report/Form/Contribute/Lybunt.php b/civicrm/CRM/Report/Form/Contribute/Lybunt.php index 0fb7f093a5d8ec4e0e09cb1981322d15687e3ad7..e5aeb0fcba2f95406d6660517d0b62887b23f10b 100644 --- a/civicrm/CRM/Report/Form/Contribute/Lybunt.php +++ b/civicrm/CRM/Report/Form/Contribute/Lybunt.php @@ -567,7 +567,7 @@ class CRM_Report_Form_Contribute_Lybunt extends CRM_Report_Form { // @todo this acl has no test coverage and is very hard to test manually so could be fragile. $this->resetFormSqlAndWhereHavingClauses(); - $this->contactTempTable = 'civicrm_report_temp_lybunt_c_' . date('Ymd_') . uniqid(); + $this->contactTempTable = CRM_Utils_SQL_TempTable::build()->setCategory('rptlybunt')->setId(date('Ymd_') . uniqid())->getName(); $this->limit(); $getContacts = " CREATE TEMPORARY TABLE $this->contactTempTable {$this->_databaseAttributes} diff --git a/civicrm/CRM/Report/Info.php b/civicrm/CRM/Report/Info.php index 83cc3bbbe5d5c4e14f20970ca94843a4bfdcb76b..8d8c429a5deb3c57505cbaca3764f771bc772c75 100644 --- a/civicrm/CRM/Report/Info.php +++ b/civicrm/CRM/Report/Info.php @@ -87,6 +87,10 @@ class CRM_Report_Info extends CRM_Core_Component_Info { ts('access Report Criteria'), ts('Change report search criteria'), ), + 'save Report Criteria' => array( + ts('save Report Criteria'), + ts('Save report search criteria'), + ), 'administer private reports' => array( ts('administer private reports'), ts('Edit all private reports'), diff --git a/civicrm/CRM/SMS/Form/Provider.php b/civicrm/CRM/SMS/Form/Provider.php index c5496c2b5710da75bc83e1e47beefe1c013fcfb5..f6aace804029dfa3e370b5eb9a0eaf9a2d8d26af 100644 --- a/civicrm/CRM/SMS/Form/Provider.php +++ b/civicrm/CRM/SMS/Form/Provider.php @@ -159,7 +159,7 @@ class CRM_SMS_Form_Provider extends CRM_Core_Form { */ public function postProcess() { - CRM_Utils_System::flushCache('CRM_SMS_DAO_Provider'); + CRM_Utils_System::flushCache(); if ($this->_action & CRM_Core_Action::DELETE) { CRM_SMS_BAO_Provider::del($this->_id); diff --git a/civicrm/CRM/Tag/Form/Tag.php b/civicrm/CRM/Tag/Form/Tag.php index ed75c7017c333f3f5d46d025ac6ac3069ec9a833..765c71eb7747372bfd480e86e9187d98bee7bcf5 100644 --- a/civicrm/CRM/Tag/Form/Tag.php +++ b/civicrm/CRM/Tag/Form/Tag.php @@ -113,7 +113,7 @@ class CRM_Tag_Form_Tag extends CRM_Core_Form { * @return void */ public function postProcess() { - CRM_Utils_System::flushCache('CRM_Core_DAO_Tag'); + CRM_Utils_System::flushCache(); // array contains the posted values // exportvalues is not used because its give value 1 of the checkbox which were checked by default, diff --git a/civicrm/CRM/UF/Form/AdvanceSetting.php b/civicrm/CRM/UF/Form/AdvanceSetting.php index 617940127f9838243cb041060a7634fb81d084d4..01db60e8b9e9c318e893e71aff83e534a2577391 100644 --- a/civicrm/CRM/UF/Form/AdvanceSetting.php +++ b/civicrm/CRM/UF/Form/AdvanceSetting.php @@ -50,6 +50,8 @@ class CRM_UF_Form_AdvanceSetting extends CRM_UF_Form_Group { $form->addGroup($options, 'is_update_dupe', ts('What to do upon duplicate match')); // we do not have any url checks to allow relative urls $form->addElement('text', 'post_URL', ts('Redirect URL'), CRM_Core_DAO::getAttribute('CRM_Core_DAO_UFGroup', 'post_URL')); + + $form->add('advcheckbox', 'add_cancel_button', ts('Include Cancel Button?')); $form->addElement('text', 'cancel_URL', ts('Cancel Redirect URL'), CRM_Core_DAO::getAttribute('CRM_Core_DAO_UFGroup', 'cancel_URL')); $form->addElement('text', 'cancel_button_text', ts('Cancel Button Text'), CRM_Core_DAO::getAttribute('CRM_Core_DAO_UFGroup', 'cancel_button_text')); $form->addElement('text', 'submit_button_text', ts('Submit Button Text'), CRM_Core_DAO::getAttribute('CRM_Core_DAO_UFGroup', 'submit_button_text')); diff --git a/civicrm/CRM/UF/Form/Group.php b/civicrm/CRM/UF/Form/Group.php index 9adc2485035fe0ff34bf0f0604e581e00ddf5ba7..9fcb8341766d4ee2fdef0e26e4735edbc17d14ee 100644 --- a/civicrm/CRM/UF/Form/Group.php +++ b/civicrm/CRM/UF/Form/Group.php @@ -270,6 +270,7 @@ class CRM_UF_Form_Group extends CRM_Core_Form { } } else { + $defaults['add_cancel_button'] = 1; $defaults['is_active'] = 1; $defaults['is_map'] = 0; $defaults['is_update_dupe'] = 0; diff --git a/civicrm/CRM/Upgrade/Form.php b/civicrm/CRM/Upgrade/Form.php index b2b09f08f2a61e32c7c0726c311d8c98491e47c4..7a6ad203c86f2612406d8de654b4f2a440b3ee4a 100644 --- a/civicrm/CRM/Upgrade/Form.php +++ b/civicrm/CRM/Upgrade/Form.php @@ -769,6 +769,7 @@ SET version = '$version' foreach ($revisions as $rev) { if (version_compare($currentVer, $rev) < 0) { $versionObject = $this->incrementalPhpObject($rev); + CRM_Upgrade_Incremental_General::updateMessageTemplate($preUpgradeMessage, $rev); if (is_callable(array($versionObject, 'setPreUpgradeMessage'))) { $versionObject->setPreUpgradeMessage($preUpgradeMessage, $rev, $currentVer); } diff --git a/civicrm/CRM/Upgrade/Incremental/Base.php b/civicrm/CRM/Upgrade/Incremental/Base.php index 1cbf1cc3f57c23a3652ec220dce7550e209d0c36..b180904d70a3ee280cfde4108cd4f1078cf901f7 100644 --- a/civicrm/CRM/Upgrade/Incremental/Base.php +++ b/civicrm/CRM/Upgrade/Incremental/Base.php @@ -183,6 +183,18 @@ class CRM_Upgrade_Incremental_Base { return TRUE; } + /** + * Do any relevant message template updates. + * + * @param CRM_Queue_TaskContext $ctx + * @param string $version + */ + public static function updateMessageTemplates($ctx, $version) { + $messageTemplateObject = new CRM_Upgrade_Incremental_MessageTemplates($version); + $messageTemplateObject->updateTemplates(); + + } + /** * Drop a column from a table if it exist. * diff --git a/civicrm/CRM/Upgrade/Incremental/General.php b/civicrm/CRM/Upgrade/Incremental/General.php index 9f0ebe39c922aebc2a7d378ce973dc5834924125..3575ce91c9aa66407d601bbdb33938abe8c89eec 100644 --- a/civicrm/CRM/Upgrade/Incremental/General.php +++ b/civicrm/CRM/Upgrade/Incremental/General.php @@ -117,13 +117,37 @@ class CRM_Upgrade_Incremental_General { } } + /** + * Perform any message template updates. 5.0+. + * @param $message + * @param $version + */ + public static function updateMessageTemplate(&$message, $version) { + if (version_compare($version, 5.0, '<')) { + return; + } + $messageObj = new CRM_Upgrade_Incremental_MessageTemplates($version); + $messages = $messageObj->getUpgradeMessages(); + if (empty($messages)) { + return; + } + $message .= '<br />' . ts("The default copies of the message templates listed below will be updated to handle new features or correct a problem. Your installation has customized versions of these message templates, and you will need to apply the updates manually after running this upgrade. <a href='%1' style='color:white; text-decoration:underline; font-weight:bold;' target='_blank'>Click here</a> for detailed instructions. %2", array( + 1 => 'http://wiki.civicrm.org/confluence/display/CRMDOC/Message+Templates#MessageTemplates-UpgradesandCustomizedSystemWorkflowTemplates', + 2 => '<ul><l>' . implode('</li><li>', $messages) . '</li></ul>', + )); + + $messageObj->updateTemplates(); + } + /** * @param $message * @param $latestVer * @param $currentVer */ public static function checkMessageTemplate(&$message, $latestVer, $currentVer) { - + if (version_compare($currentVer, 5.0, '>')) { + return; + } $sql = "SELECT orig.workflow_id as workflow_id, orig.msg_title as title FROM civicrm_msg_template diverted JOIN civicrm_msg_template orig ON ( diff --git a/civicrm/CRM/Upgrade/Incremental/MessageTemplates.php b/civicrm/CRM/Upgrade/Incremental/MessageTemplates.php new file mode 100644 index 0000000000000000000000000000000000000000..7b11e199df9e34e062c4bd00bb98bc0c2aea0c38 --- /dev/null +++ b/civicrm/CRM/Upgrade/Incremental/MessageTemplates.php @@ -0,0 +1,155 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 5 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2018 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License and the CiviCRM Licensing Exception along | + | with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ + */ + +/** + * + * @package CRM + * @copyright CiviCRM LLC (c) 2004-2018 + */ +class CRM_Upgrade_Incremental_MessageTemplates { + + /** + * Version we are upgrading to. + * + * @var string + */ + protected $upgradeVersion; + + /** + * @return string + */ + public function getUpgradeVersion() { + return $this->upgradeVersion; + } + + /** + * @param string $upgradeVersion + */ + public function setUpgradeVersion($upgradeVersion) { + $this->upgradeVersion = $upgradeVersion; + } + + /** + * CRM_Upgrade_Incremental_MessageTemplates constructor. + * + * @param string $upgradeVersion + */ + public function __construct($upgradeVersion) { + $this->setUpgradeVersion($upgradeVersion); + } + + /** + * Get any templates that have been updated. + * + * @return array + */ + protected function getTemplateUpdates() { + return [ + [ + 'version' => '5.4.alpha1', + 'upgrade_descriptor' => ts('Use email greeting at top where available'), + 'templates' => [ + ['name' => 'membership_online_receipt', 'type' => 'text'], + ['name' => 'membership_online_receipt', 'type' => 'html'], + ['name' => 'contribution_online_receipt', 'type' => 'text'], + ['name' => 'contribution_online_receipt', 'type' => 'html'], + ['name' => 'event_online_receipt', 'type' => 'text'], + ['name' => 'event_online_receipt', 'type' => 'html'], + ['name' => 'event_online_receipt', 'type' => 'subject'], + ] + ], + ]; + } + + /** + * Get any required template updates. + * + * @return array + */ + public function getTemplatesToUpdate() { + $templates = $this->getTemplateUpdates(); + $return = []; + foreach ($templates as $templateArray) { + if ($templateArray['version'] === $this->getUpgradeVersion()) { + foreach ($templateArray['templates'] as $template) { + $return[$template['name'] . '_' . $template['type']] = array_merge($template, $templateArray); + } + } + } + return $return; + } + + /** + * Get the upgrade messages. + */ + public function getUpgradeMessages() { + $updates = $this->getTemplatesToUpdate(); + $messages = []; + foreach ($updates as $key => $value) { + $templateLabel = civicrm_api3('OptionValue', 'getvalue', [ + 'return' => 'label', + 'name' => $value['name'], + 'options' => ['limit' => 1], + ]); + $messages[$templateLabel] = $value['upgrade_descriptor']; + } + return $messages; + } + + /** + * Update message templates. + */ + public function updateTemplates() { + $templates = $this->getTemplatesToUpdate(); + foreach ($templates as $template) { + $workFlowID = CRM_Core_DAO::singleValueQuery("SELECT MAX(id) as id FROM civicrm_option_value WHERE name = %1", [ + 1 => [$template['name'], 'String'], + ]); + $content = file_get_contents(\Civi::paths()->getPath('[civicrm.root]/xml/templates/message_templates/' . $template['name'] . '_' . $template['type'] . '.tpl')); + $templatesToUpdate = []; + $templatesToUpdate[] = CRM_Core_DAO::singleValueQuery("SELECT id FROM civicrm_msg_template WHERE workflow_id = $workFlowID AND is_reserved = 1"); + $defaultTemplateID = CRM_Core_DAO::singleValueQuery(" + SELECT default_template.id FROM civicrm_msg_template reserved + LEFT JOIN civicrm_msg_template default_template + ON reserved.workflow_id = default_template.workflow_id + WHERE reserved.workflow_id = $workFlowID + AND reserved.is_reserved = 1 AND default_template.is_default = 1 AND reserved.id <> default_template.id + AND reserved.msg_{$template['type']} = default_template.msg_{$template['type']} + "); + if ($defaultTemplateID) { + $templatesToUpdate[] = $defaultTemplateID; + } + + CRM_Core_DAO::executeQuery(" + UPDATE civicrm_msg_template SET msg_{$template['type']} = %1 WHERE id IN (" . implode(',', $templatesToUpdate) . ")", [ + 1 => [$content, 'String'] + ] + ); + } + } + +} diff --git a/civicrm/CRM/Upgrade/Incremental/php/FiveFour.php b/civicrm/CRM/Upgrade/Incremental/php/FiveFour.php new file mode 100644 index 0000000000000000000000000000000000000000..9c4082c0d7ce2da1fc3283035f706a6f178b7393 --- /dev/null +++ b/civicrm/CRM/Upgrade/Incremental/php/FiveFour.php @@ -0,0 +1,137 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 5 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2017 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License along with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ + */ + +/** + * Upgrade logic for FiveFour */ +class CRM_Upgrade_Incremental_php_FiveFour extends CRM_Upgrade_Incremental_Base { + + /** + * Compute any messages which should be displayed beforeupgrade. + * + * Note: This function is called iteratively for each upcoming + * revision to the database. + * + * @param string $preUpgradeMessage + * @param string $rev + * a version number, e.g. '4.4.alpha1', '4.4.beta3', '4.4.0'. + * @param null $currentVer + */ + public function setPreUpgradeMessage(&$preUpgradeMessage, $rev, $currentVer = NULL) { + // Example: Generate a pre-upgrade message. + // if ($rev == '5.12.34') { + // $preUpgradeMessage .= '<p>' . ts('A new permission has been added called %1 This Permission is now used to control access to the Manage Tags screen', array(1 => 'manage tags')) . '</p>'; + // } + } + + /** + * Compute any messages which should be displayed after upgrade. + * + * @param string $postUpgradeMessage + * alterable. + * @param string $rev + * an intermediate version; note that setPostUpgradeMessage is called repeatedly with different $revs. + */ + public function setPostUpgradeMessage(&$postUpgradeMessage, $rev) { + $postUpgradeMessage .= '<p>' . ts('A new %1 permission has been added. It is not granted by default. If your users create reports, you may wish to review your permissions.', array(1 => 'save Report Criteria')) . '</p>'; + // Example: Generate a post-upgrade message. + // if ($rev == '5.12.34') { + // $postUpgradeMessage .= '<br /><br />' . ts("By default, CiviCRM now disables the ability to import directly from SQL. To use this feature, you must explicitly grant permission 'import SQL datasource'."); + // } + } + + /** + * Upgrade function. + * + * @param string $rev + */ + public function upgrade_5_4_alpha1($rev) { + $this->addTask(ts('Upgrade DB to %1: SQL', array(1 => $rev)), 'runSql', $rev); + $this->addTask('Add Cancel Button Setting to the Profile', 'addColumn', + 'civicrm_uf_group', 'add_cancel_button', "tinyint DEFAULT '1' COMMENT 'Should a Cancel button be included in this Profile form.'"); + $this->addTask('Add location_id if missing to group_contact table (affects some older installs CRM-20711)', 'addColumn', + 'civicrm_group_contact', 'location_id', "int(10) unsigned DEFAULT NULL COMMENT 'Optional location to associate with this membership'"); + $this->addTask('dev/core#107 - Add Activity\'s default assignee options', 'addActivityDefaultAssigneeOptions'); + } + + /** + * This task adds the default assignee option values that can be selected when + * creating or editing a new workflow's activity. + * + * @return bool + */ + public static function addActivityDefaultAssigneeOptions() { + // Add option group for activity default assignees: + CRM_Core_BAO_OptionGroup::ensureOptionGroupExists(array( + 'name' => 'activity_default_assignee', + 'title' => ts('Activity default assignee'), + 'is_reserved' => 1, + )); + + // Add option values for activity default assignees: + $options = array( + array('name' => 'NONE', 'label' => ts('None'), 'is_default' => 1), + array('name' => 'BY_RELATIONSHIP', 'label' => ts('By relationship to case client')), + array('name' => 'SPECIFIC_CONTACT', 'label' => ts('Specific contact')), + array('name' => 'USER_CREATING_THE_CASE', 'label' => ts('User creating the case')), + ); + + foreach ($options as $option) { + CRM_Core_BAO_OptionValue::ensureOptionValueExists(array( + 'option_group_id' => 'activity_default_assignee', + 'name' => $option['name'], + 'label' => $option['label'], + 'is_default' => CRM_Utils_Array::value('is_default', $option, 0), + 'is_active' => TRUE, + )); + } + + return TRUE; + } + + /* + * Important! All upgrade functions MUST add a 'runSql' task. + * Uncomment and use the following template for a new upgrade version + * (change the x in the function name): + */ + + // /** + // * Upgrade function. + // * + // * @param string $rev + // */ + // public function upgrade_5_0_x($rev) { + // $this->addTask(ts('Upgrade DB to %1: SQL', array(1 => $rev)), 'runSql', $rev); + // $this->addTask('Do the foo change', 'taskFoo', ...); + // // Additional tasks here... + // // Note: do not use ts() in the addTask description because it adds unnecessary strings to transifex. + // // The above is an exception because 'Upgrade DB to %1: SQL' is generic & reusable. + // } + + // public static function taskFoo(CRM_Queue_TaskContext $ctx, ...) { + // return TRUE; + // } + +} diff --git a/civicrm/CRM/Upgrade/Incremental/sql/5.3.0.mysql.tpl b/civicrm/CRM/Upgrade/Incremental/sql/5.3.0.mysql.tpl deleted file mode 100644 index b557c2542bab20f6a402a4e4cf028ef32244753c..0000000000000000000000000000000000000000 --- a/civicrm/CRM/Upgrade/Incremental/sql/5.3.0.mysql.tpl +++ /dev/null @@ -1,18 +0,0 @@ -{* file to handle db changes in 5.3.0 during upgrade *} -ALTER TABLE civicrm_custom_group ALTER column is_multiple SET DEFAULT 0; -UPDATE civicrm_custom_group SET is_multiple = 0 WHERE is_multiple IS NULL; -ALTER TABLE civicrm_custom_group ALTER column is_active SET DEFAULT 1; -ALTER TABLE civicrm_custom_field ALTER column is_view SET DEFAULT 0; -UPDATE civicrm_custom_field SET is_view = 0 WHERE is_view IS NULL; -ALTER TABLE civicrm_custom_field ALTER column is_required SET DEFAULT 0; -UPDATE civicrm_custom_field SET is_required = 0 WHERE is_required IS NULL; -ALTER TABLE civicrm_custom_field ALTER column is_searchable SET DEFAULT 0; -UPDATE civicrm_custom_field SET is_searchable = 0 WHERE is_required IS NULL; -ALTER TABLE civicrm_custom_field ALTER column is_active SET DEFAULT 1; - -SET @UKCountryId = (SELECT id FROM civicrm_country cc WHERE cc.name = 'United Kingdom'); -INSERT INTO civicrm_state_province (country_id, abbreviation, name) -VALUES (@UKCountryId, 'MON', 'Monmouthshire'); - -{* dev/core/#152 *} -UPDATE `civicrm_custom_field` set `html_type` = "Multi-Select" WHERE `html_type` = "AdvMulti-Select"; diff --git a/civicrm/CRM/Upgrade/Incremental/sql/5.3.1.mysql.tpl b/civicrm/CRM/Upgrade/Incremental/sql/5.3.1.mysql.tpl deleted file mode 100644 index 2e2057d6dd997f7eb6abd05fed647042e255504d..0000000000000000000000000000000000000000 --- a/civicrm/CRM/Upgrade/Incremental/sql/5.3.1.mysql.tpl +++ /dev/null @@ -1,19 +0,0 @@ -{* file to handle db changes in 5.3.1 during upgrade *} -{* Re run upgrades from 5.3.0 Just in case they were missed somehow due to dodgy tarball *} -ALTER TABLE civicrm_custom_group ALTER column is_multiple SET DEFAULT 0; -UPDATE civicrm_custom_group SET is_multiple = 0 WHERE is_multiple IS NULL; -ALTER TABLE civicrm_custom_group ALTER column is_active SET DEFAULT 1; -ALTER TABLE civicrm_custom_field ALTER column is_view SET DEFAULT 0; -UPDATE civicrm_custom_field SET is_view = 0 WHERE is_view IS NULL; -ALTER TABLE civicrm_custom_field ALTER column is_required SET DEFAULT 0; -UPDATE civicrm_custom_field SET is_required = 0 WHERE is_required IS NULL; -ALTER TABLE civicrm_custom_field ALTER column is_searchable SET DEFAULT 0; -UPDATE civicrm_custom_field SET is_searchable = 0 WHERE is_required IS NULL; -ALTER TABLE civicrm_custom_field ALTER column is_active SET DEFAULT 1; - -SET @UKCountryId = (SELECT id FROM civicrm_country cc WHERE cc.name = 'United Kingdom'); -INSERT IGNORE INTO civicrm_state_province (country_id, abbreviation, name) -VALUES (@UKCountryId, 'MON', 'Monmouthshire'); - -{* dev/core/#152 *} -UPDATE `civicrm_custom_field` set `html_type` = "Multi-Select" WHERE `html_type` = "AdvMulti-Select"; diff --git a/civicrm/CRM/Upgrade/Incremental/sql/5.3.2.mysql.tpl b/civicrm/CRM/Upgrade/Incremental/sql/5.3.2.mysql.tpl deleted file mode 100644 index 3caad28f7483957c1df7f9c11554c5758d896860..0000000000000000000000000000000000000000 --- a/civicrm/CRM/Upgrade/Incremental/sql/5.3.2.mysql.tpl +++ /dev/null @@ -1 +0,0 @@ -{* file to handle db changes in 5.3.2 during upgrade *} diff --git a/civicrm/CRM/Upgrade/Incremental/sql/5.3.alpha1.mysql.tpl b/civicrm/CRM/Upgrade/Incremental/sql/5.3.alpha1.mysql.tpl index 8a3d52ecc1e18848fc0017fd593788b5e165831c..7c05f9ecaacd6d49817e864cdc591c67263400dd 100644 --- a/civicrm/CRM/Upgrade/Incremental/sql/5.3.alpha1.mysql.tpl +++ b/civicrm/CRM/Upgrade/Incremental/sql/5.3.alpha1.mysql.tpl @@ -1 +1,18 @@ {* file to handle db changes in 5.3.alpha1 during upgrade *} +ALTER TABLE civicrm_custom_group ALTER column is_multiple SET DEFAULT 0; +UPDATE civicrm_custom_group SET is_multiple = 0 WHERE is_multiple IS NULL; +ALTER TABLE civicrm_custom_group ALTER column is_active SET DEFAULT 1; +ALTER TABLE civicrm_custom_field ALTER column is_view SET DEFAULT 0; +UPDATE civicrm_custom_field SET is_view = 0 WHERE is_view IS NULL; +ALTER TABLE civicrm_custom_field ALTER column is_required SET DEFAULT 0; +UPDATE civicrm_custom_field SET is_required = 0 WHERE is_required IS NULL; +ALTER TABLE civicrm_custom_field ALTER column is_searchable SET DEFAULT 0; +UPDATE civicrm_custom_field SET is_searchable = 0 WHERE is_required IS NULL; +ALTER TABLE civicrm_custom_field ALTER column is_active SET DEFAULT 1; + +SET @UKCountryId = (SELECT id FROM civicrm_country cc WHERE cc.name = 'United Kingdom'); +INSERT IGNORE INTO civicrm_state_province (country_id, abbreviation, name) +VALUES (@UKCountryId, 'MON', 'Monmouthshire'); + +{* dev/core/#152 *} +UPDATE `civicrm_custom_field` set `html_type` = "Multi-Select" WHERE `html_type` = "AdvMulti-Select"; diff --git a/civicrm/CRM/Upgrade/Incremental/sql/5.4.0.mysql.tpl b/civicrm/CRM/Upgrade/Incremental/sql/5.4.0.mysql.tpl new file mode 100644 index 0000000000000000000000000000000000000000..4d21c517ad922acae6ebf953bf03f5afa6a0cfe6 --- /dev/null +++ b/civicrm/CRM/Upgrade/Incremental/sql/5.4.0.mysql.tpl @@ -0,0 +1 @@ +{* file to handle db changes in 5.4.0 during upgrade *} diff --git a/civicrm/CRM/Upgrade/Incremental/sql/5.4.alpha1.mysql.tpl b/civicrm/CRM/Upgrade/Incremental/sql/5.4.alpha1.mysql.tpl new file mode 100644 index 0000000000000000000000000000000000000000..e47d96cf832499a74dde24f87daee56b4da22b83 --- /dev/null +++ b/civicrm/CRM/Upgrade/Incremental/sql/5.4.alpha1.mysql.tpl @@ -0,0 +1,13 @@ +{* file to handle db changes in 5.4.alpha1 during upgrade *} + +{* +v4.7.20 updated these colums so that new installs would default to TIMESTAMP instead of DATETIME. +Status-checks and DoctorWhen have been encouraging a transition, but it wasn't mandated, and there +was little urgency... because `expired_date` was ignored, and adhoc TTLs on `created_date` had +generally long windows. Now that we're using `expired_date` in more important ways for 5.4, +we want to ensure that these values are handled precisely and consistently. +*} + +ALTER TABLE civicrm_cache + CHANGE created_date created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'When was the cache item created', + CHANGE expired_date expired_date TIMESTAMP NULL DEFAULT NULL COMMENT 'When should the cache item expire'; diff --git a/civicrm/CRM/Upgrade/Incremental/sql/5.4.beta1.mysql.tpl b/civicrm/CRM/Upgrade/Incremental/sql/5.4.beta1.mysql.tpl new file mode 100644 index 0000000000000000000000000000000000000000..b486eda69d14661084502e6f88b3a20014f3993f --- /dev/null +++ b/civicrm/CRM/Upgrade/Incremental/sql/5.4.beta1.mysql.tpl @@ -0,0 +1 @@ +{* file to handle db changes in 5.4.beta1 during upgrade *} diff --git a/civicrm/CRM/Utils/Cache.php b/civicrm/CRM/Utils/Cache.php index 00a92888fc8b7eaec46a99a0e5508f9f6854e6c6..171e4d54374c62db9af0a237900dfb1a72fa93fd 100644 --- a/civicrm/CRM/Utils/Cache.php +++ b/civicrm/CRM/Utils/Cache.php @@ -35,6 +35,9 @@ * Cache is an empty base object, we'll modify the scheme when we have different caching schemes */ class CRM_Utils_Cache { + + const DELIMITER = '/'; + /** * (Quasi-Private) Treat this as private. It is marked public to facilitate testing. * @@ -83,6 +86,7 @@ class CRM_Utils_Cache { // a generic method for utilizing any of the available db caches. $dbCacheClass = 'CRM_Utils_Cache_' . $className; $settings = self::getCacheSettings($className); + $settings['prefix'] = CRM_Utils_Array::value('prefix', $settings, '') . self::DELIMITER . 'default' . self::DELIMITER; self::$_singleton = new $dbCacheClass($settings); } return self::$_singleton; @@ -186,7 +190,7 @@ class CRM_Utils_Cache { if (defined('CIVICRM_DB_CACHE_CLASS') && in_array(CIVICRM_DB_CACHE_CLASS, array('Memcache', 'Memcached', 'Redis'))) { $dbCacheClass = 'CRM_Utils_Cache_' . CIVICRM_DB_CACHE_CLASS; $settings = self::getCacheSettings(CIVICRM_DB_CACHE_CLASS); - $settings['prefix'] = $settings['prefix'] . '_' . $params['name']; + $settings['prefix'] = CRM_Utils_Array::value('prefix', $settings, '') . self::DELIMITER . $params['name'] . self::DELIMITER; return new $dbCacheClass($settings); } break; @@ -210,4 +214,30 @@ class CRM_Utils_Cache { throw new CRM_Core_Exception("Failed to instantiate cache. No supported cache type found. " . print_r($params, 1)); } + /** + * Assert that a key is well-formed. + * + * @param string $key + * @return string + * Same $key, if it's valid. + * @throws \CRM_Utils_Cache_InvalidArgumentException + */ + public static function assertValidKey($key) { + $strict = CRM_Utils_Constant::value('CIVICRM_PSR16_STRICT', FALSE) || defined('CIVICRM_TEST'); + + if (!is_string($key)) { + throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache key: Not a string"); + } + + if ($strict && !preg_match(';^[A-Za-z0-9_\-\. ]+$;', $key)) { + throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache key: Illegal characters"); + } + + if ($strict && strlen($key) > 255) { + throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache key: Too long"); + } + + return $key; + } + } diff --git a/civicrm/CRM/Utils/Cache/APCcache.php b/civicrm/CRM/Utils/Cache/APCcache.php index e696aa19c3d2cd884fe2f69fab52070918f9caf0..26fa86b24166bd6321aae2a44dbe0b8c7fd612f6 100644 --- a/civicrm/CRM/Utils/Cache/APCcache.php +++ b/civicrm/CRM/Utils/Cache/APCcache.php @@ -31,6 +31,10 @@ * @copyright CiviCRM LLC (c) 2004-2018 */ class CRM_Utils_Cache_APCcache implements CRM_Utils_Cache_Interface { + + use CRM_Utils_Cache_NaiveMultipleTrait; // TODO Consider native implementation. + use CRM_Utils_Cache_NaiveHasTrait; // TODO Native implementation + const DEFAULT_TIMEOUT = 3600; const DEFAULT_PREFIX = ''; @@ -72,11 +76,19 @@ class CRM_Utils_Cache_APCcache implements CRM_Utils_Cache_Interface { /** * @param $key * @param $value + * @param null|int|\DateInterval $ttl * * @return bool */ - public function set($key, &$value) { - if (!apc_store($this->_prefix . $key, $value, $this->_timeout)) { + public function set($key, $value, $ttl = NULL) { + CRM_Utils_Cache::assertValidKey($key); + if (is_int($ttl) && $ttl <= 0) { + return $this->delete($key); + } + + $ttl = CRM_Utils_Date::convertCacheTtl($ttl, $this->_timeout); + $expires = time() + $ttl; + if (!apc_store($this->_prefix . $key, ['e' => $expires, 'v' => $value], $ttl)) { return FALSE; } return TRUE; @@ -84,11 +96,17 @@ class CRM_Utils_Cache_APCcache implements CRM_Utils_Cache_Interface { /** * @param $key + * @param mixed $default * * @return mixed */ - public function get($key) { - return apc_fetch($this->_prefix . $key); + public function get($key, $default = NULL) { + CRM_Utils_Cache::assertValidKey($key); + $result = apc_fetch($this->_prefix . $key, $success); + if ($success && isset($result['e']) && $result['e'] > time()) { + return $this->reobjectify($result['v']); + } + return $default; } /** @@ -97,22 +115,33 @@ class CRM_Utils_Cache_APCcache implements CRM_Utils_Cache_Interface { * @return bool|string[] */ public function delete($key) { - return apc_delete($this->_prefix . $key); + CRM_Utils_Cache::assertValidKey($key); + apc_delete($this->_prefix . $key); + return TRUE; } public function flush() { $allinfo = apc_cache_info('user'); $keys = $allinfo['cache_list']; - $prefix = $this->_prefix . "CRM_"; // Our keys follows this pattern: ([A-Za-z0-9_]+)?CRM_[A-Za-z0-9_]+ + $prefix = $this->_prefix; // Our keys follows this pattern: ([A-Za-z0-9_]+)?CRM_[A-Za-z0-9_]+ $lp = strlen($prefix); // Get prefix length foreach ($keys as $key) { $name = $key['info']; if ($prefix == substr($name, 0, $lp)) { // Ours? - apc_delete($this->_prefix . $name); + apc_delete($name); } } + return TRUE; + } + + public function clear() { + return $this->flush(); + } + + private function reobjectify($value) { + return is_object($value) ? unserialize(serialize($value)) : $value; } } diff --git a/civicrm/CRM/Utils/Cache/ArrayCache.php b/civicrm/CRM/Utils/Cache/ArrayCache.php index b2272fa1eb6c9879f5a195ccad2e2a5426a00291..287c6a3e5ce157870389971fb58800817ef1e427 100644 --- a/civicrm/CRM/Utils/Cache/ArrayCache.php +++ b/civicrm/CRM/Utils/Cache/ArrayCache.php @@ -36,11 +36,18 @@ */ class CRM_Utils_Cache_Arraycache implements CRM_Utils_Cache_Interface { + use CRM_Utils_Cache_NaiveMultipleTrait; + use CRM_Utils_Cache_NaiveHasTrait; // TODO Native implementation + + const DEFAULT_TIMEOUT = 3600; + /** * The cache storage container, an in memory array by default */ protected $_cache; + protected $_expires; + /** * Constructor. * @@ -51,35 +58,67 @@ class CRM_Utils_Cache_Arraycache implements CRM_Utils_Cache_Interface { */ public function __construct($config) { $this->_cache = array(); + $this->_expires = array(); } /** * @param string $key * @param mixed $value + * @param null|int|\DateInterval $ttl + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException */ - public function set($key, &$value) { - $this->_cache[$key] = $value; + public function set($key, $value, $ttl = NULL) { + CRM_Utils_Cache::assertValidKey($key); + $this->_cache[$key] = $this->reobjectify($value); + $this->_expires[$key] = CRM_Utils_Date::convertCacheTtlToExpires($ttl, self::DEFAULT_TIMEOUT); + return TRUE; } /** * @param string $key + * @param mixed $default * * @return mixed + * @throws \Psr\SimpleCache\InvalidArgumentException */ - public function get($key) { - return CRM_Utils_Array::value($key, $this->_cache); + public function get($key, $default = NULL) { + CRM_Utils_Cache::assertValidKey($key); + if (isset($this->_expires[$key]) && is_numeric($this->_expires[$key]) && $this->_expires[$key] <= time()) { + return $default; + } + if (array_key_exists($key, $this->_cache)) { + return $this->reobjectify($this->_cache[$key]); + } + return $default; } /** * @param string $key + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException */ public function delete($key) { + CRM_Utils_Cache::assertValidKey($key); + unset($this->_cache[$key]); + unset($this->_expires[$key]); + return TRUE; } public function flush() { unset($this->_cache); + unset($this->_expires); $this->_cache = array(); + return TRUE; + } + + public function clear() { + return $this->flush(); + } + + private function reobjectify($value) { + return is_object($value) ? unserialize(serialize($value)) : $value; } } diff --git a/civicrm/CRM/Utils/Cache/CacheException.php b/civicrm/CRM/Utils/Cache/CacheException.php new file mode 100644 index 0000000000000000000000000000000000000000..cdb0821da2fda9bbc15c388d1ebdaf4b03150dd8 --- /dev/null +++ b/civicrm/CRM/Utils/Cache/CacheException.php @@ -0,0 +1,36 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 5 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2018 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License and the CiviCRM Licensing Exception along | + | with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ + */ + +/** + * Class CRM_Utils_Cache_CacheException + * + * NOTE: PSR-16 specifies its exceptions using interfaces. For cache-consumers, + * it's better to catch based on the interface. For cache-drivers, we need + * a concrete class. + */ +class CRM_Utils_Cache_CacheException extends \CRM_Core_Exception implements \Psr\SimpleCache\CacheException { +} diff --git a/civicrm/CRM/Utils/Cache/Interface.php b/civicrm/CRM/Utils/Cache/Interface.php index f11327cb59c730d4f85eaab883e8a1d5c027efcd..9effdd9d3aa6581d490fe13e015e56aa3940b6ca 100644 --- a/civicrm/CRM/Utils/Cache/Interface.php +++ b/civicrm/CRM/Utils/Cache/Interface.php @@ -30,60 +30,78 @@ * @package CRM * @copyright CiviCRM LLC (c) 2004-2018 * - * CRM_Utils_Cache_Interface + * CRM_Utils_Cache_Interface is a long-standing interface used within CiviCRM + * for interacting with a cache service. In style and substance, it is extremely + * similar to PHP-FIG's SimpleCache interface (PSR-16). Consequently, beginning + * with CiviCRM v5.4, this extends \Psr\SimpleCache\CacheInterface. * - * PHP-FIG has been developing a draft standard for caching, - * PSR-6. The standard has not been ratified yet. When - * making changes to this interface, please take care to - * avoid *conflicst* with PSR-6's CacheItemPoolInterface. At - * time of writing, they do not conflict. Avoiding conflicts - * will enable more transition paths where Civi - * simultaneously supports both interfaces in the same - * implementation. - * - * For example, the current interface defines: - * - * function get($key) => mixed $value - * - * and PSR-6 defines: - * - * function getItem($key) => ItemInterface $item - * - * These are different styles (e.g. "weak item" vs "strong item"), - * but the two methods do not *conflict*. They can coexist, - * and you can trivially write adapters between the two. - * - * @see https://github.com/php-fig/fig-standards/blob/master/proposed/cache.md + * @see https://www.php-fig.org/psr/psr-16/ */ -interface CRM_Utils_Cache_Interface { +interface CRM_Utils_Cache_Interface extends \Psr\SimpleCache\CacheInterface { /** * Set the value in the cache. * * @param string $key * @param mixed $value + * @param null|int|\DateInterval $ttl + * @return bool */ - public function set($key, &$value); + public function set($key, $value, $ttl = NULL); /** * Get a value from the cache. * * @param string $key + * @param mixed $default * @return mixed - * NULL if $key has not been previously set + * The previously set value value, or $default (NULL). */ - public function get($key); + public function get($key, $default = NULL); /** * Delete a value from the cache. * * @param string $key + * @return bool */ public function delete($key); /** * Delete all values from the cache. + * + * NOTE: flush() and clear() should be aliases. flush() is specified by + * Civi's traditional interface, and clear() is specified by PSR-16. + * + * @return bool + * @see clear + * @deprecated */ public function flush(); + /** + * Delete all values from the cache. + * + * NOTE: flush() and clear() should be aliases. flush() is specified by + * Civi's traditional interface, and clear() is specified by PSR-16. + * + * @return bool + * @see flush + */ + public function clear(); + + /** + * Determines whether an item is present in the cache. + * + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * + * @return bool + */ + public function has($key); + } diff --git a/civicrm/CRM/Utils/Cache/InvalidArgumentException.php b/civicrm/CRM/Utils/Cache/InvalidArgumentException.php new file mode 100644 index 0000000000000000000000000000000000000000..5d9747d9164ade159121899ae45b127a78e2b83a --- /dev/null +++ b/civicrm/CRM/Utils/Cache/InvalidArgumentException.php @@ -0,0 +1,36 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 5 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2018 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License and the CiviCRM Licensing Exception along | + | with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ + */ + +/** + * Class CRM_Utils_Cache_InvalidArgumentException + * + * NOTE: PSR-16 specifies its exceptions using interfaces. For cache-consumers, + * it's better to catch based on the interface. For cache-drivers, we need + * a concrete class. + */ +class CRM_Utils_Cache_InvalidArgumentException extends \CRM_Core_Exception implements \Psr\SimpleCache\InvalidArgumentException { +} diff --git a/civicrm/CRM/Utils/Cache/Memcache.php b/civicrm/CRM/Utils/Cache/Memcache.php index e7455bd498757afcbb9d9d538ae0d33b91293106..5bbdf5e0e6c69adc0cf538d6f9e68aa8dfcdca88 100644 --- a/civicrm/CRM/Utils/Cache/Memcache.php +++ b/civicrm/CRM/Utils/Cache/Memcache.php @@ -31,11 +31,19 @@ * @copyright CiviCRM LLC (c) 2004-2018 */ class CRM_Utils_Cache_Memcache implements CRM_Utils_Cache_Interface { + + use CRM_Utils_Cache_NaiveMultipleTrait; // TODO Consider native implementation. + const DEFAULT_HOST = 'localhost'; const DEFAULT_PORT = 11211; const DEFAULT_TIMEOUT = 3600; const DEFAULT_PREFIX = ''; + /** + * If another process clears namespace, we'll find out in ~5 sec. + */ + const NS_LOCAL_TTL = 5; + /** * The host name of the memcached server. * @@ -75,6 +83,15 @@ class CRM_Utils_Cache_Memcache implements CRM_Utils_Cache_Interface { */ protected $_cache; + /** + * @var NULL|array + * + * This is the effective prefix. It may be bumped up whenever the dataset is flushed. + * + * @see https://github.com/memcached/memcached/wiki/ProgrammingTricks#deleting-by-namespace + */ + protected $_truePrefix = NULL; + /** * Constructor. * @@ -109,40 +126,82 @@ class CRM_Utils_Cache_Memcache implements CRM_Utils_Cache_Interface { /** * @param $key * @param $value + * @param null|int|\DateInterval $ttl * * @return bool */ - public function set($key, &$value) { - if (!$this->_cache->set($this->_prefix . $key, $value, FALSE, $this->_timeout)) { - return FALSE; + public function set($key, $value, $ttl = NULL) { + CRM_Utils_Cache::assertValidKey($key); + if (is_int($ttl) && $ttl <= 0) { + return $this->delete($key); } - return TRUE; + $expires = CRM_Utils_Date::convertCacheTtlToExpires($ttl, $this->_timeout); + return $this->_cache->set($this->getTruePrefix() . $key, serialize($value), FALSE, $expires); } /** * @param $key + * @param mixed $default * * @return mixed */ - public function &get($key) { - $result = $this->_cache->get($this->_prefix . $key); - return $result; + public function get($key, $default = NULL) { + CRM_Utils_Cache::assertValidKey($key); + $result = $this->_cache->get($this->getTruePrefix() . $key); + return ($result === FALSE) ? $default : unserialize($result); } + /** + * @param string $key + * + * @return bool + * @throws \Psr\SimpleCache\CacheException + */ + public function has($key) { + CRM_Utils_Cache::assertValidKey($key); + $result = $this->_cache->get($this->getTruePrefix() . $key); + return ($result !== FALSE); + } + + /** * @param $key * - * @return mixed + * @return bool */ public function delete($key) { - return $this->_cache->delete($this->_prefix . $key); + CRM_Utils_Cache::assertValidKey($key); + $this->_cache->delete($this->getTruePrefix() . $key); + return TRUE; } /** - * @return mixed + * @return bool */ public function flush() { - return $this->_cache->flush(); + $this->_truePrefix = NULL; + $this->_cache->delete($this->_prefix); + return TRUE; + } + + public function clear() { + return $this->flush(); + } + + protected function getTruePrefix() { + if ($this->_truePrefix === NULL || $this->_truePrefix['expires'] < time()) { + $key = $this->_prefix; + $value = $this->_cache->get($key); + if ($value === FALSE) { + $value = uniqid(); + $this->_cache->set($key, $value, FALSE, 0); // Indefinite. + } + $this->_truePrefix = [ + 'value' => $value, + 'expires' => time() + self::NS_LOCAL_TTL, + ]; + } + return $this->_prefix . $this->_truePrefix['value'] . '/'; } } diff --git a/civicrm/CRM/Utils/Cache/Memcached.php b/civicrm/CRM/Utils/Cache/Memcached.php index e80b1af4b354cdae264b157fd1b0b31d30d71201..546c5b933b9a0b006c478ed15f8895dfa0b41219 100644 --- a/civicrm/CRM/Utils/Cache/Memcached.php +++ b/civicrm/CRM/Utils/Cache/Memcached.php @@ -31,11 +31,19 @@ * @copyright CiviCRM LLC (c) 2004-2018 */ class CRM_Utils_Cache_Memcached implements CRM_Utils_Cache_Interface { + + use CRM_Utils_Cache_NaiveMultipleTrait; // TODO Consider native implementation. + const DEFAULT_HOST = 'localhost'; const DEFAULT_PORT = 11211; const DEFAULT_TIMEOUT = 3600; const DEFAULT_PREFIX = ''; - const MAX_KEY_LEN = 62; + const MAX_KEY_LEN = 200; + + /** + * If another process clears namespace, we'll find out in ~5 sec. + */ + const NS_LOCAL_TTL = 5; /** * The host name of the memcached server @@ -76,6 +84,15 @@ class CRM_Utils_Cache_Memcached implements CRM_Utils_Cache_Interface { */ protected $_cache; + /** + * @var NULL|array + * + * This is the effective prefix. It may be bumped up whenever the dataset is flushed. + * + * @see https://github.com/memcached/memcached/wiki/ProgrammingTricks#deleting-by-namespace + */ + protected $_truePrefix = NULL; + /** * Constructor. * @@ -110,29 +127,79 @@ class CRM_Utils_Cache_Memcached implements CRM_Utils_Cache_Interface { /** * @param $key * @param $value + * @param null|int|\DateInterval $ttl * * @return bool * @throws Exception */ - public function set($key, &$value) { + public function set($key, $value, $ttl = NULL) { + CRM_Utils_Cache::assertValidKey($key); + if (is_int($ttl) && $ttl <= 0) { + return $this->delete($key); + } + $expires = CRM_Utils_Date::convertCacheTtlToExpires($ttl, $this->_timeout); + $key = $this->cleanKey($key); - if (!$this->_cache->set($key, $value, $this->_timeout)) { - CRM_Core_Error::debug('Result Code: ', $this->_cache->getResultMessage()); - CRM_Core_Error::fatal("memcached set failed, wondering why?, $key", $value); + if (!$this->_cache->set($key, serialize($value), $expires)) { + if (PHP_SAPI === 'cli' || (Civi\Core\Container::isContainerBooted() && CRM_Core_Permission::check('view debug output'))) { + throw new CRM_Utils_Cache_CacheException("Memcached::set($key) failed: " . $this->_cache->getResultMessage()); + } + else { + Civi::log()->error("Memcached::set($key) failed: " . $this->_cache->getResultMessage()); + throw new CRM_Utils_Cache_CacheException("Memcached::set($key) failed"); + } return FALSE; + } return TRUE; } /** * @param $key + * @param mixed $default * * @return mixed */ - public function &get($key) { + public function get($key, $default = NULL) { + CRM_Utils_Cache::assertValidKey($key); $key = $this->cleanKey($key); $result = $this->_cache->get($key); - return $result; + switch ($this->_cache->getResultCode()) { + case Memcached::RES_SUCCESS: + return unserialize($result); + + case Memcached::RES_NOTFOUND: + return $default; + + default: + Civi::log()->error("Memcached::get($key) failed: " . $this->_cache->getResultMessage()); + throw new CRM_Utils_Cache_CacheException("Memcached set ($key) failed"); + } + } + + /** + * @param string $key + * + * @return bool + * @throws \Psr\SimpleCache\CacheException + */ + public function has($key) { + CRM_Utils_Cache::assertValidKey($key); + $key = $this->cleanKey($key); + if ($this->_cache->get($key) !== FALSE) { + return TRUE; + } + switch ($this->_cache->getResultCode()) { + case Memcached::RES_NOTFOUND: + return FALSE; + + case Memcached::RES_SUCCESS: + return TRUE; + + default: + Civi::log()->error("Memcached::has($key) failed: " . $this->_cache->getResultMessage()); + throw new CRM_Utils_Cache_CacheException("Memcached set ($key) failed"); + } } /** @@ -141,8 +208,13 @@ class CRM_Utils_Cache_Memcached implements CRM_Utils_Cache_Interface { * @return mixed */ public function delete($key) { + CRM_Utils_Cache::assertValidKey($key); $key = $this->cleanKey($key); - return $this->_cache->delete($key); + if ($this->_cache->delete($key)) { + return TRUE; + } + $code = $this->_cache->getResultCode(); + return ($code == Memcached::RES_DELETED || $code == Memcached::RES_NOTFOUND); } /** @@ -151,20 +223,47 @@ class CRM_Utils_Cache_Memcached implements CRM_Utils_Cache_Interface { * @return mixed|string */ public function cleanKey($key) { - $key = preg_replace('/\s+|\W+/', '_', $this->_prefix . $key); - if (strlen($key) > self::MAX_KEY_LEN) { + $truePrefix = $this->getTruePrefix(); + $maxLen = self::MAX_KEY_LEN - strlen($truePrefix); + $key = preg_replace('/\s+|\W+/', '_', $key); + if (strlen($key) > $maxLen) { $md5Key = md5($key); // this should be 32 characters in length - $subKeyLen = self::MAX_KEY_LEN - 1 - strlen($md5Key); + $subKeyLen = $maxLen - 1 - strlen($md5Key); $key = substr($key, 0, $subKeyLen) . "_" . $md5Key; } - return $key; + return $truePrefix . $key; } /** - * @return mixed + * @return bool */ public function flush() { - return $this->_cache->flush(); + $this->_truePrefix = NULL; + if ($this->_cache->delete($this->_prefix)) { + return TRUE; + } + $code = $this->_cache->getResultCode(); + return ($code == Memcached::RES_DELETED || $code == Memcached::RES_NOTFOUND); + } + + public function clear() { + return $this->flush(); + } + + protected function getTruePrefix() { + if ($this->_truePrefix === NULL || $this->_truePrefix['expires'] < time()) { + $key = $this->_prefix; + $value = $this->_cache->get($key); + if ($this->_cache->getResultCode() === Memcached::RES_NOTFOUND) { + $value = uniqid(); + $this->_cache->add($key, $value, 0); // Indefinite. + } + $this->_truePrefix = [ + 'value' => $value, + 'expires' => time() + self::NS_LOCAL_TTL, + ]; + } + return $this->_prefix . $this->_truePrefix['value'] . '/'; } } diff --git a/civicrm/CRM/Utils/Cache/NaiveHasTrait.php b/civicrm/CRM/Utils/Cache/NaiveHasTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..78ecc678876373ff384fea8a29a8aac08d5560df --- /dev/null +++ b/civicrm/CRM/Utils/Cache/NaiveHasTrait.php @@ -0,0 +1,49 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 5 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2018 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License and the CiviCRM Licensing Exception along | + | with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ + */ + +/** + * + * @package CRM + * @copyright CiviCRM LLC (c) 2004-2018 + * + * The traditional CRM_Utils_Cache_Interface did not support has(). + * To get drop-in compliance with PSR-16, we use a naive adapter. + * + * Ideally, these should be replaced with more performant/native versions. + */ +trait CRM_Utils_Cache_NaiveHasTrait { + + public function has($key) { + // This is crazy-talk. If you've got an environment setup where you might + // be investigating this, fix your preferred cache driver by + // replacing `NaiveHasTrait` with a decent function. + $hasDefaultA = ($this->get($key, NULL) === NULL); + $hasDefaultB = ($this->get($key, 123) === 123); + return !($hasDefaultA && $hasDefaultB); + } + +} diff --git a/civicrm/CRM/Utils/Cache/NaiveMultipleTrait.php b/civicrm/CRM/Utils/Cache/NaiveMultipleTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..d7ead9fc94b90c5e4fe963aa02639b44d26172cc --- /dev/null +++ b/civicrm/CRM/Utils/Cache/NaiveMultipleTrait.php @@ -0,0 +1,121 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 5 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2018 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License and the CiviCRM Licensing Exception along | + | with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ + */ + +/** + * + * @package CRM + * @copyright CiviCRM LLC (c) 2004-2018 + * + * The traditional CRM_Utils_Cache_Interface did not support multiple-key + * operations. To get drop-in compliance with PSR-16, we use a naive adapter. + * An operation like `getMultiple()` just calls `get()` multiple times. + * + * Ideally, these should be replaced with more performant/native versions. + */ +trait CRM_Utils_Cache_NaiveMultipleTrait { + + /** + * Obtains multiple cache items by their unique keys. + * + * @param iterable $keys A list of keys that can obtained in a single operation. + * @param mixed $default Default value to return for keys that do not exist. + * + * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function getMultiple($keys, $default = NULL) { + $this->assertIterable('getMultiple', $keys); + + $result = []; + foreach ($keys as $key) { + $result[$key] = $this->get($key, $default); + } + return $result; + } + + /** + * Persists a set of key => value pairs in the cache, with an optional TTL. + * + * @param iterable $values A list of key => value pairs for a multiple-set operation. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $values is neither an array nor a Traversable, + * or if any of the $values are not a legal value. + */ + public function setMultiple($values, $ttl = NULL) { + $this->assertIterable('setMultiple', $values); + + $result = TRUE; + foreach ($values as $key => $value) { + if (is_int($key)) { + $key = (string) $key; + } + $result = $this->set($key, $value, $ttl) || $result; + } + return $result; + } + + /** + * Deletes multiple cache items in a single operation. + * + * @param iterable $keys A list of string-based keys to be deleted. + * + * @return bool True if the items were successfully removed. False if there was an error. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function deleteMultiple($keys) { + $this->assertIterable('deleteMultiple', $keys); + + $result = TRUE; + foreach ($keys as $key) { + $result = $this->delete($key) || $result; + } + return $result; + } + + /** + * @param $keys + * @throws \CRM_Utils_Cache_InvalidArgumentException + */ + private function assertIterable($func, $keys) { + if (!is_array($keys) && !($keys instanceof Traversable)) { + throw new CRM_Utils_Cache_InvalidArgumentException("$func expects iterable input"); + } + } + +} diff --git a/civicrm/CRM/Utils/Cache/NoCache.php b/civicrm/CRM/Utils/Cache/NoCache.php index 66c6b74cff410ec6280e951843140dde66409bde..3c1d9b9445273b4c63e546e218d007e515235f57 100644 --- a/civicrm/CRM/Utils/Cache/NoCache.php +++ b/civicrm/CRM/Utils/Cache/NoCache.php @@ -32,6 +32,9 @@ */ class CRM_Utils_Cache_NoCache implements CRM_Utils_Cache_Interface { + use CRM_Utils_Cache_NaiveMultipleTrait; // TODO Consider native implementation. + use CRM_Utils_Cache_NaiveHasTrait; // TODO Native implementation + /** * We only need one instance of this object. So we use the singleton * pattern and cache the instance in this variable @@ -54,20 +57,22 @@ class CRM_Utils_Cache_NoCache implements CRM_Utils_Cache_Interface { /** * @param string $key * @param mixed $value + * @param null|int|\DateInterval $ttl * * @return bool */ - public function set($key, &$value) { + public function set($key, $value, $ttl = NULL) { return FALSE; } /** * @param string $key + * @param mixed $default * * @return null */ - public function get($key) { - return NULL; + public function get($key, $default = NULL) { + return $default; } /** @@ -86,4 +91,8 @@ class CRM_Utils_Cache_NoCache implements CRM_Utils_Cache_Interface { return FALSE; } + public function clear() { + return $this->flush(); + } + } diff --git a/civicrm/CRM/Utils/Cache/Redis.php b/civicrm/CRM/Utils/Cache/Redis.php index 07279d530de987f039fc4f9e3e56a1f6c76bf04f..3e2f2f7f9b3fa64ecbb6e786399f2c4d350441ab 100644 --- a/civicrm/CRM/Utils/Cache/Redis.php +++ b/civicrm/CRM/Utils/Cache/Redis.php @@ -33,6 +33,10 @@ * */ class CRM_Utils_Cache_Redis implements CRM_Utils_Cache_Interface { + + use CRM_Utils_Cache_NaiveMultipleTrait; // TODO Consider native implementation. + use CRM_Utils_Cache_NaiveHasTrait; // TODO Native implementation + const DEFAULT_HOST = 'localhost'; const DEFAULT_PORT = 6379; const DEFAULT_TIMEOUT = 3600; @@ -105,19 +109,33 @@ class CRM_Utils_Cache_Redis implements CRM_Utils_Cache_Interface { echo 'Could not connect to redisd server'; CRM_Utils_System::civiExit(); } - $this->_cache->auth(CIVICRM_DB_CACHE_PASSWORD); + if (CRM_Utils_Constant::value('CIVICRM_DB_CACHE_PASSWORD')) { + $this->_cache->auth(CIVICRM_DB_CACHE_PASSWORD); + } } /** * @param $key * @param $value + * @param null|int|\DateInterval $ttl * * @return bool * @throws Exception */ - public function set($key, &$value) { - if (!$this->_cache->set($this->_prefix . $key, serialize($value), $this->_timeout)) { - CRM_Core_Error::fatal("Redis set failed, wondering why?, $key", $value); + public function set($key, $value, $ttl = NULL) { + CRM_Utils_Cache::assertValidKey($key); + if (is_int($ttl) && $ttl <= 0) { + return $this->delete($key); + } + $ttl = CRM_Utils_Date::convertCacheTtl($ttl, self::DEFAULT_TIMEOUT); + if (!$this->_cache->setex($this->_prefix . $key, $ttl, serialize($value))) { + if (PHP_SAPI === 'cli' || (Civi\Core\Container::isContainerBooted() && CRM_Core_Permission::check('view debug output'))) { + throw new CRM_Utils_Cache_CacheException("Redis set ($key) failed: " . $this->_cache->getLastError()); + } + else { + Civi::log()->error("Redis set ($key) failed: " . $this->_cache->getLastError()); + throw new CRM_Utils_Cache_CacheException("Redis set ($key) failed"); + } return FALSE; } return TRUE; @@ -125,28 +143,42 @@ class CRM_Utils_Cache_Redis implements CRM_Utils_Cache_Interface { /** * @param $key + * @param mixed $default * * @return mixed */ - public function get($key) { + public function get($key, $default = NULL) { + CRM_Utils_Cache::assertValidKey($key); $result = $this->_cache->get($this->_prefix . $key); - return unserialize($result); + return ($result === FALSE) ? $default : unserialize($result); } /** * @param $key * - * @return mixed + * @return bool */ public function delete($key) { - return $this->_cache->delete($this->_prefix . $key); + CRM_Utils_Cache::assertValidKey($key); + $this->_cache->delete($this->_prefix . $key); + return TRUE; } /** - * @return mixed + * @return bool */ public function flush() { - return $this->_cache->flushDB(); + // FIXME: Ideally, we'd map each prefix to a different 'hash' object in Redis, + // and this would be simpler. However, that needs to go in tandem with a + // more general rethink of cache expiration/TTL. + + $keys = $this->_cache->keys($this->_prefix . '*'); + $this->_cache->del($keys); + return TRUE; + } + + public function clear() { + return $this->flush(); } } diff --git a/civicrm/CRM/Utils/Cache/SerializeCache.php b/civicrm/CRM/Utils/Cache/SerializeCache.php index 6164694c3e5cc5eb5263ce0dd08d4ed4801fc746..fdf0eb6d47b588ba1c5fe0eb087f59a97f97d221 100644 --- a/civicrm/CRM/Utils/Cache/SerializeCache.php +++ b/civicrm/CRM/Utils/Cache/SerializeCache.php @@ -36,6 +36,9 @@ */ class CRM_Utils_Cache_SerializeCache implements CRM_Utils_Cache_Interface { + use CRM_Utils_Cache_NaiveMultipleTrait; + use CRM_Utils_Cache_NaiveHasTrait; // TODO Native implementation + /** * The cache storage container, an array by default, stored in a file under templates */ @@ -67,10 +70,15 @@ class CRM_Utils_Cache_SerializeCache implements CRM_Utils_Cache_Interface { /** * @param string $key + * @param mixed $default * * @return mixed */ - public function get($key) { + public function get($key, $default = NULL) { + if ($default !== NULL) { + throw new \RuntimeException("FIXME: " . __CLASS__ . "::get() only supports NULL default"); + } + if (array_key_exists($key, $this->_cache)) { return $this->_cache[$key]; } @@ -85,32 +93,41 @@ class CRM_Utils_Cache_SerializeCache implements CRM_Utils_Cache_Interface { /** * @param string $key * @param mixed $value + * @param null|int|\DateInterval $ttl + * @return bool */ - public function set($key, &$value) { + public function set($key, $value, $ttl = NULL) { + if ($ttl !== NULL) { + throw new \RuntimeException("FIXME: " . __CLASS__ . "::set() should support non-NULL TTL"); + } if (file_exists($this->fileName($key))) { - return; + return FALSE; // WTF, write-once cache?! } $this->_cache[$key] = $value; - file_put_contents($this->fileName($key), "<?php //" . serialize($value)); + $bytes = file_put_contents($this->fileName($key), "<?php //" . serialize($value)); + return ($bytes !== FALSE); } /** * @param string $key + * @return bool */ public function delete($key) { if (file_exists($this->fileName($key))) { unlink($this->fileName($key)); } unset($this->_cache[$key]); + return TRUE; } /** * @param null $key + * @return bool */ public function flush($key = NULL) { $prefix = "CRM_"; if (!$handle = opendir(CIVICRM_TEMPLATE_COMPILEDIR)) { - return; // die? Error? + return FALSE; // die? Error? } while (FALSE !== ($entry = readdir($handle))) { if (substr($entry, 0, 4) == $prefix) { @@ -120,6 +137,11 @@ class CRM_Utils_Cache_SerializeCache implements CRM_Utils_Cache_Interface { closedir($handle); unset($this->_cache); $this->_cache = array(); + return TRUE; + } + + public function clear() { + return $this->flush(); } } diff --git a/civicrm/CRM/Utils/Cache/SqlGroup.php b/civicrm/CRM/Utils/Cache/SqlGroup.php index 6f3f45f792743f727bab58ff670ecca6a40e8451..aa4d15e47249b1f7e2f670bb62857b176637ae3d 100644 --- a/civicrm/CRM/Utils/Cache/SqlGroup.php +++ b/civicrm/CRM/Utils/Cache/SqlGroup.php @@ -38,6 +38,12 @@ */ class CRM_Utils_Cache_SqlGroup implements CRM_Utils_Cache_Interface { + // 6*60*60 + const DEFAULT_TTL = 21600; + + const TS_FMT = 'Y-m-d H:i:s'; + use CRM_Utils_Cache_NaiveMultipleTrait; // TODO Consider native implementation. + /** * The host name of the memcached server. * @@ -53,7 +59,18 @@ class CRM_Utils_Cache_SqlGroup implements CRM_Utils_Cache_Interface { /** * @var array in-memory cache to optimize redundant get()s */ - protected $frontCache; + protected $valueCache; + + /** + * @var array in-memory cache to optimize redundant get()s + * Note: expiresCache[$key]===NULL means cache-miss + */ + protected $expiresCache; + + /** + * @var string + */ + protected $table; /** * Constructor. @@ -68,6 +85,7 @@ class CRM_Utils_Cache_SqlGroup implements CRM_Utils_Cache_Interface { * @return \CRM_Utils_Cache_SqlGroup */ public function __construct($config) { + $this->table = CRM_Core_DAO_Cache::getTableName(); if (isset($config['group'])) { $this->group = $config['group']; } @@ -80,7 +98,7 @@ class CRM_Utils_Cache_SqlGroup implements CRM_Utils_Cache_Interface { else { $this->componentID = NULL; } - $this->frontCache = array(); + $this->valueCache = array(); if (CRM_Utils_Array::value('prefetch', $config, TRUE)) { $this->prefetch(); } @@ -89,22 +107,76 @@ class CRM_Utils_Cache_SqlGroup implements CRM_Utils_Cache_Interface { /** * @param string $key * @param mixed $value + * @param null|int|\DateInterval $ttl + * @return bool */ - public function set($key, &$value) { - CRM_Core_BAO_Cache::setItem($value, $this->group, $key, $this->componentID); - $this->frontCache[$key] = $value; + public function set($key, $value, $ttl = NULL) { + CRM_Utils_Cache::assertValidKey($key); + + $lock = Civi::lockManager()->acquire("cache.{$this->group}_{$key}._null"); + if (!$lock->isAcquired()) { + throw new \CRM_Utils_Cache_CacheException("SqlGroup: Failed to acquire lock on cache key."); + } + + $dataExists = CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM {$this->table} WHERE {$this->where($key)}"); + $expires = CRM_Utils_Date::convertCacheTtlToExpires($ttl, self::DEFAULT_TTL); + + $dataSerialized = CRM_Core_BAO_Cache::encode($value); + + // This table has a wonky index, so we cannot use REPLACE or + // "INSERT ... ON DUPE". Instead, use SELECT+(INSERT|UPDATE). + if ($dataExists) { + $sql = "UPDATE {$this->table} SET data = %1, created_date = FROM_UNIXTIME(%2), expired_date = FROM_UNIXTIME(%3) WHERE {$this->where($key)}"; + $args = array( + 1 => array($dataSerialized, 'String'), + 2 => array(time(), 'Positive'), + 3 => array($expires, 'Positive'), + ); + $dao = CRM_Core_DAO::executeQuery($sql, $args, FALSE, NULL, FALSE, FALSE); + } + else { + $sql = "INSERT INTO {$this->table} (group_name,path,data,created_date,expired_date) VALUES (%1,%2,%3,FROM_UNIXTIME(%4),FROM_UNIXTIME(%5))"; + $args = array( + 1 => [$this->group, 'String'], + 2 => [$key, 'String'], + 3 => [$dataSerialized, 'String'], + 4 => [time(), 'Positive'], + 5 => [$expires, 'Positive'], + ); + $dao = CRM_Core_DAO::executeQuery($sql, $args, FALSE, NULL, FALSE, FALSE); + } + + $lock->release(); + + $dao->free(); + + $this->valueCache[$key] = CRM_Core_BAO_Cache::decode($dataSerialized); + $this->expiresCache[$key] = $expires; + return TRUE; } /** * @param string $key + * @param mixed $default * * @return mixed */ - public function get($key) { - if (!array_key_exists($key, $this->frontCache)) { - $this->frontCache[$key] = CRM_Core_BAO_Cache::getItem($this->group, $key, $this->componentID); + public function get($key, $default = NULL) { + CRM_Utils_Cache::assertValidKey($key); + if (!isset($this->expiresCache[$key]) || time() >= $this->expiresCache[$key]) { + $sql = "SELECT path, data, UNIX_TIMESTAMP(expired_date) as expires FROM {$this->table} WHERE " . $this->where($key); + $dao = CRM_Core_DAO::executeQuery($sql); + while ($dao->fetch()) { + $this->expiresCache[$key] = $dao->expires; + $this->valueCache[$key] = CRM_Core_BAO_Cache::decode($dao->data); + } + $dao->free(); } - return $this->frontCache[$key]; + return (isset($this->expiresCache[$key]) && time() < $this->expiresCache[$key]) ? $this->reobjectify($this->valueCache[$key]) : $default; + } + + private function reobjectify($value) { + return is_object($value) ? unserialize(serialize($value)) : $value; } /** @@ -114,24 +186,60 @@ class CRM_Utils_Cache_SqlGroup implements CRM_Utils_Cache_Interface { * @return mixed */ public function getFromFrontCache($key, $default = NULL) { - return CRM_Utils_Array::value($key, $this->frontCache, $default); + if (isset($this->expiresCache[$key]) && time() < $this->expiresCache[$key] && $this->valueCache[$key]) { + return $this->reobjectify($this->valueCache[$key]); + } + else { + return $default; + } + } + + public function has($key) { + $this->get($key); + return isset($this->expiresCache[$key]) && time() < $this->expiresCache[$key]; } /** * @param string $key + * @return bool */ public function delete($key) { - CRM_Core_BAO_Cache::deleteGroup($this->group, $key); - unset($this->frontCache[$key]); + CRM_Utils_Cache::assertValidKey($key); + CRM_Core_DAO::executeQuery("DELETE FROM {$this->table} WHERE {$this->where($key)}"); + unset($this->valueCache[$key]); + unset($this->expiresCache[$key]); + return TRUE; } public function flush() { - CRM_Core_BAO_Cache::deleteGroup($this->group); - $this->frontCache = array(); + CRM_Core_DAO::executeQuery("DELETE FROM {$this->table} WHERE {$this->where()}"); + $this->valueCache = array(); + $this->expiresCache = array(); + return TRUE; + } + + public function clear() { + return $this->flush(); } public function prefetch() { - $this->frontCache = CRM_Core_BAO_Cache::getItems($this->group, $this->componentID); + $dao = CRM_Core_DAO::executeQuery("SELECT path, data, UNIX_TIMESTAMP(expired_date) AS expires FROM {$this->table} WHERE " . $this->where(NULL)); + $this->valueCache = array(); + $this->expiresCache = array(); + while ($dao->fetch()) { + $this->valueCache[$dao->path] = CRM_Core_BAO_Cache::decode($dao->data); + $this->expiresCache[$dao->path] = $dao->expires; + } + $dao->free(); + } + + protected function where($path = NULL) { + $clauses = array(); + $clauses[] = ('group_name = "' . CRM_Core_DAO::escapeString($this->group) . '"'); + if ($path) { + $clauses[] = ('path = "' . CRM_Core_DAO::escapeString($path) . '"'); + } + return $clauses ? implode(' AND ', $clauses) : '(1)'; } } diff --git a/civicrm/CRM/Utils/Check.php b/civicrm/CRM/Utils/Check.php index 24fd07062a1862571b4196ba3fa4306c81dda4bb..57d15a741ff8c06689727020de2b8c2d9a6d5b29 100644 --- a/civicrm/CRM/Utils/Check.php +++ b/civicrm/CRM/Utils/Check.php @@ -223,7 +223,7 @@ class CRM_Utils_Check { break; } - Civi::settings()->set('systemStatusCheckResult', $maxSeverity); + Civi::cache('checks')->set('systemStatusCheckResult', $maxSeverity); return ($max) ? $maxSeverity : $messages; } diff --git a/civicrm/CRM/Utils/Date.php b/civicrm/CRM/Utils/Date.php index 3d4570d7e194fdca20ffdd930f027345d3323a91..74aa2e35caec7e36103714ad5a4caea4da46ae7a 100644 --- a/civicrm/CRM/Utils/Date.php +++ b/civicrm/CRM/Utils/Date.php @@ -695,6 +695,58 @@ class CRM_Utils_Date { return TRUE; } + /** + * Translate a TTL to a concrete expiration time. + * + * @param NULL|int|DateInterval $ttl + * @param int $default + * The value to use if $ttl is not specified (NULL). + * @return int + * Timestamp (seconds since epoch). + * @throws \CRM_Utils_Cache_InvalidArgumentException + */ + public static function convertCacheTtlToExpires($ttl, $default) { + if ($ttl === NULL) { + $ttl = $default; + } + + if (is_int($ttl)) { + return time() + $ttl; + } + elseif ($ttl instanceof DateInterval) { + return date_add(new DateTime(), $ttl)->getTimestamp(); + } + else { + throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache TTL"); + } + } + + /** + * Normalize a TTL. + * + * @param NULL|int|DateInterval $ttl + * @param int $default + * The value to use if $ttl is not specified (NULL). + * @return int + * Seconds until expiration. + * @throws \CRM_Utils_Cache_InvalidArgumentException + */ + public static function convertCacheTtl($ttl, $default) { + if ($ttl === NULL) { + return $default; + } + elseif (is_int($ttl)) { + return $ttl; + } + elseif ($ttl instanceof DateInterval) { + return date_add(new DateTime(), $ttl)->getTimestamp() - time(); + } + else { + throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache TTL"); + } + } + + /** * @param null $timeStamp * diff --git a/civicrm/CRM/Utils/DeprecatedUtils.php b/civicrm/CRM/Utils/DeprecatedUtils.php index 00afe4332eb26b1734ec8ab6ba27b2f9ba78b884..7c726a3ee08899910641fb999b341c53214c555f 100644 --- a/civicrm/CRM/Utils/DeprecatedUtils.php +++ b/civicrm/CRM/Utils/DeprecatedUtils.php @@ -1268,7 +1268,7 @@ function _civicrm_api3_deprecated_contact_check_params( // @todo switch to using api version // $dupes = civicrm_api3('Contact', 'duplicatecheck', (array('match' => $params, 'dedupe_rule_id' => $dedupeRuleGroupID))); // $ids = $dupes['count'] ? implode(',', array_keys($dupes['values'])) : NULL; - $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($params, $params['contact_type'], 'Unsupervised', array(), CRM_Utils_Array::value('check_permissions', $params, $dedupeRuleGroupID)); + $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($params, $params['contact_type'], 'Unsupervised', array(), CRM_Utils_Array::value('check_permissions', $params), $dedupeRuleGroupID); if ($ids != NULL) { $error = CRM_Core_Error::createError("Found matching contacts: " . implode(',', $ids), CRM_Core_Error::DUPLICATE_CONTACT, diff --git a/civicrm/CRM/Utils/Hook.php b/civicrm/CRM/Utils/Hook.php index 0ad0debd39cd6052042c6e767ad0a2bb4bdae8f2..1f75b3ab8dade666643fc291f0747713462d4338 100644 --- a/civicrm/CRM/Utils/Hook.php +++ b/civicrm/CRM/Utils/Hook.php @@ -2418,4 +2418,36 @@ abstract class CRM_Utils_Hook { ); } + /** + * This hook is called before a scheduled job is executed + * + * @param CRM_Core_DAO_Job $job + * The job to be executed + * @param array $params + * The arguments to be given to the job + */ + public static function preJob($job, $params) { + return self::singleton()->invoke(array('job', 'params'), $job, $params, + self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject, + 'civicrm_preJob' + ); + } + + /** + * This hook is called after a scheduled job is executed + * + * @param CRM_Core_DAO_Job $job + * The job that was executed + * @param array $params + * The arguments given to the job + * @param array $result + * The result of the API call, or the thrown exception if any + */ + public static function postJob($job, $params, $result) { + return self::singleton()->invoke(array('job', 'params', 'result'), $job, $params, $result, + self::$_nullObject, self::$_nullObject, self::$_nullObject, + 'civicrm_postJob' + ); + } + } diff --git a/civicrm/CRM/Utils/Migrate/Import.php b/civicrm/CRM/Utils/Migrate/Import.php index 1736130bf0a0a903f469e686ff7742215ffa4176..dc9e24419ae308b18ed25461a38c41b010c7adba 100644 --- a/civicrm/CRM/Utils/Migrate/Import.php +++ b/civicrm/CRM/Utils/Migrate/Import.php @@ -242,6 +242,15 @@ WHERE v.option_group_id = %1 elseif (in_array($customGroup->extends, array('Individual', 'Organization', 'Household'))) { $valueIDs = $optionValues; } + elseif (in_array($customGroup->extends, array('Contribution', 'ContributionRecur'))) { + $sql = "SELECT id + FROM civicrm_financial_type + WHERE name IN ('{$optValues}')"; + $dao = &CRM_Core_DAO::executeQuery($sql); + while ($dao->fetch()) { + $valueIDs[] = $dao->id; + } + } else { $sql = " SELECT v.value diff --git a/civicrm/CRM/Utils/SQL/TempTable.php b/civicrm/CRM/Utils/SQL/TempTable.php new file mode 100644 index 0000000000000000000000000000000000000000..cedc46c816221adf3824df21a3298825108df015 --- /dev/null +++ b/civicrm/CRM/Utils/SQL/TempTable.php @@ -0,0 +1,272 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 5 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2018 | + +--------------------------------------------------------------------+ + | This file is a part of CiviCRM. | + | | + | CiviCRM is free software; you can copy, modify, and distribute it | + | under the terms of the GNU Affero General Public License | + | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | + | | + | CiviCRM is distributed in the hope that it will be useful, but | + | WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | + | See the GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public | + | License and the CiviCRM Licensing Exception along | + | with this program; if not, contact CiviCRM LLC | + | at info[AT]civicrm[DOT]org. If you have questions about the | + | GNU Affero General Public License or the licensing of CiviCRM, | + | see the CiviCRM license FAQ at http://civicrm.org/licensing | + +--------------------------------------------------------------------+ + */ + +/** + * + * @package CRM + * @copyright CiviCRM LLC (c) 2004-2018 + * + * Table naming rules: + * - MySQL imposes a 64 char limit. + * - All temp tables start with "civicrm_tmp". + * - Durable temp tables: "civicrm_tmp_d_{12}_{32}" + * - Ephemeral temp tables: "civicrm_tmp_e_{12}_{32}" + * + * To use `TempTable`: + * - Begin by calling `CRM_Utils_SQL_TempTable::build()`. + * - Optionally, describe the table with `setDurable()`, `setCategory()`, `setId()`. + * - Finally, call `getName()` or `createWithQuery()` or `createWithColumns()`. + * + * Example 1: Just create a table name. You'll be responsible for CREATE/DROP actions. + * + * $name = CRM_Utils_SQL_TempTable::build()->getName(); + * $name = CRM_Utils_SQL_TempTable::build()->setDurable()->getName(); + * $name = CRM_Utils_SQL_TempTable::build()->setCategory('contactstats')->setId($contact['id'])->getName(); + * + * Example 2: Create a temp table using the results of a SELECT query. + * + * $tmpTbl = CRM_Utils_SQL_TempTable::build()->createWithQuery('SELECT id, display_name FROM civicrm_contact'); + * $tmpTbl = CRM_Utils_SQL_TempTable::build()->createWithQuery(CRM_Utils_SQL_Select::from('civicrm_contact')->select('display_name')); + * + * Example 3: Create an empty temp table with list of columns. + * + * $tmpTbl = CRM_Utils_SQL_TempTable::build()->setDurable()->setUtf8()->createWithColumns('id int(10, name varchar(64)'); + * + * Example 4: Drop a table that you previously created. + * + * $tmpTbl->drop(); + * + * Example 5: Auto-drop a temp table when $tmpTbl falls out of scope + * + * $tmpTbl->setAutodrop(); + * + */ +class CRM_Utils_SQL_TempTable { + + const UTF8 = 'DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci'; + const CATEGORY_LENGTH = 12; + const CATEGORY_REGEXP = ';^[a-zA-Z0-9]+$;'; + const ID_LENGTH = 37; // MAX{64} - CATEGORY_LENGTH{12} - CONST_LENGHTH{15} = 37 + const ID_REGEXP = ';^[a-zA-Z0-9_]+$;'; + + /** + * @var bool + */ + protected $durable, $utf8; + + protected $category; + + protected $id; + + protected $autodrop; + + /** + * @return CRM_Utils_SQL_TempTable + */ + public static function build() { + $t = new CRM_Utils_SQL_TempTable(); + $t->category = NULL; + $t->id = md5(uniqid('', TRUE)); + // The constant CIVICRM_TEMP_FORCE_DURABLE is for local debugging. + $t->durable = CRM_Utils_Constant::value('CIVICRM_TEMP_FORCE_DURABLE', FALSE); + // I suspect it would be better to just say utf8=true, but a lot of existing queries don't do the utf8 bit. + $t->utf8 = CRM_Utils_Constant::value('CIVICRM_TEMP_FORCE_UTF8', FALSE); + $t->autodrop = FALSE; + return $t; + } + + public function __destruct() { + if ($this->autodrop) { + $this->drop(); + } + } + + /** + * Determine the full table name. + * + * @return string + * Ex: 'civicrm_tmp_d_foo_abcd1234abcd1234' + */ + public function getName() { + $parts = ['civicrm', 'tmp']; + $parts[] = ($this->durable ? 'd' : 'e'); + $parts[] = $this->category ? $this->category : 'dflt'; + $parts[] = $this->id ? $this->id : 'dflt'; + return implode('_', $parts); + } + + /** + * Create the table using results from a SELECT query. + * + * @param string|CRM_Utils_SQL_Select $selectQuery + * @return CRM_Utils_SQL_TempTable + */ + public function createWithQuery($selectQuery) { + $sql = sprintf('%s %s AS %s', + $this->toSQL('CREATE'), + $this->utf8 ? self::UTF8 : '', + ($selectQuery instanceof CRM_Utils_SQL_Select ? $selectQuery->toSQL() : $selectQuery) + ); + CRM_Core_DAO::executeQuery($sql, array(), TRUE, NULL, TRUE, FALSE); + return $this; + } + + /** + * Create the empty table. + * + * @parma string $columns + * SQL column listing. + * Ex: 'id int(10), name varchar(64)'. + * @return CRM_Utils_SQL_TempTable + */ + public function createWithColumns($columns) { + $sql = sprintf('%s (%s) %s', + $this->toSQL('CREATE'), + $columns, + $this->utf8 ? self::UTF8 : '' + ); + CRM_Core_DAO::executeQuery($sql, array(), TRUE, NULL, TRUE, FALSE); + return $this; + } + + /** + * Drop the table. + * + * @return CRM_Utils_SQL_TempTable + */ + public function drop() { + $sql = $this->toSQL('DROP', 'IF EXISTS'); + CRM_Core_DAO::executeQuery($sql, array(), TRUE, NULL, TRUE, FALSE); + return $this; + } + + /** + * @param string $action + * Ex: 'CREATE', 'DROP' + * @param string|NULL $ifne + * Ex: 'IF EXISTS', 'IF NOT EXISTS'. + * @return string + * Ex: 'CREATE TEMPORARY TABLE `civicrm_tmp_e_foo_abcd1234`' + * Ex: 'CREATE TABLE IF NOT EXISTS `civicrm_tmp_d_foo_abcd1234`' + */ + private function toSQL($action, $ifne = NULL) { + $parts = []; + $parts[] = $action; + if (!$this->durable) { + $parts[] = 'TEMPORARY'; + } + $parts[] = 'TABLE'; + if ($ifne) { + $parts[] = $ifne; + } + $parts[] = '`' . $this->getName() . '`'; + return implode(' ', $parts); + } + + /** + * @return string|NULL + */ + public function getCategory() { + return $this->category; + } + + /** + * @return string|NULL + */ + public function getId() { + return $this->id; + } + + /** + * @return bool + */ + public function isAutodrop() { + return $this->autodrop; + } + + /** + * @return bool + */ + public function isDurable() { + return $this->durable; + } + + /** + * @return bool + */ + public function isUtf8() { + return $this->utf8; + } + + /** + * @param bool $autodrop + * @return CRM_Utils_SQL_TempTable + */ + public function setAutodrop($autodrop = TRUE) { + $this->autodrop = $autodrop; + return $this; + } + + /** + * @param string|NULL $category + * @return CRM_Utils_SQL_TempTable + */ + public function setCategory($category) { + if ($category && !preg_match(self::CATEGORY_REGEXP, $category) || strlen($category) > self::CATEGORY_LENGTH) { + throw new \RuntimeException("Malformed temp table category"); + } + $this->category = $category; + return $this; + } + + /** + * @parma bool $value + * @return CRM_Utils_SQL_TempTable + */ + public function setDurable($durable = TRUE) { + $this->durable = $durable; + return $this; + } + + /** + * @param mixed $id + * @return CRM_Utils_SQL_TempTable + */ + public function setId($id) { + if ($id && !preg_match(self::ID_REGEXP, $id) || strlen($id) > self::ID_LENGTH) { + throw new \RuntimeException("Malformed temp table id"); + } + $this->id = $id; + return $this; + } + + public function setUtf8($value = TRUE) { + $this->utf8 = $value; + return $this; + } + +} diff --git a/civicrm/CRM/Utils/System.php b/civicrm/CRM/Utils/System.php index e2d72f5303244d9eb8c031c82696994bedd3f47f..fb5bb879ea4e173673bc3672a59e41d4a1f95651 100644 --- a/civicrm/CRM/Utils/System.php +++ b/civicrm/CRM/Utils/System.php @@ -1416,8 +1416,21 @@ class CRM_Utils_System { public static function flushCache() { // flush out all cache entries so we can reload new data // a bit aggressive, but livable for now - $cache = CRM_Utils_Cache::singleton(); - $cache->flush(); + CRM_Utils_Cache::singleton()->flush(); + + // Traditionally, systems running on memory-backed caches were quite + // zealous about destroying *all* memory-backed caches during a flush(). + // These flushes simulate that legacy behavior. However, they should probably + // be removed at some point. + $localDrivers = ['CRM_Utils_Cache_Arraycache', 'CRM_Utils_Cache_NoCache']; + if (Civi\Core\Container::isContainerBooted() + && !in_array(get_class(CRM_Utils_Cache::singleton()), $localDrivers)) { + Civi::cache('settings')->flush(); + Civi::cache('js_strings')->flush(); + Civi::cache('community_messages')->flush(); + CRM_Extension_System::singleton()->getCache()->flush(); + CRM_Cxn_CiviCxnHttp::singleton()->getCache()->flush(); + } // also reset the various static memory caches diff --git a/civicrm/Civi.php b/civicrm/Civi.php index ae5dc6f4b1dda2f1a883542edbb0ec98c17b14dc..8acfdba76862963e124de00335dd48a67e58806c 100644 --- a/civicrm/Civi.php +++ b/civicrm/Civi.php @@ -26,20 +26,15 @@ class Civi { public static $statics = array(); /** - * EXPERIMENTAL. Retrieve a named cache instance. - * - * This interface is flagged as experimental due to political - * ambiguity in PHP community -- PHP-FIG has an open but - * somewhat controversial draft standard for caching. Based on - * the current draft, it's expected that this function could - * simultaneously support both CRM_Utils_Cache_Interface and - * PSR-6, but that depends on whether PSR-6 changes any more. + * Retrieve a named cache instance. * * @param string $name * The name of the cache. The 'default' cache is biased toward * high-performance caches (eg memcache/redis/apc) when * available and falls back to single-request (static) caching. * @return CRM_Utils_Cache_Interface + * NOTE: Beginning in CiviCRM v5.4, the cache instance complies with + * PSR-16 (\Psr\SimpleCache\CacheInterface). */ public static function cache($name = 'default') { return \Civi\Core\Container::singleton()->get('cache.' . $name); diff --git a/civicrm/Civi/Angular/Manager.php b/civicrm/Civi/Angular/Manager.php index 62d896e2f8f505dc018bc4a3734bd028a7c4b82d..61f36af0eeb9dd50d93b87a51c49686ed796b3b3 100644 --- a/civicrm/Civi/Angular/Manager.php +++ b/civicrm/Civi/Angular/Manager.php @@ -246,7 +246,7 @@ class Manager { * Invalid partials configuration. */ public function getPartials($name) { - $cacheKey = "angular-partials::$name"; + $cacheKey = "angular-partials_$name"; $cacheValue = $this->cache->get($cacheKey); if ($cacheValue === NULL) { $cacheValue = ChangeSet::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name)); diff --git a/civicrm/Civi/Core/Container.php b/civicrm/Civi/Core/Container.php index ffe1bfbe3bcc93284bd68df4ecb706e19f5de7f5..82dc0614de61888b20c2b13e28bcba079d3b2598 100644 --- a/civicrm/Civi/Core/Container.php +++ b/civicrm/Civi/Core/Container.php @@ -65,7 +65,7 @@ class Container { // services. Consequently, we assume a minimal service available -- the classloader // has been setup, and civicrm.settings.php is loaded, but nothing else works. - $cacheMode = defined('CIVICRM_CONTAINER_CACHE') ? CIVICRM_CONTAINER_CACHE : 'always'; + $cacheMode = defined('CIVICRM_CONTAINER_CACHE') ? CIVICRM_CONTAINER_CACHE : 'auto'; // In pre-installation environments, don't bother with caching. if (!defined('CIVICRM_TEMPLATE_COMPILEDIR') || !defined('CIVICRM_DSN') || $cacheMode === 'never' || \CRM_Utils_System::isInUpgradeMode()) { @@ -160,12 +160,18 @@ class Container { $container->setDefinition('psr_log', new Definition('CRM_Core_Error_Log', array())); - foreach (array('js_strings', 'community_messages') as $cacheName) { - $container->setDefinition("cache.{$cacheName}", new Definition( + $basicCaches = array( + 'js_strings' => 'js_strings', + 'community_messages' => 'community_messages', + 'checks' => 'checks', + 'session' => 'CiviCRM Session', + ); + foreach ($basicCaches as $cacheSvc => $cacheGrp) { + $container->setDefinition("cache.{$cacheSvc}", new Definition( 'CRM_Utils_Cache_Interface', array( array( - 'name' => $cacheName, + 'name' => $cacheGrp, 'type' => array('*memory*', 'SqlGroup', 'ArrayCache'), ), ) diff --git a/civicrm/Civi/Core/SettingsManager.php b/civicrm/Civi/Core/SettingsManager.php index 8147afca982570b15757300a30a0253a1ca88467..e56d723b2fbaf501a18acd9953530bd9462b872b 100644 --- a/civicrm/Civi/Core/SettingsManager.php +++ b/civicrm/Civi/Core/SettingsManager.php @@ -205,7 +205,7 @@ class SettingsManager { return self::getSystemDefaults($entity); } - $cacheKey = 'defaults:' . $entity; + $cacheKey = 'defaults_' . $entity; $defaults = $this->cache->get($cacheKey); if (!is_array($defaults)) { $specs = SettingsMetadata::getMetadata(array( diff --git a/civicrm/ang/crmCaseType.js b/civicrm/ang/crmCaseType.js index ee9efb960306a0300db811fe211f5b17a779b9fe..12500df6154ca3eab8d5f03e7d4cc6e2cd042ed1 100644 --- a/civicrm/ang/crmCaseType.js +++ b/civicrm/ang/crmCaseType.js @@ -67,6 +67,13 @@ limit: 0 } }]; + reqs.defaultAssigneeTypes = ['OptionValue', 'get', { + option_group_id: 'activity_default_assignee', + sequential: 1, + options: { + limit: 0 + } + }]; reqs.relTypes = ['RelationshipType', 'get', { sequential: 1, options: { @@ -230,41 +237,101 @@ }); crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls) { - // CRM_Case_XMLProcessor::REL_TYPE_CNAME - var REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME, + var REL_TYPE_CNAME, defaultAssigneeDefaultValue, ts; + + (function init () { + // CRM_Case_XMLProcessor::REL_TYPE_CNAME + REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME; + + ts = $scope.ts = CRM.ts(null); + $scope.locks = { caseTypeName: true, activitySetName: true }; + $scope.workflows = { timeline: 'Timeline', sequence: 'Sequence' }; + defaultAssigneeDefaultValue = _.find(apiCalls.defaultAssigneeTypes.values, { is_default: '1' }) || {}; + + storeApiCallsResults(); + initCaseType(); + initCaseTypeDefinition(); + initSelectedStatuses(); + })(); + + /// Stores the api calls results in the $scope object + function storeApiCallsResults() { + $scope.activityStatuses = apiCalls.actStatuses.values; + $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name'); + $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name'); + $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption); + $scope.defaultAssigneeTypes = apiCalls.defaultAssigneeTypes.values; + $scope.relationshipTypeOptions = _.map(apiCalls.relTypes.values, function(type) { + return {id: type[REL_TYPE_CNAME], text: type.label_b_a}; + }); + $scope.defaultRelationshipTypeOptions = getDefaultRelationshipTypeOptions(); + // stores the default assignee values indexed by their option name: + $scope.defaultAssigneeTypeValues = _.chain($scope.defaultAssigneeTypes) + .indexBy('name').mapValues('value').value(); + } - ts = $scope.ts = CRM.ts(null); + /// Returns the default relationship type options. If the relationship is + /// bidirectional (Ex: Spouse of) it adds a single option otherwise it adds + /// two options representing the relationship type directions + /// (Ex: Employee of, Employer is) + function getDefaultRelationshipTypeOptions() { + return _.transform(apiCalls.relTypes.values, function(result, relType) { + var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a; + + result.push({ + label: relType.label_b_a, + value: relType.id + '_b_a' + }); - $scope.activityStatuses = apiCalls.actStatuses.values; - $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name'); - $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name'); - $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption); - $scope.relationshipTypeOptions = _.map(apiCalls.relTypes.values, function(type) { - return {id: type[REL_TYPE_CNAME], text: type.label_b_a}; - }); - $scope.locks = {caseTypeName: true, activitySetName: true}; + if (!isBidirectionalRelationship) { + result.push({ + label: relType.label_a_b, + value: relType.id + '_a_b' + }); + } + }, []); + } - $scope.workflows = { - 'timeline': 'Timeline', - 'sequence': 'Sequence' - }; + /// initializes the case type object + function initCaseType() { + var isNewCaseType = !apiCalls.caseType; + + if (isNewCaseType) { + $scope.caseType = _.cloneDeep(newCaseTypeTemplate); + } else { + $scope.caseType = apiCalls.caseType; + } + } - $scope.caseType = apiCalls.caseType ? apiCalls.caseType : _.cloneDeep(newCaseTypeTemplate); - $scope.caseType.definition = $scope.caseType.definition || []; - $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || []; - $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || []; - _.each($scope.caseType.definition.activitySets, function (set) { - _.each(set.activityTypes, function (type, name) { - type.label = $scope.activityTypes[type.name].label; + /// initializes the case type definition object + function initCaseTypeDefinition() { + $scope.caseType.definition = $scope.caseType.definition || []; + $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || []; + $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || []; + $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || []; + $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || []; + $scope.caseType.definition.timelineActivityTypes = $scope.caseType.definition.timelineActivityTypes || []; + + _.each($scope.caseType.definition.activitySets, function (set) { + _.each(set.activityTypes, function (type, name) { + var isDefaultAssigneeTypeUndefined = _.isUndefined(type.default_assignee_type); + type.label = $scope.activityTypes[type.name].label; + + if (isDefaultAssigneeTypeUndefined) { + type.default_assignee_type = defaultAssigneeDefaultValue.value; + } + }); }); - }); - $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || []; - $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || []; + } - $scope.selectedStatuses = {}; - _.each(apiCalls.caseStatuses.values, function (status) { - $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1; - }); + /// initializes the selected statuses + function initSelectedStatuses() { + $scope.selectedStatuses = {}; + + _.each(apiCalls.caseStatuses.values, function (status) { + $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1; + }); + } $scope.addActivitySet = function(workflow) { var activitySet = {}; @@ -288,14 +355,28 @@ } function addActivityToSet(activitySet, activityTypeName) { - activitySet.activityTypes.push({ - name: activityTypeName, - label: $scope.activityTypes[activityTypeName].label, - status: 'Scheduled', - reference_activity: 'Open Case', - reference_offset: '1', - reference_select: 'newest' - }); + var activity = { + name: activityTypeName, + label: $scope.activityTypes[activityTypeName].label, + status: 'Scheduled', + reference_activity: 'Open Case', + reference_offset: '1', + reference_select: 'newest', + default_assignee_type: $scope.defaultAssigneeTypeValues.NONE + }; + activitySet.activityTypes.push(activity); + if(typeof activitySet.timeline !== "undefined" && activitySet.timeline == "1") { + $scope.caseType.definition.timelineActivityTypes.push(activity); + } + } + + function resetTimelineActivityTypes() { + $scope.caseType.definition.timelineActivityTypes = []; + angular.forEach($scope.caseType.definition.activitySets, function(activitySet) { + angular.forEach(activitySet.activityTypes, function(activityType) { + $scope.caseType.definition.timelineActivityTypes.push(activityType); + }); + }); } function createActivity(name, callback) { @@ -334,6 +415,12 @@ } }; + /// Clears the activity's default assignee values for relationship and contact + $scope.clearActivityDefaultAssigneeValues = function(activity) { + activity.default_assignee_relationship = null; + activity.default_assignee_contact = null; + }; + /// Add a new role $scope.addRole = function(roles, roleName) { var names = _.pluck($scope.caseType.definition.caseRoles, 'name'); @@ -363,6 +450,7 @@ var idx = _.indexOf(array, item); if (idx != -1) { array.splice(idx, 1); + resetTimelineActivityTypes(); } }; @@ -462,6 +550,7 @@ if (!$scope.isForkable()) { CRM.alert(ts('The CiviCase XML file for this case-type prohibits editing the definition.')); } + }); crmCaseType.controller('CaseTypeListCtrl', function($scope, crmApi, caseTypes) { diff --git a/civicrm/ang/crmCaseType/timelineTable.html b/civicrm/ang/crmCaseType/timelineTable.html index bc38d3ddd57438bcd13f93a04e3f7e23f4b71b79..4d044f1b9d32b88568c82e23b13fbf4c06e1f518 100644 --- a/civicrm/ang/crmCaseType/timelineTable.html +++ b/civicrm/ang/crmCaseType/timelineTable.html @@ -11,6 +11,7 @@ Required vars: activitySet <th>{{ts('Reference')}}</th> <th>{{ts('Offset')}}</th> <th>{{ts('Select')}}</th> + <th>{{ts('Default assignee')}}</th> <th></th> </tr> </thead> @@ -21,13 +22,13 @@ Required vars: activitySet <i class="crm-i fa-arrows grip-n-drag"></i> </td> <td> - <i class="crm-i {{ activityTypes[activity.name].icon }}"></i> - {{ activity.label }} + <i class="crm-i {{activityTypes[activity.name].icon}}"></i> + {{activity.label}} </td> <td> <select ui-jq="select2" - ui-options="{dropdownAutoWidth : true}" + ui-options="{dropdownAutoWidth: true}" ng-model="activity.status" ng-options="actStatus.name as actStatus.label for actStatus in activityStatuses|orderBy:'label'" > @@ -37,9 +38,9 @@ Required vars: activitySet <td> <select ui-jq="select2" - ui-options="{dropdownAutoWidth : true}" + ui-options="{dropdownAutoWidth: true}" ng-model="activity.reference_activity" - ng-options="activityType.name as activityType.label for activityType in activitySet.activityTypes" + ng-options="activityType.name as activityType.label for activityType in caseType.definition.timelineActivityTypes" > <option value="">-- Case Start --</option> </select> @@ -55,12 +56,41 @@ Required vars: activitySet <td> <select ui-jq="select2" - ui-options="{dropdownAutoWidth : true}" + ui-options="{dropdownAutoWidth: true}" ng-model="activity.reference_select" ng-options="key as value for (key,value) in {newest: ts('Newest'), oldest: ts('Oldest')}" > </select> </td> + <td> + <select + ui-jq="select2" + ui-options="{dropdownAutoWidth: true}" + ng-model="activity.default_assignee_type" + ng-options="option.value as option.label for option in defaultAssigneeTypes" + ng-change="clearActivityDefaultAssigneeValues(activity)" + ></select> + + <p ng-if="activity.default_assignee_type === defaultAssigneeTypeValues.BY_RELATIONSHIP"> + <select + ui-jq="select2" + ui-options="{dropdownAutoWidth: true}" + ng-model="activity.default_assignee_relationship" + ng-options="option.value as option.label for option in defaultRelationshipTypeOptions" + required + ></select> + </p> + + <p ng-if="activity.default_assignee_type === defaultAssigneeTypeValues.SPECIFIC_CONTACT"> + <input + type="text" + ng-model="activity.default_assignee_contact" + placeholder="- Select contact -" + crm-entityref="{ entity: 'Contact' }" + data-create-links="true" + required /> + </p> + </td> <td> <a class="crm-hover-button" crm-icon="fa-trash" @@ -74,7 +104,7 @@ Required vars: activitySet <tfoot> <tr class="addRow"> - <td colspan="6"> + <td colspan="8"> <span crm-add-name="" crm-options="activityTypeOptions" crm-var="newActivity" diff --git a/civicrm/ang/crmMailing/BlockPreview.html b/civicrm/ang/crmMailing/BlockPreview.html index 1addd5fb46600fc63c37cbc23f471ddc132ef99c..6315466e83cf65ca57601ce2b54c398fb0296e31 100644 --- a/civicrm/ang/crmMailing/BlockPreview.html +++ b/civicrm/ang/crmMailing/BlockPreview.html @@ -37,7 +37,7 @@ Vars: mailing:obj, testContact:obj, testGroup:obj, crmMailing:FormController crm-multiple-email /> </div> - <button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testContact.email ? ts('Complete all required fields first') : ts('Send test message to %1', {1: testContact.email})}}" ng-disabled="crmMailing.$invalid || !testContact.email" ng-click="doSend({email: testContact.email})">{{ts('Send test')}}</button> + <button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testContact.email ? ts('Complete all required fields first') : ts('Send test message to %1', {1: testContact.email})}}" ng-disabled="crmMailing.$invalid || !testContact.email" ng-click="doSend({email: testContact.email})" class="crmMailing-btn-primary">{{ts('Send test')}}</button> </div> <div class="preview-group" ng-form=""> <div> @@ -51,7 +51,7 @@ Vars: mailing:obj, testContact:obj, testGroup:obj, crmMailing:FormController class="crm-action-menu fa-envelope-o" /> </div> - <button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testGroup.gid ? ts('Complete all required fields first') : ts('Send test message to group')}}" ng-disabled="crmMailing.$invalid || !testGroup.gid" crm-confirm="{resizable: true, width: '40%', height: '40%', open: previewTestGroup}" on-yes="doSend({gid: testGroup.gid})">{{ts('Send test')}}</button> + <button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testGroup.gid ? ts('Complete all required fields first') : ts('Send test message to group')}}" ng-disabled="crmMailing.$invalid || !testGroup.gid" crm-confirm="{resizable: true, width: '40%', height: '40%', open: previewTestGroup}" on-yes="doSend({gid: testGroup.gid})" class="crmMailing-btn-primary">{{ts('Send test')}}</button> </div> <div class="clear"></div> </div> diff --git a/civicrm/ang/crmMailing/EditMailingCtrl/2step.html b/civicrm/ang/crmMailing/EditMailingCtrl/2step.html index 94752d6d924bf87062de84ed9cc38fd5096a2630..cabd6f307c698770c9caf2fcc958f0ae069931bc 100644 --- a/civicrm/ang/crmMailing/EditMailingCtrl/2step.html +++ b/civicrm/ang/crmMailing/EditMailingCtrl/2step.html @@ -42,7 +42,7 @@ <div crm-mailing-block-schedule crm-mailing="mailing"></div> </div> <center> - <a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}"> + <a class="button crmMailing-submit-button crmMailing-btn-primary" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}"> <div>{{ts('Submit Mailing')}}</div> </a> </center> @@ -52,10 +52,11 @@ <button crm-icon="fa-trash" ng-show="checkPerm('delete in CiviMail')" + class="crmMailing-btn-danger-outline" ng-disabled="block.check()" crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}" on-yes="delete()">{{ts('Delete Draft')}}</button> - <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button> + <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)" class="crmMailing-btn-secondary-outline">{{ts('Save Draft')}}</button> </span> </div> </div> diff --git a/civicrm/ang/crmMailing/EditMailingCtrl/unified.html b/civicrm/ang/crmMailing/EditMailingCtrl/unified.html index 93a8758764fb0740b481dba4a8f695c4342f75c4..cc3056fa2ff34d5a3dfd31092b73fde5d47ed4a8 100644 --- a/civicrm/ang/crmMailing/EditMailingCtrl/unified.html +++ b/civicrm/ang/crmMailing/EditMailingCtrl/unified.html @@ -37,11 +37,12 @@ <div crm-mailing-block-schedule crm-mailing="mailing"></div> </div> - <button crm-icon="fa-paper-plane" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button> - <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button> + <button crm-icon="fa-paper-plane" class="crmMailing-btn-primary" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button> + <button crm-icon="fa-floppy-o" class="crmMailing-btn-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button> <button crm-icon="fa-trash" ng-show="checkPerm('delete in CiviMail')" + class="crmMailing-btn-danger-outline" ng-disabled="block.check()" crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}" on-yes="delete()">{{ts('Delete Draft')}}</button> diff --git a/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html b/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html index 04ff30dd06aae6cbd20821ade9e44fb1914ed0fe..1506b8d5930de9d701aa5e15b5ba43b0d55d1e86 100644 --- a/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html +++ b/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html @@ -33,11 +33,12 @@ <div crm-mailing-block-schedule crm-mailing="mailing"></div> </div> - <button crm-icon="fa-paper-plane" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button> - <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button> + <button crm-icon="fa-paper-plane" class="crmMailing-btn-primary" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button> + <button crm-icon="fa-floppy-o" class="crmMailing-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button> <button crm-icon="fa-trash" ng-show="checkPerm('delete in CiviMail')" + class="crmMailing-btn-danger-outline" ng-disabled="block.check()" crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}" on-yes="delete()">{{ts('Delete Draft')}}</button> diff --git a/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html b/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html index ee589fd7765e7bdba79d9b746b779b221a3c4975..9854cc5c95273aaddddb0109d1557216f2afefa0 100644 --- a/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html +++ b/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html @@ -45,7 +45,7 @@ <div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div> </div> <center> - <a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}"> + <a class="button crmMailing-submit-button crmMailing-btn-primary" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}"> <div>{{ts('Submit Mailing')}}</div> </a> </center> @@ -55,10 +55,11 @@ <button crm-icon="fa-trash" ng-show="checkPerm('delete in CiviMail')" + class="crmMailing-btn-danger-outline" ng-disabled="block.check()" crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}" on-yes="delete()">{{ts('Delete Draft')}}</button> - <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button> + <button crm-icon="fa-floppy-o" class="crmMailing-btn-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button> </span> </div> </div> diff --git a/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html b/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html index b5ae948c4750ddd97f6b626594b0bd6978acd8ae..affa76d84ec178c81b006ec372acea8997214c53 100644 --- a/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html +++ b/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html @@ -47,12 +47,12 @@ <div crm-mailing-block-approve crm-mailing="mailing"></div> </div> <center ng-if="!checkPerm('approve mailings') && !checkPerm('access CiviMail')"> - <a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}"> + <a class="button crmMailing-submit-button crmMailing-btn-primary" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}"> <div>{{ts('Submit Mailing')}}</div> </a> </center> <center ng-if="checkPerm('approve mailings') || checkPerm('access CiviMail')"> - <a class="button crmMailing-submit-button" ng-click="approve('Approved')" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}"> + <a class="button crmMailing-submit-button crmMailing-btn-primary" ng-click="approve('Approved')" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}"> <div>{{ts('Submit and Approve Mailing')}}</div> </a> </center> @@ -62,10 +62,11 @@ <button crm-icon="fa-trash" ng-show="checkPerm('delete in CiviMail')" + class="crmMailing-btn-danger-outline" ng-disabled="block.check()" crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}" on-yes="delete()">{{ts('Delete Draft')}}</button> - <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button> + <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)" class="crmMailing-btn-secondary-outline">{{ts('Save Draft')}}</button> </span> </div> </div> diff --git a/civicrm/ang/crmMailing/Recipients.js b/civicrm/ang/crmMailing/Recipients.js index ceae2db8712de152c4209abc501d22a363205e5c..759fd4c91ad2bc109dd9608f64c4cf6f0e1752b8 100644 --- a/civicrm/ang/crmMailing/Recipients.js +++ b/civicrm/ang/crmMailing/Recipients.js @@ -229,6 +229,11 @@ page_num: rcpAjaxState.page_i, params: filterParams, }; + + if('civicrm_mailing' === rcpAjaxState.entity) { + params["api.MailingRecipients.getcount"] = {}; + } + return params; }, transport: function(params) { @@ -246,12 +251,18 @@ results: function(data) { results = { children: $.map(data.values, function(obj) { - return { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type, - text: obj.label }; + if('civicrm_mailing' === rcpAjaxState.entity) { + return obj["api.MailingRecipients.getcount"] > 0 ? { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type, + text: obj.label } : ''; + } + else { + return { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type, + text: obj.label }; + } }) }; - if (rcpAjaxState.page_i == 1 && data.count) { + if (rcpAjaxState.page_i == 1 && data.count && results.children.length > 0) { results.text = ts((rcpAjaxState.type == 'include'? 'Include ' : 'Exclude ') + (rcpAjaxState.entity == 'civicrm_group'? 'Group' : 'Mailing')); } diff --git a/civicrm/ang/crmUi/wizard.html b/civicrm/ang/crmUi/wizard.html index 539f30113d4b8ad5004916d3b815949d9e1438b2..116f060dcf506127451d875b6bb042a8af57298d 100644 --- a/civicrm/ang/crmUi/wizard.html +++ b/civicrm/ang/crmUi/wizard.html @@ -9,7 +9,7 @@ </ul> <div class="crm-wizard-body" ng-transclude></div> <div class="crm-wizard-buttons"> - <button crm-icon="fa-chevron-left" ng-click="crmUiWizardCtrl.previous()" ng-show="!crmUiWizardCtrl.$first()">{{ts('Previous')}}</button> - <button crm-icon="fa-chevron-right" title="{{!crmUiWizardCtrl.$validStep() ? ts('Complete all required fields first') : ts('Next step')}}" ng-click="crmUiWizardCtrl.next()" ng-show="!crmUiWizardCtrl.$last()" ng-disabled="!crmUiWizardCtrl.$validStep()">{{ts('Next')}}</button> + <button crm-icon="fa-chevron-left" ng-click="crmUiWizardCtrl.previous()" ng-show="!crmUiWizardCtrl.$first()" class="crmUi-btn-primary">{{ts('Previous')}}</button> + <button crm-icon="fa-chevron-right" title="{{!crmUiWizardCtrl.$validStep() ? ts('Complete all required fields first') : ts('Next step')}}" ng-click="crmUiWizardCtrl.next()" ng-show="!crmUiWizardCtrl.$last()" ng-disabled="!crmUiWizardCtrl.$validStep()" class="crmUi-btn-primary">{{ts('Next')}}</button> </div> </div> diff --git a/civicrm/api/v3/Case.php b/civicrm/api/v3/Case.php index 0d27319d97b60cf8f15e0f63f2f91fcefffd5062..7aa2bdf88f3bb90b1e70d89f2c5ab2e1123fc463 100644 --- a/civicrm/api/v3/Case.php +++ b/civicrm/api/v3/Case.php @@ -39,12 +39,12 @@ * @param array $params * * @code - * //REQUIRED for create: + * // REQUIRED for create: * 'case_type_id' => int OR * 'case_type' => str (provide one or the other) * 'contact_id' => int // case client * 'subject' => str - * //REQUIRED for update: + * // REQUIRED for update: * 'id' => case Id * * //OPTIONAL @@ -77,42 +77,44 @@ function civicrm_api3_case_create($params) { // Update an existing case // FIXME: Some of this logic should move to the BAO object? // FIXME: Should we check if case with ID actually exists? - if (!isset($params['case_id']) && isset($params['id'])) { - $params['case_id'] = $params['id']; - } if (array_key_exists('creator_id', $params)) { throw new API_Exception('You cannot update creator id'); } - $mergedCaseId = $origContactIds = array(); + $mergedCaseIds = $origContactIds = array(); - // get original contact id and creator id of case + // If a contact ID is specified we need to make sure this is the main contact ID for the case (and update if necessary) if (!empty($params['contact_id'])) { $origContactIds = CRM_Case_BAO_Case::retrieveContactIdsByCaseId($params['id']); - $origContactId = CRM_Utils_Array::first($origContactIds); - } - // FIXME: Refactor as separate method to get contactId - if (count($origContactIds) > 1) { - // check valid orig contact id - if (empty($params['orig_contact_id'])) { - throw new API_Exception('Case is linked with more than one contact id. Provide the required params orig_contact_id to be replaced'); + // Get the original contact ID for the case + // FIXME: Refactor as separate method to get contactId + if (count($origContactIds) > 1) { + // Multiple original contact IDs. Need to specify which one to use as a parameter + if (empty($params['orig_contact_id'])) { + throw new API_Exception('Case is linked with more than one contact id. Provide the required params orig_contact_id to be replaced'); + } + if (!empty($params['orig_contact_id']) && !in_array($params['orig_contact_id'], $origContactIds)) { + throw new API_Exception('Invalid case contact id (orig_contact_id)'); + } + $origContactId = $params['orig_contact_id']; } - if (!empty($params['orig_contact_id']) && !in_array($params['orig_contact_id'], $origContactIds)) { - throw new API_Exception('Invalid case contact id (orig_contact_id)'); + else { + // Only one original contact ID + $origContactId = CRM_Utils_Array::first($origContactIds); } - $origContactId = $params['orig_contact_id']; - } - // check for same contact id for edit Client - if (!empty($params['contact_id']) && !in_array($params['contact_id'], $origContactIds)) { - $mergedCaseId = CRM_Case_BAO_Case::mergeCases($params['contact_id'], $params['case_id'], $origContactId, NULL, TRUE); - } + // Get the specified main contact ID for the case + $mainContactId = CRM_Utils_Array::first($params['contact_id']); - // If we merged cases then update the merged case - if (!empty($mergedCaseId[0])) { - $params['id'] = $mergedCaseId[0]; + // If the main contact ID is not in the list of original contact IDs for the case we need to change the main contact ID for the case + // This means we'll end up with a new case ID + if (!in_array($mainContactId, $origContactIds)) { + $mergedCaseIds = CRM_Case_BAO_Case::mergeCases($mainContactId, $params['id'], $origContactId, NULL, TRUE); + // If we merged cases then the first element will contain the case ID of the merged case - update that one + $params['id'] = CRM_Utils_Array::first($mergedCaseIds); + } } } @@ -146,8 +148,11 @@ function civicrm_api3_case_create($params) { /** * When creating a new case, run the xmlProcessor to get all necessary params/configuration * for the new case, as cases use an xml file to store their configuration. + * * @param $params * @param $caseBAO + * + * @throws \Exception */ function _civicrm_api3_case_create_xmlProcessor($params, $caseBAO) { // Format params for xmlProcessor @@ -629,7 +634,9 @@ function _civicrm_api3_case_read(&$cases, $options) { foreach ($cases as &$case) { if (empty($options['return']) || !empty($options['return']['contact_id'])) { // Legacy support for client_id - TODO: in apiv4 remove 'client_id' - $case['client_id'] = $case['contact_id'] = CRM_Case_BAO_Case::retrieveContactIdsByCaseId($case['id']); + // FIXME: Historically we return a 1-based array. Changing that risks breaking API clients that + // have been hardcoded to index "1", instead of the first array index (eg. using reset(), foreach etc) + $case['client_id'] = $case['contact_id'] = CRM_Case_BAO_Case::retrieveContactIdsByCaseId($case['id'], NULL, 1); } if (!empty($options['return']['contacts'])) { //get case contacts @@ -700,10 +707,26 @@ function _civicrm_api3_case_format_params(&$params) { _civicrm_api3_custom_format_params($params, $values, 'Case'); $params = array_merge($params, $values); + // A single or multiple contact_id (client_id) can be passed as a value or array. + // Convert single value to array here to simplify processing in later functions which expect an array. + if (isset($params['contact_id'])) { + if (!is_array($params['contact_id'])) { + $params['contact_id'] = array($params['contact_id']); + } + } + + // DEPRECATED: case_id - use id parameter instead. + if (!isset($params['id']) && isset($params['case_id'])) { + $params['id'] = $params['case_id']; + } + + // When creating a new case, either case_type_id or case_type must be specified. if (empty($params['case_type_id']) && empty($params['case_type'])) { + // If both case_type_id and case_type are empty we are updating a case so return here. return; } + // We are creating a new case // figure out case_type_id from case_type and vice-versa $caseTypes = CRM_Case_PseudoConstant::caseType('name', FALSE); if (empty($params['case_type_id'])) { diff --git a/civicrm/api/v3/ContributionRecur.php b/civicrm/api/v3/ContributionRecur.php index 144e62ad1a81cf6bad141cb14b2db774a264e5e8..5403ef353d243c311d701110db61fcaedba9c014 100644 --- a/civicrm/api/v3/ContributionRecur.php +++ b/civicrm/api/v3/ContributionRecur.php @@ -84,7 +84,7 @@ function civicrm_api3_contribution_recur_get($params) { */ function civicrm_api3_contribution_recur_cancel($params) { civicrm_api3_verify_one_mandatory($params, NULL, array('id')); - return CRM_Contribute_BAO_ContributionRecur::cancelRecurContribution($params['id'], CRM_Core_DAO::$_nullObject) ? civicrm_api3_create_success() : civicrm_api3_create_error(ts('Error while cancelling recurring contribution')); + return CRM_Contribute_BAO_ContributionRecur::cancelRecurContribution($params['id']) ? civicrm_api3_create_success() : civicrm_api3_create_error(ts('Error while cancelling recurring contribution')); } /** diff --git a/civicrm/api/v3/Job.php b/civicrm/api/v3/Job.php index 7a7e699dc8d9795a06a3d16d1c11cdcb44c52701..ab655369d1b5d7d14a9e5fe34423977d8c26214c 100644 --- a/civicrm/api/v3/Job.php +++ b/civicrm/api/v3/Job.php @@ -61,6 +61,41 @@ function civicrm_api3_job_create($params) { return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params, 'Job'); } +/** + * Adjust metadata for clone spec action. + * + * @param array $spec + */ +function _civicrm_api3_job_clone_spec(&$spec) { + $spec['id']['title'] = 'Job ID to clone'; + $spec['id']['type'] = CRM_Utils_Type::T_INT; + $spec['id']['api.required'] = 1; + $spec['is_active']['title'] = 'Job is Active?'; + $spec['is_active']['type'] = CRM_Utils_Type::T_BOOLEAN; + $spec['is_active']['api.required'] = 0; +} + +/** + * Clone Job. + * + * @param array $params + * + * @return array + * @throws \API_Exception + * @throws \CiviCRM_API3_Exception + */ +function civicrm_api3_job_clone($params) { + if (empty($params['id'])) { + throw new API_Exception("Mandatory key(s) missing from params array: id field is required"); + } + $id = $params['id']; + unset($params['id']); + $params['last_run'] = 'null'; + $params['scheduled_run_date'] = 'null'; + $newJobDAO = CRM_Core_BAO_Job::copy($id, $params); + return civicrm_api3('Job', 'get', array('id' => $newJobDAO->id)); +} + /** * Retrieve one or more job. * @@ -584,14 +619,15 @@ function civicrm_api3_job_cleanup($params) { $session = CRM_Utils_Array::value('session', $params, TRUE); $tempTable = CRM_Utils_Array::value('tempTables', $params, TRUE); $jobLog = CRM_Utils_Array::value('jobLog', $params, TRUE); + $expired = CRM_Utils_Array::value('expiredDbCache', $params, TRUE); $prevNext = CRM_Utils_Array::value('prevNext', $params, TRUE); $dbCache = CRM_Utils_Array::value('dbCache', $params, FALSE); $memCache = CRM_Utils_Array::value('memCache', $params, FALSE); $tplCache = CRM_Utils_Array::value('tplCache', $params, FALSE); $wordRplc = CRM_Utils_Array::value('wordRplc', $params, FALSE); - if ($session || $tempTable || $prevNext) { - CRM_Core_BAO_Cache::cleanup($session, $tempTable, $prevNext); + if ($session || $tempTable || $prevNext || $expired) { + CRM_Core_BAO_Cache::cleanup($session, $tempTable, $prevNext, $expired); } if ($jobLog) { diff --git a/civicrm/api/v3/Membership.php b/civicrm/api/v3/Membership.php index 4f0c657a9184ad1f70425e6bc19902fccf43e221..34af5cd09d151f70b6f442de929aa176fdf2e4a0 100644 --- a/civicrm/api/v3/Membership.php +++ b/civicrm/api/v3/Membership.php @@ -101,9 +101,12 @@ function civicrm_api3_membership_create($params) { _civicrm_api3_custom_format_params($params, $values, 'Membership'); $params = array_merge($params, $values); + // Calculate membership dates // Fixme: This code belongs in the BAO if (empty($params['id']) || !empty($params['num_terms'])) { + // If this is a new membership or we have a specified number of terms calculate membership dates. if (empty($params['id'])) { + // This is a new membership, calculate the membership dates. $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType( $params['membership_type_id'], CRM_Utils_Array::value('join_date', $params), @@ -113,6 +116,7 @@ function civicrm_api3_membership_create($params) { ); } else { + // This is an existing membership, calculate the membership dates after renewal $calcDates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType( $params['id'], NULL, @@ -128,20 +132,20 @@ function civicrm_api3_membership_create($params) { } // Fixme: This code belongs in the BAO - $action = CRM_Core_Action::ADD; - // we need user id during add mode - $ids = array(); - if (!empty($params['contact_id'])) { - $ids['userId'] = $params['contact_id']; + if (empty($params['id'])) { + $params['action'] = CRM_Core_Action::ADD; + // we need user id during add mode + $ids = array(); + if (!empty($params['contact_id'])) { + $ids['userId'] = $params['contact_id']; + } } - //for edit membership id should be present - // probably not required now. - if (!empty($params['id'])) { + else { + // edit mode + $params['action'] = CRM_Core_Action::UPDATE; + // $ids['membership'] is required in CRM_Price_BAO_LineItem::processPriceSet $ids['membership'] = $params['id']; - $action = CRM_Core_Action::UPDATE; } - //need to pass action to handle related memberships. - $params['action'] = $action; $membershipBAO = CRM_Member_BAO_Membership::create($params, $ids, TRUE); diff --git a/civicrm/api/v3/ReportTemplate.php b/civicrm/api/v3/ReportTemplate.php index e121c4114645756c5c1cbc030b50d88e694e3a71..d13cd54b44947a48c0b59bb9f8502cd9554a2461 100644 --- a/civicrm/api/v3/ReportTemplate.php +++ b/civicrm/api/v3/ReportTemplate.php @@ -113,6 +113,7 @@ function civicrm_api3_report_template_delete($params) { function civicrm_api3_report_template_getrows($params) { civicrm_api3_verify_one_mandatory($params, NULL, array('report_id', 'instance_id')); list($rows, $instance, $metadata) = _civicrm_api3_report_template_getrows($params); + $instance->cleanUpTemporaryTables(); return civicrm_api3_create_success($rows, $params, 'ReportTemplate', 'getrows', CRM_Core_DAO::$_nullObject, $metadata); } @@ -187,6 +188,7 @@ function _civicrm_api3_report_template_getrows($params) { function civicrm_api3_report_template_getstatistics($params) { list($rows, $reportInstance, $metadata) = _civicrm_api3_report_template_getrows($params); $stats = $reportInstance->statistics($rows); + $reportInstance->cleanUpTemporaryTables(); return civicrm_api3_create_success($stats, $params, 'ReportTemplate', 'getstatistics', CRM_Core_DAO::$_nullObject, $metadata); } /** diff --git a/civicrm/api/v3/examples/Setting/GetFields.php b/civicrm/api/v3/examples/Setting/GetFields.php index 8a4ccd83d82282b288659cbd0112c9e20344c0fb..32c450e058a8fae06cef9b0456a6ac33e0dfef39 100644 --- a/civicrm/api/v3/examples/Setting/GetFields.php +++ b/civicrm/api/v3/examples/Setting/GetFields.php @@ -1170,21 +1170,6 @@ function setting_getfields_expectedresult() { 'description' => '', 'help_text' => '', ), - 'systemStatusCheckResult' => array( - 'group_name' => 'CiviCRM Preferences', - 'group' => 'core', - 'name' => 'systemStatusCheckResult', - 'type' => 'Integer', - 'quick_form_type' => 'Element', - 'html_type' => 'text', - 'default' => 0, - 'add' => '4.7', - 'title' => 'systemStatusCheckResult', - 'is_domain' => 1, - 'is_contact' => 0, - 'description' => '', - 'help_text' => '', - ), 'recentItemsMaxCount' => array( 'group_name' => 'CiviCRM Preferences', 'group' => 'core', diff --git a/civicrm/api/v3/utils.php b/civicrm/api/v3/utils.php index 814d11e0b83a9ab9ccfd5a3a066e3a3a31cf5b3b..094beb2375b84225394995212bc08b0350135523 100644 --- a/civicrm/api/v3/utils.php +++ b/civicrm/api/v3/utils.php @@ -1977,43 +1977,15 @@ function _civicrm_api_get_custom_fields($entity, &$params) { // Regular fields have a 'name' property $value['name'] = 'custom_' . $key; $value['title'] = $value['label']; - $value['type'] = _getStandardTypeFromCustomDataType($value); + if ($value['data_type'] == 'Date' && CRM_Utils_Array::value('time_format', $value, 0) > 0) { + $value['data_type'] = 'DateTime'; + } + $value['type'] = CRM_Utils_Array::value($value['data_type'], CRM_Core_BAO_CustomField::dataToType()); $ret['custom_' . $key] = $value; } return $ret; } -/** - * Translate the custom field data_type attribute into a std 'type'. - * - * @param array $value - * - * @return int - */ -function _getStandardTypeFromCustomDataType($value) { - $dataType = $value['data_type']; - //CRM-15792 - If date custom field contains timeformat change type to DateTime - if ($value['data_type'] == 'Date' && isset($value['time_format']) && $value['time_format'] > 0) { - $dataType = 'DateTime'; - } - $mapping = array( - 'String' => CRM_Utils_Type::T_STRING, - 'Int' => CRM_Utils_Type::T_INT, - 'Money' => CRM_Utils_Type::T_MONEY, - 'Memo' => CRM_Utils_Type::T_LONGTEXT, - 'Float' => CRM_Utils_Type::T_FLOAT, - 'Date' => CRM_Utils_Type::T_DATE, - 'DateTime' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'Boolean' => CRM_Utils_Type::T_BOOLEAN, - 'StateProvince' => CRM_Utils_Type::T_INT, - 'File' => CRM_Utils_Type::T_STRING, - 'Link' => CRM_Utils_Type::T_STRING, - 'ContactReference' => CRM_Utils_Type::T_INT, - 'Country' => CRM_Utils_Type::T_INT, - ); - return $mapping[$dataType]; -} - /** * Fill params array with alternate (alias) values where a field has an alias and that is filled & the main field isn't. diff --git a/civicrm/bower_components/jquery-ui/.bower.json b/civicrm/bower_components/jquery-ui/.bower.json index 2373c86bc68d8af7283f3ca376121214276155d6..69ba102964fa2e81c1c7bba1451f376f5e85813e 100644 --- a/civicrm/bower_components/jquery-ui/.bower.json +++ b/civicrm/bower_components/jquery-ui/.bower.json @@ -16,6 +16,6 @@ "commit": "dec4c50123193d4f7c8ae6cd0bff45478e1ad276" }, "_source": "https://github.com/components/jqueryui.git", - "_target": "~1.12", + "_target": ">=1.9", "_originalSource": "jquery-ui" } \ No newline at end of file diff --git a/civicrm/civicrm-version.php b/civicrm/civicrm-version.php index 8a36ebdbb1f4c4a4385066591ff0fd6546a482f9..7f1c1ef4ef129fa89244e8bc0ca55640067bb6a1 100644 --- a/civicrm/civicrm-version.php +++ b/civicrm/civicrm-version.php @@ -1,7 +1,7 @@ <?php /** @deprecated */ function civicrmVersion( ) { - return array( 'version' => '5.3.2', + return array( 'version' => '5.4.0', 'cms' => 'Wordpress', 'revision' => '' ); } diff --git a/civicrm/composer.json b/civicrm/composer.json index 9d81135dbbec3aef07669abd4aa19d64343e6c29..1078ccf3d30346eefb791349eebd4dd61199eefb 100644 --- a/civicrm/composer.json +++ b/civicrm/composer.json @@ -55,7 +55,8 @@ "pear/Net_SMTP": "1.6.*", "pear/Net_socket": "1.0.*", "civicrm/civicrm-setup": "~0.2.0", - "guzzlehttp/guzzle": "^6.3" + "guzzlehttp/guzzle": "^6.3", + "psr/simple-cache": "~1.0.1" }, "repositories": [ { diff --git a/civicrm/composer.lock b/civicrm/composer.lock index 0cce99a2a89b969d505f497708f6b7606efa8dec..8f3e4d2659e99894ff0300d04addd2785b5eb1e3 100644 --- a/civicrm/composer.lock +++ b/civicrm/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "9c5441f5ce4c51ed3a8cc326693cd904", + "content-hash": "233f9c457d9e7d49a6d96c356e1035f1", "packages": [ { "name": "civicrm/civicrm-cxn-rpc", @@ -1131,6 +1131,54 @@ ], "time": "2012-12-21T11:40:51+00:00" }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, { "name": "sabberworm/php-css-parser", "version": "6.0.1", diff --git a/civicrm/css/searchForm.css b/civicrm/css/searchForm.css index bbafc5d745af2aa37633716758637b472672638a..a0f57b283a8c192324e68f6dc9c52dba6b12a9e5 100644 --- a/civicrm/css/searchForm.css +++ b/civicrm/css/searchForm.css @@ -62,3 +62,32 @@ color: #41477E; font-weight: bold; } + +.advanced-search-fields { + display: grid; + grid-template-columns: [col] repeat(3, calc(100% / 3 - 10px)); + width: 100%; +} + +.advanced-search-fields .search-field { + padding: 5px; +} + +.advanced-search-fields .search-field__span-2 { + grid-column: col / span 2; +} +.advanced-search-fields .search-field__span-3 { + grid-column: col / span 3; +} + +.advanced-search-fields .search-field__checkbox { + display: flex; +} + +.advanced-search-fields .search-field__checkbox input[type="checkbox"] { + order: -1; +} + +.advanced-search-fields .search-field__checkbox label { + padding-right: 5px; +} diff --git a/civicrm/ext/iatspayments/iATS_4.4.14.diff b/civicrm/ext/iatspayments/iATS_4.4.14.diff new file mode 100644 index 0000000000000000000000000000000000000000..02327db7ad48da9ff32463c19f89c9fe68d2f23e --- /dev/null +++ b/civicrm/ext/iatspayments/iATS_4.4.14.diff @@ -0,0 +1,49 @@ +--- ./CRM/Core/Payment/Form.php 2014-07-01 20:52:02.000000000 -0400 ++++ ./CRM/Core/Payment/Form.php 2014-09-12 08:27:20.564179607 -0400 +@@ -363,9 +363,9 @@ + $errors['cvv2'] = ts('Please enter a valid Card Verification Number'); + } + } +- elseif (!empty($values['credit_card_number'])) { +- $errors['credit_card_number'] = ts('Please enter a valid Card Number'); +- } ++ /* elseif (!empty($values['credit_card_number'])) { ++ $errors['credit_card_number'] = ts('Please enter a Card Number'); ++ } */ + } + + /** +--- ./CRM/Member/Form/Membership.php 2014-07-01 20:52:02.000000000 -0400 ++++ ./CRM/Member/Form/Membership.php 2014-09-11 13:42:33.470862876 -0400 +@@ -150,7 +150,7 @@ + if ($this->_mode) { + $this->_paymentProcessor = array('billing_mode' => 1); + $validProcessors = array(); +- $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, 'billing_mode IN ( 1, 3 )'); ++ $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, 'billing_mode IN ( 1, 3 ) AND payment_type = 1'); + + foreach ($processors as $ppID => $label) { + $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($ppID, $this->_mode); +--- ./CRM/Event/Form/Participant.php 2014-07-01 20:52:02.000000000 -0400 ++++ ./CRM/Event/Form/Participant.php 2014-09-11 12:36:41.549807505 -0400 +@@ -264,7 +264,7 @@ + $this->_paymentProcessor = array('billing_mode' => 1); + + $validProcessors = array(); +- $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, "billing_mode IN ( 1, 3 )"); ++ $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, "billing_mode IN ( 1, 3 ) AND payment_type = 1"); + + foreach ($processors as $ppID => $label) { + $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($ppID, $this->_mode); +--- ./CRM/Event/Form/Participant.php 2015-03-26 19:51:15.208118122 -0400 ++++ ./CRM/Event/Form/Participant.php 2015-03-26 19:52:20.455620537 -0400 +@@ -1340,7 +1340,8 @@ + $payment = CRM_Core_Payment::singleton($this->_mode, $this->_paymentProcessor, $this); + + // CRM-15622: fix for incorrect contribution.fee_amount +- $paymentParams['fee_amount'] = NULL; ++ // KG 4.4 issue only ++ // $paymentParams['fee_amount'] = NULL; + $result = $payment->doDirectPayment($paymentParams); + + if (is_a($result, 'CRM_Core_Error')) { diff --git a/civicrm/ext/iatspayments/iATS_4.5.8.diff b/civicrm/ext/iatspayments/iATS_4.5.8.diff new file mode 100644 index 0000000000000000000000000000000000000000..48e9b1c5e1f4f68ec88bdd18acce1090f590c841 --- /dev/null +++ b/civicrm/ext/iatspayments/iATS_4.5.8.diff @@ -0,0 +1,37 @@ +--- CRM/Core/Payment/Form.php ++++ CRM/Core/Payment/Form.php +@@ -364,9 +364,9 @@ class CRM_Core_Payment_Form { + $errors['cvv2'] = ts('Please enter a valid Card Verification Number'); + } + } +- elseif (!empty($values['credit_card_number'])) { +- $errors['credit_card_number'] = ts('Please enter a valid Card Number'); +- } ++ /* elseif (!empty($values['credit_card_number'])) { ++ $errors['credit_card_number'] = ts('Please enter a Card Number'); ++ } */ + } + + /** +--- CRM/Event/Form/Participant.php ++++ CRM/Event/Form/Participant.php +@@ -274,7 +274,7 @@ class CRM_Event_Form_Participant extends CRM_Contact_Form_Task { + $this->_paymentProcessor = array('billing_mode' => 1); + + $validProcessors = array(); +- $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, "billing_mode IN ( 1, 3 )"); ++ $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, "billing_mode IN ( 1, 3 ) AND payment_type = 1"); + + foreach ($processors as $ppID => $label) { + $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($ppID, $this->_mode); +--- CRM/Member/Form/Membership.php ++++ CRM/Member/Form/Membership.php +@@ -150,7 +150,7 @@ class CRM_Member_Form_Membership extends CRM_Member_Form { + if ($this->_mode) { + $this->_paymentProcessor = array('billing_mode' => 1); + $validProcessors = array(); +- $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, 'billing_mode IN ( 1, 3 )'); ++ $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, 'billing_mode IN ( 1, 3 ) AND payment_type = 1'); + + foreach ($processors as $ppID => $label) { + $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($ppID, $this->_mode); diff --git a/civicrm/js/crm.ajax.js b/civicrm/js/crm.ajax.js index 9cb5eb601e0985c32784f8fa7724f2657be552b4..e1742da8fd9873c3bd318435de350b867686df80 100644 --- a/civicrm/js/crm.ajax.js +++ b/civicrm/js/crm.ajax.js @@ -68,7 +68,7 @@ url: CRM.url('civicrm/ajax/rest'), dataType: 'json', data: params, - type: params.action.indexOf('get') < 0 ? 'POST' : 'GET' + type: params.action.indexOf('get') === 0 ? 'GET' : 'POST' }); if (status) { // Default status messages diff --git a/civicrm/packages/Cache/IntegrationTests/LICENSE b/civicrm/packages/Cache/IntegrationTests/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f166f6e87e073203fc5e311c1545d39fa40fa45d --- /dev/null +++ b/civicrm/packages/Cache/IntegrationTests/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Aaron Scherer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/civicrm/packages/Cache/IntegrationTests/LegacySimpleCacheTest.php b/civicrm/packages/Cache/IntegrationTests/LegacySimpleCacheTest.php new file mode 100644 index 0000000000000000000000000000000000000000..abf59427f09da4564076c12115fde21fff5a4746 --- /dev/null +++ b/civicrm/packages/Cache/IntegrationTests/LegacySimpleCacheTest.php @@ -0,0 +1,757 @@ +<?php + +/* + * This file is part of php-cache organization. + * + * (c) 2015-2015 Aaron Scherer <aequasi@gmail.com>, Tobias Nyholm <tobias.nyholm@gmail.com> + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\IntegrationTests; + +use PHPUnit_Framework_TestCase as TestCase; +use Psr\SimpleCache\CacheInterface; + +abstract class LegacySimpleCacheTest extends TestCase +{ + /** + * @type array with functionName => reason. + */ + protected $skippedTests = []; + + /** + * @type CacheInterface + */ + protected $cache; + + /** + * @return CacheInterface that is used in the tests + */ + abstract public function createSimpleCache(); + + protected function setUp() + { + $this->cache = $this->createSimpleCache(); + } + + protected function tearDown() + { + if ($this->cache !== null) { + $this->cache->clear(); + } + } + + /** + * Data provider for invalid keys. + * + * @return array + */ + public static function invalidKeys() + { + return [ + [''], + [true], + [false], + [null], + [2], + [2.5], + ['{str'], + ['rand{'], + ['rand{str'], + ['rand}str'], + ['rand(str'], + ['rand)str'], + ['rand/str'], + ['rand\\str'], + ['rand@str'], + ['rand:str'], + [new \stdClass()], + [['array']], + ]; + } + + /** + * @return array + */ + public static function invalidTtl() + { + return [ + [''], + [true], + [false], + ['abc'], + [2.5], + [' 1'], // can be casted to a int + ['12foo'], // can be casted to a int + ['025'], // can be interpreted as hex + [new \stdClass()], + [['array']], + ]; + } + + /** + * Data provider for valid keys. + * + * @return array + */ + public static function validKeys() + { + return [ + ['AbC19_.'], + ['1234567890123456789012345678901234567890123456789012345678901234'], + ]; + } + + /** + * Data provider for valid data to store. + * + * @return array + */ + public static function validData() + { + return [ + ['AbC19_.'], + [4711], + [47.11], + [true], + [null], + [['key' => 'value']], + [new \stdClass()], + ]; + } + + public function testSet() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $result = $this->cache->set('key', 'value'); + $this->assertTrue($result, 'set() must return true if success'); + $this->assertEquals('value', $this->cache->get('key')); + } + + public function testSetTtl() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $result = $this->cache->set('key1', 'value', 1); + $this->assertTrue($result, 'set() must return true if success'); + $this->assertEquals('value', $this->cache->get('key1')); + sleep(2); + $this->assertNull($this->cache->get('key1'), 'Value must expire after ttl.'); + + $this->cache->set('key2', 'value', new \DateInterval('PT1S')); + $this->assertEquals('value', $this->cache->get('key2')); + sleep(2); + $this->assertNull($this->cache->get('key2'), 'Value must expire after ttl.'); + } + + public function testSetExpiredTtl() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set('key0', 'value'); + $this->cache->set('key0', 'value', 0); + $this->assertNull($this->cache->get('key0')); + $this->assertFalse($this->cache->has('key0')); + + $this->cache->set('key1', 'value', -1); + $this->assertNull($this->cache->get('key1')); + $this->assertFalse($this->cache->has('key1')); + } + + public function testGet() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertNull($this->cache->get('key')); + $this->assertEquals('foo', $this->cache->get('key', 'foo')); + + $this->cache->set('key', 'value'); + $this->assertEquals('value', $this->cache->get('key', 'foo')); + } + + public function testDelete() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertTrue($this->cache->delete('key'), 'Deleting a value that does not exist should return true'); + $this->cache->set('key', 'value'); + $this->assertTrue($this->cache->delete('key'), 'Delete must return true on success'); + $this->assertNull($this->cache->get('key'), 'Values must be deleted on delete()'); + } + + public function testClear() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertTrue($this->cache->clear(), 'Clearing an empty cache should return true'); + $this->cache->set('key', 'value'); + $this->assertTrue($this->cache->clear(), 'Delete must return true on success'); + $this->assertNull($this->cache->get('key'), 'Values must be deleted on clear()'); + } + + public function testSetMultiple() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $result = $this->cache->setMultiple(['key0' => 'value0', 'key1' => 'value1']); + $this->assertTrue($result, 'setMultiple() must return true if success'); + $this->assertEquals('value0', $this->cache->get('key0')); + $this->assertEquals('value1', $this->cache->get('key1')); + } + + public function testSetMultipleWithIntegerArrayKey() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $result = $this->cache->setMultiple(['0' => 'value0']); + $this->assertTrue($result, 'setMultiple() must return true if success'); + $this->assertEquals('value0', $this->cache->get('0')); + } + + public function testSetMultipleTtl() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->setMultiple(['key2' => 'value2', 'key3' => 'value3'], 1); + $this->assertEquals('value2', $this->cache->get('key2')); + $this->assertEquals('value3', $this->cache->get('key3')); + sleep(2); + $this->assertNull($this->cache->get('key2'), 'Value must expire after ttl.'); + $this->assertNull($this->cache->get('key3'), 'Value must expire after ttl.'); + + $this->cache->setMultiple(['key4' => 'value4'], new \DateInterval('PT1S')); + $this->assertEquals('value4', $this->cache->get('key4')); + sleep(2); + $this->assertNull($this->cache->get('key4'), 'Value must expire after ttl.'); + } + + public function testSetMultipleExpiredTtl() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->setMultiple(['key0' => 'value0', 'key1' => 'value1'], 0); + $this->assertNull($this->cache->get('key0')); + $this->assertNull($this->cache->get('key1')); + } + + public function testSetMultipleWithGenerator() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $gen = function () { + yield 'key0' => 'value0'; + yield 'key1' => 'value1'; + }; + + $this->cache->setMultiple($gen()); + $this->assertEquals('value0', $this->cache->get('key0')); + $this->assertEquals('value1', $this->cache->get('key1')); + } + + public function testGetMultiple() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $result = $this->cache->getMultiple(['key0', 'key1']); + $keys = []; + foreach ($result as $i => $r) { + $keys[] = $i; + $this->assertNull($r); + } + sort($keys); + $this->assertSame(['key0', 'key1'], $keys); + + $this->cache->set('key3', 'value'); + $result = $this->cache->getMultiple(['key2', 'key3', 'key4'], 'foo'); + $keys = []; + foreach ($result as $key => $r) { + $keys[] = $key; + if ($key === 'key3') { + $this->assertEquals('value', $r); + } else { + $this->assertEquals('foo', $r); + } + } + sort($keys); + $this->assertSame(['key2', 'key3', 'key4'], $keys); + } + + public function testGetMultipleWithGenerator() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $gen = function () { + yield 1 => 'key0'; + yield 1 => 'key1'; + }; + + $this->cache->set('key0', 'value0'); + $result = $this->cache->getMultiple($gen()); + $keys = []; + foreach ($result as $key => $r) { + $keys[] = $key; + if ($key === 'key0') { + $this->assertEquals('value0', $r); + } elseif ($key === 'key1') { + $this->assertNull($r); + } else { + $this->assertFalse(true, 'This should not happend'); + } + } + sort($keys); + $this->assertSame(['key0', 'key1'], $keys); + $this->assertEquals('value0', $this->cache->get('key0')); + $this->assertNull($this->cache->get('key1')); + } + + public function testDeleteMultiple() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertTrue($this->cache->deleteMultiple([]), 'Deleting a empty array should return true'); + $this->assertTrue($this->cache->deleteMultiple(['key']), 'Deleting a value that does not exist should return true'); + + $this->cache->set('key0', 'value0'); + $this->cache->set('key1', 'value1'); + $this->assertTrue($this->cache->deleteMultiple(['key0', 'key1']), 'Delete must return true on success'); + $this->assertNull($this->cache->get('key0'), 'Values must be deleted on deleteMultiple()'); + $this->assertNull($this->cache->get('key1'), 'Values must be deleted on deleteMultiple()'); + } + + public function testDeleteMultipleGenerator() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $gen = function () { + yield 1 => 'key0'; + yield 1 => 'key1'; + }; + $this->cache->set('key0', 'value0'); + $this->assertTrue($this->cache->deleteMultiple($gen()), 'Deleting a generator should return true'); + + $this->assertNull($this->cache->get('key0'), 'Values must be deleted on deleteMultiple()'); + $this->assertNull($this->cache->get('key1'), 'Values must be deleted on deleteMultiple()'); + } + + public function testHas() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertFalse($this->cache->has('key0')); + $this->cache->set('key0', 'value0'); + $this->assertTrue($this->cache->has('key0')); + } + + public function testBasicUsageWithLongKey() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $key = str_repeat('a', 300); + + $this->assertFalse($this->cache->has($key)); + $this->assertTrue($this->cache->set($key, 'value')); + + $this->assertTrue($this->cache->has($key)); + $this->assertSame('value', $this->cache->get($key)); + + $this->assertTrue($this->cache->delete($key)); + + $this->assertFalse($this->cache->has($key)); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidKeys + */ + public function testGetInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->get($key); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidKeys + */ + public function testGetMultipleInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $result = $this->cache->getMultiple(['key1', $key, 'key2']); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + */ + public function testGetMultipleNoIterable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $result = $this->cache->getMultiple('key'); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidKeys + */ + public function testSetInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set($key, 'foobar'); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidKeys + */ + public function testSetMultipleInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + if (is_int($key)) { + $this->markTestSkipped('As keys, strings are always casted to ints so they should be accepted'); + } + + $values = function () use ($key) { + yield 'key1' => 'foo'; + yield $key => 'bar'; + yield 'key2' => 'baz'; + }; + $this->cache->setMultiple($values()); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + */ + public function testSetMultipleNoIterable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->setMultiple('key'); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidKeys + */ + public function testHasInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->has($key); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidKeys + */ + public function testDeleteInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->delete($key); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidKeys + */ + public function testDeleteMultipleInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->deleteMultiple(['key1', $key, 'key2']); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + */ + public function testDeleteMultipleNoIterable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->deleteMultiple('key'); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidTtl + */ + public function testSetInvalidTtl($ttl) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set('key', 'value', $ttl); + } + + /** + * @expectedException \Psr\SimpleCache\InvalidArgumentException + * @dataProvider invalidTtl + */ + public function testSetMultipleInvalidTtl($ttl) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->setMultiple(['key' => 'value'], $ttl); + } + + public function testNullOverwrite() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set('key', 5); + $this->cache->set('key', null); + + $this->assertNull($this->cache->get('key'), 'Setting null to a key must overwrite previous value'); + } + + public function testDataTypeString() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set('key', '5'); + $result = $this->cache->get('key'); + $this->assertTrue('5' === $result, 'Wrong data type. If we store a string we must get an string back.'); + $this->assertTrue(is_string($result), 'Wrong data type. If we store a string we must get an string back.'); + } + + public function testDataTypeInteger() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set('key', 5); + $result = $this->cache->get('key'); + $this->assertTrue(5 === $result, 'Wrong data type. If we store an int we must get an int back.'); + $this->assertTrue(is_int($result), 'Wrong data type. If we store an int we must get an int back.'); + } + + public function testDataTypeFloat() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $float = 1.23456789; + $this->cache->set('key', $float); + $result = $this->cache->get('key'); + $this->assertTrue(is_float($result), 'Wrong data type. If we store float we must get an float back.'); + $this->assertEquals($float, $result); + } + + public function testDataTypeBoolean() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set('key', false); + $result = $this->cache->get('key'); + $this->assertTrue(is_bool($result), 'Wrong data type. If we store boolean we must get an boolean back.'); + $this->assertFalse($result); + $this->assertTrue($this->cache->has('key'), 'has() should return true when true are stored. '); + } + + public function testDataTypeArray() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $array = ['a' => 'foo', 2 => 'bar']; + $this->cache->set('key', $array); + $result = $this->cache->get('key'); + $this->assertTrue(is_array($result), 'Wrong data type. If we store array we must get an array back.'); + $this->assertEquals($array, $result); + } + + public function testDataTypeObject() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $object = new \stdClass(); + $object->a = 'foo'; + $this->cache->set('key', $object); + $result = $this->cache->get('key'); + $this->assertTrue(is_object($result), 'Wrong data type. If we store object we must get an object back.'); + $this->assertEquals($object, $result); + } + + public function testBinaryData() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $data = ''; + for ($i = 0; $i < 256; $i++) { + $data .= chr($i); + } + + $array = ['a' => 'foo', 2 => 'bar']; + $this->cache->set('key', $data); + $result = $this->cache->get('key'); + $this->assertTrue($data === $result, 'Binary data must survive a round trip.'); + } + + /** + * @dataProvider validKeys + */ + public function testSetValidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set($key, 'foobar'); + $this->assertEquals('foobar', $this->cache->get($key)); + } + + /** + * @dataProvider validKeys + */ + public function testSetMultipleValidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->setMultiple([$key => 'foobar']); + $result = $this->cache->getMultiple([$key]); + $keys = []; + foreach ($result as $i => $r) { + $keys[] = $i; + $this->assertEquals($key, $i); + $this->assertEquals('foobar', $r); + } + $this->assertSame([$key], $keys); + } + + /** + * @dataProvider validData + */ + public function testSetValidData($data) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->set('key', $data); + $this->assertEquals($data, $this->cache->get('key')); + } + + /** + * @dataProvider validData + */ + public function testSetMultipleValidData($data) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->cache->setMultiple(['key' => $data]); + $result = $this->cache->getMultiple(['key']); + $keys = []; + foreach ($result as $i => $r) { + $keys[] = $i; + $this->assertEquals($data, $r); + } + $this->assertSame(['key'], $keys); + } + + public function testObjectAsDefaultValue() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $obj = new \stdClass(); + $obj->foo = 'value'; + $this->assertEquals($obj, $this->cache->get('key', $obj)); + } + + public function testObjectDoesNotChangeInCache() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $obj = new \stdClass(); + $obj->foo = 'value'; + $this->cache->set('key', $obj); + $obj->foo = 'changed'; + + $cacheObject = $this->cache->get('key'); + $this->assertEquals('value', $cacheObject->foo, 'Object in cache should not have their values changed.'); + } +} diff --git a/civicrm/packages/Cache/IntegrationTests/README.md b/civicrm/packages/Cache/IntegrationTests/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8b6b87a345c8070a494391d13fce732f6c55029c --- /dev/null +++ b/civicrm/packages/Cache/IntegrationTests/README.md @@ -0,0 +1,7 @@ +This is a fork of the unit-test from https://github.com/php-cache/integration-tests/ which provides support for older +versions of PHPUnit. It merely: + +* Changes the base-class to `PHPUnit_Framework_TestCase`. +* Changes the name to avoid collsions (`Cache\IntegrationTests\LegacySimpleCacheTest`). + +This class is only used for testing -- it is not required at runtime. diff --git a/civicrm/packages/DB.php b/civicrm/packages/DB.php index 2a06764f9578e932177765dd7effbfe34e35e311..30dd48b38089d52b803ef847229f9cbb73da03d7 100644 --- a/civicrm/packages/DB.php +++ b/civicrm/packages/DB.php @@ -790,7 +790,7 @@ class DB $parsed['dbsyntax'] = $str; } - if (!count($dsn)) { + if (empty($dsn)) { return $parsed; } diff --git a/civicrm/packages/DB/common.php b/civicrm/packages/DB/common.php index afa8359ba9bb5b341cf82cee9316312ec2a867f2..ee50c0880d71b0608a392cc375275b105cf4587f 100644 --- a/civicrm/packages/DB/common.php +++ b/civicrm/packages/DB/common.php @@ -1342,7 +1342,8 @@ class DB_common extends PEAR } } // modifyLimitQuery() would be nice here, but it causes BC issues - if (sizeof($params) > 0) { + $params = (array) $params; + if (count($params) > 0) { $sth = $this->prepare($query); if (DB::isError($sth)) { return $sth; @@ -1650,7 +1651,8 @@ class DB_common extends PEAR } } - if (sizeof($params) > 0) { + $params = (array) $params; + if (count($params) > 0) { $sth = $this->prepare($query); if (DB::isError($sth)) { diff --git a/civicrm/packages/HTML/QuickForm/date.php b/civicrm/packages/HTML/QuickForm/date.php index 040c800f374c6069c9a1eb91d1e6dc65b79a8394..9e816e680dd364943deb51fa49552247358a4740 100644 --- a/civicrm/packages/HTML/QuickForm/date.php +++ b/civicrm/packages/HTML/QuickForm/date.php @@ -159,40 +159,48 @@ class HTML_QuickForm_date extends HTML_QuickForm_group $separator .= $sign; } else { $loadSelect = true; + $ariaLabel = $this->getLabel() ?: ''; switch ($sign) { case 'D': // Sunday is 0 like with 'w' in date() $options = $locale['weekdays_short']; $emptyText = ts('-day of week-'); + $ariaLabel .= ts(' day of week'); break; case 'l': $options = $locale['weekdays_long']; $emptyText = ts('-day of week-'); + $ariaLabel .= ts(' day of week'); break; case 'd': $options = $this->_createOptionList(1, 31); $emptyText = ts('-day-'); + $ariaLabel .= ts(' day'); break; case 'j': // the no-zero-padding option (CRM-2793) $options = $this->_createOptionList(1, 31, 1, false); $emptyText = ts('-day-'); + $ariaLabel .= ts(' day'); break; case 'M': $options = $locale['months_short']; array_unshift($options , ''); unset($options[0]); $emptyText = ts('-month-'); + $ariaLabel .= ts(' month'); break; case 'm': $options = $this->_createOptionList(1, 12); $emptyText = ts('-month-'); + $ariaLabel .= ts(' month'); break; case 'F': $options = $locale['months_long']; array_unshift($options , ''); unset($options[0]); $emptyText = ts('-month-'); + $ariaLabel .= ts(' month'); break; case 'Y': $options = $this->_createOptionList( @@ -201,6 +209,7 @@ class HTML_QuickForm_date extends HTML_QuickForm_group $this->_options['minYear'] > $this->_options['maxYear']? -1: 1 ); $emptyText = ts('-year-'); + $ariaLabel .= ts(' year'); break; case 'y': $options = $this->_createOptionList( @@ -210,10 +219,12 @@ class HTML_QuickForm_date extends HTML_QuickForm_group ); array_walk($options, create_function('&$v,$k','$v = substr($v,-2);')); $emptyText = ts('-year-'); + $ariaLabel .= ts(' year'); break; case 'h': $options = $this->_createOptionList(1, 12); $emptyText = ts('-hour-'); + $ariaLabel .= ts(' hour'); break; case 'g': $options = $this->_createOptionList(1, 12); @@ -222,22 +233,27 @@ class HTML_QuickForm_date extends HTML_QuickForm_group case 'H': $options = $this->_createOptionList(0, 23); $emptyText = ts('-hour-'); + $ariaLabel .= ts(' hour'); break; case 'i': $options = $this->_createOptionList(0, 59, $this->_options['optionIncrement']['i']); $emptyText = ts('-min-'); + $ariaLabel .= ts(' minute'); break; case 's': $options = $this->_createOptionList(0, 59, $this->_options['optionIncrement']['s']); $emptyText = ts('-sec-'); + $ariaLabel .= ts(' second'); break; case 'a': $options = array('am' => 'am', 'pm' => 'pm'); $emptyText = '-am/pm-'; + $ariaLabel .= ts(' am or pm'); break; case 'A': $options = array('AM' => 'AM', 'PM' => 'PM'); $emptyText = '-AM/PM-'; + $ariaLabel .= ts(' AM or PM'); break; case 'W': $options = $this->_createOptionList(1, 53); @@ -276,6 +292,9 @@ class HTML_QuickForm_date extends HTML_QuickForm_group $attribs = $this->getAttributes(); $elementName = $this->getName(); $attribs['id'] = $elementName.'['.$sign.']'; + if ($ariaLabel !== '') { + $attribs['aria-label'] = $ariaLabel; + } $this->_elements[] = new HTML_QuickForm_select($sign, null, $options, $attribs); } diff --git a/civicrm/packages/Mail/mime.php b/civicrm/packages/Mail/mime.php index ca815231c59e08844d37dfefdd775012be86427b..8ed49b1087870b8e0e468a28c17ef492a61a25cc 100644 --- a/civicrm/packages/Mail/mime.php +++ b/civicrm/packages/Mail/mime.php @@ -1,4 +1,5 @@ <?php + /** * The Mail_Mime class is used to create MIME E-mail messages * @@ -7,7 +8,7 @@ * contain plain-text bodies, HTML bodies, attachments, inline * images and specific headers. * - * Compatible with PHP versions 4 and 5 + * Compatible with PHP version 5 and 7 * * LICENSE: This LICENSE is in the BSD license style. * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> @@ -23,8 +24,8 @@ * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. - * - Neither the name of the authors, nor the names of its contributors - * may be used to endorse or promote products derived from this + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" @@ -48,7 +49,7 @@ * @author Aleksander Machniak <alec@php.net> * @copyright 2003-2006 PEAR <pear-group@php.net> * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version CVS: $Id: mime.php 300942 2010-07-02 12:27:56Z alec $ + * @version Release: @package_version@ * @link http://pear.php.net/package/Mail_mime * * This class is based on HTML Mime Mail class from @@ -58,20 +59,7 @@ */ -/** - * require PEAR - * - * This package depends on PEAR to raise errors. - */ require_once 'PEAR.php'; - -/** - * require Mail_mimePart - * - * Mail_mimePart contains the code required to - * create all the different parts a mail can - * consist of. - */ require_once 'Mail/mimePart.php'; @@ -98,49 +86,50 @@ class Mail_mime * Contains the plain text part of the email * * @var string - * @access private */ - var $_txtbody; + protected $txtbody; /** * Contains the html part of the email * * @var string - * @access private */ - var $_htmlbody; + protected $htmlbody; /** - * list of the attached images + * Contains the text/calendar part of the email + * + * @var string + */ + protected $calbody; + + /** + * List of the attached images * * @var array - * @access private */ - var $_html_images = array(); + protected $html_images = array(); /** - * list of the attachements + * List of the attachements * * @var array - * @access private */ - var $_parts = array(); + protected $parts = array(); /** * Headers for the mail * * @var array - * @access private */ - var $_headers = array(); + protected $headers = array(); /** * Build parameters * * @var array - * @access private */ - var $_build_params = array( + protected $build_params = array( // What encoding to use for the headers // Options: quoted-printable or base64 'head_encoding' => 'quoted-printable', @@ -150,18 +139,28 @@ class Mail_mime // What encoding to use for html // Options: 7bit, 8bit, base64, or quoted-printable 'html_encoding' => 'quoted-printable', + // What encoding to use for calendar part + // Options: 7bit, 8bit, base64, or quoted-printable + 'calendar_encoding' => 'quoted-printable', // The character set to use for html 'html_charset' => 'ISO-8859-1', // The character set to use for text 'text_charset' => 'ISO-8859-1', + // The character set to use for calendar part + 'calendar_charset' => 'UTF-8', // The character set to use for headers 'head_charset' => 'ISO-8859-1', // End-of-line sequence 'eol' => "\r\n", // Delay attachment files IO until building the message - 'delay_file_io' => false + 'delay_file_io' => false, + // Default calendar method + 'calendar_method' => 'request', + // multipart part preamble (RFC2046 5.1.1) + 'preamble' => '', ); + /** * Constructor function * @@ -170,22 +169,19 @@ class Mail_mime * See $_build_params. * * @return void - * @access public */ - function __construct($params = array()) + public function __construct($params = array()) { // Backward-compatible EOL setting if (is_string($params)) { - $this->_build_params['eol'] = $params; + $this->build_params['eol'] = $params; } else if (defined('MAIL_MIME_CRLF') && !isset($params['eol'])) { - $this->_build_params['eol'] = MAIL_MIME_CRLF; + $this->build_params['eol'] = MAIL_MIME_CRLF; } // Update build parameters if (!empty($params) && is_array($params)) { - while (list($key, $value) = each($params)) { - $this->_build_params[$key] = $value; - } + $this->build_params = array_merge($this->build_params, $params); } } @@ -196,12 +192,11 @@ class Mail_mime * @param string $value Parameter value * * @return void - * @access public - * @since 1.6.0 + * @since 1.6.0 */ - function setParam($name, $value) + public function setParam($name, $value) { - $this->_build_params[$name] = $value; + $this->build_params[$name] = $value; } /** @@ -210,12 +205,11 @@ class Mail_mime * @param string $name Parameter name * * @return mixed Parameter value - * @access public - * @since 1.6.0 + * @since 1.6.0 */ - function getParam($name) + public function getParam($name) { - return isset($this->_build_params[$name]) ? $this->_build_params[$name] : null; + return isset($this->build_params[$name]) ? $this->build_params[$name] : null; } /** @@ -224,113 +218,116 @@ class Mail_mime * text/plain part that emails clients who don't support * html should show. * - * @param string $data Either a string or - * the file name with the contents + * @param string $data Either a string or the file name with the contents * @param bool $isfile If true the first param should be treated * as a file name, else as a string (default) * @param bool $append If true the text or file is appended to * the existing body, else the old body is * overwritten * - * @return mixed True on success or PEAR_Error object - * @access public + * @return mixed True on success or PEAR_Error object */ - function setTXTBody($data, $isfile = false, $append = false) + public function setTXTBody($data, $isfile = false, $append = false) { - if (!$isfile) { - if (!$append) { - $this->_txtbody = $data; - } else { - $this->_txtbody .= $data; - } - } else { - $cont = $this->_file2str($data); - if (PEAR::isError($cont)) { - return $cont; - } - if (!$append) { - $this->_txtbody = $cont; - } else { - $this->_txtbody .= $cont; - } - } - - // wordwrap the txtbody to be 750 characters to comply with RFC 2821 - // CRM-3133 - $this->_txtbody = wordwrap( $this->_txtbody, 750 ); - return true; + return $this->setBody('txtbody', $data, $isfile, $append); } /** * Get message text body * * @return string Text body - * @access public - * @since 1.6.0 + * @since 1.6.0 */ - function getTXTBody() + public function getTXTBody() { - return $this->_txtbody; + return $this->txtbody; } /** * Adds a html part to the mail. * - * @param string $data Either a string or the file name with the - * contents + * @param string $data Either a string or the file name with the contents * @param bool $isfile A flag that determines whether $data is a * filename, or a string(false, default) * - * @return bool True on success - * @access public + * @return bool True on success or PEAR_Error object */ - function setHTMLBody($data, $isfile = false) + public function setHTMLBody($data, $isfile = false) { - if (!$isfile) { - $this->_htmlbody = $data; - } else { - $cont = $this->_file2str($data); - if (PEAR::isError($cont)) { - return $cont; - } - $this->_htmlbody = $cont; - } - - // wordwrap the htmlbody to be 750 characters to comply with RFC 2821 - // CRM-3133 - $this->_htmlbody = wordwrap( $this->_htmlbody, 750 ); - return true; + return $this->setBody('htmlbody', $data, $isfile); } /** * Get message HTML body * * @return string HTML body - * @access public - * @since 1.6.0 + * @since 1.6.0 + */ + public function getHTMLBody() + { + return $this->htmlbody; + } + + /** + * Function to set a body of text/calendar part (not attachment) + * + * @param string $data Either a string or the file name with the contents + * @param bool $isfile If true the first param should be treated + * as a file name, else as a string (default) + * @param bool $append If true the text or file is appended to + * the existing body, else the old body is + * overwritten + * @param string $method iCalendar object method + * @param string $charset iCalendar character set + * @param string $encoding Transfer encoding + * + * @return mixed True on success or PEAR_Error object + * @since 1.9.0 + */ + public function setCalendarBody($data, $isfile = false, $append = false, + $method = 'request', $charset = 'UTF-8', $encoding = 'quoted-printable' + ) { + $result = $this->setBody('calbody', $data, $isfile, $append); + + if ($result === true) { + $this->build_params['calendar_method'] = $method; + $this->build_params['calendar_charset'] = $charset; + $this->build_params['calendar_encoding'] = $encoding; + } + } + + /** + * Get body of calendar part + * + * @return string Calendar part body + * @since 1.9.0 */ - function getHTMLBody() + public function getCalendarBody() { - return $this->_htmlbody; + return $this->calbody; } /** * Adds an image to the list of embedded images. + * Images added this way will be added as related parts of the HTML message. + * + * To correctly match the HTML image with the related attachment + * HTML should refer to it by a filename (specified in $file or $name + * arguments) or by cid:<content-id> (specified in $content_id arg). * * @param string $file The image file name OR image data itself * @param string $c_type The content type - * @param string $name The filename of the image. - * Only used if $file is the image data. + * @param string $name The filename of the image. Used to find + * the image in HTML content. * @param bool $isfile Whether $file is a filename or not. * Defaults to true * @param string $content_id Desired Content-ID of MIME part * Defaults to generated unique ID * - * @return bool True on success - * @access public + * @return bool True on success */ - function addHTMLImage($file, - $c_type='application/octet-stream', + public function addHTMLImage($file, + $c_type = 'application/octet-stream', $name = '', $isfile = true, $content_id = null @@ -339,25 +336,26 @@ class Mail_mime if ($isfile) { // Don't load file into memory - if ($this->_build_params['delay_file_io']) { + if ($this->build_params['delay_file_io']) { $filedata = null; $bodyfile = $file; } else { - if (PEAR::isError($filedata = $this->_file2str($file))) { + if (self::isError($filedata = $this->file2str($file))) { return $filedata; } } - $filename = ($name ? $name : $file); + + $filename = $name ? $name : $file; } else { $filedata = $file; $filename = $name; } if (!$content_id) { - $content_id = md5(uniqid(time())); + $content_id = preg_replace('/[^0-9a-zA-Z]/', '', uniqid(time(), true)); } - $this->_html_images[] = array( + $this->html_images[] = array( 'body' => $filedata, 'body_file' => $bodyfile, 'name' => $filename, @@ -371,36 +369,35 @@ class Mail_mime /** * Adds a file to the list of attachments. * - * @param string $file The file name of the file to attach - * OR the file contents itself + * @param mixed $file The file name or the file contents itself, + * it can be also Mail_mimePart object * @param string $c_type The content type * @param string $name The filename of the attachment * Only use if $file is the contents - * @param bool $isfile Whether $file is a filename or not - * Defaults to true - * @param string $encoding The type of encoding to use. - * Defaults to base64. - * Possible values: 7bit, 8bit, base64, - * or quoted-printable. + * @param bool $isfile Whether $file is a filename or not. Defaults to true + * @param string $encoding The type of encoding to use. Defaults to base64. + * Possible values: 7bit, 8bit, base64 or quoted-printable. * @param string $disposition The content-disposition of this file * Defaults to attachment. * Possible values: attachment, inline. - * @param string $charset The character set used in the filename - * of this attachment. + * @param string $charset The character set of attachment's content. * @param string $language The language of the attachment * @param string $location The RFC 2557.4 location of the attachment - * @param string $n_encoding Encoding for attachment name (Content-Type) + * @param string $n_encoding Encoding of the attachment's name in Content-Type * By default filenames are encoded using RFC2231 method * Here you can set RFC2047 encoding (quoted-printable * or base64) instead - * @param string $f_encoding Encoding for attachment filename (Content-Disposition) - * See $n_encoding description + * @param string $f_encoding Encoding of the attachment's filename + * in Content-Disposition header. * @param string $description Content-Description header + * @param string $h_charset The character set of the headers e.g. filename + * If not specified, $charset will be used + * @param array $add_headers Additional part headers. Array keys can be in form + * of <header_name>:<parameter_name> * - * @return mixed True on success or PEAR_Error object - * @access public + * @return mixed True on success or PEAR_Error object */ - function addAttachment($file, + public function addAttachment($file, $c_type = 'application/octet-stream', $name = '', $isfile = true, @@ -411,22 +408,29 @@ class Mail_mime $location = '', $n_encoding = null, $f_encoding = null, - $description = '' + $description = '', + $h_charset = null, + $add_headers = array() ) { + if ($file instanceof Mail_mimePart) { + $this->parts[] = $file; + return true; + } + $bodyfile = null; if ($isfile) { // Don't load file into memory - if ($this->_build_params['delay_file_io']) { + if ($this->build_params['delay_file_io']) { $filedata = null; $bodyfile = $file; } else { - if (PEAR::isError($filedata = $this->_file2str($file))) { + if (self::isError($filedata = $this->file2str($file))) { return $filedata; } } // Force the name the user supplied, otherwise use $file - $filename = ($name ? $name : $file); + $filename = ($name ? $name : $this->basename($file)); } else { $filedata = $file; $filename = $name; @@ -434,58 +438,68 @@ class Mail_mime if (!strlen($filename)) { $msg = "The supplied filename for the attachment can't be empty"; - $err = PEAR::raiseError($msg); - return $err; + return self::raiseError($msg); } - $filename = $this->_basename($filename); - $this->_parts[] = array( + $this->parts[] = array( 'body' => $filedata, 'body_file' => $bodyfile, 'name' => $filename, 'c_type' => $c_type, - 'encoding' => $encoding, 'charset' => $charset, + 'encoding' => $encoding, 'language' => $language, 'location' => $location, 'disposition' => $disposition, 'description' => $description, + 'add_headers' => $add_headers, 'name_encoding' => $n_encoding, - 'filename_encoding' => $f_encoding + 'filename_encoding' => $f_encoding, + 'headers_charset' => $h_charset, ); return true; } + /** + * Checks if the current message has many parts + * + * @return bool True if the message has many parts, False otherwise. + * @since 1.9.0 + */ + public function isMultipart() + { + return count($this->parts) > 0 || count($this->html_images) > 0 + || (strlen($this->htmlbody) > 0 && strlen($this->txtbody) > 0); + } + /** * Get the contents of the given file name as string * * @param string $file_name Path of file to process * - * @return string Contents of $file_name - * @access private + * @return string Contents of $file_name */ - function &_file2str($file_name) + protected function file2str($file_name) { // Check state of file and raise an error properly if (!file_exists($file_name)) { - $err = PEAR::raiseError('File not found: ' . $file_name); - return $err; + return self::raiseError('File not found: ' . $file_name); } if (!is_file($file_name)) { - $err = PEAR::raiseError('Not a regular file: ' . $file_name); - return $err; + return self::raiseError('Not a regular file: ' . $file_name); } if (!is_readable($file_name)) { - $err = PEAR::raiseError('File is not readable: ' . $file_name); - return $err; + return self::raiseError('File is not readable: ' . $file_name); } // Temporarily reset magic_quotes_runtime and read file contents if ($magic_quote_setting = get_magic_quotes_runtime()) { @ini_set('magic_quotes_runtime', 0); } + $cont = file_get_contents($file_name); + if ($magic_quote_setting) { @ini_set('magic_quotes_runtime', $magic_quote_setting); } @@ -497,53 +511,44 @@ class Mail_mime * Adds a text subpart to the mimePart object and * returns it during the build process. * - * @param mixed &$obj The object to add the part to, or - * null if a new object is to be created. - * @param string $text The text to add. + * @param mixed $obj The object to add the part to, or + * anything else if a new object is to be created. * - * @return object The text mimePart object - * @access private + * @return object The text mimePart object */ - function &_addTextPart(&$obj, $text) + protected function addTextPart($obj = null) { - $params['content_type'] = 'text/plain'; - $params['encoding'] = $this->_build_params['text_encoding']; - $params['charset'] = $this->_build_params['text_charset']; - $params['eol'] = $this->_build_params['eol']; - - if (is_object($obj)) { - $ret = $obj->addSubpart($text, $params); - return $ret; - } else { - $ret = new Mail_mimePart($text, $params); - return $ret; - } + return $this->addBodyPart($obj, $this->txtbody, 'text/plain', 'text'); } /** * Adds a html subpart to the mimePart object and * returns it during the build process. * - * @param mixed &$obj The object to add the part to, or - * null if a new object is to be created. + * @param mixed $obj The object to add the part to, or + * anything else if a new object is to be created. * - * @return object The html mimePart object - * @access private + * @return object The html mimePart object */ - function &_addHtmlPart(&$obj) + protected function addHtmlPart($obj = null) { - $params['content_type'] = 'text/html'; - $params['encoding'] = $this->_build_params['html_encoding']; - $params['charset'] = $this->_build_params['html_charset']; - $params['eol'] = $this->_build_params['eol']; + return $this->addBodyPart($obj, $this->htmlbody, 'text/html', 'html'); + } - if (is_object($obj)) { - $ret = $obj->addSubpart($this->_htmlbody, $params); - return $ret; - } else { - $ret = new Mail_mimePart($this->_htmlbody, $params); - return $ret; - } + /** + * Adds a calendar subpart to the mimePart object and + * returns it during the build process. + * + * @param mixed $obj The object to add the part to, or + * anything else if a new object is to be created. + * + * @return object The text mimePart object + */ + protected function addCalendarPart($obj = null) + { + $ctype = 'text/calendar; method='. $this->build_params['calendar_method']; + + return $this->addBodyPart($obj, $this->calbody, $ctype, 'calendar'); } /** @@ -551,18 +556,17 @@ class Mail_mime * the initial content-type and returns it during the * build process. * + * @param array $params Additional part parameters + * * @return object The multipart/mixed mimePart object - * @access private */ - function &_addMixedPart() + protected function addMixedPart($params = array()) { - $params = array(); $params['content_type'] = 'multipart/mixed'; - $params['eol'] = $this->_build_params['eol']; + $params['eol'] = $this->build_params['eol']; // Create empty multipart/mixed Mail_mimePart object to return - $ret = new Mail_mimePart('', $params); - return $ret; + return new Mail_mimePart('', $params); } /** @@ -570,23 +574,23 @@ class Mail_mime * object (or creates one), and returns it during * the build process. * - * @param mixed &$obj The object to add the part to, or - * null if a new object is to be created. + * @param mixed $obj The object to add the part to, or + * anything else if a new object is to be created. * - * @return object The multipart/mixed mimePart object - * @access private + * @return object The multipart/mixed mimePart object */ - function &_addAlternativePart(&$obj) + protected function addAlternativePart($obj = null) { $params['content_type'] = 'multipart/alternative'; - $params['eol'] = $this->_build_params['eol']; + $params['eol'] = $this->build_params['eol']; if (is_object($obj)) { - return $obj->addSubpart('', $params); + $ret = $obj->addSubpart('', $params); } else { $ret = new Mail_mimePart('', $params); - return $ret; } + + return $ret; } /** @@ -594,44 +598,43 @@ class Mail_mime * object (or creates one), and returns it during * the build process. * - * @param mixed &$obj The object to add the part to, or - * null if a new object is to be created + * @param mixed $obj The object to add the part to, or + * anything else if a new object is to be created * - * @return object The multipart/mixed mimePart object - * @access private + * @return object The multipart/mixed mimePart object */ - function &_addRelatedPart(&$obj) + protected function addRelatedPart($obj = null) { $params['content_type'] = 'multipart/related'; - $params['eol'] = $this->_build_params['eol']; + $params['eol'] = $this->build_params['eol']; if (is_object($obj)) { - return $obj->addSubpart('', $params); + $ret = $obj->addSubpart('', $params); } else { $ret = new Mail_mimePart('', $params); - return $ret; } + + return $ret; } /** * Adds an html image subpart to a mimePart object * and returns it during the build process. * - * @param object &$obj The mimePart to add the image to + * @param object $obj The mimePart to add the image to * @param array $value The image information * - * @return object The image mimePart object - * @access private + * @return object The image mimePart object */ - function &_addHtmlImagePart(&$obj, $value) + protected function addHtmlImagePart($obj, $value) { $params['content_type'] = $value['c_type']; $params['encoding'] = 'base64'; $params['disposition'] = 'inline'; - $params['dfilename'] = $value['name']; + $params['filename'] = $value['name']; $params['cid'] = $value['cid']; $params['body_file'] = $value['body_file']; - $params['eol'] = $this->_build_params['eol']; + $params['eol'] = $this->build_params['eol']; if (!empty($value['name_encoding'])) { $params['name_encoding'] = $value['name_encoding']; @@ -640,36 +643,44 @@ class Mail_mime $params['filename_encoding'] = $value['filename_encoding']; } - $ret = $obj->addSubpart($value['body'], $params); - return $ret; + return $obj->addSubpart($value['body'], $params); } /** * Adds an attachment subpart to a mimePart object * and returns it during the build process. * - * @param object &$obj The mimePart to add the image to - * @param array $value The attachment information + * @param object $obj The mimePart to add the image to + * @param mixed $value The attachment information array or Mail_mimePart object * - * @return object The image mimePart object - * @access private + * @return object The image mimePart object */ - function &_addAttachmentPart(&$obj, $value) + protected function addAttachmentPart($obj, $value) { - $params['eol'] = $this->_build_params['eol']; - $params['dfilename'] = $value['name']; + if ($value instanceof Mail_mimePart) { + return $obj->addSubpart($value); + } + + $params['eol'] = $this->build_params['eol']; + $params['filename'] = $value['name']; $params['encoding'] = $value['encoding']; $params['content_type'] = $value['c_type']; $params['body_file'] = $value['body_file']; - $params['disposition'] = isset($value['disposition']) ? + $params['disposition'] = isset($value['disposition']) ? $value['disposition'] : 'attachment'; - if ($value['charset']) { + + // content charset + if (!empty($value['charset'])) { $params['charset'] = $value['charset']; } - if ($value['language']) { + // headers charset (filename, description) + if (!empty($value['headers_charset'])) { + $params['headers_charset'] = $value['headers_charset']; + } + if (!empty($value['language'])) { $params['language'] = $value['language']; } - if ($value['location']) { + if (!empty($value['location'])) { $params['location'] = $value['location']; } if (!empty($value['name_encoding'])) { @@ -681,84 +692,80 @@ class Mail_mime if (!empty($value['description'])) { $params['description'] = $value['description']; } + if (is_array($value['add_headers'])) { + $params['headers'] = $value['add_headers']; + } - $ret = $obj->addSubpart($value['body'], $params); - return $ret; + return $obj->addSubpart($value['body'], $params); } /** * Returns the complete e-mail, ready to send using an alternative * mail delivery method. Note that only the mailpart that is made * with Mail_Mime is created. This means that, - * YOU WILL HAVE NO TO: HEADERS UNLESS YOU SET IT YOURSELF + * YOU WILL HAVE NO TO: HEADERS UNLESS YOU SET IT YOURSELF * using the $headers parameter! - * + * * @param string $separation The separation between these two parts. * @param array $params The Build parameters passed to the - * &get() function. See &get for more info. + * get() function. See get() for more info. * @param array $headers The extra headers that should be passed - * to the &headers() function. + * to the headers() method. * See that function for more info. * @param bool $overwrite Overwrite the existing headers with new. * * @return mixed The complete e-mail or PEAR error object - * @access public */ - function getMessage($separation = null, $params = null, $headers = null, + public function getMessage($separation = null, $params = null, $headers = null, $overwrite = false ) { if ($separation === null) { - $separation = $this->_build_params['eol']; + $separation = $this->build_params['eol']; } $body = $this->get($params); - if (PEAR::isError($body)) { + if (self::isError($body)) { return $body; } - $head = $this->txtHeaders($headers, $overwrite); - $mail = $head . $separation . $body; - return $mail; + return $this->txtHeaders($headers, $overwrite) . $separation . $body; } /** * Returns the complete e-mail body, ready to send using an alternative * mail delivery method. - * + * * @param array $params The Build parameters passed to the - * &get() function. See &get for more info. + * get() method. See get() for more info. * * @return mixed The e-mail body or PEAR error object - * @access public - * @since 1.6.0 + * @since 1.6.0 */ - function getMessageBody($params = null) + public function getMessageBody($params = null) { return $this->get($params, null, true); } /** * Writes (appends) the complete e-mail into file. - * + * * @param string $filename Output file location * @param array $params The Build parameters passed to the - * &get() function. See &get for more info. + * get() method. See get() for more info. * @param array $headers The extra headers that should be passed - * to the &headers() function. + * to the headers() function. * See that function for more info. * @param bool $overwrite Overwrite the existing headers with new. * * @return mixed True or PEAR error object - * @access public - * @since 1.6.0 + * @since 1.6.0 */ - function saveMessage($filename, $params = null, $headers = null, $overwrite = false) + public function saveMessage($filename, $params = null, $headers = null, $overwrite = false) { // Check state of file and raise an error properly if (file_exists($filename) && !is_writable($filename)) { - $err = PEAR::raiseError('File is not writable: ' . $filename); - return $err; + return self::raiseError('File is not writable: ' . $filename); } // Temporarily reset magic_quotes_runtime and read file contents @@ -767,15 +774,13 @@ class Mail_mime } if (!($fh = fopen($filename, 'ab'))) { - $err = PEAR::raiseError('Unable to open file: ' . $filename); - return $err; + return self::raiseError('Unable to open file: ' . $filename); } // Write message headers into file (skipping Content-* headers) $head = $this->txtHeaders($headers, $overwrite, true); if (fwrite($fh, $head) === false) { - $err = PEAR::raiseError('Error writing to file: ' . $filename); - return $err; + return self::raiseError('Error writing to file: ' . $filename); } fclose($fh); @@ -791,22 +796,29 @@ class Mail_mime } /** - * Writes (appends) the complete e-mail body into file. - * - * @param string $filename Output file location - * @param array $params The Build parameters passed to the - * &get() function. See &get for more info. + * Writes (appends) the complete e-mail body into file or stream. + * + * @param mixed $filename Output filename or file pointer where to save + * the message instead of returning it + * @param array $params The Build parameters passed to the + * get() method. See get() for more info. * * @return mixed True or PEAR error object - * @access public - * @since 1.6.0 + * @since 1.6.0 */ - function saveMessageBody($filename, $params = null) + public function saveMessageBody($filename, $params = null) { - // Check state of file and raise an error properly - if (file_exists($filename) && !is_writable($filename)) { - $err = PEAR::raiseError('File is not writable: ' . $filename); - return $err; + if (!is_resource($filename)) { + // Check state of file and raise an error properly + if (!file_exists($filename) || !is_writable($filename)) { + return self::raiseError('File is not writable: ' . $filename); + } + + if (!($fh = fopen($filename, 'ab'))) { + return self::raiseError('Unable to open file: ' . $filename); + } + } else { + $fh = $filename; } // Temporarily reset magic_quotes_runtime and read file contents @@ -814,107 +826,126 @@ class Mail_mime @ini_set('magic_quotes_runtime', 0); } - if (!($fh = fopen($filename, 'ab'))) { - $err = PEAR::raiseError('Unable to open file: ' . $filename); - return $err; + // Write the rest of the message into file + $res = $this->get($params, $fh, true); + + if (!is_resource($filename)) { + fclose($fh); } - // Write the rest of the message into file - $res = $this->get($params, $filename, true); + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } return $res ? $res : true; } /** - * Builds the multipart message from the list ($this->_parts) and + * Builds the multipart message from the list ($this->parts) and * returns the mime content. * - * @param array $params Build parameters that change the way the email - * is built. Should be associative. See $_build_params. - * @param resource $filename Output file where to save the message instead of - * returning it - * @param boolean $skip_head True if you want to return/save only the message - * without headers + * @param array $params Build parameters that change the way the email + * is built. Should be associative. See $_build_params. + * @param mixed $filename Output filename or file pointer where to save + * the message instead of returning it + * @param boolean $skip_head True if you want to return/save only the message + * without headers * * @return mixed The MIME message content string, null or PEAR error object - * @access public */ - function &get($params = null, $filename = null, $skip_head = false) + public function get($params = null, $filename = null, $skip_head = false) { - if (isset($params)) { - while (list($key, $value) = each($params)) { - $this->_build_params[$key] = $value; - } + if (!empty($params) && is_array($params)) { + $this->build_params = array_merge($this->build_params, $params); } - if (isset($this->_headers['From'])) { + if (isset($this->headers['From'])) { // Bug #11381: Illegal characters in domain ID - if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $this->_headers['From'], $matches)) { + if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $this->headers['From'], $matches)) { $domainID = $matches[1]; } else { $domainID = '@localhost'; } - foreach ($this->_html_images as $i => $img) { - $cid = $this->_html_images[$i]['cid']; + + foreach ($this->html_images as $i => $img) { + $cid = $this->html_images[$i]['cid']; if (!preg_match('#'.preg_quote($domainID).'$#', $cid)) { - $this->_html_images[$i]['cid'] = $cid . $domainID; + $this->html_images[$i]['cid'] = $cid . $domainID; } } } - if (count($this->_html_images) && isset($this->_htmlbody)) { - foreach ($this->_html_images as $key => $value) { - $regex = array(); - $regex[] = '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' . - preg_quote($value['name'], '#') . '\3#'; - $regex[] = '#(?i)url(?-i)\(\s*(["\']?)' . - preg_quote($value['name'], '#') . '\1\s*\)#'; + if (count($this->html_images) && isset($this->htmlbody)) { + foreach ($this->html_images as $key => $value) { + $rval = preg_quote($value['name'], '#'); + $regex = array( + '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' . $rval . '\3#', + '#(?i)url(?-i)\(\s*(["\']?)' . $rval . '\1\s*\)#', + ); - $rep = array(); - $rep[] = '\1\2=\3cid:' . $value['cid'] .'\3'; - $rep[] = 'url(\1cid:' . $value['cid'] . '\1)'; + $rep = array( + '\1\2=\3cid:' . $value['cid'] .'\3', + 'url(\1cid:' . $value['cid'] . '\1)', + ); - $this->_htmlbody = preg_replace($regex, $rep, $this->_htmlbody); - $this->_html_images[$key]['name'] - = $this->_basename($this->_html_images[$key]['name']); + $this->htmlbody = preg_replace($regex, $rep, $this->htmlbody); + $this->html_images[$key]['name'] + = $this->basename($this->html_images[$key]['name']); } } - $this->_checkParams(); + $this->checkParams(); - $null = null; - $attachments = count($this->_parts) ? true : false; - $html_images = count($this->_html_images) ? true : false; - $html = strlen($this->_htmlbody) ? true : false; - $text = (!$html && strlen($this->_txtbody)) ? true : false; + $attachments = count($this->parts) > 0; + $html_images = count($this->html_images) > 0; + $html = strlen($this->htmlbody) > 0; + $calendar = strlen($this->calbody) > 0; + $has_text = strlen($this->txtbody) > 0; + $text = !$html && $has_text; + $mixed_params = array('preamble' => $this->build_params['preamble']); switch (true) { + case $calendar && !$attachments && !$text && !$html: + $message = $this->addCalendarPart(); + break; + + case $calendar && !$attachments: + $message = $this->addAlternativePart($mixed_params); + if ($has_text) { + $this->addTextPart($message); + } + if ($html) { + $this->addHtmlPart($message); + } + $this->addCalendarPart($message); + break; + case $text && !$attachments: - $message =& $this->_addTextPart($null, $this->_txtbody); + $message = $this->addTextPart(); break; case !$text && !$html && $attachments: - $message =& $this->_addMixedPart(); - for ($i = 0; $i < count($this->_parts); $i++) { - $this->_addAttachmentPart($message, $this->_parts[$i]); + $message = $this->addMixedPart($mixed_params); + for ($i = 0; $i < count($this->parts); $i++) { + $this->addAttachmentPart($message, $this->parts[$i]); } break; case $text && $attachments: - $message =& $this->_addMixedPart(); - $this->_addTextPart($message, $this->_txtbody); - for ($i = 0; $i < count($this->_parts); $i++) { - $this->_addAttachmentPart($message, $this->_parts[$i]); + $message = $this->addMixedPart($mixed_params); + $this->addTextPart($message); + for ($i = 0; $i < count($this->parts); $i++) { + $this->addAttachmentPart($message, $this->parts[$i]); } break; case $html && !$attachments && !$html_images: - if (isset($this->_txtbody)) { - $message =& $this->_addAlternativePart($null); - $this->_addTextPart($message, $this->_txtbody); - $this->_addHtmlPart($message); + if ($has_text) { + $message = $this->addAlternativePart(); + $this->addTextPart($message); + $this->addHtmlPart($message); } else { - $message =& $this->_addHtmlPart($null); + $message = $this->addHtmlPart(); } break; @@ -924,23 +955,23 @@ class Mail_mime // * Content-Type: multipart/related; // * html // * image... - if (isset($this->_txtbody)) { - $message =& $this->_addAlternativePart($null); - $this->_addTextPart($message, $this->_txtbody); - - $ht =& $this->_addRelatedPart($message); - $this->_addHtmlPart($ht); - for ($i = 0; $i < count($this->_html_images); $i++) { - $this->_addHtmlImagePart($ht, $this->_html_images[$i]); + if ($has_text) { + $message = $this->addAlternativePart(); + $this->addTextPart($message); + + $ht = $this->addRelatedPart($message); + $this->addHtmlPart($ht); + for ($i = 0; $i < count($this->html_images); $i++) { + $this->addHtmlImagePart($ht, $this->html_images[$i]); } } else { // * Content-Type: multipart/related; // * html // * image... - $message =& $this->_addRelatedPart($null); - $this->_addHtmlPart($message); - for ($i = 0; $i < count($this->_html_images); $i++) { - $this->_addHtmlImagePart($message, $this->_html_images[$i]); + $message = $this->addRelatedPart(); + $this->addHtmlPart($message); + for ($i = 0; $i < count($this->html_images); $i++) { + $this->addHtmlImagePart($message, $this->html_images[$i]); } } /* @@ -950,62 +981,60 @@ class Mail_mime // * text // * html // * image... - $message =& $this->_addRelatedPart($null); - if (isset($this->_txtbody)) { - $alt =& $this->_addAlternativePart($message); - $this->_addTextPart($alt, $this->_txtbody); - $this->_addHtmlPart($alt); + $message = $this->addRelatedPart(); + if ($has_text) { + $alt = $this->addAlternativePart($message); + $this->addTextPart($alt); + $this->addHtmlPart($alt); } else { - $this->_addHtmlPart($message); + $this->addHtmlPart($message); } - for ($i = 0; $i < count($this->_html_images); $i++) { - $this->_addHtmlImagePart($message, $this->_html_images[$i]); + for ($i = 0; $i < count($this->html_images); $i++) { + $this->addHtmlImagePart($message, $this->html_images[$i]); } */ break; case $html && $attachments && !$html_images: - $message =& $this->_addMixedPart(); - if (isset($this->_txtbody)) { - $alt =& $this->_addAlternativePart($message); - $this->_addTextPart($alt, $this->_txtbody); - $this->_addHtmlPart($alt); + $message = $this->addMixedPart($mixed_params); + if ($has_text) { + $alt = $this->addAlternativePart($message); + $this->addTextPart($alt); + $this->addHtmlPart($alt); } else { - $this->_addHtmlPart($message); + $this->addHtmlPart($message); } - for ($i = 0; $i < count($this->_parts); $i++) { - $this->_addAttachmentPart($message, $this->_parts[$i]); + for ($i = 0; $i < count($this->parts); $i++) { + $this->addAttachmentPart($message, $this->parts[$i]); } break; case $html && $attachments && $html_images: - $message =& $this->_addMixedPart(); - if (isset($this->_txtbody)) { - $alt =& $this->_addAlternativePart($message); - $this->_addTextPart($alt, $this->_txtbody); - $rel =& $this->_addRelatedPart($alt); + $message = $this->addMixedPart($mixed_params); + if ($has_text) { + $alt = $this->addAlternativePart($message); + $this->addTextPart($alt); + $rel = $this->addRelatedPart($alt); } else { - $rel =& $this->_addRelatedPart($message); + $rel = $this->addRelatedPart($message); } - $this->_addHtmlPart($rel); - for ($i = 0; $i < count($this->_html_images); $i++) { - $this->_addHtmlImagePart($rel, $this->_html_images[$i]); + $this->addHtmlPart($rel); + for ($i = 0; $i < count($this->html_images); $i++) { + $this->addHtmlImagePart($rel, $this->html_images[$i]); } - for ($i = 0; $i < count($this->_parts); $i++) { - $this->_addAttachmentPart($message, $this->_parts[$i]); + for ($i = 0; $i < count($this->parts); $i++) { + $this->addAttachmentPart($message, $this->parts[$i]); } break; - } if (!isset($message)) { - $ret = null; - return $ret; + return null; } // Use saved boundary - if (!empty($this->_build_params['boundary'])) { - $boundary = $this->_build_params['boundary']; + if (!empty($this->build_params['boundary'])) { + $boundary = $this->build_params['boundary']; } else { $boundary = null; } @@ -1014,20 +1043,18 @@ class Mail_mime if ($filename) { // Append mimePart message headers and body into file $headers = $message->encodeToFile($filename, $boundary, $skip_head); - if (PEAR::isError($headers)) { + if (self::isError($headers)) { return $headers; } - $this->_headers = array_merge($this->_headers, $headers); - $ret = null; - return $ret; + $this->headers = array_merge($this->headers, $headers); + return null; } else { $output = $message->encode($boundary, $skip_head); - if (PEAR::isError($output)) { + if (self::isError($output)) { return $output; } - $this->_headers = array_merge($this->_headers, $output['headers']); - $body = $output['body']; - return $body; + $this->headers = array_merge($this->headers, $output['headers']); + return $output['body']; } } @@ -1041,11 +1068,10 @@ class Mail_mime * @param bool $overwrite Overwrite already existing headers. * @param bool $skip_content Don't return content headers: Content-Type, * Content-Disposition and Content-Transfer-Encoding - * - * @return array Assoc array with the mime headers - * @access public + * + * @return array Assoc array with the mime headers */ - function &headers($xtra_headers = null, $overwrite = false, $skip_content = false) + public function headers($xtra_headers = null, $overwrite = false, $skip_content = false) { // Add mime version header $headers['MIME-Version'] = '1.0'; @@ -1055,7 +1081,7 @@ class Mail_mime // we got them when called before get() or something in the message // has been changed after get() [#14780] if (!$skip_content) { - $headers += $this->_contentHeaders(); + $headers += $this->contentHeaders(); } if (!empty($xtra_headers)) { @@ -1063,22 +1089,22 @@ class Mail_mime } if ($overwrite) { - $this->_headers = array_merge($this->_headers, $headers); + $this->headers = array_merge($this->headers, $headers); } else { - $this->_headers = array_merge($headers, $this->_headers); + $this->headers = array_merge($headers, $this->headers); } - $headers = $this->_headers; + $headers = $this->headers; if ($skip_content) { unset($headers['Content-Type']); unset($headers['Content-Transfer-Encoding']); unset($headers['Content-Disposition']); - } else if (!empty($this->_build_params['ctype'])) { - $headers['Content-Type'] = $this->_build_params['ctype']; + } else if (!empty($this->build_params['ctype'])) { + $headers['Content-Type'] = $this->build_params['ctype']; } - $encodedHeaders = $this->_encodeHeaders($headers); + $encodedHeaders = $this->encodeHeaders($headers); return $encodedHeaders; } @@ -1092,10 +1118,9 @@ class Mail_mime * @param bool $skip_content Don't return content headers: Content-Type, * Content-Disposition and Content-Transfer-Encoding * - * @return string Plain text headers - * @access public + * @return string Plain text headers */ - function txtHeaders($xtra_headers = null, $overwrite = false, $skip_content = false) + public function txtHeaders($xtra_headers = null, $overwrite = false, $skip_content = false) { $headers = $this->headers($xtra_headers, $overwrite, $skip_content); @@ -1108,7 +1133,7 @@ class Mail_mime } $ret = ''; - $eol = $this->_build_params['eol']; + $eol = $this->build_params['eol']; foreach ($headers as $key => $val) { if (is_array($val)) { @@ -1126,31 +1151,28 @@ class Mail_mime /** * Sets message Content-Type header. * Use it to build messages with various content-types e.g. miltipart/raport - * not supported by _contentHeaders() function. + * not supported by contentHeaders() function. * * @param string $type Type name * @param array $params Hash array of header parameters * * @return void - * @access public - * @since 1.7.0 + * @since 1.7.0 */ - function setContentType($type, $params = array()) + public function setContentType($type, $params = array()) { $header = $type; - $eol = !empty($this->_build_params['eol']) - ? $this->_build_params['eol'] : "\r\n"; + $eol = !empty($this->build_params['eol']) ? $this->build_params['eol'] : "\r\n"; // add parameters - $token_regexp = '#([^\x21,\x23-\x27,\x2A,\x2B,\x2D' - . ',\x2E,\x30-\x39,\x41-\x5A,\x5E-\x7E])#'; + $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#'; + if (is_array($params)) { foreach ($params as $name => $value) { if ($name == 'boundary') { - $this->_build_params['boundary'] = $value; - } - if (!preg_match($token_regexp, $value)) { + $this->build_params['boundary'] = $value; + } else if (!preg_match($token_regexp, $value)) { $header .= ";$eol $name=$value"; } else { $value = addcslashes($value, '\\"'); @@ -1160,15 +1182,15 @@ class Mail_mime } // add required boundary parameter if not defined - if (preg_match('/^multipart\//i', $type)) { - if (empty($this->_build_params['boundary'])) { - $this->_build_params['boundary'] = '=_' . md5(rand() . microtime()); + if (stripos($type, 'multipart/') === 0) { + if (empty($this->build_params['boundary'])) { + $this->build_params['boundary'] = '=_' . md5(rand() . microtime()); } - $header .= ";$eol boundary=\"".$this->_build_params['boundary']."\""; + $header .= ";$eol boundary=\"".$this->build_params['boundary']."\""; } - $this->_build_params['ctype'] = $header; + $this->build_params['ctype'] = $header; } /** @@ -1177,11 +1199,10 @@ class Mail_mime * @param string $subject String to set the subject to. * * @return void - * @access public */ - function setSubject($subject) + public function setSubject($subject) { - $this->_headers['Subject'] = $subject; + $this->headers['Subject'] = $subject; } /** @@ -1190,11 +1211,10 @@ class Mail_mime * @param string $email The email address to use * * @return void - * @access public */ - function setFrom($email) + public function setFrom($email) { - $this->_headers['From'] = $email; + $this->headers['From'] = $email; } /** @@ -1204,14 +1224,13 @@ class Mail_mime * @param string $email The email direction to add * * @return void - * @access public */ - function addTo($email) + public function addTo($email) { - if (isset($this->_headers['To'])) { - $this->_headers['To'] .= ", $email"; + if (isset($this->headers['To'])) { + $this->headers['To'] .= ", $email"; } else { - $this->_headers['To'] = $email; + $this->headers['To'] = $email; } } @@ -1222,14 +1241,13 @@ class Mail_mime * @param string $email The email direction to add * * @return void - * @access public */ - function addCc($email) + public function addCc($email) { - if (isset($this->_headers['Cc'])) { - $this->_headers['Cc'] .= ", $email"; + if (isset($this->headers['Cc'])) { + $this->headers['Cc'] .= ", $email"; } else { - $this->_headers['Cc'] = $email; + $this->headers['Cc'] = $email; } } @@ -1240,14 +1258,13 @@ class Mail_mime * @param string $email The email direction to add * * @return void - * @access public */ - function addBcc($email) + public function addBcc($email) { - if (isset($this->_headers['Bcc'])) { - $this->_headers['Bcc'] .= ", $email"; + if (isset($this->headers['Bcc'])) { + $this->headers['Bcc'] .= ", $email"; } else { - $this->_headers['Bcc'] = $email; + $this->headers['Bcc'] = $email; } } @@ -1255,20 +1272,19 @@ class Mail_mime * Since the PHP send function requires you to specify * recipients (To: header) separately from the other * headers, the To: header is not properly encoded. - * To fix this, you can use this public method to - * encode your recipients before sending to the send - * function + * To fix this, you can use this public method to encode + * your recipients before sending to the send function. * * @param string $recipients A comma-delimited list of recipients * - * @return string Encoded data - * @access public + * @return string Encoded data */ - function encodeRecipients($recipients) + public function encodeRecipients($recipients) { - $input = array("To" => $recipients); - $retval = $this->_encodeHeaders($input); - return $retval["To"] ; + $input = array('To' => $recipients); + $retval = $this->encodeHeaders($input); + + return $retval['To'] ; } /** @@ -1277,14 +1293,14 @@ class Mail_mime * @param array $input The header data to encode * @param array $params Extra build parameters * - * @return array Encoded data - * @access private + * @return array Encoded data */ - function _encodeHeaders($input, $params = array()) + protected function encodeHeaders($input, $params = array()) { - $build_params = $this->_build_params; - while (list($key, $value) = each($params)) { - $build_params[$key] = $value; + $build_params = $this->build_params; + + if (!empty($params)) { + $build_params = array_merge($build_params, $params); } foreach ($input as $hdr_name => $hdr_value) { @@ -1295,11 +1311,13 @@ class Mail_mime $build_params['head_charset'], $build_params['head_encoding'] ); } - } else { + } else if ($hdr_value !== null) { $input[$hdr_name] = $this->encodeHeader( $hdr_name, $hdr_value, $build_params['head_charset'], $build_params['head_encoding'] ); + } else { + unset($input[$hdr_name]); } } @@ -1314,26 +1332,24 @@ class Mail_mime * @param string $charset Character set name * @param string $encoding Encoding name (base64 or quoted-printable) * - * @return string Encoded header data (without a name) - * @access public - * @since 1.5.3 + * @return string Encoded header data (without a name) + * @since 1.5.3 */ - function encodeHeader($name, $value, $charset, $encoding) + public function encodeHeader($name, $value, $charset, $encoding) { return Mail_mimePart::encodeHeader( - $name, $value, $charset, $encoding, $this->_build_params['eol'] + $name, $value, $charset, $encoding, $this->build_params['eol'] ); } /** - * Get file's basename (locale independent) + * Get file's basename (locale independent) * * @param string $filename Filename * - * @return string Basename - * @access private + * @return string Basename */ - function _basename($filename) + protected function basename($filename) { // basename() is not unicode safe and locale dependent if (stristr(PHP_OS, 'win') || stristr(PHP_OS, 'netware')) { @@ -1347,18 +1363,27 @@ class Mail_mime * Get Content-Type and Content-Transfer-Encoding headers of the message * * @return array Headers array - * @access private */ - function _contentHeaders() + protected function contentHeaders() { - $attachments = count($this->_parts) ? true : false; - $html_images = count($this->_html_images) ? true : false; - $html = strlen($this->_htmlbody) ? true : false; - $text = (!$html && strlen($this->_txtbody)) ? true : false; + $attachments = count($this->parts) > 0; + $html_images = count($this->html_images) > 0; + $html = strlen($this->htmlbody) > 0; + $calendar = strlen($this->calbody) > 0; + $has_text = strlen($this->txtbody) > 0; + $text = !$html && $has_text; $headers = array(); // See get() switch (true) { + case $calendar && !$attachments && !$html && !$has_text: + $headers['Content-Type'] = 'text/calendar'; + break; + + case $calendar && !$attachments: + $headers['Content-Type'] = 'multipart/alternative'; + break; + case $text && !$attachments: $headers['Content-Type'] = 'text/plain'; break; @@ -1370,16 +1395,16 @@ class Mail_mime $headers['Content-Type'] = 'multipart/mixed'; break; - case $html && !$attachments && !$html_images && isset($this->_txtbody): - case $html && !$attachments && $html_images && isset($this->_txtbody): + case $html && !$attachments && !$html_images && $has_text: + case $html && !$attachments && $html_images && $has_text: $headers['Content-Type'] = 'multipart/alternative'; break; - case $html && !$attachments && !$html_images && !isset($this->_txtbody): + case $html && !$attachments && !$html_images && !$has_text: $headers['Content-Type'] = 'text/html'; break; - case $html && !$attachments && $html_images && !isset($this->_txtbody): + case $html && !$attachments && $html_images && !$has_text: $headers['Content-Type'] = 'multipart/related'; break; @@ -1387,36 +1412,60 @@ class Mail_mime return $headers; } - $this->_checkParams(); + $this->checkParams(); - $eol = !empty($this->_build_params['eol']) - ? $this->_build_params['eol'] : "\r\n"; + $eol = !empty($this->build_params['eol']) + ? $this->build_params['eol'] : "\r\n"; if ($headers['Content-Type'] == 'text/plain') { // single-part message: add charset and encoding - $headers['Content-Type'] - .= ";$eol charset=" . $this->_build_params['text_charset']; + if ($this->build_params['text_charset']) { + $charset = 'charset=' . $this->build_params['text_charset']; + // place charset parameter in the same line, if possible + // 26 = strlen("Content-Type: text/plain; ") + $headers['Content-Type'] + .= (strlen($charset) + 26 <= 76) ? "; $charset" : ";$eol $charset"; + } + $headers['Content-Transfer-Encoding'] - = $this->_build_params['text_encoding']; + = $this->build_params['text_encoding']; } else if ($headers['Content-Type'] == 'text/html') { // single-part message: add charset and encoding - $headers['Content-Type'] - .= ";$eol charset=" . $this->_build_params['html_charset']; + if ($this->build_params['html_charset']) { + $charset = 'charset=' . $this->build_params['html_charset']; + // place charset parameter in the same line, if possible + $headers['Content-Type'] + .= (strlen($charset) + 25 <= 76) ? "; $charset" : ";$eol $charset"; + } + $headers['Content-Transfer-Encoding'] + = $this->build_params['html_encoding']; + } else if ($headers['Content-Type'] == 'text/calendar') { + // single-part message: add charset and encoding + if ($this->build_params['calendar_charset']) { + $charset = 'charset=' . $this->build_params['calendar_charset']; + $headers['Content-Type'] .= "; $charset"; + } + + if ($this->build_params['calendar_method']) { + $method = 'method=' . $this->build_params['calendar_method']; + $headers['Content-Type'] .= "; $method"; + } + $headers['Content-Transfer-Encoding'] - = $this->_build_params['html_encoding']; + = $this->build_params['calendar_encoding']; } else { - // multipart message: add charset and boundary - if (!empty($this->_build_params['boundary'])) { - $boundary = $this->_build_params['boundary']; - } else if (!empty($this->_headers['Content-Type']) - && preg_match('/boundary="([^"]+)"/', $this->_headers['Content-Type'], $m) + // multipart message: and boundary + if (!empty($this->build_params['boundary'])) { + $boundary = $this->build_params['boundary']; + } else if (!empty($this->headers['Content-Type']) + && preg_match('/boundary="([^"]+)"/', $this->headers['Content-Type'], $m) ) { $boundary = $m[1]; } else { $boundary = '=_' . md5(rand() . microtime()); } - $this->_build_params['boundary'] = $boundary; + $this->build_params['boundary'] = $boundary; $headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; } @@ -1427,38 +1476,140 @@ class Mail_mime * Validate and set build parameters * * @return void - * @access private */ - function _checkParams() + protected function checkParams() { $encodings = array('7bit', '8bit', 'base64', 'quoted-printable'); - $this->_build_params['text_encoding'] - = strtolower($this->_build_params['text_encoding']); - $this->_build_params['html_encoding'] - = strtolower($this->_build_params['html_encoding']); + $this->build_params['text_encoding'] + = strtolower($this->build_params['text_encoding']); + $this->build_params['html_encoding'] + = strtolower($this->build_params['html_encoding']); + $this->build_params['calendar_encoding'] + = strtolower($this->build_params['calendar_encoding']); - if (!in_array($this->_build_params['text_encoding'], $encodings)) { - $this->_build_params['text_encoding'] = '7bit'; + if (!in_array($this->build_params['text_encoding'], $encodings)) { + $this->build_params['text_encoding'] = '7bit'; + } + if (!in_array($this->build_params['html_encoding'], $encodings)) { + $this->build_params['html_encoding'] = '7bit'; } - if (!in_array($this->_build_params['html_encoding'], $encodings)) { - $this->_build_params['html_encoding'] = '7bit'; + if (!in_array($this->build_params['calendar_encoding'], $encodings)) { + $this->build_params['calendar_encoding'] = '7bit'; } // text body - if ($this->_build_params['text_encoding'] == '7bit' - && !preg_match('/ascii/i', $this->_build_params['text_charset']) - && preg_match('/[^\x00-\x7F]/', $this->_txtbody) + if ($this->build_params['text_encoding'] == '7bit' + && !preg_match('/ascii/i', $this->build_params['text_charset']) + && preg_match('/[^\x00-\x7F]/', $this->txtbody) ) { - $this->_build_params['text_encoding'] = 'quoted-printable'; + $this->build_params['text_encoding'] = 'quoted-printable'; } // html body - if ($this->_build_params['html_encoding'] == '7bit' - && !preg_match('/ascii/i', $this->_build_params['html_charset']) - && preg_match('/[^\x00-\x7F]/', $this->_htmlbody) + if ($this->build_params['html_encoding'] == '7bit' + && !preg_match('/ascii/i', $this->build_params['html_charset']) + && preg_match('/[^\x00-\x7F]/', $this->htmlbody) + ) { + $this->build_params['html_encoding'] = 'quoted-printable'; + } + // calendar body + if ($this->build_params['calendar_encoding'] == '7bit' + && !preg_match('/ascii/i', $this->build_params['calendar_charset']) + && preg_match('/[^\x00-\x7F]/', $this->calbody) ) { - $this->_build_params['html_encoding'] = 'quoted-printable'; + $this->build_params['calendar_encoding'] = 'quoted-printable'; + } + } + + /** + * Set body of specified message part + * + * @param string $type One of: txtbody, calbody, htmlbody + * @param string $data Either a string or the file name with the contents + * @param bool $isfile If true the first param should be treated + * as a file name, else as a string (default) + * @param bool $append If true the text or file is appended to + * the existing body, else the old body is + * overwritten + * + * @return mixed True on success or PEAR_Error object + */ + protected function setBody($type, $data, $isfile = false, $append = false) + { + if ($isfile) { + $data = $this->file2str($data); + if (self::isError($data)) { + return $data; + } } + + if (!$append) { + $this->{$type} = $data; + } else { + $this->{$type} .= $data; + } + + // wordwrap the txtbody to be 750 characters to comply with RFC 2821 + // CRM-3133 + $this->{$type} = wordwrap($this->{$type}, 750); + return true; } -} // End of class + /** + * Adds a subpart to the mimePart object and + * returns it during the build process. + * + * @param mixed $obj The object to add the part to, or + * anything else if a new object is to be created. + * @param string $body Part body + * @param string $ctype Part content type + * @param string $type Internal part type + * + * @return object The mimePart object + */ + protected function addBodyPart($obj, $body, $ctype, $type) + { + $params['content_type'] = $ctype; + $params['encoding'] = $this->build_params[$type . '_encoding']; + $params['charset'] = $this->build_params[$type . '_charset']; + $params['eol'] = $this->build_params['eol']; + + if (is_object($obj)) { + $ret = $obj->addSubpart($body, $params); + } else { + $ret = new Mail_mimePart($body, $params); + } + + return $ret; + } + + /** + * PEAR::isError implementation + * + * @param mixed $data Object + * + * @return bool True if object is an instance of PEAR_Error + */ + public static function isError($data) + { + // PEAR::isError() is not PHP 5.4 compatible (see Bug #19473) + if (is_a($data, 'PEAR_Error')) { + return true; + } + + return false; + } + + /** + * PEAR::raiseError implementation + * + * @param string $message A text error message + * + * @return PEAR_Error Instance of PEAR_Error + */ + public static function raiseError($message) + { + // PEAR::raiseError() is not PHP 5.4 compatible + return new PEAR_Error($message); + } +} diff --git a/civicrm/packages/Mail/mimeDecode.php b/civicrm/packages/Mail/mimeDecode.php index 4cf1ad7a59f98de0cd5610e3dbee608c50691e01..1a12673c8abdcdf3801edd647ada0aa4ed1c22ca 100644 --- a/civicrm/packages/Mail/mimeDecode.php +++ b/civicrm/packages/Mail/mimeDecode.php @@ -28,8 +28,8 @@ * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. - * - Neither the name of the authors, nor the names of its contributors - * may be used to endorse or promote products derived from this + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" @@ -52,7 +52,7 @@ * @author Sean Coates <sean@php.net> * @copyright 2003-2006 PEAR <pear-group@php.net> * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version CVS: $Id: mimeDecode.php 288500 2009-09-21 05:32:32Z alan_k $ + * @version CVS: $Id: mimeDecode.php 337165 2015-07-15 09:42:08Z alan_k $ * @link http://pear.php.net/package/Mail_mime */ @@ -141,12 +141,13 @@ class Mail_mimeDecode extends PEAR /** * Flag to determine whether to decode headers - * - * @var boolean + * (set to UTF8 to iconv convert headers) + * @var mixed * @access private */ var $_decode_headers; + /** * Flag to determine whether to include attached messages * as body in the returned object. Depends on $_include_bodies @@ -176,6 +177,12 @@ class Mail_mimeDecode extends PEAR $this->_include_bodies = true; $this->_rfc822_bodies = false; } + // BC + function Mail_mimeDecode($input) + { + $this->__construct($input); + } + /** * Begins the decoding process. If called statically @@ -188,7 +195,9 @@ class Mail_mimeDecode extends PEAR * object. * decode_bodies - Whether to decode the bodies * of the parts. (Transfer encoding) - * decode_headers - Whether to decode headers + * decode_headers - Whether to decode headers, + * - use "UTF8//IGNORE" to convert charset. + * * input - If called statically, this will be treated * as the input * @return object Decoded results @@ -197,7 +206,7 @@ class Mail_mimeDecode extends PEAR function decode($params = null) { // determine if this method has been called statically - $isStatic = !(isset($this) && get_class($this) == __CLASS__); + $isStatic = empty($this) || !is_a($this, __CLASS__); // Have we been called statically? // If so, create an object and pass details to that. @@ -221,6 +230,10 @@ class Mail_mimeDecode extends PEAR $this->_rfc822_bodies = isset($params['rfc_822bodies']) ? $params['rfc_822bodies'] : false; + if (is_string($this->_decode_headers) && !function_exists('iconv')) { + PEAR::raiseError('header decode conversion requested, however iconv is missing'); + } + $structure = $this->_decode($this->_header, $this->_body); if ($structure === false) { $structure = $this->raiseError($this->_error); @@ -247,6 +260,7 @@ class Mail_mimeDecode extends PEAR $headers = $this->_parseHeaders($headers); foreach ($headers as $value) { + $value['value'] = $this->_decodeHeader($value['value']); if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) { $return->headers[strtolower($value['name'])] = array($return->headers[strtolower($value['name'])]); $return->headers[strtolower($value['name'])][] = $value['value']; @@ -259,8 +273,8 @@ class Mail_mimeDecode extends PEAR } } - reset($headers); - while (list($key, $value) = each($headers)) { + + foreach ($headers as $key => $value) { $headers[$key]['name'] = strtolower($headers[$key]['name']); switch ($headers[$key]['name']) { @@ -273,7 +287,7 @@ class Mail_mimeDecode extends PEAR } if (isset($content_type['other'])) { - while (list($p_name, $p_value) = each($content_type['other'])) { + foreach($content_type['other'] as $p_name => $p_value) { $return->ctype_parameters[$p_name] = $p_value; } } @@ -283,7 +297,7 @@ class Mail_mimeDecode extends PEAR $content_disposition = $this->_parseHeaderValue($headers[$key]['value']); $return->disposition = $content_disposition['value']; if (isset($content_disposition['other'])) { - while (list($p_name, $p_value) = each($content_disposition['other'])) { + foreach($content_disposition['other'] as $p_name => $p_value) { $return->d_parameters[$p_name] = $p_value; } } @@ -307,6 +321,14 @@ class Mail_mimeDecode extends PEAR $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; break; + case 'multipart/signed': // PGP + $parts = $this->_boundarySplit($body, $content_type['other']['boundary'], true); + $return->parts['msg_body'] = $parts[0]; + list($part_header, $part_body) = $this->_splitBodyHeader($parts[1]); + $return->parts['sig_hdr'] = $part_header; + $return->parts['sig_body'] = $part_body; + break; + case 'multipart/parallel': case 'multipart/appledouble': // Appledouble mail case 'multipart/report': // RFC1892 @@ -314,7 +336,9 @@ class Mail_mimeDecode extends PEAR case 'multipart/digest': case 'multipart/alternative': case 'multipart/related': + case 'multipart/relative': //#20431 - android case 'multipart/mixed': + case 'application/vnd.wap.multipart.related': if(!isset($content_type['other']['boundary'])){ $this->_error = 'No boundary found for ' . $content_type['value'] . ' part'; return false; @@ -333,6 +357,7 @@ class Mail_mimeDecode extends PEAR break; case 'message/rfc822': + case 'message/delivery-status': // #bug #18693 if ($this->_rfc822_bodies) { $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body); @@ -379,7 +404,7 @@ class Mail_mimeDecode extends PEAR } for ($i = 0; $i < count($structure->parts); $i++) { - + if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') { $prepend = $prepend . $mime_number . '.'; $_mime_number = ''; @@ -399,7 +424,7 @@ class Mail_mimeDecode extends PEAR $structure->mime_id = $prepend . $mime_number; $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; } - + return $return; } @@ -417,6 +442,11 @@ class Mail_mimeDecode extends PEAR if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) { return array($match[1], $match[2]); } + // bug #17325 - empty bodies are allowed. - we just check that at least one line + // of headers exist.. + if (count(explode("\n",$input))) { + return array($input, ''); + } $this->_error = 'Could not split header and body'; return false; } @@ -435,18 +465,36 @@ class Mail_mimeDecode extends PEAR if ($input !== '') { // Unfold the input $input = preg_replace("/\r?\n/", "\r\n", $input); + //#7065 - wrapping.. with encoded stuff.. - probably not needed, + // wrapping space should only get removed if the trailing item on previous line is a + // encoded character + $input = preg_replace("/=\r\n(\t| )+/", '=', $input); $input = preg_replace("/\r\n(\t| )+/", ' ', $input); - $headers = explode("\r\n", trim($input)); + $headers = explode("\r\n", trim($input)); + $got_start = false; foreach ($headers as $value) { + if (!$got_start) { + // munge headers for mbox style from + if ($value[0] == '>') { + $value = substring($value, 1); // remove mbox > + } + if (substr($value,0,5) == 'From ') { + $value = 'Return-Path: ' . substr($value, 5); + } else { + $got_start = true; + } + } + $hdr_name = substr($value, 0, $pos = strpos($value, ':')); $hdr_value = substr($value, $pos+1); - if($hdr_value[0] == ' ') + if($hdr_value[0] == ' ') { $hdr_value = substr($hdr_value, 1); + } $return[] = array( 'name' => $hdr_name, - 'value' => $this->_decode_headers ? $this->_decodeHeader($hdr_value) : $hdr_value + 'value' => $hdr_value ); } } else { @@ -463,46 +511,190 @@ class Mail_mimeDecode extends PEAR * robust as it could be. Eg. header comments * in the wrong place will probably break it. * + * Extra things this can handle + * filename*0=...... + * filename*1=...... + * + * This is where lines are broken in, and need merging. + * + * filename*0*=ENC'lang'urlencoded data. + * filename*1*=ENC'lang'urlencoded data. + * + * + * * @param string Header value to parse * @return array Contains parsed result * @access private */ function _parseHeaderValue($input) { + if (($pos = strpos($input, ';')) === false) { + $input = $this->_decodeHeader($input); + $return['value'] = trim($input); + return $return; + } - if (($pos = strpos($input, ';')) !== false) { - $return['value'] = trim(substr($input, 0, $pos)); - $input = trim(substr($input, $pos+1)); - if (strlen($input) > 0) { + $value = substr($input, 0, $pos); + $value = $this->_decodeHeader($value); + $return['value'] = trim($value); + $input = trim(substr($input, $pos+1)); - // This splits on a semi-colon, if there's no preceeding backslash - // Now works with quoted values; had to glue the \; breaks in PHP - // the regex is already bordering on incomprehensible - $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/'; - preg_match_all($splitRegex, $input, $matches); - $parameters = array(); - for ($i=0; $i<count($matches[0]); $i++) { - $param = $matches[0][$i]; - while (substr($param, -2) == '\;') { - $param .= $matches[0][++$i]; - } - $parameters[] = $param; + if (!strlen($input) > 0) { + return $return; + } + // at this point input contains xxxx=".....";zzzz="...." + // since we are dealing with quoted strings, we need to handle this properly.. + $i = 0; + $l = strlen($input); + $key = ''; + $val = false; // our string - including quotes.. + $q = false; // in quote.. + $lq = ''; // last quote.. + + while ($i < $l) { + + $c = $input[$i]; + //var_dump(array('i'=>$i,'c'=>$c,'q'=>$q, 'lq'=>$lq, 'key'=>$key, 'val' =>$val)); + + $escaped = false; + if ($c == '\\') { + $i++; + if ($i == $l-1) { // end of string. + break; } + $escaped = true; + $c = $input[$i]; + } + - for ($i = 0; $i < count($parameters); $i++) { - $param_name = trim(substr($parameters[$i], 0, $pos = strpos($parameters[$i], '=')), "'\";\t\\ "); - $param_value = trim(str_replace('\;', ';', substr($parameters[$i], $pos + 1)), "'\";\t\\ "); - if (!empty($param_value[0]) && $param_value[0] == '"') { - $param_value = substr($param_value, 1, -1); + // state - in key.. + if ($val === false) { + if (!$escaped && $c == '=') { + $val = ''; + $key = trim($key); + $i++; + continue; + } + if (!$escaped && $c == ';') { + if ($key) { // a key without a value.. + $key= trim($key); + $return['other'][$key] = ''; } - $return['other'][$param_name] = $param_value; - $return['other'][strtolower($param_name)] = $param_value; + $key = ''; } + $key .= $c; + $i++; + continue; } - } else { - $return['value'] = trim($input); + + // state - in value.. (as $val is set..) + + if ($q === false) { + // not in quote yet. + if ((!strlen($val) || $lq !== false) && $c == ' ' || $c == "\t") { + $i++; + continue; // skip leading spaces after '=' or after '"' + } + + // do not de-quote 'xxx*= itesm.. + $key_is_trans = $key[strlen($key)-1] == '*'; + + if (!$key_is_trans && !$escaped && ($c == '"' || $c == "'")) { + // start quoted area.. + $q = $c; + // in theory should not happen raw text in value part.. + // but we will handle it as a merged part of the string.. + $val = !strlen(trim($val)) ? '' : trim($val); + $i++; + continue; + } + // got end.... + if (!$escaped && $c == ';') { + + $return['other'][$key] = trim($val); + $val = false; + $key = ''; + $lq = false; + $i++; + continue; + } + + $val .= $c; + $i++; + continue; + } + + // state - in quote.. + if (!$escaped && $c == $q) { // potential exit state.. + + // end of quoted string.. + $lq = $q; + $q = false; + $i++; + continue; + } + + // normal char inside of quoted string.. + $val.= $c; + $i++; + } + + // do we have anything left.. + if (strlen(trim($key)) || $val !== false) { + + $val = trim($val); + + $return['other'][$key] = $val; + } + + + $clean_others = array(); + // merge added values. eg. *1[*] + foreach($return['other'] as $key =>$val) { + if (preg_match('/\*[0-9]+\**$/', $key)) { + $key = preg_replace('/(.*)\*[0-9]+(\**)$/', '\1\2', $key); + if (isset($clean_others[$key])) { + $clean_others[$key] .= $val; + continue; + } + + } + $clean_others[$key] = $val; + + } + + // handle language translation of '*' ending others. + foreach( $clean_others as $key =>$val) { + if ( $key[strlen($key)-1] != '*') { + $clean_others[strtolower($key)] = $val; + continue; + } + unset($clean_others[$key]); + $key = substr($key,0,-1); + //extended-initial-value := [charset] "'" [language] "'" + // extended-other-values + $match = array(); + $info = preg_match("/^([^']+)'([^']*)'(.*)$/", $val, $match); + + $clean_others[$key] = urldecode($match[3]); + $clean_others[strtolower($key)] = $clean_others[$key]; + $clean_others[strtolower($key).'-charset'] = $match[1]; + $clean_others[strtolower($key).'-language'] = $match[2]; + + + } + + + $return['other'] = $clean_others; + + // decode values. + foreach($return['other'] as $key =>$val) { + $charset = isset($return['other'][$key . '-charset']) ? + $return['other'][$key . '-charset'] : false; + + $return['other'][$key] = $this->_decodeHeader($val, $charset); } return $return; @@ -516,7 +708,7 @@ class Mail_mimeDecode extends PEAR * @return array Contains array of resulting mime parts * @access private */ - function _boundarySplit($input, $boundary) + function _boundarySplit($input, $boundary, $eatline = false) { $parts = array(); @@ -526,13 +718,22 @@ class Mail_mimeDecode extends PEAR if ($boundary == $bs_check) { $boundary = $bs_possible; } - - $tmp = explode('--' . $boundary, $input); - - for ($i = 1; $i < count($tmp) - 1; $i++) { - $parts[] = $tmp[$i]; + // eatline is used by multipart/signed. + $tmp = $eatline ? + preg_split("/\r?\n--".preg_quote($boundary, '/')."(|--)\n/", $input) : + preg_split("/--".preg_quote($boundary, '/')."((?=\s)|--)/", $input); + + $len = count($tmp) -1; + for ($i = 1; $i < $len; $i++) { + if (strlen(trim($tmp[$i]))) { + $parts[] = $tmp[$i]; + } } + // add the last part on if it does not end with the 'closing indicator' + if (!empty($tmp[$len]) && strlen(trim($tmp[$len])) && $tmp[$len][0] != '-') { + $parts[] = $tmp[$len]; + } return $parts; } @@ -546,8 +747,11 @@ class Mail_mimeDecode extends PEAR * @return string Decoded header value * @access private */ - function _decodeHeader($input) + function _decodeHeader($input, $default_charset=false) { + if (!$this->_decode_headers) { + return $input; + } // Remove white space between encoded-words $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); @@ -571,10 +775,18 @@ class Mail_mimeDecode extends PEAR $text = str_replace('='.$value, chr(hexdec($value)), $text); break; } - + if (is_string($this->_decode_headers)) { + $conv = @iconv($charset, $this->_decode_headers, $text); + $text = ($conv === false) ? $text : $conv; + } $input = str_replace($encoded, $text, $input); } + if ($default_charset && is_string($this->_decode_headers)) { + $conv = @iconv($charset, $this->_decode_headers, $input); + $input = ($conv === false) ? $input : $conv; + } + return $input; } @@ -621,9 +833,10 @@ class Mail_mimeDecode extends PEAR $input = preg_replace("/=\r?\n/", '', $input); // Replace encoded characters - $input = preg_replace_callback('/=([a-f0-9]{2})/i', function ($matches) { - return chr(hexdec($matches[1])); - }, $input); + + $cb = create_function('$matches', ' return chr(hexdec($matches[0]));'); + + $input = preg_replace_callback( '/=([a-f0-9]{2})/i', $cb, $input); return $input; } @@ -632,7 +845,7 @@ class Mail_mimeDecode extends PEAR * Checks the input for uuencoded files and returns * an array of them. Can be called statically, eg: * - * $files = Mail_mimeDecode::uudecode($some_text); + * $files =& Mail_mimeDecode::uudecode($some_text); * * It will check for the begin 666 ... end syntax * however and won't just blindly decode whatever you @@ -705,7 +918,7 @@ class Mail_mimeDecode extends PEAR /** * getSendArray() returns the arguments required for Mail::send() - * used to build the arguments for a mail::send() call + * used to build the arguments for a mail::send() call * * Usage: * $mailtext = Full email (for example generated by a template) @@ -747,7 +960,7 @@ class Mail_mimeDecode extends PEAR } $to = substr($to,1); return array($to,$header,$this->_body); - } + } /** * Returns a xml copy of the output of diff --git a/civicrm/packages/Mail/mimePart.php b/civicrm/packages/Mail/mimePart.php index dcf3de53c4e65cbb26588d1513c22463060cd146..187b1652daa83ef14efc3765e6e2e9603c3148bd 100644 --- a/civicrm/packages/Mail/mimePart.php +++ b/civicrm/packages/Mail/mimePart.php @@ -8,7 +8,7 @@ * of mime mail. * This class however allows full control over the email. * - * Compatible with PHP versions 4 and 5 + * Compatible with PHP version 5 and 7 * * LICENSE: This LICENSE is in the BSD license style. * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> @@ -48,10 +48,16 @@ * @author Aleksander Machniak <alec@php.net> * @copyright 2003-2006 PEAR <pear-group@php.net> * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version CVS: $Id: mimePart.php 301061 2010-07-07 18:20:17Z alec $ + * @version Release: @package_version@ * @link http://pear.php.net/package/Mail_mime */ +/** + * Require PEAR + * + * This package depends on PEAR to raise errors. + */ +require_once 'PEAR.php'; /** * The Mail_mimePart class is used to create MIME E-mail messages @@ -76,105 +82,108 @@ class Mail_mimePart { /** - * The encoding type of this part - * - * @var string - * @access private - */ - var $_encoding; + * The encoding type of this part + * + * @var string + */ + protected $encoding; + + /** + * An array of subparts + * + * @var array + */ + protected $subparts = array(); /** - * An array of subparts - * - * @var array - * @access private - */ - var $_subparts; + * The output of this part after being built + * + * @var string + */ + protected $encoded; /** - * The output of this part after being built - * - * @var string - * @access private - */ - var $_encoded; + * Headers for this part + * + * @var array + */ + protected $headers = array(); /** - * Headers for this part - * - * @var array - * @access private - */ - var $_headers; + * The body of this part (not encoded) + * + * @var string + */ + protected $body; /** - * The body of this part (not encoded) - * - * @var string - * @access private - */ - var $_body; + * The location of file with body of this part (not encoded) + * + * @var string + */ + protected $body_file; /** - * The location of file with body of this part (not encoded) - * - * @var string - * @access private - */ - var $_body_file; + * The short text of multipart part preamble (RFC2046 5.1.1) + * + * @var string + */ + protected $preamble; /** - * The end-of-line sequence - * - * @var string - * @access private - */ - var $_eol = "\r\n"; + * The end-of-line sequence + * + * @var string + */ + protected $eol = "\r\n"; + /** - * Constructor. - * - * Sets up the object. - * - * @param string $body The body of the mime part if any. - * @param array $params An associative array of optional parameters: - * content_type - The content type for this part eg multipart/mixed - * encoding - The encoding to use, 7bit, 8bit, - * base64, or quoted-printable - * cid - Content ID to apply - * disposition - Content disposition, inline or attachment - * dfilename - Filename parameter for content disposition - * description - Content description - * charset - Character set to use - * name_encoding - Encoding for attachment name (Content-Type) - * By default filenames are encoded using RFC2231 - * Here you can set RFC2047 encoding (quoted-printable - * or base64) instead - * filename_encoding - Encoding for attachment filename (Content-Disposition) - * See 'name_encoding' - * eol - End of line sequence. Default: "\r\n" - * body_file - Location of file with part's body (instead of $body) - * - * @access public - */ - function __construct($body = '', $params = array()) + * Constructor. + * + * Sets up the object. + * + * @param string $body The body of the mime part if any. + * @param array $params An associative array of optional parameters: + * content_type - The content type for this part eg multipart/mixed + * encoding - The encoding to use, 7bit, 8bit, + * base64, or quoted-printable + * charset - Content character set + * cid - Content ID to apply + * disposition - Content disposition, inline or attachment + * filename - Filename parameter for content disposition + * description - Content description + * name_encoding - Encoding of the attachment name (Content-Type) + * By default filenames are encoded using RFC2231 + * Here you can set RFC2047 encoding (quoted-printable + * or base64) instead + * filename_encoding - Encoding of the attachment filename (Content-Disposition) + * See 'name_encoding' + * headers_charset - Charset of the headers e.g. filename, description. + * If not set, 'charset' will be used + * eol - End of line sequence. Default: "\r\n" + * headers - Hash array with additional part headers. Array keys + * can be in form of <header_name>:<parameter_name> + * body_file - Location of file with part's body (instead of $body) + * preamble - short text of multipart part preamble (RFC2046 5.1.1) + */ + public function __construct($body = '', $params = array()) { if (!empty($params['eol'])) { - $this->_eol = $params['eol']; + $this->eol = $params['eol']; } else if (defined('MAIL_MIMEPART_CRLF')) { // backward-copat. - $this->_eol = MAIL_MIMEPART_CRLF; + $this->eol = MAIL_MIMEPART_CRLF; + } + + // Additional part headers + if (!empty($params['headers']) && is_array($params['headers'])) { + $headers = $params['headers']; } - $c_type = array(); - $c_disp = array(); foreach ($params as $key => $value) { switch ($key) { - case 'content_type': - $c_type['type'] = $value; - break; - case 'encoding': - $this->_encoding = $value; + $this->encoding = $value; $headers['Content-Transfer-Encoding'] = $value; break; @@ -182,100 +191,110 @@ class Mail_mimePart $headers['Content-ID'] = '<' . $value . '>'; break; - case 'disposition': - $c_disp['disp'] = $value; - break; - - case 'dfilename': - $c_disp['filename'] = $value; - $c_type['name'] = $value; - break; - - case 'description': - $headers['Content-Description'] = $value; - break; - - case 'charset': - $c_type['charset'] = $value; - $c_disp['charset'] = $value; + case 'location': + $headers['Content-Location'] = $value; break; - case 'language': - $c_type['language'] = $value; - $c_disp['language'] = $value; + case 'body_file': + $this->body_file = $value; break; - case 'location': - $headers['Content-Location'] = $value; + case 'preamble': + $this->preamble = $value; break; - case 'body_file': - $this->_body_file = $value; + // for backward compatibility + case 'dfilename': + $params['filename'] = $value; break; } } // Default content-type - if (empty($c_type['type'])) { - $c_type['type'] = 'text/plain'; + if (empty($params['content_type'])) { + $params['content_type'] = 'text/plain'; } // Content-Type - if (!empty($c_type['type'])) { - $headers['Content-Type'] = $c_type['type']; - if (!empty($c_type['charset'])) { - $charset = "charset={$c_type['charset']}"; - // place charset parameter in the same line, if possible - if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) { - $headers['Content-Type'] .= '; '; - } else { - $headers['Content-Type'] .= ';' . $this->_eol . ' '; - } - $headers['Content-Type'] .= $charset; + $headers['Content-Type'] = $params['content_type']; + if (!empty($params['charset'])) { + $charset = "charset={$params['charset']}"; + // place charset parameter in the same line, if possible + if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) { + $headers['Content-Type'] .= '; '; + } else { + $headers['Content-Type'] .= ';' . $this->eol . ' '; } - if (!empty($c_type['name'])) { - $headers['Content-Type'] .= ';' . $this->_eol; - $headers['Content-Type'] .= $this->_buildHeaderParam( - 'name', $c_type['name'], - isset($c_type['charset']) ? $c_type['charset'] : 'US-ASCII', - isset($c_type['language']) ? $c_type['language'] : null, - isset($params['name_encoding']) ? $params['name_encoding'] : null - ); + $headers['Content-Type'] .= $charset; + + // Default headers charset + if (!isset($params['headers_charset'])) { + $params['headers_charset'] = $params['charset']; } } + // header values encoding parameters + $h_charset = !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII'; + $h_language = !empty($params['language']) ? $params['language'] : null; + $h_encoding = !empty($params['name_encoding']) ? $params['name_encoding'] : null; + + if (!empty($params['filename'])) { + $headers['Content-Type'] .= ';' . $this->eol; + $headers['Content-Type'] .= $this->buildHeaderParam( + 'name', $params['filename'], $h_charset, $h_language, $h_encoding + ); + } + // Content-Disposition - if (!empty($c_disp['disp'])) { - $headers['Content-Disposition'] = $c_disp['disp']; - if (!empty($c_disp['filename'])) { - $headers['Content-Disposition'] .= ';' . $this->_eol; - $headers['Content-Disposition'] .= $this->_buildHeaderParam( - 'filename', $c_disp['filename'], - isset($c_disp['charset']) ? $c_disp['charset'] : 'US-ASCII', - isset($c_disp['language']) ? $c_disp['language'] : null, - isset($params['filename_encoding']) ? $params['filename_encoding'] : null + if (!empty($params['disposition'])) { + $headers['Content-Disposition'] = $params['disposition']; + if (!empty($params['filename'])) { + $headers['Content-Disposition'] .= ';' . $this->eol; + $headers['Content-Disposition'] .= $this->buildHeaderParam( + 'filename', $params['filename'], $h_charset, $h_language, + !empty($params['filename_encoding']) ? $params['filename_encoding'] : null ); } + + // add attachment size + $size = $this->body_file ? filesize($this->body_file) : strlen($body); + if ($size) { + $headers['Content-Disposition'] .= ';' . $this->eol . ' size=' . $size; + } } - if (!empty($headers['Content-Description'])) { + if (!empty($params['description'])) { $headers['Content-Description'] = $this->encodeHeader( - 'Content-Description', $headers['Content-Description'], - isset($c_type['charset']) ? $c_type['charset'] : 'US-ASCII', - isset($params['name_encoding']) ? $params['name_encoding'] : 'quoted-printable', - $this->_eol + 'Content-Description', $params['description'], $h_charset, $h_encoding, + $this->eol ); } + // Search and add existing headers' parameters + foreach ($headers as $key => $value) { + $items = explode(':', $key); + if (count($items) == 2) { + $header = $items[0]; + $param = $items[1]; + if (isset($headers[$header])) { + $headers[$header] .= ';' . $this->eol; + } + $headers[$header] .= $this->buildHeaderParam( + $param, $value, $h_charset, $h_language, $h_encoding + ); + unset($headers[$key]); + } + } + // Default encoding - if (!isset($this->_encoding)) { - $this->_encoding = '7bit'; + if (!isset($this->encoding)) { + $this->encoding = '7bit'; } // Assign stuff to member variables - $this->_encoded = array(); - $this->_headers = $headers; - $this->_body = $body; + $this->encoded = array(); + $this->headers = $headers; + $this->body = $body; } /** @@ -287,24 +306,27 @@ class Mail_mimePart * @return An associative array containing two elements, * body and headers. The headers element is itself * an indexed array. On error returns PEAR error object. - * @access public */ - function encode($boundary=null) + public function encode($boundary=null) { - $encoded =& $this->_encoded; + $encoded =& $this->encoded; - if (count($this->_subparts)) { + if (count($this->subparts)) { $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); - $eol = $this->_eol; + $eol = $this->eol; - $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + $this->headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; $encoded['body'] = ''; - for ($i = 0; $i < count($this->_subparts); $i++) { + if ($this->preamble) { + $encoded['body'] .= $this->preamble . $eol . $eol; + } + + for ($i = 0; $i < count($this->subparts); $i++) { $encoded['body'] .= '--' . $boundary . $eol; - $tmp = $this->_subparts[$i]->encode(); - if (PEAR::isError($tmp)) { + $tmp = $this->subparts[$i]->encode(); + if (is_a($tmp, 'PEAR_Error')) { return $tmp; } foreach ($tmp['headers'] as $key => $value) { @@ -314,20 +336,19 @@ class Mail_mimePart } $encoded['body'] .= '--' . $boundary . '--' . $eol; - - } else if ($this->_body) { - $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding); - } else if ($this->_body_file) { + } else if ($this->body) { + $encoded['body'] = $this->getEncodedData($this->body, $this->encoding); + } else if ($this->body_file) { // Temporarily reset magic_quotes_runtime for file reads and writes if ($magic_quote_setting = get_magic_quotes_runtime()) { @ini_set('magic_quotes_runtime', 0); } - $body = $this->_getEncodedDataFromFile($this->_body_file, $this->_encoding); + $body = $this->getEncodedDataFromFile($this->body_file, $this->encoding); if ($magic_quote_setting) { @ini_set('magic_quotes_runtime', $magic_quote_setting); } - if (PEAR::isError($body)) { + if (is_a($body, 'PEAR_Error')) { return $body; } $encoded['body'] = $body; @@ -336,34 +357,38 @@ class Mail_mimePart } // Add headers to $encoded - $encoded['headers'] =& $this->_headers; + $encoded['headers'] =& $this->headers; return $encoded; } /** - * Encodes and saves the email into file. File must exist. - * Data will be appended to the file. + * Encodes and saves the email into file or stream. + * Data will be appended to the file/stream. * - * @param string $filename Output file location + * @param mixed $filename Existing file location + * or file pointer resource * @param string $boundary Pre-defined boundary string * @param boolean $skip_head True if you don't want to save headers * * @return array An associative array containing message headers * or PEAR error object - * @access public - * @since 1.6.0 + * @since 1.6.0 */ - function encodeToFile($filename, $boundary=null, $skip_head=false) + public function encodeToFile($filename, $boundary = null, $skip_head = false) { - if (file_exists($filename) && !is_writable($filename)) { - $err = PEAR::raiseError('File is not writeable: ' . $filename); - return $err; - } + if (!is_resource($filename)) { + if (file_exists($filename) && !is_writable($filename)) { + $err = self::raiseError('File is not writeable: ' . $filename); + return $err; + } - if (!($fh = fopen($filename, 'ab'))) { - $err = PEAR::raiseError('Unable to open file: ' . $filename); - return $err; + if (!($fh = fopen($filename, 'ab'))) { + $err = self::raiseError('Unable to open file: ' . $filename); + return $err; + } + } else { + $fh = $filename; } // Temporarily reset magic_quotes_runtime for file reads and writes @@ -371,15 +396,17 @@ class Mail_mimePart @ini_set('magic_quotes_runtime', 0); } - $res = $this->_encodePartToFile($fh, $boundary, $skip_head); + $res = $this->encodePartToFile($fh, $boundary, $skip_head); - fclose($fh); + if (!is_resource($filename)) { + fclose($fh); + } if ($magic_quote_setting) { @ini_set('magic_quotes_runtime', $magic_quote_setting); } - return PEAR::isError($res) ? $res : $this->_headers; + return is_a($res, 'PEAR_Error') ? $res : $this->headers; } /** @@ -390,19 +417,18 @@ class Mail_mimePart * @param boolean $skip_head True if you don't want to save headers * * @return array True on sucess or PEAR error object - * @access private */ - function _encodePartToFile($fh, $boundary=null, $skip_head=false) + protected function encodePartToFile($fh, $boundary = null, $skip_head = false) { - $eol = $this->_eol; + $eol = $this->eol; - if (count($this->_subparts)) { + if (count($this->subparts)) { $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); - $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + $this->headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; } if (!$skip_head) { - foreach ($this->_headers as $key => $value) { + foreach ($this->headers as $key => $value) { fwrite($fh, $key . ': ' . $value . $eol); } $f_eol = $eol; @@ -410,26 +436,31 @@ class Mail_mimePart $f_eol = ''; } - if (count($this->_subparts)) { - for ($i = 0; $i < count($this->_subparts); $i++) { + if (count($this->subparts)) { + if ($this->preamble) { + fwrite($fh, $f_eol . $this->preamble . $eol); + $f_eol = $eol; + } + + for ($i = 0; $i < count($this->subparts); $i++) { fwrite($fh, $f_eol . '--' . $boundary . $eol); - $res = $this->_subparts[$i]->_encodePartToFile($fh); - if (PEAR::isError($res)) { + $res = $this->subparts[$i]->encodePartToFile($fh); + if (is_a($res, 'PEAR_Error')) { return $res; } $f_eol = $eol; } fwrite($fh, $eol . '--' . $boundary . '--' . $eol); - - } else if ($this->_body) { - fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding)); - } else if ($this->_body_file) { + } else if ($this->body) { + fwrite($fh, $f_eol); + fwrite($fh, $this->getEncodedData($this->body, $this->encoding)); + } else if ($this->body_file) { fwrite($fh, $f_eol); - $res = $this->_getEncodedDataFromFile( - $this->_body_file, $this->_encoding, $fh + $res = $this->getEncodedDataFromFile( + $this->body_file, $this->encoding, $fh ); - if (PEAR::isError($res)) { + if (is_a($res, 'PEAR_Error')) { return $res; } } @@ -441,20 +472,23 @@ class Mail_mimePart * Adds a subpart to current mime part and returns * a reference to it * - * @param string $body The body of the subpart, if any. - * @param array $params The parameters for the subpart, same - * as the $params argument for constructor. + * @param mixed $body The body of the subpart or Mail_mimePart object + * @param array $params The parameters for the subpart, same + * as the $params argument for constructor * - * @return Mail_mimePart A reference to the part you just added. It is - * crucial if using multipart/* in your subparts that - * you use =& in your script when calling this function, - * otherwise you will not be able to add further subparts. - * @access public + * @return Mail_mimePart A reference to the part you just added. */ - function &addSubpart($body, $params) + public function addSubpart($body, $params = null) { - $this->_subparts[] = new Mail_mimePart($body, $params); - return $this->_subparts[count($this->_subparts) - 1]; + if ($body instanceof Mail_mimePart) { + $part = $body; + } else { + $part = new Mail_mimePart($body, $params); + } + + $this->subparts[] = $part; + + return $part; } /** @@ -464,18 +498,17 @@ class Mail_mimePart * @param string $encoding The encoding type to use, 7bit, base64, * or quoted-printable. * - * @return string - * @access private + * @return string Encoded data string */ - function _getEncodedData($data, $encoding) + protected function getEncodedData($data, $encoding) { switch ($encoding) { case 'quoted-printable': - return $this->_quotedPrintableEncode($data); + return self::quotedPrintableEncode($data, 76, $this->eol); break; case 'base64': - return rtrim(chunk_split(base64_encode($data), 76, $this->_eol)); + return rtrim(chunk_split(base64_encode($data), 76, $this->eol)); break; case '8bit': @@ -495,17 +528,16 @@ class Mail_mimePart * stored into it instead of returning it * * @return string Encoded data or PEAR error object - * @access private */ - function _getEncodedDataFromFile($filename, $encoding, $fh=null) + protected function getEncodedDataFromFile($filename, $encoding, $fh = null) { if (!is_readable($filename)) { - $err = PEAR::raiseError('Unable to read file: ' . $filename); + $err = self::raiseError('Unable to read file: ' . $filename); return $err; } if (!($fd = fopen($filename, 'rb'))) { - $err = PEAR::raiseError('Could not open file: ' . $filename); + $err = self::raiseError('Could not open file: ' . $filename); return $err; } @@ -514,7 +546,7 @@ class Mail_mimePart switch ($encoding) { case 'quoted-printable': while (!feof($fd)) { - $buffer = $this->_quotedPrintableEncode(fgets($fd)); + $buffer = self::quotedPrintableEncode(fgets($fd), 76, $this->eol); if ($fh) { fwrite($fh, $buffer); } else { @@ -530,7 +562,7 @@ class Mail_mimePart // because base64 encoding is memory expensive $buffer = fread($fd, 57 * 9198); // ca. 0.5 MB $buffer = base64_encode($buffer); - $buffer = chunk_split($buffer, 76, $this->_eol); + $buffer = chunk_split($buffer, 76, $this->eol); if (feof($fd)) { $buffer = rtrim($buffer); } @@ -569,14 +601,12 @@ class Mail_mimePart * @param string $input The data to encode * @param int $line_max Optional max line length. Should * not be more than 76 chars + * @param string $eol End-of-line sequence. Default: "\r\n" * * @return string Encoded data - * - * @access private */ - function _quotedPrintableEncode($input , $line_max = 76) + public static function quotedPrintableEncode($input , $line_max = 76, $eol = "\r\n") { - $eol = $this->_eol; /* // imap_8bit() is extremely fast, but doesn't handle properly some characters if (function_exists('imap_8bit') && $line_max == 76) { @@ -592,7 +622,7 @@ class Mail_mimePart $escape = '='; $output = ''; - while (list($idx, $line) = each($lines)) { + foreach ($lines as $idx => $line) { $newline = ''; $i = 0; @@ -626,18 +656,22 @@ class Mail_mimePart $output .= $newline . $escape . $eol; $newline = ''; } + $newline .= $char; } // end of for + $output .= $newline . $eol; unset($lines[$idx]); } + // Don't want last crlf $output = substr($output, 0, -1 * strlen($eol)); + return $output; } /** - * Encodes the paramater of a header. + * Encodes the parameter of a header. * * @param string $name The name of the header-parameter * @param string $value The value of the paramter @@ -648,17 +682,15 @@ class Mail_mimePart * @param int $maxLength The maximum length of a line. Defauls to 75 * * @return string - * - * @access private */ - function _buildHeaderParam($name, $value, $charset=null, $language=null, - $encoding=null, $maxLength=75 + protected function buildHeaderParam($name, $value, $charset = null, + $language = null, $encoding = null, $maxLength = 75 ) { // RFC 2045: // value needs encoding if contains non-ASCII chars or is longer than 78 chars if (!preg_match('#[^\x20-\x7E]#', $value)) { - $token_regexp = '#([^\x21,\x23-\x27,\x2A,\x2B,\x2D' - . ',\x2E,\x30-\x39,\x41-\x5A,\x5E-\x7E])#'; + $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D' + . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#'; if (!preg_match($token_regexp, $value)) { // token if (strlen($name) + strlen($value) + 3 <= $maxLength) { @@ -675,13 +707,13 @@ class Mail_mimePart // RFC2047: use quoted-printable/base64 encoding if ($encoding == 'quoted-printable' || $encoding == 'base64') { - return $this->_buildRFC2047Param($name, $value, $charset, $encoding); + return $this->buildRFC2047Param($name, $value, $charset, $encoding); } // RFC2231: $encValue = preg_replace_callback( - '/([^\x21,\x23,\x24,\x26,\x2B,\x2D,\x2E,\x30-\x39,\x41-\x5A,\x5E-\x7E])/', - array($this, '_encodeReplaceCallback'), $value + '/([^\x21\x23\x24\x26\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])/', + array($this, 'encodeReplaceCallback'), $value ); $value = "$charset'$language'$encValue"; @@ -709,7 +741,7 @@ class Mail_mimePart $headCount++; } - $headers = implode(';' . $this->_eol, $headers); + $headers = implode(';' . $this->eol, $headers); return $headers; } @@ -723,10 +755,9 @@ class Mail_mimePart * @param int $maxLength Encoded parameter max length. Default: 76 * * @return string Parameter line - * @access private */ - function _buildRFC2047Param($name, $value, $charset, - $encoding='quoted-printable', $maxLength=76 + protected function buildRFC2047Param($name, $value, $charset, + $encoding = 'quoted-printable', $maxLength = 76 ) { // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in // parameter of a MIME Content-Type or Content-Disposition field", @@ -748,7 +779,7 @@ class Mail_mimePart $_quote = substr($value, 0, $real_len); $value = substr($value, $real_len); - $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; + $quoted .= $prefix . $_quote . $suffix . $this->eol . ' '; $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' $len = strlen($value) + $add_len; } @@ -771,7 +802,7 @@ class Mail_mimePart $_quote = $matches[1]; } - $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; + $quoted .= $prefix . $_quote . $suffix . $this->eol . ' '; $value = substr($value, strlen($_quote)); $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' $len = strlen($value) + $add_len; @@ -792,18 +823,18 @@ class Mail_mimePart * @param string $encoding Encoding name (base64 or quoted-printable) * @param string $eol End-of-line sequence. Default: "\r\n" * - * @return string Encoded header data (without a name) - * @access public - * @since 1.6.1 + * @return string Encoded header data (without a name) + * @since 1.6.1 */ - static function encodeHeader($name, $value, $charset='ISO-8859-1', - $encoding='quoted-printable', $eol="\r\n" + public static function encodeHeader($name, $value, $charset = 'ISO-8859-1', + $encoding = 'quoted-printable', $eol = "\r\n" ) { // Structured headers $comma_headers = array( 'from', 'to', 'cc', 'bcc', 'sender', 'reply-to', 'resent-from', 'resent-to', 'resent-cc', 'resent-bcc', 'resent-sender', 'resent-reply-to', + 'mail-reply-to', 'mail-followup-to', 'return-receipt-to', 'disposition-notification-to', ); $other_headers = array( @@ -822,9 +853,20 @@ class Mail_mimePart $charset = 'ISO-8859-1'; } + // exploding quoted strings as well as some regexes below do not + // work properly with some charset e.g. ISO-2022-JP, we'll use UTF-8 + $mb = $charset != 'UTF-8' && function_exists('mb_convert_encoding'); + // Structured header (make sure addr-spec inside is not encoded) if (!empty($separator)) { - $parts = Mail_mimePart::_explodeQuotedString($separator, $value); + // Simple e-mail address regexp + $email_regexp = '([^\s<]+|("[^\r\n"]+"))@\S+'; + + if ($mb) { + $value = mb_convert_encoding($value, 'UTF-8', $charset); + } + + $parts = Mail_mimePart::explodeQuotedString("[\t$separator]", $value); $value = ''; foreach ($parts as $part) { @@ -835,40 +877,44 @@ class Mail_mimePart continue; } if ($value) { - $value .= $separator==',' ? $separator.' ' : ' '; + $value .= $separator == ',' ? $separator . ' ' : ' '; } else { $value = $name . ': '; } // let's find phrase (name) and/or addr-spec - if (preg_match('/^<\S+@\S+>$/', $part)) { + if (preg_match('/^<' . $email_regexp . '>$/', $part)) { $value .= $part; - } else if (preg_match('/^\S+@\S+$/', $part)) { + } else if (preg_match('/^' . $email_regexp . '$/', $part)) { // address without brackets and without name $value .= $part; - } else if (preg_match('/<*\S+@\S+>*$/', $part, $matches)) { + } else if (preg_match('/<*' . $email_regexp . '>*$/', $part, $matches)) { // address with name (handle name) $address = $matches[0]; - $word = str_replace($address, '', $part); - $word = trim($word); + $word = str_replace($address, '', $part); + $word = trim($word); + // check if phrase requires quoting if ($word) { // non-ASCII: require encoding - if (preg_match('#([\x80-\xFF]){1}#', $word)) { + if (preg_match('#([^\s\x21-\x7E]){1}#', $word)) { if ($word[0] == '"' && $word[strlen($word)-1] == '"') { // de-quote quoted-string, encoding changes // string to atom - $search = array("\\\"", "\\\\"); - $replace = array("\"", "\\"); - $word = str_replace($search, $replace, $word); $word = substr($word, 1, -1); + $word = preg_replace('/\\\\([\\\\"])/', '$1', $word); + } + if ($mb) { + $word = mb_convert_encoding($word, $charset, 'UTF-8'); } + // find length of last line if (($pos = strrpos($value, $eol)) !== false) { $last_len = strlen($value) - $pos; } else { $last_len = strlen($value); } + $word = Mail_mimePart::encodeHeaderValue( $word, $charset, $encoding, $last_len, $eol ); @@ -879,8 +925,12 @@ class Mail_mimePart $word = '"'.addcslashes($word, '\\"').'"'; } } + $value .= $word.' '.$address; } else { + if ($mb) { + $part = mb_convert_encoding($part, $charset, 'UTF-8'); + } // addr-spec not found, don't encode (?) $value .= $part; } @@ -893,27 +943,31 @@ class Mail_mimePart $value = preg_replace( '/^'.$name.':('.preg_quote($eol, '/').')* /', '', $value ); - } else { // Unstructured header // non-ASCII: require encoding - if (preg_match('#([\x80-\xFF]){1}#', $value)) { + if (preg_match('#([^\s\x21-\x7E]){1}#', $value)) { if ($value[0] == '"' && $value[strlen($value)-1] == '"') { + if ($mb) { + $value = mb_convert_encoding($value, 'UTF-8', $charset); + } // de-quote quoted-string, encoding changes // string to atom - $search = array("\\\"", "\\\\"); - $replace = array("\"", "\\"); - $value = str_replace($search, $replace, $value); $value = substr($value, 1, -1); + $value = preg_replace('/\\\\([\\\\"])/', '$1', $value); + if ($mb) { + $value = mb_convert_encoding($value, $charset, 'UTF-8'); + } } + $value = Mail_mimePart::encodeHeaderValue( $value, $charset, $encoding, strlen($name) + 2, $eol ); } else if (strlen($name.': '.$value) > 78) { // ASCII: check if header line isn't too long and use folding $value = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $value); - $tmp = wordwrap($name.': '.$value, 78, $eol . ' '); - $value = preg_replace('/^'.$name.':\s*/', '', $tmp); + $tmp = wordwrap($name . ': ' . $value, 78, $eol . ' '); + $value = preg_replace('/^' . $name . ':\s*/', '', $tmp); // hard limit 998 (RFC2822) $value = wordwrap($value, 998, $eol . ' ', true); } @@ -928,25 +982,26 @@ class Mail_mimePart * @param string $delimiter Delimiter expression string for preg_match() * @param string $string Input string * - * @return array String tokens array - * @access private + * @return array String tokens array */ - static function _explodeQuotedString($delimiter, $string) + protected static function explodeQuotedString($delimiter, $string) { $result = array(); $strlen = strlen($string); + $quoted_string = '"(?:[^"\\\\]|\\\\.)*"'; - for ($q=$p=$i=0; $i < $strlen; $i++) { - if ($string[$i] == "\"" - && (empty($string[$i-1]) || $string[$i-1] != "\\") - ) { - $q = $q ? false : true; - } else if (!$q && preg_match("/$delimiter/", $string[$i])) { + for ($p=$i=0; $i < $strlen; $i++) { + if ($string[$i] === '"') { + $r = preg_match("/$quoted_string/", $string, $matches, 0, $i); + if (!$r || empty($matches[0])) { + break; + } + $i += strlen($matches[0]) - 1; + } else if (preg_match("/$delimiter/", $string[$i])) { $result[] = substr($string, $p, $i - $p); $p = $i + 1; } } - $result[] = substr($string, $p); return $result; } @@ -960,11 +1015,10 @@ class Mail_mimePart * @param int $prefix_len Prefix length. Default: 0 * @param string $eol End-of-line sequence. Default: "\r\n" * - * @return string Encoded header data - * @access public - * @since 1.6.1 + * @return string Encoded header data + * @since 1.6.1 */ - static function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $eol="\r\n") + public static function encodeHeaderValue($value, $charset, $encoding, $prefix_len = 0, $eol = "\r\n") { // #17311: Use multibyte aware method (requires mbstring extension) if ($result = Mail_mimePart::encodeMB($value, $charset, $encoding, $prefix_len, $eol)) { @@ -998,7 +1052,7 @@ class Mail_mimePart $value = substr($value, $cutpoint); $cutpoint = $maxLength; // RFC 2047 specifies that any split header should - // be seperated by a CRLF SPACE. + // be separated by a CRLF SPACE. if ($output) { $output .= $eol . ' '; } @@ -1040,7 +1094,7 @@ class Mail_mimePart } // RFC 2047 specifies that any split header should - // be seperated by a CRLF SPACE + // be separated by a CRLF SPACE if ($output) { $output .= $eol . ' '; } @@ -1060,11 +1114,10 @@ class Mail_mimePart * * @param string $str String to encode * - * @return string Encoded string - * @access public - * @since 1.6.0 + * @return string Encoded string + * @since 1.6.0 */ - function encodeQP($str) + public static function encodeQP($str) { // Bug #17226 RFC 2047 restricts some characters // if the word is inside a phrase, permitted chars are only: @@ -1073,7 +1126,7 @@ class Mail_mimePart // "=", "_", "?" must be encoded $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; $str = preg_replace_callback( - $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $str + $regexp, array('Mail_mimePart', 'qpReplaceCallback'), $str ); return str_replace(' ', '_', $str); @@ -1090,11 +1143,10 @@ class Mail_mimePart * @param int $prefix_len Prefix length. Default: 0 * @param string $eol End-of-line sequence. Default: "\r\n" * - * @return string Encoded string - * @access public - * @since 1.8.0 + * @return string Encoded string + * @since 1.8.0 */ - static function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n") + public static function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n") { if (!function_exists('mb_substr') || !function_exists('mb_strlen')) { return; @@ -1158,7 +1210,7 @@ class Mail_mimePart $char_len = 1; } else { $char = preg_replace_callback( - $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $char + $regexp, array('Mail_mimePart', 'qpReplaceCallback'), $char ); $char_len = strlen($char); } @@ -1189,10 +1241,9 @@ class Mail_mimePart * * @param array $matches Preg_replace's matches array * - * @return string Encoded character string - * @access private + * @return string Encoded character string */ - static function _qpReplaceCallback($matches) + protected static function qpReplaceCallback($matches) { return sprintf('=%02X', ord($matches[1])); } @@ -1203,12 +1254,23 @@ class Mail_mimePart * * @param array $matches Preg_replace's matches array * - * @return string Encoded character string - * @access private + * @return string Encoded character string */ - static function _encodeReplaceCallback($matches) + protected static function encodeReplaceCallback($matches) { return sprintf('%%%02X', ord($matches[1])); } -} // End of class + /** + * PEAR::raiseError implementation + * + * @param string $message A text error message + * + * @return PEAR_Error Instance of PEAR_Error + */ + public static function raiseError($message) + { + // PEAR::raiseError() is not PHP 5.4 compatible + return new PEAR_Error($message); + } +} diff --git a/civicrm/release-notes.md b/civicrm/release-notes.md index b9971abd35814d7b63d85915c8cee75e54f880f4..f32d8821676e367f109cb0b28b29818ecf96cba3 100644 --- a/civicrm/release-notes.md +++ b/civicrm/release-notes.md @@ -14,6 +14,36 @@ Other resources for identifying changes are: * https://github.com/civicrm/civicrm-joomla * https://github.com/civicrm/civicrm-wordpress +## CiviCRM 5.4.0 + +Released August 1, 2018 + +- **[Synopsis](release-notes/5.4.0.md#synopsis)** +- **[Features](release-notes/5.4.0.md#features)** +- **[Bugs resolved](release-notes/5.4.0.md#bugs)** +- **[Miscellany](release-notes/5.4.0.md#misc)** +- **[Credits](release-notes/5.4.0.md#credits)** +- **[Feedback](release-notes/5.4.0.md#feedback)** + +## CiviCRM 5.3.2 + +Released July 25, 2018 + +- **[Synopsis](release-notes/5.3.2.md#synopsis)** +- **[Bugs resolved](release-notes/5.3.2.md#bugs)** +- **[Credits](release-notes/5.3.2.md#credits)** +- **[Feedback](release-notes/5.3.2.md#feedback)** + +## CiviCRM 5.3.1 + +Released July 18, 2018 + +- **[Security advisories](release-notes/5.3.1.md#security)** +- **[Features](release-notes/5.3.1.md#features)** +- **[Bugs resolved](release-notes/5.3.1.md#bugs)** +- **[Miscellany](release-notes/5.3.1.md#misc)** +- **[Credits](release-notes/5.3.1.md#credits)** + ## CiviCRM 5.3.0 Released July 3, 2018 diff --git a/civicrm/release-notes/5.4.0.md b/civicrm/release-notes/5.4.0.md new file mode 100644 index 0000000000000000000000000000000000000000..c24fe772918dcfdd7e9a3f9dfa1375755d8709bc --- /dev/null +++ b/civicrm/release-notes/5.4.0.md @@ -0,0 +1,667 @@ +# CiviCRM 5.4.0 + +Released August 1, 2018 + +- **[Synopsis](#synopsis)** +- **[Features](#features)** +- **[Bugs resolved](#bugs)** +- **[Miscellany](#misc)** +- **[Credits](#credits)** +- **[Feedback](#feedback)** + +## <a name="synopsis"></a>Synopsis + +| *Does this version...?* | | +|:--------------------------------------------------------------- |:-------:| +| Fix security vulnerabilities? | no | +| **Change the database schema?** | **yes** | +| **Alter the API?** | **yes** | +| **Require attention to configuration options?** | **yes** | +| **Fix problems installing or upgrading to a previous version?** | **yes** | +| **Introduce features?** | **yes** | +| **Fix bugs?** | **yes** | + +## <a name="features"></a>Features + +### Core CiviCRM + +- **[CRM-20430](https://issues.civicrm.org/jira/browse/CRM-20430) A permission + 'save Report Criteria' + ([12107](https://github.com/civicrm/civicrm-core/pull/12107))** + + Permission to edit the criteria for saved reports is now separate from the + permission to access and preview modifications of report criteria. This will + prevent non-privileged users from modifying the criteria of an organization's + standard reports. However when upgrading, site administrators will need to + grant the Save Report Criteria permission to roles that need that capability. + +- **[dev/core#174](https://lab.civicrm.org/dev/core/issues/174) Consistently use + swappable cache interfaces + ([12321](https://github.com/civicrm/civicrm-core/pull/12321), + [12322](https://github.com/civicrm/civicrm-core/pull/12322), + [12323](https://github.com/civicrm/civicrm-core/pull/12323), + [12330](https://github.com/civicrm/civicrm-core/pull/12330), + [12331](https://github.com/civicrm/civicrm-core/pull/12331), + [12336](https://github.com/civicrm/civicrm-core/pull/12336), + [12342](https://github.com/civicrm/civicrm-core/pull/12342), + [12348](https://github.com/civicrm/civicrm-core/pull/12348), + [12354](https://github.com/civicrm/civicrm-core/pull/12354), + [12362](https://github.com/civicrm/civicrm-core/pull/12362), + [12368](https://github.com/civicrm/civicrm-core/pull/12368), + [12376](https://github.com/civicrm/civicrm-core/pull/12376), + [12378](https://github.com/civicrm/civicrm-core/pull/12378), + [12379](https://github.com/civicrm/civicrm-core/pull/12379), + [12380](https://github.com/civicrm/civicrm-core/pull/12380), + [12381](https://github.com/civicrm/civicrm-core/pull/12381), + [12389](https://github.com/civicrm/civicrm-core/pull/12389), and + [215](https://github.com/civicrm/civicrm-packages/pull/215))** + + Sites using Redis or Memcache for caches and sessions will now use those + facilities exclusively rather than retaining the `civicrm_cache` table as + a backstop. This improves performance for these sites, especially those + using multiple MySQL servers. + + As a precaution, Redis or Memcache administrators with very active sites + may wish to double-check that they have ample capacity for their caches and + sessions. + + For developers, the `CRM_Utils_Cache` APIs now support expiration/TTL + times and [PSR-16 CacheInterface](https://www.php-fig.org/psr/psr-16/). + The Civi implementation is slightly more permissive than the PSR-16 + specification; for full compliance, enable `CIVICRM_PSR16_STRICT`. + +- **[CRM-21574](https://issues.civicrm.org/jira/browse/CRM-21574) Allow to + disable sending of email from source contact for tell a friend on pcp + ([12355](https://github.com/civicrm/civicrm-core/pull/12355)) (preliminary + work)** + + Emails from the "tell a friend" feature are sent as if they are from the + donor's address. This can cause delivery failures as the site may not be a + valid source for the donor's email domain. Changes here are preliminary + cleanup. + +- **[dev/core#32](https://lab.civicrm.org/dev/core/issues/32) Add option to + disable cancel button on a profile form + ([11883](https://github.com/civicrm/civicrm-core/pull/11883))** + + The Cancel button is now optional on profile forms. + +- **[CRM-21392](https://issues.civicrm.org/jira/browse/CRM-21392) Allow viewing + groups/smartgroups as as other entities (eg. memberships, events) + ([11241](https://github.com/civicrm/civicrm-core/pull/11241))** + + The Find Groups page now has a similar "View Results As" feature as the + Advanced Search: you can choose to view memberships, events, activities, etc. + of group subscribers rather than the contacts themselves. + +- **[CRM-21622](https://issues.civicrm.org/jira/browse/CRM-21622) Make the Add + Parent Groups on Group Settings be a multiselect + ([11481](https://github.com/civicrm/civicrm-core/pull/11481))** + + The New Group form now allows you to set multiple parent groups for a single + group. + +- **[CRM-21460](https://issues.civicrm.org/jira/browse/CRM-21460) Programmatic + access to job execution status + ([11337](https://github.com/civicrm/civicrm-core/pull/11337))** + + Extension developers can implement two new hooks, `hook_civicrm_preJob` and + `hook_civicrm_postJob`, which are invoked before and after executing a + scheduled job. + +- **Change default for `CIVICRM_CONTAINER_CACHE` to simplify admin/developer + experience ([12426](https://github.com/civicrm/civicrm-core/pull/12426))** + +- **[dev/core#223](https://lab.civicrm.org/dev/core/issues/223) Expose prefix + and suffix in reports + ([12394](https://github.com/civicrm/civicrm-core/pull/12394))** + + Individuals' prefix and suffix fields are now available as columns and filters + among the contact fields in most reports. + +- **[dev/core#101](https://lab.civicrm.org/dev/core/issues/101) Allow further + customization of search form in hooks + ([12078](https://github.com/civicrm/civicrm-core/pull/12078))** + + Fields are no longer explicitly included in the search form template since + that makes it hard to add or remove fields. Instead, the fields are listed in + template variables, and the template iterates over the list to render the + field elements. + +- **[dev/core#183](https://lab.civicrm.org/dev/core/issues/183) Temporary tables + should follow consistent naming convention + ([12311](https://github.com/civicrm/civicrm-core/pull/12311)) (partially + completed)** + + This moves towards naming temporary tables--both true temporary tables in + MySQL and regular tables created for temporary use--in a consistent manner in + order to help database administrators employ replication policies based upon + table names. + +- **[dev/core#144](https://lab.civicrm.org/dev/core/issues/144) + `getCustomFieldID` switch to API, add caching, add full string return option + ([12218](https://github.com/civicrm/civicrm-core/pull/12218) and + [12283](https://github.com/civicrm/civicrm-core/pull/12283))** + + The function to retrieve a custom field ID now relies upon APIv3 rather than + an *ad hoc* SQL query. It also contains an option to return the ID preceded + by `custom_`. Finally, the function caches the result and attempts to return + the cached value rather than looking it up each time. + +- **[dev/core#47](https://lab.civicrm.org/dev/core/issues/47) Add "clone" + functionality to scheduled jobs + ([11945](https://github.com/civicrm/civicrm-core/pull/11945))** + + Scheduled jobs can now be copied, including their parameters, both from the + Scheduled Jobs page and through a new `Job.clone` API method. + +### CiviCase + +- **[dev/core#107](https://lab.civicrm.org/dev/core/issues/107) After creating a + new case, the assignee for each activity must be selected manually + ([11998](https://github.com/civicrm/civicrm-core/pull/11998))** + + You can now select the default assignee for each activity in the standard + timeline for a case type. This may be defined according to a relationship + with the client, a single specific contact, or the contact who created the + case. + +- **CiviCase, added reference of activities from different timelines. + ([12236](https://github.com/civicrm/civicrm-core/pull/12236))** + + From an activity on a case timeline, you can now reference any activity + available to the case--not just those on that case timeline. + +### CiviContribute + +- **Add support for hidden fields in general (and 'token' specifically) on the + payment form. ([12332](https://github.com/civicrm/civicrm-core/pull/12332) + and [12391](https://github.com/civicrm/civicrm-core/pull/12391))** + + A new authorization token field is added to the payment form to support + token-based payment processors. + +- **[CRM-21808](https://issues.civicrm.org/jira/browse/CRM-21808) Install custom + group for Contribution or ContributioRecur subtypes based on XML file + ([11726](https://github.com/civicrm/civicrm-core/pull/11726))** + + Extensions created custom data sets for contributions can now specify the + custom data to apply only to a single financial type. + +- **(dev/core/50) and + [dev/core#195](https://lab.civicrm.org/dev/core/issues/195) Add Separate + Sub-tabs for Contributions and Recurring Contributions + ([11956](https://github.com/civicrm/civicrm-core/pull/11956) and + [12345](https://github.com/civicrm/civicrm-core/pull/12345))** + + The Contributions tab on a contact now contains two sub-tabs--one for single + contributions, and the other for recurring contribution series--instead of + displaying one after the other. + +- **[dev/core#96](https://lab.civicrm.org/dev/core/issues/96) Expose source + column in booking report + ([12374](https://github.com/civicrm/civicrm-core/pull/12374))** + + The contribution Source field is now available as a column and filter on the + Bookkeeping Transactions report. + +- **Use html5 color input for contribution page widget + ([12339](https://github.com/civicrm/civicrm-core/pull/12339))** + +### CiviMail + +- **[dev/mail#1](https://lab.civicrm.org/dev/mail/issues/1) Add pause/resume + functionality to civicrm bulk mailing. + ([11803](https://github.com/civicrm/civicrm-core/pull/11803) and + [12284](https://github.com/civicrm/civicrm-core/pull/12284))** + + A CiviMail mailing that is scheduled or in progress can now be paused in order + to allow an urgent message to go out first. + +- **Add in bootstrap button classes to CiviMail interface + ([12013](https://github.com/civicrm/civicrm-core/pull/12013) and + [12338](https://github.com/civicrm/civicrm-core/pull/12338))** + + This allows the interfaces to be themed more easily by Bootstrap-based themes. + +## <a name="bugs"></a>Bugs resolved + +### Core CiviCRM + +- **[dev/core#222](https://lab.civicrm.org/dev/core/issues/222) Importing + contacts with dedupe rule does not use the rule + ([12393](https://github.com/civicrm/civicrm-core/pull/12393))** + +- **[dev/core#284](https://lab.civicrm.org/dev/core/issues/284) Aggressive cache + clearing significantly increases test time + ([12590](https://github.com/civicrm/civicrm-core/pull/12590))** + + Settings, compiled Javascript, extension, and other caches are now only + flushed along with the `default` CiviCRM cache if an external cache like Redis or + Memcache is in use. (This reproduces the `5.3` behavior.) + +- **[dev/core#279](https://lab.civicrm.org/dev/core/issues/279) When Merging two + contacts, Flip between original and duplicate link not working in WordPress + ([12568](https://github.com/civicrm/civicrm-core/pull/12568))** + +- **Handle 'Check for Matching Contact(s)' button with ajax + ([12552](https://github.com/civicrm/civicrm-core/pull/12552))** + + This resolves a bug where the "Check for Matching Contact(s)" button would not + work if AJAX Deduping is enabled. + +- **[dev/core#131](https://lab.civicrm.org/dev/core/issues/131) Add in missing + UK shire Monmouthshire + ([12538](https://github.com/civicrm/civicrm-core/pull/12538)) (follow-up on + previous work)** + + Many individual sites had manually added the county of Monmouthshire in Wales + to the `civicrm_state_province` table, and upgrading to 5.3.0, which added the + missing record, would cause them a database error. + +- **[dev/core#258](https://lab.civicrm.org/dev/core/issues/258) Possible + unreleased regression - message template update altered customised template + ([12492](https://github.com/civicrm/civicrm-core/pull/12492))** + +- **[dev/core#249](https://lab.civicrm.org/dev/core/issues/249) Contact Export + fails in 5.4 when trying to export using the All radio button + ([12447](https://github.com/civicrm/civicrm-core/pull/12447))** + +- **[dev/core#234](https://lab.civicrm.org/dev/core/issues/234) Upgrade Steps in + 5.3.alpha1 may not have been run + ([12419](https://github.com/civicrm/civicrm-core/pull/12419), + [12420](https://github.com/civicrm/civicrm-core/pull/12420), and + [12425](https://github.com/civicrm/civicrm-core/pull/12425))** + +- **[dev/core#155](https://lab.civicrm.org/dev/core/issues/155) Improvements and + bugfixes to Option Groups UI + ([12233](https://github.com/civicrm/civicrm-core/pull/12233) and + [12411](https://github.com/civicrm/civicrm-core/pull/12411))** + + The Option Groups page now displays whether each option group is reserved or + enabled. In addition, sites using sample data no longer have the sample + custom fields reserved. + +- **[CRM-20184](https://issues.civicrm.org/jira/browse/CRM-20184) Some System + Workflow templates still miss {contact.email_greeting} + ([12296](https://github.com/civicrm/civicrm-core/pull/12296))** + + This makes receipt templates more consistent in using the contact's email + greeting when sent to individuals. + +- **[dev/core#41](https://lab.civicrm.org/dev/core/issues/41) Search Builder: + Not empty with date or integer custom fields gives a sql warning + ([12351](https://github.com/civicrm/civicrm-core/pull/12351) and + [12363](https://github.com/civicrm/civicrm-core/pull/12363))** + +- **Make all form tasks inherit from `CRM_Core_Form_Task` + ([12318](https://github.com/civicrm/civicrm-core/pull/12318))** + + This addresses some export problems caused by tasks lacking certain properties + because their classes did not extend `CRM_Core_Form_Task`. + +- **[dev/core#178](https://lab.civicrm.org/dev/core/issues/178) Redis driver - + Error messages are invisible + ([12303](https://github.com/civicrm/civicrm-core/pull/12303))** + + The log now contains error messages passed from Redis. + +- **[dev/core#180](https://lab.civicrm.org/dev/core/issues/180) Deleting custom + values and custom_hook parameter `$entity_id` empty + ([12309](https://github.com/civicrm/civicrm-core/pull/12309))** + + The ID of the affected entity is now passed to `hook_civicrm_custom` when a + value is deleted from a custom field. + +- **Fix access to scheduled reminders for admins without event access + ([11733](https://github.com/civicrm/civicrm-core/pull/11733))** + + Users with Administer CiviCRM permission but lacking Manage Events permission + would get an error when trying to create a non-event scheduled reminder. + +- **Only treat a request as a get request (and hence use GET) if it starts with + get ([12308](https://github.com/civicrm/civicrm-core/pull/12308))** + +- **[dev/core#179](https://lab.civicrm.org/dev/core/issues/179) Redis driver - + Allow anonymous connections + ([12304](https://github.com/civicrm/civicrm-core/pull/12304))** + + Redis no longer attempts to authenticate when no password is set, such as on + local development environments. + +- **[dev/core#177](https://lab.civicrm.org/dev/core/issues/177) Redis driver - + Reports incorrect value for cache-miss + ([12302](https://github.com/civicrm/civicrm-core/pull/12302))** + + When a cache key is undefined, Redis now returns `NULL` rather than `FALSE`. + +- **[dev/core#175](https://lab.civicrm.org/dev/core/issues/175) Smart group + involving relationship type filter display incorrect results. + ([12301](https://github.com/civicrm/civicrm-core/pull/12301))** + +- **Make Send SMS permission independent of Edit Contact permission + ([12067](https://github.com/civicrm/civicrm-core/pull/12067))** + + You no longer need the permission to edit a contact (in addition to the Send + SMS permission) to send the contact a SMS message. + +- **[dev/core#134](https://lab.civicrm.org/dev/core/issues/134) Search Builder + broken filter for Source Contact ID + ([12181](https://github.com/civicrm/civicrm-core/pull/12181))** + +- **[dev/core#140](https://lab.civicrm.org/dev/core/issues/140) add missing + pseudoconstant for `option_group_id` in CustomField.xml + ([12195](https://github.com/civicrm/civicrm-core/pull/12195))** + + Extensions creating custom fields can now refer to option groups by name + rather than ID. + +- **[dev/accessiblity#3](https://lab.civicrm.org/dev/accessibility/issues/3) Add + aria-label (and label?) to form elements missing them + ([12209](https://github.com/civicrm/civicrm-core/pull/12209) and + [213](https://github.com/civicrm/civicrm-packages/pull/213)) (partial work)** + + Besides adding `aria-label` attributes to sub-elements of the Quickform + date field, event info pages no longer wrap basic information labels in + `<label>` tags. + +- **[CRM-20711](https://issues.civicrm.org/jira/browse/CRM-20711) Error - DB + Constraint Violation - GroupContact, get API + ([12408](https://github.com/civicrm/civicrm-core/pull/12408))** + + This adds the `location_id` field to the `civicrm_group_contact` table on + older sites that lack it. + +- **[dev/core#152](https://lab.civicrm.org/dev/core/issues/152) - + AdvMulti-Select cleanup + ([531](https://github.com/civicrm/civicrm-drupal/pull/531)) (follow-up work)** + + This removes a couple of references to the now-removed "Advanced Multi-select" + custom field type + +- **Upgrade Mime_mail to fix issues with PHP7.2 + ([205](https://github.com/civicrm/civicrm-packages/pull/205))** + +- **Apply fixes to have DB package support PHP7.2 + ([207](https://github.com/civicrm/civicrm-packages/pull/207))** + +- **[dev/core#117](https://lab.civicrm.org/dev/core/issues/117) Remove usage of + `each()` This is deprecated in php7.2 + ([211](https://github.com/civicrm/civicrm-packages/pull/211)) (follow-up + work)** + +- **Declare Subparts as array in mimePart to support PHP7.2 + ([210](https://github.com/civicrm/civicrm-packages/pull/210))** + +- **[CRM-19798](https://issues.civicrm.org/jira/browse/CRM-19798) Memory leak in + API3 EntityTag get operations + ([12276](https://github.com/civicrm/civicrm-core/pull/12276))** + +### CiviCase + +- **Fix regression on case export from recent export fix + ([12588](https://github.com/civicrm/civicrm-core/pull/12588))** + + This resolves undefined property PHP notices when exporting cases. + +- **[dev/core#24](https://lab.civicrm.org/dev/core/issues/24) Passing an array + for contact_id/client_id to Case.Create API when updating an existing case + causes case to be "reassigned" + ([11830](https://github.com/civicrm/civicrm-core/pull/11830))** + +- **[CRM-21815](https://issues.civicrm.org/jira/browse/CRM-21815) On re-opening + a civicase - Case Coordinator (and other roles) are not reinstated + ([11736](https://github.com/civicrm/civicrm-core/pull/11736))** + +- **[dev/core#165](https://lab.civicrm.org/dev/core/issues/165) Bad link in + civicrm/case/activity/view breadcrumb + ([12279](https://github.com/civicrm/civicrm-core/pull/12279))** + +### CiviContribute + +- **[CRM-21637](https://issues.civicrm.org/jira/browse/CRM-21637) Search + Criteria for Card Type ID and Card Number not respected in Batch + ([11495](https://github.com/civicrm/civicrm-core/pull/11495))** + + The card type and card number filters for financial transactions in the + accounting batch interface now work as expected. + +- **[CRM-21854](https://issues.civicrm.org/jira/browse/CRM-21854) Contribution + start date and end dates are not respected + ([11881](https://github.com/civicrm/civicrm-core/pull/11881) and + [12504](https://github.com/civicrm/civicrm-core/pull/12504))** + +- **[dev/core#264](https://lab.civicrm.org/dev/core/issues/264) Fatal Error on + editing Financial Transaction + ([12502](https://github.com/civicrm/civicrm-core/pull/12502))** + +- **[dev/core#220](https://lab.civicrm.org/dev/core/issues/220) State + province/country doesn't show properly in the report + ([12390](https://github.com/civicrm/civicrm-core/pull/12390))** + + This resolves a bug in the Contribution History by Relationship report where + strange values would appear for the State/Province and Country fields. + +- **[dev/core#202](https://lab.civicrm.org/dev/core/issues/202) Empty row under + currency drop down + ([12356](https://github.com/civicrm/civicrm-core/pull/12356))** + + A warning now appears when a site has a disabled currency that is set as + default. + +- **[dev/core#88](https://lab.civicrm.org/dev/core/issues/88) Make sure + `financial_type_id` is set when a contribution is created + ([11907](https://github.com/civicrm/civicrm-core/pull/11907))** + + This resolves a bug where the financial type would not be set on a one-time + contribution made through a contribution page. + +- **[dev/core#170](https://lab.civicrm.org/dev/core/issues/170) contribution + detail report errors when soft credits column is exposed + ([12281](https://github.com/civicrm/civicrm-core/pull/12281) and + [12282](https://github.com/civicrm/civicrm-core/pull/12282))** + +- **Fix e-notice, remove legacy code pattern + ([12298](https://github.com/civicrm/civicrm-core/pull/12298))** + + This resolves a PHP notice when printing contribution receipts in bulk. + +- **[dev/core#56](https://lab.civicrm.org/dev/core/issues/56) Cancel Recurring + Contribution activity should have a source record id + ([11964](https://github.com/civicrm/civicrm-core/pull/11964))** + + The activity created when cancelling a recurring contribution now has the + recurring contribution ID as the `source_record_id`. + +- **[dev/financial#14](https://lab.civicrm.org/dev/financial/issues/14) PayPal + Express recurring payment causes warning messages + ([12091](https://github.com/civicrm/civicrm-core/pull/12091))** + +- **[CRM-20697](https://issues.civicrm.org/jira/browse/CRM-20697) Online pay now + anomalies (contribution transfer to new contact) + ([11578](https://github.com/civicrm/civicrm-core/pull/11578))** + + The Pay Now form now uses contact ID and checksum to validate the + contribution's contact to avoid creating new contacts when an anonymous + visitor fulfills a pending contribution. + +### CiviEvent + +- **[dev/core#272](https://lab.civicrm.org/dev/core/issues/272) Fatal Error + (Regression) on PCP pages associated with Events + ([12533](https://github.com/civicrm/civicrm-core/pull/12533))** + +- **[dev/core#124](https://lab.civicrm.org/dev/core/issues/124) Registration + approval issues ([12160](https://github.com/civicrm/civicrm-core/pull/12160) + and [12417](https://github.com/civicrm/civicrm-core/pull/12417))** + + This resolves a PHP warning and some confusing aspects of handling event + registration approvals. + +### CiviGrant + +- **[dev/core#187](https://lab.civicrm.org/dev/core/issues/187) Fix typo in + `CRM_Grant_Form_Task` that prevents retrieving session key from URL + ([12317](https://github.com/civicrm/civicrm-core/pull/12317))** + +### CiviMail + +- **[dev/core#133](https://lab.civicrm.org/dev/core/issues/133) Reply-to field + with empty string get saved in DB as `NULL` + ([12176](https://github.com/civicrm/civicrm-core/pull/12176))** + +- **[CRM-20320](https://issues.civicrm.org/jira/browse/CRM-20320) Error in PEAR + SMTP causes mailing job to fail when an error occurs during one connection + ([11840](https://github.com/civicrm/civicrm-core/pull/11840))** + + CiviMail will now disconnect the SMTP connection when a connection error is + detected and record the group of messages as not delivered. This allows it to + reconnect later and send the messages. + +- **[CRM-17753](https://issues.civicrm.org/jira/browse/CRM-17753) Newsletter + confirmation emails shouldn't use do-not-reply@yourdomain.org + ([12270](https://github.com/civicrm/civicrm-core/pull/12270)) (preliminary + work)** + +- **[CRM-21779](https://issues.civicrm.org/jira/browse/CRM-21779) Civimail + allows adding current draft mailing recipients to recipients field + ([11724](https://github.com/civicrm/civicrm-core/pull/11724))** + +### CiviMember + +- **[dev/membership#4](https://lab.civicrm.org/dev/membership/issues/4) Admin + Membership type is displayed on Public contribution page. + ([12178](https://github.com/civicrm/civicrm-core/pull/12178))** + +- **[dev/core#276](https://lab.civicrm.org/dev/core/issues/276) Undefined index: + is_override on submit credit card membership + ([12563](https://github.com/civicrm/civicrm-core/pull/12563))** + +- **[dev/core#151](https://lab.civicrm.org/dev/core/issues/151) Action to Update + Recurring Contributions From Membership View is Never Shown + ([12228](https://github.com/civicrm/civicrm-core/pull/12228))** + + The table to view recurring contributions on a membership now displays the + Edit action where appropriate. + +- **[CRM-21682](https://issues.civicrm.org/jira/browse/CRM-21682) Automatic + membership renewal fixes + ([12313](https://github.com/civicrm/civicrm-core/pull/12313) and + [12314](https://github.com/civicrm/civicrm-core/pull/12314)) (preliminary + work)** + +- **[CRM-21177](https://issues.civicrm.org/jira/browse/CRM-21177) Wrong interval + of recurring payment for auto-renewing membership + ([12289](https://github.com/civicrm/civicrm-core/pull/12289)) (preliminary + work)** + +### Joomla Integration + +- **[dev/joomla#1](https://lab.civicrm.org/dev/joomla/issues/1) CiviEvent menu + listing showing old events + ([46](https://github.com/civicrm/civicrm-joomla/pull/46))** + +### WordPress Integration + +- **[dev/wordpress#1](https://lab.civicrm.org/dev/wordpress/issues/1) + CiviCRM-WordPress - Gutenberg Compatibility: Do not parse shortcodes in REST + context ([130](https://github.com/civicrm/civicrm-wordpress/pull/130))** + +- **[CRM-21565](https://issues.civicrm.org/jira/browse/CRM-21565) Change mkdir + to use correct and more secure mode numbers + ([120](https://github.com/civicrm/civicrm-wordpress/pull/120))** + +## <a name="misc"></a>Miscellany + +- **(NFC) Convert files to use linux line endings not windows + ([12531](https://github.com/civicrm/civicrm-core/pull/12531))** + +- **Move DAO function to DAO class, call it from Merge class + ([12340](https://github.com/civicrm/civicrm-core/pull/12340))** + +- **NFC code cleanup for AuthNet, Paypal, PaypalPro IPNs + ([12386](https://github.com/civicrm/civicrm-core/pull/12386))** + +- **[dev/core#203](https://lab.civicrm.org/dev/core/issues/203) Cruft code in + `CRM_Core_BAO_OptionGroup::add()` + ([12357](https://github.com/civicrm/civicrm-core/pull/12357))** + +- **Remove unused instances of usedTable + ([12341](https://github.com/civicrm/civicrm-core/pull/12341) and + [12349](https://github.com/civicrm/civicrm-core/pull/12349))** + +- **(NFC) CiviUnitTestCase - Fix edge-case for mis-reported error + ([12347](https://github.com/civicrm/civicrm-core/pull/12347))** + +- **NFC Code cleanup to core task class + ([12316](https://github.com/civicrm/civicrm-core/pull/12316))** + +- **[NFC] Stop passing no-longer-part-of-signature variable + ([12329](https://github.com/civicrm/civicrm-core/pull/12329))** + +- **Fix typo in api explorer sample code + ([12324](https://github.com/civicrm/civicrm-core/pull/12324))** + +- **Remove unused code variable + ([12299](https://github.com/civicrm/civicrm-core/pull/12299))** + +- **Confirm & lock in group.get handling of is_active + ([12295](https://github.com/civicrm/civicrm-core/pull/12295))** + +- **Remove unused function getRecuringTransactionStatus from PayflowPro c… + ([12294](https://github.com/civicrm/civicrm-core/pull/12294))** + +- **Add upgrade function for message templates that does not involve copying the + whole template ([12224](https://github.com/civicrm/civicrm-core/pull/12224))** + +- **Function extraction BAO_Export class + ([12288](https://github.com/civicrm/civicrm-core/pull/12288))** + +- **Partial refactor of completeMembershipFromContribution + ([12271](https://github.com/civicrm/civicrm-core/pull/12271))** + +- **[NFC] Fix line endings (Jenkins where were you) + ([12280](https://github.com/civicrm/civicrm-core/pull/12280))** + +- **Towards fixing household merge export, extract function, add test, fix prev + ([12272](https://github.com/civicrm/civicrm-core/pull/12272))** + +- **Standardise line endings away from windows line endings to linux + ([212](https://github.com/civicrm/civicrm-packages/pull/212))** + +## <a name="credits"></a>Credits + +This release was developed by the following code authors: + +AGH Strategies - Alice Frumin, Andrew Hunt; Agileware - Alok Patel; Australian +Greens - Seamus Lee; Bastien Ho; Caltha - Tomasz Pietrzkowski; CiviCoop - Jaap +Jansma; CiviCRM - Coleman Watts, Tim Otten; CiviDesk - Yashodha Chaku; CompuCorp - +Camilo Rodriguez, Michael Devery, René Olivo, Vinu Varshith Sekar; Electronic +Frontier Foundation - Mark Burdett; Fuzion - Jitendra Purohit; Ginkgo Street +Labs - Frank Gómez; JMA Consulting - Monish Deb; Johan Vervloet; John +Kingsnorth; Kanzu Code - Carl Andrew Lema; Left Join Labs - Sean Madsen; MJW +Consulting - Matthew Wire; Pradeep Nayak; Romain Thouvenin; Squiffle Consulting - +Aidan Saunders; Tadpole Collective - Kevin Cristiano; Third Sector Design - +Michael McAndrew; Tom Bloor; Wikimedia Foundation - Eileen McNaughton + +Most authors also reviewed code for this release; in addition, the following +reviewers contributed their comments: + +Agileware - Justin Freeman; baernm; Blackfly Solutions - Alan Dixon; Bob +Silvern; Borislav Zlatanov; Caltha - Michal Mach; cartbar; Christian Wach; +Circle Interactive - Dave Jenkins; CiviCoop - Erik Hommel; Community Builders - +Andrew Perry; CompuCorp - Guanhuan Chen, Omar Abu Hussein; Coop SymbioTIC - +Mathieu Lutfy; Drupal Association - Lizz Trudeau; Evan Chute; Forest CRM +Consulting - Tamar Meir; Freeform Solutions - Herb van den Dool; Ginkgo Street +Labs - Dan O'Brien; Hugo do Carmo; JMA Consulting - Joe Murray; Lemniscus - Noah +Miller; Lighthouse Design and Consulting - Brian Shaughnessy; Megaphone +Technology Consulting - Jon Goldberg; MJCO - Mikey O'Toole; Oxfam Germany - +Thomas Schüttler; Richard van Oosterhout; Robin Fenwick; Semper IT - Karin +Gerritsen; Stephen Palmstrom; Tech To The People - Xavier Dutoit; Timbsoft +Technologies - Tunbola Ogunwande + +## <a name="feedback"></a>Feedback + +These release notes are edited by Andrew Hunt. If you'd like to provide +feedback on them, please log in to https://chat.civicrm.org/civicrm and contact +`@agh1`. diff --git a/civicrm/settings/Core.setting.php b/civicrm/settings/Core.setting.php index b95baf8d0ed7727855a4dc598c80af245c55c8d1..c95ed3d01850e49bb91d0f9c9591ac644a194aae 100644 --- a/civicrm/settings/Core.setting.php +++ b/civicrm/settings/Core.setting.php @@ -883,21 +883,6 @@ return array( 'description' => NULL, 'help_text' => NULL, ), - 'systemStatusCheckResult' => array( - 'group_name' => 'CiviCRM Preferences', - 'group' => 'core', - 'name' => 'systemStatusCheckResult', - 'type' => 'Integer', - 'quick_form_type' => 'Element', - 'html_type' => 'text', - 'default' => 0, - 'add' => '4.7', - 'title' => 'systemStatusCheckResult', - 'is_domain' => 1, - 'is_contact' => 0, - 'description' => NULL, - 'help_text' => NULL, - ), 'recentItemsMaxCount' => array( 'group_name' => 'CiviCRM Preferences', 'group' => 'core', diff --git a/civicrm/sql/civicrm.mysql b/civicrm/sql/civicrm.mysql index 6e62877dd1d539c4b17244fe6af9847482af4d32..3702ee53c90c72c49f07a8abadc5f7679bb74215 100644 --- a/civicrm/sql/civicrm.mysql +++ b/civicrm/sql/civicrm.mysql @@ -3506,7 +3506,8 @@ CREATE TABLE `civicrm_uf_group` ( `created_date` datetime COMMENT 'Date and time this UF group was created.', `is_proximity_search` tinyint DEFAULT 0 COMMENT 'Should we include proximity search feature in this profile search form?', `cancel_button_text` varchar(64) DEFAULT NULL COMMENT 'Custom Text to display on the Cancel button when used in create or edit mode', - `submit_button_text` varchar(64) DEFAULT NULL COMMENT 'Custom Text to display on the submit button on profile edit/create screens' + `submit_button_text` varchar(64) DEFAULT NULL COMMENT 'Custom Text to display on the submit button on profile edit/create screens', + `add_cancel_button` tinyint DEFAULT 1 COMMENT 'Should a Cancel button be included in this Profile form.' , PRIMARY KEY (`id`) diff --git a/civicrm/sql/civicrm_data.mysql b/civicrm/sql/civicrm_data.mysql index 6a787382a21eb24b84ca6436c28b3b68a4bbea5e..07f1af5c9bcc67c04d8e82b6d0ad19773b9d7a60 100644 --- a/civicrm/sql/civicrm_data.mysql +++ b/civicrm/sql/civicrm_data.mysql @@ -7937,7 +7937,8 @@ INSERT INTO civicrm_msg_template ('Contributions - Receipt (on-line)', '{if $is_pay_later}{ts}Invoice{/ts}{else}{ts}Receipt{/ts}{/if} - {$title} -', '{if $receipt_text} +', '{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} +{if $receipt_text} {$receipt_text} {/if} {if $is_pay_later} @@ -8145,7 +8146,8 @@ INSERT INTO civicrm_msg_template {$customName}: {$customValue} {/if} {/foreach} -{/if}', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +{/if} +', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> @@ -8168,7 +8170,7 @@ INSERT INTO civicrm_msg_template <tr> <td> - + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $receipt_text} <p>{$receipt_text|htmlize}</p> {/if} @@ -8620,7 +8622,8 @@ INSERT INTO civicrm_msg_template </html> ', @tpl_ovid_contribution_online_receipt, 1, 0), ('Contributions - Receipt (on-line)', '{if $is_pay_later}{ts}Invoice{/ts}{else}{ts}Receipt{/ts}{/if} - {$title} -', '{if $receipt_text} +', '{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} +{if $receipt_text} {$receipt_text} {/if} {if $is_pay_later} @@ -8828,7 +8831,8 @@ INSERT INTO civicrm_msg_template {$customName}: {$customValue} {/if} {/foreach} -{/if}', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +{/if} +', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> @@ -8851,7 +8855,7 @@ INSERT INTO civicrm_msg_template <tr> <td> - + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $receipt_text} <p>{$receipt_text|htmlize}</p> {/if} @@ -14160,8 +14164,8 @@ INSERT INTO civicrm_msg_template ', @tpl_ovid_event_offline_receipt, 0, 1) , - ('Events - Registration Confirmation and Receipt (on-line)', '{if $isOnWaitlist}{ts}Wait List Confirmation{/ts}{else}{ts}Registration Confirmation{/ts}{/if} - {$event.event_title}', '{contact.email_greeting}, - + ('Events - Registration Confirmation and Receipt (on-line)', '{if $isOnWaitlist}{ts}Wait List Confirmation{/ts}{elseif $isRequireApproval}{ts}Registration Request Confirmation{/ts}{else}{ts}Registration Confirmation{/ts}{/if} - {$event.event_title} +', '{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} {if $event.confirm_email_text AND (not $isOnWaitlist AND not $isRequireApproval)} {$event.confirm_email_text} @@ -14263,7 +14267,7 @@ INSERT INTO civicrm_msg_template {if $payer.name} You were registered by: {$payer.name} {/if} -{if $event.is_monetary} {* This section for Paid events only.*} +{if $event.is_monetary and not $isRequireApproval} {* This section for Paid events only.*} ==========================================================={if $pricesetFieldsCount }===================={/if} @@ -14489,7 +14493,7 @@ You were registered by: {$payer.name} <tr> <td> - <p>{contact.email_greeting},</p> + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $event.confirm_email_text AND (not $isOnWaitlist AND not $isRequireApproval)} <p>{$event.confirm_email_text|htmlize}</p> @@ -14641,7 +14645,7 @@ You were registered by: {$payer.name} </td> </tr> {/if} - {if $event.is_monetary} + {if $event.is_monetary and not $isRequireApproval} <tr> <th {$headerStyle}> @@ -14977,8 +14981,8 @@ You were registered by: {$payer.name} </body> </html> ', @tpl_ovid_event_online_receipt, 1, 0), - ('Events - Registration Confirmation and Receipt (on-line)', '{if $isOnWaitlist}{ts}Wait List Confirmation{/ts}{else}{ts}Registration Confirmation{/ts}{/if} - {$event.event_title}', '{contact.email_greeting}, - + ('Events - Registration Confirmation and Receipt (on-line)', '{if $isOnWaitlist}{ts}Wait List Confirmation{/ts}{elseif $isRequireApproval}{ts}Registration Request Confirmation{/ts}{else}{ts}Registration Confirmation{/ts}{/if} - {$event.event_title} +', '{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} {if $event.confirm_email_text AND (not $isOnWaitlist AND not $isRequireApproval)} {$event.confirm_email_text} @@ -15080,7 +15084,7 @@ You were registered by: {$payer.name} {if $payer.name} You were registered by: {$payer.name} {/if} -{if $event.is_monetary} {* This section for Paid events only.*} +{if $event.is_monetary and not $isRequireApproval} {* This section for Paid events only.*} ==========================================================={if $pricesetFieldsCount }===================={/if} @@ -15306,7 +15310,7 @@ You were registered by: {$payer.name} <tr> <td> - <p>{contact.email_greeting},</p> + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $event.confirm_email_text AND (not $isOnWaitlist AND not $isRequireApproval)} <p>{$event.confirm_email_text|htmlize}</p> @@ -15458,7 +15462,7 @@ You were registered by: {$payer.name} </td> </tr> {/if} - {if $event.is_monetary} + {if $event.is_monetary and not $isRequireApproval} <tr> <th {$headerStyle}> @@ -18881,7 +18885,8 @@ or want to inquire about reinstating your registration for this event.{/ts}</p> ('Memberships - Receipt (on-line)', '{if $is_pay_later}{ts}Invoice{/ts}{else}{ts}Receipt{/ts}{/if} - {$title} -', '{if $receipt_text} +', '{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} +{if $receipt_text} {$receipt_text} {/if} {if $is_pay_later} @@ -19145,7 +19150,7 @@ or want to inquire about reinstating your registration for this event.{/ts}</p> <tr> <td> - + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $receipt_text} <p>{$receipt_text|htmlize}</p> {/if} @@ -19680,7 +19685,8 @@ or want to inquire about reinstating your registration for this event.{/ts}</p> </html> ', @tpl_ovid_membership_online_receipt, 1, 0), ('Memberships - Receipt (on-line)', '{if $is_pay_later}{ts}Invoice{/ts}{else}{ts}Receipt{/ts}{/if} - {$title} -', '{if $receipt_text} +', '{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} +{if $receipt_text} {$receipt_text} {/if} {if $is_pay_later} @@ -19944,7 +19950,7 @@ or want to inquire about reinstating your registration for this event.{/ts}</p> <tr> <td> - + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $receipt_text} <p>{$receipt_text|htmlize}</p> {/if} @@ -23964,4 +23970,4 @@ INSERT INTO `civicrm_report_instance` ( `domain_id`, `title`, `report_id`, `description`, `permission`, `form_values`) VALUES ( @domainID, 'Survey Details', 'survey/detail', 'Detailed report for canvassing, phone-banking, walk lists or other surveys.', 'access CiviReport', 'a:39:{s:6:"fields";a:2:{s:9:"sort_name";s:1:"1";s:6:"result";s:1:"1";}s:22:"assignee_contact_id_op";s:2:"eq";s:25:"assignee_contact_id_value";s:0:"";s:12:"sort_name_op";s:3:"has";s:15:"sort_name_value";s:0:"";s:17:"street_number_min";s:0:"";s:17:"street_number_max";s:0:"";s:16:"street_number_op";s:3:"lte";s:19:"street_number_value";s:0:"";s:14:"street_name_op";s:3:"has";s:17:"street_name_value";s:0:"";s:15:"postal_code_min";s:0:"";s:15:"postal_code_max";s:0:"";s:14:"postal_code_op";s:3:"lte";s:17:"postal_code_value";s:0:"";s:7:"city_op";s:3:"has";s:10:"city_value";s:0:"";s:20:"state_province_id_op";s:2:"in";s:23:"state_province_id_value";a:0:{}s:13:"country_id_op";s:2:"in";s:16:"country_id_value";a:0:{}s:12:"survey_id_op";s:2:"in";s:15:"survey_id_value";a:0:{}s:12:"status_id_op";s:2:"eq";s:15:"status_id_value";s:1:"1";s:11:"custom_1_op";s:2:"in";s:14:"custom_1_value";a:0:{}s:11:"custom_2_op";s:2:"in";s:14:"custom_2_value";a:0:{}s:17:"custom_3_relative";s:1:"0";s:13:"custom_3_from";s:0:"";s:11:"custom_3_to";s:0:"";s:11:"description";s:75:"Detailed report for canvassing, phone-banking, walk lists or other surveys.";s:13:"email_subject";s:0:"";s:8:"email_to";s:0:"";s:8:"email_cc";s:0:"";s:10:"permission";s:17:"access CiviReport";s:6:"groups";s:0:"";s:9:"domain_id";i:1;}'); -UPDATE civicrm_domain SET version = '5.3.2'; +UPDATE civicrm_domain SET version = '5.4.0'; diff --git a/civicrm/sql/civicrm_generated.mysql b/civicrm/sql/civicrm_generated.mysql index 7cb8891e1484261e52837687ba75540c077ca183..81b14ab734bcf081275b0acc09cfa195d1dc026b 100644 --- a/civicrm/sql/civicrm_generated.mysql +++ b/civicrm/sql/civicrm_generated.mysql @@ -399,7 +399,7 @@ UNLOCK TABLES; LOCK TABLES `civicrm_domain` WRITE; /*!40000 ALTER TABLE `civicrm_domain` DISABLE KEYS */; -INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `config_backend`, `version`, `contact_id`, `locales`, `locale_custom_strings`) VALUES (1,'Default Domain Name',NULL,NULL,'5.3.2',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}'); +INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `config_backend`, `version`, `contact_id`, `locales`, `locale_custom_strings`) VALUES (1,'Default Domain Name',NULL,NULL,'5.4.0',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}'); /*!40000 ALTER TABLE `civicrm_domain` ENABLE KEYS */; UNLOCK TABLES; diff --git a/civicrm/sql/civicrm_sample_custom_data.mysql b/civicrm/sql/civicrm_sample_custom_data.mysql index e12103b43b7e6f4441b970e277dfc6ae738772e9..960e5952d3a8b28523efcc24eea58af026eb499c 100644 --- a/civicrm/sql/civicrm_sample_custom_data.mysql +++ b/civicrm/sql/civicrm_sample_custom_data.mysql @@ -43,7 +43,7 @@ INSERT INTO `civicrm_custom_group` (`name`, `title`, `extends`, `style`, `collap -- * create option group for storing custom options for custom fields -- * -- *******************************************************/ -INSERT INTO `civicrm_option_group` (`name`, `title`, `is_reserved`, `is_active`) VALUES ('custom_most_important_issue', 'Most Important Issue', 1, 1), ( 'custom_marital_status', 'Marital Status', 1, 1); +INSERT INTO `civicrm_option_group` (`name`, `title`, `is_reserved`, `is_active`) VALUES ('custom_most_important_issue', 'Most Important Issue', 0, 1), ( 'custom_marital_status', 'Marital Status', 1, 1); SELECT @option_most_id := max(id) from civicrm_option_group where name = 'custom_most_important_issue'; SELECT @option_marital_id := max(id) from civicrm_option_group where name = 'custom_marital_status'; @@ -78,7 +78,7 @@ CREATE TABLE `civicrm_value_constituent_information_1` (`id` int(10) unsigned NO -- Fall Fundraiser Dinner participants’ custom data -INSERT INTO civicrm_option_group ( name, title, is_active, is_reserved ) VALUES ('soup_selection', 'Soup Selection', 1, 1); +INSERT INTO `civicrm_option_group` (`name`, `title`, `is_reserved`, `is_active`) VALUES ('soup_selection', 'Soup Selection', 0, 1); SELECT @ogid := MAX(id) FROM civicrm_option_group; INSERT INTO civicrm_custom_group ( name, title, extends, extends_entity_column_id, extends_entity_column_value, style, is_active, table_name) VALUES ('Food_Preference', 'Food Preference', 'Participant', 2, '1', 'Inline', 1, 'civicrm_value_food_preference_2'); @@ -97,7 +97,7 @@ INSERT INTO `civicrm_custom_group` (`name`, `title`, `extends`, `extends_entity_ SELECT @cgid_contribution := MAX(id) FROM civicrm_custom_group; -INSERT INTO civicrm_option_group ( name, title, is_active ) VALUES ('how_long_have_you_been_a_donor', 'How long have you been a donor?', 1); +INSERT INTO `civicrm_option_group` (`name`, `title`, `is_reserved`, `is_active`) VALUES ('how_long_have_you_been_a_donor', 'How long have you been a donor?', 0, 1); SELECT @ogid_contribution := MAX(id) FROM civicrm_option_group; diff --git a/civicrm/templates/CRM/Admin/Page/APIExplorer.js b/civicrm/templates/CRM/Admin/Page/APIExplorer.js index 0951b924aae01333753642c29ae0134ad12f4c07..7d85e83586ada04045cda18379f01e795107d1e8 100644 --- a/civicrm/templates/CRM/Admin/Page/APIExplorer.js +++ b/civicrm/templates/CRM/Admin/Page/APIExplorer.js @@ -576,7 +576,6 @@ /** * Format value to look like php code - * TODO: Use short array syntax when we drop support for php 5.3 * @param val */ function phpFormat(val) { @@ -736,7 +735,7 @@ q.php += "]"; q.json += "\n}"; } - q.php += "];"; + q.php += ");"; q.json += ").done(function(result) {\n // do something\n});"; q.smarty += "}\n{foreach from=$result.values item=" + entity.toLowerCase() + "}\n {$" + entity.toLowerCase() + ".some_field}\n{/foreach}"; if (!_.includes(action, 'get')) { diff --git a/civicrm/templates/CRM/Admin/Page/OptionGroup.tpl b/civicrm/templates/CRM/Admin/Page/OptionGroup.tpl index f33ba6ed422870619a6c43cbf45bea8046d94729..e8c60e31d833edb9972bbee03a8686e5592b04a4 100644 --- a/civicrm/templates/CRM/Admin/Page/OptionGroup.tpl +++ b/civicrm/templates/CRM/Admin/Page/OptionGroup.tpl @@ -36,20 +36,31 @@ {if $rows} <div id="browseValues"> - {strip} + {if $action ne 1 and $action ne 2} + <div class="action-link"> + {crmButton q="action=add&reset=1" id="newOptionGroup" icon="plus-circle"}{ts}Add Option Group{/ts}{/crmButton} + {crmButton p="civicrm/admin" q="reset=1" class="cancel" icon="times"}{ts}Done{/ts}{/crmButton} + </div> + {/if} + + {strip} {* handle enable/disable actions*} {include file="CRM/common/enableDisableApi.tpl"} <table cellpadding="0" cellspacing="0" border="0"> <tr class="columnheader"> - <th>{ts}Title{/ts}</th> - <th>{ts}Name{/ts}</th> - <th></th> + <th>{ts}Title{/ts}</th> + <th>{ts}Name{/ts}</th> + <th>{ts}Reserved{/ts}</th> + <th>{ts}Enabled?{/ts}</th> + <th></th> </tr> {foreach from=$rows item=row} - <tr id="optionGroup-{$row.id}" class="crm-entity {cycle values="odd-row,even-row"} {$row.class}{if NOT $row.is_active} disabled{/if}"> - <td class="crm-admin-optionGroup-title">{if $row.title}{$row.title}{else}( {ts}none{/ts} ){/if}</td> - <td class="crm-admin-optionGroup-name">{$row.name}</td> - <td><a href="{crmURL p="civicrm/admin/options" q="gid=`$row.id`&reset=1"}" title="{ts}View and Edit Options{/ts}">{ts}Options{/ts}</a></td> + <tr id="optionGroup-{$row.id}" class="crm-entity {cycle values="odd-row,even-row"} {$row.class}{if NOT $row.is_active} disabled{/if}"> + <td class="crm-admin-optionGroup-title">{if $row.title}{$row.title}{else}( {ts}none{/ts} ){/if}</td> + <td class="crm-admin-optionGroup-name">{$row.name}</td> + <td class="crm-admin-optionGroup-is_reserved">{if $row.is_reserved eq 1} {ts}Yes{/ts} {else} {ts}No{/ts} {/if}</td> + <td class="crm-admin-optionGroup-is_active" id="row_{$row.id}_status">{if $row.is_active eq 1} {ts}Yes{/ts} {else} {ts}No{/ts} {/if}</td> + <td><a href="{crmURL p="civicrm/admin/options" q="gid=`$row.id`&reset=1"}" title="{ts}View and Edit Options{/ts}">{ts}Options{/ts}</a></td> </tr> {/foreach} </table> @@ -59,10 +70,10 @@ <div class="action-link"> {crmButton q="action=add&reset=1" id="newOptionGroup" icon="plus-circle"}{ts}Add Option Group{/ts}{/crmButton} {crmButton p="civicrm/admin" q="reset=1" class="cancel" icon="times"}{ts}Done{/ts}{/crmButton} - </div> + </div> {/if} </div> -{elseif $action NEQ 1 && $action NEQ 2} +{elseif $action ne 1 and $action ne 2} <div class="messages status no-popup"> <img src="{$config->resourceBase}i/Inform.gif" alt="{ts}status{/ts}"/> {capture assign=crmURL}{crmURL p='civicrm/admin/optionGroup' q="action=add&reset=1"}{/capture} diff --git a/civicrm/templates/CRM/Contact/Form/Search/Builder.js b/civicrm/templates/CRM/Contact/Form/Search/Builder.js index 019add712d87a59ba286716c87d1c6edfd4cb178..904e7766c830486ade31d116867dd07d6ee5fe4a 100644 --- a/civicrm/templates/CRM/Contact/Form/Search/Builder.js +++ b/civicrm/templates/CRM/Contact/Form/Search/Builder.js @@ -19,15 +19,16 @@ var patt = /_1$/; // pattern to check if the change event came from field name if (field !== null && patt.test(this.id)) { // based on data type remove invalid operators e.g. IS EMPTY doesn't work with Boolean type column + var operators = CRM.searchBuilder.generalOperators; if ((field in CRM.searchBuilder.fieldTypes) === true) { - if (CRM.searchBuilder.fieldTypes[field] == 'Boolean') { - CRM.searchBuilder.generalOperators = _.omit(CRM.searchBuilder.generalOperators, ['IS NOT EMPTY', 'IS EMPTY']); + if ($.inArray(CRM.searchBuilder.fieldTypes[field], ['Boolean', 'Int']) > -1) { + operators = _.omit(operators, ['IS NOT EMPTY', 'IS EMPTY']); } else if (CRM.searchBuilder.fieldTypes[field] == 'String') { - CRM.searchBuilder.generalOperators = _.omit(CRM.searchBuilder.generalOperators, ['>', '<', '>=', '<=']); + operators = _.omit(operators, ['>', '<', '>=', '<=']); } } - buildOperator(operator, CRM.searchBuilder.generalOperators); + buildOperator(operator, operators); } // These Ops don't get any input field. diff --git a/civicrm/templates/CRM/Contact/Form/Search/Criteria/Basic.tpl b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Basic.tpl index ba59ec4468c90a8595bc2c3d7799eb5577e4c9d5..52f34e135da8fc64d354316b33db79cf06f2ee37 100644 --- a/civicrm/templates/CRM/Contact/Form/Search/Criteria/Basic.tpl +++ b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Basic.tpl @@ -23,170 +23,31 @@ | see the CiviCRM license FAQ at http://civicrm.org/licensing | +--------------------------------------------------------------------+ *} -<table class="form-layout"> - <tr> - <td><label>{ts}Complete OR Partial Name{/ts}</label><br /> - {$form.sort_name.html} - </td> - <td> - <label>{ts}Complete OR Partial Email{/ts}</label><br /> - {$form.email.html} - </td> - {if $form.contact_type} - <td><label>{ts}Contact Type(s){/ts}</label><br /> - {$form.contact_type.html} - </td> - {else} - <td> </td> - {/if} - </tr> - <tr> - {if $form.group} - <td> - <div id='groupselect'><label>{ts}Group(s){/ts} <span class="description">(<a href="#" id='searchbygrouptype'>{ts}search by group type{/ts}</a>)</span></label> - <br /> - {$form.group.html} - </div> - <div id='grouptypeselect'> - <label>{ts}Group Type(s){/ts} <span class="description"> (<a href="#" id='searchbygroup'>{ts}search by group{/ts}</a>)</span></label> - <br /> - {$form.group_type.html} - {literal} - <script type="text/javascript"> - CRM.$(function($) { - function showGroupSearch() { - $('#grouptypeselect').hide(); - $('#groupselect').show(); - $('#group_type').select2('val', ''); - return false; - } - function showGroupTypeSearch() { - $('#groupselect').hide(); - $('#grouptypeselect').show(); - $('#group').select2('val', ''); - return false; - } - $('#searchbygrouptype').click(showGroupTypeSearch); - $('#searchbygroup').click(showGroupSearch); - - if ($('#group_type').val() ) { - showGroupTypeSearch(); - } - else { - showGroupSearch(); - } - - }); - </script> - {/literal} - </div> - </td> - {else} - <td> </td> - {/if} - {if $form.contact_tags} - <td><label>{ts}Select Tag(s){/ts}</label> - {$form.contact_tags.html} - </td> - {else} - <td> </td> - {/if} - {if $isTagset} - <td colspan="2">{include file="CRM/common/Tagset.tpl"}</td> - {/if} - <td>{$form.tag_search.label} {help id="id-all-tags"}<br />{$form.tag_search.html}</td> - {if ! $isTagset} - <td colspan="2"> </td> - {/if} - <td> </td> - </tr> - {if $form.all_tag_types} - <tr> - <td colspan="5"> - {$form.all_tag_types.html} {$form.all_tag_types.label} {help id="id-all-tag-types"} - </td> - </tr> - {/if} - <tr> - <td> - <div> - {$form.phone_numeric.label}<br />{$form.phone_numeric.html} - </div> - <div class="description font-italic"> - {ts}Punctuation and spaces are ignored.{/ts} +<div class="advanced-search-fields basic-fields form-layout"> + {foreach from=$basicSearchFields item=fieldSpec} + {assign var=field value=$form[$fieldSpec.name]} + {if $field} + <div class="search-field {$fieldSpec.class|escape}"> + {if $fieldSpec.template} + {include file=$fieldSpec.template} + {else} + {$field.label} + {if $fieldSpec.help} + {assign var=help value=$fieldSpec.help} + {capture assign=helpFile}{if $fieldSpec.help}{$fieldSpec.help}{else}''{/if}{/capture} + {help id=$help.id file=$help.file} + {/if} + <br /> + {$field.html} + {if $fieldSpec.description} + <div class="description font-italic"> + {$fieldSpec.description} + </div> + {/if} + {/if} </div> - </td> - <td>{$form.phone_location_type_id.label}<br />{$form.phone_location_type_id.html}</td> - <td>{$form.phone_phone_type_id.label}<br />{$form.phone_phone_type_id.html}</td> - </tr> - <tr> - <td colspan="2"> - <table class="form-layout-compressed"> - <tr> - <td colspan="2"> - {$form.privacy_toggle.html} {help id="id-privacy"} - </td> - </tr> - <tr> - <td> - {$form.privacy_options.html} - </td> - <td style="vertical-align:middle"> - <div id="privacy-operator-wrapper">{$form.privacy_operator.html} {help id="privacy-operator"}</div> - </td> - </tr> - </table> - {literal} - <script type="text/javascript"> - cj("select#privacy_options").change(function() { - if (cj(this).val() && cj(this).val().length > 1) { - cj('#privacy-operator-wrapper').show(); - } else { - cj('#privacy-operator-wrapper').hide(); - } - }).change(); - </script> - {/literal} - </td> - <td colspan="3"> - {$form.preferred_communication_method.label}<br /> - {$form.preferred_communication_method.html}<br /> - <div class="spacer"></div> - {$form.email_on_hold.html} {$form.email_on_hold.label} - </td> - </tr> - <tr> - <td> - {$form.contact_source.label} {help id="id-source" file="CRM/Contact/Form/Contact"}<br /> - {$form.contact_source.html} - </td> - <td> - {$form.job_title.label}<br /> - {$form.job_title.html} - </td> - <td colspan="3"> - {$form.preferred_language.label}<br /> - {$form.preferred_language.html} - </td> - </tr> - <tr> - <td> - {$form.contact_id.label} {help id="id-internal-id" file="CRM/Contact/Form/Contact"}<br /> - {$form.contact_id.html} - </td> - <td> - {$form.external_identifier.label} {help id="id-external-id" file="CRM/Contact/Form/Contact"}<br /> - {$form.external_identifier.html} - </td> - <td> - {if $form.uf_user} - {$form.uf_user.label} {$form.uf_user.html} - <div class="description font-italic"> - {ts 1=$config->userFramework}Does the contact have a %1 Account?{/ts} - </div> - {else} - - {/if} - </td> - </tr> -</table> + {elseif $fieldSpec.is_custom} + {include file=$fieldSpec.template} + {/if} + {/foreach} +</div> diff --git a/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/group.tpl b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/group.tpl new file mode 100644 index 0000000000000000000000000000000000000000..bcc83d76f622ca86e910ee6b68ef1943bdd10ad5 --- /dev/null +++ b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/group.tpl @@ -0,0 +1,49 @@ +<div id='groupselect'> + <label>{ts}Group(s){/ts} + <span class="description"> + (<a href="#" id='searchbygrouptype'>{ts}search by group type{/ts}</a>) + </span> + </label> + <br/> + {$form.group.html} +</div> +<div id='grouptypeselect'> + <label> + {ts}Group Type(s){/ts} + <span class="description"> + (<a href="#" id='searchbygroup'>{ts}search by group{/ts}</a>) + </span> + </label> + <br/> + {$form.group_type.html} + {literal} + <script type="text/javascript"> + CRM.$(function ($) { + function showGroupSearch() { + $('#grouptypeselect').hide(); + $('#groupselect').show(); + $('#group_type').select2('val', ''); + return false; + } + + function showGroupTypeSearch() { + $('#groupselect').hide(); + $('#grouptypeselect').show(); + $('#group').select2('val', ''); + return false; + } + + $('#searchbygrouptype').click(showGroupTypeSearch); + $('#searchbygroup').click(showGroupSearch); + + if ($('#group_type').val()) { + showGroupTypeSearch(); + } + else { + showGroupSearch(); + } + + }); + </script> + {/literal} +</div> diff --git a/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/preferred_communication_method.tpl b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/preferred_communication_method.tpl new file mode 100644 index 0000000000000000000000000000000000000000..3e94b37b2d427ee633bdd6e285c87ad3cb40b10f --- /dev/null +++ b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/preferred_communication_method.tpl @@ -0,0 +1,10 @@ +{$form.preferred_communication_method.label} +<br/> +{$form.preferred_communication_method.html} +<br/> + +{if $form.email_on_hold} + <div class="spacer"></div> + {$form.email_on_hold.html} + {$form.email_on_hold.label} +{/if} diff --git a/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/privacy_toggle.tpl b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/privacy_toggle.tpl new file mode 100644 index 0000000000000000000000000000000000000000..71e3c7642bf973f024fcc27d47f6e6d980d3d639 --- /dev/null +++ b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/privacy_toggle.tpl @@ -0,0 +1,28 @@ +<table class="form-layout-compressed"> + <tr> + <td colspan="2"> + {$form.privacy_toggle.html} {help id="id-privacy"} + </td> + </tr> + <tr> + <td> + {$form.privacy_options.html} + </td> + <td style="vertical-align:middle"> + <div id="privacy-operator-wrapper"> + {$form.privacy_operator.html} {help id="privacy-operator"} + </div> + </td> + </tr> +</table> +{literal} + <script type="text/javascript"> + cj("select#privacy_options").change(function () { + if (cj(this).val() && cj(this).val().length > 1) { + cj('#privacy-operator-wrapper').show(); + } else { + cj('#privacy-operator-wrapper').hide(); + } + }).change(); + </script> +{/literal} diff --git a/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/tag_set.tpl b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/tag_set.tpl new file mode 100644 index 0000000000000000000000000000000000000000..48d97d5abddc6a7b2a40d6d838315d1200db9484 --- /dev/null +++ b/civicrm/templates/CRM/Contact/Form/Search/Criteria/Fields/tag_set.tpl @@ -0,0 +1,5 @@ +{if $isTagset} + <div class="search-field search-field__span-2"> + {include file="CRM/common/Tagset.tpl"} + </div> +{/if} diff --git a/civicrm/templates/CRM/Contribute/Form/ContributionPage/Settings.tpl b/civicrm/templates/CRM/Contribute/Form/ContributionPage/Settings.tpl index b869e14eb965100c8395eac784a0b6c9a13ea4e0..c159c56e94a67cc16443133817b0b02f2c768d64 100644 --- a/civicrm/templates/CRM/Contribute/Form/ContributionPage/Settings.tpl +++ b/civicrm/templates/CRM/Contribute/Form/ContributionPage/Settings.tpl @@ -95,15 +95,11 @@ </tr> <tr class="crm-contribution-contributionpage-settings-form-block-start_date"> <td class ="label">{$form.start_date.label} {help id="id-start_date"}</td> - <td> - {include file="CRM/common/jcalendar.tpl" elementName=start_date} - </td> + <td>{$form.start_date.html}</td> </tr> <tr class="crm-contribution-contributionpage-settings-form-block-end_date"> <td class ="label">{$form.end_date.label}</td> - <td> - {include file="CRM/common/jcalendar.tpl" elementName=end_date} - </td> + <td>{$form.end_date.html}</td> </tr> <tr class="crm-contribution-contributionpage-settings-form-block-honor_block_is_active"> <td> </td><td>{$form.honor_block_is_active.html}{$form.honor_block_is_active.label} {help id="id-honoree_section"}</td> diff --git a/civicrm/templates/CRM/Contribute/Form/ContributionPage/Widget.tpl b/civicrm/templates/CRM/Contribute/Form/ContributionPage/Widget.tpl index d32391961fa34d280b6ea58a96f6bb8971a318c1..ff1c9f73d5ee4389def249ca3692cbabcad866e0 100644 --- a/civicrm/templates/CRM/Contribute/Form/ContributionPage/Widget.tpl +++ b/civicrm/templates/CRM/Contribute/Form/ContributionPage/Widget.tpl @@ -92,9 +92,6 @@ {ts}Edit Widget Colors{/ts} </div><!-- /.crm-accordion-header --> <div class="crm-accordion-body"> - <div class="description"> - {ts}Enter colors in hexadecimal format prefixed with <em>#</em>. EXAMPLE: <em>#FF0000</em> = Red. You can do a web search on 'hexadecimal colors' to find a chart of color codes.{/ts} - </div> <table class="form-layout-compressed"> {foreach from=$colorFields item=field key=fieldName} <tr><td class="label">{$form.$fieldName.label}<span class="crm-marker"> *</span></td><td>{$form.$fieldName.html}</td></tr> @@ -138,5 +135,5 @@ </script> {/literal} {/crmRegion} -{crmRegion name="contribute-form-contributionpage-widget-post} +{crmRegion name="contribute-form-contributionpage-widget-post"} {/crmRegion} diff --git a/civicrm/templates/CRM/Contribute/Page/Tab.tpl b/civicrm/templates/CRM/Contribute/Page/Tab.tpl index 6d910f93995b61e47891014f0160c0219217de0f..f38a200afd4c1717f8a0570effee271cde79f1e7 100644 --- a/civicrm/templates/CRM/Contribute/Page/Tab.tpl +++ b/civicrm/templates/CRM/Contribute/Page/Tab.tpl @@ -29,55 +29,91 @@ {include file="CRM/Contribute/Form/ContributionView.tpl"} {else} <div class="contact-summary-contribute-tab view-content"> - <div class="help"> + + <div id="secondaryTabContainer" class="ui-tabs ui-widget ui-widget-content ui-corner-all"> + {* Tab management *} + <script type="text/javascript"> + var selectedTab = 'contributions'; + + {literal} + CRM.$(function($) { + var tabIndex = $('#tab_' + selectedTab).prevAll().length; + $("#secondaryTabContainer").tabs({active: tabIndex}); + $(".crm-tab-button").addClass("ui-corner-bottom"); + }); + {/literal} + </script> + <ul class="ui-tabs-nav ui-corner-all ui-helper-reset ui-helper-clearfix ui-widget-header"> + <li id="tab_contributions" class="crm-tab-button ui-corner-all ui-tabs-tab ui-corner-top ui-state-default ui-tab ui-tabs-active ui-state-active"> + <a href="#contributions-subtab" title="{ts}Contributions{/ts}"> + {ts}Contributions{/ts} <em>{$rows|@count}</em> + </a> + </li> + <li id="tab_recurring" class="crm-tab-button ui-corner-all ui-tabs-tab ui-corner-top ui-state-default ui-tab"> + <a href="#recurring-subtab" title="{ts}Recurring Contributions{/ts}"> + {ts}Recurring Contributions{/ts} <em>{$activeRecurRows|@count}</em> + </a> + </li> + </ul> + + <div id="contributions-subtab" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> + <div class="help"> {if $permission EQ 'edit'} {capture assign=newContribURL}{crmURL p="civicrm/contact/view/contribution" q="reset=1&action=add&cid=`$contactId`&context=contribution"}{/capture} {capture assign=link}class="action-item" href="{$newContribURL}"{/capture} {ts 1=$link}Click <a %1>Record Contribution</a> to record a new contribution received from this contact.{/ts} - {if $newCredit} - {capture assign=newCreditURL}{crmURL p="civicrm/contact/view/contribution" q="reset=1&action=add&cid=`$contactId`&context=contribution&mode=live"}{/capture} - {capture assign=link}class="action-item" href="{$newCreditURL}"{/capture} - {ts 1=$link}Click <a %1>Submit Credit Card Contribution</a> to process a new contribution on behalf of the contributor using their credit card.{/ts} - {/if} + {if $newCredit} + {capture assign=newCreditURL}{crmURL p="civicrm/contact/view/contribution" q="reset=1&action=add&cid=`$contactId`&context=contribution&mode=live"}{/capture} + {capture assign=link}class="action-item" href="{$newCreditURL}"{/capture} + {ts 1=$link}Click <a %1>Submit Credit Card Contribution</a> to process a new contribution on behalf of the contributor using their credit card.{/ts} + {/if} {else} - {ts 1=$displayName}Contributions received from %1 since inception.{/ts} + {ts 1=$displayName}Contributions received from %1 since inception.{/ts} {/if} - </div> + </div> - {if $action eq 16 and $permission EQ 'edit'} + {if $action eq 16 and $permission EQ 'edit'} <div class="action-link"> - <a accesskey="N" href="{$newContribURL}" class="button"><span><i class="crm-i fa-plus-circle"></i> {ts}Record Contribution (Check, Cash, EFT ...){/ts}</span></a> - {if $newCredit} - <a accesskey="N" href="{$newCreditURL}" class="button"><span><i class="crm-i fa-credit-card"></i> {ts}Submit Credit Card Contribution{/ts}</span></a> - {/if} - <br /><br /> + <a accesskey="N" href="{$newContribURL}" class="button"><span><i class="crm-i fa-plus-circle"></i> {ts}Record Contribution (Check, Cash, EFT ...){/ts}</span></a> + {if $newCredit} + <a accesskey="N" href="{$newCreditURL}" class="button"><span><i class="crm-i fa-credit-card"></i> {ts}Submit Credit Card Contribution{/ts}</span></a> + {/if} + <br /><br /> </div> - <div class='clear'></div> - {/if} + <div class='clear'></div> + {/if} - {if $rows} + {if $rows} {include file="CRM/Contribute/Page/ContributionTotals.tpl" mode="view"} - <div class='clear'></div> + <div class='clear'></div> {include file="CRM/Contribute/Form/Selector.tpl"} - {else} + {else} <div class="messages status no-popup"> - <div class="icon inform-icon"></div> - {ts}No contributions have been recorded from this contact.{/ts} + <div class="icon inform-icon"></div> + {ts}No contributions have been recorded from this contact.{/ts} </div> - {/if} - - {if $recur} - <div class="crm-block crm-contact-contribute-recur"> - <h3>{ts}Recurring Contributions{/ts}</h3> - {include file="CRM/Contribute/Page/ContributionRecur.tpl"} - </div> - {/if} + {/if} - {if $softCredit} - <div class="crm-block crm-contact-contribute-softcredit"> - <h3>{ts}Soft credits{/ts} {help id="id-soft_credit"}</h3> - {include file="CRM/Contribute/Page/ContributionSoft.tpl"} - </div> - {/if} + {if $softCredit} + <div class="crm-block crm-contact-contribute-softcredit"> + <h3>{ts}Soft credits{/ts} {help id="id-soft_credit"}</h3> + {include file="CRM/Contribute/Page/ContributionSoft.tpl"} + </div> + {/if} + </div> + <div id="recurring-subtab" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> + {if $recur} + <div class="crm-block crm-contact-contribute-recur crm-contact-contribute-recur-active"> + <h3>{ts}Active Recurring Contributions{/ts}</h3> + {include file="CRM/Contribute/Page/ContributionRecur.tpl" recurRows=$activeRecurRows} + </div> + <div class="crm-block crm-contact-contribute-recur crm-contact-contribute-recur-inactive"> + <h3>{ts}Inactive Recurring Contributions{/ts}</h3> + {include file="CRM/Contribute/Page/ContributionRecur.tpl" recurRows=$inactiveRecurRows} + </div> + {/if} + </div> + <div class="clear"></div> + </div> </div> {/if} diff --git a/civicrm/templates/CRM/Event/Form/Registration/EventInfoBlock.tpl b/civicrm/templates/CRM/Event/Form/Registration/EventInfoBlock.tpl index 8241fe3f51e4127abb448971ad9a654b631cd5c4..4bd052b9a58531f361ecdf6713bb65ae9d3571a7 100644 --- a/civicrm/templates/CRM/Event/Form/Registration/EventInfoBlock.tpl +++ b/civicrm/templates/CRM/Event/Form/Registration/EventInfoBlock.tpl @@ -34,7 +34,7 @@ {/if} </td> </tr> - <tr><td><label>{ts}When{/ts}</label></td> + <tr><td>{ts}When{/ts}</td> <td width="90%"> {$event.event_start_date|crmDate} {if $event.event_end_date} @@ -51,7 +51,7 @@ {if $isShowLocation} {if $location.address.1} - <tr><td><label>{ts}Location{/ts}</label></td> + <tr><td>{ts}Location{/ts}</td> <td> {$location.address.1.display|nl2br} {if ( $event.is_map && @@ -66,7 +66,7 @@ {/if}{*End of isShowLocation condition*} {if $location.phone.1.phone || $location.email.1.email} - <tr><td><label>{ts}Contact{/ts}</label></td> + <tr><td>{ts}Contact{/ts}</td> <td> {* loop on any phones and emails for this event *} {foreach from=$location.phone item=phone} diff --git a/civicrm/templates/CRM/Event/Page/EventInfo.tpl b/civicrm/templates/CRM/Event/Page/EventInfo.tpl index 23d1b9e31414b6c24372302f791c325ef45c054a..5f0137a4182a7ec55936dff816cbe1e83e107f62 100644 --- a/civicrm/templates/CRM/Event/Page/EventInfo.tpl +++ b/civicrm/templates/CRM/Event/Page/EventInfo.tpl @@ -115,7 +115,7 @@ {/if} <div class="clear"></div> <div class="crm-section event_date_time-section"> - <div class="label"><label>{ts}When{/ts}</label></div> + <div class="label">{ts}When{/ts}</div> <div class="content"> <abbr class="dtstart" title="{$event.event_start_date|crmDate}"> {$event.event_start_date|crmDate}</abbr> @@ -140,7 +140,7 @@ {if $location.address.1} <div class="crm-section event_address-section"> - <div class="label"><label>{ts}Location{/ts}</label></div> + <div class="label">{ts}Location{/ts}</div> <div class="content">{$location.address.1.display|nl2br}</div> <div class="clear"></div> </div> @@ -164,7 +164,7 @@ {if $location.phone.1.phone || $location.email.1.email} <div class="crm-section event_contact-section"> - <div class="label"><label>{ts}Contact{/ts}</label></div> + <div class="label">{ts}Contact{/ts}</div> <div class="content"> {* loop on any phones and emails for this event *} {foreach from=$location.phone item=phone} @@ -186,7 +186,7 @@ {if $event.is_monetary eq 1 && $feeBlock.value} <div class="crm-section event_fees-section"> - <div class="label"><label>{$event.fee_label}</label></div> + <div class="label">{$event.fee_label}</div> <div class="content"> <table class="form-layout-compressed fee_block-table"> {foreach from=$feeBlock.value name=fees item=value} diff --git a/civicrm/templates/CRM/Group/Form/Search.tpl b/civicrm/templates/CRM/Group/Form/Search.tpl index 6fe311cd282cf9345217e58bd1504180eb9b9d23..99f7f48f80ffad93ec545ff7500ef0f71d123652 100644 --- a/civicrm/templates/CRM/Group/Form/Search.tpl +++ b/civicrm/templates/CRM/Group/Form/Search.tpl @@ -24,45 +24,57 @@ +--------------------------------------------------------------------+ *} <div class="crm-block crm-form-block crm-group-search-form-block"> - -<h3>{ts}Find Groups{/ts}</h3> -<table class="form-layout"> - <tr> - <td> - {$form.title.label}<br /> - {$form.title.html}<br /> - <span class="description font-italic"> + <div class="crm-accordion-wrapper crm-search_builder-accordion {if $rows and !$showSearchForm}collapsed{/if}"> + <div class="crm-accordion-header crm-master-accordion-header"> + {ts}Find Groups{/ts} + </div> + <div class="crm-accordion-body"> + <div id="searchForm"> + <table class="form-layout"> + <tr> + <td> + {$form.title.label}<br /> + {$form.title.html}<br /> + <span class="description font-italic"> {ts}Complete OR partial group name.{/ts} </span> - </td> - <td> - {$form.created_by.label}<br /> - {$form.created_by.html}<br /> - <span class="description font-italic"> + </td> + <td> + {$form.created_by.label}<br /> + {$form.created_by.html}<br /> + <span class="description font-italic"> {ts}Complete OR partial creator name.{/ts} </span> - </td> - <td id="group_type-block"> - {$form.group_type_search.label}<br /> - {$form.group_type_search.html}<br /> - <span class="description font-italic"> - {ts}Filter search by group type(s).{/ts} - </span> - </td> - <td> - {$form.visibility.label}<br /> - {$form.visibility.html}<br /> - <span class="description font-italic"> + </td> + <td> + {$form.visibility.label}<br /> + {$form.visibility.html}<br /> + <span class="description font-italic"> {ts}Filter search by visibility.{/ts} </span> - </td> - <td> - {$form.group_status.label}<br /> - {$form.group_status.html} - </td> - </tr> -</table> -</div> + </td> + </tr> + <tr> + <td id="group_type-block"> + {$form.group_type_search.label}<br /> + {$form.group_type_search.html}<br /> + <span class="description font-italic"> + {ts}Filter search by group type(s).{/ts} + </span> + </td> + <td> + {$form.group_status.label}<br /> + {$form.group_status.html} + </td> + <td> + {$form.component_mode.label}<br /> + {$form.component_mode.html} + </td> + </tr> + </table> + </div> + </div> + </div> <div class="css_right"> <a class="crm-hover-button action-item" href="{crmURL q="reset=1&update_smart_groups=1"}">{ts}Update Smart Group Counts{/ts}</a> {help id="update_smart_groups"} </div> @@ -119,6 +131,7 @@ d.group_type = groupTypes, d.visibility = $(".crm-group-search-form-block select#visibility").val(), d.status = groupStatus, + d.component_mode = $(".crm-group-search-form-block select#component_mode").val(), d.showOrgInfo = {/literal}"{$showOrgInfo}"{literal}, d.parentsOnly = parentsOnly } diff --git a/civicrm/templates/CRM/Member/Form/MembershipType.tpl b/civicrm/templates/CRM/Member/Form/MembershipType.tpl index 3c6bc9a3d04c679d7c8de33d26fe865fea481a89..1e679fcc9f4f652891e754e4a8ef6e6dc68a5f5c 100644 --- a/civicrm/templates/CRM/Member/Form/MembershipType.tpl +++ b/civicrm/templates/CRM/Member/Form/MembershipType.tpl @@ -128,7 +128,7 @@ <tr class="crm-membership-type-form-block-visibility"> <td class="label">{$form.visibility.label}</td> <td>{$form.visibility.html}<br /> - <span class="description">{ts}Is this membership type available for self-service signups ('Public') or assigned by CiviCRM 'staff' users only ('Admin'){/ts}</span> + <span class="description">{ts}Can this membership type be used for self-service signups ('Public'), or is it only for CiviCRM users with 'Edit Contributions' permission ('Admin').{/ts}</span> </td> </tr> <tr class="crm-membership-type-form-block-weight"> diff --git a/civicrm/templates/CRM/Profile/Form/Dynamic.tpl b/civicrm/templates/CRM/Profile/Form/Dynamic.tpl index ffb5047aeb687755f1cfe409be9fbbc6da6f09f9..fe559aafce940ea5fec5e4d05193924f7820e71a 100644 --- a/civicrm/templates/CRM/Profile/Form/Dynamic.tpl +++ b/civicrm/templates/CRM/Profile/Form/Dynamic.tpl @@ -36,7 +36,9 @@ <div class="crm-submit-buttons"> <span class="crm-button">{$form._qf_Edit_upload_delete.html}</span> - <a class="button cancel" href="{$cancelURL}">{$cancelButtonText}</a> + {if $includeCancelButton} + <a class="button cancel" href="{$cancelURL}">{$cancelButtonText}</a> + {/if} </div> {else} {if ! empty( $fields )} @@ -218,12 +220,14 @@ {/if} <div class="crm-submit-buttons" style='{$floatStyle}'> {include file="CRM/common/formButtons.tpl"}{if $isDuplicate}<span class="crm-button">{$form._qf_Edit_upload_duplicate.html}</span>{/if} - <a class="button cancel" href="{$cancelURL}"> - <span> - <i class="crm-i fa-times"></i> - {$cancelButtonText} - </span> - </a> + {if $includeCancelButton} + <a class="button cancel" href="{$cancelURL}"> + <span> + <i class="crm-i fa-times"></i> + {$cancelButtonText} + </span> + </a> + {/if} </div> {/if} {if $help_post && $action neq 4}<br /><div class="messages help">{$help_post}</div>{/if} diff --git a/civicrm/templates/CRM/UF/Form/AdvanceSetting.tpl b/civicrm/templates/CRM/UF/Form/AdvanceSetting.tpl index 34c1f65ac252fa3c3f4d48cb87288dbcc127d3f7..018ee8cb48da891322e14a2c20883c42aea3fda3 100644 --- a/civicrm/templates/CRM/UF/Form/AdvanceSetting.tpl +++ b/civicrm/templates/CRM/UF/Form/AdvanceSetting.tpl @@ -50,12 +50,17 @@ <td>{$form.post_URL.html} {help id='id-post_URL' file="CRM/UF/Form/Group.hlp"}</td> </tr> - <tr class="crm-uf-advancesetting-form-block-cancel_URL"> + <tr class="crm-uf-advancesetting-form-block-add_cancel_button"> + <td class="label"></td> + <td>{$form.add_cancel_button.html} {$form.add_cancel_button.label} {help id='id-add_cancel_button' file="CRM/UF/Form/Group.hlp"}</td> + </tr> + + <tr class="cancel_button_section crm-uf-advancesetting-form-block-cancel_URL"> <td class="label">{$form.cancel_URL.label}</td> <td>{$form.cancel_URL.html} {help id='id-cancel_URL' file="CRM/UF/Form/Group.hlp"}</td> </tr> - <tr class="crm-uf-advancesetting-form-block-cancel_button_text"> + <tr class="cancel_button_section crm-uf-advancesetting-form-block-cancel_button_text"> <td class="label">{$form.cancel_button_text.label}</td> <td>{$form.cancel_button_text.html} {help id='id-cancel_button_text' file="CRM/UF/Form/Group.hlp"}</td> </tr> @@ -102,3 +107,13 @@ </div><!-- / .crm-block --> </div><!-- /.crm-accordion-body --> </div><!-- /.crm-accordion-wrapper --> +{literal} + <script type="text/javascript"> + CRM.$(function($) { + $('.cancel_button_section').toggle($('#add_cancel_button').is(":checked")); + $('#add_cancel_button').click(function() { + $('.cancel_button_section').toggle($(this).is(":checked")); + }); + }); + </script> +{/literal} diff --git a/civicrm/templates/CRM/UF/Form/Group.hlp b/civicrm/templates/CRM/UF/Form/Group.hlp index c1f72b76fa65321f23b91a55fae00434450b914a..9f12c705b57668d30705f026112e2295779e14b9 100644 --- a/civicrm/templates/CRM/UF/Form/Group.hlp +++ b/civicrm/templates/CRM/UF/Form/Group.hlp @@ -36,7 +36,7 @@ {htxt id='id-used_for'} {ts}Profiles can be used in many different ways (and a single profile can be used in multiple contexts){/ts}:<br /> <ul class="left-alignment"> - <li>{ts}You can collect additional information about a donor or event participant by including profiles directly in Online Contribution Pages and Event Registration forms. When using a profile in this manner you do not need to check any of the "Used For" checkboxes.{/ts}</li> + <li>{ts}You can collect additional information about a donor or event participant by including profiles directly in Online Contribution Pages and Event Registration forms. When using a profile in this manner you do not need to check any of the "Used For" checkboxes.{/ts}</li> <li>{ts}Check <strong>Standalone Form or Directory</strong> if you want it to use this profile for custom forms, or contact listing and view screens (from the civicrm/profile path).{/ts}</li> <li>{ts}Check <strong>Search Results</strong> to use this profile to display an alternate set of results columns for CiviCRM Basic and Advanced Search.{/ts}</li> {if $config->userSystem->supports_form_extensions EQ '1'} @@ -96,6 +96,13 @@ {ts}If you are using this profile as a contact signup or edit form, and want to redirect the user to a static URL after they've submitted the form, you can also use contact tokens in URL - enter the complete URL here. If this field is left blank, the built-in Profile form will be redisplayed with a generic status message - 'Your contact information has been saved.'{/ts} {/htxt} +{htxt id='id-add_cancel_button-title'} + {ts}Add Cancel Button{/ts} +{/htxt} +{htxt id='id-add_cancel_button'} +{ts}Enable/Disable this checkbox to add/remove cancel button on the profile form.{/ts} +{/htxt} + {htxt id='id-cancel_URL-title'} {ts}Cancel Redirect{/ts} {/htxt} @@ -125,7 +132,7 @@ <p>{ts}When reCAPTCHA is enabled for a profile form, anonymous users are required to read an image with letters and numbers and enter the value in a field. This helps prevent abuse by automated scripts.{/ts}</p> <p>{ts 1="https://www.google.com/recaptcha" 2=$miscURL}To use reCAPTCHA you must sign up at <a href="%1" target="_blank">Google's reCaptcha site</a> to get your public and private keys. Then enter both keys in <a href="%2">Administer CiviCRM » System Settings » Misc (Undelete, PDFs, Limits, Logging, Captcha, etc.)</a>.{/ts}</p> <p><strong>{ts}Do not enable this feature if you are using this profile as an HTML Form Snippet embedded in a non-CiviCRM web page. reCAPTCHA requires dynamic page generation. Submitting a stand-alone form with reCAPTCHA included will always result in a reCAPTCHA validation error.{/ts}</strong></p> -{if $config->userSystem->supports_form_extensions EQ '1'} +{if $config->userSystem->supports_form_extensions EQ '1'} <p><strong>{ts}reCAPTCHA is also not available when a profile is used inside the User Registration and My Account screens.{/ts}</strong></p> {/if} {/htxt} diff --git a/civicrm/templates/CRM/common/civicrm.settings.php.template b/civicrm/templates/CRM/common/civicrm.settings.php.template index ae4d6e269fd86876a2bdfdf6a02de88816fe9da9..2895fed38c7f76c79a1f5ef4e3ec215538ee336d 100644 --- a/civicrm/templates/CRM/common/civicrm.settings.php.template +++ b/civicrm/templates/CRM/common/civicrm.settings.php.template @@ -399,6 +399,14 @@ if (!defined('CIVICRM_DB_CACHE_PREFIX')) { define('CIVICRM_DB_CACHE_PREFIX', ''); } +/** + * The cache system traditionally allowed a wide range of cache-keys, but some + * cache-keys are prohibited by PSR-16. + */ +if (!defined('CIVICRM_PSR16_STRICT')) { + define('CIVICRM_PSR16_STRICT', FALSE); +} + /** * If you have multilingual site and you are using the "inherit CMS language" * configuration option, but wish to, for example, use fr_CA instead of the diff --git a/civicrm/vendor/autoload.php b/civicrm/vendor/autoload.php index c18f803af66e0c29a1708f1b85384efe1b16f2e8..af2f5cf4b0b255e374deb3020411349760db0cf6 100644 --- a/civicrm/vendor/autoload.php +++ b/civicrm/vendor/autoload.php @@ -4,4 +4,4 @@ require_once __DIR__ . '/composer/autoload_real.php'; -return ComposerAutoloaderInit99d4995414f340fcb5325a17584ee78c::getLoader(); +return ComposerAutoloaderInitd31b8d6552946245209cda886e0ea284::getLoader(); diff --git a/civicrm/vendor/composer/autoload_psr4.php b/civicrm/vendor/composer/autoload_psr4.php index 51d1856808e80f85f1a4f8803935458cf97e7706..1db2a6d77c858a955289db753f31b6cd065f56d4 100644 --- a/civicrm/vendor/composer/autoload_psr4.php +++ b/civicrm/vendor/composer/autoload_psr4.php @@ -9,6 +9,7 @@ return array( 'Zend\\Validator\\' => array($vendorDir . '/zendframework/zend-validator/src'), 'Zend\\Stdlib\\' => array($vendorDir . '/zendframework/zend-stdlib/src'), 'Zend\\Escaper\\' => array($vendorDir . '/zendframework/zend-escaper/src'), + 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), 'PhpOffice\\PhpWord\\' => array($vendorDir . '/phpoffice/phpword/src/PhpWord'), 'PhpOffice\\Common\\' => array($vendorDir . '/phpoffice/common/src/Common'), diff --git a/civicrm/vendor/composer/autoload_real.php b/civicrm/vendor/composer/autoload_real.php index 3d4fb21e761faf42295e230e943bb5434fd468fa..a13baea823a87a75a05f9aabdf4629161461fada 100644 --- a/civicrm/vendor/composer/autoload_real.php +++ b/civicrm/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInit99d4995414f340fcb5325a17584ee78c +class ComposerAutoloaderInitd31b8d6552946245209cda886e0ea284 { private static $loader; @@ -19,9 +19,9 @@ class ComposerAutoloaderInit99d4995414f340fcb5325a17584ee78c return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInit99d4995414f340fcb5325a17584ee78c', 'loadClassLoader'), true, true); + spl_autoload_register(array('ComposerAutoloaderInitd31b8d6552946245209cda886e0ea284', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); - spl_autoload_unregister(array('ComposerAutoloaderInit99d4995414f340fcb5325a17584ee78c', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInitd31b8d6552946245209cda886e0ea284', 'loadClassLoader')); $includePaths = require __DIR__ . '/include_paths.php'; $includePaths[] = get_include_path(); @@ -31,7 +31,7 @@ class ComposerAutoloaderInit99d4995414f340fcb5325a17584ee78c if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInit99d4995414f340fcb5325a17584ee78c::getInitializer($loader)); + call_user_func(\Composer\Autoload\ComposerStaticInitd31b8d6552946245209cda886e0ea284::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { @@ -52,19 +52,19 @@ class ComposerAutoloaderInit99d4995414f340fcb5325a17584ee78c $loader->register(true); if ($useStaticLoader) { - $includeFiles = Composer\Autoload\ComposerStaticInit99d4995414f340fcb5325a17584ee78c::$files; + $includeFiles = Composer\Autoload\ComposerStaticInitd31b8d6552946245209cda886e0ea284::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { - composerRequire99d4995414f340fcb5325a17584ee78c($fileIdentifier, $file); + composerRequired31b8d6552946245209cda886e0ea284($fileIdentifier, $file); } return $loader; } } -function composerRequire99d4995414f340fcb5325a17584ee78c($fileIdentifier, $file) +function composerRequired31b8d6552946245209cda886e0ea284($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { require $file; diff --git a/civicrm/vendor/composer/autoload_static.php b/civicrm/vendor/composer/autoload_static.php index 2266cbf3166c83fae436f68b6e2485c8ce54698b..479cffc2e2c1988409aa33a6b2d7ee6566e945e6 100644 --- a/civicrm/vendor/composer/autoload_static.php +++ b/civicrm/vendor/composer/autoload_static.php @@ -4,7 +4,7 @@ namespace Composer\Autoload; -class ComposerStaticInit99d4995414f340fcb5325a17584ee78c +class ComposerStaticInitd31b8d6552946245209cda886e0ea284 { public static $files = array ( 'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php', @@ -24,6 +24,7 @@ class ComposerStaticInit99d4995414f340fcb5325a17584ee78c ), 'P' => array ( + 'Psr\\SimpleCache\\' => 16, 'Psr\\Http\\Message\\' => 17, 'PhpOffice\\PhpWord\\' => 18, 'PhpOffice\\Common\\' => 17, @@ -66,6 +67,10 @@ class ComposerStaticInit99d4995414f340fcb5325a17584ee78c array ( 0 => __DIR__ . '/..' . '/zendframework/zend-escaper/src', ), + 'Psr\\SimpleCache\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/simple-cache/src', + ), 'Psr\\Http\\Message\\' => array ( 0 => __DIR__ . '/..' . '/psr/http-message/src', @@ -371,10 +376,10 @@ class ComposerStaticInit99d4995414f340fcb5325a17584ee78c public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInit99d4995414f340fcb5325a17584ee78c::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInit99d4995414f340fcb5325a17584ee78c::$prefixDirsPsr4; - $loader->prefixesPsr0 = ComposerStaticInit99d4995414f340fcb5325a17584ee78c::$prefixesPsr0; - $loader->classMap = ComposerStaticInit99d4995414f340fcb5325a17584ee78c::$classMap; + $loader->prefixLengthsPsr4 = ComposerStaticInitd31b8d6552946245209cda886e0ea284::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitd31b8d6552946245209cda886e0ea284::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInitd31b8d6552946245209cda886e0ea284::$prefixesPsr0; + $loader->classMap = ComposerStaticInitd31b8d6552946245209cda886e0ea284::$classMap; }, null, ClassLoader::class); } diff --git a/civicrm/vendor/composer/installed.json b/civicrm/vendor/composer/installed.json index b7c16788d432272bc134b6e4dec8435e31c2d868..8bae0c3aa181b77c34b7a0be6dc274f1d41bfcaf 100644 --- a/civicrm/vendor/composer/installed.json +++ b/civicrm/vendor/composer/installed.json @@ -1166,6 +1166,56 @@ "psr-3" ] }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2017-10-23T01:57:42+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ] + }, { "name": "sabberworm/php-css-parser", "version": "6.0.1", diff --git a/civicrm/vendor/psr/simple-cache/.editorconfig b/civicrm/vendor/psr/simple-cache/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..48542cbb4180cdae02f4fccb0cc95a3d124e5c7d --- /dev/null +++ b/civicrm/vendor/psr/simple-cache/.editorconfig @@ -0,0 +1,12 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/civicrm/vendor/psr/simple-cache/LICENSE.md b/civicrm/vendor/psr/simple-cache/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..e49a7c85a10d6baea45427dbbf2dc97860d1b547 --- /dev/null +++ b/civicrm/vendor/psr/simple-cache/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2016 PHP Framework Interoperability Group + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/civicrm/vendor/psr/simple-cache/README.md b/civicrm/vendor/psr/simple-cache/README.md new file mode 100644 index 0000000000000000000000000000000000000000..43641d175ccbc07c5e29ac26408a395cae89f55c --- /dev/null +++ b/civicrm/vendor/psr/simple-cache/README.md @@ -0,0 +1,8 @@ +PHP FIG Simple Cache PSR +======================== + +This repository holds all interfaces related to PSR-16. + +Note that this is not a cache implementation of its own. It is merely an interface that describes a cache implementation. See [the specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md) for more details. + +You can find implementations of the specification by looking for packages providing the [psr/simple-cache-implementation](https://packagist.org/providers/psr/simple-cache-implementation) virtual package. diff --git a/civicrm/vendor/psr/simple-cache/composer.json b/civicrm/vendor/psr/simple-cache/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..2978fa559a7e3863ab6f511e67e6c0b29cb34fb6 --- /dev/null +++ b/civicrm/vendor/psr/simple-cache/composer.json @@ -0,0 +1,25 @@ +{ + "name": "psr/simple-cache", + "description": "Common interfaces for simple caching", + "keywords": ["psr", "psr-16", "cache", "simple-cache", "caching"], + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/civicrm/vendor/psr/simple-cache/src/CacheException.php b/civicrm/vendor/psr/simple-cache/src/CacheException.php new file mode 100644 index 0000000000000000000000000000000000000000..eba53815c0c987014014a7d62c9f48da16ad29b3 --- /dev/null +++ b/civicrm/vendor/psr/simple-cache/src/CacheException.php @@ -0,0 +1,10 @@ +<?php + +namespace Psr\SimpleCache; + +/** + * Interface used for all types of exceptions thrown by the implementing library. + */ +interface CacheException +{ +} diff --git a/civicrm/vendor/psr/simple-cache/src/CacheInterface.php b/civicrm/vendor/psr/simple-cache/src/CacheInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..99e8d9574884ae99f8336e3684ef036dd97764e5 --- /dev/null +++ b/civicrm/vendor/psr/simple-cache/src/CacheInterface.php @@ -0,0 +1,114 @@ +<?php + +namespace Psr\SimpleCache; + +interface CacheInterface +{ + /** + * Fetches a value from the cache. + * + * @param string $key The unique key of this item in the cache. + * @param mixed $default Default value to return if the key does not exist. + * + * @return mixed The value of the item from the cache, or $default in case of cache miss. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function get($key, $default = null); + + /** + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * @param string $key The key of the item to store. + * @param mixed $value The value of the item to store, must be serializable. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function set($key, $value, $ttl = null); + + /** + * Delete an item from the cache by its unique key. + * + * @param string $key The unique cache key of the item to delete. + * + * @return bool True if the item was successfully removed. False if there was an error. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function delete($key); + + /** + * Wipes clean the entire cache's keys. + * + * @return bool True on success and false on failure. + */ + public function clear(); + + /** + * Obtains multiple cache items by their unique keys. + * + * @param iterable $keys A list of keys that can obtained in a single operation. + * @param mixed $default Default value to return for keys that do not exist. + * + * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function getMultiple($keys, $default = null); + + /** + * Persists a set of key => value pairs in the cache, with an optional TTL. + * + * @param iterable $values A list of key => value pairs for a multiple-set operation. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $values is neither an array nor a Traversable, + * or if any of the $values are not a legal value. + */ + public function setMultiple($values, $ttl = null); + + /** + * Deletes multiple cache items in a single operation. + * + * @param iterable $keys A list of string-based keys to be deleted. + * + * @return bool True if the items were successfully removed. False if there was an error. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function deleteMultiple($keys); + + /** + * Determines whether an item is present in the cache. + * + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * + * @return bool + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function has($key); +} diff --git a/civicrm/vendor/psr/simple-cache/src/InvalidArgumentException.php b/civicrm/vendor/psr/simple-cache/src/InvalidArgumentException.php new file mode 100644 index 0000000000000000000000000000000000000000..6a9524a20c009663c4058f2a3a2c419c43e4f6a1 --- /dev/null +++ b/civicrm/vendor/psr/simple-cache/src/InvalidArgumentException.php @@ -0,0 +1,13 @@ +<?php + +namespace Psr\SimpleCache; + +/** + * Exception interface for invalid cache arguments. + * + * When an invalid argument is passed it must throw an exception which implements + * this interface + */ +interface InvalidArgumentException extends CacheException +{ +} diff --git a/civicrm/xml/schema/Core/CustomField.xml b/civicrm/xml/schema/Core/CustomField.xml index 0effe4046dbe2e39ed19bb39f9e434f45649460a..8d81309544f2910a1852834c8306f29b09143c72 100644 --- a/civicrm/xml/schema/Core/CustomField.xml +++ b/civicrm/xml/schema/Core/CustomField.xml @@ -262,6 +262,11 @@ <title>Field Option Group</title> <comment>For elements with options, the option group id that is used</comment> <add>1.4</add> + <pseudoconstant> + <table>civicrm_option_group</table> + <keyColumn>id</keyColumn> + <labelColumn>title</labelColumn> + </pseudoconstant> </field> <field> <name>filter</name> diff --git a/civicrm/xml/schema/Core/UFGroup.xml b/civicrm/xml/schema/Core/UFGroup.xml index 22faceb79cbc915addc9fe5470d157b54b74e268..6339d9c6cfd3c88dbfae6899ed3772de8c9594cb 100644 --- a/civicrm/xml/schema/Core/UFGroup.xml +++ b/civicrm/xml/schema/Core/UFGroup.xml @@ -281,4 +281,12 @@ <localizable>true</localizable> <add>4.7</add> </field> + <field> + <name>add_cancel_button</name> + <title>Include Cancel Button</title> + <type>boolean</type> + <default>1</default> + <comment>Should a Cancel button be included in this Profile form.</comment> + <add>5.0</add> + </field> </table> diff --git a/civicrm/xml/templates/message_templates/contribution_online_receipt_html.tpl b/civicrm/xml/templates/message_templates/contribution_online_receipt_html.tpl index ec976ed0e52579cd1878f42907ef7248e132ba8b..5f3c65aa17c897fb914d7d992a9dac3bcd53720d 100644 --- a/civicrm/xml/templates/message_templates/contribution_online_receipt_html.tpl +++ b/civicrm/xml/templates/message_templates/contribution_online_receipt_html.tpl @@ -21,7 +21,7 @@ <tr> <td> - + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $receipt_text} <p>{$receipt_text|htmlize}</p> {/if} diff --git a/civicrm/xml/templates/message_templates/contribution_online_receipt_text.tpl b/civicrm/xml/templates/message_templates/contribution_online_receipt_text.tpl index c6328bc46e8921d9a50670ff3cb1e68b9b9d3dfa..7f016a6d2a61c27144ca0acf17f736edc4e17177 100644 --- a/civicrm/xml/templates/message_templates/contribution_online_receipt_text.tpl +++ b/civicrm/xml/templates/message_templates/contribution_online_receipt_text.tpl @@ -1,3 +1,4 @@ +{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} {if $receipt_text} {$receipt_text} {/if} @@ -206,4 +207,4 @@ {$customName}: {$customValue} {/if} {/foreach} -{/if} \ No newline at end of file +{/if} diff --git a/civicrm/xml/templates/message_templates/event_online_receipt_html.tpl b/civicrm/xml/templates/message_templates/event_online_receipt_html.tpl index f7ebf3663efd15343b1bb75cea531928c0bc1d56..f6fe878351345beb4fbb3f84d3e27ac95496cac4 100644 --- a/civicrm/xml/templates/message_templates/event_online_receipt_html.tpl +++ b/civicrm/xml/templates/message_templates/event_online_receipt_html.tpl @@ -25,7 +25,7 @@ <tr> <td> - <p>{contact.email_greeting},</p> + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $event.confirm_email_text AND (not $isOnWaitlist AND not $isRequireApproval)} <p>{$event.confirm_email_text|htmlize}</p> @@ -177,7 +177,7 @@ </td> </tr> {/if} - {if $event.is_monetary} + {if $event.is_monetary and not $isRequireApproval} <tr> <th {$headerStyle}> diff --git a/civicrm/xml/templates/message_templates/event_online_receipt_subject.tpl b/civicrm/xml/templates/message_templates/event_online_receipt_subject.tpl index c847397dbb7b1bea44a209bdde25d4e668188d15..709fb37aad7a4a368a0b1824606ba371f43fe0eb 100644 --- a/civicrm/xml/templates/message_templates/event_online_receipt_subject.tpl +++ b/civicrm/xml/templates/message_templates/event_online_receipt_subject.tpl @@ -1 +1 @@ -{if $isOnWaitlist}{ts}Wait List Confirmation{/ts}{else}{ts}Registration Confirmation{/ts}{/if} - {$event.event_title} \ No newline at end of file +{if $isOnWaitlist}{ts}Wait List Confirmation{/ts}{elseif $isRequireApproval}{ts}Registration Request Confirmation{/ts}{else}{ts}Registration Confirmation{/ts}{/if} - {$event.event_title} diff --git a/civicrm/xml/templates/message_templates/event_online_receipt_text.tpl b/civicrm/xml/templates/message_templates/event_online_receipt_text.tpl index b0233b41ea55d2d484459951f0f740d329dc7778..be1e020d7036c366240a4f1577c7266440c99515 100644 --- a/civicrm/xml/templates/message_templates/event_online_receipt_text.tpl +++ b/civicrm/xml/templates/message_templates/event_online_receipt_text.tpl @@ -1,5 +1,4 @@ -{contact.email_greeting}, - +{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} {if $event.confirm_email_text AND (not $isOnWaitlist AND not $isRequireApproval)} {$event.confirm_email_text} @@ -101,7 +100,7 @@ {if $payer.name} You were registered by: {$payer.name} {/if} -{if $event.is_monetary} {* This section for Paid events only.*} +{if $event.is_monetary and not $isRequireApproval} {* This section for Paid events only.*} ==========================================================={if $pricesetFieldsCount }===================={/if} diff --git a/civicrm/xml/templates/message_templates/membership_online_receipt_html.tpl b/civicrm/xml/templates/message_templates/membership_online_receipt_html.tpl index 2e6136414f148edd0cd0fb99e2b5f8e67438b702..fbf9fdd460dff174680d97e84c99e5aaa6b4f346 100644 --- a/civicrm/xml/templates/message_templates/membership_online_receipt_html.tpl +++ b/civicrm/xml/templates/message_templates/membership_online_receipt_html.tpl @@ -21,7 +21,7 @@ <tr> <td> - + {assign var="greeting" value="{contact.email_greeting}"}{if $greeting}<p>{$greeting},</p>{/if} {if $receipt_text} <p>{$receipt_text|htmlize}</p> {/if} diff --git a/civicrm/xml/templates/message_templates/membership_online_receipt_text.tpl b/civicrm/xml/templates/message_templates/membership_online_receipt_text.tpl index aebd6d484ebba005bd41eac97aa0f7c536a6afb4..aa61b62a6c1097832e67833e82e633c2cce3dcf6 100644 --- a/civicrm/xml/templates/message_templates/membership_online_receipt_text.tpl +++ b/civicrm/xml/templates/message_templates/membership_online_receipt_text.tpl @@ -1,3 +1,4 @@ +{assign var="greeting" value="{contact.email_greeting}"}{if $greeting}{$greeting},{/if} {if $receipt_text} {$receipt_text} {/if} diff --git a/civicrm/xml/version.xml b/civicrm/xml/version.xml index 1cd4403134abf21004650a1f006be37c69d8f2d4..b760ae838d19eb2a81bf4e1821046638ec8aa61a 100644 --- a/civicrm/xml/version.xml +++ b/civicrm/xml/version.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="iso-8859-1" ?> <version> - <version_no>5.3.2</version_no> + <version_no>5.4.0</version_no> </version> diff --git a/includes/civicrm.basepage.php b/includes/civicrm.basepage.php index a9d0b525de7e13ca8126f7be8855440f54b09247..78762ba06897e5761d75fa3d10d172dc7e4a64cb 100644 --- a/includes/civicrm.basepage.php +++ b/includes/civicrm.basepage.php @@ -1,7 +1,7 @@ <?php /* +--------------------------------------------------------------------+ - | CiviCRM version 5 | + | CiviCRM version 4.7 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2018 | +--------------------------------------------------------------------+ diff --git a/includes/civicrm.shortcodes.modal.php b/includes/civicrm.shortcodes.modal.php index 6f249e1e308df5f587beeac0af5a4a4b6986d3bf..d606a8a0b44239a22832b34bcd555614af289821 100644 --- a/includes/civicrm.shortcodes.modal.php +++ b/includes/civicrm.shortcodes.modal.php @@ -1,7 +1,7 @@ <?php /* +--------------------------------------------------------------------+ - | CiviCRM version 5 | + | CiviCRM version 4.7 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2018 | +--------------------------------------------------------------------+ diff --git a/includes/civicrm.shortcodes.php b/includes/civicrm.shortcodes.php index 26e1b163cb8fbde7e6a37a4d97709e0a89759d77..833a6dec487311dafb5e08fdf4a79cd84f98f422 100644 --- a/includes/civicrm.shortcodes.php +++ b/includes/civicrm.shortcodes.php @@ -1,7 +1,7 @@ <?php /* +--------------------------------------------------------------------+ - | CiviCRM version 5 | + | CiviCRM version 4.7 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2018 | +--------------------------------------------------------------------+ diff --git a/includes/civicrm.users.php b/includes/civicrm.users.php index 800b8888eb01839c2dc2965d7e6a2aaad8310a3b..6c387d781854f30d9d303b138aad41297e22f7f0 100644 --- a/includes/civicrm.users.php +++ b/includes/civicrm.users.php @@ -1,7 +1,7 @@ <?php /* +--------------------------------------------------------------------+ - | CiviCRM version 5 | + | CiviCRM version 4.7 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2018 | +--------------------------------------------------------------------+ diff --git a/languages/civicrm.pot b/languages/civicrm.pot index 4e28eef65405b16e03f54f0622b165384f5ecd81..22349420e72fdbd042457fe592b7f43a0bd1c8a6 100644 --- a/languages/civicrm.pot +++ b/languages/civicrm.pot @@ -2,7 +2,7 @@ # This file is distributed under the same license as the CiviCRM package. msgid "" msgstr "" -"Project-Id-Version: CiviCRM 5\n" +"Project-Id-Version: CiviCRM 4.6\n" "Report-Msgid-Bugs-To: http://wordpress.org/tag/civicrm\n" "POT-Creation-Date: 2014-11-11 09:48:56+00:00\n" "MIME-Version: 1.0\n" diff --git a/uninstall.php b/uninstall.php index c73696a982a1d2e246e227c3ad4a7149e04f172c..7579c21f7061c9182f0ba4514ed42b6616ef6b16 100644 --- a/uninstall.php +++ b/uninstall.php @@ -2,7 +2,7 @@ /* +--------------------------------------------------------------------+ - | CiviCRM version 5 | + | CiviCRM version 4.7 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2018 | +--------------------------------------------------------------------+ diff --git a/wp-cli/civicrm.php b/wp-cli/civicrm.php index 4a02543027e944c97824dac49a3ce430e2c77290..a3c35c5521f2c23fde240fb128b5fa8f81ee92d5 100644 --- a/wp-cli/civicrm.php +++ b/wp-cli/civicrm.php @@ -2,7 +2,7 @@ /* +--------------------------------------------------------------------+ - | CiviCRM version 5 | + | CiviCRM version 4.7 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2018 | +--------------------------------------------------------------------+ @@ -379,7 +379,7 @@ if ( ! defined( 'CIVICRM_WPCLI_LOADED' ) ) { $upload_dir = wp_upload_dir(); $settings_dir = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR; civicrm_setup( $upload_dir['basedir'] . DIRECTORY_SEPARATOR ); - WP_CLI::launch( "chmod 0777 $settings_dir -R" ); + WP_CLI::launch( "chmod 0755 $settings_dir -R" ); # now we've got some files in place, require PEAR DB and check db setup $dsn = "mysql://{$dbuser}:{$dbpass}@{$dbhost}/{$dbname}?new_link=true"; @@ -654,7 +654,7 @@ if ( ! defined( 'CIVICRM_WPCLI_LOADED' ) ) { $restore_backup_dir .= '/plugins/restore/' . $date; - if ( ! mkdir( $restore_backup_dir, 777, true ) ) { + if ( ! mkdir( $restore_backup_dir, 0755, true ) ) { return WP_CLI::error( 'Failed creating directory: ' . $restore_backup_dir ); } @@ -999,7 +999,7 @@ if ( ! defined( 'CIVICRM_WPCLI_LOADED' ) ) { # begin upgrade $backup_dir .= '/plugins/' . $date; - if ( ! mkdir( $backup_dir, 777, true ) ) { + if ( ! mkdir( $backup_dir, 0755, true ) ) { return WP_CLI::error( 'Failed creating directory: ' . $backup_dir ); }