diff --git a/profiles/civicrm_starterkit/civicrm_starterkit.make b/profiles/civicrm_starterkit/civicrm_starterkit.make index 2234788ae4a..1259072c81a 100644 --- a/profiles/civicrm_starterkit/civicrm_starterkit.make +++ b/profiles/civicrm_starterkit/civicrm_starterkit.make @@ -13,7 +13,7 @@ projects[drupal][version] = "7.51" ; ====== CIVICRM RELATED ========= libraries[civicrm][download][type] = get -libraries[civicrm][download][url] = "https://download.civicrm.org/civicrm-5.3.1-drupal.tar.gz" +libraries[civicrm][download][url] = "https://download.civicrm.org/civicrm-5.5.1-drupal.tar.gz" libraries[civicrm][destination] = modules libraries[civicrm][directory_name] = civicrm @@ -22,7 +22,7 @@ libraries[civicrm][directory_name] = civicrm ; Private folders: https://civicrm.org/advisory/civi-sa-2014-001-risk-information-disclosure ; Define [civicrm.files] and [civicrm.private] paths since there is no htaccess file ; to set public/private folders. -libraries[civicrm][patch][pantheonsettings] = ./patches/pantheon-settings-starterkit-50.patch +libraries[civicrm][patch][pantheonsettings] = ./patches/pantheon-settings-starterkit-55.patch libraries[civicrm][patch][publicfiledir] = ./patches/public_files_config.patch ; Set session for cron. diff --git a/profiles/civicrm_starterkit/modules/civicrm/CONTRIBUTORS.txt b/profiles/civicrm_starterkit/modules/civicrm/CONTRIBUTORS.txt index 60a8f1ac4e3..c32000ea015 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CONTRIBUTORS.txt +++ b/profiles/civicrm_starterkit/modules/civicrm/CONTRIBUTORS.txt @@ -1,7 +1,7 @@ The following people and organizations sponsored and/or contributed new and improved features to the project. ************************************************ -Key Contributors and Sponsors for 5.x +Code Contributors for 5.x ************************************************ CiviCRM - Coleman Watts, Tim Otten @@ -9,17 +9,31 @@ 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 +Calibrate - Wannes De Roy +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 +eQuality Technology - Greg Rundlett +Freeform Solutions - Herb van den Dool 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 @@ -29,12 +43,20 @@ MJW Consulting - Matthew Wire myDropWizard - David Snopek Naomi Rosenberg Olivier Tétard +OSSeed Technologies - Madhavi Malgaonkar Oxfam Germany - Thomas Schüttler, Yuliyana Liyana +Pradeep Nayak Progressive Technology Project - Jamie McClelland +Richard van Oosterhout +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/profiles/civicrm_starterkit/modules/civicrm/CRM/ACL/BAO/ACL.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/ACL/BAO/ACL.php index cd9c25dfeee..15221f74565 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/ACL/BAO/ACL.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/ACL/BAO/ACL.php @@ -877,7 +877,7 @@ public static function group( $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/profiles/civicrm_starterkit/modules/civicrm/CRM/ACL/DAO/ACL.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/ACL/DAO/ACL.php index 97e9f1c151f..d5b37d89f3a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/ACL/DAO/ACL.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/ACL/DAO/ACL.php @@ -168,6 +168,7 @@ public static function &fields() { 'title' => ts('Deny ACL?'), 'description' => 'Is this ACL entry Allow (0) or Deny (1) ?', 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_acl', 'entity' => 'ACL', 'bao' => 'CRM_ACL_BAO_ACL', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/BAO/Activity.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/BAO/Activity.php index a0a6b8e4999..e7921e83cc4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/BAO/Activity.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/BAO/Activity.php @@ -920,8 +920,7 @@ public static function deprecatedGetActivities($input) { $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 @@ public static function deprecatedGetActivities($input) { // 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 ) ) @@ -1773,20 +1772,27 @@ public static function sendSMS( $smsProviderParams['To'] = ''; } - $sendResult = self::sendSMSMessage( - $contactId, - $tokenText, - $smsProviderParams, - $activityID, - $sourceContactId - ); + $doNotSms = CRM_Utils_Array::value('do_not_sms', $contact, 0); - if (PEAR::isError($sendResult)) { - // Collect all of the PEAR_Error objects - $errMsgs[] = $sendResult; + if ($doNotSms) { + $errMsgs[] = PEAR::raiseError('Contact Does not accept SMS', NULL, PEAR_ERROR_RETURN); } else { - $success++; + $sendResult = self::sendSMSMessage( + $contactId, + $tokenText, + $smsProviderParams, + $activityID, + $sourceContactId + ); + + if (PEAR::isError($sendResult)) { + // Collect all of the PEAR_Error objects + $errMsgs[] = $sendResult; + } + else { + $success++; + } } } @@ -1827,9 +1833,7 @@ public static function sendSMSMessage( $activityID, $sourceContactID = NULL ) { - $doNotSms = TRUE; $toPhoneNumber = NULL; - if ($smsProviderParams['To']) { // If phone number is specified use it $toPhoneNumber = trim($smsProviderParams['To']); @@ -1843,13 +1847,12 @@ public static function sendSMSMessage( $toPhoneNumberDetails = reset($toPhoneNumbers); $toPhoneNumber = CRM_Utils_Array::value('phone', $toPhoneNumberDetails); // Contact allows to send sms - $doNotSms = FALSE; } } // make sure both phone are valid // and that the recipient wants to receive sms - if (empty($toPhoneNumber) or $doNotSms) { + if (empty($toPhoneNumber)) { return PEAR::raiseError( 'Recipient phone number is invalid or recipient does not want to receive SMS', NULL, @@ -1857,7 +1860,7 @@ public static function sendSMSMessage( ); } - $recipient = $smsProviderParams['To']; + $recipient = $toPhoneNumber; $smsProviderParams['contact_id'] = $toID; $smsProviderParams['parent_activity_id'] = $activityID; @@ -2529,69 +2532,70 @@ public static function isOverdue($activity) { * @return array * array of exportable Fields */ - public static function &exportableFields($name = 'Activity') { - if (!isset(self::$_exportableFields[$name])) { - self::$_exportableFields[$name] = array(); - - // TODO: ideally we should retrieve all fields from xml, in this case since activity processing is done - // 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'] = array( - 'title' => ts('Source Contact'), - 'type' => CRM_Utils_Type::T_STRING, - ); + public static function exportableFields($name = 'Activity') { + self::$_exportableFields[$name] = array(); + + // TODO: ideally we should retrieve all fields from xml, in this case since activity processing is done + // 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'), + 'type' => CRM_Utils_Type::T_INT, + ]; + $exportableFields['source_contact'] = array( + 'title' => ts('Source Contact'), + 'type' => CRM_Utils_Type::T_STRING, + ); - $Activityfields = array( - 'activity_type' => array( - 'title' => ts('Activity Type'), - 'name' => 'activity_type', - 'type' => CRM_Utils_Type::T_STRING, - 'searchByLabel' => TRUE, - ), - 'activity_status' => array( - 'title' => ts('Activity Status'), - 'name' => 'activity_status', - 'type' => CRM_Utils_Type::T_STRING, - 'searchByLabel' => TRUE, - ), - 'activity_priority' => array( - 'title' => ts('Activity Priority'), - 'name' => 'activity_priority', - 'type' => CRM_Utils_Type::T_STRING, - 'searchByLabel' => TRUE, - ), - ); - $fields = array_merge($Activityfields, $exportableFields); - } - else { - // Set title to activity fields. - $fields = array( - 'case_activity_subject' => array('title' => ts('Activity Subject'), 'type' => CRM_Utils_Type::T_STRING), - 'case_source_contact_id' => array('title' => ts('Activity Reporter'), 'type' => CRM_Utils_Type::T_STRING), - 'case_recent_activity_date' => array('title' => ts('Activity Actual Date'), 'type' => CRM_Utils_Type::T_DATE), - 'case_scheduled_activity_date' => array( - 'title' => ts('Activity Scheduled Date'), - 'type' => CRM_Utils_Type::T_DATE, - ), - 'case_recent_activity_type' => array('title' => ts('Activity Type'), 'type' => CRM_Utils_Type::T_STRING), - 'case_activity_status' => array('title' => ts('Activity Status'), 'type' => CRM_Utils_Type::T_STRING), - 'case_activity_duration' => array('title' => ts('Activity Duration'), 'type' => CRM_Utils_Type::T_INT), - 'case_activity_medium_id' => array('title' => ts('Activity Medium'), 'type' => CRM_Utils_Type::T_INT), - 'case_activity_details' => array('title' => ts('Activity Details'), 'type' => CRM_Utils_Type::T_TEXT), - 'case_activity_is_auto' => array( - 'title' => ts('Activity Auto-generated?'), - 'type' => CRM_Utils_Type::T_BOOLEAN, - ), - ); - } + $Activityfields = array( + 'activity_type' => array( + 'title' => ts('Activity Type'), + 'name' => 'activity_type', + 'type' => CRM_Utils_Type::T_STRING, + 'searchByLabel' => TRUE, + ), + 'activity_status' => array( + 'title' => ts('Activity Status'), + 'name' => 'activity_status', + 'type' => CRM_Utils_Type::T_STRING, + 'searchByLabel' => TRUE, + ), + 'activity_priority' => array( + 'title' => ts('Activity Priority'), + 'name' => 'activity_priority', + 'type' => CRM_Utils_Type::T_STRING, + 'searchByLabel' => TRUE, + ), + ); + $fields = array_merge($Activityfields, $exportableFields); + } + else { + // Set title to activity fields. + $fields = array( + 'case_activity_subject' => array('title' => ts('Activity Subject'), 'type' => CRM_Utils_Type::T_STRING), + 'case_source_contact_id' => array('title' => ts('Activity Reporter'), 'type' => CRM_Utils_Type::T_STRING), + 'case_recent_activity_date' => array('title' => ts('Activity Actual Date'), 'type' => CRM_Utils_Type::T_DATE), + 'case_scheduled_activity_date' => array( + 'title' => ts('Activity Scheduled Date'), + 'type' => CRM_Utils_Type::T_DATE, + ), + 'case_recent_activity_type' => array('title' => ts('Activity Type'), 'type' => CRM_Utils_Type::T_STRING), + 'case_activity_status' => array('title' => ts('Activity Status'), 'type' => CRM_Utils_Type::T_STRING), + 'case_activity_duration' => array('title' => ts('Activity Duration'), 'type' => CRM_Utils_Type::T_INT), + 'case_activity_medium_id' => array('title' => ts('Activity Medium'), 'type' => CRM_Utils_Type::T_INT), + 'case_activity_details' => array('title' => ts('Activity Details'), 'type' => CRM_Utils_Type::T_TEXT), + 'case_activity_is_auto' => array( + 'title' => ts('Activity Auto-generated?'), + 'type' => CRM_Utils_Type::T_BOOLEAN, + ), + ); + } - // add custom data for case activities - $fields = array_merge($fields, CRM_Core_BAO_CustomField::getFieldsForImport('Activity')); + // add custom data for case activities + $fields = array_merge($fields, CRM_Core_BAO_CustomField::getFieldsForImport('Activity')); - self::$_exportableFields[$name] = $fields; - } + self::$_exportableFields[$name] = $fields; return self::$_exportableFields[$name]; } @@ -2831,6 +2835,22 @@ public static function checkPermission($activityId, $action) { return $allow; } + /** + * Checks if user has permissions to edit inbound e-mails, either bsic info + * or both basic information and content. + * + * @return bool + */ + public function checkEditInboundEmailsPermissions() { + if (CRM_Core_Permission::check('edit inbound email basic information') + || CRM_Core_Permission::check('edit inbound email basic information and content') + ) { + return TRUE; + } + + return FALSE; + } + /** * Wrapper for ajax activity selector. * diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/BAO/Query.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/BAO/Query.php index 724a30642a6..a450337ee2d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/BAO/Query.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/BAO/Query.php @@ -136,7 +136,13 @@ public static function select(&$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 @@ public static function whereClauseSingle(&$values, &$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; } } @@ -399,12 +413,16 @@ public static function from($name, $mode, $side) { break; case 'source_contact': - $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate'); - $sourceID = CRM_Utils_Array::key('Activity Source', $activityContacts); + $sourceID = CRM_Core_PseudoConstant::getKey( + 'CRM_Activity_BAO_ActivityContact', + 'record_type_id', + 'Activity Source' + ); $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)"; + LEFT JOIN civicrm_activity_contact source_activity + ON (source_activity.activity_id = civicrm_activity_contact.activity_id + AND source_activity.record_type_id = {$sourceID}) + LEFT JOIN civicrm_contact source_contact ON (source_activity.contact_id = source_contact.id)"; break; case 'parent_id': diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/DAO/Activity.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/DAO/Activity.php index c10f7d5b881..bf8a0be6140 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/DAO/Activity.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/DAO/Activity.php @@ -475,6 +475,7 @@ public static function &fields() { 'headerPattern' => '/(is.)?test(.activity)?/i', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_activity', 'entity' => 'Activity', 'bao' => 'CRM_Activity_BAO_Activity', @@ -505,6 +506,7 @@ public static function &fields() { 'name' => 'is_auto', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Auto'), + 'default' => '0', 'table_name' => 'civicrm_activity', 'entity' => 'Activity', 'bao' => 'CRM_Activity_BAO_Activity', @@ -575,6 +577,7 @@ public static function &fields() { 'headerPattern' => '/(activity.)?(trash|deleted)/i', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_activity', 'entity' => 'Activity', 'bao' => 'CRM_Activity_BAO_Activity', @@ -651,6 +654,7 @@ public static function &fields() { 'headerPattern' => '/(activity.)?(star|favorite)/i', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_activity', 'entity' => 'Activity', 'bao' => 'CRM_Activity_BAO_Activity', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Form/Activity.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Form/Activity.php index 5f78cbd9f1b..71d6ff01956 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Form/Activity.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Form/Activity.php @@ -308,6 +308,7 @@ public function preProcess() { CRM_Activity_BAO_Activity::checkPermission($this->_activityId, CRM_Core_Action::UPDATE) ) { $this->assign('permission', 'edit'); + $this->assign('allow_edit_inbound_emails', CRM_Activity_BAO_Activity::checkEditInboundEmailsPermissions()); } if (!$this->_activityTypeId && $this->_activityId) { @@ -516,10 +517,20 @@ public function preProcess() { $params = array('id' => $this->_activityId); CRM_Activity_BAO_Activity::retrieve($params, $this->_values); } + $this->set('values', $this->_values); } if ($this->_action & CRM_Core_Action::UPDATE) { + // We filter out alternatives, in case this is a stored e-mail, before sending to front-end + $this->_values['details'] = CRM_Utils_String::stripAlternatives($this->_values['details']) ?: ''; + + if ($this->_activityTypeName === 'Inbound Email' && + !CRM_Core_Permission::check('edit inbound email basic information and content') + ) { + $this->_fields['details']['type'] = 'static'; + } + CRM_Core_Form_RecurringEntity::preProcess('civicrm_activity'); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Form/Task.php index 254579876b0..3a052beec46 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() { * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Page/Tab.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Page/Tab.php index 8a4cd70d4ec..305c9dd8b6d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Page/Tab.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Page/Tab.php @@ -131,6 +131,7 @@ public function preProcess() { $this->_action = CRM_Utils_Request::retrieve('action', 'String', $this, FALSE, 'browse'); $this->assign('action', $this->_action); + $this->assign('allow_edit_inbound_emails', CRM_Activity_BAO_Activity::checkEditInboundEmailsPermissions()); // also create the form element for the activity links box $controller = new CRM_Core_Controller_Simple( diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Selector/Activity.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Selector/Activity.php index e7f2541bed8..8182c6409cf 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Selector/Activity.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Activity/Selector/Activity.php @@ -181,6 +181,13 @@ public static function actionLinks( case 'Inbound Email': $url = 'civicrm/contact/view/activity'; $qsView = "atype={$activityTypeId}&action=view&reset=1&id=%%id%%&cid=%%cid%%&context=%%cxt%%{$extraParams}"; + + if (CRM_Core_Permission::check('edit inbound email basic information') + || CRM_Core_Permission::check('edit inbound email basic information and content') + ) { + $showDelete = $showUpdate = TRUE; + $qsUpdate = "atype={$activityTypeId}&action=update&reset=1&id=%%id%%&cid=%%cid%%&context=%%cxt%%{$extraParams}"; + } break; case 'Open Case': diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Job.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Job.php index 74776dd0d41..e319f498ca6 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Job.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Job.php @@ -180,7 +180,7 @@ public function setDefaultValues() { */ 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/LocationType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/LocationType.php index 7979db2dabd..b1e1a977636 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/LocationType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/LocationType.php @@ -86,7 +86,7 @@ public function buildQuickForm() { * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/OptionGroup.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/OptionGroup.php index 059a5bf8786..0b76228f1c1 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/OptionGroup.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/OptionGroup.php @@ -78,7 +78,7 @@ public function buildQuickForm() { CRM_Core_DAO::getAttribute('CRM_Core_DAO_OptionGroup', 'description') ); - $this->addSelect('data_type', array('options' => CRM_Utils_Type::dataTypes()), TRUE); + $this->addSelect('data_type', array('options' => CRM_Utils_Type::dataTypes()), empty($this->_values['is_reserved'])); $element = $this->add('checkbox', 'is_active', ts('Enabled?')); if ($this->_action & CRM_Core_Action::UPDATE) { @@ -96,8 +96,12 @@ public function buildQuickForm() { $element->freeze(); } } + + $this->add('checkbox', 'is_reserved', ts('Reserved?')); + $this->freeze('is_reserved'); + if (!empty($this->_values['is_reserved'])) { - $this->freeze(array('name', 'is_active')); + $this->freeze(array('name', 'is_active', 'data_type')); } } @@ -110,22 +114,25 @@ public function buildQuickForm() { 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 ($this->_action & CRM_Core_Action::UPDATE) { - $ids['optionGroup'] = $this->_id; + if ($this->_action & CRM_Core_Action::ADD) { + // If we are adding option group via UI it should not be marked reserved. + if (!isset($params['is_reserved'])) { + $params['is_reserved'] = 0; + } + } + elseif ($this->_action & CRM_Core_Action::UPDATE) { + $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Options.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Options.php index f68718d881b..fd4aa9dfa18 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Options.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Options.php @@ -464,9 +464,9 @@ public function postProcess() { $params['reset_default_for'] = array('filter' => "0, " . $params['filter']); } - //make sure we should has to have space, CRM-6977 + //make sure we only have a single space, CRM-6977 and dev/mail/15 if ($this->_gName == 'from_email_address') { - $params['label'] = str_replace('"<', '" <', $params['label']); + $params['label'] = $this->sanitizeFromEmailAddress($params['label']); } } @@ -484,20 +484,7 @@ public function postProcess() { $params['color'] = 'null'; } - $groupParams = array('name' => ($this->_gName)); - $optionValue = CRM_Core_OptionValue::addOptionValue($params, $groupParams, $this->_action, $this->_id); - - // CRM-11516 - if (!empty($params['financial_account_id'])) { - $relationTypeId = key(CRM_Core_PseudoConstant::accountOptionValues('account_relationship', NULL, " AND v.name LIKE 'Asset Account is' ")); - $params = array( - 'entity_table' => 'civicrm_option_value', - 'entity_id' => $optionValue->id, - 'account_relationship' => $relationTypeId, - 'financial_account_id' => $params['financial_account_id'], - ); - CRM_Financial_BAO_FinancialTypeAccount::add($params); - } + $optionValue = CRM_Core_OptionValue::addOptionValue($params, $this->_gName, $this->_action, $this->_id); CRM_Core_Session::setStatus(ts('The %1 \'%2\' has been saved.', array( 1 => $this->_gLabel, @@ -508,4 +495,9 @@ public function postProcess() { } } + public function sanitizeFromEmailAddress($email) { + preg_match("/^\"(.*)\" *<([^@>]*@[^@>]*)>$/", $email, $parts); + return "\"{$parts[1]}\" <$parts[2]>"; + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/PaymentProcessorType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/PaymentProcessorType.php index f8161705674..6dfdaf7647c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/PaymentProcessorType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/PaymentProcessorType.php @@ -203,7 +203,7 @@ public function setDefaultValues() { * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/ScheduleReminders.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/ScheduleReminders.php index 106487c5e32..1067dad2c53 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/ScheduleReminders.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function buildQuickForm() { 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 @@ public function buildQuickForm() { array_combine(array_keys($entityRecipientLabels), array_keys($entityRecipientLabels)) )); - if (empty($this->_context)) { + if (!$this->getContext()) { $sel = &$this->add( 'hierselect', 'entity', @@ -158,7 +163,7 @@ public function buildQuickForm() { $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 @@ public function buildQuickForm() { ); 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 @@ public static function formRule($fields, $files, $self) { $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 @@ public function setDefaultValues() { $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 @@ public function postProcess() { // 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 @@ public function postProcess() { ); } - 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 @@ public function parseActionSchedule($values) { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting.php index e19ff512865..695f6f097df 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting.php @@ -233,6 +233,7 @@ public function commonProcess(&$params) { } 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting/Miscellaneous.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting/Miscellaneous.php index 5f0cb2c0246..31ef53a873c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting/Miscellaneous.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting/Miscellaneous.php @@ -49,6 +49,7 @@ class CRM_Admin_Form_Setting_Miscellaneous extends CRM_Admin_Form_Setting { 'recaptchaOptions' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'recaptchaPublicKey' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'recaptchaPrivateKey' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, + 'forceRecaptcha' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'wkhtmltopdfPath' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'recentItemsMaxCount' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'recentItemsProviders' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting/UpdateConfigBackend.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting/UpdateConfigBackend.php index 68711ff42aa..b5c7bb8c57b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting/UpdateConfigBackend.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Form/Setting/UpdateConfigBackend.php @@ -65,6 +65,7 @@ public function postProcess() { // clear all caches CRM_Core_Config::clearDBCache(); + Civi::cache('session')->clear(); CRM_Utils_System::flushCache(); parent::rebuildMenu(); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Page/AJAX.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Page/AJAX.php index 4854ffb9df7..1fef6d99e87 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Page/AJAX.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Page/AJAX.php @@ -123,7 +123,7 @@ public static function getStatusMsg() { $ret['content'] = ts('Are you sure you want to disable this CiviCRM Profile field?'); break; - case 'CRM_Contribute_BAO_ManagePremiums': + case 'CRM_Contribute_BAO_Product': $ret['content'] = ts('Are you sure you want to disable this premium? This action will remove the premium from any contribution pages that currently offer it. However it will not delete the premium record - so you can re-enable it and add it back to your contribution page(s) at a later time.'); break; @@ -265,8 +265,8 @@ static public function mappingList() { $recipientMapping = array_combine(array_keys($entityRecipientLabels), array_keys($entityRecipientLabels)); $output = array( - 'sel4' => CRM_Utils_Array::toKeyValueRows($dateFieldLabels), - 'sel5' => CRM_Utils_Array::toKeyValueRows($entityRecipientLabels), + 'sel4' => CRM_Utils_Array::makeNonAssociative($dateFieldLabels), + 'sel5' => CRM_Utils_Array::makeNonAssociative($entityRecipientLabels), 'recipientMapping' => $recipientMapping, ); @@ -291,7 +291,7 @@ public static function recipientListing() { )); CRM_Utils_JSON::output(array( - 'recipients' => CRM_Utils_Array::toKeyValueRows(CRM_Core_BAO_ActionSchedule::getRecipientListing($mappingID, $recipientType)), + 'recipients' => CRM_Utils_Array::makeNonAssociative(CRM_Core_BAO_ActionSchedule::getRecipientListing($mappingID, $recipientType)), )); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Page/Job.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Page/Job.php index 92237cccda2..9d9d31699d7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Page/Job.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Admin/Page/Job.php @@ -96,6 +96,12 @@ public function &links() { '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 @@ public function run() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Batch/BAO/Batch.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Batch/BAO/Batch.php index ceae0731229..ee73a515f07 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Batch/BAO/Batch.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Batch/BAO/Batch.php @@ -719,6 +719,8 @@ public static function getBatchFinancialItems($entityID, $returnValues, $notPres '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/BAO/Petition.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/BAO/Petition.php index 43246cc66f6..f8b0500ca27 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/BAO/Petition.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/BAO/Petition.php @@ -586,7 +586,7 @@ public static function sendEmail($params, $sendEmailMode) { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/DAO/Survey.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/DAO/Survey.php index ebb2c5275ac..935b3dda570 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/DAO/Survey.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/DAO/Survey.php @@ -349,6 +349,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Default Survey'), 'description' => 'Is this default survey?', + 'default' => '0', 'table_name' => 'civicrm_survey', 'entity' => 'Survey', 'bao' => 'CRM_Campaign_BAO_Survey', @@ -412,6 +413,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('No Email Verification'), 'description' => 'Bypass the email verification.', + 'default' => '0', 'table_name' => 'civicrm_survey', 'entity' => 'Survey', 'bao' => 'CRM_Campaign_BAO_Survey', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Form/SurveyType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Form/SurveyType.php index cad02eabe6b..0cce9e4b358 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Form/SurveyType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Form/SurveyType.php @@ -156,9 +156,8 @@ public function postProcess() { $params['filter'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionValue', $this->_id, 'filter', 'id'); } - $groupParams = array('name' => ($this->_gName)); $params['component_id'] = CRM_Core_Component::getComponentID('CiviCampaign'); - $optionValue = CRM_Core_OptionValue::addOptionValue($params, $groupParams, $this->_action, $this->_id); + $optionValue = CRM_Core_OptionValue::addOptionValue($params, $this->_gName, $this->_action, $this->_id); CRM_Core_Session::setStatus(ts('The Survey type \'%1\' has been saved.', array(1 => $optionValue->label)), ts('Saved'), 'success'); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Form/Task.php index 5d7bbd6a9bd..5ad1593f13d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/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 @@ -93,7 +65,7 @@ public function preProcess() { else { $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $this); $cacheKey = "civicrm search {$qfKey}"; - $allCids = CRM_Core_BAO_PrevNextCache::getSelection($cacheKey, "getall"); + $allCids = Civi::service('prevnext')->getSelection($cacheKey, "getall"); $ids = array_keys($allCids[$cacheKey]); $this->assign('totalSelectedVoters', count($ids)); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Selector/Search.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Selector/Search.php index 6f7d3fccab9..28c2b15b6d7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Selector/Search.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Campaign/Selector/Search.php @@ -271,7 +271,7 @@ public function buildPrevNextCache($sort) { if (!$crmPID) { $cacheKey = "civicrm search {$this->_key}"; - CRM_Core_BAO_PrevNextCache::deleteItem(NULL, $cacheKey, 'civicrm_contact'); + Civi::service('prevnext')->deleteItem(NULL, $cacheKey, 'civicrm_contact'); $sql = $this->_query->searchQuery(0, 0, $sort, FALSE, FALSE, @@ -281,18 +281,19 @@ public function buildPrevNextCache($sort) { $this->_campaignFromClause ); list($select, $from) = explode(' FROM ', $sql); - $insertSQL = " -INSERT INTO civicrm_prevnext_cache ( entity_table, entity_id1, entity_id2, cacheKey, data ) -SELECT 'civicrm_contact', contact_a.id, contact_a.id, '$cacheKey', contact_a.display_name + $selectSQL = " + SELECT 'civicrm_contact', contact_a.id, contact_a.id, '$cacheKey', contact_a.display_name FROM {$from} "; - $errorScope = CRM_Core_TemporaryErrorScope::ignoreException(); - $result = CRM_Core_DAO::executeQuery($insertSQL); - unset($errorScope); - if (is_a($result, 'DB_Error')) { + try { + Civi::service('prevnext')->fillWithSql($cacheKey, $selectSQL); + } + catch (CRM_Core_Exception $e) { + // Heavy handed, no? Seems like this merits an explanation. return; } + // also record an entry in the cache key table, so we can delete it periodically CRM_Core_BAO_Cache::setItem($cacheKey, 'CiviCRM Search PrevNextCache', $cacheKey); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/BAO/Case.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/BAO/Case.php index 8048c8ff75b..0d5f1d10c42 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/BAO/Case.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/BAO/Case.php @@ -274,16 +274,16 @@ public static function enableDisableCaseRelationships($caseId, $enable) { * 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; @@ -684,7 +684,7 @@ public static function getCases($allCases = TRUE, $params = array(), $context = $casesList[$key]['activity_list'] = sprintf('', ts('Activities'), - CRM_Utils_System::url('civicrm/case/details', array('caseid' => $case['case_id'], 'cid' => $case['contact_id'], 'type' => $type)) + CRM_Utils_System::url('civicrm/case/details', array('caseId' => $case['case_id'], 'cid' => $case['contact_id'], 'type' => $type)) ); $phone = empty($case['phone']) ? '' : '
' . $case['phone'] . ''; @@ -725,7 +725,7 @@ public static function getCases($allCases = TRUE, $params = array(), $context = } if (self::checkPermission($actId, 'edit', $case['activity_type_id'], $userID)) { $casesList[$key]['date'] .= sprintf('', - CRM_Utils_System::url('civicrm/case/activity', array('reset' => 1, 'cid' => $case['contact_id'], 'caseid' => $case['case_id'], 'action' => 'update')), + CRM_Utils_System::url('civicrm/case/activity', array('reset' => 1, 'cid' => $case['contact_id'], 'caseid' => $case['case_id'], 'action' => 'update', 'id' => $actId)), ts('Edit activity') ); } @@ -840,12 +840,13 @@ public static function getCasesSummary($allCases = TRUE) { * @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 @@ public static function getCaseRoles($contactID, $caseID, $relationshipID = NULL) 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 @@ public static function getCaseActivity($caseID, &$params, $contactID, $context = $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(); @@ -1586,7 +1590,7 @@ public static function getNextScheduledActivity($cases, $type = 'upcoming') { AND civicrm_case.id IN( {$caseID}) AND civicrm_case.is_deleted = {$cases['case_deleted']}"; - $query = self::getCaseActivityQuery($type, $userID, $condition, $cases['case_deleted']); + $query = self::getCaseActivityQuery($type, $userID, $condition); $res = CRM_Core_DAO::executeQuery($query); @@ -2741,6 +2745,12 @@ public static function checkPermission($activityId, $operation, $actTypeId = NUL //allow edit operation. $allowEditNames = array('Open Case'); + if (CRM_Core_Permission::check('edit inbound email basic information') || + CRM_Core_Permission::check('edit inbound email basic information and content') + ) { + $allowEditNames[] = 'Inbound Email'; + } + // do not allow File on Case $doNotFileNames = array( 'Open Case', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/BAO/CaseType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/BAO/CaseType.php index 42cc1baedde..89baec6098e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/BAO/CaseType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/BAO/CaseType.php @@ -242,6 +242,8 @@ public static function convertXmlToDefinition($xml) { // 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 @@ public static function convertXmlToDefinition($xml) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/DAO/Case.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/DAO/Case.php index 514544ec04c..b881d040064 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/DAO/Case.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/DAO/Case.php @@ -271,6 +271,7 @@ public static function &fields() { 'headerPattern' => '', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_case', 'entity' => 'Case', 'bao' => 'CRM_Case_BAO_Case', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/Activity/ChangeCaseStatus.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/Activity/ChangeCaseStatus.php index c00901c9abc..3452a51ea79 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/Activity/ChangeCaseStatus.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/Activity/ChangeCaseStatus.php @@ -194,7 +194,7 @@ public static function endPostProcess(&$form, &$params, $activity) { // 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/ActivityView.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/ActivityView.php index e18b086d190..16247cda225 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/ActivityView.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/ActivityView.php @@ -172,6 +172,38 @@ public function preProcess() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/Task.php index b9d05e68758..513ee62e050 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Form/Task.php @@ -49,4 +49,13 @@ public function setContactIDs() { ); } + /** + * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Task.php index 956ff16662e..954786cde55 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Task.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/Task.php @@ -67,7 +67,7 @@ public static function tasks() { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/XMLProcessor/Process.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/XMLProcessor/Process.php index f125355ec93..1929c38fb72 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Case/XMLProcessor/Process.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function activityTypes($activityTypesXML, $maxInst = FALSE, $isLabel = FA /** * @param SimpleXMLElement $caseTypeXML + * * @return array symbolic activity-type names */ public function getDeclaredActivityTypes($caseTypeXML) { @@ -342,6 +345,7 @@ public function getDeclaredActivityTypes($caseTypeXML) { /** * @param SimpleXMLElement $caseTypeXML + * * @return array symbolic relationship-type names */ public function getDeclaredRelationshipTypes($caseTypeXML) { @@ -474,6 +478,8 @@ public function createActivity($activityTypeXML, &$params) { ); } + $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 @@ public function createActivity($activityTypeXML, &$params) { 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 @@ public function getCaseManagerRoleId($caseType) { /** * @param string $caseType + * * @return array<\Civi\CCase\CaseChangeListener> */ public function getListeners($caseType) { @@ -662,6 +818,7 @@ public function getNaturalActivityTypeSort() { * @param string $settingKey * @param string $xmlTag * @param mixed $default + * * @return int */ private function getBoolSetting($settingKey, $xmlTag, $default = 0) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact.php index 01499868630..9e4488208c0 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact.php @@ -1013,7 +1013,10 @@ public static function deleteContact($id, $restore = FALSE, $skipUndelete = FALS CRM_Utils_Recent::delContact($id); self::updateContactCache($id, empty($restore)); - // delete any dupe cache entry + // delete any prevnext/dupe cache entry + // These two calls are redundant in default deployments, but they're + // meaningful if "prevnext" is memory-backed. + Civi::service('prevnext')->deleteItem($id); CRM_Core_BAO_PrevNextCache::deleteItem($id); $transaction->commit(); @@ -1022,10 +1025,6 @@ public static function deleteContact($id, $restore = FALSE, $skipUndelete = FALS 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact/Permission.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact/Permission.php index 09b8925ede7..4896b03ad8b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact/Permission.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact/Permission.php @@ -116,7 +116,7 @@ public static function allowList($contact_ids, $type = CRM_Core_Permission::VIEW if (count($result_set) < count($contact_ids)) { $rejected_contacts = array_diff_key($contact_ids, $result_set); // @todo consider storing these to the acl cache for next time, since we have fetched. - $allowed_by_relationship = self::relationshipList($rejected_contacts); + $allowed_by_relationship = self::relationshipList($rejected_contacts, $type); foreach ($allowed_by_relationship as $contact_id) { $result_set[(int) $contact_id] = TRUE; } @@ -161,7 +161,7 @@ public static function allow($id, $type = CRM_Core_Permission::VIEW) { } // check permission based on relationship, CRM-2963 - if (self::relationshipList(array($id))) { + if (self::relationshipList(array($id), $type)) { return TRUE; } @@ -330,10 +330,13 @@ public static function cacheSubquery() { * @param array $contact_ids * List of contact IDs to be filtered * + * @param int $type + * access type CRM_Core_Permission::VIEW or CRM_Core_Permission::EDIT + * * @return array * List of contact IDs that the user has permissions for */ - public static function relationshipList($contact_ids) { + public static function relationshipList($contact_ids, $type) { $result_set = array(); // no processing empty lists (avoid SQL errors as well) @@ -351,9 +354,17 @@ public static function relationshipList($contact_ids) { $queries = array(); $contact_id_list = implode(',', $contact_ids); - // add a select statement for each direection + // add a select statement for each direction $directions = array(array('from' => 'a', 'to' => 'b'), array('from' => 'b', 'to' => 'a')); + // CRM_Core_Permission::VIEW is satisfied by either CRM_Contact_BAO_Relationship::VIEW or CRM_Contact_BAO_Relationship::EDIT + if ($type == CRM_Core_Permission::VIEW) { + $is_perm_condition = ' IN ( ' . CRM_Contact_BAO_Relationship::EDIT . ' , ' . CRM_Contact_BAO_Relationship::VIEW . ' ) '; + } + else { + $is_perm_condition = ' = ' . CRM_Contact_BAO_Relationship::EDIT; + } + // NORMAL/SINGLE DEGREE RELATIONSHIPS foreach ($directions as $direction) { $user_id_column = "contact_id_{$direction['from']}"; @@ -373,7 +384,7 @@ public static function relationshipList($contact_ids) { WHERE civicrm_relationship.{$user_id_column} = {$contactID} AND civicrm_relationship.{$contact_id_column} IN ({$contact_id_list}) AND civicrm_relationship.is_active = 1 - AND civicrm_relationship.is_permission_{$direction['from']}_{$direction['to']} = 1 + AND civicrm_relationship.is_permission_{$direction['from']}_{$direction['to']} {$is_perm_condition} $AND_CAN_ACCESS_DELETED"; } @@ -394,14 +405,14 @@ public static function relationshipList($contact_ids) { $queries[] = " SELECT second_degree_relationship.contact_id_{$second_direction['to']} AS contact_id FROM civicrm_relationship first_degree_relationship - LEFT JOIN civicrm_relationship second_degree_relationship ON first_degree_relationship.contact_id_{$first_direction['to']} = second_degree_relationship.contact_id_{$first_direction['from']} + LEFT JOIN civicrm_relationship second_degree_relationship ON first_degree_relationship.contact_id_{$first_direction['to']} = second_degree_relationship.contact_id_{$second_direction['from']} {$LEFT_JOIN_DELETED} WHERE first_degree_relationship.contact_id_{$first_direction['from']} = {$contactID} AND second_degree_relationship.contact_id_{$second_direction['to']} IN ({$contact_id_list}) AND first_degree_relationship.is_active = 1 - AND first_degree_relationship.is_permission_{$first_direction['from']}_{$first_direction['to']} = 1 + AND first_degree_relationship.is_permission_{$first_direction['from']}_{$first_direction['to']} {$is_perm_condition} AND second_degree_relationship.is_active = 1 - AND second_degree_relationship.is_permission_{$second_direction['from']}_{$second_direction['to']} = 1 + AND second_degree_relationship.is_permission_{$second_direction['from']}_{$second_direction['to']} {$is_perm_condition} $AND_CAN_ACCESS_DELETED"; } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact/Utils.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact/Utils.php index f3b391362af..2cee6e99dcb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact/Utils.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Contact/Utils.php @@ -917,6 +917,9 @@ public static function clearContactCaches($isEmptyPrevNextTable = FALSE) { return; } if ($isEmptyPrevNextTable) { + // These two calls are redundant in default deployments, but they're + // meaningful if "prevnext" is memory-backed. + Civi::service('prevnext')->deleteItem(); CRM_Core_BAO_PrevNextCache::deleteItem(); } // clear acl cache if any. diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/ContactType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/ContactType.php index a299e9e5911..cb13346f556 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/ContactType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/ContactType.php @@ -387,6 +387,7 @@ public static function getSelectElements( $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Group.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Group.php index 759a6b3eace..8c08654256d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Group.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Group.php @@ -882,7 +882,7 @@ public static function getGroupList(&$params) { // 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 @@ public static function whereClause(&$params, $sortBy = TRUE, $excludeHidden = TR * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Query.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Query.php index 4e02c364dde..ef6d06646de 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Query.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Query.php @@ -1612,7 +1612,7 @@ public static function convertFormValues(&$formValues, $wildcard = 0, $useEquals $contactType = array(); $subType = array(); foreach ((array) $values as $key => $type) { - $types = explode('__', is_numeric($type) ? $key : $type); + $types = explode('__', is_numeric($type) ? $key : $type, 2); $contactType[$types[0]] = $types[0]; // Add sub-type if specified if (!empty($types[1])) { @@ -2341,7 +2341,7 @@ public function restWhere(&$values) { $this->_where[$grouping][] = CRM_Core_DAO::createSQLFilter($fieldName, $value, $type); } else { - if (!strpos($op, 'IN')) { + if (!self::caseImportant($op)) { $value = $strtolower($value); } if ($wildcard) { @@ -3558,14 +3558,13 @@ public function street_address(&$values) { $n = trim($value); if ($n) { - $value = strtolower($n); if (strpos($value, '%') === FALSE) { // only add wild card if not there $value = "%{$value}%"; } $op = 'LIKE'; // LOWER roughly translates to 'hurt my database without deriving any benefit' See CRM-19811. - $this->_where[$grouping][] = self::buildClause('LOWER(civicrm_address.street_address)', $op, $value, 'String'); + $this->_where[$grouping][] = self::buildClause('civicrm_address.street_address', $op, $value, 'String'); $this->_qill[$grouping][] = ts('Street') . " $op '$n'"; } else { @@ -4042,7 +4041,6 @@ public function relationship(&$values) { $relationType = $this->getWhereValues('relation_type_id', $grouping); $targetName = $this->getWhereValues('relation_target_name', $grouping); $relStatus = $this->getWhereValues('relation_status', $grouping); - $relPermission = $this->getWhereValues('relation_permission', $grouping); $targetGroup = $this->getWhereValues('relation_target_group', $grouping); $nameClause = $name = NULL; @@ -4066,7 +4064,7 @@ public function relationship(&$values) { $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); @@ -4188,21 +4186,7 @@ public function relationship(&$values) { } $where[$grouping][] = "(contact_b.is_deleted = {$onlyDeleted})"; - //check for permissioned, non-permissioned and all permissioned relations - if ($relPermission[2] == 1) { - $where[$grouping][] = "( -civicrm_relationship.is_permission_a_b = 1 -)"; - $this->_qill[$grouping][] = ts('Relationship - Permissioned'); - } - elseif ($relPermission[2] == 2) { - //non-allowed permission relationship. - $where[$grouping][] = "( -civicrm_relationship.is_permission_a_b = 0 -)"; - $this->_qill[$grouping][] = ts('Relationship - Non-permissioned'); - } - + $this->addRelationshipPermissionClauses($grouping, $where); $this->addRelationshipDateClauses($grouping, $where); $this->addRelationshipActivePeriodClauses($grouping, $where); if (!empty($relTypes)) { @@ -4235,6 +4219,27 @@ public function relationship(&$values) { } } + public function addRelationshipPermissionClauses($grouping, &$where) { + $relPermission = $this->getWhereValues('relation_permission', $grouping); + if ($relPermission) { + if (!is_array($relPermission[2])) { + // this form value was scalar in previous versions of Civi + $relPermission[2] = array($relPermission[2]); + } + $where[$grouping][] = "(civicrm_relationship.is_permission_a_b IN (" . implode(",", $relPermission[2]) . "))"; + + $allRelationshipPermissions = CRM_Contact_BAO_Relationship::buildOptions('is_permission_a_b'); + $relQill = ''; + foreach ($relPermission[2] as $rel) { + if (!empty($relQill)) { + $relQill .= ' OR '; + } + $relQill .= ts($allRelationshipPermissions[$rel]); + } + $this->_qill[$grouping][] = ts('Permissioned Relationships') . ' - ' . $relQill; + } + } + /** * Add start & end date criteria in * @param string $grouping @@ -5662,13 +5667,16 @@ public static function buildClause($field, $op, $value = NULL, $dataType = NULL) 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 'RLIKE': + return " {$clause} BINARY '{$value}' "; + case 'IN': case 'NOT IN': // I feel like this would be escaped properly if passed through $queryString = CRM_Core_DAO::createSqlFilter. @@ -5677,7 +5685,7 @@ public static function buildClause($field, $op, $value = NULL, $dataType = NULL) } default: - if (empty($dataType)) { + if (empty($dataType) || $dataType == 'Date') { $dataType = 'String'; } if (is_array($value)) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Relationship.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Relationship.php index 67c95393882..df64ddcbba8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Relationship.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/BAO/Relationship.php @@ -37,6 +37,12 @@ class CRM_Contact_BAO_Relationship extends CRM_Contact_DAO_Relationship { */ const ALL = 0, PAST = 1, DISABLED = 2, CURRENT = 4, INACTIVE = 8; + /** + * Constants for is_permission fields. + * Note: the slightly non-obvious ordering is due to history... + */ + const NONE = 0, EDIT = 1, VIEW = 2; + /** * Create function - use the API instead. * @@ -470,8 +476,8 @@ public static function setContactABFromIDs($params, $ids = array(), $contactID = public static function getdefaults() { return array( 'is_active' => 0, - 'is_permission_a_b' => 0, - 'is_permission_b_a' => 0, + 'is_permission_a_b' => self::NONE, + 'is_permission_b_a' => self::NONE, 'description' => '', 'start_date' => 'NULL', 'case_id' => NULL, @@ -656,7 +662,7 @@ public static function clearCurrentEmployer($id, $action) { $relTypes = CRM_Utils_Array::index(array('name_a_b'), CRM_Core_PseudoConstant::relationshipType('name')); if ( (isset($relTypes['Employee of']) && $relationship->relationship_type_id == $relTypes['Employee of']['id']) || - (isset ($relTypes['Household Member of']) && $relationship->relationship_type_id == $relTypes['Household Member of']['id']) + (isset($relTypes['Household Member of']) && $relationship->relationship_type_id == $relTypes['Household Member of']['id']) ) { $sharedContact = new CRM_Contact_DAO_Contact(); $sharedContact->id = $relationship->contact_id_a; @@ -1715,10 +1721,10 @@ public static function relatedMemberships($contactId, &$params, $ids, $action = $membershipValues['skipStatusCal'] = TRUE; } foreach (array( - 'join_date', - 'start_date', - 'end_date', - ) as $dateField) { + 'join_date', + 'start_date', + 'end_date', + ) as $dateField) { if (!empty($membershipValues[$dateField])) { $membershipValues[$dateField] = CRM_Utils_Date::processDate($membershipValues[$dateField]); } @@ -2087,6 +2093,8 @@ public static function getContactRelationshipSelector(&$params) { unset($relationships['total_relationships']); if (!empty($relationships)) { + $displayName = CRM_Contact_BAO_Contact::displayName($params['contact_id']); + // format params foreach ($relationships as $relationshipId => $values) { $relationship = array(); @@ -2117,24 +2125,44 @@ public static function getContactRelationshipSelector(&$params) { 'civicrm/contact/view/rel', "action=view&reset=1&cid={$values['cid']}&id={$values['id']}&rtype={$values['rtype']}"); - if ($params['context'] == 'current') { - if (($params['contact_id'] == $values['contact_id_a'] AND $values['is_permission_a_b'] == 1) OR - ($params['contact_id'] == $values['contact_id_b'] AND $values['is_permission_b_a'] == 1) - ) { - $relationship['sort_name'] .= ' *'; - } - - if (($values['cid'] == $values['contact_id_a'] AND $values['is_permission_a_b'] == 1) OR - ($values['cid'] == $values['contact_id_b'] AND $values['is_permission_b_a'] == 1) - ) { - $relationship['relation'] .= ' *'; - } - } - if (!empty($values['description'])) { $relationship['relation'] .= "

{$values['description']}

"; } + if ($params['context'] == 'current') { + $smarty = CRM_Core_Smarty::singleton(); + + $contactCombos = [ + [ + 'permContact' => $params['contact_id'], + 'permDisplayName' => $displayName, + 'otherContact' => $values['cid'], + 'otherDisplayName' => $values['display_name'], + 'columnKey' => 'sort_name', + ], + [ + 'permContact' => $values['cid'], + 'permDisplayName' => $values['display_name'], + 'otherContact' => $params['contact_id'], + 'otherDisplayName' => $displayName, + 'columnKey' => 'relation', + ], + ]; + + foreach ($contactCombos as $combo) { + foreach ([CRM_Contact_BAO_Relationship::EDIT, CRM_Contact_BAO_Relationship::VIEW] as $permType) { + $smarty->assign('permType', $permType); + if (($combo['permContact'] == $values['contact_id_a'] and $values['is_permission_a_b'] == $permType) + || ($combo['permContact'] == $values['contact_id_b'] and $values['is_permission_b_a'] == $permType) + ) { + $smarty->assign('permDisplayName', $combo['permDisplayName']); + $smarty->assign('otherDisplayName', $combo['otherDisplayName']); + $relationship[$combo['columnKey']] .= $smarty->fetch('CRM/Contact/Page/View/RelationshipPerm.tpl'); + } + } + } + } + $relationship['start_date'] = CRM_Utils_Date::customFormat($values['start_date']); $relationship['end_date'] = CRM_Utils_Date::customFormat($values['end_date']); $relationship['city'] = $values['city']; diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Contact.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Contact.php index 59e9108b714..d7049145cb1 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Contact.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Contact.php @@ -487,6 +487,7 @@ public static function &fields() { 'headerPattern' => '/d(o )?(not )?(email)/i', 'dataPattern' => '/^\d{1,}$/', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_contact', 'entity' => 'Contact', 'bao' => 'CRM_Contact_BAO_Contact', @@ -504,6 +505,7 @@ public static function &fields() { 'headerPattern' => '/d(o )?(not )?(call|phone)/i', 'dataPattern' => '/^\d{1,}$/', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_contact', 'entity' => 'Contact', 'bao' => 'CRM_Contact_BAO_Contact', @@ -521,6 +523,7 @@ public static function &fields() { 'headerPattern' => '/^(d(o\s)?n(ot\s)?mail)|(\w*)?bulk\s?(\w*)$/i', 'dataPattern' => '/^\d{1,}$/', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_contact', 'entity' => 'Contact', 'bao' => 'CRM_Contact_BAO_Contact', @@ -538,6 +541,7 @@ public static function &fields() { 'headerPattern' => '/d(o )?(not )?(sms)/i', 'dataPattern' => '/^\d{1,}$/', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_contact', 'entity' => 'Contact', 'bao' => 'CRM_Contact_BAO_Contact', @@ -555,6 +559,7 @@ public static function &fields() { 'headerPattern' => '/d(o )?(not )?(trade)/i', 'dataPattern' => '/^\d{1,}$/', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_contact', 'entity' => 'Contact', 'bao' => 'CRM_Contact_BAO_Contact', @@ -574,6 +579,7 @@ public static function &fields() { 'headerPattern' => '', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_contact', 'entity' => 'Contact', 'bao' => 'CRM_Contact_BAO_Contact', @@ -1218,6 +1224,7 @@ public static function &fields() { 'headerPattern' => '/i(s\s)?d(eceased)$/i', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_contact', 'entity' => 'Contact', 'bao' => 'CRM_Contact_BAO_Contact', @@ -1367,6 +1374,7 @@ public static function &fields() { 'where' => 'civicrm_contact.is_deleted', 'headerPattern' => '', 'dataPattern' => '', + 'default' => '0', 'table_name' => 'civicrm_contact', 'entity' => 'Contact', 'bao' => 'CRM_Contact_BAO_Contact', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/DashboardContact.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/DashboardContact.php index e3eb9e07bd9..a6538b2c699 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/DashboardContact.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/DashboardContact.php @@ -139,6 +139,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Column No'), 'description' => 'column no for this widget', + 'default' => '0', 'table_name' => 'civicrm_dashboard_contact', 'entity' => 'DashboardContact', 'bao' => 'CRM_Contact_BAO_DashboardContact', @@ -149,6 +150,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Dashlet is Active?'), 'description' => 'Is this widget active?', + 'default' => '0', 'table_name' => 'civicrm_dashboard_contact', 'entity' => 'DashboardContact', 'bao' => 'CRM_Contact_BAO_DashboardContact', @@ -159,6 +161,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Order'), 'description' => 'Ordering of the widgets.', + 'default' => '0', 'table_name' => 'civicrm_dashboard_contact', 'entity' => 'DashboardContact', 'bao' => 'CRM_Contact_BAO_DashboardContact', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Group.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Group.php index e1de1bb5f87..d0b22f0c736 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Group.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Group.php @@ -401,6 +401,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Group is Hidden'), 'description' => 'Is this group hidden?', + 'default' => '0', 'table_name' => 'civicrm_group', 'entity' => 'Group', 'bao' => 'CRM_Contact_BAO_Group', @@ -410,6 +411,7 @@ public static function &fields() { 'name' => 'is_reserved', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Group is Reserved'), + 'default' => '0', 'table_name' => 'civicrm_group', 'entity' => 'Group', 'bao' => 'CRM_Contact_BAO_Group', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Relationship.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Relationship.php index f61c5c220fd..740d0a75254 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Relationship.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/Relationship.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Contact/Relationship.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:a5a833da9d5016f0aeb06ba7c1058b3c) + * (GenCodeChecksum:49381da59affbf165a4c9ce87c9a68ec) */ /** @@ -85,18 +85,16 @@ class CRM_Contact_DAO_Relationship extends CRM_Core_DAO { public $description; /** - * is contact a has permission to view / edit contact and - related data for contact b ? + * Permission that Contact A has to view/update Contact B * - * @var boolean + * @var int unsigned */ public $is_permission_a_b; /** - * is contact b has permission to view / edit contact and - related data for contact a ? + * Permission that Contact B has to view/update Contact A * - * @var boolean + * @var int unsigned */ public $is_permission_b_a; @@ -251,33 +249,39 @@ public static function &fields() { ], 'is_permission_a_b' => [ 'name' => 'is_permission_a_b', - 'type' => CRM_Utils_Type::T_BOOLEAN, + 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Contact A has Permission Over Contact B'), - 'description' => 'is contact a has permission to view / edit contact and - related data for contact b ? - ', + 'description' => 'Permission that Contact A has to view/update Contact B', + 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_relationship', 'entity' => 'Relationship', 'bao' => 'CRM_Contact_BAO_Relationship', 'localizable' => 0, 'html' => [ - 'type' => 'CheckBox', + 'type' => 'Radio', ], + 'pseudoconstant' => [ + 'callback' => 'CRM_Core_SelectValues::getPermissionedRelationshipOptions', + ] ], 'is_permission_b_a' => [ 'name' => 'is_permission_b_a', - 'type' => CRM_Utils_Type::T_BOOLEAN, + 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Contact B has Permission Over Contact A'), - 'description' => 'is contact b has permission to view / edit contact and - related data for contact a ? - ', + 'description' => 'Permission that Contact B has to view/update Contact A', + 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_relationship', 'entity' => 'Relationship', 'bao' => 'CRM_Contact_BAO_Relationship', 'localizable' => 0, 'html' => [ - 'type' => 'CheckBox', + 'type' => 'Radio', ], + 'pseudoconstant' => [ + 'callback' => 'CRM_Core_SelectValues::getPermissionedRelationshipOptions', + ] ], 'case_id' => [ 'name' => 'case_id', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/SavedSearch.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/SavedSearch.php index 97ecac5207f..91db5b3474d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/SavedSearch.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/DAO/SavedSearch.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Contact/SavedSearch.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:0341555885ddaf5dbc8e4d6cfe1d899c) + * (GenCodeChecksum:4b183aa40bb90cc7c715034178104a11) */ /** diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Contact.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Contact.php index 7f1d039b1a6..1deb5ea20a1 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Contact.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Contact.php @@ -714,7 +714,7 @@ public static function formRule($fields, &$errors, $contactId, $contactType) { } // Check for duplicate contact if it wasn't already handled by ajax or disabled - if (!Civi::settings()->get('contact_ajax_check_similar')) { + if (!Civi::settings()->get('contact_ajax_check_similar') || !empty($fields['_qf_Contact_refresh_dedupe'])) { self::checkDuplicateContacts($fields, $errors, $contactId, $contactType); } @@ -760,7 +760,7 @@ public function buildQuickForm() { $className::buildQuickForm($this); // Ajax duplicate checking - $checkSimilar = $this->_action == CRM_Core_Action::ADD && Civi::settings()->get('contact_ajax_check_similar'); + $checkSimilar = Civi::settings()->get('contact_ajax_check_similar'); $this->assign('checkSimilar', $checkSimilar); if ($checkSimilar == 1) { $ruleParams = array('used' => 'Supervised', 'contact_type' => $this->_contactType); @@ -1461,7 +1461,7 @@ public function updateMembershipStatus($deceasedParams) { '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Email.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Email.php index 489f5e7b326..47ed4a44891 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Email.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Email.php @@ -58,7 +58,7 @@ public static function buildQuickForm(&$form, $blockCount = NULL, $blockEdit = F $form->applyFilter('__ALL__', 'trim'); //Email box - $form->addField("email[$blockId][email]", array('entity' => 'email')); + $form->addField("email[$blockId][email]", array('entity' => 'email', 'aria-label' => ts('Email %1', [1 => $blockId]))); $form->addRule("email[$blockId][email]", ts('Email is not valid.'), 'email'); if (isset($form->_contactType) || $blockEdit) { //Block type @@ -77,17 +77,16 @@ public static function buildQuickForm(&$form, $blockCount = NULL, $blockEdit = F $form->addElement('select', "email[$blockId][on_hold]", '', $holdOptions); } else { - $form->addField("email[$blockId][on_hold]", array('entity' => 'email', 'type' => 'advcheckbox')); + $form->addField("email[$blockId][on_hold]", array('entity' => 'email', 'type' => 'advcheckbox', 'aria-label' => ts('On Hold for Email %1?', [1 => $blockId]))); } //Bulkmail checkbox $form->assign('multipleBulk', $multipleBulk); + $js = array('id' => "Email_" . $blockId . "_IsBulkmail" , 'aria-label' => ts('Bulk Mailing for Email %1?', [1 => $blockId])); if ($multipleBulk) { - $js = array('id' => "Email_" . $blockId . "_IsBulkmail"); $form->addElement('advcheckbox', "email[$blockId][is_bulkmail]", NULL, '', $js); } else { - $js = array('id' => "Email_" . $blockId . "_IsBulkmail"); if (!$blockEdit) { $js['onClick'] = 'singleSelect( this.id );'; } @@ -95,7 +94,7 @@ public static function buildQuickForm(&$form, $blockCount = NULL, $blockEdit = F } //is_Primary radio - $js = array('id' => "Email_" . $blockId . "_IsPrimary"); + $js = array('id' => "Email_" . $blockId . "_IsPrimary", 'aria-label' => ts('Email %1 is primary?', [1 => $blockId])); if (!$blockEdit) { $js['onClick'] = 'singleSelect( this.id );'; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/IM.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/IM.php index f5df0b46aaa..99a6bd719cf 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/IM.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/IM.php @@ -61,9 +61,9 @@ public static function buildQuickForm(&$form, $blockCount = NULL, $blockEdit = F $form->addField("im[$blockId][location_type_id]", array('entity' => 'im', 'class' => 'eight', 'placeholder' => NULL, 'option_url' => NULL)); //IM box - $form->addField("im[$blockId][name]", array('entity' => 'im')); + $form->addField("im[$blockId][name]", array('entity' => 'im', 'aria-label' => ts('Instant Messenger %1', [1 => $blockId]))); //is_Primary radio - $js = array('id' => 'IM_' . $blockId . '_IsPrimary'); + $js = array('id' => 'IM_' . $blockId . '_IsPrimary', 'aria-label' => ts('Instant Messenger %1 is primary?', [1 => $blockId])); if (!$blockEdit) { $js['onClick'] = 'singleSelect( this.id );'; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Phone.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Phone.php index 756a96fca91..4262bcadd56 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Phone.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Phone.php @@ -64,8 +64,8 @@ public static function buildQuickForm(&$form, $addressBlockCount = NULL, $blockE 'placeholder' => NULL, )); //main phone number with crm_phone class - $form->addField("phone[$blockId][phone]", array('entity' => 'phone', 'class' => 'crm_phone twelve')); - $form->addField("phone[$blockId][phone_ext]", array('entity' => 'phone')); + $form->addField("phone[$blockId][phone]", array('entity' => 'phone', 'class' => 'crm_phone twelve', 'aria-label' => ts('Phone %1', [1 => $blockId]))); + $form->addField("phone[$blockId][phone_ext]", array('entity' => 'phone', 'aria-label' => ts('Phone Extension %1', [1 => $blockId]))); if (isset($form->_contactType) || $blockEdit) { //Block type select $form->addField("phone[$blockId][location_type_id]", array( @@ -76,7 +76,7 @@ public static function buildQuickForm(&$form, $addressBlockCount = NULL, $blockE )); //is_Primary radio - $js = array('id' => 'Phone_' . $blockId . '_IsPrimary', 'onClick' => 'singleSelect( this.id );'); + $js = array('id' => 'Phone_' . $blockId . '_IsPrimary', 'onClick' => 'singleSelect( this.id );', 'aria-label' => ts('Phone %1 is primary?', [1 => $blockId])); $form->addElement('radio', "phone[$blockId][is_primary]", '', '', '1', $js); } // TODO: set this up as a group, we need a valid phone_type_id if we have a phone number diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Website.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Website.php index 160e84248de..43ade78cf44 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Website.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Edit/Website.php @@ -58,7 +58,7 @@ public static function buildQuickForm(&$form, $blockCount = NULL) { $form->addField("website[$blockId][website_type_id]", array('entity' => 'website', 'class' => 'eight')); //Website box - $form->addField("website[$blockId][url]", array('entity' => 'website')); + $form->addField("website[$blockId][url]", array('entity' => 'website', 'aria-label' => ts('Website URL %1', [1 => $blockId]))); $form->addRule("website[$blockId][url]", ts('Enter a valid web address beginning with \'http://\' or \'https://\'.'), 'url'); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Relationship.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Relationship.php index cdc2898fded..fc115dcf925 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Relationship.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Relationship.php @@ -260,6 +260,7 @@ public function setDefaultValues() { else { $defaults['is_active'] = $defaults['is_current_employer'] = 1; $defaults['relationship_type_id'] = $this->_rtypeId; + $defaults['is_permission_a_b'] = $defaults['is_permission_b_a'] = CRM_Contact_BAO_Relationship::NONE; } $this->_enabled = $defaults['is_active']; @@ -339,8 +340,8 @@ public function buildQuickForm() { $this->addField('is_active', array('label' => ts('Enabled?'), 'type' => 'advcheckbox')); - $this->addField('is_permission_a_b'); - $this->addField('is_permission_b_a'); + $this->addField('is_permission_a_b', array(), TRUE); + $this->addField('is_permission_b_a', array(), TRUE); $this->addField('description', array('label' => ts('Description'))); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search.php index 5b9a2079dd6..a9ea4d57d99 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search.php @@ -497,7 +497,7 @@ public function buildQuickForm() { if ($qfKeyParam && ($this->get('component_mode') <= CRM_Contact_BAO_Query::MODE_CONTACTS || $this->get('component_mode') == CRM_Contact_BAO_Query::MODE_CONTACTSRELATED)) { $this->addClass('crm-ajax-selection-form'); $qfKeyParam = "civicrm search {$qfKeyParam}"; - $selectedContactIdsArr = CRM_Core_BAO_PrevNextCache::getSelection($qfKeyParam); + $selectedContactIdsArr = Civi::service('prevnext')->getSelection($qfKeyParam); $selectedContactIds = array_keys($selectedContactIdsArr[$qfKeyParam]); } @@ -782,7 +782,7 @@ public function postProcess() { ) { //reset the cache table for new search $cacheKey = "civicrm search {$this->controller->_key}"; - CRM_Core_BAO_PrevNextCache::deleteItem(NULL, $cacheKey); + Civi::service('prevnext')->deleteItem(NULL, $cacheKey); } //get the button name diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Builder.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Builder.php index 2a5dd79351b..4b5f0ddc29c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Builder.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Builder.php @@ -95,18 +95,10 @@ public function buildQuickForm() { // 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 @@ public static function fieldOptions() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Criteria.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Criteria.php index 3c1b3e7ea9f..845ab7c9a54 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Criteria.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public static function basic(&$form) { $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 @@ public static function basic(&$form) { } // 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 @@ public static function basic(&$form) { $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 @@ -398,9 +467,9 @@ public static function relationship(&$form) { $form->addRadio('relation_status', ts('Relationship Status'), $relStatusOption); $form->setDefaults(array('relation_status' => 0)); // relation permission - $relPermissionOption = array(ts('Any'), ts('Yes'), ts('No')); - $form->addRadio('relation_permission', ts('Permissioned Relationship?'), $relPermissionOption); - $form->setDefaults(array('relation_permission' => 0)); + $allRelationshipPermissions = CRM_Contact_BAO_Relationship::buildOptions('is_permission_a_b'); + $form->add('select', 'relation_permission', ts('Permissioned Relationship'), + array('' => ts('- select -')) + $allRelationshipPermissions, FALSE, array('multiple' => TRUE, 'class' => 'crm-select2')); //add the target group if ($form->_group) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/ContribSYBNT.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/ContribSYBNT.php index bfd961b1c27..cf2b1e950be 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/ContribSYBNT.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/ContribSYBNT.php @@ -192,10 +192,8 @@ public function all( "; 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/DateAdded.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/DateAdded.php index 3ec22422e3d..34ec772f3c7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/DateAdded.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function all( */ 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:
";
       print "$sql;";
@@ -197,7 +200,7 @@ public function from() {
       $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 @@ public function from() {
         $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 @@ public function from() {
                               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 @@ public function from() {
 
       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 @@ public function from() {
 
       //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 @@ public function from() {
 
       //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 @@ public function from() {
 
           $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 @@ public function from() {
 
           //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 @@ public function from() {
             print "$smartGroupQuery;";
             print "
"; } - $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:
";
@@ -380,12 +383,12 @@ public function from() {
 
     /* 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 @@ public function count() {
 
   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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/MultipleValues.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/MultipleValues.php
index acc1bd580f8..31b9cebbc55 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/MultipleValues.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Search/Custom/MultipleValues.php
@@ -246,7 +246,7 @@ public function where($includeContactIDs = FALSE) {
       $this->_formValues
     );
     if ($contact_type != NULL) {
-      $contactType = explode('__', $contact_type);
+      $contactType = explode('__', $contact_type, 2);
       if (count($contactType) > 1) {
         $clause[] = "contact_a.contact_type = '$contactType[0]' AND contact_a.contact_sub_type = '$contactType[1]'";
       }
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Task.php
index 5136641e3b7..369d5945136 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Form/Task.php
+++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() {
    * 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 @@ public static function preProcessCommon(&$form, $useTable = FALSE) {
     $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);
 
@@ -169,10 +172,10 @@ public static function preProcessCommon(&$form, $useTable = FALSE) {
       // rather than prevnext cache table for most of the task actions except export where we rebuild query to fetch
       // final result set
       if ($useTable) {
-        $allCids = CRM_Core_BAO_PrevNextCache::getSelection($cacheKey, "getall");
+        $allCids = Civi::service('prevnext')->getSelection($cacheKey, "getall");
       }
       else {
-        $allCids[$cacheKey] = $form->getContactIds();
+        $allCids[$cacheKey] = self::getContactIds($form);
       }
 
       $form->_contactIds = array();
@@ -230,7 +233,7 @@ public static function preProcessCommon(&$form, $useTable = FALSE) {
       }
       else {
         // fetching selected contact ids of passed cache key
-        $selectedCids = CRM_Core_BAO_PrevNextCache::getSelection($cacheKey);
+        $selectedCids = Civi::service('prevnext')->getSelection($cacheKey);
         foreach ($selectedCids[$cacheKey] as $selectedCid => $ignore) {
           if ($useTable) {
             $insertString[] = " ( {$selectedCid} ) ";
@@ -270,7 +273,7 @@ public static function preProcessCommon(&$form, $useTable = FALSE) {
     ) {
       $sel = CRM_Utils_Array::value('radio_ts', self::$_searchFormValues);
       $form->assign('searchtype', $sel);
-      $result = CRM_Core_BAO_PrevNextCache::getSelectedContacts();
+      $result = self::getSelectedContactNames();
       $form->assign("value", $result);
     }
 
@@ -283,36 +286,41 @@ public static function preProcessCommon(&$form, $useTable = FALSE) {
   }
 
   /**
-   * 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';
     }
@@ -469,6 +477,29 @@ public function mergeContactIdsByHousehold() {
     }
   }
 
+  /**
+   * @return array
+   *   List of contact names.
+   *   NOTE: These are raw values from the DB. In current data-model, that means
+   *   they are pre-encoded HTML.
+   */
+  private static function getSelectedContactNames() {
+    $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String');
+    $cacheKey = "civicrm search {$qfKey}";
+
+    $cids = array();
+    // Gymanstic time!
+    foreach (Civi::service('prevnext')->getSelection($cacheKey) as $cacheKey => $values) {
+      $cids = array_unique(array_merge($cids, array_keys($values)));
+    }
+
+    $result = CRM_Utils_SQL_Select::from('civicrm_contact')
+      ->where('id IN (#cids)', ['cids' => $cids])
+      ->execute()
+      ->fetchMap('id', 'sort_name');
+    return $result;
+  }
+
   /**
    * Given this task's list of targets, produce a hidden group.
    *
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Import/Parser.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Import/Parser.php
index 552ec453f61..6a40dea23ff 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Import/Parser.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Import/Parser.php
@@ -275,9 +275,6 @@ public function run(
         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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/AJAX.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/AJAX.php
index 79fa957b6c8..8270d7b753a 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/AJAX.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/AJAX.php
@@ -966,19 +966,19 @@ public static function selectUnselectContacts() {
           $elements[$key] = self::_convertToId($element);
         }
         CRM_Utils_Type::escapeAll($elements, 'Integer');
-        CRM_Core_BAO_PrevNextCache::markSelection($cacheKey, $actionToPerform, $elements);
+        Civi::service('prevnext')->markSelection($cacheKey, $actionToPerform, $elements);
       }
       else {
-        CRM_Core_BAO_PrevNextCache::markSelection($cacheKey, $actionToPerform);
+        Civi::service('prevnext')->markSelection($cacheKey, $actionToPerform);
       }
     }
     elseif ($variableType == 'single') {
       $cId = self::_convertToId($name);
       CRM_Utils_Type::escape($cId, 'Integer');
       $action = ($state == 'checked') ? 'select' : 'unselect';
-      CRM_Core_BAO_PrevNextCache::markSelection($cacheKey, $action, $cId);
+      Civi::service('prevnext')->markSelection($cacheKey, $action, $cId);
     }
-    $contactIds = CRM_Core_BAO_PrevNextCache::getSelection($cacheKey);
+    $contactIds = Civi::service('prevnext')->getSelection($cacheKey);
     $countSelectionCids = count($contactIds[$cacheKey]);
 
     $arrRet = array('getCount' => $countSelectionCids);
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/View.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/View.php
index aded9798511..f7e0057eefa 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/View.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/View.php
@@ -117,7 +117,7 @@ public function preProcess() {
       'nextPrevError' => 0,
     );
     if ($qfKey) {
-      $pos = CRM_Core_BAO_PrevNextCache::getPositions("civicrm search $qfKey",
+      $pos = Civi::service('prevnext')->getPositions("civicrm search $qfKey",
         $this->_contactId,
         $this->_contactId
       );
@@ -306,7 +306,7 @@ public static function checkUserPermission($page, $contactID = NULL) {
    */
   public static function setTitle($contactId, $isDeleted = FALSE) {
     static $contactDetails;
-    $displayName = $contactImage = NULL;
+    $contactImage = NULL;
     if (!isset($contactDetails[$contactId])) {
       list($displayName, $contactImage) = self::getContactDetails($contactId);
       $contactDetails[$contactId] = array(
@@ -327,6 +327,15 @@ public static function setTitle($contactId, $isDeleted = FALSE) {
     }
     if ($isDeleted) {
       $title = "{$title}";
+      $mergedTo = civicrm_api3('Contact', 'getmergedto', ['contact_id' => $contactId, 'api.Contact.get' => ['return' => 'display_name']]);
+      if ($mergedTo['count']) {
+        $mergedToContactID = $mergedTo['id'];
+        $mergedToDisplayName = $mergedTo['values'][$mergedToContactID]['api.Contact.get']['values'][0]['display_name'];
+        $title .= ' ' . ts('(This contact has been merged to %2)', [
+            1 => CRM_Utils_System::url('civicrm/contact/view', ['reset' => 1, 'cid' => $mergedToContactID]),
+            2 => $mergedToDisplayName,
+          ]);
+      }
     }
 
     // Inline-edit places its own title on the page
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/View/UserDashBoard.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/View/UserDashBoard.php
index 9ecf6863b62..2cc99577e1c 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/View/UserDashBoard.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Page/View/UserDashBoard.php
@@ -72,7 +72,9 @@ public function __construct() {
     $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
     $validUser = FALSE;
     if (empty($userID) && $this->_contactId && $userChecksum) {
+      $this->assign('userChecksum', $userChecksum);
       $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($this->_contactId, $userChecksum);
+      $this->_isChecksumUser = $validUser;
     }
 
     if (!$this->_contactId) {
@@ -168,7 +170,7 @@ public function buildUserDashBoard() {
       $this->assign('pcpInfo', $pcpInfo);
     }
 
-    if (!empty($this->_userOptions['Assigned Activities'])) {
+    if (!empty($this->_userOptions['Assigned Activities']) && empty($this->_isChecksumUser)) {
       // Assigned Activities section
       $dashboardElements[] = array(
         'class' => 'crm-dashboard-assignedActivities',
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Selector.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Selector.php
index 731f78c69d6..066e4a4476f 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Selector.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Selector.php
@@ -579,10 +579,10 @@ public function &getRows($action, $offset, $rowCount, $sort, $output = NULL) {
     // note that the default action is basic
     if ($rowCount) {
       $cacheKey = $this->buildPrevNextCache($sort);
-      $result = $this->_query->getCachedContacts($cacheKey, $offset, $rowCount, $includeContactIds);
+      $resultSet = $this->_query->getCachedContacts($cacheKey, $offset, $rowCount, $includeContactIds)->fetchGenerator();
     }
     else {
-      $result = $this->_query->searchQuery($offset, $rowCount, $sort, FALSE, $includeContactIds);
+      $resultSet = $this->_query->searchQuery($offset, $rowCount, $sort, FALSE, $includeContactIds)->fetchGenerator();
     }
 
     // process the result of the query
@@ -671,7 +671,7 @@ public function &getRows($action, $offset, $rowCount, $sort, $output = NULL) {
       );
     }
 
-    while ($result->fetch()) {
+    foreach ($resultSet as $result) {
       $row = array();
       $this->_query->convertToPseudoNames($result);
       // the columns we are interested in
@@ -881,7 +881,7 @@ public function buildPrevNextCache($sort) {
     // check for current != previous to ensure cache is not reset if paging is done without changing
     // sort criteria
     if (!$pageNum || (!empty($currentSortID) && $currentSortID != $previousSortID)) {
-      CRM_Core_BAO_PrevNextCache::deleteItem(NULL, $cacheKey, 'civicrm_contact');
+      Civi::service('prevnext')->deleteItem(NULL, $cacheKey, 'civicrm_contact');
       // this means it's fresh search, so set pageNum=1
       if (!$pageNum) {
         $pageNum = 1;
@@ -900,7 +900,7 @@ public function buildPrevNextCache($sort) {
     $sortByCharacter = CRM_Utils_Request::retrieve('sortByCharacter', 'String');
 
     //for text field pagination selection save
-    $countRow = CRM_Core_BAO_PrevNextCache::getCount($cacheKey, NULL, "entity_table = 'civicrm_contact'");
+    $countRow = Civi::service('prevnext')->getCount($cacheKey);
     // $sortByCharacter triggers a refresh in the prevNext cache
     if ($sortByCharacter && $sortByCharacter != 'all') {
       $cacheKey .= "_alphabet";
@@ -1039,17 +1039,11 @@ public function fillupPrevNextCache($sort, $cacheKey, $start = 0, $end = self::C
     // the other alternative of running the FULL query will just be incredibly inefficient
     // and slow things down way too much on large data sets / complex queries
 
-    $insertSQL = "
-INSERT INTO civicrm_prevnext_cache ( entity_table, entity_id1, entity_id2, cacheKey, data )
-SELECT DISTINCT 'civicrm_contact', contact_a.id, contact_a.id, '$cacheKey', contact_a.sort_name
-";
+    $selectSQL = "SELECT DISTINCT 'civicrm_contact', contact_a.id, contact_a.id, '$cacheKey', contact_a.sort_name";
 
-    $sql = str_replace(array("SELECT contact_a.id as contact_id", "SELECT contact_a.id as id"), $insertSQL, $sql);
+    $sql = str_replace(array("SELECT contact_a.id as contact_id", "SELECT contact_a.id as id"), $selectSQL, $sql);
     try {
-      $result = CRM_Core_DAO::executeQuery($sql, [], FALSE, NULL, FALSE, TRUE, TRUE);
-      if (is_a($result, 'DB_Error')) {
-        throw new CRM_Core_Exception($result->message);
-      }
+      Civi::service('prevnext')->fillWithSql($cacheKey, $sql);
     }
     catch (CRM_Core_Exception $e) {
       if ($coreSearch) {
@@ -1089,18 +1083,17 @@ public function rebuildPreNextCache($start, $end, $sort, $cacheKey) {
     $dao = CRM_Core_DAO::executeQuery($sql);
 
     // build insert query, note that currently we build cache for 500 (self::CACHE_SIZE) contact records at a time, hence below approach
-    $insertValues = array();
+    $rows = [];
     while ($dao->fetch()) {
-      $insertValues[] = "('civicrm_contact', {$dao->contact_id}, {$dao->contact_id}, '{$cacheKey}', '" . CRM_Core_DAO::escapeString($dao->sort_name) . "')";
+      $rows[] = [
+        'entity_table' => 'civicrm_contact',
+        'entity_id1' => $dao->contact_id,
+        'entity_id2' => $dao->contact_id,
+        'data' => $dao->sort_name,
+      ];
     }
 
-    //update pre/next cache using single insert query
-    if (!empty($insertValues)) {
-      $sql = 'INSERT INTO civicrm_prevnext_cache ( entity_table, entity_id1, entity_id2, cacheKey, data ) VALUES
-' . implode(',', $insertValues);
-
-      $result = CRM_Core_DAO::executeQuery($sql);
-    }
+    Civi::service('prevnext')->fillWithArray($cacheKey, $rows);
   }
 
   /**
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Task.php
index 71cf992bdab..f314b028493 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Task.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contact/Task.php
@@ -297,18 +297,17 @@ public static function permissionedTaskTitles($permission, $params = array()) {
         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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Contribution.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Contribution.php
index f58bd9f9eaa..bd1d1d452ef 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Contribution.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Contribution.php
@@ -1967,7 +1967,7 @@ public static function transitionComponents($params, $processContributionObject
             $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 @@ public function loadRelatedObjects(&$input, &$ids, $loadAll = FALSE) {
       }
     }
 
-    $this->loadRelatedMembershipObjects($ids);
+    $ids = $this->loadRelatedMembershipObjects($ids);
 
     if ($this->_component != 'contribute') {
       // we are in event mode
@@ -3840,7 +3840,6 @@ public static function validateFinancialType($financialTypeId, $relationName = '
    * @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 @@ public static function recordAdditionalPayment($contributionId, $trxnsData, $pay
     }
 
     // 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 @@ public static function recordAdditionalPayment($contributionId, $trxnsData, $pay
     $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 @@ public static function recordAdditionalPayment($contributionId, $trxnsData, $pay
 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 @@ public static function recordAdditionalPayment($contributionId, $trxnsData, $pay
       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 @@ public static function recordAdditionalPayment($contributionId, $trxnsData, $pay
         $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 @@ public static function recordAdditionalPayment($contributionId, $trxnsData, $pay
           }
         }
 
-        // 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 @@ public static function recordAdditionalPayment($contributionId, $trxnsData, $pay
       // 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 @@ public static function completeOrder(&$input, &$ids, $objects, $transaction, $re
     }
 
     $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 @@ public static function completeOrder(&$input, &$ids, $objects, $transaction, $re
     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,13 +4621,11 @@ public static function completeOrder(&$input, &$ids, $objects, $transaction, $re
         $values['is_email_receipt'] = $recurContrib->is_email_receipt;
       }
 
-      if (!empty($memberships)) {
+      if ($contributionParams['contribution_status_id'] === $completedContributionStatusID) {
         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))
+          $changeDate
         );
       }
     }
@@ -4823,9 +4810,11 @@ public static function createCreditNoteId() {
    *
    * @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 +4845,7 @@ public function loadRelatedMembershipObjects(&$ids) {
         }
       }
     }
+    return $ids;
   }
 
   /**
@@ -4865,7 +4855,7 @@ public function loadRelatedMembershipObjects(&$ids) {
    *
    * @param array $params
    *
-   * @return object
+   * @return CRM_Financial_DAO_FinancialTrxn
    */
   public static function recordPartialPayment($contribution, $params) {
     $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
@@ -5011,6 +5001,7 @@ public static function assignProportionalLineItems($trxnParams, $trxnId, $contri
   public static function checkLineItems(&$params) {
     $totalAmount = CRM_Utils_Array::value('total_amount', $params);
     $lineItemAmount = 0;
+
     foreach ($params['line_items'] as &$lineItems) {
       foreach ($lineItems['line_item'] as &$item) {
         if (empty($item['financial_type_id'])) {
@@ -5019,11 +5010,20 @@ public static function checkLineItems(&$params) {
         $lineItemAmount += $item['line_total'];
       }
     }
+
     if (!isset($totalAmount)) {
       $params['total_amount'] = $lineItemAmount;
     }
-    elseif ($totalAmount != $lineItemAmount) {
-      throw new API_Exception("Line item total doesn't match with total amount.");
+    else {
+      $currency = CRM_Utils_Array::value('currency', $params, '');
+
+      if (empty($currency)) {
+        $currency = CRM_Core_Config::singleton()->defaultCurrency;
+      }
+
+      if (!CRM_Utils_Money::equals($totalAmount, $lineItemAmount, $currency)) {
+        throw new CRM_Contribute_Exception_CheckLineItemsException();
+      }
     }
   }
 
@@ -5040,11 +5040,13 @@ public static function getFinancialAccountForStatusChangeTrxn($params, $default)
     if (!empty($params['financial_account_id'])) {
       return $params['financial_account_id'];
     }
+
     $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus($params['contribution_status_id'], 'name');
     $preferredAccountsRelationships = array(
       'Refunded' => 'Credit/Contra Revenue Account is',
       'Chargeback' => 'Chargeback Account is',
     );
+
     if (in_array($contributionStatus, array_keys($preferredAccountsRelationships))) {
       $financialTypeID = !empty($params['financial_type_id']) ? $params['financial_type_id'] : $params['prevContribution']->financial_type_id;
       return CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship(
@@ -5052,6 +5054,7 @@ public static function getFinancialAccountForStatusChangeTrxn($params, $default)
         $preferredAccountsRelationships[$contributionStatus]
       );
     }
+
     return $default;
   }
 
@@ -5400,15 +5403,18 @@ protected static function isPaymentInstrumentChange(&$params, $pendingStatuses)
    * 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
-   *   This shouldn't be required but historical function overload by repeattransaction probably requires it.
    *
-   * @todo investigate completely bypassing this function if $contributionStatus != Completed.
+   * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
    */
-  protected static function updateMembershipBasedOnCompletionOfContribution($contribution, $memberships, $primaryContributionID, $changeDate, $contributionStatus = 'Completed') {
+  protected static function updateMembershipBasedOnCompletionOfContribution($contribution, $primaryContributionID, $changeDate) {
+    $contribution->loadRelatedMembershipObjects();
+    if (empty($contribution->_relatedObjects['membership'])) {
+      return;
+    }
+    $memberships = $contribution->_relatedObjects['membership'];
     foreach ($memberships as $membershipTypeIdKey => $membership) {
       if ($membership) {
         $membershipParams = array(
@@ -5443,55 +5449,50 @@ protected static function updateMembershipBasedOnCompletionOfContribution($contr
         }
         $dao->free();
 
-        // Unclear why this is here but this function is overloaded by repeattransaction.
-        if ($contributionStatus === 'Pending') {
-          $membershipParams['num_terms'] = 0;
-        }
-        else {
-          $membershipParams['num_terms'] = $contribution->getNumTermsByContributionAndMembershipType(
-            $membershipParams['membership_type_id'],
-            $primaryContributionID
+        $membershipParams['num_terms'] = $contribution->getNumTermsByContributionAndMembershipType(
+          $membershipParams['membership_type_id'],
+          $primaryContributionID
+        );
+        // @todo remove all this stuff in favour of letting the api call further down handle in
+        // (it is a duplication of what the api does).
+        $dates = array_fill_keys(array(
+          'join_date',
+          'start_date',
+          'end_date',
+        ), NULL);
+        if ($currentMembership) {
+          /*
+           * Fixed FOR CRM-4433
+           * In BAO/Membership.php(renewMembership function), we skip the extend membership date and status
+           * when Contribution mode is notify and membership is for renewal )
+           */
+          CRM_Member_BAO_Membership::fixMembershipStatusBeforeRenew($currentMembership, $changeDate);
+
+          // @todo - we should pass membership_type_id instead of null here but not
+          // adding as not sure of testing
+          $dates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membershipParams['id'],
+            $changeDate, NULL, $membershipParams['num_terms']
           );
-          // @todo remove all this stuff in favour of letting the api call further down handle in
-          // (it is a duplication of what the api does).
-          $dates = array_fill_keys(array(
-            'join_date',
-            'start_date',
-            'end_date',
-          ), NULL);
-          if ($currentMembership) {
-            /*
-             * Fixed FOR CRM-4433
-             * In BAO/Membership.php(renewMembership function), we skip the extend membership date and status
-             * when Contribution mode is notify and membership is for renewal )
-             */
-            CRM_Member_BAO_Membership::fixMembershipStatusBeforeRenew($currentMembership, $changeDate);
-
-            // @todo - we should pass membership_type_id instead of null here but not
-            // adding as not sure of testing
-            $dates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membershipParams['id'],
-              $changeDate, NULL, $membershipParams['num_terms']
-            );
-            $dates['join_date'] = $currentMembership['join_date'];
-          }
+          $dates['join_date'] = $currentMembership['join_date'];
+        }
 
-          //get the status for membership.
-          $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($dates['start_date'],
-            $dates['end_date'],
-            $dates['join_date'],
-            'today',
-            TRUE,
-            $membershipParams['membership_type_id'],
-            $membershipParams
-          );
+        //get the status for membership.
+        $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($dates['start_date'],
+          $dates['end_date'],
+          $dates['join_date'],
+          'today',
+          TRUE,
+          $membershipParams['membership_type_id'],
+          $membershipParams
+        );
+
+        unset($dates['end_date']);
+        $membershipParams['status_id'] = CRM_Utils_Array::value('id', $calcStatus, 'New');
+        //we might be renewing membership,
+        //so make status override false.
+        $membershipParams['is_override'] = FALSE;
+        $membershipParams['status_override_end_date'] = 'null';
 
-          unset($dates['end_date']);
-          $membershipParams['status_id'] = CRM_Utils_Array::value('id', $calcStatus, 'New');
-          //we might be renewing membership,
-          //so make status override false.
-          $membershipParams['is_override'] = FALSE;
-          $membershipParams['status_override_end_date'] = 'null';
-        }
         //CRM-17723 - reset static $relatedContactIds array()
         // @todo move it to Civi Statics.
         $var = TRUE;
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Contribution/Utils.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Contribution/Utils.php
index 2b99789923a..0b2c99b898e 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Contribution/Utils.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Contribution/Utils.php
@@ -74,6 +74,8 @@ public static function processConfirm(
 
     // 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 @@ public static function processConfirm(
       }
 
       $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/ContributionRecur.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/ContributionRecur.php
index aa1d02b5ab2..a5c76722c43 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/ContributionRecur.php
+++ b/profiles/civicrm_starterkit/modules/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 @@ public static function deleteRecurContribution($recurId) {
    *
    * @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 @@ public static function cancelRecurContribution($recurId, $objects, $activityPara
         }
         $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 @@ public static function cancelRecurContribution($recurId, $objects, $activityPara
         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 @@ public static function calculateRecurLineItems($recurId, $total_amount, $financi
     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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/ManagePremiums.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/ManagePremiums.php
index 7b900e40446..f122fb0b887 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/ManagePremiums.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/ManagePremiums.php
@@ -29,114 +29,81 @@
  *
  * @package CRM
  * @copyright CiviCRM LLC (c) 2004-2018
+ * @deprecated
  */
-class CRM_Contribute_BAO_ManagePremiums extends CRM_Contribute_DAO_Product {
-
-  /**
-   * Static holder for the default LT.
-   */
-  static $_defaultContributionType = NULL;
+class CRM_Contribute_BAO_ManagePremiums extends CRM_Contribute_BAO_Product {
 
   /**
    * Class constructor.
    */
   public function __construct() {
+    CRM_Core_Error::deprecatedFunctionWarning('CRM_Contribute_BAO_Product::construct');
     parent::__construct();
   }
 
   /**
    * Fetch object based on array of properties.
    *
+   * @deprecated
    * @param array $params
    *   (reference ) an assoc array of name/value pairs.
    * @param array $defaults
    *   (reference ) an assoc array to hold the flattened values.
    *
-   * @return CRM_Contribute_BAO_ManagePremium
+   * @return CRM_Contribute_BAO_Product
    */
   public static function retrieve(&$params, &$defaults) {
-    $premium = new CRM_Contribute_DAO_Product();
-    $premium->copyValues($params);
-    if ($premium->find(TRUE)) {
-      $premium->product_name = $premium->name;
-      CRM_Core_DAO::storeValues($premium, $defaults);
-      return $premium;
-    }
-    return NULL;
+    CRM_Core_Error::deprecatedFunctionWarning('CRM_Contribute_BAO_Product::retrieve');
+    return parent::retrieve($params, $defaults);
   }
 
   /**
    * Update the is_active flag in the db.
    *
+   * @deprecated
    * @param int $id
    *   Id of the database record.
    * @param bool $is_active
    *   Value we want to set the is_active field.
    *
-   * @return Object
-   *   DAO object on success, null otherwise
+   * @return bool
    */
   public static function setIsActive($id, $is_active) {
-    if (!$is_active) {
-      $dao = new CRM_Contribute_DAO_PremiumsProduct();
-      $dao->product_id = $id;
-      $dao->delete();
-    }
-    return CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_Product', $id, 'is_active', $is_active);
+    CRM_Core_Error::deprecatedFunctionWarning('CRM_Contribute_BAO_Product::setIsActive');
+    return parent::setIsActive($id, $is_active);
   }
 
   /**
    * Add a premium product to the database, and return it.
    *
+   * @deprecated
    * @param array $params
    *   Reference array contains the values submitted by the form.
-   * @param array $ids
+   * @param array $ids (deprecated)
    *   Reference array contains the id.
    *
    * @return CRM_Contribute_DAO_Product
    */
-  public static function add(&$params, &$ids) {
-    $params = array_merge(array(
-      'id' => CRM_Utils_Array::value('premium', $ids),
-      'image' => '',
-      'thumbnail' => '',
-      'is_active' => 0,
-      'is_deductible' => FALSE,
-      'currency' => CRM_Core_Config::singleton()->defaultCurrency,
-    ), $params);
-
-    // Modify the submitted values for 'image' and 'thumbnail' so that we use
-    // local URLs for these images when possible.
-    $params['image'] = CRM_Utils_String::simplifyURL($params['image'], TRUE);
-    $params['thumbnail'] = CRM_Utils_String::simplifyURL($params['thumbnail'], TRUE);
-
-    // Save and return
-    $premium = new CRM_Contribute_DAO_Product();
-    $premium->copyValues($params);
-    $premium->save();
-    return $premium;
+  public static function add(&$params, $ids) {
+    CRM_Core_Error::deprecatedFunctionWarning('CRM_Contribute_BAO_Product::create');
+    $id = CRM_Utils_Array::value('id', $params, CRM_Utils_Array::value('premium', $ids));
+    if ($id) {
+      $params['id'] = $id;
+    }
+    return parent::create($params);
   }
 
   /**
    * Delete premium Types.
    *
+   * @deprecated
    * @param int $productID
+   *
+   * @throws \CRM_Core_Exception
    */
   public static function del($productID) {
-    //check dependencies
-    $premiumsProduct = new CRM_Contribute_DAO_PremiumsProduct();
-    $premiumsProduct->product_id = $productID;
-    if ($premiumsProduct->find(TRUE)) {
-      $session = CRM_Core_Session::singleton();
-      $message .= ts('This Premium is being linked to Online Contribution page. Please remove it in order to delete this Premium.', array(1 => CRM_Utils_System::url('civicrm/admin/contribute', 'reset=1')), ts('Deletion Error'), 'error');
-      CRM_Core_Session::setStatus($message);
-      return CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/admin/contribute/managePremiums', 'reset=1&action=browse'));
-    }
-
-    //delete from financial Type table
-    $premium = new CRM_Contribute_DAO_Product();
-    $premium->id = $productID;
-    $premium->delete();
+    CRM_Core_Error::deprecatedFunctionWarning('CRM_Contribute_BAO_Product::del');
+    return parent::del($productID);
   }
 
 }
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Premium.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Premium.php
index 49b3bebe77c..a7db1d5efd4 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Premium.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Premium.php
@@ -104,32 +104,33 @@ public static function del($premiumID) {
   public static function buildPremiumBlock(&$form, $pageID, $formItems = FALSE, $selectedProductID = NULL, $selectedOption = NULL) {
     $form->add('hidden', "selectProduct", $selectedProductID, array('id' => 'selectProduct'));
 
-    $dao = new CRM_Contribute_DAO_Premium();
-    $dao->entity_table = 'civicrm_contribution_page';
-    $dao->entity_id = $pageID;
-    $dao->premiums_active = 1;
-    CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($financialTypes, CRM_Core_Action::ADD);
-    $addWhere = "financial_type_id IN (0)";
-    if (!empty($financialTypes)) {
-      $addWhere = "financial_type_id IN (" . implode(',', array_keys($financialTypes)) . ")";
-    }
+    $premiumDao = new CRM_Contribute_DAO_Premium();
+    $premiumDao->entity_table = 'civicrm_contribution_page';
+    $premiumDao->entity_id = $pageID;
+    $premiumDao->premiums_active = 1;
 
-    if ($dao->find(TRUE)) {
-      $premiumID = $dao->id;
+    if ($premiumDao->find(TRUE)) {
+      $premiumID = $premiumDao->id;
       $premiumBlock = array();
-      CRM_Core_DAO::storeValues($dao, $premiumBlock);
+      CRM_Core_DAO::storeValues($premiumDao, $premiumBlock);
 
-      $dao = new CRM_Contribute_DAO_PremiumsProduct();
-      $dao->premiums_id = $premiumID;
-      $dao->whereAdd($addWhere);
-      $dao->orderBy('weight');
-      $dao->find();
+      CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($financialTypes, CRM_Core_Action::ADD);
+      $addWhere = "financial_type_id IN (0)";
+      if (!empty($financialTypes)) {
+        $addWhere = "financial_type_id IN (" . implode(',', array_keys($financialTypes)) . ")";
+      }
+      $addWhere = "{$addWhere} OR financial_type_id IS NULL";
+
+      $premiumsProductDao = new CRM_Contribute_DAO_PremiumsProduct();
+      $premiumsProductDao->premiums_id = $premiumID;
+      $premiumsProductDao->whereAdd($addWhere);
+      $premiumsProductDao->orderBy('weight');
+      $premiumsProductDao->find();
 
       $products = array();
-      $radio = array();
-      while ($dao->fetch()) {
+      while ($premiumsProductDao->fetch()) {
         $productDAO = new CRM_Contribute_DAO_Product();
-        $productDAO->id = $dao->product_id;
+        $productDAO->id = $premiumsProductDao->product_id;
         $productDAO->is_active = 1;
         if ($productDAO->find(TRUE)) {
           if ($selectedProductID != NULL) {
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Product.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Product.php
new file mode 100644
index 00000000000..22f1f9739d2
--- /dev/null
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/BAO/Product.php
@@ -0,0 +1,145 @@
+copyValues($params);
+    if ($premium->find(TRUE)) {
+      $premium->product_name = $premium->name;
+      CRM_Core_DAO::storeValues($premium, $defaults);
+      return $premium;
+    }
+    return NULL;
+  }
+
+  /**
+   * Update the is_active flag in the db.
+   *
+   * @param int $id
+   *   Id of the database record.
+   * @param bool $is_active
+   *   Value we want to set the is_active field.
+   *
+   * @return bool
+   */
+  public static function setIsActive($id, $is_active) {
+    if (!$is_active) {
+      $dao = new CRM_Contribute_DAO_PremiumsProduct();
+      $dao->product_id = $id;
+      $dao->delete();
+    }
+    return CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_Product', $id, 'is_active', $is_active);
+  }
+
+  /**
+   * Add a premium product to the database, and return it.
+   *
+   * @param array $params
+   *   Update parameters.
+   *
+   * @return CRM_Contribute_DAO_Product
+   */
+  public static function create($params) {
+    $id = CRM_Utils_Array::value('id', $params);
+    if (empty($id)) {
+      $defaultParams = [
+        'id' => $id,
+        'image' => '',
+        'thumbnail' => '',
+        'is_active' => 0,
+        'is_deductible' => FALSE,
+        'currency' => CRM_Core_Config::singleton()->defaultCurrency,
+      ];
+      $params = array_merge($defaultParams, $params);
+    }
+
+    // Modify the submitted values for 'image' and 'thumbnail' so that we use
+    // local URLs for these images when possible.
+    if (isset($params['image'])) {
+      $params['image'] = CRM_Utils_String::simplifyURL($params['image'], TRUE);
+    }
+    if (isset($params['thumbnail'])) {
+      $params['thumbnail'] = CRM_Utils_String::simplifyURL($params['thumbnail'], TRUE);
+    }
+
+    // Save and return
+    $premium = new CRM_Contribute_DAO_Product();
+    $premium->copyValues($params);
+    $premium->save();
+    return $premium;
+  }
+
+  /**
+   * Delete premium Types.
+   *
+   * @param int $productID
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public static function del($productID) {
+    //check dependencies
+    $premiumsProduct = new CRM_Contribute_DAO_PremiumsProduct();
+    $premiumsProduct->product_id = $productID;
+    if ($premiumsProduct->find(TRUE)) {
+      throw new CRM_Core_Exception('Cannot delete a Premium that is linked to a Contribution page');
+    }
+    // delete product
+    $premium = new CRM_Contribute_DAO_Product();
+    $premium->id = $productID;
+    $premium->delete();
+  }
+
+}
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Contribution.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Contribution.php
index 527cb61eb56..d92f6f7958f 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Contribution.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Contribution.php
@@ -399,6 +399,7 @@ public static function &fields() {
           'headerPattern' => '/non?.?deduct/i',
           'dataPattern' => '/^\d+(\.\d{2})?$/',
           'export' => TRUE,
+          'default' => '0',
           'table_name' => 'civicrm_contribution',
           'entity' => 'Contribution',
           'bao' => 'CRM_Contribute_BAO_Contribution',
@@ -696,6 +697,7 @@ public static function &fields() {
           'headerPattern' => '',
           'dataPattern' => '',
           'export' => TRUE,
+          'default' => '0',
           'table_name' => 'civicrm_contribution',
           'entity' => 'Contribution',
           'bao' => 'CRM_Contribute_BAO_Contribution',
@@ -713,6 +715,7 @@ public static function &fields() {
           'headerPattern' => '',
           'dataPattern' => '',
           'export' => TRUE,
+          'default' => '0',
           'table_name' => 'civicrm_contribution',
           'entity' => 'Contribution',
           'bao' => 'CRM_Contribute_BAO_Contribution',
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionPage.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionPage.php
index ab08e950890..98565ad90e4 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionPage.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionPage.php
@@ -458,6 +458,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Is Credit Card Only?'),
           'description' => 'if true - processing logic must reject transaction at confirmation stage if pay method != credit card',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -479,6 +480,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Is Recurring'),
           'description' => 'if true - allows recurring contributions, valid only for PayPal_Standard',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -521,6 +523,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Support Recurring Intervals'),
           'description' => 'if true - supports recurring intervals',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -531,6 +534,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Recurring Installments?'),
           'description' => 'if true - asks user for recurring installments',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -541,6 +545,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Adjust Recurring Start Date'),
           'description' => 'if true - user is able to adjust payment start date',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -551,6 +556,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Pay Later'),
           'description' => 'if true - allows the user to send payment directly to the org later',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -581,6 +587,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Allow Partial Payment'),
           'description' => 'is partial payment enabled for this online contribution page',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -627,6 +634,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Allow Other Amounts'),
           'description' => 'if true, page will include an input text field where user can enter their own amount',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -731,6 +739,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Send email Receipt'),
           'description' => 'if true, receipt is automatically emailed to contact on success',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
@@ -930,6 +939,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Is billing block required'),
           'description' => 'if true - billing block is required for online contribution page',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_page',
           'entity' => 'ContributionPage',
           'bao' => 'CRM_Contribute_BAO_ContributionPage',
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionRecur.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionRecur.php
index 1f1650edad7..c27f63c0044 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionRecur.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionRecur.php
@@ -508,6 +508,7 @@ public static function &fields() {
           'headerPattern' => '',
           'dataPattern' => '',
           'export' => TRUE,
+          'default' => '0',
           'table_name' => 'civicrm_contribution_recur',
           'entity' => 'ContributionRecur',
           'bao' => 'CRM_Contribute_BAO_ContributionRecur',
@@ -549,6 +550,7 @@ public static function &fields() {
           'type' => CRM_Utils_Type::T_INT,
           'title' => ts('Number of Failures'),
           'description' => 'Number of failed charge attempts since last success. Business rule could be set to deactivate on more than x failures.',
+          'default' => '0',
           'table_name' => 'civicrm_contribution_recur',
           'entity' => 'ContributionRecur',
           'bao' => 'CRM_Contribute_BAO_ContributionRecur',
@@ -576,6 +578,7 @@ public static function &fields() {
           'title' => ts('Auto Renew'),
           'description' => 'Some systems allow contributor to set a number of installments - but then auto-renew the subscription or commitment if they do not cancel.',
           'required' => TRUE,
+          'default' => '0',
           'table_name' => 'civicrm_contribution_recur',
           'entity' => 'ContributionRecur',
           'bao' => 'CRM_Contribute_BAO_ContributionRecur',
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionSoft.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionSoft.php
index 977a00bea70..88c9c47f001 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionSoft.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/ContributionSoft.php
@@ -233,6 +233,7 @@ public static function &fields() {
           'name' => 'pcp_display_in_roll',
           'type' => CRM_Utils_Type::T_BOOLEAN,
           'title' => ts('Soft Contribution Display on PCP'),
+          'default' => '0',
           'table_name' => 'civicrm_contribution_soft',
           'entity' => 'ContributionSoft',
           'bao' => 'CRM_Contribute_BAO_ContributionSoft',
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Premium.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Premium.php
index f8981352c93..9ff1e31b313 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Premium.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Premium.php
@@ -169,6 +169,7 @@ public static function &fields() {
           'title' => ts('Is Premium Active?'),
           'description' => 'Is the Premiums feature enabled for this page?',
           'required' => TRUE,
+          'default' => '0',
           'table_name' => 'civicrm_premiums',
           'entity' => 'Premium',
           'bao' => 'CRM_Contribute_BAO_Premium',
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Product.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Product.php
index 9c1328102a7..5799fa61a55 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Product.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/DAO/Product.php
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Contribute/Product.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:81e315b903d403508f379dc9c0fcf532)
+ * (GenCodeChecksum:a66a59f20355ce5773f427b85bd7bf0b)
  */
 
 /**
@@ -196,7 +196,7 @@ public static function &fields() {
           'required' => TRUE,
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'product_name' => [
@@ -213,7 +213,7 @@ public static function &fields() {
           'dataPattern' => '',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 1,
         ],
         'description' => [
@@ -223,7 +223,7 @@ public static function &fields() {
           'description' => 'Optional description of the product/premium.',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 1,
         ],
         'sku' => [
@@ -239,7 +239,7 @@ public static function &fields() {
           'dataPattern' => '',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'options' => [
@@ -249,7 +249,7 @@ public static function &fields() {
           'description' => 'Store comma-delimited list of color, size, etc. options for the product.',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 1,
         ],
         'image' => [
@@ -261,7 +261,7 @@ public static function &fields() {
           'size' => CRM_Utils_Type::HUGE,
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'thumbnail' => [
@@ -273,7 +273,7 @@ public static function &fields() {
           'size' => CRM_Utils_Type::HUGE,
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'price' => [
@@ -287,7 +287,7 @@ public static function &fields() {
           ],
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'currency' => [
@@ -300,7 +300,7 @@ public static function &fields() {
           'default' => 'NULL',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
           'html' => [
             'type' => 'Select',
@@ -320,7 +320,7 @@ public static function &fields() {
           'default' => 'NULL',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
           'FKClassName' => 'CRM_Financial_DAO_FinancialType',
           'pseudoconstant' => [
@@ -340,7 +340,7 @@ public static function &fields() {
           ],
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'cost' => [
@@ -354,7 +354,7 @@ public static function &fields() {
           ],
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'is_active' => [
@@ -365,7 +365,7 @@ public static function &fields() {
           'required' => TRUE,
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'period_type' => [
@@ -379,7 +379,7 @@ public static function &fields() {
           'default' => 'rolling',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
           'html' => [
             'type' => 'Select',
@@ -396,7 +396,7 @@ public static function &fields() {
           'default' => '0101',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'duration_unit' => [
@@ -408,7 +408,7 @@ public static function &fields() {
           'default' => 'year',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
           'html' => [
             'type' => 'Select',
@@ -424,7 +424,7 @@ public static function &fields() {
           'description' => 'Number of units for total duration of subscription, service, membership (e.g. 12 Months).',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
         'frequency_unit' => [
@@ -437,7 +437,7 @@ public static function &fields() {
           'default' => 'month',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
           'html' => [
             'type' => 'Select',
@@ -453,7 +453,7 @@ public static function &fields() {
           'description' => 'Number of units for delivery frequency of subscription, service, membership (e.g. every 3 Months).',
           'table_name' => 'civicrm_product',
           'entity' => 'Product',
-          'bao' => 'CRM_Contribute_DAO_Product',
+          'bao' => 'CRM_Contribute_BAO_Product',
           'localizable' => 0,
         ],
       ];
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Exception/CheckLineItemsException.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Exception/CheckLineItemsException.php
new file mode 100644
index 00000000000..c1c0d3a9142
--- /dev/null
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Exception/CheckLineItemsException.php
@@ -0,0 +1,13 @@
+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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Exception/PastContributionPageException.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Exception/PastContributionPageException.php
new file mode 100644
index 00000000000..fc7c8b3182e
--- /dev/null
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Exception/PastContributionPageException.php
@@ -0,0 +1,25 @@
+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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/AdditionalInfo.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/AdditionalInfo.php
index d279d644e10..33bf7bb1968 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/AdditionalInfo.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/AdditionalInfo.php
@@ -209,7 +209,7 @@ public static function processPremium($params, $contributionID, $premiumID = NUL
     );
 
     $productDetails = array();
-    CRM_Contribute_BAO_ManagePremiums::retrieve($premiumParams, $productDetails);
+    CRM_Contribute_BAO_Product::retrieve($premiumParams, $productDetails);
     $dao->financial_type_id = CRM_Utils_Array::value('financial_type_id', $productDetails);
     if (!empty($options[$selectedProductID])) {
       $dao->product_option = $options[$selectedProductID][$selectedProductOptionID];
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/CancelSubscription.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/CancelSubscription.php
index 06941d9035b..ec4aabd93be 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/CancelSubscription.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/CancelSubscription.php
@@ -224,7 +224,6 @@ public function postProcess() {
         );
       $cancelStatus = CRM_Contribute_BAO_ContributionRecur::cancelRecurContribution(
         $this->_subscriptionDetails->recur_id,
-        NULL,
         $activityParams
       );
 
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Contribution/Confirm.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Contribution/Confirm.php
index 17a0bd2f92b..a8b19f13e04 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Contribution/Confirm.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Contribution/Confirm.php
@@ -740,21 +740,18 @@ public function postProcess() {
    *
    * 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 @@ public static function submit($params) {
       }
     }
     $form->set('memberPriceFieldIDS', $membershipPriceFieldIDs);
+    $form->setRecurringMembershipParams();
     $form->processFormSubmission(CRM_Utils_Array::value('contact_id', $params));
   }
 
@@ -2395,26 +2393,19 @@ protected function doMembershipProcessing($contactID, $membershipParams, $premiu
     $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Contribution/Main.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Contribution/Main.php
index 9d7ed7c59b4..4a302b82b70 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Contribution/Main.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Contribution/Main.php
@@ -302,6 +302,15 @@ public function buildQuickForm() {
       $this->buildComponentForm($this->_id, $this);
     }
 
+    if (count($this->_paymentProcessors) >= 1 && !isset($this->_paymentProcessors[0]) && !$this->get_template_vars("isCaptcha") && $this->hasToAddForcefully()) {
+      if (!$this->_userID) {
+        $this->enableCaptchaOnForm();
+      }
+      else {
+        $this->displayCaptchaWarning();
+      }
+    }
+
     // Build payment processor form
     CRM_Core_Payment_ProcessorForm::buildQuickForm($this);
 
@@ -554,7 +563,7 @@ public static function buildRecur(&$form) {
     $form->add('checkbox', 'is_recur', ts('I want to contribute this amount'), NULL);
 
     if (!empty($form->_values['is_recur_interval']) || $className == 'CRM_Contribute_Form_Contribution') {
-      $form->add('text', 'frequency_interval', ts('Every'), $attributes['frequency_interval']);
+      $form->add('text', 'frequency_interval', ts('Every'), $attributes['frequency_interval'] + ['aria-label' => ts('Every')]);
       $form->addRule('frequency_interval', ts('Frequency must be a whole number (EXAMPLE: Every 3 months).'), 'integer');
     }
     else {
@@ -595,7 +604,7 @@ public static function buildRecur(&$form) {
           }
         }
       }
-      $frequencyUnit = &$form->add('select', 'frequency_unit', NULL, $units);
+      $frequencyUnit = &$form->addElement('select', 'frequency_unit', NULL, $units, ['aria-label' => ts('Frequency Unit')]);
     }
 
     // FIXME: Ideally we should freeze select box if there is only
@@ -1300,6 +1309,9 @@ public function assignFormVariablesByContributionID() {
     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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionBase.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionBase.php
index 97a06e145ce..ec174f47baa 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionBase.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionBase.php
@@ -313,6 +313,17 @@ public function preProcess() {
         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 @@ public function assignToTemplate() {
           $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);
 
     }
@@ -770,14 +777,43 @@ public function buildCustom($id, $name, $viewOnly = FALSE, $profileContactType =
         }
 
         if ($addCaptcha && !$viewOnly) {
-          $captcha = CRM_Utils_ReCAPTCHA::singleton();
-          $captcha->add($this);
-          $this->assign('isCaptcha', TRUE);
+          $this->enableCaptchaOnForm();
         }
       }
     }
   }
 
+  /**
+   * Enable ReCAPTCHA on Contribution form
+   */
+  protected function enableCaptchaOnForm() {
+    $captcha = CRM_Utils_ReCAPTCHA::singleton();
+    if ($captcha->hasSettingsAvailable()) {
+      $captcha->add($this);
+      $this->assign('isCaptcha', TRUE);
+    }
+  }
+
+  /**
+   * Display ReCAPTCHA warning on Contribution form
+   */
+  protected function displayCaptchaWarning() {
+    if (CRM_Core_Permission::check("administer CiviCRM")) {
+      $captcha = CRM_Utils_ReCAPTCHA::singleton();
+      if (!$captcha->hasSettingsAvailable()) {
+        $this->assign('displayCaptchaWarning', TRUE);
+      }
+    }
+  }
+
+  /**
+   * Check if ReCAPTCHA has to be added on Contribution form forcefully.
+   */
+  protected function hasToAddForcefully() {
+    $captcha = CRM_Utils_ReCAPTCHA::singleton();
+    return $captcha->hasToAddForcefully();
+  }
+
   /**
    * Add onbehalf/honoree profile fields and native module fields.
    *
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage.php
index 238353ca077..bc3f23d03ea 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage.php
@@ -332,18 +332,13 @@ public function setDefaultValues() {
       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 @@ public function setDefaultValues() {
       ), '1');
     }
     else {
-      # CRM 10860
+      // CRM-10860
       $defaults['recur_frequency_unit'] = array('month' => 1);
     }
 
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/AddProduct.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/AddProduct.php
index cf3b8055157..7191085d864 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/AddProduct.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/AddProduct.php
@@ -229,15 +229,12 @@ public function postProcess() {
 
     $urlParams = 'civicrm/admin/contribute/premium';
     if ($this->_action & CRM_Core_Action::PREVIEW) {
-      $session = CRM_Core_Session::singleton();
       $url = CRM_Utils_System::url($urlParams, 'reset=1&action=update&id=' . $this->_id);
-      $single = $session->get('singleForm');
       CRM_Utils_System::redirect($url);
       return;
     }
 
     if ($this->_action & CRM_Core_Action::DELETE) {
-      $session = CRM_Core_Session::singleton();
       $url = CRM_Utils_System::url($urlParams, 'reset=1&action=update&id=' . $this->_id);
       $dao = new CRM_Contribute_DAO_PremiumsProduct();
       $dao->id = $this->_pid;
@@ -246,7 +243,6 @@ public function postProcess() {
       CRM_Utils_System::redirect($url);
     }
     else {
-      $session = CRM_Core_Session::singleton();
       $url = CRM_Utils_System::url($urlParams, 'reset=1&action=update&id=' . $this->_id);
       if ($this->_pid) {
         $params['id'] = $this->_pid;
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Amount.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Amount.php
index fc6486c24fd..ef174d94052 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Amount.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Amount.php
@@ -372,6 +372,11 @@ public static function formRule($fields, $files, $self) {
         $errors['pay_later_receipt'] = ts('Please enter the instructions to be sent to the contributor when they choose to \'pay later\'.');
       }
     }
+    else {
+      if ($fields['amount_block_is_active'] && empty($fields['payment_processor'])) {
+        $errors['payment_processor'] = ts('You have listed fixed contribution options or selected a price set, but no payment option has been selected. Please select at least one payment processor and/or enable the pay later option.');
+      }
+    }
 
     // don't allow price set w/ membership signup, CRM-5095
     if ($priceSetId = CRM_Utils_Array::value('price_set_id', $fields)) {
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Settings.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Settings.php
index 49e595bfec0..7e94089ef4e 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Settings.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Settings.php
@@ -44,6 +44,11 @@ public function preProcess() {
    */
   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 @@ public function buildQuickForm() {
     }
 
     // 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 @@ public function postProcess() {
     $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Widget.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Widget.php
index df8905af0af..9d8a621354b 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Widget.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ContributionPage/Widget.php
@@ -85,55 +85,55 @@ public function preProcess() {
     $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ManagePremiums.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ManagePremiums.php
index 8ac350b9c6a..37bad336abd 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ManagePremiums.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/ManagePremiums.php
@@ -50,8 +50,7 @@ public function setDefaultValues() {
     $defaults = parent::setDefaultValues();
     if ($this->_id) {
       $params = array('id' => $this->_id);
-      CRM_Contribute_BAO_ManagePremiums::retrieve($params, $tempDefaults);
-      $imageUrl = (isset($tempDefaults['image'])) ? $tempDefaults['image'] : "";
+      CRM_Contribute_BAO_Product::retrieve($params, $tempDefaults);
       if (isset($tempDefaults['image']) && isset($tempDefaults['thumbnail'])) {
         $defaults['imageUrl'] = $tempDefaults['image'];
         $defaults['thumbnailUrl'] = $tempDefaults['thumbnail'];
@@ -272,7 +271,15 @@ public function postProcess() {
 
     // If deleting, then only delete and skip the rest of the post-processing
     if ($this->_action & CRM_Core_Action::DELETE) {
-      CRM_Contribute_BAO_ManagePremiums::del($this->_id);
+      try {
+        CRM_Contribute_BAO_Product::del($this->_id);
+      }
+      catch (CRM_Core_Exception $e) {
+        $message = ts("This Premium is linked to an Online Contribution page. Please remove it before deleting this Premium.", array(1 => CRM_Utils_System::url('civicrm/admin/contribute', 'reset=1')));
+        CRM_Core_Session::setStatus($message, ts('Cannot delete Premium'), 'error');
+        CRM_Core_Session::singleton()->pushUserContext(CRM_Utils_System::url('civicrm/admin/contribute/managePremiums', 'reset=1&action=browse'));
+        return;
+      }
       CRM_Core_Session::setStatus(
         ts('Selected Premium Product type has been deleted.'),
         ts('Deleted'), 'info');
@@ -287,15 +294,15 @@ public function postProcess() {
       $params[$field] = CRM_Utils_Rule::cleanMoney($params[$field]);
     }
 
-    $ids = array();
+    // If we're updating, we need to pass in the premium product Id
     if ($this->_action & CRM_Core_Action::UPDATE) {
-      $ids['premium'] = $this->_id;
+      $params['id'] = $this->_id;
     }
 
     $this->_processImages($params);
 
-    // Save to database
-    $premium = CRM_Contribute_BAO_ManagePremiums::add($params, $ids);
+    // Save the premium product to database
+    $premium = CRM_Contribute_BAO_Product::create($params);
 
     CRM_Core_Session::setStatus(
       ts("The Premium '%1' has been saved.", array(1 => $premium->name)),
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Task.php
index 29640162640..f089448db49 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Form/Task.php
+++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() {
 
   /**
    * @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 @@ public function setContributionIds($contributionIds) {
    */
   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/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/ManagePremiums.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/ManagePremiums.php
index d4abc4dcd01..462e8b33ebf 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/ManagePremiums.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/ManagePremiums.php
@@ -52,7 +52,7 @@ class CRM_Contribute_Page_ManagePremiums extends CRM_Core_Page_Basic {
    *   Classname of BAO.
    */
   public function getBAOName() {
-    return 'CRM_Contribute_BAO_ManagePremiums';
+    return 'CRM_Contribute_BAO_Product';
   }
 
   /**
@@ -150,10 +150,9 @@ public function browse() {
         'Premium',
         $dao->id
       );
-      //Financial Type
+      // Financial Type
       if (!empty($dao->financial_type_id)) {
-        require_once 'CRM/Core/DAO.php';
-        $premiums[$dao->id]['financial_type_id'] = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_FinancialType', $dao->financial_type_id, 'name');
+        $premiums[$dao->id]['financial_type'] = CRM_Core_PseudoConstant::getLabel('CRM_Financial_BAO_FinancialType', 'financial_type', $dao->financial_type_id);
       }
     }
     $this->assign('rows', $premiums);
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/Premium.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/Premium.php
index 3a61d21ed0d..0028036acbc 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/Premium.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/Premium.php
@@ -130,45 +130,45 @@ public function browse() {
     $pageID = CRM_Utils_Request::retrieve('id', 'Positive',
       $this, FALSE, 0
     );
-    $dao = new CRM_Contribute_DAO_Premium();
-    $dao->entity_table = 'civicrm_contribution_page';
-    $dao->entity_id = $pageID;
-    $dao->find(TRUE);
-    $premiumID = $dao->id;
+    $premiumDao = new CRM_Contribute_DAO_Premium();
+    $premiumDao->entity_table = 'civicrm_contribution_page';
+    $premiumDao->entity_id = $pageID;
+    $premiumDao->find(TRUE);
+    $premiumID = $premiumDao->id;
     $this->assign('products', FALSE);
     $this->assign('id', $pageID);
     if (!$premiumID) {
       return;
     }
 
-    $dao = new CRM_Contribute_DAO_PremiumsProduct();
-    $dao->premiums_id = $premiumID;
-    $dao->orderBy('weight');
-    $dao->find();
+    $premiumsProductDao = new CRM_Contribute_DAO_PremiumsProduct();
+    $premiumsProductDao->premiums_id = $premiumID;
+    $premiumsProductDao->orderBy('weight');
+    $premiumsProductDao->find();
 
-    while ($dao->fetch()) {
+    while ($premiumsProductDao->fetch()) {
       $productDAO = new CRM_Contribute_DAO_Product();
-      $productDAO->id = $dao->product_id;
+      $productDAO->id = $premiumsProductDao->product_id;
       $productDAO->is_active = 1;
 
       if ($productDAO->find(TRUE)) {
         $premiums[$productDAO->id] = array();
-        $premiums[$productDAO->id]['weight'] = $dao->weight;
+        $premiums[$productDAO->id]['weight'] = $premiumsProductDao->weight;
         CRM_Core_DAO::storeValues($productDAO, $premiums[$productDAO->id]);
 
         $action = array_sum(array_keys($this->links()));
 
-        $premiums[$dao->product_id]['action'] = CRM_Core_Action::formLink(self::links(), $action,
-          array('id' => $pageID, 'pid' => $dao->id),
+        $premiums[$premiumsProductDao->product_id]['action'] = CRM_Core_Action::formLink(self::links(), $action,
+          array('id' => $pageID, 'pid' => $premiumsProductDao->id),
           ts('more'),
           FALSE,
           'premium.contributionpage.row',
           'Premium',
-          $dao->id
+          $premiumsProductDao->id
         );
-        //Financial Type
-        if (!empty($dao->financial_type_id)) {
-          $premiums[$productDAO->id]['financial_type_id'] = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_FinancialType', $dao->financial_type_id, 'name');
+        // Financial Type
+        if (!empty($premiumsProductDao->financial_type_id)) {
+          $premiums[$productDAO->id]['financial_type'] = CRM_Core_PseudoConstant::getLabel('CRM_Financial_BAO_FinancialType', 'financial_type', $premiumsProductDao->financial_type_id);
         }
       }
     }
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/Tab.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/Tab.php
index c8260fad816..fa80f7d728f 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/Tab.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Contribute/Page/Tab.php
@@ -158,7 +158,9 @@ public function browse() {
     if ($this->_contactId) {
       $displayName = CRM_Contact_BAO_Contact::displayName($this->_contactId);
       $this->assign('displayName', $displayName);
-      $this->ajaxResponse['tabCount'] = CRM_Contact_BAO_Contact::getCountComponent('contribution', $this->_contactId);
+      $tabCount = CRM_Contact_BAO_Contact::getCountComponent('contribution', $this->_contactId);
+      $this->assign('tabCount', $tabCount);
+      $this->ajaxResponse['tabCount'] = $tabCount;
     }
   }
 
@@ -166,10 +168,53 @@ public function browse() {
    * Get all the recurring contribution information and assign to the template
    */
   private function addRecurringContributionsBlock() {
+    list($activeContributions, $activeContributionsCount) = $this->getActiveRecurringContributions();
+    list($inactiveRecurringContributions, $inactiveContributionsCount) = $this->getInactiveRecurringContributions();
+
+    if (!empty($activeContributions) || !empty($inactiveRecurringContributions)) {
+      // assign vars to templates
+      $this->assign('action', $this->_action);
+      $this->assign('activeRecurRows', $activeContributions);
+      $this->assign('contributionRecurCount', $activeContributionsCount + $inactiveContributionsCount);
+      $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,
-        'options' => array('limit' => 0, 'sort' => 'start_date ASC'),
+        'contribution_status_id' => array('NOT IN' => CRM_Contribute_BAO_ContributionRecur::getInactiveStatuses()),
+        'options' => ['limit' => 0, 'sort' => 'is_test, start_date DESC'],
+      ));
+      $recurContributions = CRM_Utils_Array::value('values', $contributionRecurResult);
+    }
+    catch (Exception $e) {
+      $recurContributions = array();
+    }
+
+    return $this->buildRecurringContributionsArray($recurContributions);
+  }
+
+  /**
+   * 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' => ['limit' => 0, 'sort' => 'is_test, start_date DESC'],
       ));
       $recurContributions = CRM_Utils_Array::value('values', $contributionRecurResult);
     }
@@ -177,48 +222,58 @@ private function addRecurringContributionsBlock() {
       $recurContributions = NULL;
     }
 
-    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']);
-        }
+  /**
+   * @param $recurContributions
+   *
+   * @return mixed
+   */
+  private function buildRecurringContributionsArray($recurContributions) {
+    $liveRecurringContributionCount = 0;
+    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']));
+
+      if (empty($recurDetail['is_test'])) {
+        $liveRecurringContributionCount++;
+      }
 
-        if ($recurContributions[$recurId]['is_active']) {
-          $details = CRM_Contribute_BAO_ContributionRecur::getSubscriptionDetails($recurContributions[$recurId]['id'], 'recur');
-          $hideUpdate = $details->membership_id & $details->auto_renew;
+      // 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']);
+      }
 
-          if ($hideUpdate) {
-            $action -= CRM_Core_Action::UPDATE;
-          }
+      if ($recurContributions[$recurId]['is_active']) {
+        $details = CRM_Contribute_BAO_ContributionRecur::getSubscriptionDetails($recurContributions[$recurId]['id'], 'recur');
+        $hideUpdate = $details->membership_id & $details->auto_renew;
 
-          $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 ($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, $liveRecurringContributionCount];
   }
 
   /**
@@ -345,8 +400,6 @@ public function setContext() {
       $qfKey = NULL;
     }
 
-    $session = CRM_Core_Session::singleton();
-
     switch ($context) {
       case 'user':
         $url = CRM_Utils_System::url('civicrm/user', 'reset=1');
diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Cache.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Cache.php
index 0e619143e33..d1f97f63f66 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Cache.php
+++ b/profiles/civicrm_starterkit/modules/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,16 @@ public static function &getItem($group, $path, $componentID = NULL) {
     $argString = "CRM_CT_{$group}_{$path}_{$componentID}";
     if (!array_key_exists($argString, self::$_cache)) {
       $cache = CRM_Utils_Cache::singleton();
-      self::$_cache[$argString] = $cache->get($argString);
+      $cleanKey = self::cleanKey($argString);
+      self::$_cache[$argString] = $cache->get($cleanKey);
       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($cleanKey, self::$_cache[$argString]);
       }
     }
     return self::$_cache[$argString];
@@ -99,7 +107,8 @@ public static function &getItems($group, $componentID = NULL) {
     $argString = "CRM_CT_CI_{$group}_{$componentID}";
     if (!array_key_exists($argString, self::$_cache)) {
       $cache = CRM_Utils_Cache::singleton();
-      self::$_cache[$argString] = $cache->get($argString);
+      $cleanKey = self::cleanKey($argString);
+      self::$_cache[$argString] = $cache->get($cleanKey);
       if (!self::$_cache[$argString]) {
         $table = self::getTableName();
         $where = self::whereCache($group, NULL, $componentID);
@@ -107,12 +116,12 @@ public static function &getItems($group, $componentID = NULL) {
 
         $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($cleanKey, self::$_cache[$argString]);
       }
     }
 
@@ -148,7 +157,7 @@ public static function setItem(&$data, $group, $path, $componentID = NULL) {
     $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 +189,13 @@ public static function setItem(&$data, $group, $path, $componentID = NULL) {
 
     $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 +246,8 @@ public static function storeSessionToCache($names, $resetSession = TRUE) {
         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 +258,7 @@ public static function storeSessionToCache($names, $resetSession = TRUE) {
         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 +285,13 @@ public static function storeSessionToCache($names, $resetSession = TRUE) {
   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 +299,32 @@ public static function restoreSessionFromCache($names) {
     }
   }
 
+  /**
+   * 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 +336,7 @@ public static function restoreSessionFromCache($names) {
    * @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 +344,10 @@ public static function cleanup($session = FALSE, $table = FALSE, $prevNext = FAL
     $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 +361,43 @@ public static function cleanup($session = FALSE, $table = FALSE, $prevNext = FAL
     }
 
     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 +426,35 @@ protected static function whereCache($group, $path, $componentID) {
     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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/ConfigSetting.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/ConfigSetting.php
index 8cc76532861..4b5f3111e80 100644
--- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/ConfigSetting.php
+++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/ConfigSetting.php
@@ -96,7 +96,7 @@ public static function retrieve(&$defaults) {
       $urlVar = 'task';
     }
 
-    if ($isUpgrade && CRM_Core_DAO::checkFieldExists('civicrm_domain', 'config_backend')) {
+    if ($isUpgrade && CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_domain', 'config_backend')) {
       $domain->selectAdd('config_backend');
     }
     else {
@@ -260,6 +260,7 @@ public static function doSiteMove($defaultValues = array()) {
 
     // clear all caches
     CRM_Core_Config::clearDBCache();
+    Civi::cache('session')->clear();
     $moveStatus .= ts('Database cache tables cleared.') . '
'; $resetSessionTable = CRM_Utils_Request::retrieve('resetSessionTable', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomField.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomField.php index 41a71643196..8dd1a491fc3 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomField.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomField.php @@ -83,6 +83,30 @@ public static function &dataType() { 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. * @@ -206,6 +230,8 @@ public static function create(&$params) { $optionGroup->name = "{$columnName}_" . date('YmdHis'); $optionGroup->title = $params['label']; $optionGroup->is_active = 1; + // Don't set reserved as it's not a built-in option group and may be useful for other custom fields. + $optionGroup->is_reserved = 0; $optionGroup->data_type = $dataType; $optionGroup->save(); $params['option_group_id'] = $optionGroup->id; @@ -376,6 +402,9 @@ public function getOptions($context = NULL) { $this->find(TRUE); } + // This will hold the list of options in format key => label + $options = []; + if (!empty($this->option_group_id)) { $options = CRM_Core_OptionGroup::valuesByID( $this->option_group_id, @@ -395,9 +424,6 @@ public function getOptions($context = NULL) { elseif ($this->data_type === 'Boolean') { $options = $context == 'validate' ? array(0, 1) : CRM_Core_SelectValues::boolean(); } - else { - return FALSE; - } CRM_Utils_Hook::customFieldOptions($this->id, $options, FALSE); CRM_Utils_Hook::fieldOptions($this->getEntity(), "custom_{$this->id}", $options, array('context' => $context)); return $options; @@ -691,6 +717,7 @@ public static function getFieldsForImport( $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, @@ -2005,6 +2032,8 @@ public static function getTableColumnGroup($fieldID, $force = FALSE) { /** * Get custom option groups. * + * @deprecated Use the API OptionGroup.get + * * @param array $includeFieldIds * Ids of custom fields for which option groups must be included. * @@ -2189,46 +2218,45 @@ public static function postProcess( } /** + * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomQuery.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomQuery.php index bc082255a3d..b71acb1081c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomQuery.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomQuery.php @@ -433,7 +433,7 @@ public function where() { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomValue.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomValue.php index 516eb9fe6dc..ce72448de28 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomValue.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomValue.php @@ -207,13 +207,18 @@ public static function deleteCustomValue($customValueID, $customGroupID) { // 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomValueTable.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomValueTable.php index 9455b06a4fa..73a431065a8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomValueTable.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/CustomValueTable.php @@ -637,6 +637,10 @@ public static function setValues(&$params) { 'extends' => $dao->extends, ); + if (!empty($params['id'])) { + $cvParam['id'] = $params['id']; + } + if ($cvParam['type'] == 'File') { $cvParam['file_id'] = $fieldValue['value']; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Domain.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Domain.php index c3a9094d6f4..58a06e16d16 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Domain.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Domain.php @@ -319,4 +319,12 @@ public static function getDefaultReceiptFrom() { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/FinancialTrxn.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/FinancialTrxn.php index ccb7d9ca0a1..393714f3b42 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/FinancialTrxn.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/FinancialTrxn.php @@ -48,7 +48,7 @@ public function __construct() { * @param array $params * (reference ) an assoc array of name/value pairs. * - * @return CRM_Core_BAO_FinancialTrxn + * @return CRM_Financial_DAO_FinancialTrxn */ public static function create($params) { $trxn = new CRM_Financial_DAO_FinancialTrxn(); @@ -116,7 +116,7 @@ public static function getBalanceTrxnAmt($contributionId, $contributionFinancial * @param array $defaults * (reference ) an assoc array to hold the flattened values. * - * @return CRM_Contribute_BAO_ContributionType + * @return \CRM_Financial_DAO_FinancialTrxn */ public static function retrieve(&$params, &$defaults) { $financialItem = new CRM_Financial_DAO_FinancialTrxn(); @@ -350,7 +350,6 @@ public static function createPremiumTrxn($params) { $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name'); $toFinancialAccountType = !empty($params['isDeleted']) ? 'Premiums Inventory Account is' : 'Cost of Sales Account is'; $fromFinancialAccountType = !empty($params['isDeleted']) ? 'Cost of Sales Account is' : 'Premiums Inventory Account is'; - $accountRelationship = array_flip($accountRelationship); $financialtrxn = array( 'to_financial_account_id' => CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($params['financial_type_id'], $toFinancialAccountType), 'from_financial_account_id' => CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($params['financial_type_id'], $fromFinancialAccountType), @@ -369,7 +368,7 @@ public static function createPremiumTrxn($params) { 'id' => $params['oldPremium']['product_id'], ); $productDetails = array(); - CRM_Contribute_BAO_ManagePremiums::retrieve($premiumParams, $productDetails); + CRM_Contribute_BAO_Product::retrieve($premiumParams, $productDetails); $params = array( 'cost' => CRM_Utils_Array::value('cost', $productDetails), 'currency' => CRM_Utils_Array::value('currency', $productDetails), @@ -387,7 +386,7 @@ public static function createPremiumTrxn($params) { * @param array $params * To create trxn entries. * - * @return bool + * @return bool|void */ public static function recordFees($params) { $domainId = CRM_Core_Config::domainID(); @@ -482,7 +481,6 @@ public static function getPartialPaymentWithType($entityId, $entityName = 'parti "; $ftTotalAmt = CRM_Core_DAO::singleValueQuery($sqlFtTotalAmt); - $value = 0; if (!$ftTotalAmt) { $ftTotalAmt = 0; } @@ -507,7 +505,7 @@ public static function getPartialPaymentWithType($entityId, $entityName = 'parti /** * @param int $contributionId * - * @return array + * @return string */ public static function getTotalPayments($contributionId) { $statusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); @@ -531,7 +529,7 @@ public static function getTotalPayments($contributionId) { * @param array $contribution * @param array $params * - * @return CRM_Core_BAO_FinancialTrxn + * @return \CRM_Financial_DAO_FinancialTrxn */ public static function getPartialPaymentTrxn($contribution, $params) { $trxn = CRM_Contribute_BAO_Contribution::recordPartialPayment($contribution, $params); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Job.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Job.php index fcb749a2c79..a67ffaa5b07 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Job.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Job.php @@ -146,4 +146,25 @@ public static function cleanup($maxEntriesToKeep = 1000, $minDaysToKeep = 30) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Navigation.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Navigation.php index 968bd1bb6e7..9b5f50a00c5 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Navigation.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/Navigation.php @@ -70,6 +70,7 @@ public static function add(&$params) { if (empty($params['id'])) { $params['is_active'] = CRM_Utils_Array::value('is_active', $params, FALSE); $params['has_separator'] = CRM_Utils_Array::value('has_separator', $params, FALSE); + $params['domain_id'] = CRM_Utils_Array::value('domain_id', $params, CRM_Core_Config::domainID()); } if (!isset($params['id']) || @@ -93,8 +94,6 @@ public static function add(&$params) { $navigation->copyValues($params); - $navigation->domain_id = CRM_Core_Config::domainID(); - $navigation->save(); return $navigation; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/OptionGroup.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/OptionGroup.php index 85b2e5203de..bbabcef8bd4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/OptionGroup.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/OptionGroup.php @@ -88,22 +88,12 @@ public static function setIsActive($id, $is_active) { * @return object */ public static function add(&$params, $ids = array()) { - if (empty($params['id'])) { - $params['id'] = CRM_Utils_Array::value('optionGroup', $ids); + if (empty($params['id']) && !empty($ids['optionGroup'])) { + CRM_Core_Error::deprecatedFunctionWarning('no $ids array'); + $params['id'] = $ids['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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/OptionValue.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/OptionValue.php index 2e621cf073c..98a331c6f44 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/OptionValue.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/OptionValue.php @@ -243,6 +243,29 @@ public static function add(&$params, $ids = array()) { $optionValue->id = $id; $optionValue->save(); CRM_Core_PseudoConstant::flush(); + + // Create relationship for payment intrument options + if (!empty($params['financial_account_id'])) { + $optionName = civicrm_api3('OptionGroup', 'getvalue', [ + 'return' => 'name', + 'id' => $params['option_group_id'], + ]); + // Only create relationship for payment intrument options + if ($optionName == 'payment_instrument') { + $relationTypeId = civicrm_api3('OptionValue', 'getvalue', [ + 'return' => 'value', + 'option_group_id' => 'account_relationship', + 'name' => 'Asset Account is', + ]); + $params = [ + 'entity_table' => 'civicrm_option_value', + 'entity_id' => $optionValue->id, + 'account_relationship' => $relationTypeId, + 'financial_account_id' => $params['financial_account_id'], + ]; + CRM_Financial_BAO_FinancialTypeAccount::add($params); + } + } return $optionValue; } @@ -547,16 +570,23 @@ public static function getOptionValuesAssocArrayFromName($optionGroupName) { * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/PrevNextCache.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/PrevNextCache.php index bf9cc0e8d47..c3078140ac4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/PrevNextCache.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/PrevNextCache.php @@ -438,164 +438,18 @@ public static function cleanupCache() { CRM_Core_DAO::executeQuery($sql, $params); } - /** - * Save checkbox selections. - * - * @param $cacheKey - * @param string $action - * @param array $cIds - * @param string $entity_table - */ - public static function markSelection($cacheKey, $action = 'unselect', $cIds = NULL, $entity_table = 'civicrm_contact') { - if (!$cacheKey) { - return; - } - $params = array(); - - $entity_whereClause = " AND entity_table = '{$entity_table}'"; - if ($cIds && $cacheKey && $action) { - if (is_array($cIds)) { - $cIdFilter = "(" . implode(',', $cIds) . ")"; - $whereClause = " -WHERE cacheKey LIKE %1 -AND (entity_id1 IN {$cIdFilter} OR entity_id2 IN {$cIdFilter}) -"; - } - else { - $whereClause = " -WHERE cacheKey LIKE %1 -AND (entity_id1 = %2 OR entity_id2 = %2) -"; - $params[2] = array("{$cIds}", 'Integer'); - } - if ($action == 'select') { - $whereClause .= "AND is_selected = 0"; - $sql = "UPDATE civicrm_prevnext_cache SET is_selected = 1 {$whereClause} {$entity_whereClause}"; - $params[1] = array("{$cacheKey}%", 'String'); - } - elseif ($action == 'unselect') { - $whereClause .= "AND is_selected = 1"; - $sql = "UPDATE civicrm_prevnext_cache SET is_selected = 0 {$whereClause} {$entity_whereClause}"; - $params[1] = array("%{$cacheKey}%", 'String'); - } - // default action is reseting - } - elseif (!$cIds && $cacheKey && $action == 'unselect') { - $sql = " -UPDATE civicrm_prevnext_cache -SET is_selected = 0 -WHERE cacheKey LIKE %1 AND is_selected = 1 - {$entity_whereClause} -"; - $params[1] = array("{$cacheKey}%", 'String'); - } - CRM_Core_DAO::executeQuery($sql, $params); - } /** * Get the selections. * - * @param string $cacheKey - * Cache key. - * @param string $action - * Action. - * $action : get - get only selection records - * getall - get all the records of the specified cache key - * @param string $entity_table - * Entity table. - * - * @return array|NULL - */ - public static function getSelection($cacheKey, $action = 'get', $entity_table = 'civicrm_contact') { - if (!$cacheKey) { - return NULL; - } - $params = array(); - - $entity_whereClause = " AND entity_table = '{$entity_table}'"; - if ($cacheKey && ($action == 'get' || $action == 'getall')) { - $actionGet = ($action == "get") ? " AND is_selected = 1 " : ""; - $sql = " -SELECT entity_id1, entity_id2 FROM civicrm_prevnext_cache -WHERE cacheKey LIKE %1 - $actionGet - $entity_whereClause -ORDER BY id -"; - $params[1] = array("{$cacheKey}%", 'String'); - - $contactIds = array($cacheKey => array()); - $cIdDao = CRM_Core_DAO::executeQuery($sql, $params); - while ($cIdDao->fetch()) { - if ($cIdDao->entity_id1 == $cIdDao->entity_id2) { - $contactIds[$cacheKey][$cIdDao->entity_id1] = 1; - } - } - return $contactIds; - } - } - - /** - * @return array - */ - public static function getSelectedContacts() { - $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String'); - $cacheKey = "civicrm search {$qfKey}"; - $query = " -SELECT * -FROM civicrm_prevnext_cache -WHERE cacheKey LIKE %1 - AND is_selected=1 - AND cacheKey NOT LIKE %2 -"; - $params1[1] = array("{$cacheKey}%", 'String'); - $params1[2] = array("{$cacheKey}_alphabet%", 'String'); - $dao = CRM_Core_DAO::executeQuery($query, $params1); - - $val = array(); - while ($dao->fetch()) { - $val[] = $dao->data; - } - return $val; - } - - /** - * @param CRM_Core_Form $form - * @param array $params + * NOTE: This stub has been preserved because one extension in `universe` + * was referencing the function. * - * @return mixed + * @deprecated + * @see CRM_Core_PrevNextCache_Sql::getSelection() */ - public static function buildSelectedContactPager(&$form, &$params) { - $params['status'] = ts('Contacts %%StatusMessage%%'); - $params['csvString'] = NULL; - $params['buttonTop'] = 'PagerTopButton'; - $params['buttonBottom'] = 'PagerBottomButton'; - $params['rowCount'] = $form->get(CRM_Utils_Pager::PAGE_ROWCOUNT); - - if (!$params['rowCount']) { - $params['rowCount'] = CRM_Utils_Pager::ROWCOUNT; - } - - $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form); - $cacheKey = "civicrm search {$qfKey}"; - - $query = " -SELECT count(*) -FROM civicrm_prevnext_cache -WHERE cacheKey LIKE %1 - AND is_selected = 1 - AND cacheKey NOT LIKE %2 -"; - $params1[1] = array("{$cacheKey}%", 'String'); - $params1[2] = array("{$cacheKey}_alphabet%", 'String'); - $paramsTotal = CRM_Core_DAO::singleValueQuery($query, $params1); - $params['total'] = $paramsTotal; - $form->_pager = new CRM_Utils_Pager($params); - $form->assign_by_ref('pager', $form->_pager); - list($offset, $rowCount) = $form->_pager->getOffsetAndRowCount(); - $params['offset'] = $offset; - $params['rowCount1'] = $rowCount; - return $params; + public static function getSelection($cacheKey, $action = 'get') { + return Civi::service('prevnext')->getSelection($cacheKey, $action); } /** diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/RecurringEntity.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/RecurringEntity.php index fb73a78cf2c..a5ab757b7b8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/RecurringEntity.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/RecurringEntity.php @@ -660,7 +660,6 @@ static public function triggerUpdate($event) { } $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/SchemaHandler.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/SchemaHandler.php index ab58a8aa4b2..1a926044fff 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/SchemaHandler.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/SchemaHandler.php @@ -627,18 +627,17 @@ public static function checkIfIndexExists($tableName, $indexName) { * * @param string $tableName * @param string $columnName + * @param bool $i18nRewrite + * Whether to rewrite the query on multilingual setups. * * @return bool */ - public static function checkIfFieldExists($tableName, $columnName) { - $result = CRM_Core_DAO::executeQuery( - "SHOW COLUMNS FROM $tableName LIKE %1", - array(1 => array($columnName, 'String')) - ); - if ($result->fetch()) { - return TRUE; - } - return FALSE; + public static function checkIfFieldExists($tableName, $columnName, $i18nRewrite = TRUE) { + $query = "SHOW COLUMNS FROM $tableName LIKE '%1'"; + $dao = CRM_Core_DAO::executeQuery($query, [1 => [$columnName, 'Alphanumeric']], TRUE, NULL, FALSE, $i18nRewrite); + $result = $dao->fetch() ? TRUE : FALSE; + $dao->free(); + return $result; } /** diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/UFField.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/UFField.php index eaac6c3ff1a..e3ce7a46b55 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/UFField.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/UFField.php @@ -896,7 +896,7 @@ public static function getAvailableFields($gid = NULL, $defaults = array()) { 'address_options', TRUE, NULL, TRUE ); - if (!$addressOptions['county']) { + if (empty($addressOptions['county'])) { unset($fields['Individual']['county'], $fields['Household']['county'], $fields['Organization']['county']); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/UFGroup.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/UFGroup.php index 722fc4b9dfc..4434d2b4395 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/UFGroup.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/BAO/UFGroup.php @@ -1673,12 +1673,12 @@ public static function getWeight($ufGroupId = NULL) { public static function getModuleUFGroup($moduleName = NULL, $count = 0, $skipPermission = TRUE, $op = CRM_Core_Permission::VIEW, $returnFields = NULL) { $selectFields = array('id', 'title', 'created_id', 'is_active', 'is_reserved', 'group_type'); - if (CRM_Core_DAO::checkFieldExists('civicrm_uf_group', 'description')) { + if (CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_uf_group', 'description')) { // CRM-13555, since description field was added later (4.4), and to avoid any problems with upgrade $selectFields[] = 'description'; } - if (CRM_Core_DAO::checkFieldExists('civicrm_uf_group', 'frontend_title')) { + if (CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_uf_group', 'frontend_title')) { $selectFields[] = 'frontend_title'; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Config.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Config.php index 349c672b83d..265532c67d8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Config.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Config.php @@ -295,6 +295,7 @@ public function cleanupCaches($sessionReset = TRUE) { // clear all caches self::clearDBCache(); + Civi::cache('session')->clear(); CRM_Utils_System::flushCache(); if ($sessionReset) { @@ -356,7 +357,6 @@ public static function clearDBCache() { '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 @@ public static function clearTempTables($timeInterval = FALSE) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Config/MagicMerge.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Config/MagicMerge.php index e9414cb783e..63f15a8a589 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Config/MagicMerge.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Config/MagicMerge.php @@ -173,6 +173,7 @@ public static function getPropertyMap() { 'recaptchaOptions' => array('setting'), 'recaptchaPublicKey' => array('setting'), 'recaptchaPrivateKey' => array('setting'), + 'forceRecaptcha' => array('setting'), 'replyTo' => array('setting'), 'secondDegRelPermissions' => array('setting'), 'smartGroupCacheTimeout' => array('setting'), diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO.php index 6df94eb19ad..c6a495b61db 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO.php @@ -48,6 +48,13 @@ */ 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 @@ public function __construct() { $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. */ @@ -826,6 +849,9 @@ public static function objectExists($value, $daoName, $daoID, $fieldName = 'name /** * Check if there is a given column in a specific table. * + * @deprecated + * @see CRM_Core_BAO_SchemaHandler::checkIfFieldExists + * * @param string $tableName * @param string $columnName * @param bool $i18nRewrite @@ -835,16 +861,7 @@ public static function objectExists($value, $daoName, $daoID, $fieldName = 'name * true if exists, else false */ public static function checkFieldExists($tableName, $columnName, $i18nRewrite = TRUE) { - $query = " -SHOW COLUMNS -FROM $tableName -LIKE %1 -"; - $params = array(1 => array($columnName, 'String')); - $dao = CRM_Core_DAO::executeQuery($query, $params, TRUE, NULL, FALSE, $i18nRewrite); - $result = $dao->fetch() ? TRUE : FALSE; - $dao->free(); - return $result; + return CRM_Core_BAO_SchemaHandler::checkIfFieldExists($tableName, $columnName, $i18nRewrite); } /** @@ -1091,6 +1108,29 @@ public function fetchAll() { return $result; } + /** + * Return the results as PHP generator. + * + * @param string $type + * Whether the generator yields 'dao' objects or 'array's. + */ + public function fetchGenerator($type = 'dao') { + while ($this->fetch()) { + switch ($type) { + case 'dao': + yield $this; + break; + + case 'array': + yield $this->toArray(); + break; + + default: + throw new \RuntimeException("Invalid record type ($type)"); + } + } + } + /** * Returns a singular value. * @@ -1995,6 +2035,8 @@ public static function setCreateDefaults(&$params, $defaults) { * @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 +2285,54 @@ public static function getReferencesToTable($tableName) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/ActionLog.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/ActionLog.php index 671e1542f8c..3958e5ff22f 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/ActionLog.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/ActionLog.php @@ -200,6 +200,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Error?'), 'description' => 'Was there any error sending the reminder?', + 'default' => '0', 'table_name' => 'civicrm_action_log', 'entity' => 'ActionLog', 'bao' => 'CRM_Core_BAO_ActionLog', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/ActionSchedule.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/ActionSchedule.php index 20d7fde9d3b..c7ee8457f1a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/ActionSchedule.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/ActionSchedule.php @@ -407,6 +407,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Start Action Offset'), 'description' => 'Reminder Interval.', + 'default' => '0', 'table_name' => 'civicrm_action_schedule', 'entity' => 'ActionSchedule', 'bao' => 'CRM_Core_BAO_ActionSchedule', @@ -458,6 +459,7 @@ public static function &fields() { 'name' => 'is_repeat', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Repeat?'), + 'default' => '0', 'table_name' => 'civicrm_action_schedule', 'entity' => 'ActionSchedule', 'bao' => 'CRM_Core_BAO_ActionSchedule', @@ -486,6 +488,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Repetition Frequency Interval'), 'description' => 'Time interval for repeating the reminder.', + 'default' => '0', 'table_name' => 'civicrm_action_schedule', 'entity' => 'ActionSchedule', 'bao' => 'CRM_Core_BAO_ActionSchedule', @@ -514,6 +517,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('End Frequency Interval'), 'description' => 'Time interval till repeating the reminder.', + 'default' => '0', 'table_name' => 'civicrm_action_schedule', 'entity' => 'ActionSchedule', 'bao' => 'CRM_Core_BAO_ActionSchedule', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Address.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Address.php index de15258fbf2..877496a9d59 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Address.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Address.php @@ -311,6 +311,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Address Primary?'), 'description' => 'Is this the primary address.', + 'default' => '0', 'table_name' => 'civicrm_address', 'entity' => 'Address', 'bao' => 'CRM_Core_BAO_Address', @@ -324,6 +325,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Billing Address'), 'description' => 'Is this the billing address.', + 'default' => '0', 'table_name' => 'civicrm_address', 'entity' => 'Address', 'bao' => 'CRM_Core_BAO_Address', @@ -706,6 +708,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is manually geocoded'), 'description' => 'Is this a manually entered geo code', + 'default' => '0', 'table_name' => 'civicrm_address', 'entity' => 'Address', 'bao' => 'CRM_Core_BAO_Address', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Country.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Country.php index 5823935f092..82af44c6856 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Country.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Country.php @@ -227,6 +227,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Abbreviate Province?'), 'description' => 'Should state/province be displayed as abbreviation for contacts from this country?', + 'default' => '0', 'table_name' => 'civicrm_country', 'entity' => 'Country', 'bao' => 'CRM_Core_BAO_Country', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/CustomField.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/CustomField.php index 1f48b8858fb..fea2e7e4c7d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/CustomField.php +++ b/profiles/civicrm_starterkit/modules/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) */ /** @@ -377,6 +377,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Custom Field Is Required?'), 'description' => 'Is a value required for this property.', + 'default' => '0', 'table_name' => 'civicrm_custom_field', 'entity' => 'CustomField', 'bao' => 'CRM_Core_BAO_CustomField', @@ -387,6 +388,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Allow Searching on Field?'), 'description' => 'Is this property searchable.', + 'default' => '0', 'table_name' => 'civicrm_custom_field', 'entity' => 'CustomField', 'bao' => 'CRM_Core_BAO_CustomField', @@ -397,6 +399,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Search as a Range'), 'description' => 'Is this property range searchable.', + 'default' => '0', 'table_name' => 'civicrm_custom_field', 'entity' => 'CustomField', 'bao' => 'CRM_Core_BAO_CustomField', @@ -486,6 +489,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Field is Viewable'), 'description' => 'Is this property set by PHP Code? A code field is viewable but not editable', + 'default' => '0', 'table_name' => 'civicrm_custom_field', 'entity' => 'CustomField', 'bao' => 'CRM_Core_BAO_CustomField', @@ -594,6 +598,11 @@ public static function &fields() { 'entity' => 'CustomField', 'bao' => 'CRM_Core_BAO_CustomField', 'localizable' => 0, + 'pseudoconstant' => [ + 'table' => 'civicrm_option_group', + 'keyColumn' => 'id', + 'labelColumn' => 'title', + ] ], 'filter' => [ 'name' => 'filter', @@ -612,6 +621,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Field Display'), 'description' => 'Should the multi-record custom field values be displayed in tab table listing', + 'default' => '0', 'table_name' => 'civicrm_custom_field', 'entity' => 'CustomField', 'bao' => 'CRM_Core_BAO_CustomField', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/CustomGroup.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/CustomGroup.php index 93d82140553..9e014563bc5 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/CustomGroup.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/CustomGroup.php @@ -309,6 +309,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Collapse Custom Group?'), 'description' => 'Will this group be in collapsed or expanded mode on initial display ?', + 'default' => '0', 'table_name' => 'civicrm_custom_group', 'entity' => 'CustomGroup', 'bao' => 'CRM_Core_BAO_CustomGroup', @@ -384,6 +385,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Supports Multiple Records'), 'description' => 'Does this group hold multiple values?', + 'default' => '0', 'table_name' => 'civicrm_custom_group', 'entity' => 'CustomGroup', 'bao' => 'CRM_Core_BAO_CustomGroup', @@ -414,6 +416,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Collapse Group Display'), 'description' => 'Will this group be in collapsed or expanded mode on advanced search display ?', + 'default' => '0', 'table_name' => 'civicrm_custom_group', 'entity' => 'CustomGroup', 'bao' => 'CRM_Core_BAO_CustomGroup', @@ -445,6 +448,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Reserved Group?'), 'description' => 'Is this a reserved Custom Group?', + 'default' => '0', 'table_name' => 'civicrm_custom_group', 'entity' => 'CustomGroup', 'bao' => 'CRM_Core_BAO_CustomGroup', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Dashboard.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Dashboard.php index 3974eab19e4..634e0f34124 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Dashboard.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Dashboard.php @@ -238,6 +238,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Dashlet Active?'), 'description' => 'Is this dashlet active?', + 'default' => '0', 'table_name' => 'civicrm_dashboard', 'entity' => 'Dashboard', 'bao' => 'CRM_Core_BAO_Dashboard', @@ -248,6 +249,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Dashlet Reserved?'), 'description' => 'Is this dashlet reserved?', + 'default' => '0', 'table_name' => 'civicrm_dashboard', 'entity' => 'Dashboard', 'bao' => 'CRM_Core_BAO_Dashboard', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Email.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Email.php index 7abc05f434d..9d7d24eba26 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Email.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Email.php @@ -209,6 +209,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Primary email'), 'description' => 'Is this the primary?', + 'default' => '0', 'table_name' => 'civicrm_email', 'entity' => 'Email', 'bao' => 'CRM_Core_BAO_Email', @@ -219,6 +220,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Billing Email?'), 'description' => 'Is this the billing?', + 'default' => '0', 'table_name' => 'civicrm_email', 'entity' => 'Email', 'bao' => 'CRM_Core_BAO_Email', @@ -234,6 +236,7 @@ public static function &fields() { 'where' => 'civicrm_email.on_hold', 'headerPattern' => '', 'dataPattern' => '', + 'default' => '0', 'table_name' => 'civicrm_email', 'entity' => 'Email', 'bao' => 'CRM_Core_BAO_Email', @@ -252,6 +255,7 @@ public static function &fields() { 'where' => 'civicrm_email.is_bulkmail', 'headerPattern' => '', 'dataPattern' => '', + 'default' => '0', 'table_name' => 'civicrm_email', 'entity' => 'Email', 'bao' => 'CRM_Core_BAO_Email', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/IM.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/IM.php index dcdf67f176d..35a1b01d9eb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/IM.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/IM.php @@ -190,6 +190,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is IM Primary?'), 'description' => 'Is this the primary IM for this contact and location.', + 'default' => '0', 'table_name' => 'civicrm_im', 'entity' => 'IM', 'bao' => 'CRM_Core_BAO_IM', @@ -200,6 +201,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is IM Billing?'), 'description' => 'Is this the billing?', + 'default' => '0', 'table_name' => 'civicrm_im', 'entity' => 'IM', 'bao' => 'CRM_Core_BAO_IM', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/MessageTemplate.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/MessageTemplate.php index 93d2b0ec7ec..90beadc0aac 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/MessageTemplate.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/MessageTemplate.php @@ -224,6 +224,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Message Template is used for SMS?'), 'description' => 'Is this message template used for sms?', + 'default' => '0', 'table_name' => 'civicrm_msg_template', 'entity' => 'MessageTemplate', 'bao' => 'CRM_Core_BAO_MessageTemplate', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OpenID.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OpenID.php index 90569e58f06..0033e3f37e2 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OpenID.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OpenID.php @@ -157,6 +157,7 @@ public static function &fields() { 'title' => ts('Allowed to login?'), 'description' => 'Whether or not this user is allowed to login', 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_openid', 'entity' => 'OpenID', 'bao' => 'CRM_Core_BAO_OpenID', @@ -167,6 +168,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is OpenID Primary?'), 'description' => 'Is this the primary email for this contact and location.', + 'default' => '0', 'table_name' => 'civicrm_openid', 'entity' => 'OpenID', 'bao' => 'CRM_Core_BAO_OpenID', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OptionGroup.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OptionGroup.php index aed6b231675..3f470ce1a5e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OptionGroup.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OptionGroup.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Core/OptionGroup.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:4f26b702ecd914e653257f0676896d87) + * (GenCodeChecksum:2fd99e11712c8619f29a79b062cf7612) */ /** @@ -168,6 +168,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Option Group Is Reserved?'), 'description' => 'Is this a predefined system option group (i.e. it can not be deleted)?', + 'required' => TRUE, 'default' => '1', 'table_name' => 'civicrm_option_group', 'entity' => 'OptionGroup', @@ -179,6 +180,8 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Option Group Is Active?'), 'description' => 'Is this option group active?', + 'required' => TRUE, + 'default' => '1', 'table_name' => 'civicrm_option_group', 'entity' => 'OptionGroup', 'bao' => 'CRM_Core_BAO_OptionGroup', @@ -189,6 +192,8 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Option Group Is Locked'), 'description' => 'A lock to remove the ability to add new options via the UI.', + 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_option_group', 'entity' => 'OptionGroup', 'bao' => 'CRM_Core_BAO_OptionGroup', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OptionValue.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OptionValue.php index 1875c752131..dbf7059bb18 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OptionValue.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/OptionValue.php @@ -286,6 +286,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Option is Default?'), 'description' => 'Is this the default option for the group?', + 'default' => '0', 'table_name' => 'civicrm_option_value', 'entity' => 'OptionValue', 'bao' => 'CRM_Core_BAO_OptionValue', @@ -322,6 +323,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Option is Header?'), 'description' => 'Is this row simply a display header? Expected usage is to render these as OPTGROUP tags within a SELECT field list of options?', + 'default' => '0', 'table_name' => 'civicrm_option_value', 'entity' => 'OptionValue', 'bao' => 'CRM_Core_BAO_OptionValue', @@ -332,6 +334,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Option Is Reserved?'), 'description' => 'Is this a predefined system object?', + 'default' => '0', 'table_name' => 'civicrm_option_value', 'entity' => 'OptionValue', 'bao' => 'CRM_Core_BAO_OptionValue', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Persistent.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Persistent.php index b6cdf47d6d4..43954d583fa 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Persistent.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Persistent.php @@ -132,6 +132,7 @@ public static function &fields() { 'title' => ts('Is Configuration?'), 'description' => 'Config Settings', 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_persistent', 'entity' => 'Persistent', 'bao' => 'CRM_Core_BAO_Persistent', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Phone.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Phone.php index 013b416c940..660ca3f0527 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Phone.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Phone.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Core/Phone.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:661faad4886dd1a40784d465b906f447) + * (GenCodeChecksum:22802e5d7d8dfce93626004aaf6cd2e2) */ /** @@ -174,6 +174,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Phone Primary?'), 'description' => 'Is this the primary phone for this contact and location.', + 'default' => '0', 'table_name' => 'civicrm_phone', 'entity' => 'Phone', 'bao' => 'CRM_Core_BAO_Phone', @@ -184,6 +185,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Billing Phone'), 'description' => 'Is this the billing?', + 'default' => '0', 'table_name' => 'civicrm_phone', 'entity' => 'Phone', 'bao' => 'CRM_Core_BAO_Phone', @@ -256,6 +258,10 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Phone Type'), 'description' => 'Which type of phone does this number belongs.', + 'export' => TRUE, + 'where' => 'civicrm_phone.phone_type_id', + 'headerPattern' => '', + 'dataPattern' => '', 'table_name' => 'civicrm_phone', 'entity' => 'Phone', 'bao' => 'CRM_Core_BAO_Phone', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/PrevNextCache.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/PrevNextCache.php index 26a8d919606..bc01e75e1c9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/PrevNextCache.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/PrevNextCache.php @@ -160,6 +160,7 @@ public static function &fields() { 'name' => 'is_selected', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Selected'), + 'default' => '0', 'table_name' => 'civicrm_prevnext_cache', 'entity' => 'PrevNextCache', 'bao' => 'CRM_Core_BAO_PrevNextCache', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Tag.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Tag.php index df103628dad..14160e2496f 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Tag.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/Tag.php @@ -194,6 +194,7 @@ public static function &fields() { 'name' => 'is_reserved', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Reserved'), + 'default' => '0', 'table_name' => 'civicrm_tag', 'entity' => 'Tag', 'bao' => 'CRM_Core_BAO_Tag', @@ -203,6 +204,7 @@ public static function &fields() { 'name' => 'is_tagset', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Tagset'), + 'default' => '0', 'table_name' => 'civicrm_tag', 'entity' => 'Tag', 'bao' => 'CRM_Core_BAO_Tag', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/UFField.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/UFField.php index bd5a61dab55..b560e442ed3 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/UFField.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/UFField.php @@ -253,6 +253,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Profile Is View Only'), 'description' => 'the field is view only and not editable in user forms.', + 'default' => '0', 'table_name' => 'civicrm_uf_field', 'entity' => 'UFField', 'bao' => 'CRM_Core_BAO_UFField', @@ -263,6 +264,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Profile Field Is Required'), 'description' => 'Is this field required when included in a user or registration form?', + 'default' => '0', 'table_name' => 'civicrm_uf_field', 'entity' => 'UFField', 'bao' => 'CRM_Core_BAO_UFField', @@ -324,6 +326,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Profile Field Is a Filter'), 'description' => 'Is this field included as a column in the selector table?', + 'default' => '0', 'table_name' => 'civicrm_uf_field', 'entity' => 'UFField', 'bao' => 'CRM_Core_BAO_UFField', @@ -334,6 +337,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Profile Field Is Searchable'), 'description' => 'Is this field included search form of profile?', + 'default' => '0', 'table_name' => 'civicrm_uf_field', 'entity' => 'UFField', 'bao' => 'CRM_Core_BAO_UFField', @@ -410,6 +414,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Profile Field Supports Multiple'), 'description' => 'Include in multi-record listing?', + 'default' => '0', 'table_name' => 'civicrm_uf_field', 'entity' => 'UFField', 'bao' => 'CRM_Core_BAO_UFField', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/UFGroup.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/UFGroup.php index c0b5d43b477..6605de613ca 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/DAO/UFGroup.php +++ b/profiles/civicrm_starterkit/modules/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. */ @@ -390,6 +397,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Show Captcha On Profile'), 'description' => 'Should a CAPTCHA widget be included this Profile form.', + 'default' => '0', 'table_name' => 'civicrm_uf_group', 'entity' => 'UFGroup', 'bao' => 'CRM_Core_BAO_UFGroup', @@ -400,6 +408,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Map Profile'), 'description' => 'Do we want to map results from this profile.', + 'default' => '0', 'table_name' => 'civicrm_uf_group', 'entity' => 'UFGroup', 'bao' => 'CRM_Core_BAO_UFGroup', @@ -410,6 +419,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Show Edit Link?'), 'description' => 'Should edit link display in profile selector', + 'default' => '0', 'table_name' => 'civicrm_uf_group', 'entity' => 'UFGroup', 'bao' => 'CRM_Core_BAO_UFGroup', @@ -420,6 +430,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Show Link to CMS User'), 'description' => 'Should we display a link to the website profile in profile selector', + 'default' => '0', 'table_name' => 'civicrm_uf_group', 'entity' => 'UFGroup', 'bao' => 'CRM_Core_BAO_UFGroup', @@ -430,6 +441,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Update on Duplicate'), 'description' => 'Should we update the contact record if we find a duplicate', + 'default' => '0', 'table_name' => 'civicrm_uf_group', 'entity' => 'UFGroup', 'bao' => 'CRM_Core_BAO_UFGroup', @@ -452,6 +464,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Create CMS User?'), 'description' => 'Should we create a cms user for this profile ', + 'default' => '0', 'table_name' => 'civicrm_uf_group', 'entity' => 'UFGroup', 'bao' => 'CRM_Core_BAO_UFGroup', @@ -514,6 +527,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Include Proximity Search?'), 'description' => 'Should we include proximity search feature in this profile search form?', + 'default' => '0', 'table_name' => 'civicrm_uf_group', 'entity' => 'UFGroup', 'bao' => 'CRM_Core_BAO_UFGroup', @@ -545,6 +559,17 @@ public static function &fields() { '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form.php index 113411d7ff8..70e560494a7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form.php +++ b/profiles/civicrm_starterkit/modules/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 */ @@ -1566,6 +1589,7 @@ public function addField($name, $props = array(), $required = FALSE, $legacyDate return $this->addEntityRef($name, $label, $props, $required); case 'Password': + $props['size'] = isset($props['size']) ? $props['size'] : 60; return $this->add('password', $name, $label, $props, $required); // Check datatypes of fields @@ -1845,7 +1869,7 @@ public function addCurrency( $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form/EntityFormTrait.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form/EntityFormTrait.php index 564edf6754e..f0031490f3e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form/EntityFormTrait.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form/EntityFormTrait.php @@ -131,6 +131,47 @@ protected function addFormButtons() { } } + /** + * Get the defaults for the entity. + */ + protected function getEntityDefaults() { + $defaults = []; + if ($this->_action != CRM_Core_Action::DELETE && + $this->getEntityId() + ) { + $params = ['id' => $this->getEntityId()]; + $baoName = $this->_BAOName; + $baoName::retrieve($params, $defaults); + } + foreach ($this->entityFields as $fieldSpec) { + $value = CRM_Utils_Request::retrieveValue($fieldSpec['name'], $this->getValidationTypeForField($fieldSpec['name'])); + if ($value !== FALSE) { + $defaults[$fieldSpec['name']] = $value; + } + } + return $defaults; + } + + /** + * Get the validation rule to apply to a function. + * + * Alphanumeric is designed to always be safe & for now we just return + * that but in future we can use tighter rules for types like int, bool etc. + * + * @param string $fieldName + * + * @return string|int|bool + */ + protected function getValidationTypeForField($fieldName) { + switch ($this->metadata[$fieldName]['type']) { + case CRM_Utils_Type::T_BOOLEAN: + return 'Boolean'; + + default: + return 'Alphanumeric'; + } + } + /** * Set translated fields. * diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form/Task.php index 2a487ba70f1..7eea0555c59 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() { /** * 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 @@ public static function preProcessCommon(&$form, $useTable = FALSE) { $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 @@ public function addDefaultButtons($title, $nextType = 'next', $backType = 'back' ); } + /** + * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/I18n/Schema.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/I18n/Schema.php index 9d72908401e..71627d76d4b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/I18n/Schema.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/I18n/Schema.php @@ -265,7 +265,7 @@ public static function addLocale($locale, $source) { // add new columns foreach ($hash as $column => $type) { // CRM-7854: skip existing columns - if (CRM_Core_DAO::checkFieldExists($table, "{$column}_{$locale}", FALSE)) { + if (CRM_Core_BAO_SchemaHandler::checkIfFieldExists($table, "{$column}_{$locale}", FALSE)) { continue; } $queries[] = "ALTER TABLE {$table} ADD {$column}_{$locale} {$type}"; diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Invoke.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Invoke.php index e5edcd82c95..eb2bef51af7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Invoke.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Invoke.php @@ -350,7 +350,7 @@ public static function statusCheck($template) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/JobManager.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/JobManager.php index e7008ea6451..fccfc18b8cf 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/JobManager.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/JobManager.php @@ -141,12 +141,15 @@ public function executeJob($job) { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Menu.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Menu.php index c7c09043550..58c9904d061 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Menu.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Menu.php @@ -317,7 +317,7 @@ public static function store($truncate = TRUE) { $menu->find(TRUE); if (!CRM_Core_Config::isUpgradeMode() || - CRM_Core_DAO::checkFieldExists('civicrm_menu', 'module_data', FALSE) + CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_menu', 'module_data', FALSE) ) { // Move unrecognized fields to $module_data. $module_data = array(); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/OptionGroup.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/OptionGroup.php index bc1f7ad2ac8..c7e07e67d61 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/OptionGroup.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/OptionGroup.php @@ -200,8 +200,8 @@ protected static function flushValues($name, $flip, $grouping, $localize, $condi /** * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/OptionValue.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/OptionValue.php index 27a1a756f07..4b1e5105ba2 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/OptionValue.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/OptionValue.php @@ -187,7 +187,7 @@ public static function getRows($groupParams, $links, $orderBy = 'weight', $skipE * * @param array $params * Array containing exported values from the invoking form. - * @param array $groupParams + * @param string $optionGroupName * Array containing group fields whose option-values is to retrieved/saved. * @param $action * @param int $optionValueID Has the id of the optionValue being edited, disabled ..etc. @@ -196,22 +196,17 @@ public static function getRows($groupParams, $links, $orderBy = 'weight', $skipE * @return CRM_Core_DAO_OptionValue * */ - public static function addOptionValue(&$params, &$groupParams, $action, $optionValueID) { + public static function addOptionValue(&$params, $optionGroupName, $action, $optionValueID) { $params['is_active'] = CRM_Utils_Array::value('is_active', $params, FALSE); // checking if the group name with the given id or name (in $groupParams) exists - if (!empty($groupParams)) { - $config = CRM_Core_Config::singleton(); - $groupParams['is_active'] = 1; - $optionGroup = CRM_Core_BAO_OptionGroup::retrieve($groupParams, $defaults); - } + $groupParams = ['name' => $optionGroupName, 'is_active' => 1]; + $optionGroup = CRM_Core_BAO_OptionGroup::retrieve($groupParams, $defaults); - // if the corresponding group doesn't exist, create one, provided $groupParams has 'name' in it. + // if the corresponding group doesn't exist, create one. if (!$optionGroup->id) { - if ($groupParams['name']) { - $newOptionGroup = CRM_Core_BAO_OptionGroup::add($groupParams, $defaults); - $params['weight'] = 1; - $optionGroupID = $newOptionGroup->id; - } + $newOptionGroup = CRM_Core_BAO_OptionGroup::add($groupParams); + $params['weight'] = 1; + $optionGroupID = $newOptionGroup->id; } else { $optionGroupID = $optionGroup->id; diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment.php index 8626cd790de..b686cd8501a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment.php @@ -682,7 +682,6 @@ public function getPaymentFormFieldsMetadata() { 'htmlType' => 'text', 'name' => 'credit_card_number', 'title' => ts('Card Number'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 20, @@ -695,7 +694,6 @@ public function getPaymentFormFieldsMetadata() { 'htmlType' => 'text', 'name' => 'cvv2', 'title' => ts('Security Code'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 5, 'maxlength' => 10, @@ -714,7 +712,6 @@ public function getPaymentFormFieldsMetadata() { '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 @@ public function getPaymentFormFieldsMetadata() { 'htmlType' => 'select', 'name' => 'credit_card_type', 'title' => ts('Card Type'), - 'cc_field' => TRUE, 'attributes' => $creditCardType, 'is_required' => FALSE, ), @@ -737,7 +733,6 @@ public function getPaymentFormFieldsMetadata() { 'htmlType' => 'text', 'name' => 'account_holder', 'title' => ts('Account Holder'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 34, @@ -750,7 +745,6 @@ public function getPaymentFormFieldsMetadata() { 'htmlType' => 'text', 'name' => 'bank_account_number', 'title' => ts('Bank Account Number'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 34, @@ -770,7 +764,6 @@ public function getPaymentFormFieldsMetadata() { 'htmlType' => 'text', 'name' => 'bank_identification_number', 'title' => ts('Bank Identification Number'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 11, @@ -789,7 +782,6 @@ public function getPaymentFormFieldsMetadata() { 'htmlType' => 'text', 'name' => 'bank_name', 'title' => ts('Bank Name'), - 'cc_field' => TRUE, 'attributes' => array( 'size' => 20, 'maxlength' => 64, @@ -803,7 +795,6 @@ public function getPaymentFormFieldsMetadata() { 'name' => 'check_number', 'title' => ts('Check Number'), 'is_required' => FALSE, - 'cc_field' => TRUE, 'attributes' => NULL, ), 'pan_truncation' => array( @@ -811,7 +802,6 @@ public function getPaymentFormFieldsMetadata() { '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 @@ public function getPaymentFormFieldsMetadata() { ), ), ), + '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php index 2696e2a1abb..e350195886a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php @@ -77,7 +77,7 @@ public function main($component = 'contribute') { // 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/Form.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/Form.php index 05de148e824..60e946d7dd0 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/Form.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/Form.php @@ -68,8 +68,7 @@ static public function setPaymentFieldsByProcessor(&$form, $processor, $billing_ $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 @@ static protected function setBillingAddressFields(&$form, $processor) { 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 @@ public static function getPaymentTypeName($paymentProcessor) { * @return string */ public static function getPaymentTypeLabel($paymentProcessor) { - return ts(($paymentProcessor['object']->getPaymentTypeLabel()) . ' Information'); + return ts('%1 Information', [$paymentProcessor->getPaymentTypeLabel()]); } /** @@ -426,4 +423,25 @@ public static function getCreditCardExpirationYear($src) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalIPN.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalIPN.php index 0100ca27d54..fa4f164a420 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalIPN.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function __construct($inputData) { /** * @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

" . 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

"; - 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

"; - return FALSE; + return; } $recur = &$objects['contributionRecur']; @@ -110,9 +108,9 @@ public function recur(&$input, &$ids, &$objects, $first) { // 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

"; - return FALSE; + return; } $now = date('YmdHis'); @@ -127,18 +125,19 @@ public function recur(&$input, &$ids, &$objects, $first) { } $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 @@ public function recur(&$input, &$ids, &$objects, $first) { 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 @@ public function recur(&$input, &$ids, &$objects, $first) { 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

"; - return FALSE; + return; case 'subscr_payment': if ($first) { @@ -180,8 +179,8 @@ public function recur(&$input, &$ids, &$objects, $first) { // 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 @@ public function recur(&$input, &$ids, &$objects, $first) { $recur->save(); if ($sendNotification) { - $autoRenewMembership = FALSE; if ($recur->id && isset($ids['membership']) && $ids['membership'] @@ -211,27 +209,28 @@ public function recur(&$input, &$ids, &$objects, $first) { } 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

"; - return TRUE; + return; } - $contribution->contact_id = $ids['contact']; - $contribution->financial_type_id = $objects['contributionType']->id; - $contribution->contribution_page_id = $ids['contributionPage']; - $contribution->contribution_recur_id = $ids['contributionRecur']; - $contribution->receive_date = $now; - $contribution->currency = $objects['contribution']->currency; - $contribution->payment_instrument_id = $objects['contribution']->payment_instrument_id; - $contribution->amount_level = $objects['contribution']->amount_level; - $contribution->campaign_id = $objects['contribution']->campaign_id; - - $objects['contribution'] = &$contribution; + if ($input['paymentStatus'] != 'Completed') { + throw new CRM_Core_Exception("Ignore all IPN payments that are not completed"); + } + + // In future moving to create pending & then complete, but this OK for now. + // Also consider accepting 'Failed' like other processors. + $input['contribution_status_id'] = $contributionStatuses['Completed']; + $input['original_contribution_id'] = $ids['contribution']; + $input['contribution_recur_id'] = $ids['contributionRecur']; + + civicrm_api3('Contribution', 'repeattransaction', $input); + return; } $this->single($input, $ids, $objects, @@ -240,27 +239,23 @@ public function recur(&$input, &$ids, &$objects, $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 - ) { + 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

"; - return FALSE; + return; } } else { @@ -269,9 +264,9 @@ public function single( 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

"; - return FALSE; + return; } } else { @@ -280,9 +275,6 @@ public function single( $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 +290,12 @@ public function single( } // 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

"; - return TRUE; + return; } $this->completeTransaction($input, $ids, $objects, $transaction, $recur); @@ -311,10 +304,10 @@ public function single( /** * 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; @@ -337,23 +330,12 @@ public function main() { $ids['onbehalf_dupe_alert'] = $this->retrieve('onBehalfDupeAlert', 'Integer', FALSE); } - $paymentProcessorID = $this->retrieve('processor_id', 'Integer', FALSE); - if (empty($paymentProcessorID)) { - $processorParams = array( - 'user_name' => $this->retrieve('business', 'String', FALSE), - 'payment_processor_type_id' => CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'PayPal_Standard', 'id', 'name'), - 'is_test' => empty($input['is_test']) ? 0 : 1, - ); + $paymentProcessorID = self::getPayPalPaymentProcessorID($input, $ids); - $processorInfo = array(); - if (!CRM_Financial_BAO_PaymentProcessor::retrieve($processorParams, $processorInfo)) { - return FALSE; - } - $paymentProcessorID = $processorInfo['id']; - } + Civi::log()->debug('PayPalIPN: Received (ContactID: ' . $ids['contact'] . '; trxn_id: ' . $input['trxn_id'] . ').'); if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) { - return FALSE; + return; } self::$_paymentProcessor = &$objects['paymentProcessor']; @@ -361,29 +343,26 @@ public function main() { 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); + $this->recur($input, $ids, $objects, $first); + return; } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); - } - } - 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); @@ -411,6 +390,64 @@ public function getInput(&$input, &$ids) { $input['fee_amount'] = $this->retrieve('mc_fee', 'Money', FALSE); $input['net_amount'] = $this->retrieve('settle_amount', 'Money', FALSE); $input['trxn_id'] = $this->retrieve('txn_id', 'String', FALSE); + + $paymentDate = $this->retrieve('payment_date', 'String', FALSE); + if (!empty($paymentDate)) { + $receiveDateTime = new DateTime($paymentDate); + $input['receive_date'] = $receiveDateTime->format('YmdHis'); + } + } + + + /** + * Gets PaymentProcessorID for PayPal + * + * @param array $input + * @param array $ids + * + * @return int + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception + */ + public function getPayPalPaymentProcessorID($input, $ids) { + // First we try and retrieve from POST params + $paymentProcessorID = $this->retrieve('processor_id', 'Integer', FALSE); + if (!empty($paymentProcessorID)) { + return $paymentProcessorID; + } + + // Then we try and get it from recurring contribution ID + if (!empty($ids['contributionRecur'])) { + $contributionRecur = civicrm_api3('ContributionRecur', 'getsingle', array( + 'id' => $ids['contributionRecur'], + 'return' => ['payment_processor_id'], + )); + if (!empty($contributionRecur['payment_processor_id'])) { + return $contributionRecur['payment_processor_id']; + } + } + + // 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). + + Civi::log()->warning('Unreliable method used to get payment_processor_id for PayPal IPN - this will cause problems if you have more than one instance'); + // Then we try and retrieve based on business email ID + $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'PayPal_Standard', 'id', 'name'); + $processorParams = [ + 'user_name' => $this->retrieve('business', 'String', FALSE), + 'payment_processor_type_id' => $paymentProcessorTypeID, + 'is_test' => empty($input['is_test']) ? 0 : 1, + 'options' => ['limit' => 1], + 'return' => ['id'], + ]; + $paymentProcessorID = civicrm_api3('PaymentProcessor', 'getvalue', $processorParams); + if (empty($paymentProcessorID)) { + Throw new CRM_Core_Exception('PayPalIPN: Could not get Payment Processor ID'); + } + return $paymentProcessorID; } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalImpl.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalImpl.php index 3fa00668924..8ab1bbec3a7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalImpl.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalImpl.php @@ -412,7 +412,6 @@ public function doExpressCheckout(&$params) { */ 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 @@ public function createRecurringPayments(&$params) { $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 @@ public function createRecurringPayments(&$params) { 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 @@ public function isSuppressSubmitButtons() { * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalProIPN.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalProIPN.php index d230762395c..abbaece1970 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalProIPN.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayPalProIPN.php @@ -161,13 +161,13 @@ public function retrieve($name, $type, $location = 'POST', $abort = TRUE) { * @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

"; - return FALSE; + return; } $recur = &$objects['contributionRecur']; @@ -176,9 +176,9 @@ public function recur(&$input, &$ids, &$objects, $first) { // 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 @@ public function recur(&$input, &$ids, &$objects, $first) { //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; @@ -237,16 +236,16 @@ public function recur(&$input, &$ids, &$objects, $first) { $recur->start_date = $now; } else { - $input['invoice_id'] = md5(uniqid(rand(), TRUE)); - $input['original_contribution_id'] = $ids['contribution']; - $input['contribution_recur_id'] = $ids['contributionRecur']; - if ($input['paymentStatus'] != 'Completed') { throw new CRM_Core_Exception("Ignore all IPN payments that are not completed"); } + // In future moving to create pending & then complete, but this OK for now. // Also consider accepting 'Failed' like other processors. - $input['contribution_status_id'] = 1; + $input['contribution_status_id'] = $contributionStatuses['Completed']; + $input['invoice_id'] = md5(uniqid(rand(), TRUE)); + $input['original_contribution_id'] = $ids['contribution']; + $input['contribution_recur_id'] = $ids['contributionRecur']; civicrm_api3('Contribution', 'repeattransaction', $input); return; @@ -256,9 +255,9 @@ public function recur(&$input, &$ids, &$objects, $first) { 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 @@ public function recur(&$input, &$ids, &$objects, $first) { // 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 @@ public function recur(&$input, &$ids, &$objects, $first) { } if ($txnType != 'recurring_payment') { - return TRUE; + return; } if (!$first) { @@ -300,9 +299,9 @@ public function recur(&$input, &$ids, &$objects, $first) { $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

"; - return TRUE; + return; } $contribution->contact_id = $recur->contact_id; @@ -319,19 +318,17 @@ public function recur(&$input, &$ids, &$objects, $first) { // 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 @@ public function single(&$input, &$ids, &$objects, $recur = FALSE, $first = FALSE // 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

contribution is" . $contribution->invoice_id . " and input is " . $input['invoice']; - return FALSE; + return; } } else { @@ -350,9 +347,9 @@ public function single(&$input, &$ids, &$objects, $recur = FALSE, $first = FALSE 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

"; - return FALSE; + return; } } else { @@ -363,24 +360,29 @@ public function single(&$input, &$ids, &$objects, $recur = FALSE, $first = FALSE $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

"; - return TRUE; + return; } $this->completeTransaction($input, $ids, $objects, $transaction, $recur); @@ -397,6 +399,9 @@ public function getPayPalPaymentProcessorID() { // 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 @@ public function getPayPalPaymentProcessorID() { * (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 @@ public function main() { } } - // 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 @@ public function main() { 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 @@ public function getInput(&$input, &$ids) { /** * 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 @@ public function handlePaymentExpress() { // 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 @@ public function handlePaymentExpress() { // 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayflowPro.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayflowPro.php index 2fcdf724084..2020b2a8cd8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayflowPro.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Payment/PayflowPro.php @@ -567,85 +567,4 @@ public function submit_transaction($submiturl, $payflow_query) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Permission.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Permission.php index ee7b49f69d6..a532a4a2c4d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Permission.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Permission.php @@ -761,6 +761,14 @@ public static function getCorePermissions() { 'delete activities' => array( $prefix . ts('Delete activities'), ), + 'edit inbound email basic information' => array( + $prefix . ts('edit inbound email basic information'), + ts('Edit all inbound email activities (for visible contacts) basic information. Content editing not allowed.'), + ), + 'edit inbound email basic information and content' => array( + $prefix . ts('edit inbound email basic information and content'), + ts('Edit all inbound email activities (for visible contacts) basic information and content.'), + ), 'access CiviCRM' => array( $prefix . ts('access CiviCRM backend and API'), ts('Master control for access to the main CiviCRM backend and API. Give to trusted roles only.'), @@ -1465,11 +1473,18 @@ public static function getEntityActionPermissions() { $permissions['option_value'] = $permissions['uf_group']; $permissions['option_group'] = $permissions['option_value']; + $permissions['custom_value'] = array( + 'gettree' => array('access CiviCRM'), + ); + $permissions['message_template'] = array( 'get' => array('access CiviCRM'), '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/PrevNextCache/Interface.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/PrevNextCache/Interface.php new file mode 100644 index 00000000000..c71b57ed24f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/PrevNextCache/Interface.php @@ -0,0 +1,115 @@ +message); + } + return TRUE; + } + + public function fillWithArray($cacheKey, $rows) { + if (empty($rows)) { + return; + } + + $insert = CRM_Utils_SQL_Insert::into('civicrm_prevnext_cache') + ->columns([ + 'entity_table', + 'entity_id1', + 'entity_id2', + 'cacheKey', + 'data' + ]); + + foreach ($rows as &$row) { + $insert->row($row + ['cacheKey' => $cacheKey]); + } + + CRM_Core_DAO::executeQuery($insert->toSQL()); + return TRUE; + } + + /** + * Save checkbox selections. + * + * @param string $cacheKey + * @param string $action + * Ex: 'select', 'unselect'. + * @param array|int|NULL $cIds + * A list of contact IDs to (un)select. + * To unselect all contact IDs, use NULL. + */ + public function markSelection($cacheKey, $action, $cIds = NULL) { + $entity_table = 'civicrm_contact'; + + if (!$cacheKey) { + return; + } + $params = array(); + + $entity_whereClause = " AND entity_table = '{$entity_table}'"; + if ($cIds && $cacheKey && $action) { + if (is_array($cIds)) { + $cIdFilter = "(" . implode(',', $cIds) . ")"; + $whereClause = " +WHERE cacheKey LIKE %1 +AND (entity_id1 IN {$cIdFilter} OR entity_id2 IN {$cIdFilter}) +"; + } + else { + $whereClause = " +WHERE cacheKey LIKE %1 +AND (entity_id1 = %2 OR entity_id2 = %2) +"; + $params[2] = array("{$cIds}", 'Integer'); + } + if ($action == 'select') { + $whereClause .= "AND is_selected = 0"; + $sql = "UPDATE civicrm_prevnext_cache SET is_selected = 1 {$whereClause} {$entity_whereClause}"; + $params[1] = array("{$cacheKey}%", 'String'); + } + elseif ($action == 'unselect') { + $whereClause .= "AND is_selected = 1"; + $sql = "UPDATE civicrm_prevnext_cache SET is_selected = 0 {$whereClause} {$entity_whereClause}"; + $params[1] = array("%{$cacheKey}%", 'String'); + } + // default action is reseting + } + elseif (!$cIds && $cacheKey && $action == 'unselect') { + $sql = " +UPDATE civicrm_prevnext_cache +SET is_selected = 0 +WHERE cacheKey LIKE %1 AND is_selected = 1 + {$entity_whereClause} +"; + $params[1] = array("{$cacheKey}%", 'String'); + } + CRM_Core_DAO::executeQuery($sql, $params); + } + + /** + * Get the selections. + * + * @param string $cacheKey + * Cache key. + * @param string $action + * One of the following: + * - 'get' - get only selection records + * - 'getall' - get all the records of the specified cache key + * + * @return array|NULL + */ + public function getSelection($cacheKey, $action = 'get') { + $entity_table = 'civicrm_contact'; + + if (!$cacheKey) { + return NULL; + } + $params = array(); + + $entity_whereClause = " AND entity_table = '{$entity_table}'"; + if ($cacheKey && ($action == 'get' || $action == 'getall')) { + $actionGet = ($action == "get") ? " AND is_selected = 1 " : ""; + $sql = " +SELECT entity_id1, entity_id2 FROM civicrm_prevnext_cache +WHERE cacheKey LIKE %1 + $actionGet + $entity_whereClause +ORDER BY id +"; + $params[1] = array("{$cacheKey}%", 'String'); + + $contactIds = array($cacheKey => array()); + $cIdDao = CRM_Core_DAO::executeQuery($sql, $params); + while ($cIdDao->fetch()) { + if ($cIdDao->entity_id1 == $cIdDao->entity_id2) { + $contactIds[$cacheKey][$cIdDao->entity_id1] = 1; + } + } + return $contactIds; + } + } + + /** + * Get the previous and next keys. + * + * @param string $cacheKey + * @param int $id1 + * @param int $id2 + * + * NOTE: I don't really get why there are two ID columns, but we'll + * keep passing them through as a matter of safe-refactoring. + * + * @return array + */ + public function getPositions($cacheKey, $id1, $id2) { + return CRM_Core_BAO_PrevNextCache::getPositions($cacheKey, $id1, $id2); + } + + /** + * Delete an item from the prevnext cache table based on the entity. + * + * @param int $id + * @param string $cacheKey + * @param string $entityTable + */ + public function deleteItem($id = NULL, $cacheKey = NULL, $entityTable = 'civicrm_contact') { + CRM_Core_BAO_PrevNextCache::deleteItem($id, $cacheKey, $entityTable); + } + + /** + * Get count of matching rows. + * + * @param string $cacheKey + * @return int + */ + public function getCount($cacheKey) { + return CRM_Core_BAO_PrevNextCache::getCount($cacheKey, NULL, "entity_table = 'civicrm_contact'"); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/PseudoConstant.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/PseudoConstant.php index 81bdb6f232e..5cef5e081eb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/PseudoConstant.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/PseudoConstant.php @@ -170,7 +170,7 @@ class CRM_Core_PseudoConstant { /** * Low-level option getter, rarely accessed directly. * NOTE: Rather than calling this function directly use CRM_*_BAO_*::buildOptions() - * @see http://wiki.civicrm.org/confluence/display/CRMDOC/Pseudoconstant+%28option+list%29+Reference + * @see https://docs.civicrm.org/dev/en/latest/framework/pseudoconstant/ * * NOTE: If someone undertakes a refactoring of this, please consider the use-case of * the Setting.getoptions API. There is no DAO/field, but it would be nice to use the @@ -536,7 +536,7 @@ public static function populate( $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/SelectValues.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/SelectValues.php index 6f4ed3c8b41..3d429fa36e6 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/SelectValues.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/SelectValues.php @@ -1096,4 +1096,17 @@ public static function activityTextOptions() { ); } + /** + * Relationship permissions + * + * @return array + */ + public static function getPermissionedRelationshipOptions() { + return array( + CRM_Contact_BAO_Relationship::NONE => ts('None'), + CRM_Contact_BAO_Relationship::VIEW => ts('View only'), + CRM_Contact_BAO_Relationship::EDIT => ts('View and update'), + ); + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Session.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Session.php index 7c1c1a42a1a..632a9e14ab9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Session.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Session.php @@ -286,7 +286,7 @@ public function getVars(&$vars, $prefix = '') { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Smarty/plugins/function.help.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Smarty/plugins/function.help.php index c4e4836c981..f768965f61a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Smarty/plugins/function.help.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Core/Smarty/plugins/function.help.php @@ -92,5 +92,5 @@ function smarty_function_help($params, &$smarty) { foreach ($params as &$param) { $param = is_bool($param) || is_numeric($param) ? (int) $param : (string) $param; } - return ' '; + return ' '; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Field.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Field.php index 5326a2ff108..518bd0d7b4e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Field.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Field.php @@ -315,24 +315,33 @@ public function buildQuickForm() { $this->add('checkbox', 'in_selector', ts('Display in Table?')); } + $optionGroupParams = [ + 'is_reserved' => 0, + 'is_active' => 1, + 'options' => ['limit' => 0, 'sort' => "title ASC"], + 'return' => ['title'], + ]; + if ($this->_action == CRM_Core_Action::UPDATE) { $this->freeze('data_type'); + if (!empty($this->_values['option_group_id'])) { + // Before dev/core#155 we didn't set the is_reserved flag properly, which should be handled by the upgrade script... + // but it is still possible that existing installs may have optiongroups linked to custom fields that are marked reserved. + $optionGroupParams['id'] = $this->_values['option_group_id']; + $optionGroupParams['options']['or'] = [["is_reserved", "id"]]; + } } - $includeFieldIds = NULL; - if ($this->_action == CRM_Core_Action::UPDATE) { - $includeFieldIds = $this->_values['id']; - } - $optionGroups = CRM_Core_BAO_CustomField::customOptionGroup($includeFieldIds); - $emptyOptGroup = FALSE; - if (empty($optionGroups)) { - $emptyOptGroup = TRUE; - $optionTypes = array('1' => ts('Create a new set of options')); - } - else { - $optionTypes = array( - '1' => ts('Create a new set of options'), - '2' => ts('Reuse an existing set'), - ); + + // Retrieve optiongroups for selection list + $optionGroupMetadata = civicrm_api3('OptionGroup', 'get', $optionGroupParams); + + // OptionGroup selection + $optionTypes = array('1' => ts('Create a new set of options')); + + if (!empty($optionGroupMetadata['values'])) { + $emptyOptGroup = FALSE; + $optionGroups = CRM_Utils_Array::collect('title', $optionGroupMetadata['values']); + $optionTypes['2'] = ts('Reuse an existing set'); $this->add('select', 'option_group_id', @@ -342,6 +351,10 @@ public function buildQuickForm() { ) + $optionGroups ); } + else { + // No custom (non-reserved) option groups + $emptyOptGroup = TRUE; + } $element = &$this->addRadio('option_type', ts('Option Type'), @@ -350,6 +363,10 @@ public function buildQuickForm() { 'onclick' => "showOptionSelect();", ), '
' ); + // if empty option group freeze the option type. + if ($emptyOptGroup) { + $element->freeze(); + } $contactGroups = CRM_Core_PseudoConstant::group(); asort($contactGroups); @@ -370,11 +387,6 @@ public function buildQuickForm() { $this->add('hidden', 'filter_selected', 'Group', array('id' => 'filter_selected')); - //if empty option group freeze the option type. - if ($emptyOptGroup) { - $element->freeze(); - } - // form fields of Custom Option rows $defaultOption = array(); $_showHide = new CRM_Core_ShowHideBlocks('', ''); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Group.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Group.php index 90b6947adb6..6139759f17e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Group.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Group.php @@ -115,11 +115,10 @@ public static function formRule($fields, $files, $self) { //validate group title as well as name. $title = $fields['title']; $name = CRM_Utils_String::munge($title, '_', 64); - $query = 'select count(*) from civicrm_custom_group where ( name like %1 OR title like %2 ) and id != %3'; + $query = 'select count(*) from civicrm_custom_group where ( name like %1) and id != %2'; $grpCnt = CRM_Core_DAO::singleValueQuery($query, array( 1 => array($name, 'String'), - 2 => array($title, 'String'), - 3 => array((int) $self->_id, 'Integer'), + 2 => array((int) $self->_id, 'Integer'), )); if ($grpCnt) { $errors['title'] = ts('Custom group \'%1\' already exists in Database.', array(1 => $title)); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Preview.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Preview.php index b068f147c18..5264207a103 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Preview.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Custom/Form/Preview.php @@ -109,7 +109,7 @@ public function setDefaultValues() { * @return void */ public function buildQuickForm() { - if (is_array($this->_groupTree[$this->_groupId])) { + if (is_array($this->_groupTree) && !empty($this->_groupTree[$this->_groupId])) { foreach ($this->_groupTree[$this->_groupId]['fields'] as & $field) { //add the form elements CRM_Core_BAO_CustomField::addQuickFormElement($this, $field['element_name'], $field['id'], CRM_Utils_Array::value('is_required', $field)); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Cxn/CiviCxnHttp.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Cxn/CiviCxnHttp.php index f2b2c6357c8..0d6024812cc 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Cxn/CiviCxnHttp.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Cxn/CiviCxnHttp.php @@ -54,7 +54,7 @@ public function send($verb, $url, $blob, $headers = array()) { $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 @@ public function send($verb, $url, $blob, $headers = array()) { 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 @@ protected function createStreamOpts($verb, $url, $blob, $headers) { return $result; } + /** + * @return \CRM_Utils_Cache_Interface|null + */ + public function getCache() { + return $this->cache; + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Dashlet/Page/Blog.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Dashlet/Page/Blog.php index 23cac0abf75..3416d186601 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Dashlet/Page/Blog.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Dashlet/Page/Blog.php @@ -73,27 +73,21 @@ public function run() { * @return array */ protected function getData() { - // Fetch data from cache - $cache = CRM_Core_DAO::executeQuery("SELECT data, created_date FROM civicrm_cache - WHERE group_name = 'dashboard' AND path = 'newsfeed'"); - if ($cache->fetch()) { - $expire = time() - (60 * 60 * 24 * self::CACHE_DAYS); - // Refresh data after CACHE_DAYS - if (strtotime($cache->created_date) < $expire) { - $new_data = $this->getFeeds(); - // If fetching the new rss feed was successful, return it - // Otherwise use the old cached data - it's better than nothing - if ($new_data) { - return $new_data; - } + $value = Civi::cache('community_messages')->get('dashboard_newsfeed'); + + if (!$value) { + $value = $this->getFeeds(); + + if ($value) { + Civi::cache('community_messages')->set('dashboard_newsfeed', $value, (60 * 60 * 24 * self::CACHE_DAYS)); } - return unserialize($cache->data); } - return $this->getFeeds(); + + return $value; } /** - * Fetch all feeds & cache results. + * Fetch all feeds. * * @return array */ @@ -104,7 +98,6 @@ protected function getFeeds() { return array(); } $feeds = $this->formatItems($newsFeed); - CRM_Core_BAO_Cache::setItem($feeds, 'dashboard', 'newsfeed'); return $feeds; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Dashlet/Page/GettingStarted.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Dashlet/Page/GettingStarted.php index cb77b45cff3..18e3e89d3c8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Dashlet/Page/GettingStarted.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Dashlet/Page/GettingStarted.php @@ -87,27 +87,21 @@ public function run() { * @return array */ private function _gettingStarted() { - // Fetch data from cache - $cache = CRM_Core_DAO::executeQuery("SELECT data, created_date FROM civicrm_cache - WHERE group_name = 'dashboard' AND path = 'gettingStarted'"); - if ($cache->fetch()) { - $expire = time() - (60 * 60 * 24 * self::CACHE_DAYS); - // Refresh data after CACHE_DAYS - if (strtotime($cache->created_date) < $expire) { - $new_data = $this->_getHtml($this->gettingStartedUrl()); - // If fetching the new html was successful, return it - // Otherwise use the old cached data - it's better than nothing - if ($new_data) { - return $new_data; - } + $value = Civi::cache('community_messages')->get('dashboard_gettingStarted'); + + if (!$value) { + $value = $this->_getHtml($this->gettingStartedUrl()); + + if ($value) { + Civi::cache('community_messages')->set('dashboard_gettingStarted', $value, (60 * 60 * 24 * self::CACHE_DAYS)); } - return unserialize($cache->data); } - return $this->_getHtml($this->gettingStartedUrl()); + + return $value; } /** - * Get html and cache results. + * Get html. * * @param $url * @@ -115,18 +109,15 @@ private function _gettingStarted() { * array of gettingStarted items; or NULL if not available */ public function _getHtml($url) { - $httpClient = new CRM_Utils_HttpClient(self::CHECK_TIMEOUT); list ($status, $html) = $httpClient->get($url); + if ($status !== CRM_Utils_HttpClient::STATUS_OK) { return NULL; } $tokensList = CRM_Utils_Token::getTokens($html); $this->replaceLinkToken($tokensList, $html); - if ($html) { - CRM_Core_BAO_Cache::setItem($html, 'dashboard', 'gettingStarted'); - } return $html; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Dedupe/Merger.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Dedupe/Merger.php index a2fe7e71874..afb53df5f54 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Dedupe/Merger.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Dedupe/Merger.php @@ -203,35 +203,23 @@ public static function getActiveRelTables($cid) { /** * 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 @@ public static function moveContactBelongings($mainId, $otherId, $tables = FALSE, // 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 @@ public static function getRowsElementsAndInfo($mainId, $otherId, $checkPermissio $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 @@ public static function addMembershipToRealtedContacts($contactID) { } } - /** - * 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 @@ protected static function dedupePair(&$migrationInfo, &$resultStats, &$deletedCo // 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Cart/DAO/Cart.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Cart/DAO/Cart.php index a5c2202e520..6bb14e6c2e9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Cart/DAO/Cart.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Cart/DAO/Cart.php @@ -104,6 +104,7 @@ public static function &fields() { 'name' => 'completed', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Complete?'), + 'default' => '0', 'table_name' => 'civicrm_event_carts', 'entity' => 'Cart', 'bao' => 'CRM_Event_Cart_BAO_Cart', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/DAO/Event.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/DAO/Event.php index caa4b866f95..1f13607d0cf 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/DAO/Event.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/DAO/Event.php @@ -609,6 +609,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Event Type'), 'description' => 'Event Type ID.Implicit FK to civicrm_option_value where option_group = event_type.', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -626,6 +627,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Participant Listing'), 'description' => 'Should we expose the participant list? Implicit FK to civicrm_option_value where option_group = participant_listing.', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -693,6 +695,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Online Registration'), 'description' => 'If true, include registration link on Event Info page.', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -776,6 +779,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is this a PAID event?'), 'description' => 'If true, one or more fee amounts must be set and a Payment Processor must be configured for Online Event Registration.', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -828,6 +832,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Map Enabled'), 'description' => 'Include a map block on the Event Information page when geocode info is available and a mapping provider has been specified?', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -841,6 +846,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Active'), 'description' => 'Is this Event enabled or disabled/cancelled?', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -997,6 +1003,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is confirm email'), 'description' => 'If true, confirmation is automatically emailed to contact on successful registration.', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1151,6 +1158,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Pay Later Allowed'), 'description' => 'if true - allows the user to send payment directly to the org later', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1190,6 +1198,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Partial Payments Enabled'), 'description' => 'is partial payment enabled for this event', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1248,6 +1257,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Allow Multiple Registrations'), 'description' => 'if true - allows the user to register multiple participants for event', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1261,6 +1271,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Maximum number of additional participants per registration'), 'description' => 'Maximum number of additional participants that can be registered on a single booking', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1271,6 +1282,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Does Event allow multiple registrations from same email address?'), 'description' => 'if true - allows the user to register multiple registrations from same email address.', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1323,6 +1335,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Allow Self-service Cancellation or Transfer'), 'description' => 'Allow self service cancellation or transfer for event?', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1336,6 +1349,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Self-service Cancellation or Transfer Time'), 'description' => 'Number of hours prior to event start date to allow self-service cancellation or transfer.', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1380,6 +1394,7 @@ public static function &fields() { 'title' => ts('Is an Event Template'), 'description' => 'whether the event has template', 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', @@ -1556,6 +1571,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is billing block required'), 'description' => 'if true than billing block is required this event', + 'default' => '0', 'table_name' => 'civicrm_event', 'entity' => 'Event', 'bao' => 'CRM_Event_BAO_Event', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/DAO/Participant.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/DAO/Participant.php index a25c71bb825..941d0eff23d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/DAO/Participant.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/DAO/Participant.php @@ -364,6 +364,7 @@ public static function &fields() { 'headerPattern' => '', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_participant', 'entity' => 'Participant', 'bao' => 'CRM_Event_BAO_Participant', @@ -378,6 +379,7 @@ public static function &fields() { 'headerPattern' => '/(is.)?(pay(.)?later)$/i', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_participant', 'entity' => 'Participant', 'bao' => 'CRM_Event_BAO_Participant', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/ManageEvent/Fee.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/ManageEvent/Fee.php index aea14851200..cbd038756d7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/ManageEvent/Fee.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/ManageEvent/Fee.php @@ -522,6 +522,11 @@ public static function formRule($values) { $errors['pay_later_receipt'] = ts('Please enter the Pay Later instructions to be displayed to your users.'); } } + else { + if (empty($values['payment_processor'])) { + $errors['payment_processor'] = ts('You have indicated that this is a paid event, but no payment option has been selected. If this is not a paid event, please select the "No" option at the top of the page. If this is a paid event, please select at least one payment processor and/or enable the pay later option.'); + } + } } return empty($errors) ? TRUE : $errors; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/ManageEvent/Location.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/ManageEvent/Location.php index f7c11764dcd..d28b2187fd0 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/ManageEvent/Location.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/ManageEvent/Location.php @@ -225,10 +225,9 @@ public function postProcess() { CRM_Core_DAO::setFieldValue('CRM_Event_DAO_Event', $this->_id, 'loc_block_id', 'null' ); - - $this->_values['address'] = array(); } + $this->_values['address'] = array(); // if 'create new loc' optioin is selected OR selected new loc is different // from old one, go ahead and delete the old loc provided thats not being // used by any other event diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Registration.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Registration.php index 7c10574c604..e284d2daf1d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Registration.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Registration.php @@ -196,7 +196,7 @@ class CRM_Event_Form_Registration extends CRM_Core_Form { */ public function preProcess() { $this->_eventId = CRM_Utils_Request::retrieve('id', 'Positive', $this, TRUE); - $this->_action = CRM_Utils_Request::retrieve('action', 'String', $this, FALSE); + $this->_action = CRM_Utils_Request::retrieve('action', 'Alphanumeric', $this, FALSE, CRM_Core_Action::ADD); //CRM-4320 $this->_participantId = CRM_Utils_Request::retrieve('participantId', 'Positive', $this); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Registration/Confirm.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Registration/Confirm.php index da1a8ee522d..29b5c5a2795 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Registration/Confirm.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Registration/Confirm.php @@ -208,7 +208,8 @@ public function buildQuickForm() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Task.php index 263a644cba8..cc7df4d31d5 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() { /** * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Page/EventInfo.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Page/EventInfo.php index 7b088ea3b87..9f58bddb300 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Page/EventInfo.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Event/Page/EventInfo.php @@ -270,8 +270,9 @@ public function run() { ); $allowRegistration = FALSE; + $isEventOpenForRegistration = CRM_Event_BAO_Event::validRegistrationRequest($values['event'], $this->_id); if (!empty($values['event']['is_online_registration'])) { - if (CRM_Event_BAO_Event::validRegistrationRequest($values['event'], $this->_id)) { + if ($isEventOpenForRegistration == 1) { // we always generate urls for the front end in joomla $action_query = $action === CRM_Core_Action::PREVIEW ? "&action=$action" : ''; $url = CRM_Utils_System::url('civicrm/event/register', @@ -337,8 +338,9 @@ public function run() { $statusMessage = ts('Event is currently full, but you can register and be a part of waiting list.'); } } - - CRM_Core_Session::setStatus($statusMessage); + if ($isEventOpenForRegistration == 1) { + CRM_Core_Session::setStatus($statusMessage); + } } // we do not want to display recently viewed items, so turn off $this->assign('displayRecent', FALSE); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/BAO/Export.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/BAO/Export.php index b86bbe8bd83..f248f51a77b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/BAO/Export.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/BAO/Export.php @@ -42,48 +42,31 @@ class CRM_Export_BAO_Export { const EXPORT_ROW_COUNT = 100000; /** - * Get Querymode based on ExportMode + * Key representing the head of household in the relationship array. * - * @param int $exportMode - * Export mode. + * e.g. 8_a_b. * - * @return string $Querymode - * Query Mode + * @var string */ - public static function getQueryMode($exportMode) { - $queryMode = CRM_Contact_BAO_Query::MODE_CONTACTS; - - switch ($exportMode) { - case CRM_Export_Form_Select::CONTRIBUTE_EXPORT: - $queryMode = CRM_Contact_BAO_Query::MODE_CONTRIBUTE; - break; - - case CRM_Export_Form_Select::EVENT_EXPORT: - $queryMode = CRM_Contact_BAO_Query::MODE_EVENT; - break; + protected static $headOfHouseholdRelationshipKey; - case CRM_Export_Form_Select::MEMBER_EXPORT: - $queryMode = CRM_Contact_BAO_Query::MODE_MEMBER; - break; - - case CRM_Export_Form_Select::PLEDGE_EXPORT: - $queryMode = CRM_Contact_BAO_Query::MODE_PLEDGE; - break; - - case CRM_Export_Form_Select::CASE_EXPORT: - $queryMode = CRM_Contact_BAO_Query::MODE_CASE; - break; - - case CRM_Export_Form_Select::GRANT_EXPORT: - $queryMode = CRM_Contact_BAO_Query::MODE_GRANT; - break; + /** + * Key representing the head of household in the relationship array. + * + * e.g. 8_a_b. + * + * @var string + */ + protected static $memberOfHouseholdRelationshipKey; - case CRM_Export_Form_Select::ACTIVITY_EXPORT: - $queryMode = CRM_Contact_BAO_Query::MODE_ACTIVITY; - break; - } - return $queryMode; - } + /** + * Key representing the head of household in the relationship array. + * + * e.g. ['8_b_a' => 'Household Member Is', '8_a_b = 'Household Member Of'.....] + * + * @var + */ + protected static $relationshipTypes = []; /** * Get default return property for export based on mode @@ -211,53 +194,6 @@ public static function getGroupBy($exportMode, $queryMode, $returnProperties, $q return $groupBy; } - /** - * Define extra properties for the export based on query mode - * - * @param string $queryMode - * Query Mode - * @return array $extraProperties - * Extra Properties - */ - public static function defineExtraProperties($queryMode) { - switch ($queryMode) { - case CRM_Contact_BAO_Query::MODE_EVENT: - $paymentFields = TRUE; - $paymentTableId = 'participant_id'; - $extraReturnProperties = array(); - break; - - case CRM_Contact_BAO_Query::MODE_MEMBER: - $paymentFields = TRUE; - $paymentTableId = 'membership_id'; - $extraReturnProperties = array(); - break; - - case CRM_Contact_BAO_Query::MODE_PLEDGE: - $extraReturnProperties = CRM_Pledge_BAO_Query::extraReturnProperties($queryMode); - $paymentFields = TRUE; - $paymentTableId = 'pledge_payment_id'; - break; - - case CRM_Contact_BAO_Query::MODE_CASE: - $extraReturnProperties = CRM_Case_BAO_Query::extraReturnProperties($queryMode); - $paymentFields = FALSE; - $paymentTableId = ''; - break; - - default: - $paymentFields = FALSE; - $paymentTableId = ''; - $extraReturnProperties = array(); - } - $extraProperties = array( - 'paymentFields' => $paymentFields, - 'paymentTableId' => $paymentTableId, - 'extraReturnProperties' => $extraReturnProperties, - ); - return $extraProperties; - } - /** * Get the list the export fields. * @@ -308,109 +244,40 @@ public static function exportComponents( $queryOperator = 'AND' ) { + $processor = new CRM_Export_BAO_ExportProcessor($exportMode, $fields, $queryOperator); $returnProperties = array(); - $paymentFields = $selectedPaymentFields = FALSE; $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id'); // Warning - this imProviders var is used in a somewhat fragile way - don't rename it // without manually testing the export of IM provider still works. $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id'); - $contactRelationshipTypes = CRM_Contact_BAO_Relationship::getContactRelationshipType( - NULL, - NULL, - NULL, - NULL, - TRUE, - 'name', - FALSE - ); + self::$relationshipTypes = $processor->getRelationshipTypes(); + //also merge Head of Household + self::$memberOfHouseholdRelationshipKey = CRM_Utils_Array::key('Household Member of', self::$relationshipTypes); + self::$headOfHouseholdRelationshipKey = CRM_Utils_Array::key('Head of Household for', self::$relationshipTypes); - $queryMode = self::getQueryMode($exportMode); + $queryMode = $processor->getQueryMode(); if ($fields) { - //construct return properties - $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id'); - $locationTypeFields = array( - 'street_address', - 'supplemental_address_1', - 'supplemental_address_2', - 'supplemental_address_3', - 'city', - 'postal_code', - 'postal_code_suffix', - 'geo_code_1', - 'geo_code_2', - 'state_province', - 'country', - 'phone', - 'email', - 'im', - ); - 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 ($processor->isRelationshipTypeKey($fieldName) && (!empty($value[2]) || !empty($value[4]))) { + $returnProperties[$fieldName] = $processor->setRelationshipReturnProperties($value, $fieldName); } elseif (is_numeric(CRM_Utils_Array::value(2, $value))) { - $locTypeId = $value[2]; + $locationName = CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $value[2]); if ($fieldName == 'phone') { - $returnProperties['location'][$locationTypes[$locTypeId]]['phone-' . CRM_Utils_Array::value(3, $value)] = 1; + $returnProperties['location'][$locationName]['phone-' . CRM_Utils_Array::value(3, $value)] = 1; } elseif ($fieldName == 'im') { - $returnProperties['location'][$locationTypes[$locTypeId]]['im-' . CRM_Utils_Array::value(3, $value)] = 1; + $returnProperties['location'][$locationName]['im-' . CRM_Utils_Array::value(3, $value)] = 1; } else { - $returnProperties['location'][$locationTypes[$locTypeId]][$fieldName] = 1; + $returnProperties['location'][$locationName][$fieldName] = 1; } } else { @@ -419,14 +286,6 @@ public static function exportComponents( if ($fieldName == 'event_id') { $returnProperties['event_id'] = 1; } - elseif ( - $exportMode == CRM_Export_Form_Select::EVENT_EXPORT && - array_key_exists($fieldName, self::componentPaymentFields()) - ) { - $selectedPaymentFields = TRUE; - $paymentTableId = 'participant_id'; - $returnProperties[$fieldName] = 1; - } else { $returnProperties[$fieldName] = 1; } @@ -438,56 +297,11 @@ public static function exportComponents( } } else { - $primary = TRUE; - $fields = CRM_Contact_BAO_Contact::exportableFields('All', TRUE, TRUE); - foreach ($fields as $key => $var) { - if ($key && (substr($key, 0, 6) != 'custom')) { - //for CRM=952 - $returnProperties[$key] = 1; - } - } - - if ($primary) { - $returnProperties['location_type'] = 1; - $returnProperties['im_provider'] = 1; - $returnProperties['phone_type_id'] = 1; - $returnProperties['provider_id'] = 1; - $returnProperties['current_employer'] = 1; - } - - $extraProperties = self::defineExtraProperties($queryMode); - $paymentFields = $extraProperties['paymentFields']; - $extraReturnProperties = $extraProperties['extraReturnProperties']; - $paymentTableId = $extraProperties['paymentTableId']; - - if ($queryMode != CRM_Contact_BAO_Query::MODE_CONTACTS) { - $componentReturnProperties = CRM_Contact_BAO_Query::defaultReturnProperties($queryMode); - if ($queryMode == CRM_Contact_BAO_Query::MODE_CONTRIBUTE) { - // soft credit columns are not automatically populated, because contribution search doesn't require them by default - $componentReturnProperties = array_merge( - $componentReturnProperties, - CRM_Contribute_BAO_Query::softCreditReturnProperties(TRUE)); - } - $returnProperties = array_merge($returnProperties, $componentReturnProperties); - - if (!empty($extraReturnProperties)) { - $returnProperties = array_merge($returnProperties, $extraReturnProperties); - } - - // unset non exportable fields for components - $nonExpoFields = array( - 'groups', - 'tags', - 'notes', - 'contribution_status_id', - 'pledge_status_id', - 'pledge_payment_status_id', - ); - foreach ($nonExpoFields as $value) { - unset($returnProperties[$value]); - } - } + $returnProperties = $processor->getDefaultReturnProperties(); } + // @todo - we are working towards this being entirely a property of the processor + $processor->setReturnProperties($returnProperties); + $paymentTableId = $processor->getPaymentTableID(); if ($mergeSameAddress) { //make sure the addressee fields are selected @@ -550,113 +364,27 @@ public static function exportComponents( CRM_Contact_BAO_ProximityQuery::fixInputParams($params); } - $query = new CRM_Contact_BAO_Query($params, $returnProperties, NULL, - FALSE, FALSE, $queryMode, - FALSE, TRUE, TRUE, NULL, $queryOperator - ); - - //sort by state - //CRM-15301 - $query->_sort = $order; - list($select, $from, $where, $having) = $query->query(); + list($query, $select, $from, $where, $having) = $processor->runQuery($params, $order, $returnProperties); if ($mergeSameHousehold == 1) { if (empty($returnProperties['id'])) { $returnProperties['id'] = 1; } - //also merge Head of Household - $relationKeyMOH = CRM_Utils_Array::key('Household Member of', $contactRelationshipTypes); - $relationKeyHOH = CRM_Utils_Array::key('Head of Household for', $contactRelationshipTypes); - foreach ($returnProperties as $key => $value) { - if (!array_key_exists($key, $contactRelationshipTypes)) { - $returnProperties[$relationKeyMOH][$key] = $value; - $returnProperties[$relationKeyHOH][$key] = $value; + if (!$processor->isRelationshipTypeKey($key)) { + $returnProperties[self::$memberOfHouseholdRelationshipKey][$key] = $value; + $returnProperties[self::$headOfHouseholdRelationshipKey][$key] = $value; } } - unset($returnProperties[$relationKeyMOH]['location_type']); - unset($returnProperties[$relationKeyMOH]['im_provider']); - unset($returnProperties[$relationKeyHOH]['location_type']); - unset($returnProperties[$relationKeyHOH]['im_provider']); + unset($returnProperties[self::$memberOfHouseholdRelationshipKey]['location_type']); + unset($returnProperties[self::$memberOfHouseholdRelationshipKey]['im_provider']); + unset($returnProperties[self::$headOfHouseholdRelationshipKey]['location_type']); + unset($returnProperties[self::$headOfHouseholdRelationshipKey]['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, $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 @@ -724,14 +452,14 @@ public static function exportComponents( $addPaymentHeader = FALSE; $paymentDetails = array(); - if ($paymentFields || $selectedPaymentFields) { + if ($processor->isExportPaymentFields()) { // get payment related in for event and members $paymentDetails = CRM_Contribute_BAO_Contribution::getContributionDetails($exportMode, $ids); //get all payment headers. // If we haven't selected specific payment fields, load in all the // payment headers. - if (!$selectedPaymentFields) { + if (!$processor->isExportSpecifiedPaymentFields()) { $paymentHeaders = self::componentPaymentFields(); if (!empty($paymentDetails)) { $addPaymentHeader = TRUE; @@ -759,7 +487,7 @@ public static function exportComponents( // for CRM-3157 purposes $i18n = CRM_Core_I18n::singleton(); - list($outputColumns, $headerRows, $sqlColumns, $metadata) = self::getExportStructureArrays($returnProperties, $query, $contactRelationshipTypes, $relationQuery, $selectedPaymentFields); + list($outputColumns, $headerRows, $sqlColumns, $metadata) = self::getExportStructureArrays($returnProperties, $processor); $limitReached = FALSE; while (!$limitReached) { @@ -804,112 +532,23 @@ public static function exportComponents( } } - if ($field == 'id') { - $row[$field] = $iterationDAO->contact_id; - // special case for calculated field - } - elseif ($field == 'source_contact_id') { - $row[$field] = $iterationDAO->contact_id; - } - elseif ($field == 'pledge_balance_amount') { - $row[$field] = $iterationDAO->pledge_amount - $iterationDAO->pledge_total_paid; - // special case for calculated field - } - elseif ($field == 'pledge_next_pay_amount') { - $row[$field] = $iterationDAO->pledge_next_pay_amount + $iterationDAO->pledge_outstanding_amount; - } - elseif (array_key_exists($field, $contactRelationshipTypes)) { + if ($processor->isRelationshipTypeKey($field)) { $relDAO = CRM_Utils_Array::value($iterationDAO->contact_id, $allRelContactArray[$field]); $relationQuery[$field]->convertToPseudoNames($relDAO); self::fetchRelationshipDetails($relDAO, $value, $field, $row); } - elseif (isset($fieldValue) && - $fieldValue != '' - ) { - //check for custom data - if ($cfID = CRM_Core_BAO_CustomField::getKeyID($field)) { - $row[$field] = CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID); - } - - elseif (in_array($field, array( - 'email_greeting', - 'postal_greeting', - 'addressee', - ))) { - //special case for greeting replacement - $fldValue = "{$field}_display"; - $row[$field] = $iterationDAO->$fldValue; - } - else { - //normal fields with a touch of CRM-3157 - switch ($field) { - case 'country': - case 'world_region': - $row[$field] = $i18n->crm_translate($fieldValue, array('context' => 'country')); - break; - - case 'state_province': - $row[$field] = $i18n->crm_translate($fieldValue, array('context' => 'province')); - break; - - case 'gender': - case 'preferred_communication_method': - case 'preferred_mail_format': - case 'communication_style': - $row[$field] = $i18n->crm_translate($fieldValue); - break; - - default: - if (isset($metadata[$field])) { - // No I don't know why we do it this way & whether we could - // make better use of pseudoConstants. - if (!empty($metadata[$field]['context'])) { - $row[$field] = $i18n->crm_translate($fieldValue, $metadata[$field]); - break; - } - if (!empty($metadata[$field]['pseudoconstant'])) { - // This is not our normal syntax for pseudoconstants but I am a bit loath to - // call an external function until sure it is not increasing php processing given this - // may be iterated 100,000 times & we already have the $imProvider var loaded. - // That can be next refactor... - // Yes - definitely feeling hatred for this bit of code - I know you will beat me up over it's awfulness - // but I have to reach a stable point.... - $varName = $metadata[$field]['pseudoconstant']['var']; - $labels = $$varName; - $row[$field] = $labels[$fieldValue]; - break; - } - - } - $row[$field] = $fieldValue; - break; - } - } - } - elseif ($selectedPaymentFields && array_key_exists($field, self::componentPaymentFields())) { - $paymentData = CRM_Utils_Array::value($iterationDAO->$paymentTableId, $paymentDetails); - $payFieldMapper = array( - 'componentPaymentField_total_amount' => 'total_amount', - 'componentPaymentField_contribution_status' => 'contribution_status', - 'componentPaymentField_payment_instrument' => 'pay_instru', - 'componentPaymentField_transaction_id' => 'trxn_id', - 'componentPaymentField_received_date' => 'receive_date', - ); - $row[$field] = CRM_Utils_Array::value($payFieldMapper[$field], $paymentData, ''); - } else { - // if field is empty or null - $row[$field] = ''; + $row[$field] = self::getTransformedFieldValue($field, $iterationDAO, $fieldValue, $i18n, $metadata, $paymentDetails, $processor); } } // add payment headers if required - if ($addPaymentHeader && $paymentFields) { + if ($addPaymentHeader && $processor->isExportPaymentFields()) { // @todo rather than do this for every single row do it before the loop starts. // where other header definitions take place. $headerRows = array_merge($headerRows, $paymentHeaders); foreach (array_keys($paymentHeaders) as $paymentHdr) { - self::sqlColumnDefn($query, $sqlColumns, $paymentHdr); + self::sqlColumnDefn($processor, $sqlColumns, $paymentHdr); } } @@ -924,8 +563,8 @@ public static function exportComponents( // data will already be in $row. Otherwise, add payment related // information, if appropriate. if ($addPaymentHeader) { - if (!$selectedPaymentFields) { - if ($paymentFields) { + if (!$processor->isExportSpecifiedPaymentFields()) { + if ($processor->isExportPaymentFields()) { $paymentData = CRM_Utils_Array::value($row[$paymentTableId], $paymentDetails); if (!is_array($paymentData) || empty($paymentData)) { $paymentData = $nullContributionDetails; @@ -978,8 +617,8 @@ public static function exportComponents( // merge the records if they have corresponding households if ($mergeSameHousehold) { - self::mergeSameHousehold($exportTempTable, $headerRows, $sqlColumns, $relationKeyMOH); - self::mergeSameHousehold($exportTempTable, $headerRows, $sqlColumns, $relationKeyHOH); + self::mergeSameHousehold($exportTempTable, $headerRows, $sqlColumns, self::$memberOfHouseholdRelationshipKey); + self::mergeSameHousehold($exportTempTable, $headerRows, $sqlColumns, self::$headOfHouseholdRelationshipKey); } // call export hook @@ -992,7 +631,7 @@ public static function exportComponents( } else { // return tableName and sqlColumns in test context - return array($exportTempTable, $sqlColumns); + return array($exportTempTable, $sqlColumns, $headerRows); } // delete the export temp table and component table @@ -1137,127 +776,16 @@ public static function exportCustom($customSearchClass, $formValues, $order) { } /** - * @param $query + * @param \CRM_Export_BAO_ExportProcessor $processor * @param $sqlColumns * @param $field */ - public static function sqlColumnDefn($query, &$sqlColumns, $field) { + public static function sqlColumnDefn($processor, &$sqlColumns, $field) { if (substr($field, -4) == '_a_b' || substr($field, -4) == '_b_a') { return; } - $fieldName = CRM_Utils_String::munge(strtolower($field), '_', 64); - if ($fieldName == 'id') { - $fieldName = 'civicrm_primary_id'; - } - - // early exit for master_id, CRM-12100 - // in the DB it is an ID, but in the export, we retrive the display_name of the master record - // also for current_employer, CRM-16939 - if ($fieldName == 'master_id' || $fieldName == 'current_employer') { - $sqlColumns[$fieldName] = "$fieldName varchar(128)"; - return; - } - - if (substr($fieldName, -11) == 'campaign_id') { - // CRM-14398 - $sqlColumns[$fieldName] = "$fieldName varchar(128)"; - return; - } - - $lookUp = array('prefix_id', 'suffix_id'); - // set the sql columns - if (isset($query->_fields[$field]['type'])) { - switch ($query->_fields[$field]['type']) { - case CRM_Utils_Type::T_INT: - case CRM_Utils_Type::T_BOOLEAN: - if (in_array($field, $lookUp)) { - $sqlColumns[$fieldName] = "$fieldName varchar(255)"; - } - else { - $sqlColumns[$fieldName] = "$fieldName varchar(16)"; - } - break; - - case CRM_Utils_Type::T_STRING: - if (isset($query->_fields[$field]['maxlength'])) { - $sqlColumns[$fieldName] = "$fieldName varchar({$query->_fields[$field]['maxlength']})"; - } - else { - $sqlColumns[$fieldName] = "$fieldName varchar(255)"; - } - break; - - case CRM_Utils_Type::T_TEXT: - case CRM_Utils_Type::T_LONGTEXT: - case CRM_Utils_Type::T_BLOB: - case CRM_Utils_Type::T_MEDIUMBLOB: - $sqlColumns[$fieldName] = "$fieldName longtext"; - break; - - case CRM_Utils_Type::T_FLOAT: - case CRM_Utils_Type::T_ENUM: - case CRM_Utils_Type::T_DATE: - case CRM_Utils_Type::T_TIME: - case CRM_Utils_Type::T_TIMESTAMP: - case CRM_Utils_Type::T_MONEY: - case CRM_Utils_Type::T_EMAIL: - case CRM_Utils_Type::T_URL: - case CRM_Utils_Type::T_CCNUM: - default: - $sqlColumns[$fieldName] = "$fieldName varchar(32)"; - break; - } - } - else { - if (substr($fieldName, -3, 3) == '_id') { - $sqlColumns[$fieldName] = "$fieldName varchar(255)"; - } - elseif (substr($fieldName, -5, 5) == '_note') { - $sqlColumns[$fieldName] = "$fieldName text"; - } - else { - $changeFields = array( - 'groups', - 'tags', - 'notes', - ); - - if (in_array($fieldName, $changeFields)) { - $sqlColumns[$fieldName] = "$fieldName text"; - } - else { - // set the sql columns for custom data - if (isset($query->_fields[$field]['data_type'])) { - - switch ($query->_fields[$field]['data_type']) { - case 'String': - // May be option labels, which could be up to 512 characters - $length = max(512, CRM_Utils_Array::value('text_length', $query->_fields[$field])); - $sqlColumns[$fieldName] = "$fieldName varchar($length)"; - break; - - case 'Country': - case 'StateProvince': - case 'Link': - $sqlColumns[$fieldName] = "$fieldName varchar(255)"; - break; - - case 'Memo': - $sqlColumns[$fieldName] = "$fieldName text"; - break; - - default: - $sqlColumns[$fieldName] = "$fieldName varchar(255)"; - break; - } - } - else { - $sqlColumns[$fieldName] = "$fieldName text"; - } - } - } - } + $sqlColumns[$processor->getMungedFieldName($field)] = $processor->getSqlColumnDefinition($field); } /** @@ -1265,7 +793,7 @@ public static function sqlColumnDefn($query, &$sqlColumns, $field) { * @param $details * @param $sqlColumns */ - public static function writeDetailsToTable($tableName, &$details, &$sqlColumns) { + public static function writeDetailsToTable($tableName, $details, $sqlColumns) { if (empty($details)) { return; } @@ -1282,10 +810,10 @@ public static function writeDetailsToTable($tableName, &$details, &$sqlColumns) $sqlClause = array(); - foreach ($details as $dontCare => $row) { + foreach ($details as $row) { $id++; $valueString = array($id); - foreach ($row as $dontCare => $value) { + foreach ($row as $value) { if (empty($value)) { $valueString[] = "''"; } @@ -1312,9 +840,9 @@ public static function writeDetailsToTable($tableName, &$details, &$sqlColumns) * * @return string */ - public static function createTempTable(&$sqlColumns) { + 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}"; @@ -1780,12 +1308,11 @@ public static function writeCSVFromTable($exportTempTable, $headerRows, $sqlColu * Manipulate header rows for relationship fields. * * @param $headerRows - * @param $contactRelationshipTypes */ - public static function manipulateHeaderRows(&$headerRows, $contactRelationshipTypes) { + public static function manipulateHeaderRows(&$headerRows) { foreach ($headerRows as & $header) { $split = explode('-', $header); - if ($relationTypeName = CRM_Utils_Array::value($split[0], $contactRelationshipTypes)) { + if ($relationTypeName = CRM_Utils_Array::value($split[0], self::$relationshipTypes)) { $split[0] = $relationTypeName; $header = implode('-', $split); } @@ -1873,41 +1400,32 @@ public static function componentPaymentFields() { * @param array $headerRows * @param array $sqlColumns * Columns to go in the temp table. - * @param CRM_Contact_BAO_Query $query + * @param \CRM_Export_BAO_ExportProcessor $processor * @param array|string $value * @param array $phoneTypes * @param array $imProviders - * @param array $contactRelationshipTypes - * @param string $relationQuery - * @param array $selectedPaymentFields + * * @return array */ - public static function setHeaderRows($field, $headerRows, $sqlColumns, $query, $value, $phoneTypes, $imProviders, $contactRelationshipTypes, $relationQuery, $selectedPaymentFields) { + public static function setHeaderRows($field, $headerRows, $sqlColumns, $processor, $value, $phoneTypes, $imProviders) { - // Split campaign into 2 fields for id and title - if (substr($field, -14) == 'campaign_title') { - $headerRows[] = ts('Campaign Title'); - } - elseif (substr($field, -11) == 'campaign_id') { + $queryFields = $processor->getQueryFields(); + if (substr($field, -11) == 'campaign_id') { + // @todo - set this correctly in the xml rather than here. $headerRows[] = ts('Campaign ID'); } - elseif (isset($query->_fields[$field]['title'])) { - $headerRows[] = $query->_fields[$field]['title']; - } - elseif ($field == 'phone_type_id') { - $headerRows[] = ts('Phone Type'); + elseif (isset($queryFields[$field]['title'])) { + $headerRows[] = $queryFields[$field]['title']; } elseif ($field == 'provider_id') { + // @todo - set this correctly in the xml rather than here. $headerRows[] = ts('IM Service Provider'); } - elseif (substr($field, 0, 5) == 'case_' && $query->_fields['case'][$field]['title']) { - $headerRows[] = $query->_fields['case'][$field]['title']; - } - elseif (array_key_exists($field, $contactRelationshipTypes)) { + elseif ($processor->isRelationshipTypeKey($field)) { foreach ($value as $relationField => $relationValue) { // below block is same as primary block (duplicate) - if (isset($relationQuery[$field]->_fields[$relationField]['title'])) { - if ($relationQuery[$field]->_fields[$relationField]['name'] == 'name') { + if (isset($queryFields[$relationField]['title'])) { + if ($queryFields[$relationField]['name'] == 'name') { $headerName = $field . '-' . $relationField; } else { @@ -1915,28 +1433,28 @@ public static function setHeaderRows($field, $headerRows, $sqlColumns, $query, $ $headerName = $field . '-' . 'current_employer'; } else { - $headerName = $field . '-' . $relationQuery[$field]->_fields[$relationField]['name']; + $headerName = $field . '-' . $queryFields[$relationField]['name']; } } $headerRows[] = $headerName; - self::sqlColumnDefn($query, $sqlColumns, $headerName); + self::sqlColumnDefn($processor, $sqlColumns, $headerName); } elseif ($relationField == 'phone_type_id') { $headerName = $field . '-' . 'Phone Type'; $headerRows[] = $headerName; - self::sqlColumnDefn($query, $sqlColumns, $headerName); + self::sqlColumnDefn($processor, $sqlColumns, $headerName); } elseif ($relationField == 'provider_id') { $headerName = $field . '-' . 'Im Service Provider'; $headerRows[] = $headerName; - self::sqlColumnDefn($query, $sqlColumns, $headerName); + self::sqlColumnDefn($processor, $sqlColumns, $headerName); } elseif ($relationField == 'state_province_id') { $headerName = $field . '-' . 'state_province_id'; $headerRows[] = $headerName; - self::sqlColumnDefn($query, $sqlColumns, $headerName); + self::sqlColumnDefn($processor, $sqlColumns, $headerName); } elseif (is_array($relationValue) && $relationField == 'location') { // fix header for location type case @@ -1944,7 +1462,7 @@ public static function setHeaderRows($field, $headerRows, $sqlColumns, $query, $ foreach (array_keys($val) as $fld) { $type = explode('-', $fld); - $hdr = "{$ltype}-" . $relationQuery[$field]->_fields[$type[0]]['title']; + $hdr = "{$ltype}-" . $queryFields[$type[0]]['title']; if (!empty($type[1])) { if (CRM_Utils_Array::value(0, $type) == 'phone') { @@ -1956,21 +1474,21 @@ public static function setHeaderRows($field, $headerRows, $sqlColumns, $query, $ } $headerName = $field . '-' . $hdr; $headerRows[] = $headerName; - self::sqlColumnDefn($query, $sqlColumns, $headerName); + self::sqlColumnDefn($processor, $sqlColumns, $headerName); } } } } - self::manipulateHeaderRows($headerRows, $contactRelationshipTypes); + self::manipulateHeaderRows($headerRows); } - elseif ($selectedPaymentFields && array_key_exists($field, self::componentPaymentFields())) { + elseif ($processor->isExportPaymentFields() && array_key_exists($field, self::componentPaymentFields())) { $headerRows[] = CRM_Utils_Array::value($field, self::componentPaymentFields()); } else { $headerRows[] = $field; } - self::sqlColumnDefn($query, $sqlColumns, $field); + self::sqlColumnDefn($processor, $sqlColumns, $field); return array($headerRows, $sqlColumns); } @@ -1985,10 +1503,8 @@ public static function setHeaderRows($field, $headerRows, $sqlColumns, $query, $ * as a step on the refactoring path rather than how it should be. * * @param array $returnProperties - * @param CRM_Contact_BAO_Contact $query - * @param array $contactRelationshipTypes - * @param string $relationQuery - * @param array $selectedPaymentFields + * @param \CRM_Export_BAO_ExportProcessor $processor + * * @return array * - outputColumns Array of columns to be exported. The values don't matter but the key must match the * alias for the field generated by BAO_Query object. @@ -2005,15 +1521,16 @@ public static function setHeaderRows($field, $headerRows, $sqlColumns, $query, $ * - b) this code is old & outdated. Submit your answers to circular bin or better * yet find a way to comment them for posterity. */ - public static function getExportStructureArrays($returnProperties, $query, $contactRelationshipTypes, $relationQuery, $selectedPaymentFields) { + public static function getExportStructureArrays($returnProperties, $processor) { $metadata = $headerRows = $outputColumns = $sqlColumns = array(); $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id'); $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id'); + $queryFields = $processor->getQueryFields(); foreach ($returnProperties as $key => $value) { if ($key != 'location' || !is_array($value)) { $outputColumns[$key] = $value; - list($headerRows, $sqlColumns) = self::setHeaderRows($key, $headerRows, $sqlColumns, $query, $value, $phoneTypes, $imProviders, $contactRelationshipTypes, $relationQuery, $selectedPaymentFields); + list($headerRows, $sqlColumns) = self::setHeaderRows($key, $headerRows, $sqlColumns, $processor, $value, $phoneTypes, $imProviders); } else { foreach ($value as $locationType => $locationFields) { @@ -2021,7 +1538,7 @@ public static function getExportStructureArrays($returnProperties, $query, $cont $type = explode('-', $locationFieldName); $actualDBFieldName = $type[0]; - $outputFieldName = $locationType . '-' . $query->_fields[$actualDBFieldName]['title']; + $outputFieldName = $locationType . '-' . $queryFields[$actualDBFieldName]['title']; $daoFieldName = CRM_Utils_String::munge($locationType) . '-' . $actualDBFieldName; if (!empty($type[1])) { @@ -2037,8 +1554,8 @@ public static function getExportStructureArrays($returnProperties, $query, $cont // Warning: shame inducing hack. $metadata[$daoFieldName]['pseudoconstant']['var'] = 'imProviders'; } - self::sqlColumnDefn($query, $sqlColumns, $outputFieldName); - list($headerRows, $sqlColumns) = self::setHeaderRows($outputFieldName, $headerRows, $sqlColumns, $query, $value, $phoneTypes, $imProviders, $contactRelationshipTypes, $relationQuery, $selectedPaymentFields); + self::sqlColumnDefn($processor, $sqlColumns, $outputFieldName); + list($headerRows, $sqlColumns) = self::setHeaderRows($outputFieldName, $headerRows, $sqlColumns, $processor, $value, $phoneTypes, $imProviders); if ($actualDBFieldName == 'country' || $actualDBFieldName == 'world_region') { $metadata[$daoFieldName] = array('context' => 'country'); } @@ -2103,6 +1620,10 @@ private static function fetchRelationshipDetails($relDAO, $value, $field, &$row) } elseif (is_array($relationValue) && $relationField == 'location') { foreach ($relationValue as $ltype => $val) { + // If the location name has a space in it the we need to handle that. This + // is kinda hacky but specifically covered in the ExportTest so later efforts to + // improve it should be secure in the knowled it will be caught. + $ltype = str_replace(' ', '_', $ltype); foreach (array_keys($val) as $fld) { $type = explode('-', $fld); $fldValue = "{$ltype}-" . $type[0]; @@ -2167,4 +1688,211 @@ private static function fetchRelationshipDetails($relDAO, $value, $field, &$row) } } + /** + * 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 $returnProperties + * @param $queryMode + * @return array + */ + protected static function buildRelatedContactArray($selectAll, $ids, $exportMode, $componentTable, $returnProperties, $queryMode) { + $allRelContactArray = $relationQuery = array(); + + foreach (self::$relationshipTypes 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); + } + + /** + * @param $field + * @param $iterationDAO + * @param $fieldValue + * @param $i18n + * @param $metadata + * @param $paymentDetails + * + * @param \CRM_Export_BAO_ExportProcessor $processor + * + * @return string + */ + protected static function getTransformedFieldValue($field, $iterationDAO, $fieldValue, $i18n, $metadata, $paymentDetails, $processor) { + + if ($field == 'id') { + return $iterationDAO->contact_id; + // special case for calculated field + } + elseif ($field == 'source_contact_id') { + return $iterationDAO->contact_id; + } + elseif ($field == 'pledge_balance_amount') { + return $iterationDAO->pledge_amount - $iterationDAO->pledge_total_paid; + // special case for calculated field + } + elseif ($field == 'pledge_next_pay_amount') { + return $iterationDAO->pledge_next_pay_amount + $iterationDAO->pledge_outstanding_amount; + } + elseif (isset($fieldValue) && + $fieldValue != '' + ) { + //check for custom data + if ($cfID = CRM_Core_BAO_CustomField::getKeyID($field)) { + return CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID); + } + + elseif (in_array($field, array( + 'email_greeting', + 'postal_greeting', + 'addressee', + ))) { + //special case for greeting replacement + $fldValue = "{$field}_display"; + return $iterationDAO->$fldValue; + } + else { + //normal fields with a touch of CRM-3157 + switch ($field) { + case 'country': + case 'world_region': + return $i18n->crm_translate($fieldValue, array('context' => 'country')); + + case 'state_province': + return $i18n->crm_translate($fieldValue, array('context' => 'province')); + + case 'gender': + case 'preferred_communication_method': + case 'preferred_mail_format': + case 'communication_style': + return $i18n->crm_translate($fieldValue); + + default: + if (isset($metadata[$field])) { + // No I don't know why we do it this way & whether we could + // make better use of pseudoConstants. + if (!empty($metadata[$field]['context'])) { + return $i18n->crm_translate($fieldValue, $metadata[$field]); + } + if (!empty($metadata[$field]['pseudoconstant'])) { + // This is not our normal syntax for pseudoconstants but I am a bit loath to + // call an external function until sure it is not increasing php processing given this + // may be iterated 100,000 times & we already have the $imProvider var loaded. + // That can be next refactor... + // Yes - definitely feeling hatred for this bit of code - I know you will beat me up over it's awfulness + // but I have to reach a stable point.... + $varName = $metadata[$field]['pseudoconstant']['var']; + if ($varName === 'imProviders') { + return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_IM', 'provider_id', $fieldValue); + } + if ($varName === 'phoneTypes') { + return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_Phone', 'phone_type_id', $fieldValue); + } + } + + } + return $fieldValue; + } + } + } + elseif ($processor->isExportSpecifiedPaymentFields() && array_key_exists($field, self::componentPaymentFields())) { + $paymentTableId = $processor->getPaymentTableID(); + $paymentData = CRM_Utils_Array::value($iterationDAO->$paymentTableId, $paymentDetails); + $payFieldMapper = array( + 'componentPaymentField_total_amount' => 'total_amount', + 'componentPaymentField_contribution_status' => 'contribution_status', + 'componentPaymentField_payment_instrument' => 'pay_instru', + 'componentPaymentField_transaction_id' => 'trxn_id', + 'componentPaymentField_received_date' => 'receive_date', + ); + return CRM_Utils_Array::value($payFieldMapper[$field], $paymentData, ''); + } + else { + // if field is empty or null + return ''; + } + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/BAO/ExportProcessor.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/BAO/ExportProcessor.php new file mode 100644 index 00000000000..57494147043 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/BAO/ExportProcessor.php @@ -0,0 +1,633 @@ + 'Household Member Is', '8_a_b = 'Household Member Of'.....] + * + * @var + */ + protected $relationshipTypes = []; + + /** + * Array of properties to retrieve for relationships. + * + * @var array + */ + protected $relationshipReturnProperties = []; + + /** + * @var array + */ + protected $returnProperties = []; + + /** + * CRM_Export_BAO_ExportProcessor constructor. + * + * @param int $exportMode + * @param array|NULL $requestedFields + * @param string $queryOperator + */ + public function __construct($exportMode, $requestedFields, $queryOperator) { + $this->setExportMode($exportMode); + $this->setQueryMode(); + $this->setQueryOperator($queryOperator); + $this->setRequestedFields($requestedFields); + $this->setRelationshipTypes(); + } + + /** + * @return array|null + */ + public function getRequestedFields() { + return $this->requestedFields; + } + + /** + * @param array|null $requestedFields + */ + public function setRequestedFields($requestedFields) { + $this->requestedFields = $requestedFields; + } + + + /** + * @return array + */ + public function getReturnProperties() { + return $this->returnProperties; + } + + /** + * @param array $returnProperties + */ + public function setReturnProperties($returnProperties) { + $this->returnProperties = $returnProperties; + } + + /** + * @return array + */ + public function getRelationshipTypes() { + return $this->relationshipTypes; + } + + /** + */ + public function setRelationshipTypes() { + $this->relationshipTypes = CRM_Contact_BAO_Relationship::getContactRelationshipType( + NULL, + NULL, + NULL, + NULL, + TRUE, + 'name', + FALSE + ); + } + + + /** + * @param $fieldName + * @return bool + */ + public function isRelationshipTypeKey($fieldName) { + return array_key_exists($fieldName, $this->relationshipTypes); + } + + /** + * @return string + */ + public function getQueryOperator() { + return $this->queryOperator; + } + + /** + * @param string $queryOperator + */ + public function setQueryOperator($queryOperator) { + $this->queryOperator = $queryOperator; + } + + /** + * @return array + */ + public function getQueryFields() { + return $this->queryFields; + } + + /** + * @param array $queryFields + */ + public function setQueryFields($queryFields) { + $this->queryFields = $queryFields; + } + + /** + * @return int + */ + public function getQueryMode() { + return $this->queryMode; + } + + /** + * Set the query mode based on the export mode. + */ + public function setQueryMode() { + + switch ($this->getExportMode()) { + case CRM_Export_Form_Select::CONTRIBUTE_EXPORT: + $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTRIBUTE; + break; + + case CRM_Export_Form_Select::EVENT_EXPORT: + $this->queryMode = CRM_Contact_BAO_Query::MODE_EVENT; + break; + + case CRM_Export_Form_Select::MEMBER_EXPORT: + $this->queryMode = CRM_Contact_BAO_Query::MODE_MEMBER; + break; + + case CRM_Export_Form_Select::PLEDGE_EXPORT: + $this->queryMode = CRM_Contact_BAO_Query::MODE_PLEDGE; + break; + + case CRM_Export_Form_Select::CASE_EXPORT: + $this->queryMode = CRM_Contact_BAO_Query::MODE_CASE; + break; + + case CRM_Export_Form_Select::GRANT_EXPORT: + $this->queryMode = CRM_Contact_BAO_Query::MODE_GRANT; + break; + + case CRM_Export_Form_Select::ACTIVITY_EXPORT: + $this->queryMode = CRM_Contact_BAO_Query::MODE_ACTIVITY; + break; + + default: + $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTACTS; + } + } + + /** + * @return int + */ + public function getExportMode() { + return $this->exportMode; + } + + /** + * @param int $exportMode + */ + public function setExportMode($exportMode) { + $this->exportMode = $exportMode; + } + + /** + * @param $params + * @param $order + * @param $returnProperties + * @return array + */ + public function runQuery($params, $order, $returnProperties) { + $query = new CRM_Contact_BAO_Query($params, $returnProperties, NULL, + FALSE, FALSE, $this->getQueryMode(), + FALSE, TRUE, TRUE, NULL, $this->getQueryOperator() + ); + + //sort by state + //CRM-15301 + $query->_sort = $order; + list($select, $from, $where, $having) = $query->query(); + $this->setQueryFields($query->_fields); + return array($query, $select, $from, $where, $having); + } + + /** + * Get array of fields to return, over & above those defined in the main contact exportable fields. + * + * These include export mode specific fields & some fields apparently required as 'exportableFields' + * but not returned by the function of the same name. + * + * @return array + * Array of fields to return in the format ['field_name' => 1,...] + */ + public function getAdditionalReturnProperties() { + + $missing = [ + 'location_type', + 'im_provider', + 'phone_type_id', + 'provider_id', + 'current_employer', + ]; + if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) { + $componentSpecificFields = []; + } + else { + $componentSpecificFields = CRM_Contact_BAO_Query::defaultReturnProperties($this->getQueryMode()); + } + if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_PLEDGE) { + $componentSpecificFields = array_merge($componentSpecificFields, CRM_Pledge_BAO_Query::extraReturnProperties($this->getQueryMode())); + unset($componentSpecificFields['contribution_status_id']); + unset($componentSpecificFields['pledge_status_id']); + unset($componentSpecificFields['pledge_payment_status_id']); + } + if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CASE) { + $componentSpecificFields = array_merge($componentSpecificFields, CRM_Case_BAO_Query::extraReturnProperties($this->getQueryMode())); + } + if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTRIBUTE) { + $componentSpecificFields = array_merge($componentSpecificFields, CRM_Contribute_BAO_Query::softCreditReturnProperties(TRUE)); + unset($componentSpecificFields['contribution_status_id']); + } + return array_merge(array_fill_keys($missing, 1), $componentSpecificFields); + } + + /** + * Should payment fields be appended to the export. + * + * (This is pretty hacky so hopefully this function won't last long - notice + * how obviously it should be part of the above function!). + */ + public function isExportPaymentFields() { + if ($this->getRequestedFields() === NULL + && in_array($this->getQueryMode(), [ + CRM_Contact_BAO_Query::MODE_EVENT, + CRM_Contact_BAO_Query::MODE_MEMBER, + CRM_Contact_BAO_Query::MODE_PLEDGE, + ])) { + return TRUE; + } + elseif ($this->isExportSpecifiedPaymentFields()) { + return TRUE; + } + return FALSE; + } + + /** + * Has specific payment fields been requested (as opposed to via all fields). + * + * If specific fields have been requested then they get added at various points. + * + * @return bool + */ + public function isExportSpecifiedPaymentFields() { + if ($this->getRequestedFields() !== NULL && $this->hasRequestedComponentPaymentFields()) { + return TRUE; + } + } + + /** + * Get the name of the id field in the table that connects contributions to the export entity. + */ + public function getPaymentTableID() { + if ($this->getRequestedFields() === NULL) { + $mapping = [ + CRM_Contact_BAO_Query::MODE_EVENT => 'participant_id', + CRM_Contact_BAO_Query::MODE_MEMBER => 'membership_id', + CRM_Contact_BAO_Query::MODE_PLEDGE => 'pledge_payment_id', + ]; + return isset($mapping[$this->getQueryMode()]) ? $mapping[$this->getQueryMode()] : ''; + } + elseif ($this->hasRequestedComponentPaymentFields()) { + return 'participant_id'; + } + return FALSE; + } + + /** + * Have component payment fields been requested. + * + * @return bool + */ + protected function hasRequestedComponentPaymentFields() { + if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_EVENT) { + $participantPaymentFields = array_intersect_key($this->getComponentPaymentFields(), $this->getReturnProperties()); + if (!empty($participantPaymentFields)) { + return TRUE; + } + } + return FALSE; + } + + /** + * Get fields that indicate payment fields have been requested for a component. + * + * @return array + */ + protected function getComponentPaymentFields() { + return [ + 'componentPaymentField_total_amount' => ts('Total Amount'), + 'componentPaymentField_contribution_status' => ts('Contribution Status'), + 'componentPaymentField_received_date' => ts('Date Received'), + 'componentPaymentField_payment_instrument' => ts('Payment Method'), + 'componentPaymentField_transaction_id' => ts('Transaction ID'), + ]; + } + + /** + * Get the default properties when not specified. + * + * In the UI this appears as 'Primary fields only' but in practice it's + * most of the kitchen sink and the hallway closet thrown in. + * + * Since CRM-952 custom fields are excluded, but no other form of mercy is shown. + * + * @return array + */ + public function getDefaultReturnProperties() { + $returnProperties = []; + $fields = CRM_Contact_BAO_Contact::exportableFields('All', TRUE, TRUE); + $skippedFields = ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) ? [] : [ + 'groups', + 'tags', + 'notes' + ]; + + foreach ($fields as $key => $var) { + if ($key && (substr($key, 0, 6) != 'custom') && !in_array($key, $skippedFields)) { + $returnProperties[$key] = 1; + } + } + $returnProperties = array_merge($returnProperties, $this->getAdditionalReturnProperties()); + return $returnProperties; + } + + /** + * Add the field to relationship return properties & return it. + * + * This function is doing both setting & getting which is yuck but it is an interim + * refactor. + * + * @param array $value + * @param string $relationshipKey + * + * @return array + */ + public function setRelationshipReturnProperties($value, $relationshipKey) { + $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, $this->getValidLocationFields()) && is_numeric($relLocTypeId)) { + $locationName = CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $relLocTypeId); + if ($relPhoneTypeId) { + $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['phone-' . $relPhoneTypeId] = 1; + } + elseif ($relIMProviderId) { + $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['im-' . $relIMProviderId] = 1; + } + else { + $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName][$relationField] = 1; + } + } + else { + $this->relationshipReturnProperties[$relationshipKey][$relationField] = 1; + } + return $this->relationshipReturnProperties[$relationshipKey]; + } + + /** + * Get the default location fields to request. + * + * @return array + */ + public function getValidLocationFields() { + return [ + 'street_address', + 'supplemental_address_1', + 'supplemental_address_2', + 'supplemental_address_3', + 'city', + 'postal_code', + 'postal_code_suffix', + 'geo_code_1', + 'geo_code_2', + 'state_province', + 'country', + 'phone', + 'email', + 'im', + ]; + } + + /** + * Get the sql column definition for the given field. + * + * @param $field + * + * @return mixed + */ + public function getSqlColumnDefinition($field) { + $fieldName = $this->getMungedFieldName($field); + + // early exit for master_id, CRM-12100 + // in the DB it is an ID, but in the export, we retrive the display_name of the master record + // also for current_employer, CRM-16939 + if ($fieldName == 'master_id' || $fieldName == 'current_employer') { + return "$fieldName varchar(128)"; + } + + if (substr($fieldName, -11) == 'campaign_id') { + // CRM-14398 + return "$fieldName varchar(128)"; + } + + $queryFields = $this->getQueryFields(); + $lookUp = ['prefix_id', 'suffix_id']; + // set the sql columns + if (isset($queryFields[$field]['type'])) { + switch ($queryFields[$field]['type']) { + case CRM_Utils_Type::T_INT: + case CRM_Utils_Type::T_BOOLEAN: + if (in_array($field, $lookUp)) { + return "$fieldName varchar(255)"; + } + else { + return "$fieldName varchar(16)"; + } + + case CRM_Utils_Type::T_STRING: + if (isset($queryFields[$field]['maxlength'])) { + return "$fieldName varchar({$queryFields[$field]['maxlength']})"; + } + else { + return "$fieldName varchar(255)"; + } + + case CRM_Utils_Type::T_TEXT: + case CRM_Utils_Type::T_LONGTEXT: + case CRM_Utils_Type::T_BLOB: + case CRM_Utils_Type::T_MEDIUMBLOB: + return "$fieldName longtext"; + + case CRM_Utils_Type::T_FLOAT: + case CRM_Utils_Type::T_ENUM: + case CRM_Utils_Type::T_DATE: + case CRM_Utils_Type::T_TIME: + case CRM_Utils_Type::T_TIMESTAMP: + case CRM_Utils_Type::T_MONEY: + case CRM_Utils_Type::T_EMAIL: + case CRM_Utils_Type::T_URL: + case CRM_Utils_Type::T_CCNUM: + default: + return "$fieldName varchar(32)"; + } + } + else { + if (substr($fieldName, -3, 3) == '_id') { + return "$fieldName varchar(255)"; + } + elseif (substr($fieldName, -5, 5) == '_note') { + return "$fieldName text"; + } + else { + $changeFields = [ + 'groups', + 'tags', + 'notes', + ]; + + if (in_array($fieldName, $changeFields)) { + return "$fieldName text"; + } + else { + // set the sql columns for custom data + if (isset($queryFields[$field]['data_type'])) { + + switch ($queryFields[$field]['data_type']) { + case 'String': + // May be option labels, which could be up to 512 characters + $length = max(512, CRM_Utils_Array::value('text_length', $queryFields[$field])); + return "$fieldName varchar($length)"; + + case 'Country': + case 'StateProvince': + case 'Link': + return "$fieldName varchar(255)"; + + case 'Memo': + return "$fieldName text"; + + default: + return "$fieldName varchar(255)"; + } + } + else { + return "$fieldName text"; + } + } + } + } + } + + /** + * Get the munged field name. + * + * @param string $field + * @return string + */ + public function getMungedFieldName($field) { + $fieldName = CRM_Utils_String::munge(strtolower($field), '_', 64); + if ($fieldName == 'id') { + $fieldName = 'civicrm_primary_id'; + } + return $fieldName; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/Form/Select.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/Form/Select.php index 3e36b238ca9..af478d4adfa 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/Form/Select.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() { $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 @@ public function preProcess() { 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 @@ public function preProcess() { } } - $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 @@ public function buildQuickForm() { * @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 @@ static public function formRule($params, $files, $self) { /** * Process the uploaded file. * - * @return void + * @throws \CRM_Core_Exception */ public function postProcess() { $params = $this->controller->exportValues($this->_name); @@ -535,4 +520,13 @@ public static function getGreetingOptions() { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/Form/Select/Case.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/Form/Select/Case.php new file mode 100644 index 00000000000..c95aba984ae --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Export/Form/Select/Case.php @@ -0,0 +1,53 @@ +cache && !$fresh) { - $moduleExtensions = $this->cache->get($this->cacheKey . '/moduleFiles'); + $moduleExtensions = $this->cache->get($this->cacheKey . '_moduleFiles'); } if ($fresh == 'NOCACHE') { @@ -319,7 +319,7 @@ public function getActiveModuleFiles($fresh = FALSE) { } if ($this->cache) { - $this->cache->set($this->cacheKey . '/moduleFiles', $moduleExtensions); + $this->cache->set($this->cacheKey . '_moduleFiles', $moduleExtensions); } } return $moduleExtensions; @@ -465,7 +465,7 @@ public function refresh() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialAccount.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialAccount.php index 062eef27e1e..e171ee9a48b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialAccount.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialAccount.php @@ -32,11 +32,6 @@ */ class CRM_Financial_BAO_FinancialAccount extends CRM_Financial_DAO_FinancialAccount { - /** - * Static holder for the default LT. - */ - static $_defaultContributionType = NULL; - /** * Class constructor. */ diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialItem.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialItem.php index 63eee63f636..35e6b448d98 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialItem.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialItem.php @@ -47,7 +47,7 @@ public function __construct() { * @param array $defaults * (reference ) an assoc array to hold the flattened values. * - * @return CRM_Financial_BAO_FinancialItem + * @return CRM_Financial_DAO_FinancialItem */ public static function retrieve(&$params, &$defaults) { $financialItem = new CRM_Financial_DAO_FinancialItem(); @@ -189,9 +189,9 @@ public static function create(&$params, $ids = NULL, $trxnIds = NULL) { * Takes an associative array and creates a entity financial transaction object. * * @param array $params - * (reference ) an assoc array of name/value pairs. + * an assoc array of name/value pairs. * - * @return CRM_Core_BAO_FinancialTrxn + * @return CRM_Financial_DAO_EntityFinancialTrxn */ public static function createEntityTrxn($params) { $entity_trxn = new CRM_Financial_DAO_EntityFinancialTrxn(); @@ -204,9 +204,9 @@ public static function createEntityTrxn($params) { * Retrive entity financial trxn details. * * @param array $params - * (reference ) an assoc array of name/value pairs. + * an assoc array of name/value pairs. * @param bool $maxId - * To retrive max id. + * To retrieve max id. * * @return array */ @@ -247,7 +247,7 @@ public static function retrieveEntityFinancialTrxn($params, $maxId = FALSE) { * @param array $error * Error to display. * - * @return array + * @return array|bool */ public static function checkContactPresent($contactIds, &$error) { if (empty($contactIds)) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialType.php index 3f5f5efe8d7..901e7888ee4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/BAO/FinancialType.php @@ -32,10 +32,6 @@ */ class CRM_Financial_BAO_FinancialType extends CRM_Financial_DAO_FinancialType { - /** - * Static holder for the default LT. - */ - static $_defaultContributionType = NULL; /** * Static cache holder of available financial types for this session */ @@ -60,7 +56,7 @@ public function __construct() { * @param array $defaults * (reference ) an assoc array to hold the flattened values. * - * @return CRM_Contribute_BAO_ContributionType + * @return CRM_Financial_DAO_FinancialType */ public static function retrieve(&$params, &$defaults) { $financialType = new CRM_Financial_DAO_FinancialType(); @@ -80,8 +76,7 @@ public static function retrieve(&$params, &$defaults) { * @param bool $is_active * Value we want to set the is_active field. * - * @return Object - * DAO object on success, null otherwise + * @return bool */ public static function setIsActive($id, $is_active) { return CRM_Core_DAO::setFieldValue('CRM_Financial_DAO_FinancialType', $id, 'is_active', $is_active); @@ -139,7 +134,7 @@ public static function del($financialTypeId) { $financialType = new CRM_Financial_DAO_FinancialType(); $financialType->id = $financialTypeId; $financialType->find(TRUE); - // tables to ingore checks for financial_type_id + // tables to ignore checks for financial_type_id $ignoreTables = array('CRM_Financial_DAO_EntityFinancialAccount'); // TODO: if (!$financialType->find(true)) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialAccount.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialAccount.php index 4d7f073092b..f2ab3fff119 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialAccount.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialAccount.php @@ -279,6 +279,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Header Financial Account?'), 'description' => 'Is this a header account which does not allow transactions to be posted against it directly, but only to its sub-accounts?', + 'default' => '0', 'table_name' => 'civicrm_financial_account', 'entity' => 'FinancialAccount', 'bao' => 'CRM_Financial_BAO_FinancialAccount', @@ -300,6 +301,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Tax Financial Account?'), 'description' => 'Is this account for taxes?', + 'default' => '0', 'table_name' => 'civicrm_financial_account', 'entity' => 'FinancialAccount', 'bao' => 'CRM_Financial_BAO_FinancialAccount', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialItem.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialItem.php index 480b78afdca..7cfd9b651eb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialItem.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialItem.php @@ -207,6 +207,7 @@ public static function &fields() { 20, 2 ], + 'default' => '0', 'table_name' => 'civicrm_financial_item', 'entity' => 'FinancialItem', 'bao' => 'CRM_Financial_BAO_FinancialItem', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialTrxn.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialTrxn.php index 6ac9060720c..bf194318976 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialTrxn.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialTrxn.php @@ -321,6 +321,7 @@ public static function &fields() { 'headerPattern' => '', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_financial_trxn', 'entity' => 'FinancialTrxn', 'bao' => 'CRM_Financial_DAO_FinancialTrxn', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialType.php index 7ee17ac490c..8f805cb8c3d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/DAO/FinancialType.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Financial/FinancialType.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:9d787931917508983d68631821eea721) + * (GenCodeChecksum:6d85bc0675253407de19ac9226ba4478) */ /** @@ -114,6 +114,15 @@ public static function &fields() { 'entity' => 'FinancialType', 'bao' => 'CRM_Financial_BAO_FinancialType', 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => ts("Name"), + ], + 'pseudoconstant' => [ + 'table' => 'civicrm_financial_type', + 'keyColumn' => 'id', + 'labelColumn' => 'name', + ] ], 'description' => [ 'name' => 'description', @@ -126,6 +135,10 @@ public static function &fields() { 'entity' => 'FinancialType', 'bao' => 'CRM_Financial_BAO_FinancialType', 'localizable' => 0, + 'html' => [ + 'type' => 'TextArea', + 'label' => ts("Description"), + ], ], 'is_deductible' => [ 'name' => 'is_deductible', @@ -137,6 +150,10 @@ public static function &fields() { 'entity' => 'FinancialType', 'bao' => 'CRM_Financial_BAO_FinancialType', 'localizable' => 0, + 'html' => [ + 'type' => 'CheckBox', + 'label' => ts("Tax-Deductible?"), + ], ], 'is_reserved' => [ 'name' => 'is_reserved', @@ -147,6 +164,10 @@ public static function &fields() { 'entity' => 'FinancialType', 'bao' => 'CRM_Financial_BAO_FinancialType', 'localizable' => 0, + 'html' => [ + 'type' => 'CheckBox', + 'label' => ts("Reserved?"), + ], ], 'is_active' => [ 'name' => 'is_active', @@ -157,6 +178,10 @@ public static function &fields() { 'entity' => 'FinancialType', 'bao' => 'CRM_Financial_BAO_FinancialType', 'localizable' => 0, + 'html' => [ + 'type' => 'CheckBox', + 'label' => ts("Enabled?"), + ], ], ]; CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Form/FinancialAccount.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Form/FinancialAccount.php index 5b9b8f49c31..1b1eb9e3acf 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Form/FinancialAccount.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Form/FinancialAccount.php @@ -200,7 +200,14 @@ public function postProcess() { if ($this->_action & CRM_Core_Action::UPDATE) { $params['id'] = $this->_id; } - + foreach ([ + 'is_active', + 'is_deductible', + 'is_tax', + 'is_default', + ] as $field) { + $params[$field] = CRM_Utils_Array::value($field, $params, FALSE); + } $financialAccount = CRM_Financial_BAO_FinancialAccount::add($params); CRM_Core_Session::setStatus(ts('The Financial Account \'%1\' has been saved.', array(1 => $financialAccount->name)), ts('Saved'), 'success'); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Form/FinancialType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Form/FinancialType.php index 10a30c7f248..3600ef65473 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Form/FinancialType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Form/FinancialType.php @@ -36,6 +36,22 @@ */ class CRM_Financial_Form_FinancialType extends CRM_Contribute_Form { + use CRM_Core_Form_EntityFormTrait; + + /** + * Fields for the entity to be assigned to the template. + * + * @var array + */ + protected $entityFields = []; + + /** + * Deletion message to be assigned to the form. + * + * @var string + */ + protected $deleteMessage; + /** * Set variables up before form is built. */ @@ -46,40 +62,67 @@ public function preProcess() { ) { CRM_Core_Error::fatal(ts('You do not have permission to access this page.')); } + $this->_id = CRM_Utils_Request::retrieve('id', 'Positive', $this); parent::preProcess(); + $this->setPageTitle(ts('Financial Type')); + if ($this->_id) { + $this->_title = CRM_Core_PseudoConstant::getLabel( + 'CRM_Financial_BAO_FinancialType', + 'financial_type', + $this->_id + ); + $this->assign('aid', $this->_id); + } + } + + /** + * Set entity fields to be assigned to the form. + */ + protected function setEntityFields() { + $this->entityFields = [ + 'name' => [ + 'name' => 'name', + 'required' => TRUE, + ], + 'description' => ['name' => 'description'], + 'is_deductible' => [ + 'name' => 'is_deductible', + 'description' => ts('Are contributions of this type tax-deductible?'), + ], + 'is_reserved' => ['name' => 'is_reserved'], + 'is_active' => ['name' => 'is_active'], + ]; + } + + /** + * Explicitly declare the entity api name. + */ + public function getDefaultEntity() { + return 'FinancialType'; + } + + /** + * Set the delete message. + * + * We do this from the constructor in order to do a translation. + */ + public function setDeleteMessage() { + $this->deleteMessage = ts('WARNING: You cannot delete a financial type if it is currently used by any Contributions, Contribution Pages or Membership Types. Consider disabling this option instead.') . ts('Deleting a financial type cannot be undone.') . ts('Do you want to continue?'); } /** * Build the form object. */ public function buildQuickForm() { - parent::buildQuickForm(); - $this->setPageTitle(ts('Financial Type')); - - $this->_id = CRM_Utils_Request::retrieve('id', 'Positive', $this); - if ($this->_id) { - $this->_title = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_FinancialType', $this->_id, 'name'); - } + self::buildQuickEntityForm(); if ($this->_action & CRM_Core_Action::DELETE) { return; } - $this->applyFilter('__ALL__', 'trim'); - $this->add('text', 'name', ts('Name'), CRM_Core_DAO::getAttribute('CRM_Financial_DAO_FinancialType', 'name'), TRUE); - - $this->add('text', 'description', ts('Description'), CRM_Core_DAO::getAttribute('CRM_Financial_DAO_FinancialType', 'description')); - - $this->add('checkbox', 'is_deductible', ts('Tax-Deductible?'), CRM_Core_DAO::getAttribute('CRM_Financial_DAO_FinancialType', 'is_deductible')); - $this->add('checkbox', 'is_active', ts('Enabled?'), CRM_Core_DAO::getAttribute('CRM_Financial_DAO_FinancialType', 'is_active')); - $this->add('checkbox', 'is_reserved', ts('Reserved?'), CRM_Core_DAO::getAttribute('CRM_Financial_DAO_FinancialType', 'is_reserved')); - if ($this->_action == CRM_Core_Action::UPDATE) { - $this->assign('aid', $this->_id); - } - if ($this->_action == CRM_Core_Action::UPDATE && CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_FinancialType', $this->_id, 'is_reserved', 'vid')) { - $this->freeze(array('is_active')); + if ($this->_action == CRM_Core_Action::UPDATE && CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_FinancialType', $this->_id, 'is_reserved')) { + $this->freeze(['is_active']); } - $this->addRule('name', ts('A financial type with this name already exists. Please select another name.'), 'objectExists', - array('CRM_Financial_DAO_FinancialType', $this->_id) + ['CRM_Financial_DAO_FinancialType', $this->_id] ); } @@ -95,35 +138,46 @@ public function postProcess() { CRM_Core_Session::setStatus(ts('Selected financial type has been deleted.'), ts('Record Deleted'), 'success'); } else { - $params = $ids = array(); // store the submitted values in an array $params = $this->exportValues(); - - if ($this->_action & CRM_Core_Action::UPDATE) { - $ids['financialType'] = $this->_id; + if ($this->_id) { + $params['id'] = $this->_id; } - - $financialType = CRM_Financial_BAO_FinancialType::add($params, $ids); + foreach ([ + 'is_active', + 'is_reserved', + 'is_deductible', + ] as $field) { + $params[$field] = CRM_Utils_Array::value($field, $params, FALSE); + } + $financialType = civicrm_api3('FinancialType', 'create', $params); if ($this->_action & CRM_Core_Action::UPDATE) { $url = CRM_Utils_System::url('civicrm/admin/financial/financialType', 'reset=1&action=browse'); - CRM_Core_Session::setStatus(ts('The financial type "%1" has been updated.', array(1 => $financialType->name)), ts('Saved'), 'success'); + CRM_Core_Session::setStatus(ts('The financial type "%1" has been updated.', [1 => $params['name']]), ts('Saved'), 'success'); } else { - $url = CRM_Utils_System::url('civicrm/admin/financial/financialType/accounts', 'reset=1&action=browse&aid=' . $financialType->id); - $statusArray = array( - 1 => $financialType->name, - 2 => $financialType->name, - 3 => CRM_Utils_Array::value(0, $financialType->titles), - 4 => CRM_Utils_Array::value(1, $financialType->titles), - 5 => CRM_Utils_Array::value(2, $financialType->titles), - ); - if (empty($financialType->titles)) { - $text = ts('Your Financial "%1" Type has been created and assigned to an existing financial account with the same title. You should review the assigned account and determine whether additional account relationships are needed.', $statusArray); + $url = CRM_Utils_System::url('civicrm/admin/financial/financialType/accounts', 'reset=1&action=browse&aid=' . $financialType['id']); + + $statusArray = [ + 1 => $params['name'], + ]; + $financialAccounts = civicrm_api3('EntityFinancialAccount', 'get', [ + 'return' => ["financial_account_id.name"], + 'entity_table' => "civicrm_financial_type", + 'entity_id' => $financialType['id'], + 'options' => ['sort' => "id"], + 'account_relationship' => ['!=' => "Income Account is"], + ]); + if (!empty($financialAccounts['values'])) { + foreach ($financialAccounts['values'] as $financialAccount) { + $statusArray[] = $financialAccount['financial_account_id.name']; + } + $text = ts('Your Financial "%1" Type has been created, along with a corresponding income account "%1". That income account, along with standard financial accounts "%2", "%3" and "%4" have been linked to the financial type. You may edit or replace those relationships here.', $statusArray); } else { - $text = ts('Your Financial "%1" Type has been created, along with a corresponding income account "%2". That income account, along with standard financial accounts "%3", "%4" and "%5" have been linked to the financial type. You may edit or replace those relationships here.', $statusArray); + $text = ts('Your Financial "%1" Type has been created and assigned to an existing financial account with the same title. You should review the assigned account and determine whether additional account relationships are needed.', $statusArray); } - CRM_Core_Session::setStatus($text, ts('Saved'), 'success', array('expires' => 0)); + CRM_Core_Session::setStatus($text, ts('Saved'), 'success', ['expires' => 0]); } $session = CRM_Core_Session::singleton(); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Page/AJAX.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Page/AJAX.php index 1c3a9bc2663..c5a51b5f993 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Page/AJAX.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Financial/Page/AJAX.php @@ -293,6 +293,8 @@ public static function getFinancialTransactionsList() { '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Friend/BAO/Friend.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Friend/BAO/Friend.php index 2d650fd8d7e..38455411ff9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Friend/BAO/Friend.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public static function add(&$params) { */ 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 @@ public static function retrieve(&$params, &$values) { * 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 @@ public static function create(&$params) { } } - $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 @@ public static function create(&$params) { '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,27 @@ public static function create(&$params) { $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. + 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 +201,6 @@ public static function create(&$params) { $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 +208,7 @@ public static function create(&$params) { $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']; } @@ -217,23 +217,25 @@ public static function create(&$params) { $mailParams['module'] = 'event'; } elseif ($params['entity_table'] == 'civicrm_pcp') { - $mailParams['email_from'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Email', $params['source_contact_id'], - 'email', 'contact_id' - ); + if (Civi::settings()->get('allow_mail_from_logged_in_contact')) { + $mailParams['email_from'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Email', $params['source_contact_id'], + 'email', 'contact_id' + ); + } $urlPath = 'civicrm/pcp/info'; $mailParams['module'] = 'contribute'; } $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 +265,12 @@ public static function buildFriendForm($form) { } /** - * 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 +285,7 @@ public static function getValues(&$defaults) { } /** - * Process that send tell a friend e-mails + * Process that sends tell a friend e-mails * * @param int $contactID * @param array $values @@ -297,33 +299,41 @@ public static function sendMail($contactID, &$values) { $fromName = $email; } - // use contact email, CRM-4963 + if (Civi::settings()->get('allow_mail_from_logged_in_contact')) { + // use contact email, CRM-4963 + if (empty($values['email_from'])) { + $values['email_from'] = $email; + } + } + + // If we have no "email_from" when we get to here, explicitly set it to the default domain email. if (empty($values['email_from'])) { - $values['email_from'] = $email; + list($domainFromName, $domainEmail) = CRM_Core_BAO_Domain::getNameAndEmail(); + $values['email_from'] = $domainEmail; + $values['domain'] = $domainFromName; } + $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 +346,14 @@ public static function sendMail($contactID, &$values) { * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Friend/Form.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Friend/Form.php index 7f0329ac49a..9b2a9c9f765 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Friend/Form.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/CRM/Grant/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Grant/Form/Task.php index 51f505428ee..58fd8f9acff 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Grant/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() { /** * @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 @@ public static function preProcessCommon(&$form, $useTable = FALSE) { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Form/Edit.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Form/Edit.php index a4d944e18f7..810ca19d1b8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Form/Edit.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Form/Edit.php @@ -364,7 +364,7 @@ public static function formRule($fields, $fileParams, $options) { * 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 @@ public static function buildParentGroups(&$form) { $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 @@ public static function buildParentGroups(&$form) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Form/Search.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Form/Search.php index f05dfdcb830..a8edc042fde 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Form/Search.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Form/Search.php @@ -81,6 +81,17 @@ public function buildQuickForm() { 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 @@ public function postProcess() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Page/AJAX.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Page/AJAX.php index a1d9211f9f0..da2e8f1ea65 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Page/AJAX.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Group/Page/AJAX.php @@ -54,6 +54,7 @@ public static function getGroupList() { 'created_by' => 'String', 'group_type' => 'String', 'visibility' => 'String', + 'component_mode' => 'String', 'status' => 'Integer', 'parentsOnly' => 'Integer', 'showOrgInfo' => 'Boolean', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Logging/Schema.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Logging/Schema.php index 996653ce6cd..a24906ef91b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Logging/Schema.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Logging/Schema.php @@ -945,7 +945,7 @@ public static function disableLoggingForThisConnection() { * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/Mailing.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/Mailing.php index 8e34d917e12..212284f00ee 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/Mailing.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/Mailing.php @@ -228,35 +228,39 @@ public static function getRecipients($mailingID) { ); if ($isSMSmode) { - $includeFilters = array( - "mg.group_type = 'Include'", - 'mg.search_id IS NULL', - "$contact.is_opt_out = 0", - "$contact.is_deceased <> 1", - "$entityTable.phone_type_id = " . CRM_Core_PseudoConstant::getKey('CRM_Core_DAO_Phone', 'phone_type_id', 'Mobile'), - "$entityTable.phone IS NOT NULL", - "$entityTable.phone != ''", - "$entityTable.is_primary = 1", - "mg.mailing_id = #mailingID", - 'temp.contact_id IS null', + $criteria = array( + 'is_opt_out' => CRM_Utils_SQL_Select::fragment()->where("$contact.is_opt_out = 0"), + 'is_deceased' => CRM_Utils_SQL_Select::fragment()->where("$contact.is_deceased <> 1"), + 'do_not_sms' => CRM_Utils_SQL_Select::fragment()->where("$contact.do_not_sms = 0"), + 'location_filter' => CRM_Utils_SQL_Select::fragment()->where("$entityTable.phone_type_id = " . CRM_Core_PseudoConstant::getKey('CRM_Core_DAO_Phone', 'phone_type_id', 'Mobile')), + 'phone_not_null' => CRM_Utils_SQL_Select::fragment()->where("$entityTable.phone IS NOT NULL"), + 'phone_not_empty' => CRM_Utils_SQL_Select::fragment()->where("$entityTable.phone != ''"), + 'is_primary' => CRM_Utils_SQL_Select::fragment()->where("$entityTable.is_primary = 1"), + 'mailing_id' => CRM_Utils_SQL_Select::fragment()->where("mg.mailing_id = #mailingID"), + 'temp_contact_null' => CRM_Utils_SQL_Select::fragment()->where('temp.contact_id IS null'), + 'order_by' => CRM_Utils_SQL_Select::fragment()->orderBy("$entityTable.is_primary = 1"), ); - $order_by = array("$entityTable.is_primary = 1"); } else { // Criterias to filter recipients that need to be included - $includeFilters = array( - "$contact.do_not_email = 0", - "$contact.is_opt_out = 0", - "$contact.is_deceased <> 1", - $location_filter, - "$entityTable.email IS NOT NULL", - "$entityTable.email != ''", - "$entityTable.on_hold = 0", - "mg.mailing_id = #mailingID", - 'temp.contact_id IS NULL', + $criteria = array( + 'do_not_email' => CRM_Utils_SQL_Select::fragment()->where("$contact.do_not_email = 0"), + 'is_opt_out' => CRM_Utils_SQL_Select::fragment()->where("$contact.is_opt_out = 0"), + 'is_deceased' => CRM_Utils_SQL_Select::fragment()->where("$contact.is_deceased <> 1"), + 'location_filter' => CRM_Utils_SQL_Select::fragment()->where($location_filter), + 'email_not_null' => CRM_Utils_SQL_Select::fragment()->where("$entityTable.email IS NOT NULL"), + 'email_not_empty' => CRM_Utils_SQL_Select::fragment()->where("$entityTable.email != ''"), + 'email_not_on_hold' => CRM_Utils_SQL_Select::fragment()->where("$entityTable.on_hold = 0"), + 'mailing_id' => CRM_Utils_SQL_Select::fragment()->where("mg.mailing_id = #mailingID"), + 'temp_contact_null' => CRM_Utils_SQL_Select::fragment()->where('temp.contact_id IS NULL'), + 'order_by' => CRM_Utils_SQL_Select::fragment()->orderBy($order_by), ); } + // Allow user to alter query responsible to fetch mailing recipients before build, + // by changing the mail filters identified $params + CRM_Utils_Hook::alterMailingRecipients($mailingObj, $criteria, 'pre'); + // Get the group contacts, but only those which are not in the // exclusion temp table. if (!empty($recipientsGroup['Include'])) { @@ -267,7 +271,7 @@ public static function getRecipients($mailingID) { ->join('mg', " INNER JOIN civicrm_mailing_group mg ON gc.group_id = mg.entity_id AND mg.search_id IS NULL ") ->join('temp', " LEFT JOIN $excludeTempTablename temp ON $contact.id = temp.contact_id ") ->where('gc.group_id IN (#groups) AND gc.status = "Added"') - ->where($includeFilters) + ->merge($criteria) ->groupBy(array("$contact.id", "$entityTable.id")) ->replaceInto($includedTempTablename, array('contact_id', $entityColumn)) ->param('#groups', $recipientsGroup['Include']) @@ -293,8 +297,7 @@ public static function getRecipients($mailingID) { ->join('mg', " INNER JOIN civicrm_mailing_group mg ON gc.group_id = mg.entity_id AND mg.search_id IS NULL ") ->join('temp', " LEFT JOIN $excludeTempTablename temp ON $contact.id = temp.contact_id ") ->where('gc.group_id IN (#groups)') - ->where($includeFilters) - ->orderBy($order_by) + ->merge($criteria) ->replaceInto($includedTempTablename, array('contact_id', $entityColumn)) ->param('#groups', $includeSmartGroupIDs) ->param('#mailingID', $mailingID) @@ -370,6 +373,8 @@ public static function getRecipients($mailingID) { $mailingGroup->reset(); $mailingGroup->query(" DROP TEMPORARY TABLE $excludeTempTablename "); $mailingGroup->query(" DROP TEMPORARY TABLE $includedTempTablename "); + + CRM_Utils_Hook::alterMailingRecipients($mailingObj, $criteria, 'post'); } /** @@ -1453,7 +1458,7 @@ public static function add(&$params, $ids = array()) { } $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/MailingJob.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/MailingJob.php index 9b2a80b2079..12379bcc8e6 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/MailingJob.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/MailingJob.php @@ -598,6 +598,7 @@ public function deliverGroup(&$fields, &$mailing, &$mailer, &$job_date, &$attach $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 @@ -675,18 +676,17 @@ public function deliverGroup(&$fields, &$mailing, &$mailer, &$job_date, &$attach if (is_a($result, 'PEAR_Error') && !$mailing->sms_provider_id) { // CRM-9191 $message = $result->getMessage(); - if ( - strpos($message, 'Failed to write to socket') !== FALSE || - strpos($message, 'Failed to set sender') !== FALSE - ) { + if ($this->isTemporaryError($message)) { // lets log this message and code $code = $result->getCode(); 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,9 +776,50 @@ public function deliverGroup(&$fields, &$mailing, &$mailer, &$job_date, &$attach $job_date ); + if ($retryGroup) { + return FALSE; + } + return $result; } + /** + * Determine if an SMTP error is temporary or permanent. + * + * @param string $message + * PEAR error message. + * @return bool + * TRUE - Temporary/retriable error + * FALSE - Permanent/non-retriable error + */ + protected function isTemporaryError($message) { + // SMTP response code is buried in the message. + $code = preg_match('/ \(code: (.+), response: /', $message, $matches) ? $matches[1] : ''; + + if (strpos($message, 'Failed to write to socket') !== FALSE) { + return TRUE; + } + + // Register 5xx SMTP response code (permanent failure) as bounce. + if (isset($code{0}) && $code{0} === '5') { + return FALSE; + } + + if (strpos($message, 'Failed to set sender') !== FALSE) { + return TRUE; + } + + if (strpos($message, 'Failed to add recipient') !== FALSE) { + return TRUE; + } + + if (strpos($message, 'Failed to send data') !== FALSE) { + return TRUE; + } + + return FALSE; + } + /** * Cancel a mailing. * @@ -817,11 +858,54 @@ public static function cancel($mailingId) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/Recipients.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/Recipients.php index 4ca5be9cde4..5dfeb1e58af 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/Recipients.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/Recipients.php @@ -102,7 +102,7 @@ public static function updateRandomRecipients($sourceMailingId, $newMailingID, $ CRM_Core_DAO::executeQuery("DROP TEMPORARY TABLE IF EXISTS srcMailing_$sourceMailingId"); $sql = " CREATE TEMPORARY TABLE srcMailing_$sourceMailingId - (mailing_recipient_id int, id int PRIMARY KEY AUTO_INCREMENT, INDEX(mailing_recipient_id)) + (mailing_recipient_id int unsigned, id int PRIMARY KEY AUTO_INCREMENT, INDEX(mailing_recipient_id)) ENGINE=HEAP"; CRM_Core_DAO::executeQuery($sql); $sql = " diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/TrackableURL.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/TrackableURL.php index 6d563f3ee24..b1e40d7009a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/TrackableURL.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/BAO/TrackableURL.php @@ -97,7 +97,7 @@ public static function getTrackerURL($url, $mailing_id, $queue_id) { $returnUrl = "{$urlCache[$mailing_id . $url]}&qid={$queue_id}"; if ($hrefExists) { - $returnUrl = "href='{$returnUrl}'"; + $returnUrl = "href='{$returnUrl}' rel='nofollow'"; } return $returnUrl; diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/Component.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/Component.php index d0c861594b6..38aef9f12ae 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/Component.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/Component.php @@ -182,6 +182,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Mailing Component is Default?'), 'description' => 'Is this the default component for this component_type?', + 'default' => '0', 'table_name' => 'civicrm_mailing_component', 'entity' => 'Component', 'bao' => 'CRM_Mailing_BAO_Component', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/Mailing.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/Mailing.php index aafeb0090d3..286256a503b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/Mailing.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/Mailing.php @@ -674,6 +674,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Override Verp'), 'description' => 'Should we overrite VERP address in Reply-To', + 'default' => '0', 'table_name' => 'civicrm_mailing', 'entity' => 'Mailing', 'bao' => 'CRM_Mailing_BAO_Mailing', @@ -814,6 +815,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Mailing Archived?'), 'description' => 'Is this mailing archived?', + 'default' => '0', 'table_name' => 'civicrm_mailing', 'entity' => 'Mailing', 'bao' => 'CRM_Mailing_BAO_Mailing', @@ -865,6 +867,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('No Duplicate emails?'), 'description' => 'Remove duplicate emails?', + 'default' => '0', 'table_name' => 'civicrm_mailing', 'entity' => 'Mailing', 'bao' => 'CRM_Mailing_BAO_Mailing', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/MailingJob.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/MailingJob.php index 93cd6819486..931685895fd 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/MailingJob.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/DAO/MailingJob.php @@ -216,6 +216,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Mailing Job Is Test?'), 'description' => 'Is this job for a test mail?', + 'default' => '0', 'table_name' => 'civicrm_mailing_job', 'entity' => 'MailingJob', 'bao' => 'CRM_Mailing_BAO_MailingJob', @@ -250,6 +251,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Mailing Job Offset'), 'description' => 'Offset of the child job', + 'default' => '0', 'table_name' => 'civicrm_mailing_job', 'entity' => 'MailingJob', 'bao' => 'CRM_Mailing_BAO_MailingJob', @@ -260,6 +262,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Mailing Job Limit'), 'description' => 'Queue size limit for each child job', + 'default' => '0', 'table_name' => 'civicrm_mailing_job', 'entity' => 'MailingJob', 'bao' => 'CRM_Mailing_BAO_MailingJob', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Confirm.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Confirm.php index f01e188d8ab..1d3c2d4d18d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Confirm.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Confirm.php @@ -118,8 +118,6 @@ public static function confirm($contact_id, $subscribe_id, $hash) { $component->find(TRUE); - $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain(); - $html = $component->body_html; if ($component->body_text) { @@ -143,11 +141,11 @@ public static function confirm($contact_id, $subscribe_id, $hash) { $mailParams = array( 'groupName' => 'Mailing Event ' . $component->component_type, 'subject' => $component->subject, - 'from' => "\"$domainEmailName\" ", + '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Reply.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Reply.php index 00d8cd3eac0..679822ffe02 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Reply.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Reply.php @@ -173,8 +173,6 @@ public static function send($queue_id, &$mailing, &$bodyTxt, $replyto, &$bodyHTM } } else { - $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain(); - if (empty($eq->display_name)) { $from = $eq->email; } @@ -189,7 +187,7 @@ public static function send($queue_id, &$mailing, &$bodyTxt, $replyto, &$bodyHTM '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 @@ private static function autoRespond(&$mailing, $queue_id, $replyto) { $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\" ", - '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Resubscribe.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Resubscribe.php index 483f35641d2..360bd36bfec 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Resubscribe.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Resubscribe.php @@ -267,14 +267,12 @@ public static function send_resub_response($queue_id, $groups, $is_domain = FALS $message->setTxtBody($text); } - $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain(); - $headers = array( 'Subject' => $component->subject, - 'From' => "\"$domainEmailName\" ", + '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Subscribe.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Subscribe.php index d3257557b75..d82776413b4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Subscribe.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Subscribe.php @@ -227,7 +227,7 @@ public function send_confirm_request($email) { '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Unsubscribe.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Unsubscribe.php index 76f3fef7fb6..aa6950cb8c8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Unsubscribe.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Event/BAO/Unsubscribe.php @@ -418,10 +418,10 @@ public static function send_unsub_response($queue_id, $groups, $is_domain = FALS $headers = array( 'Subject' => $component->subject, - 'From' => "\"$domainEmailName\" ", + '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Browse.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Browse.php index c5db18c6aec..dad45758b2f 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Browse.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Browse.php @@ -82,6 +82,7 @@ public function postProcess() { } 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Search.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Search.php index ce959101007..f64d36dd90d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Search.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Search.php @@ -92,7 +92,7 @@ public function setDefaultValues() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Task.php index 68ff661c565..dc9290be3fc 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() { /** * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Page/Browse.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Page/Browse.php index d5e2ac1a514..bb801f45fe4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Page/Browse.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Page/Browse.php @@ -181,6 +181,7 @@ public function run() { 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 @@ public function run() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Selector/Browse.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Selector/Browse.php index 4668cf2938f..8af4a7f00c1 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Selector/Browse.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Mailing/Selector/Browse.php @@ -294,6 +294,18 @@ public function &getRows($action, $offset, $rowCount, $sort, $output = NULL) { '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 @@ public function &getRows($action, $offset, $rowCount, $sort, $output = NULL) { ) { $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 @@ public function whereClause(&$params, $sortBy = TRUE) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/BAO/Membership.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/BAO/Membership.php index ce6df5a6059..1826901429c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/BAO/Membership.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/BAO/Membership.php @@ -134,7 +134,7 @@ public static function add(&$params, $ids = array()) { $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 @@ public static function fixMembershipStatusBeforeRenew(&$currentMembership, $chan ) ); - CRM_Member_BAO_MembershipLog::add($logParams, CRM_Core_DAO::$_nullArray); + CRM_Member_BAO_MembershipLog::add($logParams); } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/BAO/MembershipType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/BAO/MembershipType.php index 66c9cd2a8e9..796e047a073 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/BAO/MembershipType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/BAO/MembershipType.php @@ -807,41 +807,39 @@ public static function createMembershipPriceField($params, $previousID, $members * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/Membership.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/Membership.php index 969b243937f..136cc97aca0 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/Membership.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/Membership.php @@ -406,6 +406,7 @@ public static function &fields() { 'headerPattern' => '/(is.)?test(.member(ship)?)?/i', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_membership', 'entity' => 'Membership', 'bao' => 'CRM_Member_BAO_Membership', @@ -423,6 +424,7 @@ public static function &fields() { 'headerPattern' => '/(is.)?(pay(.)?later)$/i', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_membership', 'entity' => 'Membership', 'bao' => 'CRM_Member_BAO_Membership', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipBlock.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipBlock.php index cba09a6af55..118a8ff4251 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipBlock.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipBlock.php @@ -281,6 +281,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Required'), 'description' => 'Is membership sign up optional', + 'default' => '0', 'table_name' => 'civicrm_membership_block', 'entity' => 'MembershipBlock', 'bao' => 'CRM_Member_BAO_MembershipBlock', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipStatus.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipStatus.php index e38b1559339..4df669ce82c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipStatus.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipStatus.php @@ -346,6 +346,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Reserved'), 'description' => 'Is this membership_status reserved.', + 'default' => '0', 'table_name' => 'civicrm_membership_status', 'entity' => 'MembershipStatus', 'bao' => 'CRM_Member_BAO_MembershipStatus', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipType.php index a5469039bf7..275933dc11e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/DAO/MembershipType.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Member/MembershipType.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:51b478b01b1ef20a54b1039ccc29d0ab) + * (GenCodeChecksum:b36330fa029a5c1467441e89b7c45c63) */ /** @@ -305,6 +305,7 @@ public static function &fields() { 18, 9 ], + 'default' => '0', 'table_name' => 'civicrm_membership_type', 'entity' => 'MembershipType', 'bao' => 'CRM_Member_BAO_MembershipType', @@ -484,10 +485,14 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Auto Renew'), 'description' => '0 = No auto-renew option; 1 = Give option, but not required; 2 = Auto-renew required;', + 'default' => '0', 'table_name' => 'civicrm_membership_type', 'entity' => 'MembershipType', 'bao' => 'CRM_Member_BAO_MembershipType', 'localizable' => 0, + 'html' => [ + 'type' => 'Radio', + ], 'pseudoconstant' => [ 'callback' => 'CRM_Core_SelectValues::memberAutoRenew', ] diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/Membership.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/Membership.php index 32018d525d7..b81bc5ec3bb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/Membership.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/Membership.php @@ -947,7 +947,7 @@ private function prepareStatusOverrideValues() { * the selected override option is not 'until date'. */ private function setOverrideDateValue() { - if (!CRM_Member_StatusOverrideTypes::isUntilDate($this->_params['is_override'])) { + if (!CRM_Member_StatusOverrideTypes::isUntilDate(CRM_Utils_Array::value('is_override', $this->_params))) { $this->_params['status_override_end_date'] = ''; } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/MembershipStatus.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/MembershipStatus.php index 10b468febd9..e51f08918ef 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/MembershipStatus.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/MembershipStatus.php @@ -106,13 +106,7 @@ public function preProcess() { * @return array */ public function setDefaultValues() { - $defaults = array(); - - if ($this->getEntityId()) { - $params = array('id' => $this->getEntityId()); - $baoName = $this->_BAOName; - $baoName::retrieve($params, $defaults); - } + $defaults = $this->getEntityDefaults(); if ($this->_action & CRM_Core_Action::ADD) { $defaults['is_active'] = 1; diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/MembershipType.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/MembershipType.php index ba2ab9c19ae..3a5d4926bc9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/MembershipType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/MembershipType.php @@ -38,6 +38,109 @@ */ class CRM_Member_Form_MembershipType extends CRM_Member_Form_MembershipConfig { + use CRM_Core_Form_EntityFormTrait; + + /** + * Fields for the entity to be assigned to the template. + * + * Fields may have keys + * - name (required to show in tpl from the array) + * - description (optional, will appear below the field) + * - not-auto-addable - this class will not attempt to add the field using addField. + * (this will be automatically set if the field does not have html in it's metadata + * or is not a core field on the form's entity). + * - help (option) add help to the field - e.g ['id' => 'id-source', 'file' => 'CRM/Contact/Form/Contact']] + * - template - use a field specific template to render this field + * - required + * - is_freeze (field should be frozen). + * + * @var array + */ + protected $entityFields = []; + + /** + * Set entity fields to be assigned to the form. + */ + protected function setEntityFields() { + $this->entityFields = [ + 'name' => [ + 'required' => 'TRUE', + 'name' => 'name', + 'description' => ts("e.g. 'Student', 'Senior', 'Honor Society'..."), + ], + 'description' => [ + 'name' => 'description', + 'description' => ts("Description of this membership type for internal use. May include eligibility, benefits, terms, etc."), + ], + 'member_of_contact_id' => [ + 'name' => 'member_of_contact_id', + 'description' => ts("Members assigned this membership type belong to which organization (e.g. this is for membership in 'Save the Whales - Northwest Chapter'). NOTE: This organization/group/chapter must exist as a CiviCRM Organization type contact."), + ], + 'minimum_fee' => [ + 'name' => 'minimum_fee', + 'description' => ts('Minimum fee required for this membership type. For free/complimentary memberships - set minimum fee to zero (0). NOTE: When using CiviCRM to process sales taxes this should be the tax exclusive amount.'), + 'formatter' => 'crmMoney', + ], + 'financial_type_id' => [ + 'name' => 'financial_type_id', + 'description' => ts('Select the financial type assigned to fees for this membership type (for example \'Membership Fees\'). This is required for all membership types - including free or complimentary memberships.'), + ], + 'auto_renew' => [ + 'name' => 'auto_renew', + 'options' => CRM_Core_SelectValues::memberAutoRenew(), + 'place_holder' => ts('You will need to select and configure a supported payment processor (currently Authorize.Net, PayPal Pro, or PayPal Website Standard) in order to offer automatically renewing memberships.'), + ], + 'duration_interval' => [ + 'name' => 'duration_interval', + ], + 'duration_unit' => [ + 'name' => 'duration_unit', + 'description' => ts('Duration of this membership (e.g. 30 days, 2 months, 5 years, 1 lifetime)'), + ], + 'period_type' => [ + 'name' => 'period_type', + 'description' => ts("Select 'rolling' if membership periods begin at date of signup. Select 'fixed' if membership periods begin on a set calendar date."), + 'help' => ['id' => 'period-type', 'file' => "CRM/Member/Page/MembershipType.hlp"], + ], + 'fixed_period_start_day' => [ + 'name' => 'fixed_period_start_day', + 'description' => ts("Month and day on which a fixed period membership or subscription begins. Example: A fixed period membership with Start Day set to Jan 01 means that membership periods would be 1/1/06 - 12/31/06 for anyone signing up during 2006."), + ], + 'fixed_period_rollover_day' => [ + 'name' => 'fixed_period_rollover_day', + 'description' => ts('Membership signups on or after this date cover the following calendar year as well. Example: If the rollover day is November 30, membership period for signups during December will cover the following year.'), + ], + 'relationship_type_id' => [ + 'name' => 'relationship_type_id', + ], + 'max_related' => [ + 'name' => 'max_related', + 'description' => ts('Maximum number of related memberships (leave blank for unlimited).'), + ], + 'visibility' => [ + 'name' => 'visibility', + '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')."), + ], + 'weight' => [ + 'name' => 'weight', + ], + 'is_active' => [ + 'name' => 'is_active', + ], + ]; + + if (!CRM_Financial_BAO_PaymentProcessor::hasPaymentProcessorSupporting(array('Recurring'))) { + $this->entityFields['auto_renew']['not-auto-addable'] = TRUE; + $this->entityFields['auto_renew']['documentation_link'] = ['page' => 'user/contributions/payment-processors']; + } + } + + /** + * Deletion message to be assigned to the form. + * + * @var string + */ + protected $deleteMessage; /** * Explicitly declare the entity api name. @@ -46,6 +149,15 @@ public function getDefaultEntity() { return 'MembershipType'; } + /** + * Set the delete message. + * + * We do this from the constructor in order to do a translation. + */ + public function setDeleteMessage() { + $this->deleteMessage = ts('WARNING: Deleting this option will result in the loss of all membership records of this type.') . ts('This may mean the loss of a substantial amount of data, and the action cannot be undone.') . ts('Do you want to continue?'); + } + /** * Explicitly declare the form context. */ @@ -127,31 +239,22 @@ public function setDefaultValues() { * @throws \CiviCRM_API3_Exception */ public function buildQuickForm() { - parent::buildQuickForm(); + self::buildQuickEntityForm(); if ($this->_action & CRM_Core_Action::DELETE) { return; } - - $this->applyFilter('__ALL__', 'trim'); - $this->addField('name', [], TRUE); - $this->addField('description'); - $this->addField('minimum_fee'); - $this->addField('duration_unit', [], TRUE); - $this->addField('period_type', [], TRUE); - $this->addField('is_active'); - $this->addField('weight'); - $this->addField('max_related'); + // This is a temporary variable as we work towards moving over towards using the EntityField.tpl. + // Fields in this array have been tested & in the tpl have been switched over to metadata. + // Note this kinda 'works from the top' - ie. once we hit a field that needs some thought we need + // to stop & make that one work. + $this->assign('tpl_standardised_fields', ['name', 'description', 'member_of_contact_id', 'minimum_fee']); $this->addRule('name', ts('A membership type with this name already exists. Please select another name.'), 'objectExists', array('CRM_Member_DAO_MembershipType', $this->_id) ); $this->addRule('minimum_fee', ts('Please enter a monetary value for the Minimum Fee.'), 'money'); - $this->add('text', 'duration_interval', ts('Duration Interval'), - CRM_Core_DAO::getAttribute('CRM_Member_DAO_MembershipType', 'duration_interval') - ); - $props = array('api' => array('params' => array('contact_type' => 'Organization'))); $this->addEntityRef('member_of_contact_id', ts('Membership Organization'), $props, TRUE); @@ -307,41 +410,16 @@ public function postProcess() { CRM_Core_Session::setStatus(ts('Selected membership type has been deleted.'), ts('Record Deleted'), 'success'); } else { - $buttonName = $this->controller->getButtonName(); - $submitted = $this->controller->exportValues($this->_name); - - $fields = array( - 'name', - 'weight', - 'is_active', - 'member_of_contact_id', - 'visibility', - 'period_type', - 'minimum_fee', - 'description', - 'auto_renew', - 'duration_unit', - 'duration_interval', - 'financial_type_id', - 'fixed_period_start_day', - 'fixed_period_rollover_day', - 'month_fixed_period_rollover_day', - 'max_related', - ); - - $params = array(); - foreach ($fields as $fld) { - $params[$fld] = CRM_Utils_Array::value($fld, $submitted, 'null'); - } + $params = $this->exportValues(); if ($params['minimum_fee']) { $params['minimum_fee'] = CRM_Utils_Rule::cleanMoney($params['minimum_fee']); } $hasRelTypeVal = FALSE; - if (!CRM_Utils_System::isNull($submitted['relationship_type_id'])) { + if (!CRM_Utils_System::isNull($params['relationship_type_id'])) { // To insert relation ids and directions with value separator - $relTypeDirs = $submitted['relationship_type_id']; + $relTypeDirs = $params['relationship_type_id']; $relIds = $relDirection = array(); foreach ($relTypeDirs as $key => $value) { $relationId = explode('_', $value); @@ -400,12 +478,14 @@ public function postProcess() { $params['id'] = $this->_id; } - $membershipType = CRM_Member_BAO_MembershipType::add($params); + $membershipTypeResult = civicrm_api3('MembershipType', 'create', $params); + $membershipTypeName = $membershipTypeResult['values'][$membershipTypeResult['id']]['name']; - CRM_Core_Session::setStatus(ts('The membership type \'%1\' has been saved.', - array(1 => $membershipType->name) + CRM_Core_Session::setStatus(ts("The membership type '%1' has been saved.", + array(1 => $membershipTypeName) ), ts('Saved'), 'success'); $session = CRM_Core_Session::singleton(); + $buttonName = $this->controller->getButtonName(); if ($buttonName == $this->getButtonName('upload', 'new')) { $session->replaceUserContext( CRM_Utils_System::url('civicrm/admin/member/membershipType/add', 'action=add&reset=1') diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/Task.php index 009b67f201f..e8c6e9bfe11 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() { /** * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Page/RecurringContributions.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Page/RecurringContributions.php index 67ef43d1d2c..67b2b939381 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Page/RecurringContributions.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Page/RecurringContributions.php @@ -112,14 +112,18 @@ private function getRecurContributions($membershipID) { */ 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Page/Tab.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Page/Tab.php index 58347f05dd6..a461dab701c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Page/Tab.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Page/Tab.php @@ -59,9 +59,7 @@ public function browse() { $membership = array(); $dao = new CRM_Member_DAO_Membership(); $dao->contact_id = $this->_contactId; - $dao->is_test = 0; $dao->whereAdd($addWhere); - //$dao->orderBy('name'); $dao->find(); //CRM--4418, check for view, edit, delete diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Task.php index a8843f6e6c9..572dbae0114 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Task.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Member/Task.php @@ -102,6 +102,16 @@ public static function tasks() { '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/profiles/civicrm_starterkit/modules/civicrm/CRM/PCP/DAO/PCP.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/PCP/DAO/PCP.php index 09371761b8b..72a9d6eb0cb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/PCP/DAO/PCP.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/PCP/DAO/PCP.php @@ -298,6 +298,7 @@ public static function &fields() { 'name' => 'is_thermometer', 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Use Thermometer?'), + 'default' => '0', 'table_name' => 'civicrm_pcp', 'entity' => 'PCP', 'bao' => 'CRM_PCP_BAO_PCP', @@ -310,6 +311,7 @@ public static function &fields() { 'name' => 'is_honor_roll', 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Show Honor Roll?'), + 'default' => '0', 'table_name' => 'civicrm_pcp', 'entity' => 'PCP', 'bao' => 'CRM_PCP_BAO_PCP', @@ -362,6 +364,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Enabled?'), 'description' => 'Is Personal Campaign Page enabled/active?', + 'default' => '0', 'table_name' => 'civicrm_pcp', 'entity' => 'PCP', 'bao' => 'CRM_PCP_BAO_PCP', @@ -375,6 +378,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Notify Owner?'), 'description' => 'Notify owner via email when someone donates to page?', + 'default' => '0', 'table_name' => 'civicrm_pcp', 'entity' => 'PCP', 'bao' => 'CRM_PCP_BAO_PCP', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/PCP/DAO/PCPBlock.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/PCP/DAO/PCPBlock.php index c1f007d5640..37fcaeb0d76 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/PCP/DAO/PCPBlock.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/PCP/DAO/PCPBlock.php @@ -225,6 +225,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Owner Notification'), 'description' => 'FK to civicrm_option_group with name = PCP owner notifications', + 'default' => '0', 'table_name' => 'civicrm_pcp_block', 'entity' => 'PCPBlock', 'bao' => 'CRM_PCP_BAO_PCPBlock', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/BAO/PledgeBlock.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/BAO/PledgeBlock.php index 4762ce1839c..496ddf674df 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/BAO/PledgeBlock.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/BAO/PledgeBlock.php @@ -281,11 +281,11 @@ public static function buildPledgeBlock($form) { $form->addRadio('is_pledge', ts('Pledge Frequency Interval'), $pledgeOptions, NULL, array('
') ); - $form->addElement('text', 'pledge_installments', ts('Installments'), array('size' => 3)); + $form->addElement('text', 'pledge_installments', ts('Installments'), ['size' => 3, 'aria-label' => ts('Installments')]); if (!empty($pledgeBlock['is_pledge_interval'])) { $form->assign('is_pledge_interval', CRM_Utils_Array::value('is_pledge_interval', $pledgeBlock)); - $form->addElement('text', 'pledge_frequency_interval', NULL, array('size' => 3)); + $form->addElement('text', 'pledge_frequency_interval', NULL, ['size' => 3, 'aria-label' => ts('Frequency Intervals')]); } else { $form->add('hidden', 'pledge_frequency_interval', 1); @@ -299,7 +299,7 @@ public static function buildPledgeBlock($form) { $freqUnits[$val] = !empty($pledgeBlock['is_pledge_interval']) ? "{$frequencyUnits[$val]}(s)" : $frequencyUnits[$val]; } } - $form->addElement('select', 'pledge_frequency_unit', NULL, $freqUnits); + $form->addElement('select', 'pledge_frequency_unit', NULL, $freqUnits, ['aria-label' => ts('Frequency Units')]); // CRM-18854 if (CRM_Utils_Array::value('is_pledge_start_date_visible', $pledgeBlock)) { if (CRM_Utils_Array::value('pledge_start_date', $pledgeBlock)) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/Pledge.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/Pledge.php index 21d857a3d9f..bfe1c096128 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/Pledge.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/Pledge.php @@ -575,6 +575,7 @@ public static function &fields() { 'headerPattern' => '', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_pledge', 'entity' => 'Pledge', 'bao' => 'CRM_Pledge_BAO_Pledge', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/PledgeBlock.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/PledgeBlock.php index 19ee593688d..1efdbf69aca 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/PledgeBlock.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/PledgeBlock.php @@ -188,6 +188,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Expose Frequency Interval?'), 'description' => 'Is frequency interval exposed on the contribution form.', + 'default' => '0', 'table_name' => 'civicrm_pledge_block', 'entity' => 'PledgeBlock', 'bao' => 'CRM_Pledge_BAO_PledgeBlock', @@ -244,6 +245,7 @@ public static function &fields() { 'title' => ts('Show Recurring Donation Start Date?'), 'description' => 'If true - recurring start date is shown.', 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_pledge_block', 'entity' => 'PledgeBlock', 'bao' => 'CRM_Pledge_BAO_PledgeBlock', @@ -255,6 +257,7 @@ public static function &fields() { 'title' => ts('Allow Edits to Recurring Donation Start Date?'), 'description' => 'If true - recurring start date is editable.', 'required' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_pledge_block', 'entity' => 'PledgeBlock', 'bao' => 'CRM_Pledge_BAO_PledgeBlock', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/PledgePayment.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/PledgePayment.php index d3f05e4656b..3a996ff0ed4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/PledgePayment.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/DAO/PledgePayment.php @@ -266,6 +266,7 @@ public static function &fields() { 'headerPattern' => '', 'dataPattern' => '', 'export' => TRUE, + 'default' => '0', 'table_name' => 'civicrm_pledge_payment', 'entity' => 'PledgePayment', 'bao' => 'CRM_Pledge_BAO_PledgePayment', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/Form/Task.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/Form/Task.php index 092fece3a3e..4a8b8556144 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Pledge/Form/Task.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function preProcess() { * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/BAO/PriceField.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/BAO/PriceField.php index d7dc9614344..0dd12fd66c5 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/BAO/PriceField.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/BAO/PriceField.php @@ -413,10 +413,10 @@ public static function addQuickFormElement( if ($field->is_display_amounts) { $opt['label'] = !empty($opt['label']) ? $opt['label'] . ' - ' : ''; $preHelpText = $postHelpText = ''; - if (isset($opt['help_pre'])) { + if (!empty($opt['help_pre'])) { $preHelpText = '' . $opt['help_pre'] . ': '; } - if (isset($opt['help_post'])) { + if (!empty($opt['help_post'])) { $postHelpText = ': ' . $opt['help_post'] . ''; } if (isset($taxAmount) && $invoicing) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceField.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceField.php index 9cfe9768066..aeaf386e72e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceField.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceField.php @@ -254,6 +254,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Price Field Quantity Required?'), 'description' => 'Enter a quantity for this field?', + 'default' => '0', 'table_name' => 'civicrm_price_field', 'entity' => 'PriceField', 'bao' => 'CRM_Price_BAO_PriceField', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceFieldValue.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceFieldValue.php index e656e0ed28e..f07a16e344e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceFieldValue.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceFieldValue.php @@ -382,6 +382,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Default Price Field Option?'), 'description' => 'Is this default price field option', + 'default' => '0', 'table_name' => 'civicrm_price_field_value', 'entity' => 'PriceFieldValue', 'bao' => 'CRM_Price_BAO_PriceFieldValue', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceSet.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceSet.php index 3abf87d17f2..cd26b23f777 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceSet.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Price/DAO/PriceSet.php @@ -318,6 +318,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Is Price Set Quick Config?'), 'description' => 'Is set if edited on Contribution or Event Page rather than through Manage Price Sets', + 'default' => '0', 'table_name' => 'civicrm_price_set', 'entity' => 'PriceSet', 'bao' => 'CRM_Price_BAO_PriceSet', @@ -331,6 +332,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Price Set Is Reserved'), 'description' => 'Is this a predefined system price set (i.e. it can not be deleted, edited)?', + 'default' => '0', 'table_name' => 'civicrm_price_set', 'entity' => 'PriceSet', 'bao' => 'CRM_Price_BAO_PriceSet', @@ -344,6 +346,7 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Minimum Amount'), 'description' => 'Minimum Amount required for this set.', + 'default' => '0', 'table_name' => 'civicrm_price_set', 'entity' => 'PriceSet', 'bao' => 'CRM_Price_BAO_PriceSet', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Profile/Form/Edit.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Profile/Form/Edit.php index 601283c1e6f..ada0354e0ad 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Profile/Form/Edit.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Profile/Form/Edit.php @@ -222,6 +222,7 @@ public function buildQuickForm() { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Profile/Page/MultipleRecordFieldsListing.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Profile/Page/MultipleRecordFieldsListing.php index 9bff1a8a159..e8ab497e072 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Profile/Page/MultipleRecordFieldsListing.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Profile/Page/MultipleRecordFieldsListing.php @@ -341,7 +341,7 @@ public function browse() { // TODO: Not all widget types and validation rules are supported by crmEditable so some fields will not be in-place editable $fieldAttributes = array('class' => "crmf-custom_{$fieldId}_$recId"); $editable = FALSE; - if (!$options[$fieldId]['attributes']['is_view'] && $linkAction & CRM_Core_Action::UPDATE) { + if (!$options[$fieldId]['attributes']['is_view'] && $this->_pageViewType == 'customDataView' && $linkAction & CRM_Core_Action::UPDATE) { $spec = $options[$fieldId]['attributes']; switch ($spec['html_type']) { case 'Text': diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/BAO/ReportInstance.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/BAO/ReportInstance.php index 0b254ee73cb..a7dc5733cb4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/BAO/ReportInstance.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/BAO/ReportInstance.php @@ -364,24 +364,35 @@ public static function doFormDelete($instanceId, $bounceTo = 'civicrm/report/lis * - 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/DAO/ReportInstance.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/DAO/ReportInstance.php index 4effadcc03d..141e6f6312c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/DAO/ReportInstance.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/DAO/ReportInstance.php @@ -500,6 +500,7 @@ public static function &fields() { 'name' => 'is_reserved', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('Instance is Reserved'), + 'default' => '0', 'table_name' => 'civicrm_report_instance', 'entity' => 'ReportInstance', 'bao' => 'CRM_Report_BAO_ReportInstance', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form.php index 97f2e78d0f3..722c35f3236 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form.php +++ b/profiles/civicrm_starterkit/modules/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 @@ -1060,6 +1068,15 @@ public function setParams($params) { $this->_params = $params; } + /** + * Getter for $_params. + * + * @return void|array $params + */ + public function getParams() { + return $this->_params; + } + /** * Setter for $_id. * @@ -1114,6 +1131,36 @@ public function getDefaultValues() { 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']); + } + } + + /** + * Create a temporary table. + * + * This function creates a table AND adds the details to the developer tab & $this->>temporary tables. + * + * @todo improve presentation on the developer tab since CREATE TEMPORARY is removed. + * + * @param string $identifier + * @param $sql + * @param bool $isTrueTemporary + * Is this a mysql temporary table or temporary in a less technical sense. + * + * @return string + */ + public function createTemporaryTable($identifier, $sql, $isTrueTemporary = TRUE) { + $this->addToDeveloperTab($sql); + $name = CRM_Utils_SQL_TempTable::build()->setUtf8(TRUE)->setDurable($isTrueTemporary)->createWithQuery($sql)->getName(); + $this->temporaryTables[$identifier] = ['temporary' => $isTrueTemporary, 'name' => $name]; + return $name; + } + /** * Add columns to report. */ @@ -3672,7 +3719,7 @@ public function buildGroupTempTable() { 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 @@ -4383,70 +4430,51 @@ public function addAddressFields($groupBy = TRUE, $orderBy = FALSE, $filters = T /** * Do AlterDisplay processing on Address Fields. + * If there are multiple address field values then + * on basis of provided separator the code values are translated into respective labels * * @param array $row * @param array $rows * @param int $rowNum * @param string $baseUrl * @param string $linkText + * @param string $separator * * @return bool */ - public function alterDisplayAddressFields(&$row, &$rows, &$rowNum, $baseUrl, $linkText) { + public function alterDisplayAddressFields(&$row, &$rows, &$rowNum, $baseUrl, $linkText, $separator = ',') { $criteriaQueryParams = CRM_Report_Utils_Report::getPreviewCriteriaQueryParams($this->_defaults, $this->_params); $entryFound = FALSE; - // handle country - if (array_key_exists('civicrm_address_country_id', $row)) { - if ($value = $row['civicrm_address_country_id']) { - $rows[$rowNum]['civicrm_address_country_id'] = CRM_Core_PseudoConstant::country($value, FALSE); - if ($baseUrl) { - $url = CRM_Report_Utils_Report::getNextUrl($baseUrl, - "reset=1&force=1&{$criteriaQueryParams}&" . - "country_id_op=in&country_id_value={$value}", - $this->_absoluteUrl, $this->_id - ); - $rows[$rowNum]['civicrm_address_country_id_link'] = $url; - $rows[$rowNum]['civicrm_address_country_id_hover'] = ts("%1 for this country.", - array(1 => $linkText) - ); - } - } - - $entryFound = TRUE; - } - if (array_key_exists('civicrm_address_county_id', $row)) { - if ($value = $row['civicrm_address_county_id']) { - $rows[$rowNum]['civicrm_address_county_id'] = CRM_Core_PseudoConstant::county($value, FALSE); - if ($baseUrl) { - $url = CRM_Report_Utils_Report::getNextUrl($baseUrl, - "reset=1&force=1&{$criteriaQueryParams}&" . - "county_id_op=in&county_id_value={$value}", - $this->_absoluteUrl, $this->_id - ); - $rows[$rowNum]['civicrm_address_county_id_link'] = $url; - $rows[$rowNum]['civicrm_address_county_id_hover'] = ts("%1 for this county.", - array(1 => $linkText) - ); - } - } - $entryFound = TRUE; - } - // handle state province - if (array_key_exists('civicrm_address_state_province_id', $row)) { - if ($value = $row['civicrm_address_state_province_id']) { - $rows[$rowNum]['civicrm_address_state_province_id'] = CRM_Core_PseudoConstant::stateProvince($value, FALSE); - if ($baseUrl) { - $url = CRM_Report_Utils_Report::getNextUrl($baseUrl, - "reset=1&force=1&{$criteriaQueryParams}&state_province_id_op=in&state_province_id_value={$value}", - $this->_absoluteUrl, $this->_id - ); - $rows[$rowNum]['civicrm_address_state_province_id_link'] = $url; - $rows[$rowNum]['civicrm_address_state_province_id_hover'] = ts("%1 for this state.", - array(1 => $linkText) - ); + $columnMap = array( + 'civicrm_address_country_id' => 'country', + 'civicrm_address_county_id' => 'county', + 'civicrm_address_state_province_id' => 'stateProvince', + ); + foreach ($columnMap as $fieldName => $fnName) { + if (array_key_exists($fieldName, $row)) { + if ($values = $row[$fieldName]) { + $values = (array) explode($separator, $values); + $rows[$rowNum][$fieldName] = []; + $addressField = $fnName == 'stateProvince' ? 'state' : $fnName; + foreach ($values as $value) { + $rows[$rowNum][$fieldName][] = CRM_Core_PseudoConstant::$fnName($value); + } + $rows[$rowNum][$fieldName] = implode($separator, $rows[$rowNum][$fieldName]); + if ($baseUrl) { + $url = CRM_Report_Utils_Report::getNextUrl($baseUrl, + sprintf("reset=1&force=1&%s&%s_op=in&%s_value=%s", + $criteriaQueryParams, + str_replace('civicrm_address_', '', $fieldName), + str_replace('civicrm_address_', '', $fieldName), + implode(',', $values) + ), $this->_absoluteUrl, $this->_id + ); + $rows[$rowNum]["{$fieldName}_link"] = $url; + $rows[$rowNum]["{$fieldName}_hover"] = ts("%1 for this %2.", array(1 => $linkText, 2 => $addressField)); + } + $entryFound = TRUE; } } - $entryFound = TRUE; } return $entryFound; @@ -5357,12 +5385,27 @@ protected function getContactColumns($options = array()) { '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'), 'options' => CRM_Contact_BAO_Contact::buildOptions('gender_id'), 'operatorType' => CRM_Report_Form::OP_MULTISELECT, - 'alter_display' => 'alterGenderID', 'is_fields' => TRUE, 'is_filters' => TRUE, ), @@ -5375,7 +5418,7 @@ protected function getContactColumns($options = array()) { ), 'age' => array( 'title' => $options['prefix_label'] . ts('Age'), - 'dbAlias' => 'TIMESTAMPDIFF(YEAR, ' . $tableAlias . '.birth_date, CURDATE())', + 'dbAlias' => 'TIMESTAMPDIFF(YEAR, ' . $tableAlias . '_civireport.birth_date, CURDATE())', 'type' => CRM_Utils_Type::T_INT, 'is_fields' => TRUE, ), @@ -5448,6 +5491,16 @@ protected function getAddressColumns($options = array()) { 'type' => 1, 'is_fields' => TRUE, ), + $options['prefix'] . 'odd_street_number' => array( + 'title' => ts('Odd / Even Street Number'), + 'name' => 'odd_street_number', + 'type' => CRM_Utils_Type::T_INT, + 'no_display' => TRUE, + 'required' => TRUE, + 'dbAlias' => '(address_civireport.street_number % 2)', + 'is_fields' => TRUE, + 'is_order_bys' => TRUE, + ), $options['prefix'] . 'street_name' => array( 'name' => 'street_name', 'title' => ts($options['prefix_label'] . 'Street Name'), diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Activity.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Activity.php index b38b95d24d3..9b55f415fa9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Activity.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Activity.php @@ -91,6 +91,9 @@ public function __construct() { $condition = " AND ( v.component_id IS NULL {$include} )"; $this->activityTypes = CRM_Core_OptionGroup::values('activity_type', FALSE, FALSE, FALSE, $condition); asort($this->activityTypes); + + // @todo split the 3 different contact tables into their own array items. + // this will massively simplify the needs of this report. $this->_columns = array( 'civicrm_contact' => array( 'dao' => 'CRM_Contact_DAO_Contact', @@ -394,7 +397,12 @@ public function addressFields($orderBy = FALSE) { /** * Build select clause. * - * @param null $recordType + * @todo get rid of $recordType param. It's only because 3 separate contact tables + * are mis-declared as one that we need it. + * + * @param null $recordType deprecated + * Parameter to hack around the bad decision made in construct to misrepresent + * different tables as the same table. */ public function select($recordType = 'target') { if (!array_key_exists("contact_{$recordType}", $this->_params['fields']) && @@ -416,6 +424,7 @@ public function select($recordType = 'target') { $removeKeys = array(); if ($recordType == 'target') { + // @todo - fix up the way the tables are declared in construct & remove this. foreach ($this->_selectClauses as $key => $clause) { if (strstr($clause, 'civicrm_contact_assignee.') || strstr($clause, 'civicrm_contact_source.') || @@ -430,13 +439,15 @@ public function select($recordType = 'target') { } } elseif ($recordType == 'assignee') { + // @todo - fix up the way the tables are declared in construct & remove this. foreach ($this->_selectClauses as $key => $clause) { if (strstr($clause, 'civicrm_contact_target.') || strstr($clause, 'civicrm_contact_source.') || strstr($clause, 'civicrm_email_target.') || strstr($clause, 'civicrm_email_source.') || strstr($clause, 'civicrm_phone_target.') || - strstr($clause, 'civicrm_phone_source.') + strstr($clause, 'civicrm_phone_source.') || + strstr($clause, 'civicrm_address_') ) { $removeKeys[] = $key; unset($this->_selectClauses[$key]); @@ -444,13 +455,15 @@ public function select($recordType = 'target') { } } elseif ($recordType == 'source') { + // @todo - fix up the way the tables are declared in construct & remove this. foreach ($this->_selectClauses as $key => $clause) { if (strstr($clause, 'civicrm_contact_target.') || strstr($clause, 'civicrm_contact_assignee.') || strstr($clause, 'civicrm_email_target.') || strstr($clause, 'civicrm_email_assignee.') || strstr($clause, 'civicrm_phone_target.') || - strstr($clause, 'civicrm_phone_assignee.') + strstr($clause, 'civicrm_phone_assignee.') || + strstr($clause, 'civicrm_address_') ) { $removeKeys[] = $key; unset($this->_selectClauses[$key]); @@ -460,6 +473,7 @@ public function select($recordType = 'target') { elseif ($recordType == 'final') { $this->_selectClauses = $this->_selectAliasesTotal; foreach ($this->_selectClauses as $key => $clause) { + // @todo - fix up the way the tables are declared in construct & remove this. if (strstr($clause, 'civicrm_contact_contact_target') || strstr($clause, 'civicrm_contact_contact_assignee') || strstr($clause, 'civicrm_contact_contact_source') || @@ -468,9 +482,10 @@ public function select($recordType = 'target') { strstr($clause, 'civicrm_email_contact_source_email') || strstr($clause, 'civicrm_email_contact_assignee_email') || strstr($clause, 'civicrm_email_contact_target_email') || - strstr($clause, 'civicrm_phone_contact_target_phone') + strstr($clause, 'civicrm_phone_contact_target_phone') || + strstr($clause, 'civicrm_address_') ) { - $this->_selectClauses[$key] = "GROUP_CONCAT($clause SEPARATOR ';') as $clause"; + $this->_selectClauses[$key] = "GROUP_CONCAT(DISTINCT $clause SEPARATOR ';') as $clause"; } } } @@ -480,7 +495,7 @@ public function select($recordType = 'target') { unset($this->_selectAliases[$key]); } - if ($recordType != 'final') { + if ($recordType == 'target') { foreach ($this->_columns['civicrm_address']['order_bys'] as $fieldName => $field) { $orderByFld = $this->_columns['civicrm_address']['order_bys'][$fieldName]; $fldInfo = $this->_columns['civicrm_address']['fields'][$fieldName]; @@ -496,91 +511,35 @@ public function select($recordType = 'target') { /** * Build from clause. - * - * @param string $recordType + * @todo remove this function & declare the 3 contact tables separately */ - public function from($recordType = 'target') { + public function from() { $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate'); - $activityTypeId = CRM_Core_DAO::getFieldValue("CRM_Core_DAO_OptionGroup", 'activity_type', 'id', 'name'); - $assigneeID = CRM_Utils_Array::key('Activity Assignees', $activityContacts); $targetID = CRM_Utils_Array::key('Activity Targets', $activityContacts); - $sourceID = CRM_Utils_Array::key('Activity Source', $activityContacts); - - if ($recordType == 'target') { - $this->_from = " - FROM civicrm_activity {$this->_aliases['civicrm_activity']} - INNER JOIN civicrm_activity_contact {$this->_aliases['civicrm_activity_contact']} - ON {$this->_aliases['civicrm_activity']}.id = {$this->_aliases['civicrm_activity_contact']}.activity_id AND - {$this->_aliases['civicrm_activity_contact']}.record_type_id = {$targetID} - INNER JOIN civicrm_contact civicrm_contact_target - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_contact_target.id - {$this->_aclFrom}"; - - if ($this->isTableSelected('civicrm_email')) { - $this->_from .= " - LEFT JOIN civicrm_email civicrm_email_target - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_email_target.contact_id AND - civicrm_email_target.is_primary = 1"; - } - - if ($this->isTableSelected('civicrm_phone')) { - $this->_from .= " - LEFT JOIN civicrm_phone civicrm_phone_target - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_phone_target.contact_id AND - civicrm_phone_target.is_primary = 1 "; - } - $this->_aliases['civicrm_contact'] = 'civicrm_contact_target'; - } - - if ($recordType == 'assignee') { - $this->_from = " - FROM civicrm_activity {$this->_aliases['civicrm_activity']} - INNER JOIN civicrm_activity_contact {$this->_aliases['civicrm_activity_contact']} - ON {$this->_aliases['civicrm_activity']}.id = {$this->_aliases['civicrm_activity_contact']}.activity_id AND - {$this->_aliases['civicrm_activity_contact']}.record_type_id = {$assigneeID} - INNER JOIN civicrm_contact civicrm_contact_assignee - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_contact_assignee.id - {$this->_aclFrom}"; - if ($this->isTableSelected('civicrm_email')) { - $this->_from .= " - LEFT JOIN civicrm_email civicrm_email_assignee - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_email_assignee.contact_id AND - civicrm_email_assignee.is_primary = 1"; - } - if ($this->isTableSelected('civicrm_phone')) { - $this->_from .= " - LEFT JOIN civicrm_phone civicrm_phone_assignee - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_phone_assignee.contact_id AND - civicrm_phone_assignee.is_primary = 1 "; - } - $this->_aliases['civicrm_contact'] = 'civicrm_contact_assignee'; + $this->_from = " + FROM civicrm_activity {$this->_aliases['civicrm_activity']} + INNER JOIN civicrm_activity_contact {$this->_aliases['civicrm_activity_contact']} + ON {$this->_aliases['civicrm_activity']}.id = {$this->_aliases['civicrm_activity_contact']}.activity_id AND + {$this->_aliases['civicrm_activity_contact']}.record_type_id = {$targetID} + INNER JOIN civicrm_contact civicrm_contact_target + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_contact_target.id + {$this->_aclFrom}"; + + if ($this->isTableSelected('civicrm_email')) { + $this->_from .= " + LEFT JOIN civicrm_email civicrm_email_target + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_email_target.contact_id AND + civicrm_email_target.is_primary = 1"; } - if ($recordType == 'source') { - $this->_from = " - FROM civicrm_activity {$this->_aliases['civicrm_activity']} - INNER JOIN civicrm_activity_contact {$this->_aliases['civicrm_activity_contact']} - ON {$this->_aliases['civicrm_activity']}.id = {$this->_aliases['civicrm_activity_contact']}.activity_id AND - {$this->_aliases['civicrm_activity_contact']}.record_type_id = {$sourceID} - INNER JOIN civicrm_contact civicrm_contact_source - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_contact_source.id - {$this->_aclFrom}"; - - if ($this->isTableSelected('civicrm_email')) { - $this->_from .= " - LEFT JOIN civicrm_email civicrm_email_source - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_email_source.contact_id AND - civicrm_email_source.is_primary = 1"; - } - if ($this->isTableSelected('civicrm_phone')) { - $this->_from .= " - LEFT JOIN civicrm_phone civicrm_phone_source - ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_phone_source.contact_id AND - civicrm_phone_source.is_primary = 1 "; - } - $this->_aliases['civicrm_contact'] = 'civicrm_contact_source'; + if ($this->isTableSelected('civicrm_phone')) { + $this->_from .= " + LEFT JOIN civicrm_phone civicrm_phone_target + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_phone_target.contact_id AND + civicrm_phone_target.is_primary = 1 "; } + $this->_aliases['civicrm_contact'] = 'civicrm_contact_target'; $this->joinAddressFromContact(); } @@ -588,6 +547,9 @@ public function from($recordType = 'target') { /** * Build where clause. * + * @todo get rid of $recordType param. It's only because 3 separate contact tables + * are mis-declared as one that we need it. + * * @param string $recordType */ public function where($recordType = NULL) { @@ -731,7 +693,7 @@ public function add2group($groupID) { $new_having = ' addtogroup_contact_id'; $having = str_ireplace(' civicrm_contact_contact_target_id', $new_having, $this->_having); $query = "$select -FROM civireport_activity_temp_target tar +FROM {$this->temporaryTables['activity_temp_table']} tar GROUP BY civicrm_activity_id $having {$this->_orderBy}"; $select = 'AS addtogroup_contact_id'; $query = str_ireplace('AS civicrm_contact_contact_target_id', $select, $query); @@ -805,21 +767,21 @@ public function buildQuery($applyLimit = TRUE) { } } + // @todo - all this temp table stuff is here because pre 4.4 the activity contact + // form did not exist. + // Fixing the way the construct method declares them will make all this redundant. // 1. fill temp table with target results $this->buildACLClause(array('civicrm_contact_target')); $this->select('target'); - $this->from('target'); + $this->from(); $this->customDataFrom(); $this->where('target'); - $insertCols = implode(',', $this->_selectAliases); - $tempQuery = "CREATE TEMPORARY TABLE civireport_activity_temp_target {$this->_databaseAttributes} AS -{$this->_select} {$this->_from} {$this->_where} "; - $this->executeReportQuery($tempQuery); + $tempTableName = $this->createTemporaryTable('activity_temp_table', "{$this->_select} {$this->_from} {$this->_where}"); // 2. add new columns to hold assignee and source results // fixme: add when required $tempQuery = " - ALTER TABLE civireport_activity_temp_target + ALTER TABLE $tempTableName MODIFY COLUMN civicrm_contact_contact_target_id VARCHAR(128), ADD COLUMN civicrm_contact_contact_assignee VARCHAR(128), ADD COLUMN civicrm_contact_contact_source VARCHAR(128), @@ -834,11 +796,12 @@ public function buildQuery($applyLimit = TRUE) { // 3. fill temp table with assignee results $this->buildACLClause(array('civicrm_contact_assignee')); $this->select('assignee'); - $this->from('assignee'); + $this->buildAssigneeFrom(); + $this->customDataFrom(); $this->where('assignee'); $insertCols = implode(',', $this->_selectAliases); - $tempQuery = "INSERT INTO civireport_activity_temp_target ({$insertCols}) + $tempQuery = "INSERT INTO $tempTableName ({$insertCols}) {$this->_select} {$this->_from} {$this->_where}"; $this->executeReportQuery($tempQuery); @@ -846,11 +809,11 @@ public function buildQuery($applyLimit = TRUE) { // 4. fill temp table with source results $this->buildACLClause(array('civicrm_contact_source')); $this->select('source'); - $this->from('source'); + $this->buildSourceFrom(); $this->customDataFrom(); $this->where('source'); $insertCols = implode(',', $this->_selectAliases); - $tempQuery = "INSERT INTO civireport_activity_temp_target ({$insertCols}) + $tempQuery = "INSERT INTO $tempTableName ({$insertCols}) {$this->_select} {$this->_from} {$this->_where}"; $this->executeReportQuery($tempQuery); @@ -865,7 +828,7 @@ public function buildQuery($applyLimit = TRUE) { $this->orderBy(); foreach ($this->_sections as $alias => $section) { if (!empty($section) && $section['name'] == 'activity_date_time') { - $this->alterSectionHeaderForDateTime('civireport_activity_temp_target', $section['tplField']); + $this->alterSectionHeaderForDateTime($tempTableName, $section['tplField']); } } @@ -882,7 +845,7 @@ public function buildQuery($applyLimit = TRUE) { } $sql = "{$this->_select} - FROM civireport_activity_temp_target tar + FROM $tempTableName tar INNER JOIN civicrm_activity {$this->_aliases['civicrm_activity']} ON {$this->_aliases['civicrm_activity']}.id = tar.civicrm_activity_id INNER JOIN civicrm_activity_contact {$this->_aliases['civicrm_activity_contact']} ON {$this->_aliases['civicrm_activity_contact']}.activity_id = {$this->_aliases['civicrm_activity']}.id AND {$this->_aliases['civicrm_activity_contact']}.record_type_id = {$sourceID} @@ -930,7 +893,10 @@ public function alterDisplay(&$rows) { $activityStatus = CRM_Core_PseudoConstant::activityStatus(); $priority = CRM_Core_PseudoConstant::get('CRM_Activity_DAO_Activity', 'priority_id'); $viewLinks = FALSE; - $context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this, FALSE, 'report'); + + // Would we ever want to retrieve from the form controller?? + $form = $this->noController ? NULL : $this; + $context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $form, FALSE, 'report'); $actUrl = ''; if (CRM_Core_Permission::check('access CiviCRM')) { @@ -1088,7 +1054,7 @@ public function alterDisplay(&$rows) { } } - $entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, 'activity', 'List all activities for this ') ? TRUE : $entryFound; + $entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, 'activity', 'List all activities for this', ';') ? TRUE : $entryFound; if (!$entryFound) { break; @@ -1114,7 +1080,7 @@ public function sectionTotals() { $this->_select = CRM_Contact_BAO_Query::appendAnyValueToSelect($ifnulls, $sectionAliases); $query = $this->_select . - ", count(DISTINCT civicrm_activity_id) as ct from civireport_activity_temp_target group by " . + ", count(DISTINCT civicrm_activity_id) as ct from {$this->temporaryTables['activity_temp_table']} group by " . implode(", ", $sectionAliases); // initialize array of total counts @@ -1147,4 +1113,72 @@ public function sectionTotals() { } } + /** + * @todo remove this function & declare the 3 contact tables separately + * + * (Currently the construct method incorrectly melds them - this is an interim + * refactor in order to get this under ReportTemplateTests) + */ + protected function buildAssigneeFrom() { + $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate'); + $assigneeID = CRM_Utils_Array::key('Activity Assignees', $activityContacts); + $this->_from = " + FROM civicrm_activity {$this->_aliases['civicrm_activity']} + INNER JOIN civicrm_activity_contact {$this->_aliases['civicrm_activity_contact']} + ON {$this->_aliases['civicrm_activity']}.id = {$this->_aliases['civicrm_activity_contact']}.activity_id AND + {$this->_aliases['civicrm_activity_contact']}.record_type_id = {$assigneeID} + INNER JOIN civicrm_contact civicrm_contact_assignee + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_contact_assignee.id + {$this->_aclFrom}"; + + if ($this->isTableSelected('civicrm_email')) { + $this->_from .= " + LEFT JOIN civicrm_email civicrm_email_assignee + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_email_assignee.contact_id AND + civicrm_email_assignee.is_primary = 1"; + } + if ($this->isTableSelected('civicrm_phone')) { + $this->_from .= " + LEFT JOIN civicrm_phone civicrm_phone_assignee + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_phone_assignee.contact_id AND + civicrm_phone_assignee.is_primary = 1 "; + } + $this->_aliases['civicrm_contact'] = 'civicrm_contact_assignee'; + $this->joinAddressFromContact(); + } + + /** + * @todo remove this function & declare the 3 contact tables separately + * + * (Currently the construct method incorrectly melds them - this is an interim + * refactor in order to get this under ReportTemplateTests) + */ + protected function buildSourceFrom() { + $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate'); + $sourceID = CRM_Utils_Array::key('Activity Source', $activityContacts); + $this->_from = " + FROM civicrm_activity {$this->_aliases['civicrm_activity']} + INNER JOIN civicrm_activity_contact {$this->_aliases['civicrm_activity_contact']} + ON {$this->_aliases['civicrm_activity']}.id = {$this->_aliases['civicrm_activity_contact']}.activity_id AND + {$this->_aliases['civicrm_activity_contact']}.record_type_id = {$sourceID} + INNER JOIN civicrm_contact civicrm_contact_source + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_contact_source.id + {$this->_aclFrom}"; + + if ($this->isTableSelected('civicrm_email')) { + $this->_from .= " + LEFT JOIN civicrm_email civicrm_email_source + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_email_source.contact_id AND + civicrm_email_source.is_primary = 1"; + } + if ($this->isTableSelected('civicrm_phone')) { + $this->_from .= " + LEFT JOIN civicrm_phone civicrm_phone_source + ON {$this->_aliases['civicrm_activity_contact']}.contact_id = civicrm_phone_source.contact_id AND + civicrm_phone_source.is_primary = 1 "; + } + $this->_aliases['civicrm_contact'] = 'civicrm_contact_source'; + $this->joinAddressFromContact(); + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contact/Relationship.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contact/Relationship.php index dd20fba0ea8..0e34078cdfb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contact/Relationship.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contact/Relationship.php @@ -256,10 +256,10 @@ public function __construct() { 'title' => ts('Relationship End Date'), ), 'is_permission_a_b' => array( - 'title' => ts('Is permission A over B?'), + 'title' => ts('Permission A has to access B'), ), 'is_permission_b_a' => array( - 'title' => ts('Is permission B over A?'), + 'title' => ts('Permission B has to access A'), ), 'description' => array( 'title' => ts('Description'), @@ -310,22 +310,14 @@ public function __construct() { ), 'is_permission_a_b' => array( 'title' => ts('Does contact A have permission over contact B?'), - 'operatorType' => CRM_Report_Form::OP_SELECT, - 'options' => array( - '' => ts('- Any -'), - 1 => ts('Yes'), - 0 => ts('No'), - ), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contact_BAO_Relationship::buildOptions('is_permission_a_b'), 'type' => CRM_Utils_Type::T_INT, ), 'is_permission_b_a' => array( 'title' => ts('Does contact B have permission over contact A?'), - 'operatorType' => CRM_Report_Form::OP_SELECT, - 'options' => array( - '' => ts('- Any -'), - 1 => ts('Yes'), - 0 => ts('No'), - ), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contact_BAO_Relationship::buildOptions('is_permission_b_a'), 'type' => CRM_Utils_Type::T_INT, ), ), @@ -792,6 +784,19 @@ public function alterDisplay(&$rows) { $entryFound = TRUE; } + // Handle permissioned relationships + if (array_key_exists('civicrm_relationship_is_permission_a_b', $row)) { + $rows[$rowNum]['civicrm_relationship_is_permission_a_b'] + = ts(self::permissionedRelationship($row['civicrm_relationship_is_permission_a_b'])); + $entryFound = TRUE; + } + + if (array_key_exists('civicrm_relationship_is_permission_b_a', $row)) { + $rows[$rowNum]['civicrm_relationship_is_permission_b_a'] + = ts(self::permissionedRelationship($row['civicrm_relationship_is_permission_b_a'])); + $entryFound = TRUE; + } + // skip looking further in rows, if first row itself doesn't // have the column we need if (!$entryFound) { @@ -800,6 +805,19 @@ public function alterDisplay(&$rows) { } } + /** + * Convert values to permissioned relationship descriptions + * @param [int] $key + * @return [string] + */ + public static function permissionedRelationship($key) { + static $lookup; + if (!$lookup) { + $lookup = CRM_Contact_BAO_Relationship::buildOptions("is_permission_a_b"); + }; + return CRM_Utils_Array::value($key, $lookup); + } + /** * @param $valid bool - set to 1 if we are looking for a valid relationship, 0 if not * diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Bookkeeping.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Bookkeeping.php index 088e2d730e8..8e4c25d1e3d 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Bookkeeping.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Bookkeeping.php @@ -301,6 +301,10 @@ public function __construct() { '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 @@ public function __construct() { '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/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Detail.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Detail.php index 7910fa1453c..2369127d1ae 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Detail.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function statistics(&$rows) { } /** - * This function appears to have been overrriden for the purposes of facilitating soft credits in the report. - * - * 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. + * Build the report query. * - * 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. + * @param bool $applyLimit * - * 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 @@ public function postProcess() { 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 @@ public function postProcess() { $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 @@ public function beginPostProcessCommon() { * 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 @@ public function alterDisplay(&$rows) { 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 @@ public function alterDisplay(&$rows) { $dao->civicrm_contact_id); $string = $string . ($string ? $separator : '') . "{$dao->civicrm_contact_sort_name} " . - 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; } @@ -796,6 +823,12 @@ public function sectionTotals() { // pull section aliases out of $this->_sections $sectionAliases = array_keys($this->_sections); + // hack alert - but it's tested so go forth & make pretty, or whack the new mole that popped up with gay abandon. + if (in_array('civicrm_contribution_total_amount', $this->_selectAliases)) { + $keyToHack = array_search('civicrm_contribution_total_amount', $this->_selectAliases); + $this->_selectAliases[$keyToHack] = 'civicrm_contribution_total_amount_sum'; + } + $ifnulls = array(); foreach (array_merge($sectionAliases, $this->_selectAliases) as $alias) { $ifnulls[] = "ifnull($alias, '') as $alias"; @@ -810,10 +843,10 @@ public function sectionTotals() { $addtotals = ''; - if (array_search("civicrm_contribution_total_amount", $this->_selectAliases) !== + if (array_search("civicrm_contribution_total_amount_sum", $this->_selectAliases) !== FALSE ) { - $addtotals = ", sum(civicrm_contribution_total_amount) as sumcontribs"; + $addtotals = ", sum(civicrm_contribution_total_amount_sum) as sumcontribs"; $showsumcontribs = TRUE; } @@ -899,6 +932,10 @@ public function softCreditFrom() { {$this->_aclFrom} "; + //Join temp table if report is filtered by group. This is specific to 'notin' operator and covered in unit test(ref dev/core#212) + if (!empty($this->_params['gid_op']) && $this->_params['gid_op'] == 'notin') { + $this->joinGroupTempTable('civicrm_contact', 'id', $this->_aliases['civicrm_contact']); + } $this->appendAdditionalFromJoins(); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/History.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/History.php index 117ece48366..ac23fd4bfd4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/History.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/History.php @@ -867,6 +867,8 @@ public function alterDisplay(&$rows) { $entryFound = TRUE; } + $entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, NULL, NULL) ? TRUE : $entryFound; + } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Lybunt.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Lybunt.php index 0fb7f093a5d..e5aeb0fcba2 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Lybunt.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Lybunt.php @@ -567,7 +567,7 @@ public function beginPostProcessCommon() { // @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Repeat.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Repeat.php index 6b87d4ebfdb..471b076dab1 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Repeat.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Contribute/Repeat.php @@ -400,6 +400,11 @@ public function from() { .{$this->contributionJoinTableColumn} LEFT JOIN $this->tempTableRepeat2 {$this->_aliases['civicrm_contribution']}2 ON {$this->groupByTableAlias}.$fromCol = {$this->_aliases['civicrm_contribution']}2.{$this->contributionJoinTableColumn}"; + + //Join temp table if report is filtered by group. This is specific to 'notin' operator and covered in unit test(ref dev/core#212) + if (!empty($this->_params['gid_op']) && $this->_params['gid_op'] == 'notin') { + $this->joinGroupTempTable('civicrm_contact', 'id', $this->_aliases['civicrm_contact']); + } } /** @@ -825,6 +830,7 @@ public function statistics(&$rows) { public function postProcess() { $this->beginPostProcess(); + $this->buildGroupTempTable(); $this->select(); $this->from(); $this->where(); diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Register.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Register.php index 094520df38e..4f7bd460289 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Register.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Register.php @@ -191,8 +191,7 @@ public function postProcess() { // get the submitted form values. $params = $this->controller->exportValues($this->_name); - $groupParams = array('name' => ('report_template')); - $optionValue = CRM_Core_OptionValue::addOptionValue($params, $groupParams, $this->_action, $this->_id); + $optionValue = CRM_Core_OptionValue::addOptionValue($params, 'report_template', $this->_action, $this->_id); CRM_Core_Session::setStatus(ts('The %1 \'%2\' has been saved.', array( 1 => 'Report Template', 2 => $optionValue->label, diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Walklist/Walklist.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Walklist/Walklist.php index 04644112776..5727e3b5ec5 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Walklist/Walklist.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Form/Walklist/Walklist.php @@ -76,55 +76,6 @@ public function __construct() { ), ), ), - 'civicrm_address' => array( - 'dao' => 'CRM_Core_DAO_Address', - 'fields' => array( - 'street_number' => array( - 'title' => ts('Street Number'), - 'type' => 1, - ), - 'street_address' => NULL, - 'city' => NULL, - 'postal_code' => NULL, - 'state_province_id' => array( - 'title' => ts('State/Province'), - 'default' => TRUE, - 'type' => CRM_Utils_Type::T_INT, - ), - 'country_id' => array( - 'title' => ts('Country'), - ), - 'odd_street_number' => array( - 'title' => ts('Odd/Even Street Number'), - 'type' => CRM_Utils_Type::T_INT, - 'no_display' => TRUE, - 'required' => TRUE, - 'dbAlias' => '(address_civireport.street_number % 2)', - ), - ), - 'filters' => array( - 'street_number' => array( - 'title' => ts('Street Number'), - 'type' => 1, - 'name' => 'street_number', - ), - 'street_address' => NULL, - 'city' => NULL, - ), - 'order_bys' => array( - 'street_name' => array( - 'title' => ts('Street Name'), - ), - 'street_number' => array( - 'title' => ts('Street Number'), - ), - 'odd_street_number' => array( - 'title' => ts('Odd/Even Street Number'), - 'dbAlias' => 'civicrm_address_odd_street_number', - ), - ), - 'grouping' => 'location-fields', - ), 'civicrm_email' => array( 'dao' => 'CRM_Core_DAO_Email', 'fields' => array('email' => array('default' => TRUE)), @@ -135,7 +86,7 @@ public function __construct() { 'fields' => array('phone' => NULL), 'grouping' => 'location-fields', ), - ); + ) + $this->getAddressColumns(array('group_bys' => FALSE)); parent::__construct(); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Info.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Info.php index 83cc3bbbe5d..8d8c429a5de 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Info.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Report/Info.php @@ -87,6 +87,10 @@ public function getPermissions($getAllUnconditionally = FALSE, $descriptions = F 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/profiles/civicrm_starterkit/modules/civicrm/CRM/SMS/DAO/Provider.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/SMS/DAO/Provider.php index 990f58c84ea..fc295eef0d5 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/SMS/DAO/Provider.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/SMS/DAO/Provider.php @@ -237,6 +237,7 @@ public static function &fields() { 'name' => 'is_default', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('SMS Provider is Default?'), + 'default' => '0', 'table_name' => 'civicrm_sms_provider', 'entity' => 'Provider', 'bao' => 'CRM_SMS_BAO_Provider', @@ -249,6 +250,7 @@ public static function &fields() { 'name' => 'is_active', 'type' => CRM_Utils_Type::T_BOOLEAN, 'title' => ts('SMS Provider is Active?'), + 'default' => '0', 'table_name' => 'civicrm_sms_provider', 'entity' => 'Provider', 'bao' => 'CRM_SMS_BAO_Provider', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/SMS/Form/Provider.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/SMS/Form/Provider.php index c5496c2b571..f6aace80402 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/SMS/Form/Provider.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/SMS/Form/Provider.php @@ -159,7 +159,7 @@ public function setDefaultValues() { */ 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Tag/Form/Tag.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Tag/Form/Tag.php index ed75c7017c3..765c71eb774 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Tag/Form/Tag.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Tag/Form/Tag.php @@ -113,7 +113,7 @@ public function buildQuickForm() { * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/UF/Form/AdvanceSetting.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/UF/Form/AdvanceSetting.php index 617940127f9..01db60e8b9e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/UF/Form/AdvanceSetting.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/UF/Form/AdvanceSetting.php @@ -50,6 +50,8 @@ public static function buildAdvanceSetting(&$form) { $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/profiles/civicrm_starterkit/modules/civicrm/CRM/UF/Form/Group.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/UF/Form/Group.php index 9adc2485035..9fcb8341766 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/UF/Form/Group.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/UF/Form/Group.php @@ -270,6 +270,7 @@ public function setDefaultValues() { } } else { + $defaults['add_cancel_button'] = 1; $defaults['is_active'] = 1; $defaults['is_map'] = 0; $defaults['is_update_dupe'] = 0; diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Form.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Form.php index b2b09f08f2a..7a6ad203c86 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Form.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Form.php @@ -769,6 +769,7 @@ public function setPreUpgradeMessage(&$preUpgradeMessage, $currentVer, $latestVe 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/Base.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/Base.php index 1cbf1cc3f57..b180904d70a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/Base.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/Base.php @@ -183,6 +183,18 @@ public static function addColumn($ctx, $table, $column, $properties, $localizabl 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/General.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/General.php index 9f0ebe39c92..5737fbd5e47 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/General.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/General.php @@ -117,13 +117,41 @@ public static function setPreUpgradeMessage(&$preUpgradeMessage, $currentVer, $l } } + /** + * 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; + } + $messagesHtml = array_map(function($k, $v) { + return sprintf("

  • %s - %s
  • ", htmlentities($k), htmlentities($v)); + }, array_keys($messages), $messages); + + $message .= '
    ' . 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. Click here for detailed instructions. %2", array( + 1 => 'http://wiki.civicrm.org/confluence/display/CRMDOC/Message+Templates#MessageTemplates-UpgradesandCustomizedSystemWorkflowTemplates', + 2 => '
      ' . implode('', $messagesHtml) . '
    ', + )); + + $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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/MessageTemplates.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/MessageTemplates.php new file mode 100644 index 00000000000..7b11e199df9 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/MessageTemplates.php @@ -0,0 +1,155 @@ +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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveFive.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveFive.php new file mode 100644 index 00000000000..c1d96128a19 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveFive.php @@ -0,0 +1,87 @@ +' . ts('A new permission, "%1", has been added. This permission is now used to control access to the Manage Tags screen.', array(1 => ts('manage tags'))) . '

    '; + // } + } + + /** + * 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) { + // Example: Generate a post-upgrade message. + // if ($rev == '5.12.34') { + // $postUpgradeMessage .= '

    ' . 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'."); + // } + } + + /* + * 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveFour.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveFour.php new file mode 100644 index 00000000000..ff9091a1f41 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveFour.php @@ -0,0 +1,139 @@ +' . ts('A new permission, "%1", has been added. This permission is now used to control access to the Manage Tags screen.', array(1 => ts('manage tags'))) . '

    '; + // } + } + + /** + * 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) { + if ($rev == '5.4.alpha1') { + $postUpgradeMessage .= '

    ' . ts('A new permission, "%1", has been added. It is not granted by default. If your users create reports, you may wish to review their permissions.', array(1 => ts('save Report Criteria'))) . '

    '; + } + // Example: Generate a post-upgrade message. + // if ($rev == '5.12.34') { + // $postUpgradeMessage .= '

    ' . 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveOne.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveOne.php index d97a2b6233f..1f3113896b3 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveOne.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveOne.php @@ -42,7 +42,7 @@ class CRM_Upgrade_Incremental_php_FiveOne extends CRM_Upgrade_Incremental_Base { public function setPreUpgradeMessage(&$preUpgradeMessage, $rev, $currentVer = NULL) { // Example: Generate a pre-upgrade message. // if ($rev == '5.12.34') { - // $preUpgradeMessage .= '

    ' . 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')) . '

    '; + // $preUpgradeMessage .= '

    ' . ts('A new permission, "%1", has been added. This permission is now used to control access to the Manage Tags screen.', array(1 => ts('manage tags'))) . '

    '; // } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveThree.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveThree.php index f4b75d32db4..5eebd3bc64b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveThree.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveThree.php @@ -50,7 +50,7 @@ public function setPreUpgradeMessage(&$preUpgradeMessage, $rev, $currentVer = NU } // Example: Generate a pre-upgrade message. // if ($rev == '5.12.34') { - // $preUpgradeMessage .= '

    ' . 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')) . '

    '; + // $preUpgradeMessage .= '

    ' . ts('A new permission, "%1", has been added. This permission is now used to control access to the Manage Tags screen.', array(1 => ts('manage tags'))) . '

    '; // } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveZero.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveZero.php index 30551dd604b..e77abab3318 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveZero.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FiveZero.php @@ -43,7 +43,7 @@ class CRM_Upgrade_Incremental_php_FiveZero extends CRM_Upgrade_Incremental_Base public function setPreUpgradeMessage(&$preUpgradeMessage, $rev, $currentVer = NULL) { // Example: Generate a pre-upgrade message. //if ($rev == '5.12.34') { - // $preUpgradeMessage .= '

    ' . 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')) . '

    '; + // $preUpgradeMessage .= '

    ' . ts('A new permission, "%1", has been added. This permission is now used to control access to the Manage Tags screen.', array(1 => ts('manage tags'))) . '

    '; //} } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FourSeven.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FourSeven.php index 0f667a7c919..2fa54ff1d45 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FourSeven.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FourSeven.php @@ -1140,7 +1140,7 @@ public static function alterIndexAndTypeForImageURL() { * @return bool */ public static function addMailingTemplateType() { - if (!CRM_Core_DAO::checkFieldExists('civicrm_mailing', 'template_type', FALSE)) { + if (!CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_mailing', 'template_type', FALSE)) { CRM_Core_DAO::executeQuery(' ALTER TABLE civicrm_mailing ADD COLUMN `template_type` varchar(64) NOT NULL DEFAULT \'traditional\' COMMENT \'The language/processing system used for email templates.\', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FourThree.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FourThree.php index e224cac48a7..f1030b54617 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FourThree.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/FourThree.php @@ -277,7 +277,7 @@ public function upgrade_4_3_alpha1($rev) { */ public function upgrade_4_3_alpha2($rev) { //CRM-11847 - $isColumnPresent = CRM_Core_DAO::checkFieldExists('civicrm_dedupe_rule_group', 'is_default'); + $isColumnPresent = CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_dedupe_rule_group', 'is_default'); if ($isColumnPresent) { CRM_Core_DAO::executeQuery('ALTER TABLE civicrm_dedupe_rule_group DROP COLUMN is_default'); } @@ -300,7 +300,7 @@ public function upgrade_4_3_beta2($rev) { // CRM-12002 if ( CRM_Core_DAO::checkTableExists('log_civicrm_line_item') && - CRM_Core_DAO::checkFieldExists('log_civicrm_line_item', 'label') + CRM_Core_BAO_SchemaHandler::checkIfFieldExists('log_civicrm_line_item', 'label') ) { CRM_Core_DAO::executeQuery('ALTER TABLE `log_civicrm_line_item` CHANGE `label` `label` VARCHAR(255) NULL DEFAULT NULL'); } @@ -335,7 +335,7 @@ public function upgrade_4_3_beta5($rev) { // CRM-12205 if ( CRM_Core_DAO::checkTableExists('log_civicrm_financial_trxn') && - CRM_Core_DAO::checkFieldExists('log_civicrm_financial_trxn', 'trxn_id') + CRM_Core_BAO_SchemaHandler::checkIfFieldExists('log_civicrm_financial_trxn', 'trxn_id') ) { CRM_Core_DAO::executeQuery('ALTER TABLE `log_civicrm_financial_trxn` CHANGE `trxn_id` `trxn_id` VARCHAR(255) NULL DEFAULT NULL'); } @@ -343,7 +343,7 @@ public function upgrade_4_3_beta5($rev) { // CRM-12367 - add this column to single lingual sites only $upgrade = new CRM_Upgrade_Form(); if (!$upgrade->multilingual && - !CRM_Core_DAO::checkFieldExists('civicrm_premiums', 'premiums_nothankyou_label') + !CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_premiums', 'premiums_nothankyou_label') ) { $query = " ALTER TABLE civicrm_premiums @@ -925,7 +925,7 @@ public function task_4_3_alpha1_checkDBConstraints() { } } // check if column contact_id is present or not in civicrm_financial_account - $fieldExists = CRM_Core_DAO::checkFieldExists('civicrm_financial_account', 'contact_id', FALSE); + $fieldExists = CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_financial_account', 'contact_id', FALSE); if (!$fieldExists) { $query = " ALTER TABLE civicrm_financial_account diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/Template.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/Template.php index 7fe34e90de7..5c15aa15755 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/Template.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/php/Template.php @@ -48,7 +48,7 @@ class CRM_Upgrade_Incremental_php_ extends CRM_Upgrad public function setPreUpgradeMessage(&$preUpgradeMessage, $rev, $currentVer = NULL) { // Example: Generate a pre-upgrade message. // if ($rev == '5.12.34') { - // $preUpgradeMessage .= '

    ' . 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')) . '

    '; + // $preUpgradeMessage .= '

    ' . ts('A new permission, "%1", has been added. This permission is now used to control access to the Manage Tags screen.', array(1 => ts('manage tags'))) . '

    '; // } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.3.0.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.3.0.mysql.tpl deleted file mode 100644 index b557c2542ba..00000000000 --- a/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.3.1.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.3.1.mysql.tpl deleted file mode 100644 index 2e2057d6dd9..00000000000 --- a/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.3.alpha1.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.3.alpha1.mysql.tpl index 8a3d52ecc1e..7c05f9ecaac 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.3.alpha1.mysql.tpl +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.4.alpha1.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.4.alpha1.mysql.tpl new file mode 100644 index 00000000000..e47d96cf832 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.4.beta1.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.4.beta1.mysql.tpl new file mode 100644 index 00000000000..b486eda69d1 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.0.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.0.mysql.tpl new file mode 100644 index 00000000000..b27009b9dc6 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.0.mysql.tpl @@ -0,0 +1 @@ +{* file to handle db changes in 5.5.0 during upgrade *} diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.1.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.1.mysql.tpl new file mode 100644 index 00000000000..dd195d7382e --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.1.mysql.tpl @@ -0,0 +1 @@ +{* file to handle db changes in 5.5.1 during upgrade *} diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.alpha1.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.alpha1.mysql.tpl new file mode 100644 index 00000000000..504659d4374 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.alpha1.mysql.tpl @@ -0,0 +1,18 @@ +{* file to handle db changes in 5.5.alpha1 during upgrade *} +#https://lab.civicrm.org/dev/core/issues/228 +UPDATE civicrm_option_group SET is_active = 0 WHERE is_active IS NULL; +ALTER TABLE civicrm_option_group MODIFY COLUMN is_active TINYINT(4) NOT NULL DEFAULT 1 COMMENT 'Is this option group active?'; +UPDATE civicrm_option_group SET is_locked = 0 WHERE is_locked IS NULL; +ALTER TABLE civicrm_option_group MODIFY COLUMN is_locked TINYINT(4) NOT NULL DEFAULT 1 COMMENT 'A lock to remove the ability to add new options via the UI.'; +#is_reserved already has a default so is effectively required but let's be explicit. +UPDATE civicrm_option_group SET `is_reserved` = 0 WHERE `is_reserved` IS NULL; +ALTER TABLE civicrm_option_group MODIFY COLUMN `is_reserved` tinyint(4) NOT NULL DEFAULT 1 COMMENT 'Is this a predefined system option group (i.e. it can not be deleted)?'; + +#https://lab.civicrm.org/dev/core/issues/155 +{* Fix is_reserved flag on civicrm_option_group table *} +UPDATE civicrm_option_group AS cog INNER JOIN civicrm_custom_field AS ccf +ON cog.id = ccf.option_group_id +SET cog.is_reserved = 0 WHERE cog.is_active = 1 AND ccf.is_active = 1; +UPDATE civicrm_option_group SET is_reserved = 1 WHERE name='environment'; + +UPDATE civicrm_navigation SET url = 'civicrm/admin/options?action=browse&reset=1' WHERE name = 'Dropdown Options' AND domain_id = {$domainID}; diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.beta1.mysql.tpl b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.beta1.mysql.tpl new file mode 100644 index 00000000000..e2172c7ed06 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Upgrade/Incremental/sql/5.5.beta1.mysql.tpl @@ -0,0 +1,6 @@ +{* file to handle db changes in 5.5.beta1 during upgrade *} + +UPDATE civicrm_action_schedule SET start_action_date = 'start_date' WHERE start_action_date = 'event_start_date'; +UPDATE civicrm_action_schedule SET start_action_date = 'end_date' WHERE start_action_date = 'event_end_date'; +UPDATE civicrm_action_schedule SET start_action_date = 'join_date' WHERE start_action_date = 'membership_join_date'; +UPDATE civicrm_action_schedule SET start_action_date = 'end_date' WHERE start_action_date = 'membership_end_date'; diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Array.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Array.php index a9d6cdd07ef..73b4dab45a2 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Array.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Array.php @@ -866,9 +866,13 @@ public static function subset($array, $keys) { * This is necessary to preserve sort order when sending an array through json_encode. * * @param array $associative + * Ex: ['foo' => 'bar']. * @param string $keyName + * Ex: 'key'. * @param string $valueName + * Ex: 'value'. * @return array + * Ex: [0 => ['key' => 'foo', 'value' => 'bar']]. */ public static function makeNonAssociative($associative, $keyName = 'key', $valueName = 'value') { $output = array(); @@ -965,38 +969,58 @@ public static function extend(&$array, $other) { } /** - * Get a single value from an array-tre. + * Get a single value from an array-tree. * - * @param array $arr - * Ex: array('foo'=>array('bar'=>123)). - * @param array $pathParts - * Ex: array('foo',bar'). - * @return mixed|NULL + * @param array $values + * Ex: ['foo' => ['bar' => 123]]. + * @param array $path + * Ex: ['foo', 'bar']. + * @param mixed $default + * @return mixed * Ex 123. */ - public static function pathGet($arr, $pathParts) { - $r = $arr; - foreach ($pathParts as $part) { - if (!isset($r[$part])) { - return NULL; + public static function pathGet($values, $path, $default = NULL) { + foreach ($path as $key) { + if (!is_array($values) || !isset($values[$key])) { + return $default; + } + $values = $values[$key]; + } + return $values; + } + + /** + * Check if a key isset which may be several layers deep. + * + * This is a helper for when the calling function does not know how many layers deep + * the path array is so cannot easily check. + * + * @param array $values + * @param array $path + * @return bool + */ + public static function pathIsset($values, $path) { + foreach ($path as $key) { + if (!is_array($values) || !isset($values[$key])) { + return FALSE; } - $r = $r[$part]; + $values = $values[$key]; } - return $r; + return TRUE; } /** * Set a single value in an array tree. * - * @param array $arr - * Ex: array('foo'=>array('bar'=>123)). + * @param array $values + * Ex: ['foo' => ['bar' => 123]]. * @param array $pathParts - * Ex: array('foo',bar'). + * Ex: ['foo', 'bar']. * @param $value * Ex: 456. */ - public static function pathSet(&$arr, $pathParts, $value) { - $r = &$arr; + public static function pathSet(&$values, $pathParts, $value) { + $r = &$values; $last = array_pop($pathParts); foreach ($pathParts as $part) { if (!isset($r[$part])) { @@ -1017,19 +1041,10 @@ public static function pathSet(&$arr, $pathParts, $value) { * @param string $valueField * Ex: 'value'. * @return array - * Ex: array( - * 0 => array('key' => 'foo', 'value' => 'bar') - * ). + * @deprecated */ public static function toKeyValueRows($array, $keyField = 'key', $valueField = 'value') { - $result = array(); - foreach ($array as $key => $value) { - $result[] = array( - $keyField => $key, - $valueField => $value, - ); - } - return $result; + return self::makeNonAssociative($array, $keyField, $valueField); } /** @@ -1152,77 +1167,4 @@ public static function findInTree($search, $tree, $field = 'id') { return NULL; } - /** - * Check if a key isset which may be several layers deep. - * - * This is a helper for when the calling function does not know how many layers deep the - * path array is so cannot easily check. - * - * @param array $array - * @param array $path - * @return bool - * @throws \CRM_Core_Exception - */ - public static function recursiveIsset($array, $path) { - foreach ($path as $key) { - if (!is_array($array) || !isset($array[$key])) { - return FALSE; - } - $array = $array[$key]; - } - return TRUE; - } - - /** - * Check if a key isset which may be several layers deep. - * - * This is a helper for when the calling function does not know how many layers deep the - * path array is so cannot easily check. - * - * @param array $array - * @param array $path - * An array of keys - e.g [0, 'bob', 8] where we want to check if $array[0]['bob'][8] - * @param mixed $default - * Value to return if not found. - * @return bool - * @throws \CRM_Core_Exception - */ - public static function recursiveValue($array, $path, $default = NULL) { - foreach ($path as $key) { - if (!is_array($array) || !isset($array[$key])) { - return $default; - } - $array = $array[$key]; - } - return $array; - } - - /** - * Append the value to the array using the key provided. - * - * e.g if value is 'llama' & path is [0, 'email', 'location'] result will be - * [0 => ['email' => ['location' => 'llama']] - * - * @param $path - * @param $value - * @param array $source - * - * @return array - */ - public static function recursiveBuild($path, $value, $source = []) { - $arrayKey = array_shift($path); - // Recurse through array keys - if ($path) { - if (!isset($source[$arrayKey])) { - $source[$arrayKey] = []; - } - $source[$arrayKey] = self::recursiveBuild($path, $value, $source[$arrayKey]); - } - // Final iteration - else { - $source[$arrayKey] = $value; - } - return $source; - } - } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache.php index 00a92888fc8..0ce1128c83b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache.php +++ b/profiles/civicrm_starterkit/modules/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. * @@ -64,25 +67,11 @@ public function __construct(&$config) { */ public static function &singleton() { if (self::$_singleton === NULL) { - $className = 'ArrayCache'; // default to ArrayCache for now - - // Maintain backward compatibility for now. - // Setting CIVICRM_USE_MEMCACHE or CIVICRM_USE_ARRAYCACHE will - // override the CIVICRM_DB_CACHE_CLASS setting. - // Going forward, CIVICRM_USE_xxxCACHE should be deprecated. - if (defined('CIVICRM_USE_MEMCACHE') && CIVICRM_USE_MEMCACHE) { - $className = 'Memcache'; - } - elseif (defined('CIVICRM_USE_ARRAYCACHE') && CIVICRM_USE_ARRAYCACHE) { - $className = 'ArrayCache'; - } - elseif (defined('CIVICRM_DB_CACHE_CLASS') && CIVICRM_DB_CACHE_CLASS) { - $className = CIVICRM_DB_CACHE_CLASS; - } - + $className = self::getCacheDriver(); // 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; @@ -171,6 +160,9 @@ public static function getCacheSettings($cachePlugin) { * @param array $params * Array with keys: * - name: string, unique symbolic name. + * For a naming convention, use `snake_case` or `CamelCase` to maximize + * portability/cleanliness. Any other punctuation or whitespace + * should function correctly, but it can be harder to inspect/debug. * - type: array|string, list of acceptable cache types, in order of preference. * - prefetch: bool, whether to prefetch all data in cache (if possible). * @return CRM_Utils_Cache_Interface @@ -180,13 +172,17 @@ public static function getCacheSettings($cachePlugin) { public static function create($params = array()) { $types = (array) $params['type']; + if (!empty($params['name'])) { + $params['name'] = CRM_Core_BAO_Cache::cleanKey($params['name']); + } + foreach ($types as $type) { switch ($type) { case '*memory*': 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 +206,56 @@ public static function create($params = array()) { 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; + } + + /** + * @return string + * Ex: 'ArrayCache', 'Memcache', 'Redis'. + */ + public static function getCacheDriver() { + $className = 'ArrayCache'; // default to ArrayCache for now + + // Maintain backward compatibility for now. + // Setting CIVICRM_USE_MEMCACHE or CIVICRM_USE_ARRAYCACHE will + // override the CIVICRM_DB_CACHE_CLASS setting. + // Going forward, CIVICRM_USE_xxxCACHE should be deprecated. + if (defined('CIVICRM_USE_MEMCACHE') && CIVICRM_USE_MEMCACHE) { + $className = 'Memcache'; + return $className; + } + elseif (defined('CIVICRM_USE_ARRAYCACHE') && CIVICRM_USE_ARRAYCACHE) { + $className = 'ArrayCache'; + return $className; + } + elseif (defined('CIVICRM_DB_CACHE_CLASS') && CIVICRM_DB_CACHE_CLASS) { + $className = CIVICRM_DB_CACHE_CLASS; + return $className; + } + return $className; + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/APCcache.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/APCcache.php index e696aa19c3d..26fa86b2416 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/APCcache.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function __construct(&$config) { /** * @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 @@ public function set($key, &$value) { /** * @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 @@ public function get($key) { * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/ArrayCache.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/ArrayCache.php index b2272fa1eb6..287c6a3e5ce 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/ArrayCache.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/CacheException.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/CacheException.php new file mode 100644 index 00000000000..cdb0821da2f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/CacheException.php @@ -0,0 +1,36 @@ + 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/InvalidArgumentException.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/InvalidArgumentException.php new file mode 100644 index 00000000000..5d9747d9164 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/InvalidArgumentException.php @@ -0,0 +1,36 @@ +_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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/Memcached.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/Memcached.php index e80b1af4b35..546c5b933b9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/Memcached.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function __construct($config) { /** * @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 @@ public function &get($key) { * @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 @@ public function delete($key) { * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NaiveHasTrait.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NaiveHasTrait.php new file mode 100644 index 00000000000..78ecc678876 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NaiveHasTrait.php @@ -0,0 +1,49 @@ +get($key, NULL) === NULL); + $hasDefaultB = ($this->get($key, 123) === 123); + return !($hasDefaultA && $hasDefaultB); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NaiveMultipleTrait.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NaiveMultipleTrait.php new file mode 100644 index 00000000000..d7ead9fc94b --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NaiveMultipleTrait.php @@ -0,0 +1,121 @@ + 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NoCache.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NoCache.php index 66c6b74cff4..3c1d9b94452 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/NoCache.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function __construct($config) { /** * @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 @@ public function flush() { return FALSE; } + public function clear() { + return $this->flush(); + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/Redis.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/Redis.php index 07279d530de..ad1d07cd3a3 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/Redis.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/Redis.php @@ -33,25 +33,15 @@ * */ 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; const DEFAULT_PREFIX = ''; - /** - * The host name of the redisd server - * - * @var string - */ - protected $_host = self::DEFAULT_HOST; - - /** - * The port on which to connect on - * - * @var int - */ - protected $_port = self::DEFAULT_PORT; - /** * The default timeout to use * @@ -77,6 +67,34 @@ class CRM_Utils_Cache_Redis implements CRM_Utils_Cache_Interface { */ protected $_cache; + /** + * Create a connection. If a connection already exists, re-use it. + * + * @param array $config + * @return Redis + */ + public static function connect($config) { + $host = isset($config['host']) ? $config['host'] : self::DEFAULT_HOST; + $port = isset($config['port']) ? $config['port'] : self::DEFAULT_PORT; + $pass = CRM_Utils_Constant::value('CIVICRM_DB_CACHE_PASSWORD'); // Ugh. + $id = implode(':', ['connect', $host, $port /* $pass is constant */]); + if (!isset(Civi::$statics[__CLASS__][$id])) { + // Ideally, we'd track the connection in the service-container, but the + // cache connection is boot-critical. + $redis = new Redis(); + if (!$redis->connect($host, $port)) { + // dont use fatal here since we can go in an infinite loop + echo 'Could not connect to redisd server'; + CRM_Utils_System::civiExit(); + } + if ($pass) { + $redis->auth($pass); + } + Civi::$statics[__CLASS__][$id] = $redis; + } + return Civi::$statics[__CLASS__][$id]; + } + /** * Constructor * @@ -86,12 +104,6 @@ class CRM_Utils_Cache_Redis implements CRM_Utils_Cache_Interface { * @return \CRM_Utils_Cache_Redis */ public function __construct($config) { - if (isset($config['host'])) { - $this->_host = $config['host']; - } - if (isset($config['port'])) { - $this->_port = $config['port']; - } if (isset($config['timeout'])) { $this->_timeout = $config['timeout']; } @@ -99,25 +111,31 @@ public function __construct($config) { $this->_prefix = $config['prefix']; } - $this->_cache = new Redis(); - if (!$this->_cache->connect($this->_host, $this->_port)) { - // dont use fatal here since we can go in an infinite loop - echo 'Could not connect to redisd server'; - CRM_Utils_System::civiExit(); - } - $this->_cache->auth(CIVICRM_DB_CACHE_PASSWORD); + $this->_cache = self::connect($config); } /** * @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 @@ public function set($key, &$value) { /** * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/SerializeCache.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/SerializeCache.php index 6164694c3e5..fdf0eb6d47b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/SerializeCache.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function fileName($key) { /** * @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 @@ public function get($key) { /** * @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), "fileName($key), "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 @@ public function flush($key = NULL) { closedir($handle); unset($this->_cache); $this->_cache = array(); + return TRUE; + } + + public function clear() { + return $this->flush(); } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/SqlGroup.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/SqlGroup.php index 6f3f45f7927..beaf5fd1ddb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Cache/SqlGroup.php +++ b/profiles/civicrm_starterkit/modules/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 @@ public function __construct($config) { else { $this->componentID = NULL; } - $this->frontCache = array(); + $this->valueCache = array(); if (CRM_Utils_Array::value('prefetch', $config, TRUE)) { $this->prefetch(); } @@ -89,22 +107,80 @@ public function __construct($config) { /** * @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."); + } + + if (is_int($ttl) && $ttl <= 0) { + return $this->delete($key); + } + + $dataExists = CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM {$this->table} WHERE {$this->where($key)}"); + $expires = round(microtime(1)) + CRM_Utils_Date::convertCacheTtl($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 +190,60 @@ public function get($key) { * @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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Check.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Check.php index 24fd07062a1..57d15a741ff 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Check.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Check.php @@ -223,7 +223,7 @@ public static function checkAll($max = FALSE) { break; } - Civi::settings()->set('systemStatusCheckResult', $maxSeverity); + Civi::cache('checks')->set('systemStatusCheckResult', $maxSeverity); return ($max) ? $maxSeverity : $messages; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Check/Component/Env.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Check/Component/Env.php index df866185edc..a253716e42f 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Check/Component/Env.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Check/Component/Env.php @@ -910,7 +910,7 @@ public function checkResourceUrl() { $arrowUrl = CRM_Core_Config::singleton()->userFrameworkResourceURL . 'packages/jquery/css/images/arrow.png'; $headers = get_headers($arrowUrl); $fileExists = stripos($headers[0], "200 OK") ? 1 : 0; - if (!$fileExists) { + if ($fileExists === FALSE) { $messages[] = new CRM_Utils_Check_Message( __FUNCTION__, ts('The Resource URL is not set correctly. Please set the CiviCRM Resource URL.', diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Date.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Date.php index 3d4570d7e19..74aa2e35cae 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Date.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Date.php @@ -695,6 +695,58 @@ public static function isDate(&$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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/DeprecatedUtils.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/DeprecatedUtils.php index 00afe4332eb..7c726a3ee08 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/DeprecatedUtils.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/GeocodeProvider.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/GeocodeProvider.php index f87c8eac115..e11a2eb67ad 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/GeocodeProvider.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/GeocodeProvider.php @@ -84,7 +84,7 @@ public static function getUsableClassName() { // or extend a base class. While we identify and implement a geocoding // abstraction library (rather than continue to roll our own), we settle for // this check. - if (!method_exists($provider, 'format')) { + if (!method_exists($provider, 'format') && $provider !== FALSE) { Civi::log()->error('Configured geocoder is invalid, must provide a format method', ['geocode_class' => $provider]); $provider = FALSE; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Hook.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Hook.php index 612a26c67a6..da50eb035d7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Hook.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Hook.php @@ -2418,4 +2418,57 @@ public static function alterEntityRefParams(&$params, $formName) { ); } + /** + * 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' + ); + } + + /** + * This hook is called before and after constructing mail recipients. + * Allows user to alter filter and/or search query to fetch mail recipients + * + * @param CRM_Mailing_DAO_Mailing $mailingObject + * @param array $criteria + * A list of SQL criteria; you can add/remove/replace/modify criteria. + * Array(string $name => CRM_Utils_SQL_Select $criterion). + * Ex: array('do_not_email' => CRM_Utils_SQL_Select::fragment()->where("$contact.do_not_email = 0")). + * @param string $context + * Ex: 'pre', 'post' + * @return mixed + */ + public static function alterMailingRecipients(&$mailingObject, &$criteria, $context) { + return self::singleton()->invoke(array('mailingObject', 'params', 'context'), + $mailingObject, $criteria, $context, + self::$_nullObject, self::$_nullObject, self::$_nullObject, + 'civicrm_alterMailingRecipients' + ); + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Migrate/Import.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Migrate/Import.php index 1736130bf0a..dc9e24419ae 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Migrate/Import.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Migrate/Import.php @@ -242,6 +242,15 @@ public function customGroups(&$xml, &$idMap) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Money.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Money.php index 28ecf6aafe9..59168cb606a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Money.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Money.php @@ -154,4 +154,36 @@ public static function subtractCurrencies($leftOp, $rightOp, $currency) { } } + /** + * Tests if two currency values are equal, taking into account the currency's + * precision, so that if the difference between the two values is less than + * one more order of magnitude for the precision, then the values are + * considered as equal. So, if the currency has precision of 2 decimal + * points, a difference of more than 0.001 will cause the values to be + * considered as different. Anything less than 0.001 will be considered as + * equal. + * + * Eg. + * + * 1.2312 == 1.2319 with a currency precision of 2 decimal points + * 1.2310 != 1.2320 with a currency precision of 2 decimal points + * 1.3000 != 1.2000 with a currency precision of 2 decimal points + * + * @param $value1 + * @param $value2 + * @param $currency + * + * @return bool + */ + public static function equals($value1, $value2, $currency) { + $precision = 1 / pow(10, self::getCurrencyPrecision($currency) + 1); + $difference = self::subtractCurrencies($value1, $value2, $currency); + + if (abs($difference) > $precision) { + return FALSE; + } + + return TRUE; + } + } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/ReCAPTCHA.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/ReCAPTCHA.php index de4021bb1f9..80861599309 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/ReCAPTCHA.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/ReCAPTCHA.php @@ -62,6 +62,29 @@ public static function &singleton() { return self::$_singleton; } + + /** + * Check if reCaptcha settings is avilable to add on form. + */ + public static function hasSettingsAvailable() { + $config = CRM_Core_Config::singleton(); + if ($config->recaptchaPublicKey == NULL || $config->recaptchaPublicKey == "") { + return FALSE; + } + return TRUE; + } + + /** + * Check if reCaptcha has to be added on form forcefully. + */ + public static function hasToAddForcefully() { + $config = CRM_Core_Config::singleton(); + if (!$config->forceRecaptcha) { + return FALSE; + } + return TRUE; + } + /** * Add element to form. * diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Rule.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Rule.php index 61a467451fd..2bc78c243e2 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Rule.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/Rule.php @@ -157,7 +157,7 @@ public static function mysqlOrderBy($str) { // at all, so we split and loop over. $parts = explode(',', $str); foreach ($parts as $part) { - if (!preg_match('/^((`[\w-]{1,64}`|[\w-]{1,64})\.)?(`[\w-]{1,64}`|[\w-]{1,64})( (asc|desc))?$/i', trim($part))) { + if (!preg_match('/^((`[\w-]{1,64}`|[\w-]{1,64})\.)*(`[\w-]{1,64}`|[\w-]{1,64})( (asc|desc))?$/i', trim($part))) { return FALSE; } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/BaseParamQuery.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/BaseParamQuery.php new file mode 100644 index 00000000000..82fb91cfc36 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/BaseParamQuery.php @@ -0,0 +1,232 @@ +strict = $strict; + return $this; + } + + /** + * Given a string like "field_name = @value", replace "@value" with an escaped SQL string + * + * @param string $expr SQL expression + * @param null|array $args a list of values to insert into the SQL expression; keys are prefix-coded: + * prefix '@' => escape SQL + * prefix '#' => literal number, skip escaping but do validation + * prefix '!' => literal, skip escaping and validation + * if a value is an array, then it will be imploded + * + * PHP NULL's will be treated as SQL NULL's. The PHP string "null" will be treated as a string. + * + * @param string $activeMode + * + * @return string + */ + public function interpolate($expr, $args, $activeMode = self::INTERPOLATE_INPUT) { + if ($args === NULL) { + return $expr; + } + else { + if ($this->mode === self::INTERPOLATE_AUTO) { + $this->mode = $activeMode; + } + elseif ($activeMode !== $this->mode) { + throw new RuntimeException("Cannot mix interpolation modes."); + } + + $select = $this; + return preg_replace_callback('/([#!@])([a-zA-Z0-9_]+)/', function($m) use ($select, $args) { + if (isset($args[$m[2]])) { + $values = $args[$m[2]]; + } + elseif (isset($args[$m[1] . $m[2]])) { + // Backward compat. Keys in $args look like "#myNumber" or "@myString". + $values = $args[$m[1] . $m[2]]; + } + elseif ($select->strict) { + throw new CRM_Core_Exception('Cannot build query. Variable "' . $m[1] . $m[2] . '" is unknown.'); + } + else { + // Unrecognized variables are ignored. Mitigate risk of accidents. + return $m[0]; + } + $values = is_array($values) ? $values : array($values); + switch ($m[1]) { + case '@': + $parts = array_map(array($select, 'escapeString'), $values); + return implode(', ', $parts); + + // TODO: ensure all uses of this un-escaped literal are safe + case '!': + return implode(', ', $values); + + case '#': + foreach ($values as $valueKey => $value) { + if ($value === NULL) { + $values[$valueKey] = 'NULL'; + } + elseif (!is_numeric($value)) { + //throw new API_Exception("Failed encoding non-numeric value" . var_export(array($m[0] => $values), TRUE)); + throw new CRM_Core_Exception("Failed encoding non-numeric value (" . $m[0] . ")"); + } + } + return implode(', ', $values); + + default: + throw new CRM_Core_Exception("Unrecognized prefix"); + } + }, $expr); + } + } + + /** + * @param string|NULL $value + * @return string + * SQL expression, e.g. "it\'s great" (with-quotes) or NULL (without-quotes) + */ + public function escapeString($value) { + return $value === NULL ? 'NULL' : '"' . CRM_Core_DAO::escapeString($value) . '"'; + } + + /** + * Set one (or multiple) parameters to interpolate into the query. + * + * @param array|string $keys + * Key name, or an array of key-value pairs. + * @param null|mixed $value + * The new value of the parameter. + * Values may be strings, ints, or arrays thereof -- provided that the + * SQL query uses appropriate prefix (e.g. "@", "!", "#"). + * @return $this + */ + public function param($keys, $value = NULL) { + if ($this->mode === self::INTERPOLATE_AUTO) { + $this->mode = self::INTERPOLATE_OUTPUT; + } + elseif ($this->mode !== self::INTERPOLATE_OUTPUT) { + throw new RuntimeException("Select::param() only makes sense when interpolating on output."); + } + + if (is_array($keys)) { + foreach ($keys as $k => $v) { + $this->params[$k] = $v; + } + } + else { + $this->params[$keys] = $value; + } + return $this; + } + + /** + * Has an offset been set. + * + * @param string $offset + * + * @return bool + */ + public function offsetExists($offset) { + return isset($this->params[$offset]); + } + + /** + * Get the value of a SQL parameter. + * + * @code + * $select['cid'] = 123; + * $select->where('contact.id = #cid'); + * echo $select['cid']; + * @endCode + * + * @param string $offset + * @return mixed + * @see param() + * @see ArrayAccess::offsetGet + */ + public function offsetGet($offset) { + return $this->params[$offset]; + } + + /** + * Set the value of a SQL parameter. + * + * @code + * $select['cid'] = 123; + * $select->where('contact.id = #cid'); + * echo $select['cid']; + * @endCode + * + * @param string $offset + * @param mixed $value + * The new value of the parameter. + * Values may be strings, ints, or arrays thereof -- provided that the + * SQL query uses appropriate prefix (e.g. "@", "!", "#"). + * @see param() + * @see ArrayAccess::offsetSet + */ + public function offsetSet($offset, $value) { + $this->param($offset, $value); + } + + /** + * Unset the value of a SQL parameter. + * + * @param string $offset + * @see param() + * @see ArrayAccess::offsetUnset + */ + public function offsetUnset($offset) { + unset($this->params[$offset]); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/Delete.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/Delete.php new file mode 100644 index 00000000000..b646d3b3e20 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/Delete.php @@ -0,0 +1,269 @@ +where('activity_type_id = #type', array('type' => 234)) + * ->where('status_id IN (#statuses)', array('statuses' => array(1,2,3)) + * ->where('subject like @subj', array('subj' => '%hello%')) + * ->where('!dynamicColumn = 1', array('dynamicColumn' => 'coalesce(is_active,0)')) + * ->where('!column = @value', array( + * 'column' => $customField->column_name, + * 'value' => $form['foo'] + * )) + * echo $del->toSQL(); + * @endcode + * + * Design principles: + * - Portable + * - No knowledge of the underlying SQL API (except for escaping -- CRM_Core_DAO::escapeString) + * - No knowledge of the underlying data model + * - SQL clauses correspond to PHP functions ($select->where("foo_id=123")) + * - Variable escaping is concise and controllable based on prefixes, eg + * - similar to Drupal's t() + * - use "@varname" to insert the escaped value + * - use "!varname" to insert raw (unescaped) values + * - use "#varname" to insert a numerical value (these are validated but not escaped) + * - to disable any preprocessing, simply omit the variable list + * - control characters (@!#) are mandatory in expressions but optional in arg-keys + * - Variables may be individual values or arrays; arrays are imploded with commas + * - Conditionals are AND'd; if you need OR's, do it yourself + * - Use classes/functions with documentation (rather than undocumented array-trees) + * - For any given string, interpolation is only performed once. After an interpolation, + * a string may never again be subjected to interpolation. + * + * The "interpolate-once" principle can be enforced by either interpolating on input + * xor output. The notations for input and output interpolation are a bit different, + * and they may not be mixed. + * + * @code + * // Interpolate on input. Set params when using them. + * $select->where('activity_type_id = #type', array( + * 'type' => 234, + * )); + * + * // Interpolate on output. Set params independently. + * $select + * ->where('activity_type_id = #type') + * ->param('type', 234), + * @endcode + * + * @package CRM + * @copyright CiviCRM LLC (c) 2004-2018 + */ +class CRM_Utils_SQL_Delete extends CRM_Utils_SQL_BaseParamQuery { + + private $from; + private $wheres = array(); + + /** + * Create a new DELETE query. + * + * @param string $from + * Table-name and optional alias. + * @param array $options + * @return CRM_Utils_SQL_Delete + */ + public static function from($from, $options = array()) { + return new self($from, $options); + } + + /** + * Create a new DELETE query. + * + * @param string $from + * Table-name and optional alias. + * @param array $options + */ + public function __construct($from, $options = array()) { + $this->from = $from; + $this->mode = isset($options['mode']) ? $options['mode'] : self::INTERPOLATE_AUTO; + } + + /** + * Make a new copy of this query. + * + * @return CRM_Utils_SQL_Delete + */ + public function copy() { + return clone $this; + } + + /** + * Merge something or other. + * + * @param CRM_Utils_SQL_Delete $other + * @param array|NULL $parts + * ex: 'wheres' + * @return CRM_Utils_SQL_Delete + */ + public function merge($other, $parts = NULL) { + if ($other === NULL) { + return $this; + } + + if ($this->mode === self::INTERPOLATE_AUTO) { + $this->mode = $other->mode; + } + elseif ($other->mode === self::INTERPOLATE_AUTO) { + // Noop. + } + elseif ($this->mode !== $other->mode) { + // Mixing modes will lead to someone getting an expected substitution. + throw new RuntimeException("Cannot merge queries that use different interpolation modes ({$this->mode} vs {$other->mode})."); + } + + $arrayFields = array('wheres', 'params'); + foreach ($arrayFields as $f) { + if ($parts === NULL || in_array($f, $parts)) { + $this->{$f} = array_merge($this->{$f}, $other->{$f}); + } + } + + $flatFields = array('from'); + foreach ($flatFields as $f) { + if ($parts === NULL || in_array($f, $parts)) { + if ($other->{$f} !== NULL) { + $this->{$f} = $other->{$f}; + } + } + } + + return $this; + } + + /** + * Limit results by adding extra condition(s) to the WHERE clause + * + * @param string|array $exprs list of SQL expressions + * @param null|array $args use NULL to disable interpolation; use an array of variables to enable + * @return CRM_Utils_SQL_Delete + */ + public function where($exprs, $args = NULL) { + $exprs = (array) $exprs; + foreach ($exprs as $expr) { + $evaluatedExpr = $this->interpolate($expr, $args); + $this->wheres[$evaluatedExpr] = $evaluatedExpr; + } + return $this; + } + + /** + * Set one (or multiple) parameters to interpolate into the query. + * + * @param array|string $keys + * Key name, or an array of key-value pairs. + * @param null|mixed $value + * The new value of the parameter. + * Values may be strings, ints, or arrays thereof -- provided that the + * SQL query uses appropriate prefix (e.g. "@", "!", "#"). + * @return \CRM_Utils_SQL_Delete + */ + public function param($keys, $value = NULL) { + // Why bother with an override? To provide better type-hinting in `@return`. + return parent::param($keys, $value); + } + + /** + * @param array|NULL $parts + * List of fields to check (e.g. 'wheres'). + * Defaults to all. + * @return bool + */ + public function isEmpty($parts = NULL) { + $empty = TRUE; + $fields = array( + 'from', + 'wheres', + ); + if ($parts !== NULL) { + $fields = array_intersect($fields, $parts); + } + foreach ($fields as $field) { + if (!empty($this->{$field})) { + $empty = FALSE; + } + } + return $empty; + } + + /** + * @return string + * SQL statement + */ + public function toSQL() { + $sql = 'DELETE '; + + if ($this->from !== NULL) { + $sql .= 'FROM ' . $this->from . "\n"; + } + if ($this->wheres) { + $sql .= 'WHERE (' . implode(') AND (', $this->wheres) . ")\n"; + } + if ($this->mode === self::INTERPOLATE_OUTPUT) { + $sql = $this->interpolate($sql, $this->params, self::INTERPOLATE_OUTPUT); + } + return $sql; + } + + /** + * Execute the query. + * + * To examine the results, use a function like `fetch()`, `fetchAll()`, + * `fetchValue()`, or `fetchMap()`. + * + * @param string|NULL $daoName + * The return object should be an instance of this class. + * Ex: 'CRM_Contact_BAO_Contact'. + * @param bool $i18nRewrite + * If the system has multilingual features, should the field/table + * names be rewritten? + * @return CRM_Core_DAO + * @see CRM_Core_DAO::executeQuery + * @see CRM_Core_I18n_Schema::rewriteQuery + */ + public function execute($daoName = NULL, $i18nRewrite = TRUE) { + // Don't pass through $params. toSQL() handles interpolation. + $params = array(); + + // Don't pass through $abort, $trapException. Just use straight-up exceptions. + $abort = TRUE; + $trapException = FALSE; + $errorScope = CRM_Core_TemporaryErrorScope::useException(); + + // Don't pass through freeDAO. You can do it yourself. + $freeDAO = FALSE; + + return CRM_Core_DAO::executeQuery($this->toSQL(), $params, $abort, $daoName, + $freeDAO, $i18nRewrite, $trapException); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/Select.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/Select.php index 23adc513d4b..e53df900418 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/Select.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/Select.php @@ -47,7 +47,6 @@ * - Portable * - No knowledge of the underlying SQL API (except for escaping -- CRM_Core_DAO::escapeString) * - No knowledge of the underlying data model - * - Single file * - SQL clauses correspond to PHP functions ($select->where("foo_id=123")) * - Variable escaping is concise and controllable based on prefixes, eg * - similar to Drupal's t() @@ -81,38 +80,8 @@ * @package CRM * @copyright CiviCRM LLC (c) 2004-2018 */ -class CRM_Utils_SQL_Select implements ArrayAccess { +class CRM_Utils_SQL_Select extends CRM_Utils_SQL_BaseParamQuery { - /** - * Interpolate values as soon as they are passed in (where(), join(), etc). - * - * Default. - * - * Pro: Every clause has its own unique namespace for parameters. - * Con: Probably slower. - * Advice: Use this when aggregating SQL fragments from agents who - * maintained by different parties. - */ - const INTERPOLATE_INPUT = 'in'; - - /** - * Interpolate values when rendering SQL output (toSQL()). - * - * Pro: Probably faster. - * Con: Must maintain an aggregated list of all parameters. - * Advice: Use this when you have control over the entire query. - */ - const INTERPOLATE_OUTPUT = 'out'; - - /** - * Determine mode automatically. When the first attempt is made - * to use input-interpolation (eg `where(..., array(...))`) or - * output-interpolation (eg `param(...)`), the mode will be - * set. Subsequent calls will be validated using the same mode. - */ - const INTERPOLATE_AUTO = 'auto'; - - private $mode = NULL; private $insertInto = NULL; private $insertVerb = 'INSERT INTO '; private $insertIntoFields = array(); @@ -125,12 +94,8 @@ class CRM_Utils_SQL_Select implements ArrayAccess { private $orderBys = array(); private $limit = NULL; private $offset = NULL; - private $params = array(); private $distinct = NULL; - // Public to work-around PHP 5.3 limit. - public $strict = NULL; - /** * Create a new SELECT query. * @@ -177,7 +142,7 @@ public function copy() { /** * Merge something or other. * - * @param CRM_Utils_SQL_Select $other + * @param array|CRM_Utils_SQL_Select $other * @param array|NULL $parts * ex: 'joins', 'wheres' * @return CRM_Utils_SQL_Select @@ -187,6 +152,13 @@ public function merge($other, $parts = NULL) { return $this; } + if (is_array($other)) { + foreach ($other as $fragment) { + $this->merge($fragment, $parts); + } + return $this; + } + if ($this->mode === self::INTERPOLATE_AUTO) { $this->mode = $other->mode; } @@ -349,22 +321,8 @@ public function orderBy($exprs, $args = NULL, $weight = 0) { * @return \CRM_Utils_SQL_Select */ public function param($keys, $value = NULL) { - if ($this->mode === self::INTERPOLATE_AUTO) { - $this->mode = self::INTERPOLATE_OUTPUT; - } - elseif ($this->mode !== self::INTERPOLATE_OUTPUT) { - throw new RuntimeException("Select::param() only makes sense when interpolating on output."); - } - - if (is_array($keys)) { - foreach ($keys as $k => $v) { - $this->params[$k] = $v; - } - } - else { - $this->params[$keys] = $value; - } - return $this; + // Why bother with an override? To provide bett er type-hinting in `@return`. + return parent::param($keys, $value); } /** @@ -477,101 +435,6 @@ public function isEmpty($parts = NULL) { return $empty; } - /** - * Enable (or disable) strict mode. - * - * In strict mode, unknown variables will generate exceptions. - * - * @param bool $strict - * @return CRM_Utils_SQL_Select - */ - public function strict($strict = TRUE) { - $this->strict = $strict; - return $this; - } - - /** - * Given a string like "field_name = @value", replace "@value" with an escaped SQL string - * - * @param string $expr SQL expression - * @param null|array $args a list of values to insert into the SQL expression; keys are prefix-coded: - * prefix '@' => escape SQL - * prefix '#' => literal number, skip escaping but do validation - * prefix '!' => literal, skip escaping and validation - * if a value is an array, then it will be imploded - * - * PHP NULL's will be treated as SQL NULL's. The PHP string "null" will be treated as a string. - * - * @param string $activeMode - * - * @return string - */ - public function interpolate($expr, $args, $activeMode = self::INTERPOLATE_INPUT) { - if ($args === NULL) { - return $expr; - } - else { - if ($this->mode === self::INTERPOLATE_AUTO) { - $this->mode = $activeMode; - } - elseif ($activeMode !== $this->mode) { - throw new RuntimeException("Cannot mix interpolation modes."); - } - - $select = $this; - return preg_replace_callback('/([#!@])([a-zA-Z0-9_]+)/', function($m) use ($select, $args) { - if (isset($args[$m[2]])) { - $values = $args[$m[2]]; - } - elseif (isset($args[$m[1] . $m[2]])) { - // Backward compat. Keys in $args look like "#myNumber" or "@myString". - $values = $args[$m[1] . $m[2]]; - } - elseif ($select->strict) { - throw new CRM_Core_Exception('Cannot build query. Variable "' . $m[1] . $m[2] . '" is unknown.'); - } - else { - // Unrecognized variables are ignored. Mitigate risk of accidents. - return $m[0]; - } - $values = is_array($values) ? $values : array($values); - switch ($m[1]) { - case '@': - $parts = array_map(array($select, 'escapeString'), $values); - return implode(', ', $parts); - - // TODO: ensure all uses of this un-escaped literal are safe - case '!': - return implode(', ', $values); - - case '#': - foreach ($values as $valueKey => $value) { - if ($value === NULL) { - $values[$valueKey] = 'NULL'; - } - elseif (!is_numeric($value)) { - //throw new API_Exception("Failed encoding non-numeric value" . var_export(array($m[0] => $values), TRUE)); - throw new CRM_Core_Exception("Failed encoding non-numeric value (" . $m[0] . ")"); - } - } - return implode(', ', $values); - - default: - throw new CRM_Core_Exception("Unrecognized prefix"); - } - }, $expr); - } - } - - /** - * @param string|NULL $value - * @return string - * SQL expression, e.g. "it\'s great" (with-quotes) or NULL (without-quotes) - */ - public function escapeString($value) { - return $value === NULL ? 'NULL' : '"' . CRM_Core_DAO::escapeString($value) . '"'; - } - /** * @return string * SQL statement @@ -655,65 +518,4 @@ public function execute($daoName = NULL, $i18nRewrite = TRUE) { $freeDAO, $i18nRewrite, $trapException); } - /** - * Has an offset been set. - * - * @param string $offset - * - * @return bool - */ - public function offsetExists($offset) { - return isset($this->params[$offset]); - } - - /** - * Get the value of a SQL parameter. - * - * @code - * $select['cid'] = 123; - * $select->where('contact.id = #cid'); - * echo $select['cid']; - * @endCode - * - * @param string $offset - * @return mixed - * @see param() - * @see ArrayAccess::offsetGet - */ - public function offsetGet($offset) { - return $this->params[$offset]; - } - - /** - * Set the value of a SQL parameter. - * - * @code - * $select['cid'] = 123; - * $select->where('contact.id = #cid'); - * echo $select['cid']; - * @endCode - * - * @param string $offset - * @param mixed $value - * The new value of the parameter. - * Values may be strings, ints, or arrays thereof -- provided that the - * SQL query uses appropriate prefix (e.g. "@", "!", "#"). - * @see param() - * @see ArrayAccess::offsetSet - */ - public function offsetSet($offset, $value) { - $this->param($offset, $value); - } - - /** - * Unset the value of a SQL parameter. - * - * @param string $offset - * @see param() - * @see ArrayAccess::offsetUnset - */ - public function offsetUnset($offset) { - unset($this->params[$offset]); - } - } diff --git a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/TempTable.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/TempTable.php new file mode 100644 index 00000000000..cedc46c8162 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/SQL/TempTable.php @@ -0,0 +1,272 @@ +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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/System.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/System.php index e2d72f53032..fb5bb879ea4 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/System.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/System.php @@ -1416,8 +1416,21 @@ public static function civiExit($status = 0) { 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/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/System/Drupal8.php b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/System/Drupal8.php index 499a1431a87..1413f2bd7b9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/System/Drupal8.php +++ b/profiles/civicrm_starterkit/modules/civicrm/CRM/Utils/System/Drupal8.php @@ -422,7 +422,7 @@ public function loadBootStrap($params = array(), $loadUser = TRUE, $throwError = chdir($root); // Create a mock $request object - $autoloader = require_once $root . '/vendor/autoload.php'; + $autoloader = require_once $root . '/autoload.php'; if ($autoloader === TRUE) { $autoloader = ComposerAutoloaderInitDrupal8::getLoader(); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/Civi.php b/profiles/civicrm_starterkit/modules/civicrm/Civi.php index ae5dc6f4b1d..8acfdba7686 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/Civi.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/Civi/Angular/Manager.php b/profiles/civicrm_starterkit/modules/civicrm/Civi/Angular/Manager.php index 62d896e2f8f..61f36af0eeb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/Civi/Angular/Manager.php +++ b/profiles/civicrm_starterkit/modules/civicrm/Civi/Angular/Manager.php @@ -246,7 +246,7 @@ public function getRawPartials($name) { * 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/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/Container.php b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/Container.php index ffe1bfbe3bc..6eb4a4d66a9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/Container.php +++ b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/Container.php @@ -65,7 +65,7 @@ public function loadContainer() { // 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 @@ public function createContainer() { $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'), ), ) @@ -208,6 +214,16 @@ public function createContainer() { ->setFactory(array($class, 'singleton')); } + $container->setDefinition('prevnext', new Definition( + 'CRM_Core_PrevNextCache_Interface', + [new Reference('service_container')] + ))->setFactory(array(new Reference(self::SELF), 'createPrevNextCache')); + + $container->setDefinition('prevnext.driver.sql', new Definition( + 'CRM_Core_PrevNextCache_Sql', + [] + )); + $container->setDefinition('civi.mailing.triggers', new Definition( 'Civi\Core\SqlTrigger\TimestampTriggers', array('civicrm_mailing', 'Mailing') @@ -393,6 +409,18 @@ public function createApiKernel($dispatcher, $magicFunctionProvider) { return $kernel; } + /** + * @param ContainerInterface $container + * @return \CRM_Core_PrevNextCache_Interface + */ + public static function createPrevNextCache($container) { + $cacheDriver = \CRM_Utils_Cache::getCacheDriver(); + $service = 'prevnext.driver.' . strtolower($cacheDriver); + return $container->has($service) + ? $container->get($service) + : $container->get('prevnext.driver.sql'); + } + /** * Get a list of boot services. * diff --git a/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SettingsBag.php b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SettingsBag.php index e3fcd7adb82..167be544c39 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SettingsBag.php +++ b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SettingsBag.php @@ -139,7 +139,7 @@ public function loadValues() { $isUpgradeMode = \CRM_Core_Config::isUpgradeMode(); - if ($isUpgradeMode && empty($this->contactId) && \CRM_Core_DAO::checkFieldExists('civicrm_domain', 'config_backend', FALSE)) { + if ($isUpgradeMode && empty($this->contactId) && \CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_domain', 'config_backend', FALSE)) { $config_backend = \CRM_Core_DAO::singleValueQuery('SELECT config_backend FROM civicrm_domain WHERE id = %1', array(1 => array($this->domainId, 'Positive'))); $oldSettings = \CRM_Upgrade_Incremental_php_FourSeven::convertBackendToSettings($this->domainId, $config_backend); @@ -373,7 +373,7 @@ protected function setDb($name, $value) { if (!isset(\Civi::$statics[__CLASS__]['upgradeMode'])) { \Civi::$statics[__CLASS__]['upgradeMode'] = \CRM_Core_Config::isUpgradeMode(); } - if (\Civi::$statics[__CLASS__]['upgradeMode'] && \CRM_Core_DAO::checkFieldExists('civicrm_setting', 'group_name')) { + if (\Civi::$statics[__CLASS__]['upgradeMode'] && \CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_setting', 'group_name')) { $dao->group_name = 'placeholder'; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SettingsManager.php b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SettingsManager.php index 8147afca982..e56d723b2fb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SettingsManager.php +++ b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SettingsManager.php @@ -205,7 +205,7 @@ protected function getDefaults($entity) { 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/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SqlTrigger/StaticTriggers.php b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SqlTrigger/StaticTriggers.php index 40cd3a57ba5..30a02a8fa13 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SqlTrigger/StaticTriggers.php +++ b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SqlTrigger/StaticTriggers.php @@ -89,7 +89,7 @@ public function alterTriggerInfo(&$info, $tableFilter = NULL) { if (\CRM_Core_Config::isUpgradeMode() && isset($trigger['upgrade_check'])) { $uc = $trigger['upgrade_check']; - if (!\CRM_Core_DAO::checkFieldExists($uc['table'], $uc['column']) + if (!\CRM_Core_BAO_SchemaHandler::checkIfFieldExists($uc['table'], $uc['column']) ) { continue; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SqlTrigger/TimestampTriggers.php b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SqlTrigger/TimestampTriggers.php index c7e1127322b..dcce0e39307 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SqlTrigger/TimestampTriggers.php +++ b/profiles/civicrm_starterkit/modules/civicrm/Civi/Core/SqlTrigger/TimestampTriggers.php @@ -145,7 +145,7 @@ public function alterTriggerInfo(&$info, $tableFilter = NULL) { // In the past, this was a version-based check, but checkFieldExists() // seems more robust. if (\CRM_Core_Config::isUpgradeMode()) { - if (!\CRM_Core_DAO::checkFieldExists($this->getTableName(), + if (!\CRM_Core_BAO_SchemaHandler::checkIfFieldExists($this->getTableName(), $this->getCreatedDate()) ) { return; diff --git a/profiles/civicrm_starterkit/modules/civicrm/Civi/Test/Api3TestTrait.php b/profiles/civicrm_starterkit/modules/civicrm/Civi/Test/Api3TestTrait.php index 1efcd13ce69..2a10abf7e73 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/Civi/Test/Api3TestTrait.php +++ b/profiles/civicrm_starterkit/modules/civicrm/Civi/Test/Api3TestTrait.php @@ -235,6 +235,9 @@ public function callAPISuccessGetValue($entity, $params, $type = NULL) { 'debug' => 1, ); $result = $this->civicrm_api($entity, 'getvalue', $params); + if (is_array($result) && (!empty($result['is_error']) || isset($result['values']))) { + throw new \Exception('Invalid getvalue result' . print_r($result, TRUE)); + } if ($type) { if ($type == 'integer') { // api seems to return integers as strings diff --git a/profiles/civicrm_starterkit/modules/civicrm/PATCHES.txt b/profiles/civicrm_starterkit/modules/civicrm/PATCHES.txt index 5a8f44a89cd..1f36fb46aad 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/PATCHES.txt +++ b/profiles/civicrm_starterkit/modules/civicrm/PATCHES.txt @@ -1,5 +1,5 @@ The following patches have been applied to this project: -- pantheon-settings-starterkit-50.patch +- pantheon-settings-starterkit-55.patch - public_files_config.patch - cron.patch - extern-cms-bootstrap.patch diff --git a/profiles/civicrm_starterkit/modules/civicrm/README.md b/profiles/civicrm_starterkit/modules/civicrm/README.md index cf7462f47db..69835a0b75c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/README.md +++ b/profiles/civicrm_starterkit/modules/civicrm/README.md @@ -46,5 +46,5 @@ questions and ideas in the [Developer Discussion room](https://chat.civicrm.org/ Installing the latest developmental code requires some [special steps](http://wiki.civicrm.org/confluence/display/CRMDOC/Contributing+to+CiviCRM+using+GitHub). -Report all issues to CiviCRM via JIRA: -https://issues.civicrm.org +Report all issues to CiviCRM via GitLab: +https://lab.civicrm.org diff --git a/profiles/civicrm_starterkit/modules/civicrm/ang/crmCaseType.js b/profiles/civicrm_starterkit/modules/civicrm/ang/crmCaseType.js index ee9efb96030..12500df6154 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ang/crmCaseType.js +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/ang/crmCaseType/list.html b/profiles/civicrm_starterkit/modules/civicrm/ang/crmCaseType/list.html index 52d1a3d0a6f..a9caecc34a2 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ang/crmCaseType/list.html +++ b/profiles/civicrm_starterkit/modules/civicrm/ang/crmCaseType/list.html @@ -35,7 +35,7 @@

    {{ts('Case Types')}}

    {{ts('Edit')}} - + {{ts('more')}}
    - - + +
    diff --git a/profiles/civicrm_starterkit/modules/civicrm/api/v3/ActivityType.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/ActivityType.php index 3f6d121f1a1..db7bd23d73c 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/ActivityType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/api/v3/ActivityType.php @@ -72,13 +72,12 @@ function civicrm_api3_activity_type_get($params) { function civicrm_api3_activity_type_create($params) { $action = 1; - $groupParams = array('name' => 'activity_type'); if ($optionValueID = CRM_Utils_Array::value('option_value_id', $params)) { $action = 2; } - $activityObject = CRM_Core_OptionValue::addOptionValue($params, $groupParams, $action, $optionValueID); + $activityObject = CRM_Core_OptionValue::addOptionValue($params, 'activity_type', $action, $optionValueID); $activityType = array(); _civicrm_api3_object_to_array($activityObject, $activityType[$activityObject->id]); return civicrm_api3_create_success($activityType, $params, 'activity_type', 'create'); diff --git a/profiles/civicrm_starterkit/modules/civicrm/api/v3/Case.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/Case.php index 0d27319d97b..7aa2bdf88f3 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/Case.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/api/v3/Contact.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/Contact.php index 5d3ecc7af1c..f6afa246bfc 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/Contact.php +++ b/profiles/civicrm_starterkit/modules/civicrm/api/v3/Contact.php @@ -1162,6 +1162,149 @@ function _civicrm_api3_contact_merge_spec(&$params) { ); } +/** + * Get the ultimate contact a contact was merged to. + * + * @param array $params + * + * @return array + * API Result Array + * @throws API_Exception + */ +function civicrm_api3_contact_getmergedto($params) { + $contactID = _civicrm_api3_contact_getmergedto($params); + if ($contactID) { + $values = [$contactID => ['id' => $contactID]]; + } + else { + $values = []; + } + return civicrm_api3_create_success($values, $params); +} + +/** + * Get the contact our contact was finally merged to. + * + * If the contact has been merged multiple times the crucial parent activity will have + * wound up on the ultimate contact so we can figure out the final resting place of the + * contact with only 2 activities even if 50 merges took place. + * + * @param array $params + * + * @return int|false + */ +function _civicrm_api3_contact_getmergedto($params) { + $contactID = FALSE; + $deleteActivity = civicrm_api3('ActivityContact', 'get', [ + 'contact_id' => $params['contact_id'], + 'activity_id.activity_type_id' => 'Contact Deleted By Merge', + 'is_deleted' => 0, + 'is_test' => $params['is_test'], + 'record_type_id' => 'Activity Targets', + 'return' => ['activity_id.parent_id'], + 'sequential' => 1, + 'options' => [ + 'limit' => 1, + 'sort' => 'activity_id.activity_date_time DESC' + ], + ])['values']; + if (!empty($deleteActivity)) { + $contactID = civicrm_api3('ActivityContact', 'getvalue', [ + 'activity_id' => $deleteActivity[0]['activity_id.parent_id'], + 'record_type_id' => 'Activity Targets', + 'return' => 'contact_id', + ]); + } + return $contactID; +} + +/** + * Adjust metadata for contact_merge api function. + * + * @param array $params + */ +function _civicrm_api3_contact_getmergedto_spec(&$params) { + $params['contact_id'] = [ + 'title' => ts('ID of contact to find ultimate contact for'), + 'type' => CRM_Utils_Type::T_INT, + 'api.required' => TRUE, + ]; + $params['is_test'] = [ + 'title' => ts('Get test deletions rather than live?'), + 'type' => CRM_Utils_Type::T_BOOLEAN, + 'api.default' => 0, + ]; +} + +/** + * Get the ultimate contact a contact was merged to. + * + * @param array $params + * + * @return array + * API Result Array + * @throws API_Exception + */ +function civicrm_api3_contact_getmergedfrom($params) { + $contacts = _civicrm_api3_contact_getmergedfrom($params); + return civicrm_api3_create_success($contacts, $params); +} + +/** + * Get all the contacts merged into our contact. + * + * @param array $params + * + * @return array + */ +function _civicrm_api3_contact_getmergedfrom($params) { + $activities = []; + $deleteActivities = civicrm_api3('ActivityContact', 'get', [ + 'contact_id' => $params['contact_id'], + 'activity_id.activity_type_id' => 'Contact Merged', + 'is_deleted' => 0, + 'is_test' => $params['is_test'], + 'record_type_id' => 'Activity Targets', + 'return' => 'activity_id', + ])['values']; + + foreach ($deleteActivities as $deleteActivity) { + $activities[] = $deleteActivity['activity_id']; + } + if (empty($activities)) { + return []; + } + + $activityContacts = civicrm_api3('ActivityContact', 'get', [ + 'activity_id.parent_id' => ['IN' => $activities], + 'record_type_id' => 'Activity Targets', + 'return' => 'contact_id', + ])['values']; + $contacts = []; + foreach ($activityContacts as $activityContact) { + $contacts[$activityContact['contact_id']] = ['id' => $activityContact['contact_id']]; + } + return $contacts; +} + +/** + * Adjust metadata for contact_merge api function. + * + * @param array $params + */ +function _civicrm_api3_contact_getmergedfrom_spec(&$params) { + $params['contact_id'] = [ + 'title' => ts('ID of contact to find ultimate contact for'), + 'type' => CRM_Utils_Type::T_INT, + 'api.required' => TRUE, + ]; + $params['is_test'] = [ + 'title' => ts('Get test deletions rather than live?'), + 'type' => CRM_Utils_Type::T_BOOLEAN, + 'api.default' => 0, + ]; +} + /** * Adjust metadata for contact_proximity api function. * @@ -1360,7 +1503,7 @@ function civicrm_api3_contact_duplicatecheck($params) { $params['match'], $params['match']['contact_type'], $params['rule_type'], - array(), + CRM_Utils_Array::value('exclude', $params, []), CRM_Utils_Array::value('check_permissions', $params), CRM_Utils_Array::value('dedupe_rule_id', $params) ); diff --git a/profiles/civicrm_starterkit/modules/civicrm/api/v3/ContributionRecur.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/ContributionRecur.php index 144e62ad1a8..5403ef353d2 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/ContributionRecur.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/api/v3/CustomValue.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/CustomValue.php index 20aa18c5188..3a855d21f31 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/CustomValue.php +++ b/profiles/civicrm_starterkit/modules/civicrm/api/v3/CustomValue.php @@ -149,7 +149,10 @@ function civicrm_api3_custom_value_get($params) { if (!empty(substr($id, 7))) { $returnVal = substr($id, 7); } - foreach ((array) $returnVal as $value) { + if (!is_array($returnVal)) { + $returnVal = explode(',', $returnVal); + } + foreach ($returnVal as $value) { list($c, $i) = CRM_Utils_System::explode('_', $value, 2); if ($c == 'custom' && is_numeric($i)) { $names['custom_' . $i] = 'custom_' . $i; @@ -341,6 +344,7 @@ function civicrm_api3_custom_value_gettree($params) { if ($ret || !empty($params['check_permissions'])) { $entityData = civicrm_api3($params['entity_type'], 'getsingle', array( 'id' => $params['entity_id'], + 'check_permissions' => !empty($params['check_permissions']), 'return' => array_merge(array('id'), array_values($ret)), )); foreach ($ret as $param => $key) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/api/v3/FinancialType.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/FinancialType.php index cc597b3445d..59f0d8fe348 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/FinancialType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/api/v3/FinancialType.php @@ -42,6 +42,12 @@ function civicrm_api3_financial_type_create($params) { return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params, 'FinancialType'); } +function _civicrm_api3_financial_type_create_spec(&$params) { + $params['name']['api.required'] = 1; + $params['name']['type'] = CRM_Utils_Type::T_STRING; + unset($params['name']['pseudoconstant']); +} + /** * Get a FinancialType. * diff --git a/profiles/civicrm_starterkit/modules/civicrm/api/v3/Job.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/Job.php index 7a7e699dc8d..ab655369d1b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/Job.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/api/v3/Membership.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/Membership.php index 4f0c657a918..34af5cd09d1 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/Membership.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/api/v3/MembershipType.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/MembershipType.php index 5011383c689..82de053cfbd 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/MembershipType.php +++ b/profiles/civicrm_starterkit/modules/civicrm/api/v3/MembershipType.php @@ -59,8 +59,7 @@ function civicrm_api3_membership_type_create($params) { * Array of parameters determined by getfields. */ function _civicrm_api3_membership_type_create_spec(&$params) { - // todo could set default here probably - $params['domain_id']['api.required'] = 1; + $params['domain_id']['api.default'] = CRM_Core_Config::domainID(); $params['member_of_contact_id']['api.required'] = 1; $params['financial_type_id']['api.required'] = 1; $params['name']['api.required'] = 1; diff --git a/profiles/civicrm_starterkit/modules/civicrm/api/v3/Navigation.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/Navigation.php index 1ce843095c3..bce0ce008b6 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/Navigation.php +++ b/profiles/civicrm_starterkit/modules/civicrm/api/v3/Navigation.php @@ -87,17 +87,6 @@ function civicrm_api3_navigation_get($params) { return _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params); } -/** - * Adjust metadata for navigation create action. - * - * @param array $params - */ -function _civicrm_api3_navigation_create_spec(&$params) { - $params['domain_id']['api.default'] = CRM_Core_Config::domainID(); - $params['domain_id']['type'] = CRM_Utils_Type::T_INT; - $params['domain_id']['title'] = 'Domain ID'; -} - /** * Create navigation item. * diff --git a/profiles/civicrm_starterkit/modules/civicrm/api/v3/OptionGroup.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/OptionGroup.php index 24822c85205..f86c3a21bbb 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/OptionGroup.php +++ b/profiles/civicrm_starterkit/modules/civicrm/api/v3/OptionGroup.php @@ -53,7 +53,9 @@ function civicrm_api3_option_group_get($params) { * @return array */ function civicrm_api3_option_group_create($params) { - return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params, 'OptionGroup'); + $result = _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params, 'OptionGroup'); + civicrm_api('option_value', 'getfields', array('version' => 3, 'cache_clear' => 1)); + return $result; } /** diff --git a/profiles/civicrm_starterkit/modules/civicrm/api/v3/ReportTemplate.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/ReportTemplate.php index e121c411464..d13cd54b449 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/ReportTemplate.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/api/v3/examples/Setting/GetFields.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/examples/Setting/GetFields.php index 8a4ccd83d82..32c450e058a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/examples/Setting/GetFields.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/api/v3/utils.php b/profiles/civicrm_starterkit/modules/civicrm/api/v3/utils.php index 814d11e0b83..094beb2375b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/api/v3/utils.php +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/bower_components/jquery-ui/.bower.json b/profiles/civicrm_starterkit/modules/civicrm/bower_components/jquery-ui/.bower.json index 69ba102964f..d28097dd886 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/bower_components/jquery-ui/.bower.json +++ b/profiles/civicrm_starterkit/modules/civicrm/bower_components/jquery-ui/.bower.json @@ -5,6 +5,7 @@ "jquery-ui.js" ], "ignore": [], + "license": "MIT", "dependencies": { "jquery": ">=1.6" }, @@ -13,9 +14,9 @@ "_resolution": { "type": "version", "tag": "1.12.1", - "commit": "dec4c50123193d4f7c8ae6cd0bff45478e1ad276" + "commit": "44ecf3794cc56b65954cc19737234a3119d036cc" }, "_source": "https://github.com/components/jqueryui.git", - "_target": ">=1.9", + "_target": "~1.12", "_originalSource": "jquery-ui" } \ No newline at end of file diff --git a/profiles/civicrm_starterkit/modules/civicrm/bower_components/jquery-ui/bower.json b/profiles/civicrm_starterkit/modules/civicrm/bower_components/jquery-ui/bower.json index cc0cf5ba93d..965aba7b03e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/bower_components/jquery-ui/bower.json +++ b/profiles/civicrm_starterkit/modules/civicrm/bower_components/jquery-ui/bower.json @@ -6,6 +6,7 @@ ], "ignore": [ ], + "license": "MIT", "dependencies": { "jquery": ">=1.6" } diff --git a/profiles/civicrm_starterkit/modules/civicrm/civicrm-version.php b/profiles/civicrm_starterkit/modules/civicrm/civicrm-version.php index 469335def05..46450132d5b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/civicrm-version.php +++ b/profiles/civicrm_starterkit/modules/civicrm/civicrm-version.php @@ -1,7 +1,7 @@ '5.3.1', + return array( 'version' => '5.5.1', 'cms' => 'Drupal', 'revision' => '' ); } diff --git a/profiles/civicrm_starterkit/modules/civicrm/composer.json b/profiles/civicrm_starterkit/modules/civicrm/composer.json index 9d81135dbbe..1078ccf3d30 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/composer.json +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/composer.lock b/profiles/civicrm_starterkit/modules/civicrm/composer.lock index 0cce99a2a89..8f3e4d2659e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/composer.lock +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/css/civicrm.css b/profiles/civicrm_starterkit/modules/civicrm/css/civicrm.css index 6b2fdcd1843..a89e60458bc 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/css/civicrm.css +++ b/profiles/civicrm_starterkit/modules/civicrm/css/civicrm.css @@ -2118,6 +2118,10 @@ a.crm-i:hover { color: #6177D5; } +.crm-i.crm-i-green { + color: #86c661; +} + .crm-i-button { position: relative; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/css/civicrmNavigation.css b/profiles/civicrm_starterkit/modules/civicrm/css/civicrmNavigation.css index 990b6e28108..e005bb004bd 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/css/civicrmNavigation.css +++ b/profiles/civicrm_starterkit/modules/civicrm/css/civicrmNavigation.css @@ -115,12 +115,14 @@ div.menu-item { padding: 1px 10px 1px 4px; height: auto; } -img.menu-item-arrow{ +#civicrm-menu .menu-item-arrow, +#root-menu-div .menu-item-arrow { position: absolute; right: 4px; - top: 8px; + top: 6px; } -#civicrm-menu i { +#civicrm-menu i, +#root-menu-div i { margin-right: 5px; } li.menu-separator{ diff --git a/profiles/civicrm_starterkit/modules/civicrm/css/searchForm.css b/profiles/civicrm_starterkit/modules/civicrm/css/searchForm.css index bbafc5d745a..a0f57b283a8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/css/searchForm.css +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/drupal/civicrm.info b/profiles/civicrm_starterkit/modules/civicrm/drupal/civicrm.info index 93fa42f9175..deb7cd81e01 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/civicrm.info +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/civicrm.info @@ -1,6 +1,6 @@ name = CiviCRM description = Constituent relationship management system. Allows sites to manage contacts, relationships and groups, and track contact activities, contributions, memberships and events. See the CiviCRM website for more information. -version = 7.x-5.3.1 +version = 7.x-5.5.1 package = CiviCRM core = 7.x project = civicrm diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_contact_ref/civicrm_contact_ref.info b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_contact_ref/civicrm_contact_ref.info index 05ecb50c746..c529e7ddadd 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_contact_ref/civicrm_contact_ref.info +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_contact_ref/civicrm_contact_ref.info @@ -1,6 +1,6 @@ name = CiviCRM Contact Reference Field description = Makes a CiviCRM Contact Reference Field. -version = 7.x-5.3.1 +version = 7.x-5.5.1 core = 7.x package = CiviCRM project = civicrm diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_engage/civicrm_engage.info b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_engage/civicrm_engage.info index 0be61681134..9ed0d4b6ea5 100755 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_engage/civicrm_engage.info +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_engage/civicrm_engage.info @@ -1,6 +1,6 @@ name = CiviEngage description = Walklist and Phone-banking support for CiviCRM. -version = 7.x-5.3.1 +version = 7.x-5.5.1 core = 7.x package = CiviCRM project = civicrm diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_group_roles/civicrm_group_roles.info b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_group_roles/civicrm_group_roles.info index a4f6b18757d..d2796d26a94 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_group_roles/civicrm_group_roles.info +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_group_roles/civicrm_group_roles.info @@ -1,6 +1,6 @@ name = CiviGroup Roles Sync description = Sync Drupal Roles to CiviCRM Groups. -version = 7.x-5.3.1 +version = 7.x-5.5.1 core = 7.x package = CiviCRM project = civicrm diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_member_roles/civicrm_member_roles.info b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_member_roles/civicrm_member_roles.info index 062d51cc29b..d5c875f1b2e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_member_roles/civicrm_member_roles.info +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_member_roles/civicrm_member_roles.info @@ -1,6 +1,6 @@ name = CiviMember Roles Sync description = Synchronize CiviCRM Contacts with Membership Status to a specified Drupal Role both automatically and manually. -version = 7.x-5.3.1 +version = 7.x-5.5.1 core = 7.x package = CiviCRM project = civicrm diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_og_sync/civicrm_og_sync.info b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_og_sync/civicrm_og_sync.info index 5b4b84621ef..5d2f4033345 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_og_sync/civicrm_og_sync.info +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_og_sync/civicrm_og_sync.info @@ -1,6 +1,6 @@ name = CiviCRM OG Sync description = Synchronize Organic Groups and CiviCRM Groups and ACL's. More information at: http://wiki.civicrm.org/confluence/display/CRMDOC/CiviCRM+vs.+Organic+Groups -version = 7.x-5.3.1 +version = 7.x-5.5.1 core = 7.x package = CiviCRM project = civicrm diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_rules/civicrm_rules.info b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_rules/civicrm_rules.info index e06adc22790..cfcad700614 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_rules/civicrm_rules.info +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrm_rules/civicrm_rules.info @@ -1,6 +1,6 @@ name = CiviCRM Rules Integration description = Integrate CiviCRM and Drupal Rules Module. Expose Contact, Contribution and other Objects along with Form / Page Operations. -version = 7.x-5.3.1 +version = 7.x-5.5.1 core = 7.x package = CiviCRM project = civicrm diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrmtheme/civicrmtheme.info b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrmtheme/civicrmtheme.info index e73eedb9474..5f7894d7aaa 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrmtheme/civicrmtheme.info +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/civicrmtheme/civicrmtheme.info @@ -1,6 +1,6 @@ name = CiviCRM Theme description = Define alternate themes for CiviCRM. -version = 7.x-5.3.1 +version = 7.x-5.5.1 core = 7.x package = CiviCRM project = civicrm diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/views/civicrm.views.inc b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/views/civicrm.views.inc index 71678ca8ac0..253112ab7b3 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/views/civicrm.views.inc +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/views/civicrm.views.inc @@ -461,7 +461,7 @@ function civicrm_views_get_field($data_type, $html_type = NULL) { // Relying on html types as opposed to data types seems like a code smell. // Would love to be able to remove this logic. $customHTMLTypes = array( - 'Select', 'Multi-Select', 'AdvMulti-Select', 'Radio', 'CheckBox', + 'Select', 'Multi-Select', 'Radio', 'CheckBox', 'Select State/Province', 'Select Country', 'Multi-Select Country', 'Multi-Select State/Province', 'Autocomplete-Select', ); @@ -563,7 +563,7 @@ function civicrm_views_get_filter($data_type, $html_type = NULL, $option_group_i // Relying on html types as opposed to data types seems like a code smell. // Would love to be able to remove this logic. $customMultiValueHTMLTypes = array( - 'Multi-Select', 'AdvMulti-Select', 'CheckBox', 'Multi-Select Country', + 'Multi-Select', 'CheckBox', 'Multi-Select Country', 'Multi-Select State/Province', ); if ($html_type == 'Multi-Select Country') { diff --git a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/views/civicrm/civicrm_handler_field_custom.inc b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/views/civicrm/civicrm_handler_field_custom.inc index 4c54b1b3130..a0c032e7535 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/views/civicrm/civicrm_handler_field_custom.inc +++ b/profiles/civicrm_starterkit/modules/civicrm/drupal/modules/views/civicrm/civicrm_handler_field_custom.inc @@ -70,7 +70,7 @@ class civicrm_handler_field_custom extends views_handler_field { if (!is_null($value)) { // get the field id from the db if (!empty($this->definition['title'])) { - $customFieldID = CRM_Core_DAO::getFieldValue('CRM_Core_BAO_CustomField', $this->definition['title'], 'id', 'label'); + $customFieldID = CRM_Core_DAO::getFieldValue('CRM_Core_BAO_CustomField', $this->real_field, 'id', 'column_name'); return CRM_Core_BAO_CustomField::displayValue($value, $customFieldID); } // could not get custom id, lets just return what we have diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/.gitignore b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/.gitignore new file mode 100644 index 00000000000..0d74f047891 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/.gitignore @@ -0,0 +1,74 @@ +# Created by .ignore support plugin (hsz.mobi) +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +### Windows template +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Page/AJAX.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Page/AJAX.php new file mode 100644 index 00000000000..01e7dd09eb0 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Page/AJAX.php @@ -0,0 +1,64 @@ +urlPath[3])) { + $calls = CRM_Utils_Request::retrieve('calls', 'String', CRM_Core_DAO::$_nullObject, TRUE, NULL, 'POST', TRUE); + $calls = json_decode($calls, TRUE); + $response = []; + foreach ($calls as $index => $call) { + $response[$index] = $this->execute($call[0], $call[1], CRM_Utils_Array::value(2, $call, [])); + } + } + // Call single + else { + $entity = $this->urlPath[3]; + $action = $this->urlPath[4]; + $params = CRM_Utils_Request::retrieve('params', 'String'); + $params = $params ? json_decode($params, TRUE) : []; + $response = $this->execute($entity, $action, $params); + } + } + catch (Exception $e) { + http_response_code(500); + $response = [ + 'error_code' => $e->getCode(), + ]; + if (CRM_Core_Permission::check('view debug output')) { + $response['error_message'] = $e->getMessage(); + if (CRM_Core_BAO_Setting::getItem(NULL, 'backtrace')) { + $response['backtrace'] = $e->getTrace(); + } + } + } + CRM_Utils_System::setHttpHeader('Content-Type', 'application/json'); + echo json_encode($response); + CRM_Utils_System::civiExit(); + } + + /** + * Run api call & prepare result for json encoding + * + * @param $entity + * @param $action + * @param $params + * @return array + */ + protected function execute($entity, $action, $params) { + $params['checkPermissions'] = TRUE; + $result = civicrm_api4($entity, $action, $params); + // Convert arrayObject into something more suitable for json + $vals = ['values' => (array) $result]; + foreach (get_class_vars(get_class($result)) as $key => $val) { + $vals[$key] = $result->$key; + } + return $vals; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Upgrader.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Upgrader.php new file mode 100644 index 00000000000..40a39849970 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Upgrader.php @@ -0,0 +1,158 @@ + 'API Explorer', + 'return' => ['id', 'parent_id'], + 'sequential' => 1, + 'api.Navigation.create' => ['label' => ts("Api Explorer v3")], + ]); + civicrm_api3('Navigation', 'create', [ + 'parent_id' => $v3Item['values'][0]['parent_id'], + 'label' => ts("Api Explorer v4"), + 'weight' => 2, + 'name' => "Api Explorer v4", + 'permission' => "administer CiviCRM", + 'url' => "civicrm/a/#/api4", + 'is_active' => 1, + ]); + } + catch (Exception $e) { + // Couldn't create menu item. + } + } + + /** + * Example: Work with entities usually not available during the install step. + * + * This method can be used for any post-install tasks. For example, if a step + * of your installation depends on accessing an entity that is itself + * created during the installation (e.g., a setting or a managed entity), do + * so here to avoid order of operation problems. + * + public function postInstall() { + $customFieldId = civicrm_api3('CustomField', 'getvalue', array( + 'return' => array("id"), + 'name' => "customFieldCreatedViaManagedHook", + )); + civicrm_api3('Setting', 'create', array( + 'myWeirdFieldSetting' => array('id' => $customFieldId, 'weirdness' => 1), + )); + } + + /** + * Uninstall script + */ + public function uninstall() { + // Remove Api4 Explorer navigation menu item + civicrm_api3('Navigation', 'get', [ + 'name' => 'Api Explorer v4', + 'return' => ['id'], + 'api.Navigation.delete' => [], + ]); + } + + /** + * Example: Run a simple query when a module is enabled. + * + public function enable() { + CRM_Core_DAO::executeQuery('UPDATE foo SET is_active = 1 WHERE bar = "whiz"'); + } + + /** + * Example: Run a simple query when a module is disabled. + * + public function disable() { + CRM_Core_DAO::executeQuery('UPDATE foo SET is_active = 0 WHERE bar = "whiz"'); + } + + /** + * Example: Run a couple simple queries. + * + * @return TRUE on success + * @throws Exception + * + public function upgrade_4200() { + $this->ctx->log->info('Applying update 4200'); + CRM_Core_DAO::executeQuery('UPDATE foo SET bar = "whiz"'); + CRM_Core_DAO::executeQuery('DELETE FROM bang WHERE willy = wonka(2)'); + return TRUE; + } // */ + + + /** + * Example: Run an external SQL script. + * + * @return TRUE on success + * @throws Exception + public function upgrade_4201() { + $this->ctx->log->info('Applying update 4201'); + // this path is relative to the extension base dir + $this->executeSqlFile('sql/upgrade_4201.sql'); + return TRUE; + } // */ + + + /** + * Example: Run a slow upgrade process by breaking it up into smaller chunk. + * + * @return TRUE on success + * @throws Exception + public function upgrade_4202() { + $this->ctx->log->info('Planning update 4202'); // PEAR Log interface + + $this->addTask(ts('Process first step'), 'processPart1', $arg1, $arg2); + $this->addTask(ts('Process second step'), 'processPart2', $arg3, $arg4); + $this->addTask(ts('Process second step'), 'processPart3', $arg5); + return TRUE; + } + public function processPart1($arg1, $arg2) { sleep(10); return TRUE; } + public function processPart2($arg3, $arg4) { sleep(10); return TRUE; } + public function processPart3($arg5) { sleep(10); return TRUE; } + // */ + + + /** + * Example: Run an upgrade with a query that touches many (potentially + * millions) of records by breaking it up into smaller chunks. + * + * @return TRUE on success + * @throws Exception + public function upgrade_4203() { + $this->ctx->log->info('Planning update 4203'); // PEAR Log interface + + $minId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(min(id),0) FROM civicrm_contribution'); + $maxId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(max(id),0) FROM civicrm_contribution'); + for ($startId = $minId; $startId <= $maxId; $startId += self::BATCH_SIZE) { + $endId = $startId + self::BATCH_SIZE - 1; + $title = ts('Upgrade Batch (%1 => %2)', array( + 1 => $startId, + 2 => $endId, + )); + $sql = ' + UPDATE civicrm_contribution SET foobar = whiz(wonky()+wanker) + WHERE id BETWEEN %1 and %2 + '; + $params = array( + 1 => array($startId, 'Integer'), + 2 => array($endId, 'Integer'), + ); + $this->addTask($title, 'executeSql', $sql, $params); + } + return TRUE; + } // */ + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Upgrader/Base.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Upgrader/Base.php new file mode 100644 index 00000000000..bfea5fef9a3 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/CRM/Api4/Upgrader/Base.php @@ -0,0 +1,375 @@ +ctx = array_shift($args); + $instance->queue = $instance->ctx->queue; + $method = array_shift($args); + return call_user_func_array([$instance, $method], $args); + } + + public function __construct($extensionName, $extensionDir) { + $this->extensionName = $extensionName; + $this->extensionDir = $extensionDir; + } + + // ******** Task helpers ******** + + /** + * Run a CustomData file. + * + * @param string $relativePath the CustomData XML file path (relative to this extension's dir) + * @return bool + */ + public function executeCustomDataFile($relativePath) { + $xml_file = $this->extensionDir . '/' . $relativePath; + return $this->executeCustomDataFileByAbsPath($xml_file); + } + + /** + * Run a CustomData file + * + * @param string $xml_file the CustomData XML file path (absolute path) + * + * @return bool + */ + protected static function executeCustomDataFileByAbsPath($xml_file) { + $import = new CRM_Utils_Migrate_Import(); + $import->run($xml_file); + return TRUE; + } + + /** + * Run a SQL file. + * + * @param string $relativePath the SQL file path (relative to this extension's dir) + * + * @return bool + */ + public function executeSqlFile($relativePath) { + CRM_Utils_File::sourceSQLFile( + CIVICRM_DSN, + $this->extensionDir . DIRECTORY_SEPARATOR . $relativePath + ); + return TRUE; + } + + /** + * @param string $tplFile + * The SQL file path (relative to this extension's dir). + * Ex: "sql/mydata.mysql.tpl". + * @return bool + */ + public function executeSqlTemplate($tplFile) { + // Assign multilingual variable to Smarty. + $upgrade = new CRM_Upgrade_Form(); + + $tplFile = CRM_Utils_File::isAbsolute($tplFile) ? $tplFile : $this->extensionDir . DIRECTORY_SEPARATOR . $tplFile; + $smarty = CRM_Core_Smarty::singleton(); + $smarty->assign('domainID', CRM_Core_Config::domainID()); + CRM_Utils_File::sourceSQLFile( + CIVICRM_DSN, $smarty->fetch($tplFile), NULL, TRUE + ); + return TRUE; + } + + /** + * Run one SQL query. + * + * This is just a wrapper for CRM_Core_DAO::executeSql, but it + * provides syntatic sugar for queueing several tasks that + * run different queries + */ + public function executeSql($query, $params = []) { + // FIXME verify that we raise an exception on error + CRM_Core_DAO::executeQuery($query, $params); + return TRUE; + } + + /** + * Syntatic sugar for enqueuing a task which calls a function in this class. + * + * The task is weighted so that it is processed + * as part of the currently-pending revision. + * + * After passing the $funcName, you can also pass parameters that will go to + * the function. Note that all params must be serializable. + */ + public function addTask($title) { + $args = func_get_args(); + $title = array_shift($args); + $task = new CRM_Queue_Task( + [get_class($this), '_queueAdapter'], + $args, + $title + ); + return $this->queue->createItem($task, ['weight' => -1]); + } + + // ******** Revision-tracking helpers ******** + + /** + * Determine if there are any pending revisions. + * + * @return bool + */ + public function hasPendingRevisions() { + $revisions = $this->getRevisions(); + $currentRevision = $this->getCurrentRevision(); + + if (empty($revisions)) { + return FALSE; + } + if (empty($currentRevision)) { + return TRUE; + } + + return ($currentRevision < max($revisions)); + } + + /** + * Add any pending revisions to the queue. + */ + public function enqueuePendingRevisions(CRM_Queue_Queue $queue) { + $this->queue = $queue; + + $currentRevision = $this->getCurrentRevision(); + foreach ($this->getRevisions() as $revision) { + if ($revision > $currentRevision) { + $title = ts('Upgrade %1 to revision %2', [ + 1 => $this->extensionName, + 2 => $revision, + ]); + + // note: don't use addTask() because it sets weight=-1 + + $task = new CRM_Queue_Task( + [get_class($this), '_queueAdapter'], + ['upgrade_' . $revision], + $title + ); + $this->queue->createItem($task); + + $task = new CRM_Queue_Task( + [get_class($this), '_queueAdapter'], + ['setCurrentRevision', $revision], + $title + ); + $this->queue->createItem($task); + } + } + } + + /** + * Get a list of revisions. + * + * @return array(revisionNumbers) sorted numerically + */ + public function getRevisions() { + if (!is_array($this->revisions)) { + $this->revisions = []; + + $clazz = new ReflectionClass(get_class($this)); + $methods = $clazz->getMethods(); + foreach ($methods as $method) { + if (preg_match('/^upgrade_(.*)/', $method->name, $matches)) { + $this->revisions[] = $matches[1]; + } + } + sort($this->revisions, SORT_NUMERIC); + } + + return $this->revisions; + } + + public function getCurrentRevision() { + $revision = CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName); + if (!$revision) { + $revision = $this->getCurrentRevisionDeprecated(); + } + return $revision; + } + + private function getCurrentRevisionDeprecated() { + $key = $this->extensionName . ':version'; + if ($revision = CRM_Core_BAO_Setting::getItem('Extension', $key)) { + $this->revisionStorageIsDeprecated = TRUE; + } + return $revision; + } + + public function setCurrentRevision($revision) { + CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision); + // clean up legacy schema version store (CRM-19252) + $this->deleteDeprecatedRevision(); + return TRUE; + } + + private function deleteDeprecatedRevision() { + if ($this->revisionStorageIsDeprecated) { + $setting = new CRM_Core_BAO_Setting(); + $setting->name = $this->extensionName . ':version'; + $setting->delete(); + CRM_Core_Error::debug_log_message("Migrated extension schema revision ID for {$this->extensionName} from civicrm_setting (deprecated) to civicrm_extension.\n"); + } + } + + // ******** Hook delegates ******** + + /** + * @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install + */ + public function onInstall() { + $files = glob($this->extensionDir . '/sql/*_install.sql'); + if (is_array($files)) { + foreach ($files as $file) { + CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); + } + } + $files = glob($this->extensionDir . '/sql/*_install.mysql.tpl'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeSqlTemplate($file); + } + } + $files = glob($this->extensionDir . '/xml/*_install.xml'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeCustomDataFileByAbsPath($file); + } + } + if (is_callable([$this, 'install'])) { + $this->install(); + } + } + + /** + * @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall + */ + public function onPostInstall() { + $revisions = $this->getRevisions(); + if (!empty($revisions)) { + $this->setCurrentRevision(max($revisions)); + } + if (is_callable([$this, 'postInstall'])) { + $this->postInstall(); + } + } + + /** + * @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall + */ + public function onUninstall() { + $files = glob($this->extensionDir . '/sql/*_uninstall.mysql.tpl'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeSqlTemplate($file); + } + } + if (is_callable([$this, 'uninstall'])) { + $this->uninstall(); + } + $files = glob($this->extensionDir . '/sql/*_uninstall.sql'); + if (is_array($files)) { + foreach ($files as $file) { + CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); + } + } + } + + /** + * @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable + */ + public function onEnable() { + // stub for possible future use + if (is_callable([$this, 'enable'])) { + $this->enable(); + } + } + + /** + * @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable + */ + public function onDisable() { + // stub for possible future use + if (is_callable([$this, 'disable'])) { + $this->disable(); + } + } + + public function onUpgrade($op, CRM_Queue_Queue $queue = NULL) { + switch ($op) { + case 'check': + return [$this->hasPendingRevisions()]; + + case 'enqueue': + return $this->enqueuePendingRevisions($queue); + + default: + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Address/Create.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Address/Create.php new file mode 100644 index 00000000000..b755dea7068 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Address/Create.php @@ -0,0 +1,45 @@ +values[_civicrm_api_get_entity_name_from_camel($fieldName)] = $this->$fieldName; + } + parent::_run($result); + } + + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Contact/Create.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Contact/Create.php new file mode 100644 index 00000000000..a07276b5496 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Contact/Create.php @@ -0,0 +1,25 @@ +values['skipCleanMoney'] = TRUE; + parent::_run($result); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Create.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Create.php new file mode 100644 index 00000000000..f10b0ac0e08 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Create.php @@ -0,0 +1,110 @@ + value pairs. + * @method $this addValue($field, $value) Set field value. + */ +class Create extends AbstractAction { + + /** + * Field values to set + * + * @var array + */ + protected $values = []; + + /** + * @param $key + * + * @return mixed|null + */ + public function getValue($key) { + return isset($this->values[$key]) ? $this->values[$key] : NULL; + } + + /** + * @inheritDoc + */ + public function _run(Result $result) { + $this->validateValues(); + $params = $this->values; + $this->fillDefaults($params); + + $resultArray = $this->writeObject($params); + + $result->exchangeArray([$resultArray]); + } + + /** + * @throws \API_Exception + */ + protected function validateValues() { + if (!empty($this->values['id'])) { + throw new \API_Exception('Cannot pass id to Create action. Use Update action instead.'); + } + $unmatched = []; + foreach ($this->getEntityFields() as $fieldName => $fieldInfo) { + if (!$this->getValue($fieldName) && !empty($fieldInfo['required']) && !isset($fieldInfo['default_value'])) { + $unmatched[] = $fieldName; + } + } + if ($unmatched) { + throw new \API_Exception("Mandatory values missing from Api4 {$this->getEntity()}::{$this->getAction()}: '" . implode("', '", $unmatched) . "'", "mandatory_missing", array("fields" => $unmatched)); + } + } + + /** + * Fill field defaults which were declared by the api. + * + * Note: default values from core are ignored because the BAO or database layer will supply them. + * + * @param array $params + */ + protected function fillDefaults(&$params) { + $fields = $this->getEntityFields(); + $bao = $this->getBaoName(); + $coreFields = array_column($bao::fields(), NULL, 'name'); + + foreach ($fields as $name => $field) { + // If a default value is set in the api but not in core, the api should supply it. + if (!isset($params[$name]) && !empty($field['default_value']) && empty($coreFields[$name]['default'])) { + $params[$name] = $field['default_value']; + } + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Create.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Create.php new file mode 100644 index 00000000000..1670a954e51 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Create.php @@ -0,0 +1,66 @@ +getCustomGroup(); + } + + /** + * @inheritDoc + */ + protected function fillDefaults(&$params) { + foreach ($this->getEntityFields() as $name => $field) { + if (empty($params[$name])) { + $params[$name] = $field['default_value']; + } + } + } + + /** + * @inheritDoc + */ + protected function writeObject($params) { + FormattingUtil::formatWriteParams($params, $this->getEntity(), $this->getEntityFields()); + + return \CRM_Core_BAO_CustomValueTable::setValues($params); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Delete.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Delete.php new file mode 100644 index 00000000000..8877cdce5ab --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Delete.php @@ -0,0 +1,63 @@ +getParamDefaults(); + if ($defaults['where'] && !array_diff_key($this->where, $defaults['where'])) { + throw new \API_Exception('Cannot delete with no "where" paramater specified'); + } + // run the parent action (get) to get the list + parent::_run($result); + // Then act on the result + $customTable = CoreUtil::getCustomTableByName($this->getCustomGroup()); + $ids = []; + foreach ($result as $item) { + \CRM_Utils_Hook::pre('delete', $this->getEntity(), $item['id'], \CRM_Core_DAO::$_nullArray); + \CRM_Utils_SQL_Delete::from($customTable) + ->where('id = #value') + ->param('#value', $item['id']) + ->execute(); + \CRM_Utils_Hook::post('delete', $this->getEntity(), $item['id'], \CRM_Core_DAO::$_nullArray); + $ids[] = $item['id']; + } + + $result->exchangeArray($ids); + return $result; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Get.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Get.php new file mode 100644 index 00000000000..bd6ba291503 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Get.php @@ -0,0 +1,25 @@ +getCustomGroup(); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetFields.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetFields.php new file mode 100644 index 00000000000..45f0424e846 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetFields.php @@ -0,0 +1,35 @@ +get('spec_gatherer'); + $spec = $gatherer->getSpec('Custom_' . $this->getCustomGroup(), $this->getAction(), $this->includeCustom); + $specArray = SpecFormatter::specToArray($spec->getFields($this->fields), (array) $this->select, $this->getOptions); + $result->action = 'getFields'; + $result->exchangeArray(array_values($specArray)); + } + + /** + * @inheritDoc + */ + public function getParamInfo($param = NULL) { + $info = parent::getParamInfo($param); + if (!$param) { + // This param is meaningless here. + unset($info['includeCustom']); + } + return $info; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Replace.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Replace.php new file mode 100644 index 00000000000..00728ad48b9 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Replace.php @@ -0,0 +1,56 @@ +getCustomGroup(); + } + + /** + * @inheritDoc + */ + protected function writeObject($params) { + FormattingUtil::formatWriteParams($params, $this->getEntity(), $this->getEntityFields()); + return \CRM_Core_BAO_CustomValueTable::setValues($params); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Update.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Update.php new file mode 100644 index 00000000000..7438ee94618 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Update.php @@ -0,0 +1,61 @@ + value pairs. + * @method $this addValue($field, $value) Set field value to update. + */ +class Update extends DefaultUpdate { + + /** + * @inheritDoc + */ + protected $select = ['id', 'entity_id']; + + /** + * @inheritDoc + */ + public function getEntity() { + return 'Custom_' . $this->getCustomGroup(); + } + + /** + * @inheritDoc + */ + protected function writeObject($params) { + FormattingUtil::formatWriteParams($params, $this->getEntity(), $this->getEntityFields()); + + return \CRM_Core_BAO_CustomValueTable::setValues($params); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Delete.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Delete.php new file mode 100644 index 00000000000..a690a2999ee --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Delete.php @@ -0,0 +1,100 @@ +getBaoName(); + $this->setSelect(['id']); + $defaults = $this->getParamDefaults(); + if ($defaults['where'] && !array_diff_key($this->where, $defaults['where'])) { + throw new \API_Exception('Cannot delete with no "where" paramater specified'); + } + // run the parent action (get) to get the list + parent::_run($result); + // Then act on the result + $ids = []; + if (method_exists($baoName, 'del')) { + foreach ($result as $item) { + $args = [$item['id']]; + $bao = call_user_func_array([$baoName, 'del'], $args); + if ($bao !== FALSE) { + $ids[] = $item['id']; + } + else { + throw new \API_Exception("Could not delete {$this->getEntity()} id {$item['id']}"); + } + } + } + else { + foreach ($result as $item) { + $bao = new $baoName(); + $bao->id = $item['id']; + // delete it + $action_result = $bao->delete(); + if ($action_result) { + $ids[] = $item['id']; + } + else { + throw new \API_Exception("Could not delete {$this->getEntity()} id {$item['id']}"); + } + } + } + $result->exchangeArray($ids); + return $result; + } + + /** + * @inheritDoc + */ + public function getParamInfo($param = NULL) { + $info = parent::getParamInfo($param); + if (!$param) { + // Delete doesn't actually let you select fields. + unset($info['select']); + } + return $info; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/Get.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/Get.php new file mode 100644 index 00000000000..c206a87df8e --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/Get.php @@ -0,0 +1,148 @@ + $matches[1]]; + if (!$this->select || $this->select != ['name']) { + $this->addDocs($entity); + } + $entities[$matches[1]] = $entity; + } + } + } + unset($entities['CustomValue']); + + if ($this->includeCustom) { + $this->addCustomEntities($entities); + } + + ksort($entities); + if ($this->select) { + foreach ($entities as &$entity) { + $entity = array_intersect_key($entity, array_flip($this->select)); + } + } + $result->exchangeArray(array_values($entities)); + } + + /** + * Add custom-field pseudo-entities + * + * @param $entities + * @throws \API_Exception + */ + private function addCustomEntities(&$entities) { + $customEntities = CustomGroup::get() + ->addWhere('is_multiple', '=', 1) + ->addWhere('is_active', '=', 1) + ->setSelect(['name', 'title', 'help_pre', 'help_post', 'extends']) + ->setCheckPermissions(FALSE) + ->execute(); + foreach ($customEntities as $customEntity) { + $fieldName = 'Custom_' . $customEntity['name']; + $entities[$fieldName] = [ + 'name' => $fieldName, + 'description' => $customEntity['title'] . ' custom group - extends ' . $customEntity['extends'], + ]; + if (!empty($customEntity['help_pre'])) { + $entities[$fieldName]['comment'] = $this->plainTextify($customEntity['help_pre']); + } + if (!empty($customEntity['help_post'])) { + $pre = empty($entities[$fieldName]['comment']) ? '' : $entities[$fieldName]['comment'] . "\n\n"; + $entities[$fieldName]['comment'] = $pre . $this->plainTextify($customEntity['help_post']); + } + } + } + + /** + * Convert html to plain text. + * + * @param $input + * @return mixed + */ + private function plainTextify($input) { + return html_entity_decode(strip_tags($input), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } + + /** + * Add info from code docblock. + * + * @param $entity + */ + private function addDocs(&$entity) { + $reflection = new \ReflectionClass("\\Civi\\Api4\\" . $entity['name']); + $entity += ReflectionUtils::getCodeDocs($reflection); + unset($entity['package'], $entity['method']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/GetFields.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/GetFields.php new file mode 100644 index 00000000000..74806ecc362 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/GetFields.php @@ -0,0 +1,64 @@ +getAction(); + $includeCustom = $this->getIncludeCustom(); + $entities = \Civi\Api4\Entity::get()->execute(); + foreach ($entities as $entity) { + $entity = ((array) $entity) + ['fields' => []]; + // Prevent infinite recursion + if ($entity['name'] != 'Entity') { + $entity['fields'] = (array) civicrm_api4($entity['name'], 'getFields', ['action' => $action, 'includeCustom' => $includeCustom, 'select' => $this->select]); + } + $result[] = $entity; + } + } + + /** + * @inheritDoc + */ + public function getParamInfo($param = NULL) { + $info = parent::getParamInfo($param); + if (!$param) { + // This action doesn't actually let you select fields. + unset($info['fields']); + } + return $info; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/GetLinks.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/GetLinks.php new file mode 100644 index 00000000000..fc8cca4984a --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Entity/GetLinks.php @@ -0,0 +1,62 @@ +get('schema_map'); + foreach ($schema->getTables() as $table) { + $entity = AllTables::getBriefName(AllTables::getClassForTable($table->getName())); + // Since this is an api function, exclude tables that don't have an api + if (class_exists('\Civi\Api4\\' . $entity)) { + $item = [ + 'entity' => $entity, + 'table' => $table->getName(), + 'links' => [], + ]; + foreach ($table->getTableLinks() as $link) { + $link = $link->toArray(); + $link['entity'] = AllTables::getBriefName(AllTables::getClassForTable($link['targetTable'])); + $item['links'][] = $link; + } + $result[] = $item; + } + } + return $result; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Get.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Get.php new file mode 100644 index 00000000000..97dd13cfaf5 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Get.php @@ -0,0 +1,143 @@ +addWhere('contact_type', 'IN', array('Individual', 'Household')) + * + * @var array + */ + protected $where = []; + /** + * Array of field(s) to use in ordering the results + * + * Defaults to id ASC + * + * $example->addOrderBy('sort_name', 'ASC') + * + * @var array + */ + protected $orderBy = []; + /** + * Maximum number of results to return. + * + * Defaults to unlimited. + * + * @var int + */ + protected $limit = 0; + /** + * Zero-based index of first result to return. + * + * Defaults to "0" - first record. + * + * @var int + */ + protected $offset = 0; + + /** + * @param string $field + * @param string $op + * @param mixed $value + * @return $this + * @throws \API_Exception + */ + public function addWhere($field, $op, $value = NULL) { + if (!in_array($op, \CRM_Core_DAO::acceptedSQLOperators())) { + throw new \API_Exception('Unsupported operator'); + } + $this->where[] = [$field, $op, $value]; + return $this; + } + + /** + * Adds one or more AND/OR/NOT clause groups + * + * @param string $operator + * @param mixed $condition1 ... $conditionN + * Either a nested array of arguments, or a variable number of arguments passed to this function. + * + * @return $this + * @throws \API_Exception + */ + public function addClause($operator, $condition1) { + if (!is_array($condition1[0])) { + $condition1 = array_slice(func_get_args(), 1); + } + $this->where[] = [$operator, $condition1]; + return $this; + } + + /** + * @param string $field + * @param string $direction + * @return $this + */ + public function addOrderBy($field, $direction = 'ASC') { + $this->orderBy[$field] = $direction; + return $this; + } + + public function _run(Result $result) { + $query = new Api4SelectQuery($this->getEntity(), $this->checkPermissions); + $query->select = $this->select; + $query->where = $this->where; + $query->orderBy = $this->orderBy; + $query->limit = $this->limit; + $query->offset = $this->offset; + $result->exchangeArray($query->run()); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/GetActions.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/GetActions.php new file mode 100644 index 00000000000..0cb6e2acf11 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/GetActions.php @@ -0,0 +1,109 @@ +getEntity()); + // First search entity-specific actions (including those provided by extensions + foreach ($includePaths as $path) { + $dir = \CRM_Utils_File::addTrailingSlash($path) . 'Civi/Api4/Action/' . $this->getEntity(); + $this->scanDir($dir); + } + // Scan all generic actions unless this entity does not extend generic entity + if ($entityReflection->getParentClass()) { + foreach ($includePaths as $path) { + $dir = \CRM_Utils_File::addTrailingSlash($path) . 'Civi/Api4/Action'; + $this->scanDir($dir); + } + } + // For oddball entities, just return their static methods + else { + foreach ($entityReflection->getMethods(\ReflectionMethod::IS_STATIC) as $method) { + $this->loadAction($method->getName()); + } + } + $result->exchangeArray(array_values($this->_actions)); + } + + /** + * @param $dir + */ + private function scanDir($dir) { + if (is_dir($dir)) { + foreach (glob("$dir/*.php") as $file) { + $matches = []; + preg_match('/(\w*).php/', $file, $matches); + $actionName = array_pop($matches); + if ($actionName !== 'AbstractAction') { + $this->loadAction(lcfirst($actionName)); + } + } + } + } + + /** + * @param $actionName + */ + private function loadAction($actionName) { + try { + if (!isset($this->_actions[$actionName])) { + /* @var AbstractAction $action */ + $action = call_user_func(["\\Civi\\Api4\\" . $this->getEntity(), $actionName]); + if (is_object($action)) { + $actionReflection = new \ReflectionClass($action); + $actionInfo = ReflectionUtils::getCodeDocs($actionReflection); + unset($actionInfo['method']); + $this->_actions[$actionName] = ['name' => $actionName] + $actionInfo; + $this->_actions[$actionName]['params'] = $action->getParamInfo(); + } + } + } + catch (NotImplementedException $e) { + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/GetFields.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/GetFields.php new file mode 100644 index 00000000000..321ea042fd6 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/GetFields.php @@ -0,0 +1,116 @@ +get('spec_gatherer'); + // Any fields name with a dot in it is custom + if ($this->fields) { + $this->includeCustom = strpos(implode('', $this->fields), '.') !== FALSE; + } + $spec = $gatherer->getSpec($this->getEntity(), $this->getAction(), $this->includeCustom); + $fields = SpecFormatter::specToArray($spec->getFields($this->fields), (array) $this->select, $this->getOptions); + // Fixme - $this->action ought to already be set. Might be a name conflict upstream causing it to be nullified? + $result->action = 'getFields'; + $result->exchangeArray(array_values($fields)); + } + + /** + * @return string + */ + public function getAction() { + return $this->action; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Navigation/Get.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Navigation/Get.php new file mode 100644 index 00000000000..f42f623c0f8 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Navigation/Get.php @@ -0,0 +1,46 @@ +addWhere('contact_id.contact_type', 'IN', array('Individual', 'Household')) + */ + protected $where = [ + ['is_test', '=', 0], + ]; + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Replace.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Replace.php new file mode 100644 index 00000000000..60b11b15c89 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Replace.php @@ -0,0 +1,91 @@ +indexBy('id'); + $saved = []; + + // Save all items + foreach ($this->records as $idx => $record) { + $saved[] = $this->writeObject($record); + if (!empty($record['id'])) { + unset($toDelete[$record['id']]); + } + } + + if ($toDelete) { + civicrm_api4($this->getEntity(), 'Delete', ['where' => [['id', 'IN', array_keys($toDelete)]]]); + } + $result->deleted = array_keys($toDelete); + $result->exchangeArray($saved); + } + + /** + * @inheritDoc + */ + public function getParamInfo($param = NULL) { + $info = parent::getParamInfo($param); + if (!$param) { + // This action doesn't actually let you select fields. + unset($info['select']); + } + return $info; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Update.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Update.php new file mode 100644 index 00000000000..6d2222cb136 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Action/Update.php @@ -0,0 +1,103 @@ + value pairs. + * @method $this addValue($field, $value) Set field value to update. + */ +class Update extends Get { + + /** + * Criteria for get to fetch id against which the update will occur + * + * @var array + */ + protected $select = ['id']; + + /** + * Criteria for selecting items to update. + * + * @required + * @var array + */ + protected $where = []; + + /** + * Field values to update. + * + * @var array + */ + protected $values = []; + + /** + * @param $key + * + * @return mixed|null + */ + public function getValue($key) { + return isset($this->values[$key]) ? $this->values[$key] : NULL; + } + + /** + * @inheritDoc + */ + public function _run(Result $result) { + if (!empty($this->values['id'])) { + throw new \Exception('Cannot update the id of an existing object.'); + } + // For some reason the contact bao requires this + if ($this->getEntity() == 'Contact') { + $this->select[] = 'contact_type'; + } + parent::_run($result); + // Then act on the result + $updated_results = []; + foreach ($result as $item) { + $updated_results[] = $this->writeObject($this->values + $item); + } + $result->exchangeArray($updated_results); + } + + /** + * @inheritDoc + */ + public function getParamInfo($param = NULL) { + $info = parent::getParamInfo($param); + if (!$param) { + // Update doesn't actually let you select fields. + unset($info['select']); + } + return $info; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Activity.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Activity.php new file mode 100644 index 00000000000..778c1c40141 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Activity.php @@ -0,0 +1,13 @@ +request = $request; + } + + /** + * @return AbstractAction + */ + public function getRequest() { + return $this->request; + } + + /** + * @param $request + */ + public function setRequest(AbstractAction $request) { + $this->request = $request; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/PostSelectQueryEvent.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/PostSelectQueryEvent.php new file mode 100644 index 00000000000..4489033b223 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/PostSelectQueryEvent.php @@ -0,0 +1,64 @@ +results = $results; + $this->query = $query; + } + + /** + * @return array + */ + public function getResults() { + return $this->results; + } + + /** + * @param array $results + * @return $this + */ + public function setResults($results) { + $this->results = $results; + + return $this; + } + + /** + * @return Api4SelectQuery + */ + public function getQuery() { + return $this->query; + } + + /** + * @param Api4SelectQuery $query + * @return $this + */ + public function setQuery($query) { + $this->query = $query; + + return $this; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/SchemaMapBuildEvent.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/SchemaMapBuildEvent.php new file mode 100644 index 00000000000..f79f6b4be86 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/SchemaMapBuildEvent.php @@ -0,0 +1,39 @@ +schemaMap = $schemaMap; + } + + /** + * @return SchemaMap + */ + public function getSchemaMap() { + return $this->schemaMap; + } + + /** + * @param SchemaMap $schemaMap + * + * @return $this + */ + public function setSchemaMap($schemaMap) { + $this->schemaMap = $schemaMap; + + return $this; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/AbstractPrepareSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/AbstractPrepareSubscriber.php new file mode 100644 index 00000000000..d4725e0dc82 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/AbstractPrepareSubscriber.php @@ -0,0 +1,24 @@ + 'onApiPrepare', + ]; + } + + /** + * @param PrepareEvent $event + */ + abstract public function onApiPrepare(PrepareEvent $event); + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivityPreCreationSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivityPreCreationSubscriber.php new file mode 100644 index 00000000000..2067972ecb5 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivityPreCreationSubscriber.php @@ -0,0 +1,40 @@ +getValue('activity_type'); + if ($activityType) { + $result = OptionValue::get() + ->setCheckPermissions(FALSE) + ->addWhere('name', '=', $activityType) + ->addWhere('option_group.name', '=', 'activity_type') + ->execute(); + + if ($result->count() !== 1) { + throw new \Exception('Activity type must match a *single* type'); + } + + $request->addValue('activity_type_id', $result->first()['id']); + } + } + + /** + * @param Create $request + * + * @return bool + */ + protected function applies(Create $request) { + return $request->getEntity() === 'Activity'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivitySchemaMapSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivitySchemaMapSubscriber.php new file mode 100644 index 00000000000..52d58397d89 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivitySchemaMapSubscriber.php @@ -0,0 +1,40 @@ + 'onSchemaBuild', + ]; + } + + /** + * @param SchemaMapBuildEvent $event + */ + public function onSchemaBuild(SchemaMapBuildEvent $event) { + $schema = $event->getSchemaMap(); + $table = $schema->getTableByName('civicrm_activity'); + + $middleAlias = StringHelper::createRandom(10, implode(range('a', 'z'))); + $middleLink = new ActivityToActivityContactAssigneesJoinable($middleAlias); + + $bridge = new BridgeJoinable('civicrm_contact', 'id', 'assignees', $middleLink); + $bridge->setBaseTable('civicrm_activity_contact'); + $bridge->setJoinType(Joinable::JOIN_TYPE_ONE_TO_MANY); + + $table->addTableLink('contact_id', $bridge); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php new file mode 100644 index 00000000000..edea3de6599 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php @@ -0,0 +1,54 @@ + 'onSchemaBuild', + ]; + } + + /** + * @param SchemaMapBuildEvent $event + */ + public function onSchemaBuild(SchemaMapBuildEvent $event) { + $schema = $event->getSchemaMap(); + $table = $schema->getTableByName('civicrm_contact'); + $this->addCreatedActivitiesLink($table); + $this->fixPreferredLanguageAlias($table); + } + + /** + * @param Table $table + */ + private function addCreatedActivitiesLink($table) { + $alias = 'created_activities'; + $joinable = new Joinable('civicrm_activity_contact', 'contact_id', $alias); + $joinable->addCondition($alias . '.record_type_id = 1'); + $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY); + $table->addTableLink('id', $joinable); + } + + /** + * @param Table $table + */ + private function fixPreferredLanguageAlias($table) { + foreach ($table->getExternalLinks() as $link) { + if ($link->getAlias() === 'languages') { + $link->setAlias('preferred_language'); + return; + } + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomFieldPreCreationSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomFieldPreCreationSubscriber.php new file mode 100644 index 00000000000..c5dee8124fd --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomFieldPreCreationSubscriber.php @@ -0,0 +1,91 @@ +formatOptionParams($request); + $this->setDefaults($request); + } + + /** + * @param Create $request + * + * @return bool + */ + protected function applies(Create $request) { + return $request->getEntity() === 'CustomField'; + } + + /** + * Sets defaults required for option group and value creation + * @see CRM_Core_BAO_CustomField::create() + * + * @param Create $request + */ + protected function formatOptionParams(Create $request) { + $options = $request->getValue('options'); + + if (!is_array($options)) { + return; + } + + $dataTypeKey = 'data_type'; + $optionLabelKey = 'option_label'; + $optionWeightKey = 'option_weight'; + $optionStatusKey = 'option_status'; + $optionValueKey = 'option_value'; + $optionTypeKey = 'option_type'; + + $dataType = $request->getValue($dataTypeKey); + $optionLabel = $request->getValue($optionLabelKey); + $optionWeight = $request->getValue($optionWeightKey); + $optionStatus = $request->getValue($optionStatusKey); + $optionValue = $request->getValue($optionValueKey); + $optionType = $request->getValue($optionTypeKey); + + if (!$optionType) { + $request->addValue($optionTypeKey, self::OPTION_TYPE_NEW); + } + + if (!$dataType) { + $request->addValue($dataTypeKey, 'String'); + } + + if (!$optionLabel) { + $request->addValue($optionLabelKey, array_values($options)); + } + + if (!$optionValue) { + $request->addValue($optionValueKey, array_keys($options)); + } + + if (!$optionStatus) { + $statuses = array_fill(0, count($options), self::OPTION_STATUS_ACTIVE); + $request->addValue($optionStatusKey, $statuses); + } + + if (!$optionWeight) { + $request->addValue($optionWeightKey, range(1, count($options))); + } + } + + /** + * @param Create $request + */ + private function setDefaults(Create $request) { + if (!$request->getValue('option_type')) { + $request->addValue('option_type', NULL); + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomGroupPreCreationSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomGroupPreCreationSubscriber.php new file mode 100644 index 00000000000..bcbaf395975 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomGroupPreCreationSubscriber.php @@ -0,0 +1,29 @@ +getValue('extends'); + $title = $request->getValue('title'); + $name = $request->getValue('name'); + + if (is_string($extends)) { + $request->addValue('extends', [$extends]); + } + + if (NULL === $title && $name) { + $request->addValue('title', $name); + } + } + + protected function applies(Create $request) { + return $request->getEntity() === 'CustomGroup'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/OptionValuePreCreationSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/OptionValuePreCreationSubscriber.php new file mode 100644 index 00000000000..7d3633c8996 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/OptionValuePreCreationSubscriber.php @@ -0,0 +1,50 @@ +setOptionGroupId($request); + } + + /** + * @param Create $request + * + * @return bool + */ + protected function applies(Create $request) { + return $request->getEntity() === 'OptionValue'; + } + + /** + * @param Create $request + * @throws \API_Exception + * @throws \Exception + */ + private function setOptionGroupId(Create $request) { + $optionGroupName = $request->getValue('option_group'); + if (!$optionGroupName || $request->getValue('option_group_id')) { + return; + } + + $optionGroup = OptionGroup::get() + ->setCheckPermissions(FALSE) + ->addSelect('id') + ->addWhere('name', '=', $optionGroupName) + ->execute(); + + if ($optionGroup->count() !== 1) { + throw new \Exception('Option group name must match only a single group'); + } + + $request->addValue('option_group_id', $optionGroup->first()['id']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PermissionCheckSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PermissionCheckSubscriber.php new file mode 100644 index 00000000000..62d542d0e0c --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PermissionCheckSubscriber.php @@ -0,0 +1,65 @@ + [ + ['onApiAuthorize', Events::W_LATE], + ], + ]; + } + + /** + * @param \Civi\API\Event\AuthorizeEvent $event + * API authorization event. + */ + public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) { + /* @var \Civi\Api4\Generic\AbstractAction $apiRequest */ + $apiRequest = $event->getApiRequest(); + if ($apiRequest['version'] == 4) { + if (!$apiRequest->getCheckPermissions() || $apiRequest->isAuthorized()) { + $event->authorize(); + $event->stopPropagation(); + } + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php new file mode 100644 index 00000000000..ff7e6d20d61 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php @@ -0,0 +1,369 @@ + 'onPostQuery' + ]; + } + + /** + * @param PostSelectQueryEvent $event + */ + public function onPostQuery(PostSelectQueryEvent $event) { + $results = $event->getResults(); + $event->setResults($this->postRun($results, $event->getQuery())); + } + + /** + * @param array $results + * @param Api4SelectQuery $query + * + * @return array + */ + protected function postRun(array $results, Api4SelectQuery $query) { + if (empty($results)) { + return $results; + } + + $fieldSpec = $query->getApiFieldSpec(); + $this->unserializeFields($results, $query->getEntity(), $fieldSpec); + + // Group the selects to avoid queries for each field + $groupedSelects = $this->getJoinedDotSelects($query); + foreach ($groupedSelects as $finalAlias => $selects) { + $joinPath = $this->getJoinPathInfo($selects[0], $query); + $selects = $this->formatSelects($finalAlias, $selects, $query); + $joinResults = $this->getJoinResults($query, $finalAlias, $selects); + $this->formatJoinResults($joinResults, $query, $finalAlias); + + // Insert join results into original result + foreach ($results as &$primaryResult) { + $baseId = $primaryResult['id']; + $filtered = array_filter($joinResults, function ($res) use ($baseId) { + return ($res['_base_id'] === $baseId); + }); + $filtered = array_values($filtered); + ArrayInsertionUtil::insert($primaryResult, $joinPath, $filtered); + } + } + + return array_values($results); + } + + /** + * @param array $joinResults + * @param Api4SelectQuery $query + * @param string $alias + */ + private function formatJoinResults(&$joinResults, $query, $alias) { + $join = $query->getJoinedTable($alias); + $fields = []; + foreach ($join->getEntityFields() as $field) { + $name = explode('.', $field->getName()); + $fields[array_pop($name)] = $field->toArray(); + } + if ($fields) { + $this->unserializeFields($joinResults, NULL, $fields); + } + } + + /** + * Unserialize values + * + * @param array $results + * @param string $entity + * @param array $fields + */ + protected function unserializeFields(&$results, $entity, $fields = []) { + if (empty($fields)) { + $params = ['action' => 'get', 'includeCustom' => FALSE]; + $fields = civicrm_api4($entity, 'getFields', $params)->indexBy('name'); + } + + foreach ($results as &$result) { + foreach ($result as $field => &$value) { + if (!empty($fields[$field]['serialize']) && is_string($value)) { + $serializationType = $fields[$field]['serialize']; + $value = \CRM_Core_DAO::unSerializeField($value, $serializationType); + } + } + } + } + + /** + * @param Api4SelectQuery $query + * + * @return array + */ + private function getJoinedDotSelects(Api4SelectQuery $query) { + // Remove selects that are not in a joined table + $fkAliases = $query->getFkSelectAliases(); + $joinedDotSelects = array_filter( + $query->getSelect(), + function ($select) use ($fkAliases) { + return isset($fkAliases[$select]); + } + ); + + $selects = []; + // group related selects by alias so they can be executed in one query + foreach ($joinedDotSelects as $select) { + $parts = explode('.', $select); + $finalAlias = $parts[count($parts) - 2]; + $selects[$finalAlias][] = $select; + } + + // sort by depth, e.g. email selects should be done before email.location + uasort($selects, function ($a, $b) { + $aFirst = $a[0]; + $bFirst = $b[0]; + return substr_count($aFirst, '.') > substr_count($bFirst, '.'); + }); + + return $selects; + } + + + /** + * @param array $selects + * @param $serializationType + * @param Api4SelectQuery $query + * + * @return array + */ + private function getResultsForSerializedField( + array $selects, + $serializationType, + Api4SelectQuery $query + ) { + // Get the alias (Selects are grouped and all target the same table) + $sampleField = current($selects); + $alias = strstr($sampleField, '.', TRUE); + + // Fetch the results with the serialized field + $selects['serialized'] = $query::MAIN_TABLE_ALIAS . '.' . $alias; + $serializedResults = $this->runWithNewSelects($selects, $query); + $newResults = []; + + // Create a new results array, with a separate entry for each option value + foreach ($serializedResults as $result) { + $optionValues = \CRM_Core_DAO::unSerializeField( + $result['serialized'], + $serializationType + ); + unset($result['serialized']); + foreach ($optionValues as $value) { + $newResults[] = array_merge($result, ['value' => $value]); + } + } + + $optionValueValues = array_unique(array_column($newResults, 'value')); + $optionValues = $this->getOptionValuesFromValues( + $selects, + $query, + $optionValueValues + ); + $valueField = $alias . '.value'; + + // Index by value + foreach ($optionValues as $key => $subResult) { + $optionValues[$subResult['value']] = $subResult; + unset($subResult[$key]); + + // Exclude 'value' if not in original selects + if (!in_array($valueField, $selects)) { + unset($optionValues[$subResult['value']]['value']); + } + } + + // Replace serialized with the sub-select results + foreach ($newResults as &$result) { + $result = array_merge($result, $optionValues[$result['value']]); + unset($result['value']); + } + + return $newResults; + } + + + /** + * Prepares selects for the subquery to fetch join results + * + * @param string $alias + * @param array $selects + * @param Api4SelectQuery $query + * + * @return array + */ + private function formatSelects($alias, $selects, Api4SelectQuery $query) { + $mainAlias = $query::MAIN_TABLE_ALIAS; + $selectFields = []; + + foreach ($selects as $select) { + $selectAlias = $query->getFkSelectAliases()[$select]; + $fieldAlias = substr($select, strrpos($select, '.') + 1); + $selectFields[$fieldAlias] = $selectAlias; + } + + $firstSelect = $selects[0]; + $pathParts = explode('.', $firstSelect); + $numParts = count($pathParts); + $parentAlias = $numParts > 2 ? $pathParts[$numParts - 3] : $mainAlias; + + $selectFields['id'] = sprintf('%s.id', $alias); + $selectFields['_parent_id'] = $parentAlias . '.id'; + $selectFields['_base_id'] = $mainAlias . '.id'; + + return $selectFields; + } + + /** + * @param array $selects + * @param Api4SelectQuery $query + * + * @return array + */ + private function runWithNewSelects(array $selects, Api4SelectQuery $query) { + $aliasedSelects = array_map(function ($field, $alias) { + return sprintf('%s as "%s"', $field, $alias); + }, $selects, array_keys($selects)); + + $newSelect = sprintf('SELECT DISTINCT %s', implode(", ", $aliasedSelects)); + $sql = str_replace("\n", ' ', $query->getQuery()->toSQL()); + $originalSelect = substr($sql, 0, strpos($sql, ' FROM')); + $sql = str_replace($originalSelect, $newSelect, $sql); + + $relatedResults = []; + $resultDAO = \CRM_Core_DAO::executeQuery($sql); + while ($resultDAO->fetch()) { + $relatedResult = []; + foreach ($selects as $alias => $column) { + $returnName = $alias; + $alias = str_replace('.', '_', $alias); + if (property_exists($resultDAO, $alias)) { + $relatedResult[$returnName] = $resultDAO->$alias; + } + }; + $relatedResults[] = $relatedResult; + } + + return $relatedResults; + } + + /** + * @param Api4SelectQuery $query + * @param $alias + * @param $selects + * @return array + */ + protected function getJoinResults(Api4SelectQuery $query, $alias, $selects) { + $apiFieldSpec = $query->getApiFieldSpec(); + if (!empty($apiFieldSpec[$alias]['serialize'])) { + $type = $apiFieldSpec[$alias]['serialize']; + $joinResults = $this->getResultsForSerializedField($selects, $type, $query); + } + else { + $joinResults = $this->runWithNewSelects($selects, $query); + } + + // Remove results with no matching entries + $joinResults = array_filter($joinResults, function ($result) { + return !empty($result['id']); + }); + + return $joinResults; + } + + /** + * Separates a string like 'emails.location_type.label' into an array, where + * each value in the array tells whether it is 1-1 or 1-n join type + * + * @param string $pathString + * Dot separated path to the field + * @param Api4SelectQuery $query + * + * @return array + * Index is table alias and value is boolean whether is 1-to-many join + */ + private function getJoinPathInfo($pathString, $query) { + $pathParts = explode('.', $pathString); + array_pop($pathParts); // remove field + $path = []; + $isMultipleChecker = function($alias) use ($query) { + foreach ($query->getJoinedTables() as $table) { + if ($table->getAlias() === $alias) { + return $table->getJoinType() === Joinable::JOIN_TYPE_ONE_TO_MANY; + } + } + return FALSE; + }; + + foreach ($pathParts as $part) { + $path[$part] = $isMultipleChecker($part); + } + + return $path; + } + + /** + * Get all the option_value values required in the query + * + * @param array $selects + * @param Api4SelectQuery $query + * @param array $values + * + * @return array + */ + private function getOptionValuesFromValues( + array $selects, + Api4SelectQuery $query, + array $values + ) { + $sampleField = current($selects); + $alias = strstr($sampleField, '.', TRUE); + + // Get the option value table that was joined + $relatedTable = NULL; + foreach ($query->getJoinedTables() as $joinedTable) { + if ($joinedTable->getAlias() === $alias) { + $relatedTable = $joinedTable; + } + } + + // We only want subselects related to the joined table + $subSelects = array_filter($selects, function ($select) use ($alias) { + return strpos($select, $alias) === 0; + }); + + // Fetch all related option_value entries + $valueField = $alias . '.value'; + $subSelects[] = $valueField; + $tableName = $relatedTable->getTargetTable(); + $conditions = $relatedTable->getExtraJoinConditions(); + $conditions[] = $valueField . ' IN ("' . implode('", "', $values) . '")'; + $subQuery = new \CRM_Utils_SQL_Select($tableName . ' ' . $alias); + $subQuery->where($conditions); + $subQuery->select($subSelects); + $subResults = $subQuery->execute()->fetchAll(); + + return $subResults; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PreCreationSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PreCreationSubscriber.php new file mode 100644 index 00000000000..13f54a99c3a --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PreCreationSubscriber.php @@ -0,0 +1,53 @@ +getApiRequest(); + if (!$apiRequest instanceof Create) { + return; + } + + $this->addDefaultCreationValues($apiRequest); + if ($this->applies($apiRequest)) { + $this->modify($apiRequest); + } + } + + /** + * Modify the request + * + * @param Create $request + * + * @return void + */ + abstract protected function modify(Create $request); + + /** + * Check if this subscriber should be applied to the request + * + * @param Create $request + * + * @return bool + */ + abstract protected function applies(Create $request); + + /** + * Sets default values common to all creation requests + * + * @param Create $request + */ + protected function addDefaultCreationValues(Create $request) { + if (NULL === $request->getValue('is_active')) { + $request->addValue('is_active', 1); + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ValidateFieldsSubscriber.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ValidateFieldsSubscriber.php new file mode 100644 index 00000000000..f73edf46c7f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ValidateFieldsSubscriber.php @@ -0,0 +1,97 @@ +getApiRequest(); + if (is_a($apiRequest, 'Civi\Api4\Generic\AbstractAction')) { + $paramInfo = $apiRequest->getParamInfo(); + foreach ($paramInfo as $param => $info) { + $getParam = 'get' . ucfirst($param); + $value = $apiRequest->$getParam(); + // Required fields + if (!empty($info['required']) && (!$value && $value !== 0 && $value !== '0')) { + throw new \API_Exception('Parameter "' . $param . '" is required.'); + } + if (!empty($info['type']) && !self::checkType($value, $info['type'])) { + throw new \API_Exception('Parameter "' . $param . '" is not of the correct type. Expecting ' . implode(' or ', $info['type']) . '.'); + } + } + } + } + + /** + * Validate variable type on input + * + * @param $value + * @param $types + * @return bool + * @throws \API_Exception + */ + public static function checkType($value, $types) { + if ($value === NULL) { + return TRUE; + } + foreach ($types as $type) { + switch ($type) { + case 'array': + case 'bool': + case 'string': + case 'object': + $tester = 'is_' . $type; + if ($tester($value)) { + return TRUE; + } + break; + + case 'int': + if (\CRM_Utils_Rule::integer($value)) { + return TRUE; + } + break; + + default: + throw new \API_Exception('Unknown paramater type: ' . $type); + } + } + return FALSE; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php new file mode 100644 index 00000000000..574b60ce71a --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php @@ -0,0 +1,514 @@ +entity = $entity; + $this->thisReflection = new \ReflectionClass($this); + } + + /** + * Strictly enforce api parameters + * @param $name + * @param $value + * @throws \Exception + */ + public function __set($name, $value) { + throw new \API_Exception('Unknown api parameter'); + } + + /** + * @throws \API_Exception + */ + public function setVersion() { + throw new \API_Exception('Cannot modify api version'); + } + + /** + * Magic function to provide addFoo, getFoo and setFoo for params. + * + * @param $name + * @param $arguments + * @return static|mixed + * @throws \API_Exception + */ + public function __call($name, $arguments) { + $param = lcfirst(substr($name, 3)); + $mode = substr($name, 0, 3); + // Handle plural when adding to e.g. $values with "addValue" method. + if ($mode == 'add' && $this->paramExists($param . 's')) { + $param .= 's'; + } + if ($this->paramExists($param)) { + switch ($mode) { + case 'get': + return $this->$param; + + case 'set': + if (is_array($this->$param)) { + // Don't overwrite any defaults + $this->$param = $arguments[0] + $this->$param; + } + else { + $this->$param = $arguments[0]; + } + return $this; + + case 'add': + if (!is_array($this->$param)) { + throw new \API_Exception('Cannot add to non-array param'); + } + if (array_key_exists(1, $arguments)) { + $this->{$param}[$arguments[0]] = $arguments[1]; + } + else { + $this->{$param}[] = $arguments[0]; + } + return $this; + } + } + throw new \API_Exception('Unknown api parameter: ' . $name); + } + + /** + * Invoke api call. + * + * At this point all the params have been sent in and we initiate the api call & return the result. + * This is basically the outer wrapper for api v4. + * + * @return Result|array + * @throws UnauthorizedException + */ + final public function execute() { + /** @var Kernel $kernel */ + $kernel = \Civi::service('civi_api_kernel'); + + return $kernel->runRequest($this); + } + + /** + * @param \Civi\Api4\Generic\Result $result + */ + abstract public function _run(Result $result); + + /** + * Serialize this object's params into an array + * @return array + */ + public function getParams() { + $params = []; + foreach ($this->thisReflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $property) { + $name = $property->getName(); + $params[$name] = $this->$name; + } + return $params; + } + + /** + * @param $customGroup + * @return static + */ + public function setCustomGroup($customGroup) { + $this->customGroup = $customGroup; + return $this; + } + + /** + * @return string + */ + public function getCustomGroup() { + return $this->customGroup; + } + + /** + * Get documentation for one or all params + * + * @param string $param + * @return array of arrays [description, type, default, (comment)] + */ + public function getParamInfo($param = NULL) { + if (!isset($this->thisParamInfo)) { + $defaults = $this->getParamDefaults(); + foreach ($this->thisReflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $property) { + $name = $property->getName(); + if ($name != 'version') { + $this->thisParamInfo[$name] = ReflectionUtils::getCodeDocs($property, 'Property'); + $this->thisParamInfo[$name]['default'] = $defaults[$name]; + } + } + } + return $param ? $this->thisParamInfo[$param] : $this->thisParamInfo; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * + * @return string + */ + public function getAction() { + $name = get_class($this); + return lcfirst(substr($name, strrpos($name, '\\') + 1)); + } + + /** + * @param string $param + * @return bool + */ + protected function paramExists($param) { + return array_key_exists($param, $this->getParams()); + } + + /** + * @return array + */ + protected function getParamDefaults() { + return array_intersect_key($this->thisReflection->getDefaultProperties(), $this->getParams()); + } + + /** + * @return \CRM_Core_DAO|string + */ + protected function getBaoName() { + require_once 'api/v3/utils.php'; + return \_civicrm_api3_get_BAO($this->getEntity()); + } + + /** + * @inheritDoc + */ + public function offsetExists($offset) { + return in_array($offset, ['entity', 'action', 'params', 'version', 'check_permissions']) || isset($this->thisArrayStorage[$offset]); + } + + /** + * @inheritDoc + */ + public function &offsetGet($offset) { + $val = NULL; + if (in_array($offset, ['entity', 'action', 'params', 'version'])) { + $getter = 'get' . ucfirst($offset); + $val = $this->$getter(); + return $val; + } + if ($offset == 'check_permissions') { + return $this->checkPermissions; + } + if (isset ($this->thisArrayStorage[$offset])) { + return $this->thisArrayStorage[$offset]; + } + else { + return $val; + } + } + + /** + * @inheritDoc + */ + public function offsetSet($offset, $value) { + if (in_array($offset, ['entity', 'action', 'params', 'version'])) { + throw new \API_Exception('Cannot modify api4 state via array access'); + } + if ($offset == 'check_permissions') { + $this->setCheckPermissions($value); + } + else { + $this->thisArrayStorage[$offset] = $value; + } + } + + /** + * @inheritDoc + */ + public function offsetUnset($offset) { + if (in_array($offset, ['entity', 'action', 'params', 'check_permissions', 'version'])) { + throw new \API_Exception('Cannot modify api4 state via array access'); + } + unset($this->thisArrayStorage[$offset]); + } + + /** + * Extract the true fields from a BAO + * + * (Used by create and update actions) + * @param object $bao + * @return array + */ + public static function baoToArray($bao) { + $fields = $bao->fields(); + $values = []; + foreach ($fields as $key => $field) { + $name = $field['name']; + if (property_exists($bao, $name)) { + $values[$name] = $bao->$name; + } + } + return $values; + } + + /** + * Is this api call permitted? + * + * This function is called if checkPermissions is set to true. + * + * @return bool + */ + public function isAuthorized() { + $permissions = $this->getPermissions(); + return \CRM_Core_Permission::check($permissions); + } + + public function getPermissions() { + $permissions = call_user_func(["\\Civi\\Api4\\" . $this->entity, 'permissions']); + $permissions += [ + // applies to getFields, getActions, etc. + 'meta' => ['access CiviCRM'], + // catch-all, applies to create, get, delete, etc. + 'default' => ['administer CiviCRM'], + ]; + $action = $this->getAction(); + if (isset($permissions[$action])) { + return $permissions[$action]; + } + elseif (in_array($action, ['getActions', 'getFields'])) { + return $permissions['meta']; + } + return $permissions['default']; + } + + /** + * Write a bao object as part of a create/update action. + * + * @param $params + * @return array + * @throws \API_Exception + */ + protected function writeObject($params) { + $entityId = UtilsArray::value('id', $params); + FormattingUtil::formatWriteParams($params, $this->getEntity(), $this->getEntityFields()); + $this->formatCustomParams($params, $entityId); + + $baoName = $this->getBaoName(); + $bao = new $baoName(); + + // For some reason the contact bao requires this + if ($entityId && $this->getEntity() == 'Contact') { + $params['contact_id'] = $entityId; + } + // Some BAOs are weird and don't support a straightforward "create" method. + $oddballs = [ + 'Website' => 'add', + 'Address' => 'add', + ]; + $method = UtilsArray::value($this->getEntity(), $oddballs, 'create'); + if (!method_exists($bao, $method)) { + $method = 'add'; + } + if (method_exists($bao, $method)) { + $createResult = $bao->$method($params); + } + else { + $createResult = $this->genericCreateMethod($params); + } + + if (!$createResult) { + $errMessage = sprintf('%s write operation failed', $this->getEntity()); + throw new \API_Exception($errMessage); + } + + // trim back the junk and just get the array: + return $this->baoToArray($createResult); + } + + /** + * Fallback when a BAO does not contain create or add functions + * + * @param $params + * @return mixed + */ + private function genericCreateMethod($params) { + $baoName = $this->getBaoName(); + $hook = empty($params['id']) ? 'create' : 'edit'; + + \CRM_Utils_Hook::pre($hook, $this->getEntity(), UtilsArray::value('id', $params), $params); + /** @var \CRM_Core_DAO $instance */ + $instance = new $baoName(); + $instance->copyValues($params, TRUE); + $instance->save(); + \CRM_Utils_Hook::post($hook, $this->getEntity(), $instance->id, $instance); + + return $instance; + } + + /** + * Returns schema fields for this entity & action. + * + * @return array + * @throws \API_Exception + */ + public function getEntityFields() { + if (!$this->entityFields) { + $this->entityFields = civicrm_api4($this->getEntity(), 'getFields', ['action' => $this->getAction(), 'includeCustom' => FALSE]) + ->indexBy('name'); + } + return $this->entityFields; + } + + /** + * @param $params + * @param $entityId + * @return mixed + */ + private function formatCustomParams(&$params, $entityId) { + $customParams = []; + + // $customValueID is the ID of the custom value in the custom table for this + // entity (i guess this assumes it's not a multi value entity) + foreach ($params as $name => $value) { + if (strpos($name, '.') === FALSE) { + continue; + } + + list($customGroup, $customField) = explode('.', $name); + + $customFieldId = \CRM_Core_BAO_CustomField::getFieldValue( + \CRM_Core_DAO_CustomField::class, + $customField, + 'id', + 'name' + ); + $customFieldType = \CRM_Core_BAO_CustomField::getFieldValue( + \CRM_Core_DAO_CustomField::class, + $customField, + 'html_type', + 'name' + ); + $customFieldExtends = \CRM_Core_BAO_CustomGroup::getFieldValue( + \CRM_Core_DAO_CustomGroup::class, + $customGroup, + 'extends', + 'name' + ); + + // todo are we sure we don't want to allow setting to NULL? need to test + if ($customFieldId && NULL !== $value) { + + if ($customFieldType == 'CheckBox') { + // this function should be part of a class + formatCheckBoxField($value, 'custom_' . $customFieldId, $this->getEntity()); + } + + \CRM_Core_BAO_CustomField::formatCustomField( + $customFieldId, + $customParams, + $value, + $customFieldExtends, + NULL, // todo check when this is needed + $entityId, + FALSE, + FALSE, + TRUE + ); + } + } + + if ($customParams) { + $params['custom'] = $customParams; + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php new file mode 100644 index 00000000000..3d21dd99300 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php @@ -0,0 +1,96 @@ +setCustomGroup($args[0]); + } + return $actionObject; + } + + /** + * Returns a list of permissions needed to access the various actions in this api. + * + * @return array + */ + public static function permissions() { + // Get entity name from called class + $entity = substr(static::class, strrpos(static::class, '\\') + 1); + $permissions = \CRM_Core_Permission::getEntityActionPermissions(); + + // For legacy reasons the permissions are keyed by lowercase entity name + $lcentity = _civicrm_api_get_entity_name_from_camel($entity); + // Merge permissions for this entity with the defaults + return \CRM_Utils_Array::value($lcentity, $permissions, []) + $permissions['default']; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/Result.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/Result.php new file mode 100644 index 00000000000..fc855c7072d --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Generic/Result.php @@ -0,0 +1,84 @@ +exchangeArray($newResults); + } + return $this; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Group.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Group.php new file mode 100644 index 00000000000..97c6d4f584b --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Group.php @@ -0,0 +1,13 @@ + [ + ['onApiResolve', Events::W_EARLY], + ], + ]; + } + /** + * @param ResolveEvent $event + * API resolution event. + */ + public function onApiResolve(ResolveEvent $event) { + $apiRequest = $event->getApiRequest(); + if ($apiRequest instanceof AbstractAction) { + $event->setApiRequest($apiRequest); + $event->setApiProvider($this); + $event->stopPropagation(); + } + } + + /** + * @inheritDoc + * + * @param array|AbstractAction $apiRequest + * + * @return array|mixed + */ + public function invoke($apiRequest) { + $result = new Result(); + $result->action = $apiRequest['action']; + $result->entity = $apiRequest['entity']; + $apiRequest->_run($result); + return $result; + } + + /** + * @inheritDoc + * @param int $version + * @return array + */ + public function getEntityNames($version) { + /** FIXME */ + return []; + } + + /** + * @inheritDoc + * @param int $version + * @param string $entity + * @return array + */ + public function getActionNames($version, $entity) { + /** FIXME Civi\API\V4\Action\GetActions */ + return []; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Query/Api4SelectQuery.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Query/Api4SelectQuery.php new file mode 100644 index 00000000000..6cc30ad21be --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Query/Api4SelectQuery.php @@ -0,0 +1,514 @@ +=', '>', '<', 'LIKE', "<>", "!=", + * * "NOT LIKE", 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', + * * 'IS NOT NULL', or 'IS NULL'. + */ +class Api4SelectQuery extends SelectQuery { + + /** + * @var int + */ + protected $apiVersion = 4; + + /** + * @var array + * Maps select fields to [, ] + */ + protected $fkSelectAliases = []; + + /** + * @var Joinable[] + * The joinable tables that have been joined so far + */ + protected $joinedTables = []; + + /** + * @param string $entity + * @param bool $checkPermissions + */ + public function __construct($entity, $checkPermissions) { + require_once 'api/v3/utils.php'; + $this->entity = $entity; + + $baoName = CoreUtil::getDAOFromApiName($entity); + $bao = new $baoName(); + + $this->entityFieldNames = _civicrm_api3_field_names(_civicrm_api3_build_fields_array($bao)); + $this->apiFieldSpec = $this->getFields(); + + \CRM_Utils_SQL_Select::from($this->getTableName($baoName) . ' ' . self::MAIN_TABLE_ALIAS); + + // Add ACLs first to avoid redundant subclauses + $this->checkPermissions = $checkPermissions; + $this->query->where($this->getAclClause(self::MAIN_TABLE_ALIAS, $baoName)); + } + + /** + * Why walk when you can + * + * @return array|int + */ + public function run() { + $this->preRun(); + $baseResults = parent::run(); + $event = new PostSelectQueryEvent($baseResults, $this); + \Civi::dispatcher()->dispatch(Events::POST_SELECT_QUERY, $event); + + return $event->getResults(); + } + + /** + * Gets all FK fields and does the required joins + */ + protected function preRun() { + $whereFields = array_column($this->where, 0); + $allFields = array_merge($whereFields, $this->select, $this->orderBy); + $dotFields = array_unique(array_filter($allFields, function ($field) { + return strpos($field, '.') !== FALSE; + })); + + foreach ($dotFields as $dotField) { + $this->joinFK($dotField); + } + } + + /** + * Populate $this->selectFields + * + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function buildSelectFields() { + $return_all_fields = (empty($this->select) || !is_array($this->select)); + $return = $return_all_fields ? $this->entityFieldNames : $this->select; + if ($return_all_fields || in_array('custom', $this->select)) { + foreach (array_keys($this->apiFieldSpec) as $fieldName) { + if (strpos($fieldName, 'custom_') === 0) { + $return[] = $fieldName; + } + } + } + + // Always select the ID if the table has one. + if (array_key_exists('id', $this->apiFieldSpec) || strstr($this->entity, 'Custom_')) { + $this->selectFields[self::MAIN_TABLE_ALIAS . ".id"] = "id"; + } + + // core return fields + foreach ($return as $fieldName) { + $field = $this->getField($fieldName); + if ($field && in_array($field['name'], $this->entityFieldNames)) { + $this->selectFields[self::MAIN_TABLE_ALIAS . "." . UtilsArray::value('column_name', $field, $field['name'])] = $field['name']; + } + elseif (strpos($fieldName, '.')) { + $fkField = $this->addFkField($fieldName, 'LEFT'); + if ($fkField) { + $this->selectFields[implode('.', $fkField)] = $fieldName; + } + } + elseif ($field && strpos($fieldName, 'custom_') === 0) { + list($table_name, $column_name) = $this->addCustomField($field, 'LEFT'); + + if ($field['data_type'] != 'ContactReference') { + // 'ordinary' custom field. We will select the value as custom_XX. + $this->selectFields["$table_name.$column_name"] = $fieldName; + } + else { + // contact reference custom field. The ID will be stored in custom_XX_id. + // custom_XX will contain the sort name of the contact. + $this->query->join("c_$fieldName", "LEFT JOIN civicrm_contact c_$fieldName ON c_$fieldName.id = `$table_name`.`$column_name`"); + $this->selectFields["$table_name.$column_name"] = $fieldName . "_id"; + // We will call the contact table for the join c_XX. + $this->selectFields["c_$fieldName.sort_name"] = $fieldName; + } + } + } + } + + /** + * @inheritDoc + */ + protected function buildWhereClause() { + foreach ($this->where as $clause) { + $sql_clause = $this->treeWalkWhereClause($clause); + $this->query->where($sql_clause); + } + } + + /** + * @inheritDoc + */ + protected function buildOrderBy() { + foreach ($this->orderBy as $field => $dir) { + if ($dir !== 'ASC' && $dir !== 'DESC') { + throw new \API_Exception("Invalid sort direction. Cannot order by $field $dir"); + } + if ($this->getField($field)) { + $this->query->orderBy(self::MAIN_TABLE_ALIAS . '.' . $field . " $dir"); + } + // TODO: Handle joined fields, custom fields, etc. + else { + throw new \API_Exception("Invalid sort field. Cannot order by $field $dir"); + } + } + } + + /** + * Recursively validate and transform a branch or leaf clause array to SQL. + * + * @param array $clause + * @return string SQL where clause + * + * @uses validateClauseAndComposeSql() to generate the SQL etc. + * @todo if an 'and' is nested within and 'and' (or or-in-or) then should + * flatten that to be a single list of clauses. + */ + protected function treeWalkWhereClause($clause) { + switch ($clause[0]) { + case 'OR': + case 'AND': + // handle branches + if (count($clause[1]) === 1) { + // a single set so AND|OR is immaterial + return $this->treeWalkWhereClause($clause[1][0]); + } + else { + $sql_subclauses = []; + foreach ($clause[1] as $subclause) { + $sql_subclauses[] = $this->treeWalkWhereClause($subclause); + } + return '(' . implode("\n" . $clause[0], $sql_subclauses) . ')'; + } + + case 'NOT': + // If we get a group of clauses with no operator, assume AND + if (!is_string($clause[1][0])) { + $clause[1] = ['AND', $clause[1]]; + } + return 'NOT (' . $this->treeWalkWhereClause($clause[1]) . ')'; + + default: + return $this->validateClauseAndComposeSql($clause); + } + } + + /** + * Validate and transform a leaf clause array to SQL. + * @param array $clause [$fieldName, $operator, $criteria] + * @return string SQL + * @throws \API_Exception + * @throws \Exception + */ + protected function validateClauseAndComposeSql($clause) { + // Pad array for unary operators + list($key, $operator, $value) = array_pad($clause, 3, NULL); + $fieldSpec = $this->getField($key); + // derive table and column: + $table_name = NULL; + $column_name = NULL; + if (in_array($key, $this->entityFieldNames)) { + $table_name = self::MAIN_TABLE_ALIAS; + $column_name = $key; + } + elseif (strpos($key, '.') && isset($this->fkSelectAliases[$key])) { + list($table_name, $column_name) = explode('.', $this->fkSelectAliases[$key]); + } + + if (!$table_name || !$column_name) { + throw new \API_Exception("Invalid field '$key' in where clause."); + } + + FormattingUtil::formatValue($value, $fieldSpec, $this->getEntity()); + + $sql_clause = \CRM_Core_DAO::createSQLFilter("`$table_name`.`$column_name`", [$operator => $value]); + if ($sql_clause === NULL) { + throw new \API_Exception("Invalid value in where clause for field '$key'"); + } + return $sql_clause; + } + + /** + * @inheritDoc + */ + protected function getFields() { + $fields = civicrm_api4($this->entity, 'getFields', ['action' => 'get', 'includeCustom' => FALSE])->indexBy('name'); + return (array) $fields; + } + + /** + * Fetch a field from the getFields list + * + * @param string $fieldName + * + * @return string|null + */ + protected function getField($fieldName) { + if ($fieldName) { + $fieldPath = explode('.', $fieldName); + if (count($fieldPath) > 1) { + $fieldName = implode('.', array_slice($fieldPath, -2)); + } + return UtilsArray::value($fieldName, $this->apiFieldSpec); + } + return NULL; + } + + /** + * @param $key + */ + protected function joinFK($key) { + $stack = explode('.', $key); + + if (count($stack) < 2) { + return; + } + + $joiner = \Civi::container()->get('joiner'); + $pathArray = explode('.', $key); + $field = array_pop($pathArray); + $pathString = implode('.', $pathArray); + + if (!$joiner->canJoin($this, $pathString)) { + return; + } + + $joinPath = $joiner->join($this, $pathString); + /** @var Joinable $lastLink */ + $lastLink = array_pop($joinPath); + + // Cache field info for retrieval by $this->getField() + $prefix = array_pop($pathArray) . '.'; + if (!isset($this->apiFieldSpec[$prefix . $field])) { + $joinEntity = $lastLink->getEntity(); + // Custom fields are already prefixed + if ($lastLink instanceof CustomGroupJoinable) { + $prefix = ''; + } + foreach ($lastLink->getEntityFields() as $fieldObject) { + $this->apiFieldSpec[$prefix . $fieldObject->getName()] = $fieldObject->toArray() + ['entity' => $joinEntity]; + } + } + + // custom groups use aliases for field names + if ($lastLink instanceof CustomGroupJoinable) { + $field = $lastLink->getSqlColumn(); + } + + $this->fkSelectAliases[$key] = sprintf('%s.%s', $lastLink->getAlias(), $field); + } + + /** + * @param Joinable $joinable + * + * @return $this + */ + public function addJoinedTable(Joinable $joinable) { + $this->joinedTables[] = $joinable; + + return $this; + } + + /** + * @return FALSE|string + */ + public function getFrom() { + return TableHelper::getTableForClass(TableHelper::getFullName($this->entity)); + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @return array + */ + public function getSelect() { + return $this->select; + } + + /** + * @return array + */ + public function getWhere() { + return $this->where; + } + + /** + * @return array + */ + public function getOrderBy() { + return $this->orderBy; + } + + /** + * @return mixed + */ + public function getLimit() { + return $this->limit; + } + + /** + * @return mixed + */ + public function getOffset() { + return $this->offset; + } + + /** + * @return array + */ + public function getSelectFields() { + return $this->selectFields; + } + + /** + * @return bool + */ + public function isFillUniqueFields() { + return $this->isFillUniqueFields; + } + + /** + * @return \CRM_Utils_SQL_Select + */ + public function getQuery() { + return $this->query; + } + + /** + * @return array + */ + public function getJoins() { + return $this->joins; + } + + /** + * @return array + */ + public function getApiFieldSpec() { + return $this->apiFieldSpec; + } + + /** + * @return array + */ + public function getEntityFieldNames() { + return $this->entityFieldNames; + } + + /** + * @return array + */ + public function getAclFields() { + return $this->aclFields; + } + + /** + * @return bool|string + */ + public function getCheckPermissions() { + return $this->checkPermissions; + } + + /** + * @return int + */ + public function getApiVersion() { + return $this->apiVersion; + } + + /** + * @return array + */ + public function getFkSelectAliases() { + return $this->fkSelectAliases; + } + + /** + * @return Joinable[] + */ + public function getJoinedTables() { + return $this->joinedTables; + } + + /** + * @return Joinable + */ + public function getJoinedTable($alias) { + foreach ($this->joinedTables as $join) { + if ($join->getAlias() == $alias) { + return $join; + } + } + } + + /** + * Get table name on basis of entity + * + * @param string $baoName + * + * @return void + */ + public function getTableName($baoName) { + if (strstr($this->entity, 'Custom_')) { + $this->query = \CRM_Utils_SQL_Select::from(CoreUtil::getCustomTableByName(str_replace('Custom_', '', $this->entity)) . ' ' . self::MAIN_TABLE_ALIAS); + $this->entityFieldNames = array_keys($this->apiFieldSpec); + } + else { + $bao = new $baoName(); + $this->query = \CRM_Utils_SQL_Select::from($bao->tableName() . ' ' . self::MAIN_TABLE_ALIAS); + $bao->free(); + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Relationship.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Relationship.php new file mode 100644 index 00000000000..f5156b6247f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Relationship.php @@ -0,0 +1,13 @@ +addCondition(sprintf('%s.record_type_id = (%s)', $alias, $subSelect)); + parent::__construct('civicrm_activity_contact', 'activity_id', $alias); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php new file mode 100644 index 00000000000..370c5898559 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php @@ -0,0 +1,23 @@ +middleLink = $middleLink; + } + + /** + * @return Joinable + */ + public function getMiddleLink() { + return $this->middleLink; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php new file mode 100644 index 00000000000..cf6c3c86a05 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php @@ -0,0 +1,55 @@ +entity = $entity; + $this->sqlColumn = $columnName; + parent::__construct($targetTable, 'entity_id', $alias); + $this->joinType = $isMultiRecord ? + self::JOIN_TYPE_ONE_TO_MANY : self::JOIN_TYPE_ONE_TO_ONE; + } + + public function getEntityFields() { + if (!$this->entityFields) { + $fields = CustomField::get() + ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_required', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value']) + ->addWhere('custom_group.table_name', '=', $this->getTargetTable()) + ->execute(); + foreach ($fields as $field) { + $this->entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity()); + } + } + return $this->entityFields; + } + + /** + * @return string + */ + public function getSqlColumn() { + return $this->sqlColumn; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php new file mode 100644 index 00000000000..25c2a3753b3 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php @@ -0,0 +1,265 @@ +targetTable = $targetTable; + $this->targetColumn = $targetColumn; + if (!$this->entity) { + $this->entity = Tables::getBriefName(Tables::getClassForTable($targetTable)); + } + $this->alias = $alias ?: str_replace('civicrm_', '', $targetTable); + } + + /** + * Gets conditions required when joining to a base table + * + * @param string|null $baseTableAlias + * Name of the base table, if aliased. + * + * @return array + */ + public function getConditionsForJoin($baseTableAlias = NULL) { + $baseCondition = sprintf( + '%s.%s = %s.%s', + $baseTableAlias ?: $this->baseTable, + $this->baseColumn, + $this->getAlias(), + $this->targetColumn + ); + + return array_merge([$baseCondition], $this->conditions); + } + + /** + * @return string + */ + public function getBaseTable() { + return $this->baseTable; + } + + /** + * @param string $baseTable + * + * @return $this + */ + public function setBaseTable($baseTable) { + $this->baseTable = $baseTable; + + return $this; + } + + /** + * @return string + */ + public function getBaseColumn() { + return $this->baseColumn; + } + + /** + * @param string $baseColumn + * + * @return $this + */ + public function setBaseColumn($baseColumn) { + $this->baseColumn = $baseColumn; + + return $this; + } + + /** + * @return string + */ + public function getAlias() { + return $this->alias; + } + + /** + * @param string $alias + * + * @return $this + */ + public function setAlias($alias) { + $this->alias = $alias; + + return $this; + } + + /** + * @return string + */ + public function getTargetTable() { + return $this->targetTable; + } + + /** + * @return string + */ + public function getTargetColumn() { + return $this->targetColumn; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @param $condition + * + * @return $this + */ + public function addCondition($condition) { + $this->conditions[] = $condition; + + return $this; + } + + /** + * @return array + */ + public function getExtraJoinConditions() { + return $this->conditions; + } + + /** + * @param array $conditions + * + * @return $this + */ + public function setConditions($conditions) { + $this->conditions = $conditions; + + return $this; + } + + /** + * @return string + */ + public function getJoinSide() { + return $this->joinSide; + } + + /** + * @param string $joinSide + * + * @return $this + */ + public function setJoinSide($joinSide) { + $this->joinSide = $joinSide; + + return $this; + } + + /** + * @return int + */ + public function getJoinType() { + return $this->joinType; + } + + /** + * @param int $joinType + * + * @return $this + */ + public function setJoinType($joinType) { + $this->joinType = $joinType; + + return $this; + } + + /** + * @return array + */ + public function toArray() { + return get_object_vars($this); + } + + /** + * @return FieldSpec[] + */ + public function getEntityFields() { + if (!$this->entityFields) { + $bao = Tables::getClassForTable($this->getTargetTable()); + if ($bao) { + foreach ($bao::fields() as $field) { + $this->entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity()); + } + } + } + return $this->entityFields; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php new file mode 100644 index 00000000000..96f65488127 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php @@ -0,0 +1,61 @@ +optionGroupName = $optionGroup; + $optionValueTable = 'civicrm_option_value'; + + // default join alias to option group name, e.g. activity_type + if (!$alias && !is_numeric($optionGroup)) { + $alias = $optionGroup; + } + + parent::__construct($optionValueTable, $keyColumn, $alias); + + if (!is_numeric($optionGroup)) { + $subSelect = 'SELECT id FROM civicrm_option_group WHERE name = "%s"'; + $subQuery = sprintf($subSelect, $optionGroup); + $condition = sprintf('%s.option_group_id = (%s)', $alias, $subQuery); + } + else { + $condition = sprintf('%s.option_group_id = %d', $alias, $optionGroup); + } + + $this->addCondition($condition); + } + + /** + * The existing condition must also be re-aliased + * + * @param string $alias + * + * @return $this + */ + public function setAlias($alias) { + foreach ($this->conditions as $index => $condition) { + $search = $this->alias . '.'; + $replace = $alias . '.'; + $this->conditions[$index] = str_replace($search, $replace, $condition); + } + + parent::setAlias($alias); + + return $this; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php new file mode 100644 index 00000000000..cb30ab5703c --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php @@ -0,0 +1,98 @@ +schemaMap = $schemaMap; + } + + /** + * @param Api4SelectQuery $query + * The query object to do the joins on + * @param string $joinPath + * A path of aliases in dot notation, e.g. contact.phone + * @param string $side + * Can be LEFT or INNER + * + * @throws \Exception + * @return Joinable[] + * The path used to make the join + */ + public function join(Api4SelectQuery $query, $joinPath, $side = 'LEFT') { + $fullPath = $this->getPath($query->getFrom(), $joinPath); + $baseTable = $query::MAIN_TABLE_ALIAS; + + foreach ($fullPath as $link) { + $target = $link->getTargetTable(); + $alias = $link->getAlias(); + $conditions = $link->getConditionsForJoin($baseTable); + + $query->join($side, $target, $alias, $conditions); + $query->addJoinedTable($link); + + $baseTable = $link->getAlias(); + } + + return $fullPath; + } + + /** + * @param Api4SelectQuery $query + * @param $joinPath + * + * @return bool + */ + public function canJoin(Api4SelectQuery $query, $joinPath) { + return !empty($this->getPath($query->getFrom(), $joinPath)); + } + + /** + * @param string $baseTable + * @param string $joinPath + * + * @return array + * @throws \Exception + */ + protected function getPath($baseTable, $joinPath) { + $cacheKey = sprintf('%s.%s', $baseTable, $joinPath); + if (!isset($this->cache[$cacheKey])) { + $stack = explode('.', $joinPath); + $fullPath = []; + + foreach ($stack as $key => $targetAlias) { + $links = $this->schemaMap->getPath($baseTable, $targetAlias); + + if (empty($links)) { + throw new \Exception(sprintf('Cannot join %s to %s', $baseTable, $targetAlias)); + } + else { + $fullPath = array_merge($fullPath, $links); + $lastLink = end($links); + $baseTable = $lastLink->getTargetTable(); + } + } + + $this->cache[$cacheKey] = $fullPath; + } + + return $this->cache[$cacheKey]; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php new file mode 100644 index 00000000000..3989afeb4ec --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php @@ -0,0 +1,140 @@ +getTableByName($baseTableName); + $path = []; + + if (!$table) { + return $path; + } + + $this->findPaths($table, $targetTableAlias, 1, $path); + + foreach ($path as $index => $pathLink) { + if ($pathLink instanceof BridgeJoinable) { + $start = array_slice($path, 0, $index); + $middle = [$pathLink->getMiddleLink()]; + $end = array_slice($path, $index, count($path) - $index); + $path = array_merge($start, $middle, $end); + } + } + + return $path; + } + + /** + * @return Table[] + */ + public function getTables() { + return $this->tables; + } + + /** + * @param $name + * + * @return Table|null + */ + public function getTableByName($name) { + foreach ($this->tables as $table) { + if ($table->getName() === $name) { + return $table; + } + } + + return NULL; + } + + /** + * Adds a table to the schema map if it has not already been added + * + * @param Table $table + * + * @return $this + */ + public function addTable(Table $table) { + if (!$this->getTableByName($table->getName())) { + $this->tables[] = $table; + } + + return $this; + } + + /** + * @param array $tables + */ + public function addTables(array $tables) { + foreach ($tables as $table) { + $this->addTable($table); + } + } + + /** + * Recursive function to traverse the schema looking for a path + * + * @param Table $table + * The current table to base fromm + * @param string $target + * The target joinable table alias + * @param int $depth + * The current level of recursion which reflects the number of joins needed + * @param Joinable[] $path + * (By-reference) The possible paths to the target table + * @param Joinable[] $currentPath + * For internal use only to track the path to reach the target table + */ + private function findPaths(Table $table, $target, $depth, &$path, $currentPath = [] + ) { + static $visited = []; + + // reset if new call + if ($depth === 1) { + $visited = []; + } + + $canBeShorter = empty($path) || count($currentPath) + 1 < count($path); + $tooFar = $depth > self::MAX_JOIN_DEPTH; + $beenHere = in_array($table->getName(), $visited); + + if ($tooFar || $beenHere || !$canBeShorter) { + return; + } + + // prevent circular reference + $visited[] = $table->getName(); + + foreach ($table->getExternalLinks() as $link) { + if ($link->getAlias() === $target) { + $path = array_merge($currentPath, [$link]); + } + else { + $linkTable = $this->getTableByName($link->getTargetTable()); + if ($linkTable) { + $nextStep = array_merge($currentPath, [$link]); + $this->findPaths($linkTable, $target, $depth + 1, $path, $nextStep); + } + } + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php new file mode 100644 index 00000000000..aeb3f4c63f6 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php @@ -0,0 +1,208 @@ +dispatcher = $dispatcher; + $this->apiEntities = array_keys((array) Entity::get()->addSelect('name')->execute()->indexBy('name')); + } + + /** + * @return SchemaMap + */ + public function build() { + $map = new SchemaMap(); + $this->loadTables($map); + + $event = new SchemaMapBuildEvent($map); + $this->dispatcher->dispatch(Events::SCHEMA_MAP_BUILD, $event); + + return $map; + } + + /** + * Add all tables and joins + * + * @param SchemaMap $map + */ + private function loadTables(SchemaMap $map) { + /** @var \CRM_Core_DAO $daoName */ + foreach (TableHelper::get() as $daoName => $data) { + $table = new Table($data['table']); + foreach ($daoName::fields() as $field => $fieldData) { + $this->addJoins($table, $field, $fieldData); + } + $map->addTable($table); + if (in_array($data['name'], $this->apiEntities)) { + $this->addCustomFields($map, $table, $data['name']); + } + } + + $this->addBackReferences($map); + } + + /** + * @param Table $table + * @param string $field + * @param array $data + */ + private function addJoins(Table $table, $field, array $data) { + $fkClass = UtilsArray::value('FKClassName', $data); + + // can there be multiple methods e.g. pseudoconstant and fkclass + if ($fkClass) { + $tableName = TableHelper::getTableForClass($fkClass); + $fkKey = UtilsArray::value('FKKeyColumn', $data, 'id'); + $alias = str_replace('_id', '', $field); + $joinable = new Joinable($tableName, $fkKey, $alias); + $joinable->setJoinType($joinable::JOIN_TYPE_MANY_TO_ONE); + $table->addTableLink($field, $joinable); + } + elseif (UtilsArray::value('pseudoconstant', $data)) { + $this->addPseudoConstantJoin($table, $field, $data); + } + } + + /** + * @param Table $table + * @param string $field + * @param array $data + */ + private function addPseudoConstantJoin(Table $table, $field, array $data) { + $pseudoConstant = UtilsArray::value('pseudoconstant', $data); + $tableName = UtilsArray::value('table', $pseudoConstant); + $optionGroupName = UtilsArray::value('optionGroupName', $pseudoConstant); + $keyColumn = UtilsArray::value('keyColumn', $pseudoConstant, 'id'); + + if ($tableName) { + $alias = str_replace('civicrm_', '', $tableName); + $joinable = new Joinable($tableName, $keyColumn, $alias); + $condition = UtilsArray::value('condition', $pseudoConstant); + if ($condition) { + $joinable->addCondition($condition); + } + $table->addTableLink($field, $joinable); + } + elseif ($optionGroupName) { + $keyColumn = UtilsArray::value('keyColumn', $pseudoConstant, 'value'); + $joinable = new OptionValueJoinable($optionGroupName, NULL, $keyColumn); + + if (!empty($data['serialize'])) { + $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY); + } + + $table->addTableLink($field, $joinable); + } + } + + /** + * Loop through existing links and provide link from the other side + * + * @param SchemaMap $map + */ + private function addBackReferences(SchemaMap $map) { + foreach ($map->getTables() as $table) { + foreach ($table->getTableLinks() as $link) { + // there are too many possible joins from option value so skip + if ($link instanceof OptionValueJoinable) { + continue; + } + + $target = $map->getTableByName($link->getTargetTable()); + $tableName = $link->getBaseTable(); + $plural = str_replace('civicrm_', '', $this->getPlural($tableName)); + $joinable = new Joinable($tableName, $link->getBaseColumn(), $plural); + $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY); + $target->addTableLink($link->getTargetColumn(), $joinable); + } + } + } + + /** + * Simple implementation of pluralization. + * Could be replaced with symfony/inflector + * + * @param string $singular + * + * @return string + */ + private function getPlural($singular) { + $last_letter = substr($singular, -1); + switch ($last_letter) { + case 'y': + return substr($singular, 0, -1) . 'ies'; + + case 's': + return $singular . 'es'; + + default: + return $singular . 's'; + } + } + + /** + * @param \Civi\Api4\Service\Schema\SchemaMap $map + * @param \Civi\Api4\Service\Schema\Table $baseTable + * @param string $entity + */ + private function addCustomFields(SchemaMap $map, Table $baseTable, $entity) { + // Don't be silly + if (!array_key_exists($entity, \CRM_Core_SelectValues::customGroupExtends())) { + return; + } + $queryEntity = (array) $entity; + if ($entity == 'Contact') { + $queryEntity = ['Contact', 'Individual', 'Organization', 'Household']; + } + $fieldData = \CRM_Utils_SQL_Select::from('civicrm_custom_field f') + ->join('custom_group', 'INNER JOIN civicrm_custom_group g ON g.id = f.custom_group_id') + ->select(['g.name as custom_group_name', 'g.table_name', 'g.is_multiple', 'f.name', 'label', 'column_name', 'option_group_id']) + ->where('g.extends IN (@entity)', ['@entity' => $queryEntity]) + ->where('g.is_active') + ->execute(); + + while ($fieldData->fetch()) { + $tableName = $fieldData->table_name; + + $customTable = $map->getTableByName($tableName); + if (!$customTable) { + $customTable = new Table($tableName); + } + + if (!empty($fieldData->option_group_id)) { + $optionValueJoinable = new OptionValueJoinable($fieldData->option_group_id, $fieldData->label); + $customTable->addTableLink($fieldData->column_name, $optionValueJoinable); + } + + $map->addTable($customTable); + $alias = $fieldData->custom_group_name; + $isMultiple = !empty($fieldData->is_multiple); + $joinable = new CustomGroupJoinable($tableName, $alias, $isMultiple, $entity, $fieldData->column_name); + $baseTable->addTableLink('id', $joinable); + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php new file mode 100644 index 00000000000..1f464a45d2a --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php @@ -0,0 +1,128 @@ +name = $name; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) { + $this->name = $name; + + return $this; + } + + /** + * @return Joinable[] + */ + public function getTableLinks() { + return $this->tableLinks; + } + + /** + * @return Joinable[] + * Only those links that are not joining the table to itself + */ + public function getExternalLinks() { + return array_filter($this->tableLinks, function (Joinable $joinable) { + return $joinable->getTargetTable() !== $this->getName(); + }); + } + + /** + * @param Joinable $linkToRemove + */ + public function removeLink(Joinable $linkToRemove) { + foreach ($this->tableLinks as $index => $link) { + if ($link === $linkToRemove) { + unset($this->tableLinks[$index]); + } + } + } + + /** + * @param string $baseColumn + * @param Joinable $joinable + * + * @return $this + */ + public function addTableLink($baseColumn, Joinable $joinable) { + $target = $joinable->getTargetTable(); + $targetCol = $joinable->getTargetColumn(); + $alias = $joinable->getAlias(); + + if (!$this->hasLink($target, $targetCol, $alias)) { + if (!$joinable->getBaseTable()) { + $joinable->setBaseTable($this->getName()); + } + if (!$joinable->getBaseColumn()) { + $joinable->setBaseColumn($baseColumn); + } + $this->tableLinks[] = $joinable; + } + + return $this; + } + + /** + * @param mixed $tableLinks + * + * @return $this + */ + public function setTableLinks($tableLinks) { + $this->tableLinks = $tableLinks; + + return $this; + } + + /** + * @param $target + * @param $targetCol + * @param $alias + * + * @return bool + */ + private function hasLink($target, $targetCol, $alias) { + foreach ($this->tableLinks as $link) { + if ($link->getTargetTable() === $target + && $link->getTargetColumn() === $targetCol + && $link->getAlias() === $alias + ) { + return TRUE; + } + } + + return FALSE; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php new file mode 100644 index 00000000000..2c68934426f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php @@ -0,0 +1,118 @@ +setFkEntity('Contact'); + $dataType = 'Integer'; + break; + + case 'File': + case 'StateProvince': + case 'Country': + $this->setFkEntity($dataType); + $dataType = 'Integer'; + break; + } + return parent::setDataType($dataType); + } + + /** + * @return int + */ + public function getCustomFieldId() { + return $this->customFieldId; + } + + /** + * @param int $customFieldId + * + * @return $this + */ + public function setCustomFieldId($customFieldId) { + $this->customFieldId = $customFieldId; + + return $this; + } + + /** + * @return int + */ + public function getCustomGroupName() { + return $this->customGroup; + } + + /** + * @param string $customGroupName + * + * @return $this + */ + public function setCustomGroupName($customGroupName) { + $this->customGroup = $customGroupName; + + return $this; + } + + /** + * @return string + */ + public function getCustomTableName() { + return $this->tableName; + } + + /** + * @param string $customFieldColumnName + * + * @return $this + */ + public function setCustomTableName($customFieldColumnName) { + $this->tableName = $customFieldColumnName; + + return $this; + } + + /** + * @return string + */ + public function getCustomFieldColumnName() { + return $this->columnName; + } + + /** + * @param string $customFieldColumnName + * + * @return $this + */ + public function setCustomFieldColumnName($customFieldColumnName) { + $this->columnName = $customFieldColumnName; + + return $this; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php new file mode 100644 index 00000000000..943d7aebee5 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php @@ -0,0 +1,297 @@ + 'Integer', + 'Link' => 'Url', + 'Memo' => 'Text', + ]; + + /** + * @param string $name + * @param string $entity + * @param string $dataType + */ + public function __construct($name, $entity, $dataType = 'String') { + $this->entity = $entity; + $this->setName($name); + $this->setDataType($dataType); + } + + /** + * @return mixed + */ + public function getDefaultValue() { + return $this->defaultValue; + } + + /** + * @param mixed $defaultValue + * + * @return $this + */ + public function setDefaultValue($defaultValue) { + $this->defaultValue = $defaultValue; + + return $this; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getTitle() { + return $this->title; + } + + /** + * @param string $title + * + * @return $this + */ + public function setTitle($title) { + $this->title = $title; + + return $this; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @return string + */ + public function getDescription() { + return $this->description; + } + + /** + * @param string $description + * + * @return $this + */ + public function setDescription($description) { + $this->description = $description; + + return $this; + } + + /** + * @return bool + */ + public function isRequired() { + return $this->required; + } + + /** + * @param bool $required + * + * @return $this + */ + public function setRequired($required) { + $this->required = $required; + + return $this; + } + + /** + * @return string + */ + public function getDataType() { + return $this->dataType; + } + + /** + * @param $dataType + * + * @return $this + * @throws \Exception + */ + public function setDataType($dataType) { + if (array_key_exists($dataType, self::$typeAliases)) { + $dataType = self::$typeAliases[$dataType]; + } + + if (!in_array($dataType, $this->getValidDataTypes())) { + throw new \Exception(sprintf('Invalid data type "%s', $dataType)); + } + + $this->dataType = $dataType; + + return $this; + } + + /** + * @return int + */ + public function getSerialize() { + return $this->serialize; + } + + /** + * @param int|null $serialize + */ + public function setSerialize($serialize) { + $this->serialize = $serialize; + } + + /** + * Add valid types that are not not part of \CRM_Utils_Type::dataTypes + * + * @return array + */ + private function getValidDataTypes() { + $extraTypes = ['Boolean', 'Text', 'Float', 'Url']; + $extraTypes = array_combine($extraTypes, $extraTypes); + + return array_merge(\CRM_Utils_Type::dataTypes(), $extraTypes); + } + + /** + * @return array + */ + public function getOptions() { + if (!isset($this->options) || $this->options === TRUE) { + $fieldName = $this->getName(); + + if ($this instanceof CustomFieldSpec) { + // buildOptions relies on the custom_* type of field names + $fieldName = sprintf('custom_%d', $this->getCustomFieldId()); + } + + $dao = CoreUtil::getDAOFromApiName($this->getEntity()); + $options = $dao::buildOptions($fieldName); + + if (!is_array($options) || !$options) { + $options = FALSE; + } + + $this->setOptions($options); + } + return $this->options; + } + + /** + * @param array|bool $options + * + * @return $this + */ + public function setOptions($options) { + $this->options = $options; + return $this; + } + + /** + * @return string + */ + public function getFkEntity() { + return $this->fkEntity; + } + + /** + * @param string $fkEntity + * + * @return $this + */ + public function setFkEntity($fkEntity) { + $this->fkEntity = $fkEntity; + + return $this; + } + + /** + * @param array $values + * @return array + */ + public function toArray($values = []) { + $ret = []; + foreach (get_object_vars($this) as $key => $val) { + $key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key)); + if (!$values || in_array($key, $values)) { + $ret[$key] = $val; + } + } + return $ret; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php new file mode 100644 index 00000000000..dc254342cb0 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php @@ -0,0 +1,27 @@ +setRequired(TRUE); + $sourceContactField->setFkEntity('Contact'); + + $spec->addFieldSpec($sourceContactField); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Activity' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php new file mode 100644 index 00000000000..afba9c79d64 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php @@ -0,0 +1,29 @@ +getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('location_type_id')->setRequired(TRUE); + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'Address' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php new file mode 100644 index 00000000000..94c68d9d093 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php @@ -0,0 +1,32 @@ +getFieldByName('contact_type') + ->setRequired(TRUE) + ->setDefaultValue('Individual'); + + $spec->getFieldByName('is_opt_out')->setRequired(FALSE); + $spec->getFieldByName('is_deleted')->setRequired(FALSE); + + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'Contact' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php new file mode 100644 index 00000000000..f55deb1c1ea --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php @@ -0,0 +1,29 @@ +getFieldByName('label')->setRequired(TRUE); + $spec->getFieldByName('name')->setRequired(TRUE); + $spec->getFieldByName('parent_id')->setRequired(TRUE); + + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'ContactType' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php new file mode 100644 index 00000000000..14861871e1c --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php @@ -0,0 +1,23 @@ +getFieldByName('financial_type_id')->setRequired(TRUE); + $spec->getFieldByName('receive_date')->setDefaultValue('now'); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Contribution' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php new file mode 100644 index 00000000000..cd033754e9c --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php @@ -0,0 +1,22 @@ +getFieldByName('extends')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'CustomGroup' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php new file mode 100644 index 00000000000..b69d6df11d7 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php @@ -0,0 +1,49 @@ +getAction(); + $extraFields = [ + 'id' => [ + 'required' => ($action === 'update'), + 'title' => ts('Custom Table Unique ID'), + 'fk_entity' => NULL, + ], + 'entity_id' => [ + 'required' => ($action === 'create'), + 'title' => ts('Entity ID'), + 'fk_entity' => 'Contact', + ], + ]; + foreach ($extraFields as $name => $field) { + // Do not add id field on create action + if ('create' === $action && 'id' === $name) { + continue; + } + $fieldSpec = new FieldSpec($name, $spec->getEntity(), 'Integer'); + $fieldSpec->setTitle($field['title']); + $fieldSpec->setRequired($field['required']); + if (!empty($field['fk_entity'])) { + $fieldSpec->setFkEntity($field['fk_entity']); + } + + $spec->addFieldSpec($fieldSpec); + } + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return strstr($entity, 'Custom_'); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php new file mode 100644 index 00000000000..136b0e54025 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php @@ -0,0 +1,25 @@ +getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('email')->setRequired(TRUE); + $spec->getFieldByName('on_hold')->setRequired(FALSE); + $spec->getFieldByName('is_bulkmail')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Email' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php new file mode 100644 index 00000000000..42b74a6f750 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php @@ -0,0 +1,22 @@ +getFieldByName('is_template')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Event' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php new file mode 100644 index 00000000000..8af69a0a533 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php @@ -0,0 +1,22 @@ +getFieldByName('title')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Group' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php new file mode 100644 index 00000000000..7d5fc2707f2 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php @@ -0,0 +1,22 @@ +getFieldByName('domain_id')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Navigation' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php new file mode 100644 index 00000000000..4ea634c189e --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php @@ -0,0 +1,23 @@ +getFieldByName('weight')->setRequired(FALSE); + $spec->getFieldByName('value')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'OptionValue' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php new file mode 100644 index 00000000000..bb757d43907 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php @@ -0,0 +1,24 @@ +getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('location_type_id')->setRequired(TRUE); + $spec->getFieldByName('phone')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Phone' && $action === 'create'; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php new file mode 100644 index 00000000000..8be77e68bed --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php @@ -0,0 +1,23 @@ +entity = $entity; + $this->action = $action; + } + + public function addFieldSpec(FieldSpec $field) { + $this->fields[] = $field; + } + + /** + * @param $name + * + * @return FieldSpec|null + */ + public function getFieldByName($name) { + foreach ($this->fields as $field) { + if ($field->getName() === $name) { + return $field; + } + } + + return NULL; + } + + /** + * @return array + * Gets all the field names currently part of the specification + */ + public function getFieldNames() { + return array_map(function(FieldSpec $field) { + return $field->getName(); + }, $this->fields); + } + + /** + * @return array|FieldSpec[] + */ + public function getRequiredFields() { + return array_filter($this->fields, function (FieldSpec $field) { + return $field->isRequired(); + }); + } + + /** + * @param array $fieldNames + * Optional array of fields to return + * @return FieldSpec[] + */ + public function getFields($fieldNames = NULL) { + if (!$fieldNames) { + return $this->fields; + } + $fields = []; + foreach ($this->fields as $field) { + if (in_array($field->getName(), $fieldNames)) { + $fields[] = $field; + } + } + return $fields; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @return string + */ + public function getAction() { + return $this->action; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php new file mode 100644 index 00000000000..474e20d8189 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php @@ -0,0 +1,118 @@ +getOptions(); + } + $fieldArray[$field->getName()] = $field->toArray($return); + } + + return $fieldArray; + } + + /** + * @param array $data + * @param string $entity + * + * @return FieldSpec + */ + public static function arrayToField(array $data, $entity) { + $dataTypeName = self::getDataType($data); + + if (!empty($data['custom_group_id'])) { + $field = new CustomFieldSpec($data['name'], $entity, $dataTypeName); + if (strpos($entity, 'Custom_') !== 0) { + $field->setName($data['custom_group']['name'] . '.' . $data['name']); + } + else { + $field->setCustomTableName($data['custom_group']['table_name']); + $field->setCustomFieldColumnName($data['column_name']); + } + $field->setCustomFieldId(ArrayHelper::value('id', $data)); + $field->setCustomGroupName($data['custom_group']['name']); + $field->setRequired((bool) ArrayHelper::value('is_required', $data, FALSE)); + $field->setTitle(ArrayHelper::value('label', $data)); + $field->setOptions(self::customFieldHasOptions($data)); + if (\CRM_Core_BAO_CustomField::isSerialized($data)) { + $field->setSerialize(\CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND); + } + } + else { + $name = ArrayHelper::value('name', $data); + $field = new FieldSpec($name, $entity, $dataTypeName); + $field->setRequired((bool) ArrayHelper::value('required', $data, FALSE)); + $field->setTitle(ArrayHelper::value('title', $data)); + $field->setOptions(!empty($data['pseudoconstant'])); + $field->setSerialize(ArrayHelper::value('serialize', $data)); + } + + $field->setDefaultValue(ArrayHelper::value('default', $data)); + $field->setDescription(ArrayHelper::value('description', $data)); + + $fkAPIName = ArrayHelper::value('FKApiName', $data); + $fkClassName = ArrayHelper::value('FKClassName', $data); + if ($fkAPIName || $fkClassName) { + $field->setFkEntity($fkAPIName ?: TableHelper::getBriefName($fkClassName)); + } + + return $field; + } + + /** + * Does this custom field have options + * + * @param array $field + * @return bool + */ + private static function customFieldHasOptions($field) { + // This will include boolean fields with Yes/No options. + if (in_array($field['html_type'], ['Radio', 'CheckBox'])) { + return TRUE; + } + // Do this before the "Select" string search because date fields have a "Select Date" html_type + // and contactRef fields have an "Autocomplete-Select" html_type - contacts are an FK not an option list. + if (in_array($field['data_type'], ['ContactReference', 'Date'])) { + return FALSE; + } + if (strpos($field['html_type'], 'Select')) { + return TRUE; + } + return !empty($field['option_group_id']); + } + + /** + * Get the data type from an array. Defaults to 'data_type' with fallback to + * mapping for the integer value 'type' + * + * @param array $data + * + * @return string + */ + private static function getDataType(array $data) { + if (isset($data['data_type'])) { + return $data['data_type']; + } + + $dataTypeInt = ArrayHelper::value('type', $data); + $dataTypeName = \CRM_Utils_Type::typeToString($dataTypeInt); + + return $dataTypeName; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php new file mode 100644 index 00000000000..b845c886fc3 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php @@ -0,0 +1,125 @@ +addDAOFields($entity, $action, $specification); + if ($includeCustom && array_key_exists($entity, \CRM_Core_SelectValues::customGroupExtends())) { + $this->addCustomFields($entity, $specification); + } + } + // Custom pseudo-entities + else { + $this->getCustomGroupFields(substr($entity, 7), $specification); + } + + foreach ($this->specProviders as $provider) { + if ($provider->applies($entity, $action)) { + $provider->modifySpec($specification); + } + } + + return $specification; + } + + /** + * @param SpecProviderInterface $provider + */ + public function addSpecProvider(SpecProviderInterface $provider) { + $this->specProviders[] = $provider; + } + + /** + * @param string $entity + * @param RequestSpec $specification + */ + private function addDAOFields($entity, $action, RequestSpec $specification) { + $DAOFields = $this->getDAOFields($entity); + + foreach ($DAOFields as $DAOField) { + if ($DAOField['name'] == 'id' && $action == 'create') { + continue; + } + $field = SpecFormatter::arrayToField($DAOField, $entity); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $entity + * @param RequestSpec $specification + */ + private function addCustomFields($entity, RequestSpec $specification) { + $extends = ($entity == 'Contact') ? ['Contact', 'Individual', 'Organization', 'Household'] : [$entity]; + $customFields = CustomField::get() + ->addWhere('custom_group.extends', 'IN', $extends) + ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_required', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value']) + ->execute(); + + foreach ($customFields as $fieldArray) { + $field = SpecFormatter::arrayToField($fieldArray, $entity); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $customGroup + * @param RequestSpec $specification + */ + private function getCustomGroupFields($customGroup, RequestSpec $specification) { + $customFields = CustomField::get() + ->addWhere('custom_group.name', '=', $customGroup) + ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_required', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value', 'custom_group.table_name', 'column_name']) + ->execute(); + + foreach ($customFields as $fieldArray) { + $field = SpecFormatter::arrayToField($fieldArray, 'Custom_' . $customGroup); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $entityName + * + * @return array + */ + private function getDAOFields($entityName) { + $dao = CoreUtil::getDAOFromApiName($entityName); + + return $dao::fields(); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/UFGroup.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/UFGroup.php new file mode 100644 index 00000000000..b2d78f654bd --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/UFGroup.php @@ -0,0 +1,13 @@ + is_multiple ], ..] + * @param mixed $values + * The value to be inserted + */ + public static function insert(&$array, $parts, $values) { + $key = key($parts); + $isMulti = array_shift($parts); + if (!isset($array[$key])) { + $array[$key] = $isMulti ? [] : NULL; + } + if (empty($parts)) { + $values = self::filterValues($array, $isMulti, $values); + $array[$key] = $values; + } + else { + if ($isMulti) { + foreach ($array[$key] as &$subArray) { + self::insert($subArray, $parts, $values); + } + } + else { + self::insert($array[$key], $parts, $values); + } + } + } + + /** + * @param $parentArray + * @param $isMulti + * @param $values + * + * @return array|mixed + */ + private static function filterValues($parentArray, $isMulti, $values) { + $parentID = UtilsArray::value('id', $parentArray); + + if ($parentID) { + $values = array_filter($values, function ($value) use ($parentID) { + return UtilsArray::value('_parent_id', $value) == $parentID; + }); + } + + $unsets = ['_parent_id', '_base_id']; + array_walk($values, function (&$value) use ($unsets) { + foreach ($unsets as $unset) { + if (isset($value[$unset])) { + unset($value[$unset]); + } + } + }); + + if (!$isMulti) { + $values = array_shift($values); + } + return $values; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/CoreUtil.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/CoreUtil.php new file mode 100644 index 00000000000..b43e62ca26d --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/CoreUtil.php @@ -0,0 +1,42 @@ +addSelect('table_name') + ->addWhere('name', '=', $customGroupName) + ->execute() + ->first()['table_name']; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/FormattingUtil.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/FormattingUtil.php new file mode 100644 index 00000000000..4b5fbd5fb91 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/FormattingUtil.php @@ -0,0 +1,101 @@ + $field) { + if (!empty($params[$name])) { + $value =& $params[$name]; + // Hack for null values -- see comment below + if ($value === 'null') { + $value = 'Null'; + } + FormattingUtil::formatValue($value, $field, $entity); + // Ensure we have an array for serialized fields + if (!empty($field['serialize'] && !is_array($value))) { + $value = (array) $value; + } + } + /* + * Because of the wacky way that database values are saved we need to format + * some of the values here. In this strange world the string 'null' is used to + * unset values. Hence if we encounter true null we change it to string 'null'. + * + * If we encounter the string 'null' then we assume the user actually wants to + * set the value to string null. However since the string null is reserved for + * unsetting values we must change it. Another quirk of the DB_DataObject is + * that it allows 'Null' to be set, but any other variation of string 'null' + * will be converted to true null, e.g. 'nuLL', 'NUlL' etc. so we change it to + * 'Null'. + */ + elseif (array_key_exists($name, $params) && $params[$name] === NULL) { + $params[$name] = 'null'; + } + + if (strstr($entity, 'Custom_')) { + if ($name == 'entity_id') { + $params['entityID'] = $params['entity_id']; + unset($params['entity_id']); + } + elseif (!empty($field['custom_field_id'])) { + $params['custom_' . $field['custom_field_id']] = $params[$name]; + unset($params[$name]); + } + } + } + } + + /** + * Transform raw api input to appropriate format for use in a SQL query. + * + * This is used by read AND write actions (Get, Create, Update, Replace) + * + * @param $value + * @param $fieldSpec + * @throws \API_Exception + */ + public static function formatValue(&$value, $fieldSpec, $entity) { + if (is_array($value)) { + foreach ($value as &$val) { + self::formatValue($val, $fieldSpec, $entity); + } + return; + } + $fk = UtilsArray::value('fk_entity', $fieldSpec); + if ($fieldSpec['name'] == 'id') { + $fk = $entity; + } + $dataType = UtilsArray::value('data_type', $fieldSpec); + + if ($fk === 'Domain' && $value === 'current_domain') { + $value = \CRM_Core_Config::domainID(); + } + + if ($fk === 'Contact' && !is_numeric($value)) { + $value = \_civicrm_api3_resolve_contactID($value); + if ('unknown-user' === $value) { + throw new \API_Exception("\"{$fieldSpec['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, array('error_field' => $fieldSpec['name'], "type" => "integer")); + } + } + + switch ($dataType) { + case 'Timestamp': + $value = date('Y-m-d H:i:s', strtotime($value)); + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/ReflectionUtils.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/ReflectionUtils.php new file mode 100644 index 00000000000..4796419e1d1 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Utils/ReflectionUtils.php @@ -0,0 +1,119 @@ +getDocComment()); + + // Recurse into parent functions + if (isset($docs['inheritDoc'])) { + unset($docs['inheritDoc']); + $newReflection = NULL; + try { + if ($type) { + $name = $reflection->getName(); + $reflectionClass = $reflection->getDeclaringClass()->getParentClass(); + if ($reflectionClass) { + $getItem = "get$type"; + $newReflection = $reflectionClass->$getItem($name); + } + } + else { + $newReflection = $reflection->getParentClass(); + } + } + catch (\ReflectionException $e) {} + if ($newReflection) { + // Mix in + $additionalDocs = self::getCodeDocs($newReflection, $type); + if (!empty($docs['comment']) && !empty($additionalDocs['comment'])) { + $docs['comment'] .= "\n\n" . $additionalDocs['comment']; + } + $docs += $additionalDocs; + } + } + return $docs; + } + + /** + * @param string $comment + * @return array + */ + public static function parseDocBlock($comment) { + $info = []; + foreach (preg_split("/((\r?\n)|(\r\n?))/", $comment) as $num => $line) { + if (!$num || strpos($line, '*/') !== FALSE) { + continue; + } + $line = ltrim(trim($line), '* '); + if (strpos($line, '@') === 0) { + $words = explode(' ', $line); + $key = substr($words[0], 1); + if ($key == 'var') { + $info['type'] = explode('|', $words[1]); + } + elseif ($key == 'options') { + $val = str_replace(', ', ',', implode(' ', array_slice($words, 1))); + $info['options'] = explode(',', $val); + } + else { + // Unrecognized annotation, but we'll duly add it to the info array + $val = implode(' ', array_slice($words, 1)); + $info[$key] = strlen($val) ? $val : TRUE; + } + } + elseif ($num == 1) { + $info['description'] = $line; + } + elseif (!$line) { + if (isset($info['comment'])) { + $info['comment'] .= "\n"; + } + } + else { + $info['comment'] = isset($info['comment']) ? "{$info['comment']}\n$line" : $line; + } + } + if (isset($info['comment'])) { + $info['comment'] = trim($info['comment']); + } + return $info; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Website.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Website.php new file mode 100644 index 00000000000..5cf1ed040a6 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/Civi/Api4/Website.php @@ -0,0 +1,13 @@ + +Licensed under the GNU Affero Public License 3.0 (below). + +------------------------------------------------------------------------------- + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4.ang.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4.ang.php new file mode 100644 index 00000000000..51a5da06815 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4.ang.php @@ -0,0 +1,20 @@ + \CRM_Core_DAO::acceptedSQLOperators(), +]; +\Civi::resources()->addVars('api4', $vars); +return [ + 'js' => [ + 'ang/api4.js', + 'ang/api4/*.js', + 'ang/api4/*/*.js', + ], + 'css' => [ + 'css/explorer.css', + ], + 'partials' => [ + 'ang/api4', + ], + 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'crmRouteBinder'], +]; diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4.js b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4.js new file mode 100644 index 00000000000..42070394de5 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4.js @@ -0,0 +1,6 @@ +(function(angular, $, _) { + // Declare a list of dependencies. + angular.module('api4', [ + 'crmUi', 'crmUtil', 'ngRoute', 'ui.sortable' + ]); +})(angular, CRM.$, CRM._); diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Explorer.html b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Explorer.html new file mode 100644 index 00000000000..ca098684154 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Explorer.html @@ -0,0 +1,122 @@ +
    +
    + +

    + {{ ts('CiviCRM API v4') }}{{ entity ? (' (' + entity + '::' + action + ')') : '' }} +

    + +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + values * +
    + + +
    +
    + +
    +
    +
    + orderBy * +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +

    {{ helpTitle }}

    +
    +
    +

    {{ helpContent.description }}

    +
    +

    {{ text }}

    +
    +

    + {{ key }}: {{ item }} +

    +
    +
    +
    +
    +
    +
    +
    +
    +

    {{ ts('Code') }}

    +
    +
    + + + + + +
    {{ type }}
    {{ item }}
    +
    +
    +
    +
    +
    +
    +

    + + + + + {{ ts('Result') }} +

    +
    +
    +
    {{ code }}
    +
    +
    +
    +
    + + +
    diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Explorer.js b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Explorer.js new file mode 100644 index 00000000000..8bd2e7425cd --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Explorer.js @@ -0,0 +1,467 @@ +(function(angular, $, _, undefined) { + + // Cache schema metadata + var schema = []; + // Cache fk schema data + var links = []; + // Cache list of entities + var entities = []; + // Cache list of actions + var actions = []; + + angular.module('api4').config(function($routeProvider) { + $routeProvider.when('/api4/:api4entity?/:api4action?', { + controller: 'Api4Explorer', + templateUrl: '~/api4/Explorer.html', + reloadOnSearch: false + }); + } + ); + + angular.module('api4').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, crmUiHelp, crmApi4) { + var ts = $scope.ts = CRM.ts('api4'); + $scope.entities = entities; + $scope.operators = arrayToSelect2(CRM.vars.api4.operators); + $scope.actions = actions; + $scope.fields = []; + $scope.fieldsAndJoins = []; + $scope.availableParams = {}; + $scope.params = {}; + var getMetaParams = schema.length ? {} : {schema: ['Entity', 'getFields'], links: ['Entity', 'getLinks']}, + objectParams = {orderBy: 'ASC', values: ''}, + helpTitle = '', + helpContent = {}; + $scope.helpTitle = ''; + $scope.helpContent = {}; + $scope.entity = $routeParams.api4entity; + $scope.result = []; + $scope.status = 'default'; + $scope.loading = false; + $scope.controls = {}; + $scope.code = { + php: '', + javascript: '' + }; + + function ucfirst(str) { + return str[0].toUpperCase() + str.slice(1); + } + + function lcfirst(str) { + return str[0].toLowerCase() + str.slice(1); + } + + function pluralize(str) { + switch (str[str.length-1]) { + case 's': + return str + 'es'; + case 'y': + return str.slice(0, -1) + 'ies'; + default: + return str + 's'; + } + } + + // Turn a flat array into a select2 array + function arrayToSelect2(array) { + var out = []; + _.each(array, function(item) { + out.push({id: item, text: item}); + }); + return out; + } + + // Reformat an existing array of objects for compatibility with select2 + function formatForSelect2(input, container, key, extra, prefix) { + _.each(input, function(item) { + var id = (prefix || '') + item[key]; + var formatted = {id: id, text: id}; + if (extra) { + _.merge(formatted, _.pick(item, extra)); + } + container.push(formatted); + }); + return container; + } + + function entityFields(entity) { + return _.result(_.findWhere(schema, {name: entity}), 'fields'); + } + + function getFieldList() { + var fields = []; + formatForSelect2(entityFields($scope.entity), fields, 'name', ['description', 'required', 'default_value']); + return fields; + } + + function addJoins(fieldList) { + var fields = _.cloneDeep(fieldList), + fks = _.findWhere(links, {entity: $scope.entity}) || {}; + _.each(fks.links, function(link) { + var linkFields = entityFields(link.entity); + if (linkFields) { + fields.push({ + text: link.alias, + description: 'Join to ' + link.entity, + children: formatForSelect2(linkFields, [], 'name', ['description'], link.alias + '.') + }); + } + }); + return fields; + } + + $scope.help = function(title, param) { + if (!param) { + $scope.helpTitle = helpTitle; + $scope.helpContent = helpContent; + } else { + $scope.helpTitle = title; + $scope.helpContent = param; + } + }; + + $scope.valuesFields = function() { + var fields = []; + _.each(_.cloneDeep($scope.fields), function(field, index) { + if ((field.id === 'id' && $scope.action === 'create') || field.children) { + return; + } + if ($scope.params.values && typeof $scope.params.values[field.id] !== 'undefined') { + field.disabled = true; + } + fields.push(field); + }); + return fields; + }; + + $scope.selectOptions = function() { + if ($scope.availableParams.select.options) { + return arrayToSelect2($scope.availableParams.select.options); + } else { + return $scope.fieldsAndJoins; + } + }; + + $scope.formatSelect2Item = function(row) { + return _.escape(row.text) + + (isFieldRequiredForCreate(row) ? ' *' : '') + + (row.description ? '

    ' + _.escape(row.description) + '

    ' : ''); + }; + + function isFieldRequiredForCreate(field) { + return field.required && !field.default_value; + } + + // Get all params that have been set + function getParams() { + var params = {}; + _.each($scope.params, function(param, key) { + if (param != $scope.availableParams[key].default && !(typeof param === 'object' && _.isEmpty(param))) { + params[key] = param; + } + }); + _.each(objectParams, function(defaultVal, key) { + if (params[key]) { + var newParam = {}; + _.each(params[key], function(item) { + newParam[item[0]] = item[1]; + }); + params[key] = newParam; + } + }); + return params; + } + + function selectAction() { + $scope.action = $routeParams.api4action; + $scope.fields = getFieldList(); + $scope.fieldsAndJoins = addJoins($scope.fields); + if ($scope.action) { + var actionInfo = _.findWhere(actions, {id: $scope.action}); + _.each(actionInfo.params, function (param, name) { + var format, + defaultVal = _.cloneDeep(param.default); + if (param.type) { + switch (param.type[0]) { + case 'int': + case 'bool': + format = param.type[0]; + break; + + case 'array': + case 'object': + format = 'json'; + break; + + default: + format = 'raw'; + } + if (name == 'limit') { + defaultVal = 25; + } + $scope.$bindToRoute({ + expr: 'params["' + name + '"]', + param: name, + format: format, + default: defaultVal, + deep: name === 'where' + }); + } + if (typeof objectParams[name] !== 'undefined') { + $scope.$watch('params.' + name, function(values) { + // Remove empty values + _.each(values, function(clause, index) { + if (!clause[0]) { + $scope.params[name].splice(index, 1); + } + }); + }, true); + $scope.$watch('controls.' + name, function(value) { + var field = value; + $timeout(function() { + if (field) { + var defaultOp = objectParams[name]; + if (_.isEmpty($scope.params[name])) { + $scope.params[name] = [[field, defaultOp]]; + } else { + $scope.params[name].push([field, defaultOp]); + } + $scope.controls[name] = null; + } + }); + }); + } + }); + $scope.availableParams = actionInfo.params; + } + writeCode(); + } + + function stringify(value, trim) { + var str = JSON.stringify(value).replace(/,/g, ', '); + if (trim) { + str = str.slice(1, -1); + } + return str.trim(); + } + + function writeCode() { + var code = { + php: ts('Select an entity and action'), + javascript: '' + }, + entity = $scope.entity, + action = $scope.action, + params = getParams(), + result = 'result'; + if ($scope.entity && $scope.action) { + if (action.slice(0, 3) === 'get') { + result = lcfirst(action.replace(/s$/, '').slice(3) || entity); + } + var results = lcfirst(pluralize(result)), + paramCount = _.size(params), + i = 0; + code.javascript = "CRM.api4('" + entity + "', '" + action + "', {"; + _.each(params, function(param, key) { + code.javascript += "\n " + key + ': ' + stringify(param) + + (++i < paramCount ? ',' : ''); + if (key === 'checkPermissions') { + code.javascript += ' // IGNORED: permissions are always enforced from client-side requests'; + } + }); + code.javascript += "\n}).done(function(" + results + ") {\n // do something with " + results + " array\n});"; + if (entity.substr(0, 7) !== 'Custom_') { + code.php = '$' + results + " = \\Civi\\Api4\\" + entity + '::' + action + '()'; + } else { + code.php = '$' + results + " = \\Civi\\Api4\\CustomValue()::" + action + "('" + entity.substr(7) + "')"; + } + _.each(params, function(param, key) { + var val = ''; + if (typeof objectParams[key] !== 'undefined') { + _.each(param, function(item, index) { + val = stringify(index) + ', ' + stringify(item); + code.php += "\n ->add" + ucfirst(key).replace(/s$/, '') + '(' + val + ')'; + }); + } else if (key === 'where') { + _.each(param, function (clause) { + if (clause[0] === 'AND' || clause[0] === 'OR' || clause[0] === 'NOT') { + code.php += "\n ->addClause('" + clause[0] + "', " + stringify(clause[1], true) + ')'; + } else { + code.php += "\n ->addWhere(" + stringify(clause, true) + ")"; + } + }); + } else { + code.php += "\n ->set" + ucfirst(key) + '(' + stringify(param) + ')'; + } + }); + code.php += "\n ->execute();\nforeach ($" + results + ' as $' + result + ') {\n // do something\n}'; + } + $scope.code = code; + } + + $scope.execute = function() { + $scope.status = 'warning'; + $scope.loading = true; + crmApi4($scope.entity, $scope.action, getParams()) + .then(function(data) { + var meta = {length: data.length}, + result = JSON.stringify(data, null, 2); + data.length = 0; + _.assign(meta, data); + $scope.loading = false; + $scope.status = 'success'; + $scope.result = [JSON.stringify(meta).replace('{', '').replace(/}$/, ''), result]; + }, function(data) { + $scope.loading = false; + $scope.status = 'danger'; + $scope.result = [JSON.stringify(data, null, 2)]; + }); + }; + + function fetchMeta() { + crmApi4(getMetaParams) + .then(function(data) { + if (data.schema) { + schema = data.schema; + entities.length = 0; + formatForSelect2(schema, entities, 'name', ['description']); + if ($scope.entity && !$scope.action) { + showEntityHelp($scope.entity); + } + } + if (data.links) { + links = data.links; + } + if (data.actions) { + formatForSelect2(data.actions, actions, 'name', ['description', 'params']); + selectAction(); + } + }); + } + + // Help for an entity with no action selected + function showEntityHelp(entity) { + var entityInfo = _.findWhere(schema, {name: entity}); + $scope.helpTitle = helpTitle = $scope.entity; + $scope.helpContent = helpContent = { + description: entityInfo.description, + comment: entityInfo.comment + }; + } + + if (!$scope.entity) { + $scope.helpTitle = helpTitle = ts('Help'); + $scope.helpContent = helpContent = {description: ts('Welcome to the api explorer.'), comment: ts('Select an entity to begin.')}; + if (getMetaParams.schema) { + fetchMeta(); + } + } else if (!actions.length) { + if (getMetaParams.schema) { + entities.push({id: $scope.entity, text: $scope.entity}); + } + getMetaParams.actions = [$scope.entity, 'getActions']; + fetchMeta(); + } else { + selectAction(); + } + + if ($scope.entity && schema.length) { + showEntityHelp($scope.entity); + } + + // Update route when changing entity + $scope.$watch('entity', function(newVal, oldVal) { + if (oldVal !== newVal) { + // Flush actions cache to re-fetch for new entity + actions = []; + $location.url('/api4/' + newVal); + } + }); + + // Update route when changing actions + $scope.$watch('action', function(newVal, oldVal) { + if ($scope.entity && $routeParams.api4action !== newVal && !_.isUndefined(newVal)) { + $location.url('/api4/' + $scope.entity + '/' + newVal); + } else if (newVal) { + $scope.helpTitle = helpTitle = $scope.entity + '::' + newVal; + $scope.helpContent = helpContent = _.pick(_.findWhere(actions, {id: newVal}), ['description', 'comment']); + } + }); + + $scope.$watch('params', writeCode, true); + writeCode(); + + }); + + angular.module('api4').directive('crmApi4WhereClause', function($timeout) { + return { + scope: { + data: '=crmApi4WhereClause' + }, + templateUrl: '~/api4/WhereClause.html', + link: function (scope, element, attrs) { + var ts = scope.ts = CRM.ts('api4'); + scope.newClause = ''; + scope.conjunctions = ['AND', 'OR', 'NOT']; + + scope.addGroup = function(op) { + scope.data.where.push([op, []]); + }; + + scope.removeGroup = function() { + scope.data.groupParent.splice(scope.data.groupIndex, 1); + }; + + scope.onSort = function(event, ui) { + $('.api4-where-fieldset').toggleClass('api4-sorting', event.type === 'sortstart'); + $('.api4-input.form-inline').css('margin-left', ''); + }; + + // Indent clause while dragging between nested groups + scope.onSortOver = function(event, ui) { + var offset = 0; + if (ui.sender) { + offset = $(ui.placeholder).offset().left - $(ui.sender).offset().left; + } + $('.api4-input.form-inline.ui-sortable-helper').css('margin-left', '' + offset + 'px'); + }; + + scope.$watch('newClause', function(value) { + var field = value; + $timeout(function() { + if (field) { + scope.data.where.push([field, '=', '']); + scope.newClause = null; + } + }); + }); + scope.$watch('data.where', function(values) { + // Remove empty values + _.each(values, function(clause, index) { + if (typeof clause !== 'undefined' && !clause[0]) { + values.splice(index, 1); + } + }); + }, true); + } + }; + }); + + // Collapsible optgroups for select2 + $(function() { + $('body') + .on('select2-open', function(e) { + if ($(e.target).hasClass('collapsible-optgroups')) { + $('#select2-drop') + .off('.collapseOptionGroup') + .addClass('collapsible-optgroups-enabled') + .on('click.collapseOptionGroup', '.select2-result-with-children > .select2-result-label', function() { + $(this).parent().toggleClass('optgroup-expanded'); + }); + } + }) + .on('select2-close', function() { + $('#select2-drop').off('.collapseOptionGroup').removeClass('collapsible-optgroups-enabled'); + }); + }); +})(angular, CRM.$, CRM._); diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Utils.js b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Utils.js new file mode 100644 index 00000000000..b0d3ec69938 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/Utils.js @@ -0,0 +1,37 @@ +(function(angular, $, _) { + + angular.module('api4').factory('crmApi4', function($q) { + var crmApi4 = function(entity, action, params, message) { + // JSON serialization in CRM.api4 is not aware of Angular metadata like $$hash, so use angular.toJson() + var deferred = $q.defer(); + var p; + var backend = crmApi4.backend || CRM.api4; + if (_.isObject(entity)) { + // eval content is locally generated. + /*jshint -W061 */ + p = backend(eval('('+angular.toJson(entity)+')'), action); + } else { + // eval content is locally generated. + /*jshint -W061 */ + p = backend(entity, action, eval('('+angular.toJson(params)+')'), message); + } + p.then( + function(result) { + deferred.resolve(result); + }, + function(error) { + deferred.reject(error); + } + ); + return deferred.promise; + }; + crmApi4.backend = null; + crmApi4.val = function(value) { + var d = $.Deferred(); + d.resolve(value); + return d.promise(); + }; + return crmApi4; + }); + +})(angular, CRM.$, CRM._); diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/WhereClause.html b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/WhereClause.html new file mode 100644 index 00000000000..c2b06ab52b1 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/ang/api4/WhereClause.html @@ -0,0 +1,39 @@ +{{ data.label || data.op + ' group' }} * +
    + +
    +
    +
    +
    + + Where + {{ data.op }} + + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + +
    \ No newline at end of file diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/api4.civix.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/api4.civix.php new file mode 100644 index 00000000000..bf2acf221da --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/api4.civix.php @@ -0,0 +1,368 @@ +template_dir)) { + array_unshift($template->template_dir, $extDir); + } + else { + $template->template_dir = [$extDir, $template->template_dir]; + } + + $include_path = $extRoot . PATH_SEPARATOR . get_include_path(); + set_include_path($include_path); +} + +/** + * (Delegated) Implements hook_civicrm_xmlMenu(). + * + * @param $files array(string) + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu + */ +function _api4_civix_civicrm_xmlMenu(&$files) { + foreach (_api4_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) { + $files[] = $file; + } +} + +/** + * Implements hook_civicrm_install(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install + */ +function _api4_civix_civicrm_install() { + _api4_civix_civicrm_config(); + if ($upgrader = _api4_civix_upgrader()) { + $upgrader->onInstall(); + } +} + +/** + * Implements hook_civicrm_postInstall(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall + */ +function _api4_civix_civicrm_postInstall() { + _api4_civix_civicrm_config(); + if ($upgrader = _api4_civix_upgrader()) { + if (is_callable([$upgrader, 'onPostInstall'])) { + $upgrader->onPostInstall(); + } + } +} + +/** + * Implements hook_civicrm_uninstall(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall + */ +function _api4_civix_civicrm_uninstall() { + _api4_civix_civicrm_config(); + if ($upgrader = _api4_civix_upgrader()) { + $upgrader->onUninstall(); + } +} + +/** + * (Delegated) Implements hook_civicrm_enable(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable + */ +function _api4_civix_civicrm_enable() { + _api4_civix_civicrm_config(); + if ($upgrader = _api4_civix_upgrader()) { + if (is_callable([$upgrader, 'onEnable'])) { + $upgrader->onEnable(); + } + } +} + +/** + * (Delegated) Implements hook_civicrm_disable(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable + * @return mixed + */ +function _api4_civix_civicrm_disable() { + _api4_civix_civicrm_config(); + if ($upgrader = _api4_civix_upgrader()) { + if (is_callable([$upgrader, 'onDisable'])) { + $upgrader->onDisable(); + } + } +} + +/** + * (Delegated) Implements hook_civicrm_upgrade(). + * + * @param $op string, the type of operation being performed; 'check' or 'enqueue' + * @param $queue CRM_Queue_Queue, (for 'enqueue') the modifiable list of pending up upgrade tasks + * + * @return mixed based on op. for 'check', returns array(boolean) (TRUE if upgrades are pending) + * for 'enqueue', returns void + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade + */ +function _api4_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { + if ($upgrader = _api4_civix_upgrader()) { + return $upgrader->onUpgrade($op, $queue); + } +} + +/** + * @return CRM_Api4_Upgrader + */ +function _api4_civix_upgrader() { + if (!file_exists(__DIR__ . '/CRM/Api4/Upgrader.php')) { + return NULL; + } + else { + return CRM_Api4_Upgrader_Base::instance(); + } +} + +/** + * Search directory tree for files which match a glob pattern + * + * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored. + * Note: In Civi 4.3+, delegate to CRM_Utils_File::findFiles() + * + * @param $dir string, base dir + * @param $pattern string, glob pattern, eg "*.txt" + * @return array(string) + */ +function _api4_civix_find_files($dir, $pattern) { + if (is_callable(['CRM_Utils_File', 'findFiles'])) { + return CRM_Utils_File::findFiles($dir, $pattern); + } + + $todos = [$dir]; + $result = []; + while (!empty($todos)) { + $subdir = array_shift($todos); + foreach (_api4_civix_glob("$subdir/$pattern") as $match) { + if (!is_dir($match)) { + $result[] = $match; + } + } + if ($dh = opendir($subdir)) { + while (FALSE !== ($entry = readdir($dh))) { + $path = $subdir . DIRECTORY_SEPARATOR . $entry; + if ($entry{0} == '.') { + } + elseif (is_dir($path)) { + $todos[] = $path; + } + } + closedir($dh); + } + } + return $result; +} +/** + * (Delegated) Implements hook_civicrm_managed(). + * + * Find any *.mgd.php files, merge their content, and return. + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed + */ +function _api4_civix_civicrm_managed(&$entities) { + $mgdFiles = _api4_civix_find_files(__DIR__, '*.mgd.php'); + foreach ($mgdFiles as $file) { + $es = include $file; + foreach ($es as $e) { + if (empty($e['module'])) { + $e['module'] = 'org.civicrm.api4'; + } + $entities[] = $e; + if (empty($e['params']['version'])) { + $e['params']['version'] = '3'; + } + } + } +} + +/** + * (Delegated) Implements hook_civicrm_caseTypes(). + * + * Find any and return any files matching "xml/case/*.xml" + * + * Note: This hook only runs in CiviCRM 4.4+. + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes + */ +function _api4_civix_civicrm_caseTypes(&$caseTypes) { + if (!is_dir(__DIR__ . '/xml/case')) { + return; + } + + foreach (_api4_civix_glob(__DIR__ . '/xml/case/*.xml') as $file) { + $name = preg_replace('/\.xml$/', '', basename($file)); + if ($name != CRM_Case_XMLProcessor::mungeCaseType($name)) { + $errorMessage = sprintf("Case-type file name is malformed (%s vs %s)", $name, CRM_Case_XMLProcessor::mungeCaseType($name)); + CRM_Core_Error::fatal($errorMessage); + // throw new CRM_Core_Exception($errorMessage); + } + $caseTypes[$name] = [ + 'module' => 'org.civicrm.api4', + 'name' => $name, + 'file' => $file, + ]; + } +} + +/** + * (Delegated) Implements hook_civicrm_angularModules(). + * + * Find any and return any files matching "ang/*.ang.php" + * + * Note: This hook only runs in CiviCRM 4.5+. + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules + */ +function _api4_civix_civicrm_angularModules(&$angularModules) { + if (!is_dir(__DIR__ . '/ang')) { + return; + } + + $files = _api4_civix_glob(__DIR__ . '/ang/*.ang.php'); + foreach ($files as $file) { + $name = preg_replace(':\.ang\.php$:', '', basename($file)); + $module = include $file; + if (empty($module['ext'])) { + $module['ext'] = 'org.civicrm.api4'; + } + $angularModules[$name] = $module; + } +} + +/** + * Glob wrapper which is guaranteed to return an array. + * + * The documentation for glob() says, "On some systems it is impossible to + * distinguish between empty match and an error." Anecdotally, the return + * result for an empty match is sometimes array() and sometimes FALSE. + * This wrapper provides consistency. + * + * @link http://php.net/glob + * @param string $pattern + * @return array, possibly empty + */ +function _api4_civix_glob($pattern) { + $result = glob($pattern); + return is_array($result) ? $result : []; +} + +/** + * Inserts a navigation menu item at a given place in the hierarchy. + * + * @param array $menu - menu hierarchy + * @param string $path - path where insertion should happen (ie. Administer/System Settings) + * @param array $item - menu you need to insert (parent/child attributes will be filled for you) + */ +function _api4_civix_insert_navigation_menu(&$menu, $path, $item) { + // If we are done going down the path, insert menu + if (empty($path)) { + $menu[] = [ + 'attributes' => array_merge([ + 'label' => CRM_Utils_Array::value('name', $item), + 'active' => 1, + ], $item), + ]; + return TRUE; + } + else { + // Find an recurse into the next level down + $found = FALSE; + $path = explode('/', $path); + $first = array_shift($path); + foreach ($menu as $key => &$entry) { + if ($entry['attributes']['name'] == $first) { + if (!isset($entry['child'])) { + $entry['child'] = []; + } + $found = _api4_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item, $key); + } + } + return $found; + } +} + +/** + * (Delegated) Implements hook_civicrm_navigationMenu(). + */ +function _api4_civix_navigationMenu(&$nodes) { + if (!is_callable(['CRM_Core_BAO_Navigation', 'fixNavigationMenu'])) { + _api4_civix_fixNavigationMenu($nodes); + } +} + +/** + * Given a navigation menu, generate navIDs for any items which are + * missing them. + */ +function _api4_civix_fixNavigationMenu(&$nodes) { + $maxNavID = 1; + array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) { + if ($key === 'navID') { + $maxNavID = max($maxNavID, $item); + } + }); + _api4_civix_fixNavigationMenuItems($nodes, $maxNavID, NULL); +} + +function _api4_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) { + $origKeys = array_keys($nodes); + foreach ($origKeys as $origKey) { + if (!isset($nodes[$origKey]['attributes']['parentID']) && $parentID !== NULL) { + $nodes[$origKey]['attributes']['parentID'] = $parentID; + } + // If no navID, then assign navID and fix key. + if (!isset($nodes[$origKey]['attributes']['navID'])) { + $newKey = ++$maxNavID; + $nodes[$origKey]['attributes']['navID'] = $newKey; + $nodes[$newKey] = $nodes[$origKey]; + unset($nodes[$origKey]); + $origKey = $newKey; + } + if (isset($nodes[$origKey]['child']) && is_array($nodes[$origKey]['child'])) { + _api4_civix_fixNavigationMenuItems($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']); + } + } +} + +/** + * (Delegated) Implements hook_civicrm_alterSettingsFolders(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders + */ +function _api4_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { + static $configured = FALSE; + if ($configured) { + return; + } + $configured = TRUE; + + $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings'; + if (is_dir($settingsDir) && !in_array($settingsDir, $metaDataFolders)) { + $metaDataFolders[] = $settingsDir; + } +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/api4.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/api4.php new file mode 100644 index 00000000000..4278dc04d9d --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/api4.php @@ -0,0 +1,174 @@ +runRequest($request); +} + +/** + * @param ContainerBuilder $container + */ +function api4_civicrm_container($container) { + $loader = new XmlFileLoader($container, new FileLocator(__DIR__)); + $loader->load('services.xml'); + + $container->getDefinition('civi_api_kernel')->addMethodCall( + 'registerApiProvider', + [new Reference('action_object_provider')] + ); + + // add event subscribers$container->get( + $dispatcher = $container->getDefinition('dispatcher'); + $subscribers = $container->findTaggedServiceIds('event_subscriber'); + + foreach (array_keys($subscribers) as $subscriber) { + $dispatcher->addMethodCall( + 'addSubscriber', + [new Reference($subscriber)] + ); + } + + // add spec providers + $providers = $container->findTaggedServiceIds('spec_provider'); + $gatherer = $container->getDefinition('spec_gatherer'); + + foreach (array_keys($providers) as $provider) { + $gatherer->addMethodCall( + 'addSpecProvider', + [new Reference($provider)] + ); + } + + if (defined('CIVICRM_UF') && CIVICRM_UF === 'UnitTests') { + $loader->load('tests/services.xml'); + } +} + +/** + * Implements hook_civicrm_coreResourceList(). + */ +function api4_civicrm_coreResourceList(&$list, $region) { + if ($region == 'html-header') { + Civi::resources()->addScriptFile('org.civicrm.api4', 'js/api4.js', -9000, $region); + } +} + +/** + * Implements hook_civicrm_config(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config + */ +function api4_civicrm_config(&$config) { + _api4_civix_civicrm_config($config); +} + +/** + * Implements hook_civicrm_xmlMenu(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu + */ +function api4_civicrm_xmlMenu(&$files) { + _api4_civix_civicrm_xmlMenu($files); +} + +/** + * Implements hook_civicrm_install(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install + */ +function api4_civicrm_install() { + _api4_civix_civicrm_install(); +} + +/** + * Implements hook_civicrm_uninstall(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall + */ +function api4_civicrm_uninstall() { + _api4_civix_civicrm_uninstall(); +} + +/** + * Implements hook_civicrm_enable(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable + */ +function api4_civicrm_enable() { + _api4_civix_civicrm_enable(); +} + +/** + * Implements hook_civicrm_disable(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable + */ +function api4_civicrm_disable() { + _api4_civix_civicrm_disable(); +} + +/** + * Implements hook_civicrm_upgrade(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade + */ +function api4_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { + return _api4_civix_civicrm_upgrade($op, $queue); +} + +/** + * Implements hook_civicrm_managed(). + * + * Generate a list of entities to create/deactivate/delete when this module + * is installed, disabled, uninstalled. + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed + */ +function api4_civicrm_managed(&$entities) { + _api4_civix_civicrm_managed($entities); +} + +/** + * Implements hook_civicrm_angularModules(). + * + * Generate a list of Angular modules. + * + * Note: This hook only runs in CiviCRM 4.5+. It may + * use features only available in v4.6+. + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes + */ +function api4_civicrm_angularModules(&$angularModules) { + _api4_civix_civicrm_angularModules($angularModules); +} + +/** + * Implements hook_civicrm_alterSettingsFolders(). + * + * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders + */ +function api4_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { + _api4_civix_civicrm_alterSettingsFolders($metaDataFolders); +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/css/explorer.css b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/css/explorer.css new file mode 100644 index 00000000000..9af359c51f4 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/css/explorer.css @@ -0,0 +1,156 @@ +/* Style rules for Api4 Explorer */ + +#bootstrap-theme.api4-explorer-page .panel-heading { + height: 50px; +} +#bootstrap-theme.api4-explorer-page .explorer-params-panel .panel-heading { + padding-top: 12px; +} +#bootstrap-theme.api4-explorer-page .explorer-params-panel .panel-heading button { + position: relative; + top: -5px; +} +#bootstrap-theme.api4-explorer-page .row .panel-body { + min-height: 400px; + overflow-x: auto; +} +#bootstrap-theme.api4-explorer-page .row .explorer-help-panel .panel-body { + max-height: 400px; + overflow: auto; +} +/* Fix weird shorditch style */ +#bootstrap-theme.api4-explorer-page .row .panel-heading { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + margin-bottom: 0; +} +#bootstrap-theme.api4-explorer-page .explorer-code-panel table td:first-child { + width: 5em; +} + +#bootstrap-theme.api4-explorer-page .explorer-params-panel > .panel-body > div.api4-input { + margin-bottom: 10px; +} + +#bootstrap-theme.api4-explorer-page form label { + text-transform: capitalize; +} + +#bootstrap-theme.api4-explorer-page fieldset { + padding: 6px; + border: 1px solid lightgrey; + margin-bottom: 10px; + position: relative; +} + +#bootstrap-theme.api4-explorer-page fieldset legend { + background-color: white; + font-size: 13px; + margin: 0; + width: auto; + border: 0 none; + padding: 2px 5px; + text-transform: capitalize; +} +#bootstrap-theme.api4-explorer-page fieldset > .btn-group { + position: absolute; + right: 0; + top: 11px; +} +#bootstrap-theme.api4-explorer-page fieldset > .btn-group .btn { + border: 0 none; +} + +#bootstrap-theme.api4-explorer-page fieldset div.api4-input { + margin-bottom: 10px; +} + +#bootstrap-theme.api4-explorer-page fieldset div.api4-input.ui-sortable-helper { + background-color: rgba(255, 255, 255, .9); +} + +#bootstrap-theme.api4-explorer-page fieldset div.api4-input.ui-sortable-helper { + background-color: rgba(255, 255, 255, .9); +} + +#bootstrap-theme.api4-explorer-page div.api4-input.form-inline .form-control { + margin-right: 6px; +} + +#bootstrap-theme.api4-explorer-page div.api4-input.form-inline .form-control:not(.api4-option-selected) { + transition: none; + box-shadow: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; +} + +#bootstrap-theme.api4-explorer-page div.api4-input.form-inline .form-control label { + font-weight: normal; + position: relative; + top: -2px; +} + +#bootstrap-theme.api4-explorer-page .api4-where-fieldset fieldset { + float: right; + width: calc(100% - 58px); + margin-top: -8px; +} + +#bootstrap-theme.api4-explorer-page .api4-where-fieldset.api4-sorting fieldset .api4-where-group-sortable { + min-height: 3.5em; +} + +#bootstrap-theme.api4-explorer-page .api4-input-group { + display: inline-block; +} + +#bootstrap-theme.api4-explorer-page .api4-clause-badge { + width: 55px; + display: inline-block; + cursor: move; +} +#bootstrap-theme.api4-explorer-page .api4-clause-badge .badge { + opacity: .5; + position: relative; +} +#bootstrap-theme.api4-explorer-page .api4-clause-badge .caret { + margin: 0; +} +#bootstrap-theme.api4-explorer-page .api4-clause-badge .crm-i { + display: none; + padding: 0 6px; +} +#bootstrap-theme.api4-explorer-page .ui-sortable-helper .api4-clause-badge .badge span { + display: none; +} +#bootstrap-theme.api4-explorer-page .ui-sortable-helper .api4-clause-badge .crm-i { + display: inline-block; +} + +#bootstrap-theme.api4-explorer-page .api4-operator { + width: 70px; +} + +#bootstrap-theme.api4-explorer-page .api4-add-where-group-menu { + min-width: 80px; + background-color: rgba(186, 225, 251, 0.94); +} +#bootstrap-theme.api4-explorer-page .api4-add-where-group-menu a { + padding: 5px 10px; +} + +/* Collapsible optgroups for select2 */ +div.select2-drop.collapsible-optgroups-enabled .select2-result-with-children:not(.optgroup-expanded) > .select2-result-sub > li.select2-result { + display: none; +} +div.select2-drop.collapsible-optgroups-enabled .select2-result-with-children > .select2-result-label:before { + font-family: FontAwesome; + content: "\f0da"; + display: inline-block; + padding-right: 3px; + vertical-align: middle; + font-weight: normal; +} +div.select2-drop.collapsible-optgroups-enabled .select2-result-with-children.optgroup-expanded > .select2-result-label:before { + content: "\f0d7"; +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/images/ApiExplorer.png b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/images/ApiExplorer.png new file mode 100644 index 00000000000..093cf2a6b2e Binary files /dev/null and b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/images/ApiExplorer.png differ diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/info.xml b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/info.xml new file mode 100644 index 00000000000..afe7956acee --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/info.xml @@ -0,0 +1,29 @@ + + + api4 + API v4 + Makes version 4 of the CiviCRM API available to other extensions. + AGPL-3.0 + + Coleman Watts + coleman@civicrm.org + + + https://github.com/civicrm/api4 + https://wiki.civicrm.org/confluence/display/CRM/API+v4+Spec + http://www.gnu.org/licenses/agpl-3.0.html + + 2018-09-05 + 4.0.0 + stable + + 5.0 + + This extension does nothing on its own. Install it if other extensions require you to do so. + + + + + CRM/Api4 + + diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/js/api4.js b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/js/api4.js new file mode 100644 index 00000000000..bde57c27e49 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/js/api4.js @@ -0,0 +1,41 @@ +(function($, _) { + + // result is an array, but in js, an array is also an object + // Assign all the metadata properties to it, mirroring the results arrayObject in php + function arrayObject(data) { + var result = data.values || []; + delete(data.values); + _.assign(result, data); + return result; + } + + CRM.api4 = function(entity, action, params) { + var deferred = $.Deferred(); + if (typeof entity === 'string') { + $.post(CRM.url('civicrm/ajax/api4/' + entity + '/' + action), { + params: JSON.stringify(params) + }) + .done(function (data) { + deferred.resolve(arrayObject(data)); + }) + .fail(function (data) { + deferred.reject(data.responseJSON); + }); + } else { + $.post(CRM.url('civicrm/ajax/api4'), { + calls: JSON.stringify(entity) + }) + .done(function(data) { + _.each(data, function(item, index) { + data[index] = arrayObject(item); + }); + deferred.resolve(data); + }) + .fail(function (data) { + deferred.reject(data.responseJSON); + }); + } + + return deferred; + }; +})(CRM.$, CRM._); \ No newline at end of file diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/phpunit.xml.dist b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/phpunit.xml.dist new file mode 100644 index 00000000000..bf600a87c9e --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + ./tests/phpunit + + + + + + ./ + + + + + + + + + diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/readme.md b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/readme.md new file mode 100644 index 00000000000..1efc8b5194f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/readme.md @@ -0,0 +1,94 @@ +CiviCRM API Version 4 +===================== + +Welcome +------- + +This is the latest version of the API (Application Programming Interface) for CiviCRM. If you are here because you're trying to install an extension that requires this, just install this and you're done! + +If you are a developer, read on... + +Using Api4 +---------- + +Once installed you can navigate to **Support -> Developer -> Api4 Explorer** in the menu. This gives a live, interactive code generator in which you can build and test api calls: + +![Screenshot](/images/ApiExplorer.png) + +Output +------ + +The php binding returns an [arrayObject](http://php.net/manual/en/class.arrayobject.php). This gives immediate access to the results, plus allows returning additional metadata properties. + + +```php +$result = \Civi\Api4\Contact::get()->execute(); + +// you can loop through the results directly +foreach ($result as $contact) {} + +// you can just grab the first one +$contact1 = $result->first(); + +// reindex results on-the-fly (replacement for sequential=1 in v3) +$result->indexBy('id'); + +// or fetch some metadata about the call +$entity = $result->entity; // "Contact" +``` + +We can do the something very similar in javascript thanks to js arrays also being objects: + +```javascript +CRM.api4('Contact', 'get', params).done(function(result) { + // you can loop through the results + result.forEach(function(contact, n) {}); + + // you can just grab the first one + var contact1 = result[0]; + + // or fetch some metadata about the call + var entity = result.entity; // "Contact" +}); +``` + +Notable changes from Version 3: +------------------------------- + +* Instead of a single `$params` array, each api action has multiple methods to set various parameters. +* Output is an array with object properties rather than a nested array. +* Use the `Update` action to update an entity rather than `Create` with an id. +* Use `$result->indexBy('id');` rather than `sequential => 0`. +* `getSingle` is gone, use `$result->first()`. +* Custom fields are refered to by name rather than id. E.g. use `constituent_information.Most_Important_Issue` instead of `custom_4`. + +Creating Apis for an Extension +------------------------------ + +If your extension creates one or more entities (sql tables with a DAO object) you can expose it to the api simply by creating a class (e.g. `\Civi\Api4\MyEntity`), and optionally declare permissions, set default values, and add custom actions. + + +Architecture +------------ + +* A series of **action classes** inherit from the base [`Action`](Civi/Api4/Action.php) class, e.g. [`Create`](Civi/Api4/Action/Create.php). +* Each entity may extend the generic action class to provide extra parameters or functionality. +* [`Update`](Civi/Api4/Action/Update.php), [`Replace`](Civi/Api4/Action/Replace.php) and [`Delete`](Civi/Api4/Action/Delete.php) actions extend the [`Get`](Civi/Api4/Action/Get.php) class allowing them to perform bulk operations. +* The `Action` class uses the magic [__call()](http://php.net/manual/en/language.oop5.overloading.php#object.call) method to `set`, `add` and `get` parameters. +* The base action `execute()` method calls the core [`civi_api_kernel`](https://github.com/civicrm/civicrm-core/blob/master/Civi/API/Kernel.php) +service `runRequest()` method. Action objects find their business access objects via [V3 API code](https://github.com/civicrm/civicrm-core/blob/master/api/v3/utils.php#L381). +* Each action object has a `_run()` method that accepts a decorated [arrayobject](http://php.net/manual/en/class.arrayobject.php) ([`Result`](Civi/API/Result.php)) as a parameter and is accessed by the action's `execute()` method. +* The **get** action class uses a [`Api4SelectQuery`](Civi/API/Api4SelectQuery.php) object +(based on the core +[SelectQuery](https://github.com/civicrm/civicrm-core/blob/master/Civi/API/SelectQuery.php) object. + +Security +-------- + +Each `action` object has a `$checkPermissions` property. This always defaults to `TRUE`, and for calls from REST it cannot be disabled. + +Tests +----- + +Tests are located in the `tests` directory (surprise!) +To run the entire Api4 test suite go to the api4 extension directory and type `phpunit4` from the command line. diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/services.xml b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/services.xml new file mode 100644 index 00000000000..95de18ec265 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/services.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/BaseCustomValueTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/BaseCustomValueTest.php new file mode 100644 index 00000000000..0ec46510528 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/BaseCustomValueTest.php @@ -0,0 +1,30 @@ + [ + 'civicrm_custom_group', + 'civicrm_custom_field', + 'civicrm_contact', + 'civicrm_option_group', + 'civicrm_option_value' + ], + ]; + + $this->dropByPrefix('civicrm_value_mycontact'); + $this->cleanup($cleanup_params); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/BasicCustomFieldTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/BasicCustomFieldTest.php new file mode 100644 index 00000000000..080273301b4 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/BasicCustomFieldTest.php @@ -0,0 +1,70 @@ +setCheckPermissions(FALSE) + ->addValue('name', 'MyContactFields') + ->addValue('extends', 'Contact') + ->execute() + ->first(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavColor') + ->addValue('custom_group_id', $customGroup['id']) + ->addValue('html_type', 'Text') + ->addValue('data_type', 'String') + ->execute(); + + $contactId = Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Johann') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->addValue('MyContactFields.FavColor', 'Red') + ->execute() + ->first()['id']; + + $contact = Contact::get() + ->setCheckPermissions(FALSE) + ->addSelect('first_name') + ->addSelect('MyContactFields.FavColor') + ->addWhere('id', '=', $contactId) + ->addWhere('MyContactFields.FavColor', '=', 'Red') + ->execute() + ->first(); + + $this->assertArrayHasKey('MyContactFields', $contact); + $contactFields = $contact['MyContactFields']; + $this->assertArrayHasKey('FavColor', $contactFields); + $this->assertEquals('Red', $contactFields['FavColor']); + + Contact::update() + ->addWhere('id', '=', $contactId) + ->addValue('MyContactFields.FavColor', 'Blue') + ->execute(); + + $contact = Contact::get() + ->setCheckPermissions(FALSE) + ->addSelect('MyContactFields.FavColor') + ->addWhere('id', '=', $contactId) + ->execute() + ->first(); + + $contactFields = $contact['MyContactFields']; + $this->assertEquals('Blue', $contactFields['FavColor']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ComplexQueryTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ComplexQueryTest.php new file mode 100644 index 00000000000..dcc14ddd8c5 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ComplexQueryTest.php @@ -0,0 +1,90 @@ +cleanup(['tablesToTruncate' => $relatedTables]); + $this->loadDataSet('DefaultDataSet'); + + return parent::setUpHeadless(); + } + + /** + * Fetch all activities for housing support cases. Expects a single activity + * loaded from the data set. + */ + public function testGetAllHousingSupportActivities() { + $results = Activity::get() + ->setCheckPermissions(FALSE) + ->addWhere('activity_type.name', '=', 'housing_support') + ->execute(); + + $this->assertCount(1, $results); + } + + /** + * Fetch all activities with a blue tag; and return all tags on the activities + */ + public function testGetAllTagsForBlueTaggedActivities() { + + } + + /** + * Fetch contacts named 'Bob' and all of their blue activities + */ + public function testGetAllBlueActivitiesForBobs() { + + } + + /** + * Get all contacts in a zipcode and return their Home or Work email addresses + */ + public function testGetHomeOrWorkEmailsForContactsWithZipcode() { + + } + + /** + * Fetch all activities where Bob is the assignee or source + */ + public function testGetActivitiesWithBobAsAssigneeOrSource() { + + } + + /** + * Get all contacts which + * (a) have address in zipcode 94117 or 94118 or in city "San Francisco","LA" + * and + * (b) are not deceased and + * (c) have a custom-field "most_important_issue=Environment". + */ + public function testAWholeLotOfConditions() { + + } + + /** + * Get participants who attended CiviCon 2012 but not CiviCon 2013. + * Return their name and email. + */ + public function testGettingNameAndEmailOfAttendeesOfCiviCon2012Only() { + + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CreateCustomValueTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CreateCustomValueTest.php new file mode 100644 index 00000000000..762b46173c2 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CreateCustomValueTest.php @@ -0,0 +1,64 @@ + 'Red', 'g' => 'Green', 'b' => 'Blue']; + + $customGroup = CustomGroup::create() + ->setCheckPermissions(FALSE) + ->addValue('name', 'MyContactFields') + ->addValue('extends', 'Contact') + ->execute() + ->first(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'Color') + ->addValue('options', $optionValues) + ->addValue('custom_group_id', $customGroup['id']) + ->addValue('html_type', 'Select') + ->addValue('data_type', 'String') + ->execute(); + + $customField = CustomField::get() + ->setCheckPermissions(FALSE) + ->addWhere('label', '=', 'Color') + ->execute() + ->first(); + + $this->assertNotNull($customField['option_group_id']); + $optionGroupId = $customField['option_group_id']; + + $optionGroup = OptionGroup::get() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $optionGroupId) + ->execute() + ->first(); + + $this->assertEquals('Color', $optionGroup['title']); + + $createdOptionValues = OptionValue::get() + ->setCheckPermissions(FALSE) + ->addWhere('option_group_id', '=', $optionGroupId) + ->execute() + ->getArrayCopy(); + + $values = array_column($createdOptionValues, 'value'); + $labels = array_column($createdOptionValues, 'label'); + $createdOptionValues = array_combine($values, $labels); + + $this->assertEquals($optionValues, $createdOptionValues); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CreateWithOptionGroupTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CreateWithOptionGroupTest.php new file mode 100644 index 00000000000..ffb86c88905 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CreateWithOptionGroupTest.php @@ -0,0 +1,178 @@ +dropByPrefix('civicrm_value_financial'); + $this->dropByPrefix('civicrm_value_favorite'); + parent::setUp(); + } + + public function testGetWithCustomData() { + $customGroupId = CustomGroup::create() + ->setCheckPermissions(FALSE) + ->addValue('name', 'FavoriteThings') + ->addValue('extends', 'Contact') + ->execute() + ->first()['id']; + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavColor') + ->addValue('options', ['r' => 'Red', 'g' => 'Green', 'b' => 'Blue']) + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Select') + ->addValue('data_type', 'String') + ->execute(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavFood') + ->addValue('options', ['1' => 'Corn', '2' => 'Potatoes', '3' => 'Cheese']) + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Select') + ->addValue('data_type', 'String') + ->execute(); + + $customGroupId = CustomGroup::create() + ->setCheckPermissions(FALSE) + ->addValue('name', 'FinancialStuff') + ->addValue('extends', 'Contact') + ->execute() + ->first()['id']; + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'Salary') + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Number') + ->addValue('data_type', 'Money') + ->execute(); + + Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Jerome') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->addValue('FavoriteThings.FavColor', 'r') + ->addValue('FavoriteThings.FavFood', '1') + ->addValue('FinancialStuff.Salary', 50000) + ->execute(); + + $result = Contact::get() + ->setCheckPermissions(FALSE) + ->addSelect('first_name') + ->addSelect('FavoriteThings.FavColor.label') + ->addSelect('FavoriteThings.FavFood.label') + ->addSelect('FinancialStuff.Salary') + ->addWhere('FavoriteThings.FavFood.label', 'IN', ['Corn', 'Potatoes']) + ->addWhere('FinancialStuff.Salary', '>', '10000') + ->execute() + ->first(); + + $this->assertArrayHasKey('FavoriteThings', $result); + $favoriteThings = $result['FavoriteThings']; + $favoriteFood = $favoriteThings['FavFood']; + $favoriteColor = $favoriteThings['FavColor']; + $financialStuff = $result['FinancialStuff']; + $this->assertEquals('Red', $favoriteColor['label']); + $this->assertEquals('Corn', $favoriteFood['label']); + $this->assertEquals(50000, $financialStuff['Salary']); + } + + public function testWithCustomDataForMultipleContacts() { + $customGroupId = CustomGroup::create() + ->setCheckPermissions(FALSE) + ->addValue('name', 'FavoriteThings') + ->addValue('extends', 'Contact') + ->execute() + ->first()['id']; + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavColor') + ->addValue('options', ['r' => 'Red', 'g' => 'Green', 'b' => 'Blue']) + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Select') + ->addValue('data_type', 'String') + ->execute(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavFood') + ->addValue('options', ['1' => 'Corn', '2' => 'Potatoes', '3' => 'Cheese']) + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Select') + ->addValue('data_type', 'String') + ->execute(); + + $customGroupId = CustomGroup::create() + ->setCheckPermissions(FALSE) + ->addValue('name', 'FinancialStuff') + ->addValue('extends', 'Contact') + ->execute() + ->first()['id']; + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'Salary') + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Number') + ->addValue('data_type', 'Money') + ->execute(); + + Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Red') + ->addValue('last_name', 'Corn') + ->addValue('contact_type', 'Individual') + ->addValue('FavoriteThings.FavColor', 'r') + ->addValue('FavoriteThings.FavFood', '1') + ->addValue('FinancialStuff.Salary', 10000) + ->execute(); + + Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Blue') + ->addValue('last_name', 'Cheese') + ->addValue('contact_type', 'Individual') + ->addValue('FavoriteThings.FavColor', 'b') + ->addValue('FavoriteThings.FavFood', '3') + ->addValue('FinancialStuff.Salary', 500000) + ->execute(); + + $result = Contact::get() + ->setCheckPermissions(FALSE) + ->addSelect('first_name') + ->addSelect('last_name') + ->addSelect('FavoriteThings.FavColor.label') + ->addSelect('FavoriteThings.FavFood.label') + ->addSelect('FinancialStuff.Salary') + ->addWhere('FavoriteThings.FavFood.label', 'IN', ['Corn', 'Cheese']) + ->execute(); + + $blueCheese = NULL; + foreach ($result as $contact) { + if ($contact['first_name'] === 'Blue') { + $blueCheese = $contact; + } + } + + $this->assertEquals('Blue', $blueCheese['FavoriteThings']['FavColor']['label']); + $this->assertEquals('Cheese', $blueCheese['FavoriteThings']['FavFood']['label']); + $this->assertEquals(500000, $blueCheese['FinancialStuff']['Salary']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CustomValuePerformanceTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CustomValuePerformanceTest.php new file mode 100644 index 00000000000..bd669d3e898 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CustomValuePerformanceTest.php @@ -0,0 +1,94 @@ +setCheckPermissions(FALSE) + ->addValue('name', 'MyContactFields') + ->addValue('title', 'MyContactFields') + ->addValue('extends', 'Contact') + ->execute() + ->first()['id']; + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavColor') + ->addValue('custom_group_id', $customGroupId) + ->addValue('options', ['r' => 'Red', 'g' => 'Green', 'b' => 'Blue']) + ->addValue('html_type', 'Select') + ->addValue('data_type', 'String') + ->execute(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavAnimal') + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Text') + ->addValue('data_type', 'String') + ->execute(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavLetter') + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Text') + ->addValue('data_type', 'String') + ->execute(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavFood') + ->addValue('custom_group_id', $customGroupId) + ->addValue('html_type', 'Text') + ->addValue('data_type', 'String') + ->execute(); + + $this->beginQueryCount(); + + Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Red') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->addValue('MyContactFields.FavColor', 'r') + ->addValue('MyContactFields.FavAnimal', 'Sheep') + ->addValue('MyContactFields.FavLetter', 'z') + ->addValue('MyContactFields.FavFood', 'Coconuts') + ->execute(); + + Contact::get() + ->setCheckPermissions(FALSE) + ->addSelect('display_name') + ->addSelect('MyContactFields.FavColor.label') + ->addSelect('MyContactFields.FavColor.weight') + ->addSelect('MyContactFields.FavColor.is_default') + ->addSelect('MyContactFields.FavAnimal') + ->addSelect('MyContactFields.FavLetter') + ->addWhere('MyContactFields.FavColor', '=', 'r') + ->addWhere('MyContactFields.FavFood', '=', 'Coconuts') + ->addWhere('MyContactFields.FavAnimal', '=', 'Sheep') + ->addWhere('MyContactFields.FavLetter', '=', 'z') + ->execute() + ->first(); + + // FIXME: This count is artificially high due to the line + // $this->entity = Tables::getBriefName(Tables::getClassForTable($targetTable)); + // In class Joinable. TODO: Investigate why. + $this->markTestIncomplete("Query count: " . $this->getQueryCount()); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CustomValueTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CustomValueTest.php new file mode 100644 index 00000000000..0391806d097 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/CustomValueTest.php @@ -0,0 +1,194 @@ + 'Red', 'g' => 'Green', 'b' => 'Blue']; + + $customGroup = CustomGroup::create() + ->setCheckPermissions(FALSE) + ->addValue('name', 'MyContactFields') + ->addValue('extends', 'Contact') + ->addValue('is_multiple', TRUE) + ->execute() + ->first(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'Color') + ->addValue('options', $optionValues) + ->addValue('custom_group_id', $customGroup['id']) + ->addValue('html_type', 'Select') + ->addValue('data_type', 'String') + ->execute(); + + $customField = CustomField::get() + ->setCheckPermissions(FALSE) + ->addWhere('label', '=', 'Color') + ->execute() + ->first(); + + $this->contactID = Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Johann') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->execute() + ->first()['id']; + } + + /** + * Test CustomValue::getFields + */ + public function testGetFields() { + // Create custom group and its field + $this->createCustomData(); + + // Retrieve and check the fields of CustomValue = Custom_MyContactFields + $fields = CustomValue::getFields('MyContactFields')->execute(); + $expectedResult = [ + [ + 'custom_field_id' => 1, + 'custom_group' => 'MyContactFields', + 'table_name' => 'civicrm_value_mycontactfiel_1', + 'column_name' => 'color_1', + 'name' => 'Color', + 'title' => ts('Color'), + 'entity' => 'Custom_MyContactFields', + 'data_type' => 'String', + 'fk_entity' => NULL, + ], + [ + 'name' => 'id', + 'title' => ts('Custom Table Unique ID'), + 'entity' => 'Custom_MyContactFields', + 'data_type' => 'Integer', + 'fk_entity' => NULL, + ], + [ + 'name' => 'entity_id', + 'title' => ts('Entity ID'), + 'entity' => 'Custom_MyContactFields', + 'data_type' => 'Integer', + 'fk_entity' => 'Contact', + ], + ]; + + foreach ($expectedResult as $key => $field) { + foreach ($field as $attr => $value) { + $this->assertEquals($expectedResult[$key][$attr], $fields[$key][$attr]); + } + } + } + + /** + * Test CustomValue::Get/Create/Update/Replace/Delete + */ + public function testCRUD() { + $this->createCustomData(); + + // CASE 1: Test CustomValue::create + // Create two records for a single contact and using CustomValue::get ensure that two records are created + CustomValue::create('MyContactFields') + ->addValue("Color", 'Green') + ->addValue("entity_id", $this->contactID) + ->execute(); + CustomValue::create('MyContactFields') + ->addValue("Color", 'Red') + ->addValue("entity_id", $this->contactID) + ->execute(); + // fetch custom values using API4 CustomValue::get + $result = CustomValue::get('MyContactFields')->execute(); + + // check if two custom values are created + $this->assertEquals(2, count($result)); + $expectedResult = [ + [ + 'id' => 1, + 'Color' => 'Green', + 'entity_id' => $this->contactID, + ], + [ + 'id' => 2, + 'Color' => 'Red', + 'entity_id' => $this->contactID, + ], + ]; + // match the data + foreach ($expectedResult as $key => $field) { + foreach ($field as $attr => $value) { + $this->assertEquals($expectedResult[$key][$attr], $result[$key][$attr]); + } + } + + // CASE 2: Test CustomValue::update + // Update a records whose id is 1 and change the custom field (name = Color) value to 'White' from 'Green' + CustomValue::update('MyContactFields') + ->addWhere("id", "=", 1) + ->addValue("Color", 'White') + ->execute(); + + // ensure that the value is changed for id = 1 + $color = CustomValue::get('MyContactFields') + ->addWhere("id", "=", 1) + ->execute() + ->first()['Color']; + $this->assertEquals('White', $color); + + // CASE 3: Test CustomValue::replace + // create a second contact which will be used to replace the custom values, created earlier + $secondContactID = Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Adam') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->execute() + ->first()['id']; + // Replace all the records which was created earlier with entity_id = first contact + // with custom record ['Color' => 'Rainbow', 'entity_id' => $secondContactID] + CustomValue::replace('MyContactFields') + ->setRecords([['Color' => 'Rainbow', 'entity_id' => $secondContactID]]) + ->addWhere('entity_id', '=', $this->contactID) + ->execute(); + + // Check the two records created earlier is replaced by new contact + $result = CustomValue::get('MyContactFields')->execute(); + $this->assertEquals(1, count($result)); + + $expectedResult = [ + [ + 'id' => 3, + 'Color' => 'Rainbow', + 'entity_id' => $secondContactID, + ], + ]; + foreach ($expectedResult as $key => $field) { + foreach ($field as $attr => $value) { + $this->assertEquals($expectedResult[$key][$attr], $result[$key][$attr]); + } + } + + // CASE 4: Test CustomValue::delete + // There is only record left whose id = 3, delete that record on basis of criteria id = 3 + CustomValue::delete('MyContactFields')->addWhere("id", "=", 3)->execute(); + $result = CustomValue::get('MyContactFields')->execute(); + // check that there are no custom values present + $this->assertEquals(0, count($result)); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ExtendFromIndividualTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ExtendFromIndividualTest.php new file mode 100644 index 00000000000..2d3be50ca3c --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ExtendFromIndividualTest.php @@ -0,0 +1,54 @@ +setCheckPermissions(FALSE) + ->addValue('name', 'MyContactFields') + ->addValue('extends', 'Individual') // not Contact + ->execute() + ->first(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavColor') + ->addValue('custom_group_id', $customGroup['id']) + ->addValue('html_type', 'Text') + ->addValue('data_type', 'String') + ->execute(); + + $contactId = Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Johann') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->addValue('MyContactFields.FavColor', 'Red') + ->execute() + ->first()['id']; + + $contact = Contact::get() + ->setCheckPermissions(FALSE) + ->addSelect('display_name') + ->addSelect('MyContactFields.FavColor') + ->addWhere('id', '=', $contactId) + ->execute() + ->first(); + + $this->assertArrayHasKey('MyContactFields', $contact); + $contactFields = $contact['MyContactFields']; + $favColor = $contactFields['FavColor']; + $this->assertEquals('Red', $favColor); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/FkJoinTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/FkJoinTest.php new file mode 100644 index 00000000000..20ec50dcf65 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/FkJoinTest.php @@ -0,0 +1,78 @@ +cleanup(['tablesToTruncate' => $relatedTables]); + $this->loadDataSet('DefaultDataSet'); + + return parent::setUpHeadless(); + } + + /** + * Fetch all activities for housing support cases. Expects a single activity + * loaded from the data set. + */ + public function testThreeLevelJoin() { + $results = Activity::get() + ->setCheckPermissions(FALSE) + ->addWhere('activity_type.name', '=', 'housing_support') + ->execute(); + + $this->assertCount(1, $results); + } + + public function testActivityContactJoin() { + $results = Activity::get() + ->setCheckPermissions(FALSE) + ->addSelect('assignees.id') + ->addSelect('assignees.first_name') + ->addSelect('assignees.display_name') + ->addWhere('assignees.first_name', '=', 'Test') + ->execute(); + + $firstResult = $results->first(); + + $this->assertCount(1, $results); + $this->assertTrue(is_array($firstResult['assignees'])); + + $firstAssignee = array_shift($firstResult['assignees']); + $this->assertEquals($firstAssignee['first_name'], 'Test'); + } + + public function testContactPhonesJoin() { + $testContact = $this->getReference('test_contact_1'); + $testPhone = $this->getReference('test_phone_1'); + + $results = Contact::get() + ->setCheckPermissions(FALSE) + ->addSelect('phones.phone') + ->addWhere('id', '=', $testContact['id']) + ->addWhere('phones.location_type.name', '=', 'Home') + ->execute() + ->first(); + + $this->assertArrayHasKey('phones', $results); + $this->assertCount(1, $results['phones']); + $firstPhone = array_shift($results['phones']); + $this->assertEquals($testPhone['phone'], $firstPhone['phone']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/GetExtraFieldsTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/GetExtraFieldsTest.php new file mode 100644 index 00000000000..bc9b10e7ade --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/GetExtraFieldsTest.php @@ -0,0 +1,26 @@ +execute() + ->getArrayCopy(); + + $baseFields = \CRM_Contact_BAO_Contact::fields(); + $baseFieldNames = array_column($baseFields, 'name'); + $returnedFieldNames = array_column($returnedFields, 'name'); + $notReturned = array_diff($baseFieldNames, $returnedFieldNames); + + $this->assertEmpty($notReturned); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/NullValueTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/NullValueTest.php new file mode 100644 index 00000000000..dc4f656aadf --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/NullValueTest.php @@ -0,0 +1,55 @@ +set('display_name_format', $format); + return parent::setUpHeadless(); + } + + public function testStringNull() { + $contact = Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Joseph') + ->addValue('last_name', 'null') + ->addValue('contact_type', 'Individual') + ->execute() + ->first(); + + $this->assertSame('Null', $contact['last_name']); + $this->assertSame('Joseph Null', $contact['display_name']); + } + + public function testSettingToNull() { + $contact = Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'ILoveMy') + ->addValue('last_name', 'LastName') + ->addValue('contact_type', 'Individual') + ->execute() + ->first(); + + $this->assertSame('ILoveMy LastName', $contact['display_name']); + $contactId = $contact['id']; + + $contact = Contact::update() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $contactId) + ->addValue('last_name', NULL) + ->execute() + ->first(); + + $this->assertSame(NULL, $contact['last_name']); + $this->assertSame('ILoveMy', $contact['display_name']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ReplaceTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ReplaceTest.php new file mode 100644 index 00000000000..5306c75159f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/ReplaceTest.php @@ -0,0 +1,70 @@ +addValue('first_name', 'Lotsa') + ->addValue('last_name', 'Emails') + ->execute() + ->first()['id']; + $cid2 = Contact::create() + ->addValue('first_name', 'Notso') + ->addValue('last_name', 'Many') + ->execute() + ->first()['id']; + $e0 = Email::create() + ->setValues(['contact_id' => $cid2, 'email' => 'nosomany@example.com', 'location_type_id' => 1]) + ->execute() + ->first()['id']; + $e1 = Email::create() + ->setValues(['contact_id' => $cid1, 'email' => 'first@example.com', 'location_type_id' => 1]) + ->execute() + ->first()['id']; + $e2 = Email::create() + ->setValues(['contact_id' => $cid1, 'email' => 'second@example.com', 'location_type_id' => 1]) + ->execute() + ->first()['id']; + $replacement = [ + ['email' => 'firstedited@example.com', 'id' => $e1], + ['contact_id' => $cid1, 'email' => 'third@example.com', 'location_type_id' => 1] + ]; + $replaced = Email::replace() + ->setRecords($replacement) + ->addWhere('contact_id', '=', $cid1) + ->execute(); + // Should have saved 2 records + $this->assertEquals(2, $replaced->count()); + // Should have deleted email2 + $this->assertEquals([$e2], $replaced->deleted); + // Verify contact now has the new email records + $results = Email::get() + ->addWhere('contact_id', '=', $cid1) + ->execute() + ->indexBy('id'); + $this->assertEquals('firstedited@example.com', $results[$e1]['email']); + $this->assertEquals(2, $results->count()); + $this->assertArrayNotHasKey($e2, (array) $results); + $this->assertArrayNotHasKey($e0, (array) $results); + unset($results[$e1]); + foreach ($results as $result) { + $this->assertEquals('third@example.com', $result['email']); + } + // Validate our other contact's email did not get deleted + $c2email = Email::get() + ->addWhere('contact_id', '=', $cid2) + ->execute() + ->first(); + $this->assertEquals('nosomany@example.com', $c2email['email']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/UpdateContactTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/UpdateContactTest.php new file mode 100644 index 00000000000..71265598a66 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/UpdateContactTest.php @@ -0,0 +1,34 @@ +setCheckPermissions(FALSE) + ->addValue('first_name', 'Johann') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->execute() + ->first()['id']; + + $contact = Contact::update() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $contactId) + ->addValue('first_name', 'Testy') + ->execute() + ->first(); + $this->assertEquals('Testy', $contact['first_name']); + $this->assertEquals('Tester', $contact['last_name']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/UpdateCustomValueTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/UpdateCustomValueTest.php new file mode 100644 index 00000000000..99a9f01101f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Action/UpdateCustomValueTest.php @@ -0,0 +1,56 @@ +setCheckPermissions(FALSE) + ->addValue('name', 'MyContactFields') + ->addValue('extends', 'Contact') + ->execute() + ->first(); + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavColor') + ->addValue('custom_group_id', $customGroup['id']) + ->addValue('html_type', 'Text') + ->addValue('data_type', 'String') + ->execute(); + + $contactId = Contact::create() + ->setCheckPermissions(FALSE) + ->addValue('first_name', 'Red') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->addValue('MyContactFields.FavColor', 'Red') + ->execute() + ->first()['id']; + + Contact::update() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $contactId) + ->addValue('first_name', 'Red') + ->addValue('last_name', 'Tester') + ->addValue('contact_type', 'Individual') + ->addValue('MyContactFields.FavColor', 'Blue') + ->execute(); + + $result = CustomValueTable::getEntityValues($contactId, 'Contact'); + + $this->assertEquals(1, count($result)); + $this->assertContains('Blue', $result); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/ConformanceTest.json b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/ConformanceTest.json new file mode 100644 index 00000000000..85b74c177a9 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/ConformanceTest.json @@ -0,0 +1,62 @@ +{ + "Contact": [ + { + "first_name": "Janice", + "last_name": "Voss", + "contact_type": "Individual" + } + ], + "CustomGroup": [ + { + "name": "MyFavoriteThings", + "extends": "Contact" + } + ], + "OptionGroup": [ + { + "name": "payment_instrument" + }, + { + "name": "contribution_status" + }, + { + "name": "account_relationship" + }, + { + "name": "event_type" + }, + { + "name": "activity_type" + } + ], + "OptionValue": [ + { + "option_group": "contribution_status", + "label": "Completed" + }, + { + "option_group": "payment_instrument", + "label": "cash", + "is_default": 1 + }, + { + "option_group": "account_relationship", + "label": "Sales Tax Account is" + }, + { + "option_group": "event_type", + "label": "major_historical_event" + }, + { + "option_group": "activity_type", + "label": "Contribution" + } + ], + "Event": [ + { + "start_date": "20401010000000", + "title": "The Singularity", + "event_type_id": "major_historical_event" + } + ] +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/DefaultDataSet.json b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/DefaultDataSet.json new file mode 100644 index 00000000000..1f8bd9e09de --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/DefaultDataSet.json @@ -0,0 +1,80 @@ +{ + "Contact": [ + { + "first_name": "Test", + "last_name": "Contact", + "contact_type": "Individual", + "@ref": "test_contact_1" + }, + { + "first_name": "Second", + "last_name": "Test", + "contact_type": "Individual", + "@ref": "test_contact_2" + } + ], + "OptionGroup": [ + { + "name": "activity_type" + }, + { + "name": "activity_contacts" + } + ], + "OptionValue": [ + { + "label": "Housing Support", + "name": "housing_support", + "option_group": "activity_type" + }, + { + "label": "Another Activity Type", + "name": "another_activity_type", + "option_group": "activity_type" + }, + { + "label": "Activity Source", + "name": "Activity Source", + "option_group": "activity_contacts" + }, + { + "label": "Activity Assignees", + "name": "Activity Assignees", + "option_group": "activity_contacts" + }, + { + "label": "Activity Targets", + "name": "Activity Targets", + "option_group": "activity_contacts" + } + ], + "Activity": [ + { + "name": "Test Housing Support Activity", + "activity_type": "housing_support", + "source_contact_id": "@ref test_contact_1.id" + }, + { + "name": "Another Activity", + "activity_type": "another_activity_type", + "source_contact_id": "@ref test_contact_1.id", + "assignee_contact_id": [ + "@ref test_contact_1.id", + "@ref test_contact_2.id" + ] + } + ], + "Phone": [ + { + "contact_id": "@ref test_contact_1.id", + "phone": "+35355439483", + "location_type_id": "1", + "@ref": "test_phone_1" + }, + { + "contact_id": "@ref test_contact_1.id", + "phone": "+3538733439483", + "location_type_id": "2" + } + ] +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/MultiContactMultiEmail.json b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/MultiContactMultiEmail.json new file mode 100644 index 00000000000..ce3fbcaf92e --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/MultiContactMultiEmail.json @@ -0,0 +1,42 @@ +{ + "Contact": [ + { + "first_name": "First", + "last_name": "Contact", + "contact_type": "Individual", + "@ref": "test_contact_1" + }, + { + "first_name": "Second", + "last_name": "Contact", + "contact_type": "Individual", + "@ref": "test_contact_2" + } + ], + "Email": [ + { + "contact_id": "@ref test_contact_1.id", + "email": "test_contact_one_home@fakedomain.com", + "location_type_id": 1, + "@ref": "test_email_1" + }, + { + "contact_id": "@ref test_contact_1.id", + "email": "test_contact_one_work@fakedomain.com", + "location_type_id": 2, + "@ref": "test_email_2" + }, + { + "contact_id": "@ref test_contact_2.id", + "email": "test_contact_two_home@fakedomain.com", + "location_type_id": 1, + "@ref": "test_email_3" + }, + { + "contact_id": "@ref test_contact_2.id", + "email": "test_contact_two_work@fakedomain.com", + "location_type_id": 2, + "@ref": "test_email_4" + } + ] +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/ParticipantRoleOptionGroup.json b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/ParticipantRoleOptionGroup.json new file mode 100644 index 00000000000..6010c1ed741 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/ParticipantRoleOptionGroup.json @@ -0,0 +1,25 @@ +{ + "OptionGroup": [ + { + "name": "participant_role" + } + ], + "OptionValue": [ + { + "label": "Attendee", + "option_group": "participant_role" + }, + { + "label": "Volunteer", + "option_group": "participant_role" + }, + { + "label": "Host", + "option_group": "participant_role" + }, + { + "label": "Speaker", + "option_group": "participant_role" + } + ] +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/SingleContact.json b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/SingleContact.json new file mode 100644 index 00000000000..2613fec7a07 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/DataSets/SingleContact.json @@ -0,0 +1,126 @@ +{ + "Contact": [ + { + "first_name": "Single", + "last_name": "Contact", + "contact_type": "Individual", + "preferred_communication_method": "phone", + "@ref": "test_contact_1" + } + ], + "OptionGroup": [ + { + "name": "activity_type" + }, + { + "name": "activity_contacts" + }, + { + "name": "preferred_communication_method" + } + ], + "OptionValue": [ + { + "label": "Did Something Amazing", + "name": "did_something_great", + "option_group": "activity_type" + }, + { + "label": "Did That Other Thing", + "name": "another_activity_type", + "option_group": "activity_type" + }, + { + "label": "Activity Source", + "option_group": "activity_contacts" + }, + { + "label": "Activity Assignees", + "option_group": "activity_contacts" + }, + { + "label": "Activity Targets", + "option_group": "activity_contacts" + }, + { + "label": "Phone", + "value": "phone", + "option_group": "preferred_communication_method" + }, + { + "label": "Email", + "value": "email", + "option_group": "preferred_communication_method" + } + ], + "Activity": [ + { + "subject": "Won A Nobel Prize", + "activity_type": "did_something_great", + "source_contact_id": "@ref test_contact_1.id", + "@ref": "test_activity_1" + }, + { + "subject": "Cleaned The House", + "activity_type": "another_activity_type", + "source_contact_id": "@ref test_contact_1.id", + "assignee_contact_id": [ + "@ref test_contact_1.id" + ], + "@ref": "test_activity_2" + } + ], + "Phone": [ + { + "contact_id": "@ref test_contact_1.id", + "phone": "+1111111111111", + "location_type_id": 1 + }, + { + "contact_id": "@ref test_contact_1.id", + "phone": "+2222222222222", + "location_type_id": 2 + } + ], + "Email": [ + { + "contact_id": "@ref test_contact_1.id", + "email": "test_contact_home@fakedomain.com", + "location_type_id": 1 + }, + { + "contact_id": "@ref test_contact_1.id", + "email": "test_contact_work@fakedomain.com", + "location_type_id": 2 + } + ], + "Address": [ + { + "contact_id": "@ref test_contact_1.id", + "street_address": "123 Sesame St.", + "location_type_id": 1 + } + ], + "Website": [ + { + "contact_id": "@ref test_contact_1.id", + "url": "http://test.com", + "website_id": 1 + } + ], + "OpenID": [ + { + "contact_id": "@ref test_contact_1.id", + "openid": "123", + "allowed_to_login": 1, + "location_type_id": 1 + } + ], + "IM": [ + { + "contact_id": "@ref test_contact_1.id", + "name": "123", + "location_type_id": 1 + } + ] +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ConformanceTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ConformanceTest.php new file mode 100644 index 00000000000..edde5a90a91 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ConformanceTest.php @@ -0,0 +1,221 @@ +dropByPrefix('civicrm_value_myfavorite'); + $this->cleanup(['tablesToTruncate' => $tablesToTruncate]); + $this->loadDataSet('ConformanceTest'); + $this->creationParamProvider = \Civi::container()->get('test.param_provider'); + parent::setUp(); + // calculateTaxAmount() for contribution triggers a deprecation notice + \PHPUnit_Framework_Error_Deprecated::$enabled = FALSE; + } + + public function getEntities() { + $result = []; + $entities = Entity::get()->setCheckPermissions(FALSE)->execute(); + foreach ($entities as $entity) { + if ($entity['name'] != 'Entity') { + $result[] = [$entity['name']]; + } + } + return $result; + } + + /** + * Fixme: This should use getEntities as a dataProvider but that fails for some reason + */ + public function testConformance() { + $entities = $this->getEntities(); + $this->assertNotEmpty($entities); + + foreach ($entities as $data) { + $entity = $data[0]; + $entityClass = 'Civi\Api4\\' . $entity; + + $this->checkActions($entityClass); + $this->checkFields($entityClass, $entity); + $id = $this->checkCreation($entity, $entityClass); + $this->checkGet($entityClass, $id, $entity); + $this->checkUpdateFailsFromCreate($entityClass, $id); + $this->checkWrongParamType($entityClass); + $this->checkDeleteWithNoId($entityClass); + $this->checkDeletion($entityClass, $id); + $this->checkPostDelete($entityClass, $id, $entity); + } + } + + /** + * @param string $entityClass + * @param $entity + */ + protected function checkFields($entityClass, $entity) { + $fields = $entityClass::getFields() + ->setCheckPermissions(FALSE) + ->setIncludeCustom(FALSE) + ->execute() + ->indexBy('name'); + + $errMsg = sprintf('%s is missing required ID field', $entity); + $subset = ['data_type' => 'Integer']; + + $this->assertArraySubset($subset, $fields['id'], $errMsg); + } + + /** + * @param string $entityClass + */ + protected function checkActions($entityClass) { + $actions = $entityClass::getActions() + ->setCheckPermissions(FALSE) + ->execute() + ->indexBy('name'); + + $this->assertNotEmpty($actions->getArrayCopy()); + } + + /** + * @param string $entity + * @param AbstractEntity|string $entityClass + * + * @return mixed + */ + protected function checkCreation($entity, $entityClass) { + $requiredParams = $this->creationParamProvider->getRequired($entity); + $createResult = $entityClass::create() + ->setValues($requiredParams) + ->setCheckPermissions(FALSE) + ->execute() + ->first(); + + $this->assertArrayHasKey('id', $createResult, "create missing ID"); + $id = $createResult['id']; + + $this->assertGreaterThanOrEqual(1, $id, "$entity ID not positive"); + + return $id; + } + + /** + * @param AbstractEntity|string $entityClass + * @param int $id + */ + protected function checkUpdateFailsFromCreate($entityClass, $id) { + $exceptionThrown = ''; + try { + $entityClass::create() + ->setCheckPermissions(FALSE) + ->addValue('id', $id) + ->execute(); + } + catch (\API_Exception $e) { + $exceptionThrown = $e->getMessage(); + } + $this->assertContains('id', $exceptionThrown); + } + + /** + * @param AbstractEntity|string $entityClass + * @param int $id + * @param string $entity + */ + protected function checkGet($entityClass, $id, $entity) { + $getResult = $entityClass::get() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $id) + ->execute(); + + $errMsg = sprintf('Failed to fetch a %s after creation', $entity); + $this->assertEquals(1, count($getResult), $errMsg); + } + + /** + * @param AbstractEntity|string $entityClass + */ + protected function checkDeleteWithNoId($entityClass) { + $exceptionThrown = ''; + try { + $entityClass::delete() + ->execute(); + } + catch (\API_Exception $e) { + $exceptionThrown = $e->getMessage(); + } + $this->assertContains('required', $exceptionThrown); + } + + /** + * @param AbstractEntity|string $entityClass + */ + protected function checkWrongParamType($entityClass) { + $exceptionThrown = ''; + try { + $entityClass::get() + ->setCheckPermissions('nada') + ->execute(); + } + catch (\API_Exception $e) { + $exceptionThrown = $e->getMessage(); + } + $this->assertContains('checkPermissions', $exceptionThrown); + $this->assertContains('type', $exceptionThrown); + } + + /** + * @param AbstractEntity|string $entityClass + * @param int $id + */ + protected function checkDeletion($entityClass, $id) { + $deleteResult = $entityClass::delete() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $id) + ->execute(); + + // should get back an array of deleted id + $this->assertEquals([$id], (array) $deleteResult); + } + + /** + * @param AbstractEntity|string $entityClass + * @param int $id + * @param string $entity + */ + protected function checkPostDelete($entityClass, $id, $entity) { + $getDeletedResult = $entityClass::get() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $id) + ->execute(); + + $errMsg = sprintf('Entity "%s" was not deleted', $entity); + $this->assertEquals(0, count($getDeletedResult), $errMsg); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ContactJoinTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ContactJoinTest.php new file mode 100644 index 00000000000..d2db18a328c --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ContactJoinTest.php @@ -0,0 +1,106 @@ +cleanup(['tablesToTruncate' => $relatedTables]); + $this->loadDataSet('SingleContact'); + + return parent::setUpHeadless(); + } + + public function testContactJoin() { + + $contact = $this->getReference('test_contact_1'); + $entitiesToTest = ['Address', 'OpenID', 'IM', 'Website', 'Email', 'Phone']; + + foreach ($entitiesToTest as $entity) { + $results = civicrm_api4($entity, 'get', [ + 'where' => [['contact_id', '=', $contact['id']]], + 'select' => ['contact.display_name', 'contact.id'], + ]); + foreach ($results as $result) { + $this->assertEquals($contact['id'], $result['contact']['id']); + $this->assertEquals($contact['display_name'], $result['contact']['display_name']); + } + } + } + + public function testJoinToPCMWillReturnArray() { + $contact = Contact::create()->setValues([ + 'preferred_communication_method' => [1, 2, 3], + 'contact_type' => 'Individual', + 'first_name' => 'Test', + 'last_name' => 'PCM', + ])->execute()->first(); + + $fetchedContact = Contact::get() + ->addWhere('id', '=', $contact['id']) + ->addSelect('preferred_communication_method') + ->execute() + ->first(); + + $this->assertCount(3, $fetchedContact["preferred_communication_method"]); + } + + public function testJoinToPCMOptionValueWillShowLabel() { + $options = OptionValue::get() + ->addWhere('option_group.name', '=', 'preferred_communication_method') + ->execute() + ->getArrayCopy(); + + $optionValues = array_column($options, 'value'); + $labels = array_column($options, 'label'); + + $contact = Contact::create()->setValues([ + 'preferred_communication_method' => $optionValues, + 'contact_type' => 'Individual', + 'first_name' => 'Test', + 'last_name' => 'PCM', + ])->execute()->first(); + + $contact2 = Contact::create()->setValues([ + 'preferred_communication_method' => $optionValues, + 'contact_type' => 'Individual', + 'first_name' => 'Test', + 'last_name' => 'PCM2', + ])->execute()->first(); + + $contactIds = array_column([$contact, $contact2], 'id'); + + $fetchedContact = Contact::get() + ->addWhere('id', 'IN', $contactIds) + ->addSelect('preferred_communication_method.label') + ->execute() + ->first(); + + $preferredMethod = $fetchedContact['preferred_communication_method']; + $returnedLabels = array_column($preferredMethod, 'label'); + + $this->assertEquals($labels, $returnedLabels); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/EntityTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/EntityTest.php new file mode 100644 index 00000000000..8f7ecd00586 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/EntityTest.php @@ -0,0 +1,35 @@ +setCheckPermissions(FALSE) + ->execute() + ->indexBy('name'); + $this->assertArrayHasKey('Entity', $result, + "Entity::get missing itself"); + $this->assertArrayHasKey('Participant', $result, + "Entity::get missing Participant"); + } + + public function testEntity() { + $result = Entity::getActions() + ->setCheckPermissions(FALSE) + ->execute() + ->indexBy('name'); + $this->assertNotContains( + 'create', + array_keys((array) $result), + "Entity entity has more than basic actions"); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ParticipantTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ParticipantTest.php new file mode 100644 index 00000000000..e96b9cef79d --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Entity/ParticipantTest.php @@ -0,0 +1,200 @@ +cleanup(['tablesToTruncate' => $truncateTables]); + $this->loadDataSet('ParticipantRoleOptionGroup'); + } + + public function testGetActions() { + $result = Participant::getActions() + ->setCheckPermissions(FALSE) + ->execute() + ->indexBy('name'); + + $getParams = $result['get']['params']; + $whereDescription = 'Array of conditions keyed by field.'; + + $this->assertEquals(TRUE, $getParams['checkPermissions']['default']); + $this->assertEquals($whereDescription, $getParams['where']['description']); + } + + public function testGet() { + + if ($this->getRowCount('civicrm_participant') > 0) { + $this->markTestSkipped('Participant table must be empty'); + } + + // With no records: + $result = Participant::get()->setCheckPermissions(FALSE)->execute(); + $this->assertEquals(0, $result->count(), "count of empty get is not 0"); + + // Check that the $result knows what the inputs were + $this->assertEquals('Participant', $result->entity); + $this->assertEquals('get', $result->action); + $this->assertEquals(4, $result->version); + + // Create some test related records before proceeding + $participantCount = 20; + $contactCount = 7; + $eventCount = 5; + + // All events will either have this number or one less because of the + // rotating participation creation method. + $expectedFirstEventCount = ceil($participantCount / $eventCount); + + $dummy = [ + 'contacts' => $this->createEntity([ + 'type' => 'Individual', + 'count' => $contactCount, + 'seq' => 1]), + 'events' => $this->createEntity([ + 'type' => 'Event', + 'count' => $eventCount, + 'seq' => 1]), + 'sources' => ['Paddington', 'Springfield', 'Central'], + ]; + + // - create dummy participants record + for ($i = 0; $i < $participantCount; $i++) { + $dummy['participants'][$i] = $this->sample([ + 'type' => 'Participant', + 'overrides' => [ + 'event_id' => $dummy['events'][$i % $eventCount]['id'], + 'contact_id' => $dummy['contacts'][$i % $contactCount]['id'], + 'source' => $dummy['sources'][$i % 3], // 3 = number of sources + ]])['sample_params']; + + Participant::create() + ->setValues($dummy['participants'][$i]) + ->setCheckPermissions(FALSE) + ->execute(); + } + $sqlCount = $this->getRowCount('civicrm_participant'); + $this->assertEquals($participantCount, $sqlCount, "Unexpected count"); + + $firstEventId = $dummy['events'][0]['id']; + $secondEventId = $dummy['events'][1]['id']; + $firstContactId = $dummy['contacts'][0]['id']; + + $firstOnlyResult = Participant::get() + ->setCheckPermissions(FALSE) + ->addClause('AND', ['event_id', '=', $firstEventId]) + ->execute(); + + $this->assertEquals($expectedFirstEventCount, count($firstOnlyResult), + "count of first event is not $expectedFirstEventCount"); + + // get first two events using different methods + $firstTwo = Participant::get() + ->setCheckPermissions(FALSE) + ->addWhere('event_id', 'IN', [$firstEventId, $secondEventId]) + ->execute(); + + $firstResult = $result->first(); + + // verify counts + // count should either twice the first event count or one less + $this->assertLessThanOrEqual( + $expectedFirstEventCount * 2, + count($firstTwo), + "count is too high" + ); + + $this->assertGreaterThanOrEqual( + $expectedFirstEventCount * 2 - 1, + count($firstTwo), + "count is too low" + ); + + $firstParticipantResult = Participant::get() + ->setCheckPermissions(FALSE) + ->addWhere('event_id', '=', $firstEventId) + ->addWhere('contact_id', '=', $firstContactId) + ->execute(); + + $this->assertEquals(1, count($firstParticipantResult), "more than one registration"); + + $firstParticipantId = $firstParticipantResult->first()['id']; + + // get a result which excludes $first_participant + $otherParticipantResult = Participant::get() + ->setCheckPermissions(FALSE) + ->setSelect(['id']) + ->addClause('NOT', [ + ['event_id', '=', $firstEventId], + ['contact_id', '=', $firstContactId], + ] + ) + ->execute() + ->indexBy('id'); + + // check alternate syntax for NOT + $otherParticipantResult2 = Participant::get() + ->setCheckPermissions(FALSE) + ->setSelect(['id']) + ->addClause('NOT', 'AND', [ + ['event_id', '=', $firstEventId], + ['contact_id', '=', $firstContactId], + ] + ) + ->execute() + ->indexBy('id'); + + $this->assertEquals($otherParticipantResult, $otherParticipantResult2); + + $this->assertEquals($participantCount - 1, + count($otherParticipantResult), + "failed to exclude a single record on complex criteria"); + // check the record we have excluded is the right one: + + $this->assertFalse( + $otherParticipantResult->offsetExists($firstParticipantId), + 'excluded wrong record'); + + // retrieve a participant record and update some records + $patchRecord = [ + 'source' => "not " . $firstResult['source'], + ]; + + Participant::update() + ->addWhere('event_id', '=', $firstEventId) + ->setCheckPermissions(FALSE) + ->setLimit(20) + ->setValues($patchRecord) + ->setCheckPermissions(FALSE) + ->execute(); + + // - delete some records + $secondEventId = $dummy['events'][1]['id']; + $deleteResult = Participant::delete() + ->addWhere('event_id', '=', $secondEventId) + ->setCheckPermissions(FALSE) + ->execute(); + $expectedDeletes = [2, 7, 12, 17]; + $this->assertEquals($expectedDeletes, (array) $deleteResult, + "didn't delete every second record as expected"); + + $sqlCount = $this->getRowCount('civicrm_participant'); + $this->assertEquals( + $participantCount - count($expectedDeletes), + $sqlCount, + "records not gone from database after delete"); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/Api4SelectQueryComplexJoinTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/Api4SelectQueryComplexJoinTest.php new file mode 100644 index 00000000000..049c40713c8 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/Api4SelectQueryComplexJoinTest.php @@ -0,0 +1,86 @@ +cleanup(['tablesToTruncate' => $relatedTables]); + $this->loadDataSet('SingleContact'); + return parent::setUpHeadless(); + } + + public function testWithComplexRelatedEntitySelect() { + $query = new Api4SelectQuery('Contact', FALSE); + $query->select[] = 'id'; + $query->select[] = 'display_name'; + $query->select[] = 'phones.phone'; + $query->select[] = 'emails.email'; + $query->select[] = 'emails.location_type.name'; + $query->select[] = 'created_activities.contact_id'; + $query->select[] = 'created_activities.activity.subject'; + $query->select[] = 'created_activities.activity.activity_type.name'; + $query->where[] = ['first_name', '=', 'Single']; + $results = $query->run(); + + $testActivities = [ + $this->getReference('test_activity_1'), + $this->getReference('test_activity_2'), + ]; + $activitySubjects = array_column($testActivities, 'subject'); + + $this->assertCount(1, $results); + $firstResult = array_shift($results); + $this->assertArrayHasKey('created_activities', $firstResult); + $firstCreatedActivity = array_shift($firstResult['created_activities']); + $this->assertArrayHasKey('activity', $firstCreatedActivity); + $firstActivity = $firstCreatedActivity['activity']; + $this->assertContains($firstActivity['subject'], $activitySubjects); + $this->assertArrayHasKey('activity_type', $firstActivity); + $activityType = $firstActivity['activity_type']; + $this->assertArrayHasKey('name', $activityType); + } + + public function testWithSelectOfOrphanDeepValues() { + $query = new Api4SelectQuery('Contact', FALSE); + $query->select[] = 'id'; + $query->select[] = 'first_name'; + $query->select[] = 'emails.location_type.name'; // emails not selected + $results = $query->run(); + $firstResult = array_shift($results); + + $this->assertEmpty($firstResult['emails']); + } + + public function testOrderDoesNotMatter() { + $query = new Api4SelectQuery('Contact', FALSE); + $query->select[] = 'id'; + $query->select[] = 'first_name'; + $query->select[] = 'emails.location_type.name'; // before emails selection + $query->select[] = 'emails.email'; + $results = $query->run(); + $firstResult = array_shift($results); + + $this->assertNotEmpty($firstResult['emails'][0]['location_type']['name']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/Api4SelectQueryTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/Api4SelectQueryTest.php new file mode 100644 index 00000000000..b67bb0b8135 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/Api4SelectQueryTest.php @@ -0,0 +1,109 @@ +cleanup(['tablesToTruncate' => $relatedTables]); + $this->loadDataSet('DefaultDataSet'); + $displayNameFormat = '{contact.first_name}{ }{contact.last_name}'; + \Civi::settings()->set('display_name_format', $displayNameFormat); + + return parent::setUpHeadless(); + } + + public function testBasicSelect() { + $query = new Api4SelectQuery('Contact', FALSE); + $results = $query->run(); + + $this->assertCount(2, $results); + $this->assertEquals('Test', array_shift($results)['first_name']); + } + + public function testWithSingleWhereJoin() { + $phoneNum = $this->getReference('test_phone_1')['phone']; + + $query = new Api4SelectQuery('Contact', FALSE); + $query->where[] = ['phones.phone', '=', $phoneNum]; + $results = $query->run(); + + $this->assertCount(1, $results); + } + + public function testOneToManyJoin() { + $phoneNum = $this->getReference('test_phone_1')['phone']; + + $query = new Api4SelectQuery('Contact', FALSE); + $query->select[] = 'id'; + $query->select[] = 'first_name'; + $query->select[] = 'phones.phone'; + $query->where[] = ['phones.phone', '=', $phoneNum]; + $results = $query->run(); + + $this->assertCount(1, $results); + $firstResult = array_shift($results); + $this->assertArrayHasKey('phones', $firstResult); + $firstPhone = array_shift($firstResult['phones']); + $this->assertEquals($phoneNum, $firstPhone['phone']); + } + + public function testManyToOneJoin() { + $phoneNum = $this->getReference('test_phone_1')['phone']; + $contact = $this->getReference('test_contact_1'); + + $query = new Api4SelectQuery('Phone', FALSE); + $query->select[] = 'id'; + $query->select[] = 'phone'; + $query->select[] = 'contact.display_name'; + $query->select[] = 'contact.first_name'; + $query->where[] = ['phone', '=', $phoneNum]; + $results = $query->run(); + + $this->assertCount(1, $results); + $firstResult = array_shift($results); + $this->assertArrayHasKey('contact', $firstResult); + $resultContact = $firstResult['contact']; + $this->assertEquals($contact['display_name'], $resultContact['display_name']); + } + + public function testOneToManyMultipleJoin() { + $query = new Api4SelectQuery('Contact', FALSE); + $query->select[] = 'id'; + $query->select[] = 'first_name'; + $query->select[] = 'phones.phone'; + $results = $query->run(); + + $this->assertCount(2, $results); + + foreach ($results as $result) { + if ($result['id'] == 2) { + // Contact has no phones + $this->assertEmpty($result['phones']); + } + elseif ($result['id'] == 1) { + $this->assertCount(2, $result['phones']); + } + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/OneToOneJoinTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/OneToOneJoinTest.php new file mode 100644 index 00000000000..edb1ca427fb --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/OneToOneJoinTest.php @@ -0,0 +1,65 @@ +addValue('name', 'languages') + ->execute() + ->first()['id']; + + OptionValue::create() + ->addValue('option_group_id', $languageGroupId) + ->addValue('name', 'hy_AM') + ->addValue('value', 'hy') + ->addValue('label', 'Armenian') + ->execute(); + + OptionValue::create() + ->addValue('option_group_id', $languageGroupId) + ->addValue('name', 'eu_ES') + ->addValue('value', 'eu') + ->addValue('label', 'Basque') + ->execute(); + + $armenianContact = Contact::create() + ->addValue('first_name', 'Contact') + ->addValue('last_name', 'One') + ->addValue('contact_type', 'Individual') + ->addValue('preferred_language', 'hy_AM') + ->execute() + ->first(); + + $basqueContact = Contact::create() + ->addValue('first_name', 'Contact') + ->addValue('last_name', 'Two') + ->addValue('contact_type', 'Individual') + ->addValue('preferred_language', 'eu_ES') + ->execute() + ->first(); + + $contacts = Contact::get() + ->addWhere('id', 'IN', [$armenianContact['id'], $basqueContact['id']]) + ->addSelect('preferred_language.label') + ->addSelect('last_name') + ->execute() + ->indexBy('last_name') + ->getArrayCopy(); + + $this->assertEquals($contacts['One']['preferred_language']['label'], 'Armenian'); + $this->assertEquals($contacts['Two']['preferred_language']['label'], 'Basque'); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/OptionValueJoinTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/OptionValueJoinTest.php new file mode 100644 index 00000000000..eaeeca24637 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/OptionValueJoinTest.php @@ -0,0 +1,48 @@ +cleanup(['tablesToTruncate' => $relatedTables]); + $this->loadDataSet('SingleContact'); + + return parent::setUpHeadless(); + } + + public function testCommunicationMethodJoin() { + $query = new Api4SelectQuery('Contact', FALSE); + $query->select[] = 'first_name'; + $query->select[] = 'preferred_communication_method.label'; + $results = $query->run(); + $first = array_shift($results); + $firstPreferredMethod = array_shift($first['preferred_communication_method']); + + $this->assertEquals( + 'Phone', + $firstPreferredMethod['label'] + ); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/SelectQueryMultiJoinTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/SelectQueryMultiJoinTest.php new file mode 100644 index 00000000000..860bd785201 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Query/SelectQueryMultiJoinTest.php @@ -0,0 +1,74 @@ +cleanup(['tablesToTruncate' => ['civicrm_contact', 'civicrm_email']]); + $this->loadDataSet('MultiContactMultiEmail'); + return parent::setUpHeadless(); + } + + public function testOneToManySelect() { + $results = Contact::get() + ->addSelect('emails.email') + ->execute() + ->indexBy('id') + ->getArrayCopy(); + + $firstContactId = $this->getReference('test_contact_1')['id']; + $secondContactId = $this->getReference('test_contact_2')['id']; + + $firstContact = $results[$firstContactId]; + $secondContact = $results[$secondContactId]; + $firstContactEmails = array_column($firstContact['emails'], 'email'); + $secondContactEmails = array_column($secondContact['emails'], 'email'); + + $expectedFirstEmails = [ + 'test_contact_one_home@fakedomain.com', + 'test_contact_one_work@fakedomain.com', + ]; + $expectedSecondEmails = [ + 'test_contact_two_home@fakedomain.com', + 'test_contact_two_work@fakedomain.com', + ]; + + $this->assertEquals($expectedFirstEmails, $firstContactEmails); + $this->assertEquals($expectedSecondEmails, $secondContactEmails); + } + + public function testManyToOneSelect() { + $results = Email::get() + ->addSelect('contact.display_name') + ->execute() + ->indexBy('id') + ->getArrayCopy(); + + $firstEmail = $this->getReference('test_email_1'); + $secondEmail = $this->getReference('test_email_2'); + $thirdEmail = $this->getReference('test_email_3'); + $fourthEmail = $this->getReference('test_email_4'); + $firstContactEmailIds = [$firstEmail['id'], $secondEmail['id']]; + $secondContactEmailIds = [$thirdEmail['id'], $fourthEmail['id']]; + + foreach ($results as $id => $email) { + $displayName = $email['contact']['display_name']; + if (in_array($id, $firstContactEmailIds)) { + $this->assertEquals('First Contact', $displayName); + } + elseif (in_array($id, $secondContactEmailIds)) { + $this->assertEquals('Second Contact', $displayName); + } + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/Schema/SchemaMapRealTableTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/Schema/SchemaMapRealTableTest.php new file mode 100644 index 00000000000..183d34dc8f6 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/Schema/SchemaMapRealTableTest.php @@ -0,0 +1,22 @@ +get('schema_map'); + $this->assertNotEmpty($map->getTables()); + } + + public function testSimplePathWillExist() { + $map = \Civi::container()->get('schema_map'); + $path = $map->getPath('civicrm_contact', 'emails'); + $this->assertCount(1, $path); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/Schema/SchemaMapperTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/Schema/SchemaMapperTest.php new file mode 100644 index 00000000000..04952f7bfdf --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/Schema/SchemaMapperTest.php @@ -0,0 +1,90 @@ +assertEmpty($map->getPath('foo', 'bar')); + } + + public function testWillHavePathWithSingleJump() { + $phoneTable = new Table('civicrm_phone'); + $locationTable = new Table('civicrm_location_type'); + $link = new Joinable('civicrm_location_type', 'id', 'location'); + $phoneTable->addTableLink('location_type_id', $link); + + $map = new SchemaMap(); + $map->addTables([$phoneTable, $locationTable]); + + $this->assertNotEmpty($map->getPath('civicrm_phone', 'location')); + } + + public function testWillHavePathWithDoubleJump() { + $activity = new Table('activity'); + $activityContact = new Table('activity_contact'); + $middleLink = new Joinable('activity_contact', 'activity_id'); + $contactLink = new Joinable('contact', 'id'); + $activity->addTableLink('id', $middleLink); + $activityContact->addTableLink('contact_id', $contactLink); + + $map = new SchemaMap(); + $map->addTables([$activity, $activityContact]); + + $this->assertNotEmpty($map->getPath('activity', 'contact')); + } + + public function testPathWithTripleJoin() { + $first = new Table('first'); + $second = new Table('second'); + $third = new Table('third'); + $first->addTableLink('id', new Joinable('second', 'id')); + $second->addTableLink('id', new Joinable('third', 'id')); + $third->addTableLink('id', new Joinable('fourth', 'id')); + + $map = new SchemaMap(); + $map->addTables([$first, $second, $third]); + + $this->assertNotEmpty($map->getPath('first', 'fourth')); + } + + public function testCircularReferenceWillNotBreakIt() { + $contactTable = new Table('contact'); + $carTable = new Table('car'); + $carLink = new Joinable('car', 'id'); + $ownerLink = new Joinable('contact', 'id'); + $contactTable->addTableLink('car_id', $carLink); + $carTable->addTableLink('owner_id', $ownerLink); + + $map = new SchemaMap(); + $map->addTables([$contactTable, $carTable]); + + $this->assertEmpty($map->getPath('contact', 'foo')); + } + + public function testCannotGoOverJoinLimit() { + $first = new Table('first'); + $second = new Table('second'); + $third = new Table('third'); + $fourth = new Table('fourth'); + $first->addTableLink('id', new Joinable('second', 'id')); + $second->addTableLink('id', new Joinable('third', 'id')); + $third->addTableLink('id', new Joinable('fourth', 'id')); + $fourth->addTableLink('id', new Joinable('fifth', 'id')); + + $map = new SchemaMap(); + $map->addTables([$first, $second, $third, $fourth]); + + $this->assertEmpty($map->getPath('first', 'fifth')); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/TestCreationParameterProvider.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/TestCreationParameterProvider.php new file mode 100644 index 00000000000..948e980eef7 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Service/TestCreationParameterProvider.php @@ -0,0 +1,138 @@ +gatherer = $gatherer; + } + + /** + * @param $entity + * + * @return array + */ + public function getRequired($entity) { + $createSpec = $this->gatherer->getSpec($entity, 'create', FALSE); + $requiredFields = $createSpec->getRequiredFields(); + + if ($entity === 'Contact') { + $requiredFields[] = $createSpec->getFieldByName('first_name'); + $requiredFields[] = $createSpec->getFieldByName('last_name'); + } + + $requiredParams = []; + foreach ($requiredFields as $requiredField) { + $value = $this->getRequiredValue($requiredField); + $requiredParams[$requiredField->getName()] = $value; + } + + unset($requiredParams['id']); + + return $requiredParams; + } + + /** + * Attempt to get a value using field option, defaults, FKEntity, or a random + * value based on the data type. + * + * @param FieldSpec $field + * + * @return mixed + * @throws \Exception + */ + private function getRequiredValue(FieldSpec $field) { + + if ($field->getOptions()) { + return $this->getOption($field); + } + elseif ($field->getDefaultValue()) { + return $field->getDefaultValue(); + } + elseif ($field->getFkEntity()) { + return $this->getFkID($field); + } + + $randomValue = $this->getRandomValue($field->getDataType()); + + if ($randomValue) { + return $randomValue; + } + + throw new \Exception('Could not provide default value'); + } + + /** + * @param FieldSpec $field + * + * @return mixed + */ + private function getOption(FieldSpec $field) { + $options = $field->getOptions(); + $useKeyNames = ['data_type', 'html_type']; + $shouldUseKey = in_array($field->getName(), $useKeyNames); + $isIdField = substr($field->getName(), -3) === '_id'; + + if ($isIdField || $shouldUseKey) { + return array_rand($options); // return key (ID) + } + else { + return $options[array_rand($options)]; + } + } + + /** + * @param FieldSpec $field + * + * @return mixed + * @throws \Exception + */ + private function getFkID(FieldSpec $field) { + $fkEntity = $field->getFkEntity(); + $params = ['checkPermissions' => FALSE]; + $entityList = civicrm_api4($fkEntity, 'get', $params); + if ($entityList->count() < 1) { + $msg = sprintf('At least one %s is required in test', $fkEntity); + throw new \Exception($msg); + } + + return $entityList->first()['id']; + } + + /** + * @param $dataType + * + * @return int|null|string + */ + private function getRandomValue($dataType) { + switch ($dataType) { + case 'Boolean': + return TRUE; + + case 'Integer': + return rand(1, 2000); + + case 'String': + return StringHelper::createRandom(10, implode('', range('a', 'z'))); + + case 'Money': + return sprintf('%d.%2d', rand(0, 2000), rand(1, 99)); + } + + return NULL; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/RequestSpecTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/RequestSpecTest.php new file mode 100644 index 00000000000..45aa41b2dd5 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/RequestSpecTest.php @@ -0,0 +1,42 @@ +setRequired(TRUE); + $nonRequiredField = new FieldSpec('age', 'Contact', 'Integer'); + $nonRequiredField->setRequired(FALSE); + $spec->addFieldSpec($requiredField); + $spec->addFieldSpec($nonRequiredField); + + $requiredFields = $spec->getRequiredFields(); + + $this->assertCount(1, $requiredFields); + $this->assertEquals('name', array_shift($requiredFields)->getName()); + } + + public function testGettingFieldNames() { + $spec = new RequestSpec('Contact', 'get'); + $nameField = new FieldSpec('name', 'Contact'); + $ageField = new FieldSpec('age', 'Contact', 'Integer'); + $spec->addFieldSpec($nameField); + $spec->addFieldSpec($ageField); + + $fieldNames = $spec->getFieldNames(); + + $this->assertCount(2, $fieldNames); + $this->assertEquals(['name', 'age'], $fieldNames); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/SpecFormatterTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/SpecFormatterTest.php new file mode 100644 index 00000000000..bdb1c4599a4 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/SpecFormatterTest.php @@ -0,0 +1,90 @@ +addFieldSpec($field); + $arraySpec = SpecFormatter::specToArray($spec->getFields()); + + $this->assertEquals('String', $arraySpec[$fieldName]['data_type']); + } + + /** + * @dataProvider arrayFieldSpecProvider + * + * @param array $fieldData + * @param string $expectedName + * @param string $expectedType + */ + public function testArrayToField($fieldData, $expectedName, $expectedType) { + $field = SpecFormatter::arrayToField($fieldData, 'TestEntity'); + + $this->assertEquals($expectedName, $field->getName()); + $this->assertEquals($expectedType, $field->getDataType()); + } + + public function testCustomFieldWillBeReturned() { + $customGroupId = 1432; + $customFieldId = 3333; + $name = 'MyFancyField'; + + $data = [ + 'custom_group_id' => $customGroupId, + 'custom_group' => ['name' => 'my_group'], + 'id' => $customFieldId, + 'name' => $name, + 'data_type' => 'String', + 'html_type' => 'MultiSelect', + ]; + + /** @var CustomFieldSpec $field */ + $field = SpecFormatter::arrayToField($data, 'TestEntity'); + + $this->assertInstanceOf(CustomFieldSpec::class, $field); + $this->assertEquals('my_group', $field->getCustomGroupName()); + $this->assertEquals($customFieldId, $field->getCustomFieldId()); + $this->assertEquals(\CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND, $field->getSerialize()); + } + + /** + * @return array + */ + public function arrayFieldSpecProvider() { + return [ + [ + [ + 'name' => 'Foo', + 'title' => 'Bar', + 'type' => \CRM_Utils_Type::T_STRING + ], + 'Foo', + 'String' + ], + [ + [ + 'name' => 'MyField', + 'title' => 'Bar', + 'type' => \CRM_Utils_Type::T_STRING, + 'data_type' => 'Boolean' // this should take precedence + ], + 'MyField', + 'Boolean' + ], + ]; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/SpecGathererTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/SpecGathererTest.php new file mode 100644 index 00000000000..e6fe19f109a --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Spec/SpecGathererTest.php @@ -0,0 +1,94 @@ +dropByPrefix('civicrm_value_favorite'); + $this->cleanup([ + 'tablesToTruncate' => [ + 'civicrm_custom_group', + 'civicrm_custom_field' + ], + ]); + return parent::setUpHeadless(); + } + + public function testBasicFieldsGathering() { + $gatherer = new SpecGatherer(); + $specs = $gatherer->getSpec('Contact', 'get', FALSE); + $contactDAO = _civicrm_api3_get_DAO('Contact'); + $contactFields = $contactDAO::fields(); + $specFieldNames = $specs->getFieldNames(); + $contactFieldNames = array_column($contactFields, 'name'); + + $this->assertEmpty(array_diff_key($contactFieldNames, $specFieldNames)); + } + + public function testWithSpecProvider() { + $gather = new SpecGatherer(); + + $provider = $this->prophesize(SpecProviderInterface::class); + $provider->applies('Contact', 'create')->willReturn(TRUE); + $provider->modifySpec(Argument::any())->will(function ($args) { + /** @var RequestSpec $spec */ + $spec = $args[0]; + $spec->addFieldSpec(new FieldSpec('foo', 'Contact')); + }); + $gather->addSpecProvider($provider->reveal()); + + $spec = $gather->getSpec('Contact', 'create', FALSE); + $fieldNames = $spec->getFieldNames(); + + $this->assertContains('foo', $fieldNames); + } + + public function testPseudoConstantOptionsWillBeAdded() { + $customGroupId = CustomGroup::create() + ->setCheckPermissions(FALSE) + ->addValue('name', 'FavoriteThings') + ->addValue('extends', 'Contact') + ->execute() + ->first()['id']; + + $options = ['r' => 'Red', 'g' => 'Green', 'p' => 'Pink']; + + CustomField::create() + ->setCheckPermissions(FALSE) + ->addValue('label', 'FavColor') + ->addValue('custom_group_id', $customGroupId) + ->addValue('options', $options) + ->addValue('html_type', 'Select') + ->addValue('data_type', 'String') + ->execute(); + + $gatherer = new SpecGatherer(); + $spec = $gatherer->getSpec('Contact', 'get', TRUE); + + $regularField = $spec->getFieldByName('contact_type'); + $this->assertNotEmpty($regularField->getOptions()); + $this->assertContains('Individual', $regularField->getOptions()); + + $customField = $spec->getFieldByName('FavoriteThings.FavColor'); + $this->assertNotEmpty($customField->getOptions()); + $this->assertContains('Green', $customField->getOptions()); + $this->assertEquals('Pink', $customField->getOptions()['p']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/QueryCounterTrait.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/QueryCounterTrait.php new file mode 100644 index 00000000000..c7e10f1b93e --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/QueryCounterTrait.php @@ -0,0 +1,43 @@ +startCount = $this->getCurrentGlobalQueryCount(); + } + + /** + * @return int + * The number of queries since the counter was started + */ + protected function getQueryCount() { + return $this->getCurrentGlobalQueryCount() - $this->startCount; + } + + /** + * @return int + * @throws \Exception + */ + private function getCurrentGlobalQueryCount() { + global $_DB_DATAOBJECT; + + if (!$_DB_DATAOBJECT) { + throw new \Exception('Database object not set so cannot count queries'); + } + + return ArrayHelper::value('RESULTSEQ', $_DB_DATAOBJECT, 0); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/TableDropperTrait.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/TableDropperTrait.php new file mode 100644 index 00000000000..6e54347334a --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/TableDropperTrait.php @@ -0,0 +1,23 @@ +fetch(); + $dropTableQuery = $dropTableQuery->statement; + + if ($dropTableQuery) { + \CRM_Core_DAO::executeQuery($dropTableQuery); + } + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/TestDataLoaderTrait.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/TestDataLoaderTrait.php new file mode 100644 index 00000000000..1db22090466 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Traits/TestDataLoaderTrait.php @@ -0,0 +1,69 @@ + $entities) { + foreach ($entities as $entityValues) { + + $entityValues = $this->replaceReferences($entityValues); + + $params = ['values' => $entityValues, 'checkPermissions' => FALSE]; + $result = civicrm_api4($entityName, 'create', $params); + if (isset($entityValues['@ref'])) { + $this->references[$entityValues['@ref']] = $result->first(); + } + } + } + } + + /** + * @param $name + * + * @return null|mixed + */ + protected function getReference($name) { + return isset($this->references[$name]) ? $this->references[$name] : NULL; + } + + /** + * @param array $entityValues + * + * @return array + */ + private function replaceReferences($entityValues) { + foreach ($entityValues as $name => $value) { + if (is_array($value)) { + $entityValues[$name] = $this->replaceReferences($value); + } + elseif (substr($value, 0, 4) === '@ref') { + $referenceName = substr($value, 5); + list ($reference, $property) = explode('.', $referenceName); + $entityValues[$name] = $this->references[$reference][$property]; + } + } + return $entityValues; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/UnitTestCase.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/UnitTestCase.php new file mode 100644 index 00000000000..dcd0f3bb360 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/UnitTestCase.php @@ -0,0 +1,235 @@ +installMe(__DIR__)->apply(); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * + * This method is called after a test is executed. + */ + public function tearDown() { + parent::tearDown(); + } + + /** + * Quick clean by emptying tables created for the test. + * + * @param array $params + */ + public function cleanup($params) { + $params += [ + 'tablesToTruncate' => [], + ]; + \CRM_Core_DAO::executeQuery("SET FOREIGN_KEY_CHECKS = 0;"); + foreach ($params['tablesToTruncate'] as $table) { + \Civi::log()->info('truncating: ' . $table); + $sql = "TRUNCATE TABLE $table"; + \CRM_Core_DAO::executeQuery($sql); + } + \CRM_Core_DAO::executeQuery("SET FOREIGN_KEY_CHECKS = 1;"); + } + + /** + * Quick record counter + * + * @param string $table_name + * @returns int record count + */ + public function getRowCount($table_name) { + $sql = "SELECT count(*) FROM $table_name"; + return (int) \CRM_Core_DAO::singleValueQuery($sql); + } + + /** + * Create sample entities (using V3 for now). + * + * @param array $params (type, seq, overrides, count) + * @return array (either single, or array of array if count >1) + */ + public static function createEntity($params) { + $params += [ + 'count' => 1, + 'seq' => 0, + ]; + $entities = []; + $entity = NULL; + for ($i = 0; $i < $params['count']; $i++) { + $params['seq']++; + $data = self::sample($params); + $api_params = ['sequential' => 1] + $data['sample_params']; + $result = civicrm_api3($data['entity'], 'create', $api_params); + if ($result['is_error']) { + throw new \Exception("creating $data[entity] failed"); + } + $entity = $result['values'][0]; + if (!($entity['id'] > 0)) { + throw new \Exception("created entity is malformed"); + } + $entities[] = $entity; + } + return $params['count'] == 1 ? $entity : $entities; + } + + /** + * Helper function for creating sample entities. + * + * Depending on the supplied sequence integer, plucks values from the dummy data. + * Constructs a foreign entity when an ID is required but isn't supplied in the overrides. + * + * Inspired by CiviUnitTestCase:: + * @todo - extract this function to own class and share with CiviUnitTestCase? + * @param array $params + * - type: string roughly matching entity type + * - seq: (optional) int sequence number for the values of this type + * - overrides: (optional) array of fill in parameters + * + * @return array + * - entity: string API entity type (usually the type supplied except for contact subtypes) + * - sample_params: array API sample_params properties of sample entity + */ + public static function sample($params) { + $params += [ + 'seq' => 0, + 'overrides' => [], + ]; + $type = $params['type']; + // sample data - if field is array then chosed based on `seq` + $sample_params = []; + if (in_array($type, ['Individual', 'Organization', 'Household'])) { + $sample_params['contact_type'] = $type; + $entity = 'Contact'; + } + else { + $entity = $type; + } + // use the seq to pluck a set of params out + foreach (self::sampleData($type) as $key => $value) { + if (is_array($value)) { + $sample_params[$key] = $value[$params['seq'] % count($value)]; + } + else { + $sample_params[$key] = $value; + } + } + if ($type == 'Individual') { + $sample_params['email'] = strtolower( + $sample_params['first_name'] . '_' . $sample_params['last_name'] . '@civicrm.org' + ); + $sample_params['prefix_id'] = 3; + $sample_params['suffix_id'] = 3; + } + if (!count($sample_params)) { + throw new \Exception("unknown sample type: $type"); + } + $sample_params = $params['overrides'] + $sample_params; + // make foreign enitiies if they haven't been supplied + foreach ($sample_params as $key => $value) { + if (substr($value, 0, 6) === 'dummy.') { + $foreign_entity = self::createEntity([ + 'type' => substr($value, 6), + 'seq' => $params['seq']]); + $sample_params[$key] = $foreign_entity['id']; + } + } + return compact("entity", "sample_params"); + } + + /** + * Provider of sample data. + * + * @return array + * Array values represent a set of allowable items. + * Strings in the form "dummy.Entity" require creating a foreign entity first. + */ + public static function sampleData($type) { + $data = [ + 'Individual' => [ + // The number of values in each list need to be coprime numbers to not have duplicates + 'first_name' => ['Anthony', 'Joe', 'Terrence', 'Lucie', 'Albert', 'Bill', 'Kim'], + 'middle_name' => ['J.', 'M.', 'P', 'L.', 'K.', 'A.', 'B.', 'C.', 'D', 'E.', 'Z.'], + 'last_name' => ['Anderson', 'Miller', 'Smith', 'Collins', 'Peterson'], + 'contact_type' => 'Individual', + ], + 'Organization' => [ + 'organization_name' => [ + 'Unit Test Organization', + 'Acme', + 'Roberts and Sons', + 'Cryo Space Labs', + 'Sharper Pens', + ], + ], + 'Household' => [ + 'household_name' => ['Unit Test household'], + ], + 'Event' => [ + 'title' => 'Annual CiviCRM meet', + 'summary' => 'If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now', + 'description' => 'This event is intended to give brief idea about progess of CiviCRM and giving solutions to common user issues', + 'event_type_id' => 1, + 'is_public' => 1, + 'start_date' => 20081021, + 'end_date' => 20081023, + 'is_online_registration' => 1, + 'registration_start_date' => 20080601, + 'registration_end_date' => 20081015, + 'max_participants' => 100, + 'event_full_text' => 'Sorry! We are already full', + 'is_monetary' => 0, + 'is_active' => 1, + 'is_show_location' => 0, + ], + 'Participant' => [ + 'event_id' => 'dummy.Event', + 'contact_id' => 'dummy.Individual', + 'status_id' => 2, + 'role_id' => 1, + 'register_date' => 20070219, + 'source' => 'Wimbeldon', + 'event_level' => 'Payment', + ], + 'Contribution' => [ + 'contact_id' => 'dummy.Individual', + 'financial_type_id' => 1, // donation, 2 = member, 3 = campaign contribution, 4=event + 'total_amount' => 7.3, + ], + 'Activity' => [ + //'activity_type_id' => 1, + 'subject' => 'unit testing', + 'source_contact_id' => 'dummy.Individual', + ], + ]; + if ($type == 'Contact') { + $type = 'Individual'; + } + return $data[$type]; + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/ArrayInsertionServiceTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/ArrayInsertionServiceTest.php new file mode 100644 index 00000000000..462f07cc32f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/ArrayInsertionServiceTest.php @@ -0,0 +1,67 @@ + FALSE, 'bar' => FALSE]; + $inserter = new ArrayInsertionUtil(); + $inserter::insert($arr, $path, ['LALA']); + + $expected = [ + 'foo' => [ + 'bar' => 'LALA' + ], + ]; + + $this->assertEquals($expected, $arr); + } + + public function testInsertionOfContactEmailLocation() { + $contacts = [ + [ + 'id' => 1, + 'first_name' => 'Jim' + ], + [ + 'id' => 2, + 'first_name' => 'Karen' + ], + ]; + $emails = [ + [ + 'email' => 'jim@jim.com', + 'id' => 2, + '_parent_id' => 1 + ], + ]; + $locationTypes = [ + [ + 'name' => 'Home', + 'id' => 3, + '_parent_id' => 2 + ], + ]; + + $emailPath = ['emails' => TRUE]; + $locationPath = ['emails' => TRUE, 'location' => FALSE]; + $inserter = new ArrayInsertionUtil(); + + foreach ($contacts as &$contact) { + $inserter::insert($contact, $emailPath, $emails); + $inserter::insert($contact, $locationPath, $locationTypes); + } + + $locationType = $contacts[0]['emails'][0]['location']['name']; + $this->assertEquals('Home', $locationType); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/ReflectionUtilsTest.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/ReflectionUtilsTest.php new file mode 100644 index 00000000000..dec76161ccc --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/ReflectionUtilsTest.php @@ -0,0 +1,45 @@ +assertEquals(TRUE, $doc['internal']); + $this->assertEquals('Grandchild class', $doc['description']); + + $expectedComment = 'This is an extended description. + +There is a line break in this description. + +This is the base class.'; + + $this->assertEquals($expectedComment, $doc['comment']); + } + + /** + * Test that property annotations are returned across @inheritDoc + */ + public function testGetDocBlockForProperty() { + $grandChild = new TestV4ReflectionGrandchild(); + $reflection = new \ReflectionClass($grandChild); + $doc = ReflectionUtils::getCodeDocs($reflection->getProperty('foo'), 'Property'); + + $this->assertEquals('This is the foo property.', $doc['description']); + $this->assertEquals("In the child class, foo has been barred.\n\nIn general, you can do nothing with it.", $doc['comment']); + } + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/TestV4ReflectionBase.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/TestV4ReflectionBase.php new file mode 100644 index 00000000000..5ebf5ff6fc6 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/TestV4ReflectionBase.php @@ -0,0 +1,22 @@ + 1]; + +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/TestV4ReflectionGrandchild.php b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/TestV4ReflectionGrandchild.php new file mode 100644 index 00000000000..1e57f080c47 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/phpunit/Utils/TestV4ReflectionGrandchild.php @@ -0,0 +1,19 @@ +addPsr4('Civi\\Test\\Api4\\', __DIR__); + +/** + * Call the "cv" command. + * + * @param string $cmd + * The rest of the command to send. + * @param string $decode + * Ex: 'json' or 'phpcode'. + * @return string + * Response output (if the command executed normally). + * @throws \RuntimeException + * If the command terminates abnormally. + */ +function cv($cmd, $decode = 'json') { + $cmd = 'cv ' . $cmd; + $descriptorSpec = [0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => STDERR]; + $oldOutput = getenv('CV_OUTPUT'); + putenv("CV_OUTPUT=json"); + $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); + putenv("CV_OUTPUT=$oldOutput"); + fclose($pipes[0]); + $result = stream_get_contents($pipes[1]); + fclose($pipes[1]); + if (proc_close($process) !== 0) { + throw new RuntimeException("Command failed ($cmd):\n$result"); + } + switch ($decode) { + case 'raw': + return $result; + + case 'phpcode': + // If the last output is /*PHPCODE*/, then we managed to complete execution. + if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") { + throw new \RuntimeException("Command failed ($cmd):\n$result"); + } + return $result; + + case 'json': + return json_decode($result, 1); + + default: + throw new RuntimeException("Bad decoder format ($decode)"); + } +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/services.xml b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/services.xml new file mode 100644 index 00000000000..220ba910ab6 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/tests/services.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/api4/xml/Menu/api4.xml b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/xml/Menu/api4.xml new file mode 100644 index 00000000000..57e8b25cece --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/api4/xml/Menu/api4.xml @@ -0,0 +1,9 @@ + + + + civicrm/ajax/api4 + CRM_Api4_Page_AJAX + Api v4 + access CiviCRM + + diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/Core/Payment/iATSService.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/Core/Payment/iATSService.php index c9c897467ad..2dca9ccab51 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/Core/Payment/iATSService.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/Core/Payment/iATSService.php @@ -413,20 +413,21 @@ public function updateSubscriptionBillingInfo(&$message = '', $params = array()) $fakeForm = new IATSCustomerUpdateBillingInfo(); $fakeForm->updatedBillingInfo = $params; - $fakeForm->postProcess(); - - $status = CRM_Core_Session::singleton()->getStatus(TRUE); - $matches = array(); - preg_match('/\[AUTHORIZATIONRESULT\]\s*?=>\s(\d*)(.*)/', $status[0]['text'], $matches); - - if ($matches[2] == 'OK') { + try { + $fakeForm->postProcess(); + } + catch (Exception $error) { // what could go wrong? + $message = $error->getMessage(); + CRM_Core_Session::setStatus($message, ts('Warning'), 'alert'); + $e = CRM_Core_Error::singleton(); + return $e; + } + if ('OK' == $fakeForm->getAuthorizationResult()) { return TRUE; } - $message = $matches[2]; - + $message = $fakeForm->getResultMessage(); + CRM_Core_Session::setStatus($message, ts('Warning'), 'alert'); $e = CRM_Core_Error::singleton(); - $e->push($matches[1] ?: 0, 0, array(), $matches[2]); - return $e; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/IATSCustomerLink.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/IATSCustomerLink.php index 5ebc8d108a7..39e22b9249a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/IATSCustomerLink.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/IATSCustomerLink.php @@ -13,6 +13,8 @@ */ class CRM_iATS_Form_IATSCustomerLink extends CRM_Core_Form { + private $iats_result = array(); + /** * Get the field names and labels expected by iATS CustomerLink, * and the corresponding fields in CiviCRM. @@ -108,8 +110,26 @@ protected function updateCreditCardCustomer($params) { // Make the soap request. $response = $iats->request($credentials, $params); // note: don't log this to the iats_response table. - $result = $iats->result($response, TRUE); - return $result; + $this->iats_result = $iats->result($response, TRUE); + return $this->iats_result; + } + + /** + * Get an appropriate message for the user after an update is attempted. + */ + protected function getResultMessage() { + $message = array(); + foreach($this->iats_result as $key => $value) { + $message[] = strtolower($key).": $value"; + } + return '
    '.implode('
    ',$message).'
    '; + } + + /** + * Test whether the update was successful + */ + public function getAuthorizationResult() { + return $this->iats_result['AUTHORIZATIONRESULT']; } /** @@ -175,12 +195,9 @@ public function buildQuickForm() { public function postProcess() { $values = $this->exportValues(); // Send update to iATS - // print_r($values); die(); - $result = $this->updateCreditCardCustomer($values); - $message = '
    ' . print_r($result, TRUE) . '
    '; - // , $type, $options);. - CRM_Core_Session::setStatus($message, 'Customer Updated'); - if ($result['AUTHORIZATIONRESULT'] == 'OK') { + $this->updateCreditCardCustomer($values); + CRM_Core_Session::setStatus($this->getResultMessage(), 'Card Update Result'); + if ('OK' == $this->getAuthorizationResult()) { // Update my copy of the expiry date. list($month, $year) = explode('/', $values['creditCardExpiry']); $exp = sprintf('%02d%02d', $year, $month); diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/IATSCustomerUpdateBillingInfo.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/IATSCustomerUpdateBillingInfo.php index af9d8b4ed3f..03dd92a3096 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/IATSCustomerUpdateBillingInfo.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/IATSCustomerUpdateBillingInfo.php @@ -11,6 +11,12 @@ public function __construct() { public function exportValues($elementList = NULL, $filterInternal = FALSE) { $ubi = $this->updatedBillingInfo; + // updatedBillingInfo array changed sometime after 4.7.27 + $crid = !empty($ubi['crid']) ? $ubi['crid'] : $ubi['recur_id']; + if (empty($crid)) { + $alert = ts('This system is unable to perform self-service updates to credit cards. Please contact the administrator of this site.'); + throw new Exception($alert); + } $mop = array( 'Visa' => 'VISA', 'MasterCard' => 'MC', @@ -21,7 +27,7 @@ public function exportValues($elementList = NULL, $filterInternal = FALSE) { $dao = CRM_Core_DAO::executeQuery("SELECT cr.payment_processor_id, cc.customer_code, cc.cid FROM civicrm_contribution_recur cr LEFT JOIN civicrm_iats_customer_codes cc ON cr.id = cc.recur_id - WHERE cr.id=%1", array(1 => array($ubi['crid'], 'Int'))); + WHERE cr.id=%1", array(1 => array($crid, 'Int'))); $dao->fetch(); $values = array( @@ -43,4 +49,4 @@ public function exportValues($elementList = NULL, $filterInternal = FALSE) { } -} \ No newline at end of file +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/ContributeDetail.mgd.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/ContributeDetail.mgd.php new file mode 100644 index 00000000000..16743f18db2 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/ContributeDetail.mgd.php @@ -0,0 +1,26 @@ + + array( + 'name' => 'CRM_iATS_Form_Report_ContributeDetail', + 'entity' => 'ReportTemplate', + 'params' => + array( + 'version' => 3, + 'label' => 'iATS Payments - Contribution Reconciliation', + 'description' => 'Donor Report (Detail) Report with extra iATS Reconciliation fields.', + 'class_name' => 'CRM_iATS_Form_Report_ContributeDetail', + 'report_url' => 'com.iatspayments.com/contributedetail', + 'component' => 'CiviContribute', + ), + ), +); diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/ContributeDetail.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/ContributeDetail.php new file mode 100644 index 00000000000..0c35702825f --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/ContributeDetail.php @@ -0,0 +1,986 @@ + 'Visa', + 'ACHEFT' => 'ACH/EFT', + 'UNKNOW' => 'Uknown', + 'MC' => 'MasterCard', + 'AMX' => 'AMEX', + 'DSC' => 'Discover', + ); + + /** + * This report has been optimised for group filtering. + * + * CRM-19170 + * + * @var bool + */ + protected $groupFilterNotOptimised = FALSE; + + /** + * Class constructor. + */ + public function __construct() { + $this->_autoIncludeIndexedFieldsAsOrderBys = 1; + // Check if CiviCampaign is a) enabled and b) has active campaigns + $config = CRM_Core_Config::singleton(); + $campaignEnabled = in_array("CiviCampaign", $config->enableComponents); + if ($campaignEnabled) { + $getCampaigns = CRM_Campaign_BAO_Campaign::getPermissionedCampaigns(NULL, NULL, TRUE, FALSE, TRUE); + $this->activeCampaigns = $getCampaigns['campaigns']; + asort($this->activeCampaigns); + } + + $this->_columns = array_merge($this->getColumns('Contact', array( + 'order_bys_defaults' => array('sort_name' => 'ASC '), + 'fields_defaults' => array('sort_name'), + 'fields_excluded' => array('id'), + 'fields_required' => array('id'), + 'filters_defaults' => array('is_deleted' => 0), + 'no_field_disambiguation' => TRUE, + )), array( + 'civicrm_email' => array( + 'dao' => 'CRM_Core_DAO_Email', + 'fields' => array( + 'email' => array( + 'title' => ts('Donor Email'), + 'default' => TRUE, + ), + ), + 'grouping' => 'contact-fields', + ), + 'civicrm_line_item' => array( + 'dao' => 'CRM_Price_DAO_LineItem', + ), + 'civicrm_phone' => array( + 'dao' => 'CRM_Core_DAO_Phone', + 'fields' => array( + 'phone' => array( + 'title' => ts('Donor Phone'), + 'default' => TRUE, + 'no_repeat' => TRUE, + ), + ), + 'grouping' => 'contact-fields', + ), + 'civicrm_contribution' => array( + 'dao' => 'CRM_Contribute_DAO_Contribution', + 'fields' => array( + 'contribution_id' => array( + 'name' => 'id', + 'no_display' => TRUE, + 'required' => TRUE, + ), + 'list_contri_id' => array( + 'name' => 'id', + 'title' => ts('Contribution ID'), + ), + 'financial_type_id' => array( + 'title' => ts('Financial Type'), + 'default' => TRUE, + ), + 'contribution_status_id' => array( + 'title' => ts('Contribution Status'), + ), + 'contribution_page_id' => array( + 'title' => ts('Contribution Page'), + ), + 'source' => array( + 'title' => ts('Source'), + ), + 'payment_instrument_id' => array( + 'title' => ts('Payment Type'), + ), + 'check_number' => array( + 'title' => ts('Check Number'), + ), + 'currency' => array( + 'required' => TRUE, + 'no_display' => TRUE, + ), + 'trxn_id' => NULL, + 'receive_date' => array('default' => TRUE), + 'receipt_date' => NULL, + 'total_amount' => array( + 'title' => ts('Amount'), + 'required' => TRUE, + 'statistics' => array('sum' => ts('Amount')), + ), + 'non_deductible_amount' => array( + 'title' => ts('Non-deductible Amount'), + ), + 'fee_amount' => NULL, + 'net_amount' => NULL, + 'contribution_or_soft' => array( + 'title' => ts('Contribution OR Soft Credit?'), + 'dbAlias' => "'Contribution'", + ), + 'soft_credits' => array( + 'title' => ts('Soft Credits'), + 'dbAlias' => "NULL", + ), + 'soft_credit_for' => array( + 'title' => ts('Soft Credit For'), + 'dbAlias' => "NULL", + ), + ), + 'filters' => array( + 'contribution_or_soft' => array( + 'title' => ts('Contribution OR Soft Credit?'), + 'clause' => "(1)", + 'operatorType' => CRM_Report_Form::OP_SELECT, + 'type' => CRM_Utils_Type::T_STRING, + 'options' => array( + 'contributions_only' => ts('Contributions Only'), + 'soft_credits_only' => ts('Soft Credits Only'), + 'both' => ts('Both'), + ), + ), + 'receive_date' => array('operatorType' => CRM_Report_Form::OP_DATE), + 'contribution_source' => array( + 'title' => ts('Source'), + 'name' => 'source', + 'type' => CRM_Utils_Type::T_STRING, + ), + 'currency' => array( + 'title' => ts('Currency'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Core_OptionGroup::values('currencies_enabled'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ), + 'non_deductible_amount' => array( + 'title' => ts('Non-deductible Amount'), + ), + 'financial_type_id' => array( + 'title' => ts('Financial Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(), + 'type' => CRM_Utils_Type::T_INT, + ), + 'contribution_page_id' => array( + 'title' => ts('Contribution Page'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::contributionPage(), + 'type' => CRM_Utils_Type::T_INT, + ), + 'payment_instrument_id' => array( + 'title' => ts('Payment Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::paymentInstrument(), + 'type' => CRM_Utils_Type::T_INT, + ), + 'contribution_status_id' => array( + 'title' => ts('Contribution Status'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::contributionStatus(), + 'default' => array(1), + 'type' => CRM_Utils_Type::T_INT, + ), + 'total_amount' => array('title' => ts('Contribution Amount')), + ), + 'order_bys' => array( + 'financial_type_id' => array('title' => ts('Financial Type')), + 'contribution_status_id' => array('title' => ts('Contribution Status')), + 'payment_instrument_id' => array('title' => ts('Payment Method')), + 'receive_date' => array('title' => ts('Date Received')), + ), + 'group_bys' => array( + 'contribution_id' => array( + 'name' => 'id', + 'required' => TRUE, + 'title' => ts('Contribution'), + ), + ), + 'grouping' => 'contri-fields', + ), + 'civicrm_contribution_soft' => array( + 'dao' => 'CRM_Contribute_DAO_ContributionSoft', + 'fields' => array( + 'soft_credit_type_id' => array('title' => ts('Soft Credit Type')), + ), + 'filters' => array( + 'soft_credit_type_id' => array( + 'title' => ts('Soft Credit Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Core_OptionGroup::values('soft_credit_type'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ), + ), + ), + 'civicrm_financial_trxn' => array( + 'dao' => 'CRM_Financial_DAO_FinancialTrxn', + 'fields' => array( + 'card_type_id' => array( + 'title' => ts('Credit Card Type'), + ), + ), + 'filters' => array( + 'card_type_id' => array( + 'title' => ts('Credit Card Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Financial_DAO_FinancialTrxn::buildOptions('card_type_id'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ), + ), + ), + 'civicrm_batch' => array( + 'dao' => 'CRM_Batch_DAO_EntityBatch', + 'grouping' => 'contri-fields', + 'fields' => array( + 'batch_id' => array( + 'name' => 'batch_id', + 'title' => ts('Batch Name'), + ), + ), + 'filters' => array( + 'bid' => array( + 'title' => ts('Batch Name'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Batch_BAO_Batch::getBatches(), + 'type' => CRM_Utils_Type::T_INT, + 'dbAlias' => 'batch_civireport.batch_id', + ), + ), + ), + 'civicrm_contribution_ordinality' => array( + 'dao' => 'CRM_Contribute_DAO_Contribution', + 'alias' => 'cordinality', + 'filters' => array( + 'ordinality' => array( + 'title' => ts('Contribution Ordinality'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => array( + 0 => 'First by Contributor', + 1 => 'Second or Later by Contributor', + ), + 'type' => CRM_Utils_Type::T_INT, + ), + ), + ), + 'civicrm_note' => array( + 'dao' => 'CRM_Core_DAO_Note', + 'fields' => array( + 'contribution_note' => array( + 'name' => 'note', + 'title' => ts('Contribution Note'), + ), + ), + 'filters' => array( + 'note' => array( + 'name' => 'note', + 'title' => ts('Contribution Note'), + 'operator' => 'like', + 'type' => CRM_Utils_Type::T_STRING, + ), + ), + ), + )) + $this->addAddressFields(FALSE); + // The tests test for this variation of the sort_name field. Don't argue with the tests :-). + $this->_columns['civicrm_contact']['fields']['sort_name']['title'] = ts('Donor Name'); + $this->_groupFilter = TRUE; + $this->_tagFilter = TRUE; + + // If we have active campaigns add those elements to both the fields and filters + if ($campaignEnabled && !empty($this->activeCampaigns)) { + $this->_columns['civicrm_contribution']['fields']['campaign_id'] = array( + 'title' => ts('Campaign'), + 'default' => 'false', + ); + $this->_columns['civicrm_contribution']['filters']['campaign_id'] = array( + 'title' => ts('Campaign'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => $this->activeCampaigns, + 'type' => CRM_Utils_Type::T_INT, + ); + $this->_columns['civicrm_contribution']['order_bys']['campaign_id'] = array('title' => ts('Campaign')); + } + + $this->_currencyColumn = 'civicrm_contribution_currency'; + + + // self::$contributionStatus = CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id'); + // $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(); + $this->_columns['civicrm_iats_journal'] = array( + 'fields' => + array( + 'id' => array('title' => 'CiviCRM Journal Id', 'default' => TRUE), + 'iats_id' => array('title' => 'iATS Journal Id', 'default' => TRUE), + 'tnid' => array('title' => 'Transaction ID', 'default' => TRUE), + 'tntyp' => array('title' => 'Transaction type', 'default' => TRUE), + 'agt' => array('title' => 'Client/Agent code', 'default' => TRUE), + 'cstc' => array('title' => 'Customer code', 'default' => TRUE), + 'inv' => array('title' => 'Invoice Reference', 'default' => TRUE), + 'dtm' => array('title' => 'Transaction date', 'default' => TRUE), + 'amt' => array('title' => 'Amount', 'default' => TRUE), + 'rst' => array('title' => 'Result string', 'default' => TRUE), + 'dtm' => array('title' => 'Transaction Date Time', 'default' => TRUE), + 'status_id' => array('title' => 'Payment Status', 'default' => TRUE), + ), + 'order_bys' => + array( + 'id' => array('title' => ts('CiviCRM Journal Id'), 'default' => TRUE, 'default_order' => 'DESC'), + 'iats_id' => array('title' => ts('iATS Journal Id')), + 'dtm' => array('title' => ts('Transaction Date Time')), + ), + 'filters' => + array( + 'dtm' => array( + 'title' => 'Transaction date', + 'operatorType' => CRM_Report_Form::OP_DATE, + 'type' => CRM_Utils_Type::T_DATE, + ), + 'inv' => array( + 'title' => 'Invoice Reference', + 'type' => CRM_Utils_Type::T_STRING, + ), + 'amt' => array( + 'title' => 'Amount', + 'operatorType' => CRM_Report_Form::OP_FLOAT, + 'type' => CRM_Utils_Type::T_FLOAT + ), + 'tntyp' => array( + 'title' => 'Type', + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => self::$_iats_transaction_types, + 'type' => CRM_Utils_Type::T_STRING, + ), + 'rst' => array( + 'title' => 'Result string', + 'type' => CRM_Utils_Type::T_STRING, + ), + 'status_id' => array( + 'title' => ts('iATS Journal Payment Status'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::contributionStatus(), + 'type' => CRM_Utils_Type::T_INT, + ), + ), + ); + parent::__construct(); + } + + /** + * Set the FROM clause for the report. + */ + public function from() { + $this->setFromBase('civicrm_contact'); + $this->_from .= " + INNER JOIN civicrm_contribution {$this->_aliases['civicrm_contribution']} + ON {$this->_aliases['civicrm_contact']}.id = {$this->_aliases['civicrm_contribution']}.contact_id + AND {$this->_aliases['civicrm_contribution']}.is_test = 0"; + + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'both' + ) { + $this->_from .= "\n LEFT JOIN civicrm_contribution_soft contribution_soft_civireport + ON contribution_soft_civireport.contribution_id = {$this->_aliases['civicrm_contribution']}.id"; + } + elseif (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'soft_credits_only' + ) { + $this->_from .= "\n INNER JOIN civicrm_contribution_soft contribution_soft_civireport + ON contribution_soft_civireport.contribution_id = {$this->_aliases['civicrm_contribution']}.id"; + } + $this->appendAdditionalFromJoins(); + } + + /** + * @param $rows + * + * @return array + */ + public function statistics(&$rows) { + $statistics = parent::statistics($rows); + + $totalAmount = $average = $fees = $net = array(); + $count = 0; + $select = " + SELECT COUNT({$this->_aliases['civicrm_contribution']}.total_amount ) as count, + SUM( {$this->_aliases['civicrm_contribution']}.total_amount ) as amount, + ROUND(AVG({$this->_aliases['civicrm_contribution']}.total_amount), 2) as avg, + {$this->_aliases['civicrm_contribution']}.currency as currency, + SUM( {$this->_aliases['civicrm_contribution']}.fee_amount ) as fees, + SUM( {$this->_aliases['civicrm_contribution']}.net_amount ) as net + "; + + $group = "\nGROUP BY {$this->_aliases['civicrm_contribution']}.currency"; + $sql = "{$select} {$this->_from} {$this->_where} {$group}"; + $dao = CRM_Core_DAO::executeQuery($sql); + $this->addToDeveloperTab($sql); + + while ($dao->fetch()) { + $totalAmount[] = CRM_Utils_Money::format($dao->amount, $dao->currency) . " (" . $dao->count . ")"; + $fees[] = CRM_Utils_Money::format($dao->fees, $dao->currency); + $net[] = CRM_Utils_Money::format($dao->net, $dao->currency); + $average[] = CRM_Utils_Money::format($dao->avg, $dao->currency); + $count += $dao->count; + } + $statistics['counts']['amount'] = array( + 'title' => ts('Total Amount (Contributions)'), + 'value' => implode(', ', $totalAmount), + 'type' => CRM_Utils_Type::T_STRING, + ); + $statistics['counts']['count'] = array( + 'title' => ts('Total Contributions'), + 'value' => $count, + ); + $statistics['counts']['fees'] = array( + 'title' => ts('Fees'), + 'value' => implode(', ', $fees), + 'type' => CRM_Utils_Type::T_STRING, + ); + $statistics['counts']['net'] = array( + 'title' => ts('Net'), + 'value' => implode(', ', $net), + 'type' => CRM_Utils_Type::T_STRING, + ); + $statistics['counts']['avg'] = array( + 'title' => ts('Average'), + 'value' => implode(', ', $average), + 'type' => CRM_Utils_Type::T_STRING, + ); + + // Stats for soft credits + if ($this->_softFrom && + CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) != + 'contributions_only' + ) { + $totalAmount = $average = array(); + $count = 0; + $select = " +SELECT COUNT(contribution_soft_civireport.amount ) as count, + SUM(contribution_soft_civireport.amount ) as amount, + ROUND(AVG(contribution_soft_civireport.amount), 2) as avg, + {$this->_aliases['civicrm_contribution']}.currency as currency"; + $sql = " +{$select} +{$this->_softFrom} +GROUP BY {$this->_aliases['civicrm_contribution']}.currency"; + $dao = CRM_Core_DAO::executeQuery($sql); + $this->addToDeveloperTab($sql); + while ($dao->fetch()) { + $totalAmount[] = CRM_Utils_Money::format($dao->amount, $dao->currency) . " (" . + $dao->count . ")"; + $average[] = CRM_Utils_Money::format($dao->avg, $dao->currency); + $count += $dao->count; + } + $statistics['counts']['softamount'] = array( + 'title' => ts('Total Amount (Soft Credits)'), + 'value' => implode(', ', $totalAmount), + 'type' => CRM_Utils_Type::T_STRING, + ); + $statistics['counts']['softcount'] = array( + 'title' => ts('Total Soft Credits'), + 'value' => $count, + ); + $statistics['counts']['softavg'] = array( + 'title' => ts('Average (Soft Credits)'), + 'value' => implode(', ', $average), + 'type' => CRM_Utils_Type::T_STRING, + ); + } + + return $statistics; + } + + /** + * This function appears to have been overrriden for the purposes of facilitating soft credits in the report. + * + * 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. + * + * 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. + */ + public function postProcess() { + // get the acl clauses built before we assemble the query + $this->buildACLClause($this->_aliases['civicrm_contact']); + + $this->beginPostProcess(); + // 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' && + !empty($this->_params['fields']['soft_credit_type_id']) + ) { + 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(); + } + } + + // 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->setPager(); + + // 2. customize main contribution query for soft credit, and build temp table 2 with soft credit contributions only + $this->softCreditFrom(); + // also include custom group from if included + // since this might be included in select + $this->customDataFrom(); + + $select = str_ireplace('contribution_civireport.total_amount', 'contribution_soft_civireport.amount', $this->_select); + $select = str_ireplace("'Contribution' as", "'Soft Credit' as", $select); + // We really don't want to join soft credit in if not required. + 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}"; + $tempQuery = "CREATE TEMPORARY TABLE civireport_contribution_detail_temp2 {$this->_databaseAttributes} AS {$sql}"; + $this->addToDeveloperTab($tempQuery); + CRM_Core_DAO::executeQuery($tempQuery); + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'soft_credits_only' + ) { + // revise pager : prev, next based on soft-credits only + $this->setPager(); + } + + // copy _from for later use of stats calculation for soft credits, and reset $this->_from to main query + $this->_softFrom = $this->_from; + + // simple reset of ->_from + $this->from(); + + // also include custom group from if included + // since this might be included in select + $this->customDataFrom(); + + // 3. Decide where to populate temp3 table from + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'contributions_only' + ) { + $tempQuery = "(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)"; + } + else { + $tempQuery = " +(SELECT * FROM civireport_contribution_detail_temp1) +UNION ALL +(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); + } + + /** + * Alter display of rows. + * + * Iterate through the rows retrieved via SQL and make changes for display purposes, + * such as rendering contacts as links. + * + * @param array $rows + * 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(); + $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(); + $paymentInstruments = CRM_Contribute_PseudoConstant::paymentInstrument(); + $contributionPages = CRM_Contribute_PseudoConstant::contributionPage(); + $batches = CRM_Batch_BAO_Batch::getBatches(); + foreach ($rows as $rowNum => $row) { + if (!empty($this->_noRepeats) && $this->_outputMode != 'csv') { + // don't repeat contact details if its same as the previous row + if (array_key_exists('civicrm_contact_id', $row)) { + if ($cid = $row['civicrm_contact_id']) { + if ($rowNum == 0) { + $prev_cid = $cid; + } + else { + if ($prev_cid == $cid) { + $display_flag = 1; + $prev_cid = $cid; + } + else { + $display_flag = 0; + $prev_cid = $cid; + } + } + + if ($display_flag) { + foreach ($row as $colName => $colVal) { + // Hide repeats in no-repeat columns, but not if the field's a section header + if (in_array($colName, $this->_noRepeats) && + !array_key_exists($colName, $this->_sections) + ) { + unset($rows[$rowNum][$colName]); + } + } + } + $entryFound = TRUE; + } + } + } + + if (CRM_Utils_Array::value('civicrm_contribution_contribution_or_soft', $rows[$rowNum]) == + 'Contribution' + ) { + unset($rows[$rowNum]['civicrm_contribution_soft_soft_credit_type_id']); + } + + $entryFound = $this->alterDisplayContactFields($row, $rows, $rowNum, 'contribution/detail', ts('View Contribution Details')) ? TRUE : $entryFound; + // convert donor sort name to link + if (array_key_exists('civicrm_contact_sort_name', $row) && + !empty($rows[$rowNum]['civicrm_contact_sort_name']) && + array_key_exists('civicrm_contact_id', $row) + ) { + $url = CRM_Utils_System::url("civicrm/contact/view", + 'reset=1&cid=' . $row['civicrm_contact_id'], + $this->_absoluteUrl + ); + $rows[$rowNum]['civicrm_contact_sort_name_link'] = $url; + $rows[$rowNum]['civicrm_contact_sort_name_hover'] = ts("View Contact Summary for this Contact."); + } + + if ($value = CRM_Utils_Array::value('civicrm_contribution_financial_type_id', $row)) { + $rows[$rowNum]['civicrm_contribution_financial_type_id'] = $contributionTypes[$value]; + $entryFound = TRUE; + } + if ($value = CRM_Utils_Array::value('civicrm_contribution_contribution_status_id', $row)) { + $rows[$rowNum]['civicrm_contribution_contribution_status_id'] = $contributionStatus[$value]; + $entryFound = TRUE; + } + if ($value = CRM_Utils_Array::value('civicrm_contribution_contribution_page_id', $row)) { + $rows[$rowNum]['civicrm_contribution_contribution_page_id'] = $contributionPages[$value]; + $entryFound = TRUE; + } + if ($value = CRM_Utils_Array::value('civicrm_contribution_payment_instrument_id', $row)) { + $rows[$rowNum]['civicrm_contribution_payment_instrument_id'] = $paymentInstruments[$value]; + $entryFound = TRUE; + } + if (!empty($row['civicrm_batch_batch_id'])) { + $rows[$rowNum]['civicrm_batch_batch_id'] = CRM_Utils_Array::value($row['civicrm_batch_batch_id'], $batches); + $entryFound = TRUE; + } + if (!empty($row['civicrm_financial_trxn_card_type_id'])) { + $rows[$rowNum]['civicrm_financial_trxn_card_type_id'] = $this->getLabels($row['civicrm_financial_trxn_card_type_id'], 'CRM_Financial_DAO_FinancialTrxn', 'card_type_id'); + $entryFound = TRUE; + } + + // Contribution amount links to viewing contribution + if (($value = CRM_Utils_Array::value('civicrm_contribution_total_amount', $row)) && + CRM_Core_Permission::check('access CiviContribute') + ) { + $url = CRM_Utils_System::url("civicrm/contact/view/contribution", + "reset=1&id=" . $row['civicrm_contribution_contribution_id'] . + "&cid=" . $row['civicrm_contact_id'] . + "&action=view&context=contribution&selectedChild=contribute", + $this->_absoluteUrl + ); + $rows[$rowNum]['civicrm_contribution_total_amount_link'] = $url; + $rows[$rowNum]['civicrm_contribution_total_amount_hover'] = ts("View Details of this Contribution."); + $entryFound = TRUE; + } + + // convert campaign_id to campaign title + if (array_key_exists('civicrm_contribution_campaign_id', $row)) { + if ($value = $row['civicrm_contribution_campaign_id']) { + $rows[$rowNum]['civicrm_contribution_campaign_id'] = $this->activeCampaigns[$value]; + $entryFound = TRUE; + } + } + + // soft credits + if (array_key_exists('civicrm_contribution_soft_credits', $row) && + 'Contribution' == + CRM_Utils_Array::value('civicrm_contribution_contribution_or_soft', $rows[$rowNum]) && + 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 +FROM civireport_contribution_detail_temp2 +WHERE civicrm_contribution_contribution_id={$row['civicrm_contribution_contribution_id']}"; + $this->addToDeveloperTab($query); + $dao = CRM_Core_DAO::executeQuery($query); + $string = ''; + $separator = ($this->_outputMode !== 'csv') ? "
    " : ' '; + while ($dao->fetch()) { + $url = CRM_Utils_System::url("civicrm/contact/view", 'reset=1&cid=' . + $dao->civicrm_contact_id); + $string = $string . ($string ? $separator : '') . + "{$dao->civicrm_contact_sort_name} " . + CRM_Utils_Money::format($dao->civicrm_contribution_total_amount_sum, $dao->civicrm_contribution_currency); + } + $rows[$rowNum]['civicrm_contribution_soft_credits'] = $string; + } + + if (array_key_exists('civicrm_contribution_soft_credit_for', $row) && + 'Soft Credit' == + CRM_Utils_Array::value('civicrm_contribution_contribution_or_soft', $rows[$rowNum]) && + array_key_exists('civicrm_contribution_contribution_id', $row) + ) { + $query = " +SELECT civicrm_contact_id, civicrm_contact_sort_name +FROM civireport_contribution_detail_temp1 +WHERE civicrm_contribution_contribution_id={$row['civicrm_contribution_contribution_id']}"; + $this->addToDeveloperTab($query); + $dao = CRM_Core_DAO::executeQuery($query); + $string = ''; + while ($dao->fetch()) { + $url = CRM_Utils_System::url("civicrm/contact/view", 'reset=1&cid=' . + $dao->civicrm_contact_id); + $string = $string . + "\n{$dao->civicrm_contact_sort_name}"; + } + $rows[$rowNum]['civicrm_contribution_soft_credit_for'] = $string; + } + + // CRM-18312 - hide 'contribution_or_soft' column if unchecked. + if (!empty($this->noDisplayContributionOrSoftColumn)) { + unset($rows[$rowNum]['civicrm_contribution_contribution_or_soft']); + unset($this->_columnHeaders['civicrm_contribution_contribution_or_soft']); + } + + //convert soft_credit_type_id into label + if (array_key_exists('civicrm_contribution_soft_soft_credit_type_id', $rows[$rowNum])) { + $rows[$rowNum]['civicrm_contribution_soft_soft_credit_type_id'] = CRM_Core_PseudoConstant::getLabel( + 'CRM_Contribute_BAO_ContributionSoft', + 'soft_credit_type_id', + $row['civicrm_contribution_soft_soft_credit_type_id'] + ); + } + + $entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, 'contribute/detail', 'List all contribution(s) for this ') ? TRUE : $entryFound; + + // skip looking further in rows, if first row itself doesn't + // have the column we need + if (!$entryFound) { + break; + } + $lastKey = $rowNum; + } + } + + public function sectionTotals() { + + // Reports using order_bys with sections must populate $this->_selectAliases in select() method. + if (empty($this->_selectAliases)) { + return; + } + + if (!empty($this->_sections)) { + // build the query with no LIMIT clause + $select = str_ireplace('SELECT SQL_CALC_FOUND_ROWS ', 'SELECT ', $this->_select); + $sql = "{$select} {$this->_from} {$this->_where} {$this->_groupBy} {$this->_having} {$this->_orderBy}"; + + // pull section aliases out of $this->_sections + $sectionAliases = array_keys($this->_sections); + + $ifnulls = array(); + foreach (array_merge($sectionAliases, $this->_selectAliases) as $alias) { + $ifnulls[] = "ifnull($alias, '') as $alias"; + } + $this->_select = "SELECT " . implode(", ", $ifnulls); + $this->_select = CRM_Contact_BAO_Query::appendAnyValueToSelect($ifnulls, $sectionAliases); + + /* Group (un-limited) report by all aliases and get counts. This might + * be done more efficiently when the contents of $sql are known, ie. by + * overriding this method in the report class. + */ + + $addtotals = ''; + + if (array_search("civicrm_contribution_total_amount", $this->_selectAliases) !== + FALSE + ) { + $addtotals = ", sum(civicrm_contribution_total_amount) as sumcontribs"; + $showsumcontribs = TRUE; + } + + $query = $this->_select . + "$addtotals, count(*) as ct from civireport_contribution_detail_temp3 group by " . + implode(", ", $sectionAliases); + // initialize array of total counts + $sumcontribs = $totals = array(); + $dao = CRM_Core_DAO::executeQuery($query); + $this->addToDeveloperTab($query); + while ($dao->fetch()) { + + // let $this->_alterDisplay translate any integer ids to human-readable values. + $rows[0] = $dao->toArray(); + $this->alterDisplay($rows); + $row = $rows[0]; + + // add totals for all permutations of section values + $values = array(); + $i = 1; + $aliasCount = count($sectionAliases); + foreach ($sectionAliases as $alias) { + $values[] = $row[$alias]; + $key = implode(CRM_Core_DAO::VALUE_SEPARATOR, $values); + if ($i == $aliasCount) { + // the last alias is the lowest-level section header; use count as-is + $totals[$key] = $dao->ct; + if ($showsumcontribs) { + $sumcontribs[$key] = $dao->sumcontribs; + } + } + else { + // other aliases are higher level; roll count into their total + $totals[$key] = (array_key_exists($key, $totals)) ? $totals[$key] + $dao->ct : $dao->ct; + if ($showsumcontribs) { + $sumcontribs[$key] = array_key_exists($key, $sumcontribs) ? $sumcontribs[$key] + $dao->sumcontribs : $dao->sumcontribs; + } + } + } + } + if ($showsumcontribs) { + $totalandsum = array(); + // ts exception to avoid having ts("%1 %2: %3") + $title = '%1 contributions / soft-credits: %2'; + + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'contributions_only' + ) { + $title = '%1 contributions: %2'; + } + elseif (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'soft_credits_only' + ) { + $title = '%1 soft-credits: %2'; + } + foreach ($totals as $key => $total) { + $totalandsum[$key] = ts($title, array( + 1 => $total, + 2 => CRM_Utils_Money::format($sumcontribs[$key]), + )); + } + $this->assign('sectionTotals', $totalandsum); + } + else { + $this->assign('sectionTotals', $totals); + } + } + } + + /** + * Generate the from clause as it relates to the soft credits. + */ + public function softCreditFrom() { + + $this->_from = " + FROM civireport_contribution_detail_temp1 temp1_civireport + INNER JOIN civicrm_contribution {$this->_aliases['civicrm_contribution']} + ON temp1_civireport.civicrm_contribution_contribution_id = {$this->_aliases['civicrm_contribution']}.id + INNER JOIN civicrm_contribution_soft contribution_soft_civireport + ON contribution_soft_civireport.contribution_id = {$this->_aliases['civicrm_contribution']}.id + INNER JOIN civicrm_contact {$this->_aliases['civicrm_contact']} + ON {$this->_aliases['civicrm_contact']}.id = contribution_soft_civireport.contact_id + {$this->_aclFrom} + "; + + $this->appendAdditionalFromJoins(); + } + + /** + * Append the joins that are required regardless of context. + */ + public function appendAdditionalFromJoins() { + if (!empty($this->_params['ordinality_value'])) { + $this->_from .= " + INNER JOIN (SELECT c.id, IF(COUNT(oc.id) = 0, 0, 1) AS ordinality FROM civicrm_contribution c LEFT JOIN civicrm_contribution oc ON c.contact_id = oc.contact_id AND oc.receive_date < c.receive_date GROUP BY c.id) {$this->_aliases['civicrm_contribution_ordinality']} + ON {$this->_aliases['civicrm_contribution_ordinality']}.id = {$this->_aliases['civicrm_contribution']}.id"; + } + $this->joinPhoneFromContact(); + $this->joinAddressFromContact(); + $this->joinEmailFromContact(); + + // include contribution note + if (!empty($this->_params['fields']['contribution_note']) || + !empty($this->_params['note_value']) + ) { + $this->_from .= " + LEFT JOIN civicrm_note {$this->_aliases['civicrm_note']} + ON ( {$this->_aliases['civicrm_note']}.entity_table = 'civicrm_contribution' AND + {$this->_aliases['civicrm_contribution']}.id = {$this->_aliases['civicrm_note']}.entity_id )"; + } + //for contribution batches + if (!empty($this->_params['fields']['batch_id']) || + !empty($this->_params['bid_value']) + ) { + $this->_from .= " + LEFT JOIN civicrm_entity_financial_trxn eft + ON eft.entity_id = {$this->_aliases['civicrm_contribution']}.id AND + eft.entity_table = 'civicrm_contribution' + LEFT JOIN civicrm_entity_batch {$this->_aliases['civicrm_batch']} + ON ({$this->_aliases['civicrm_batch']}.entity_id = eft.financial_trxn_id + AND {$this->_aliases['civicrm_batch']}.entity_table = 'civicrm_financial_trxn')"; + } + // for credit card type + $this->addFinancialTrxnFromClause(); + // for iats journal data + $this->addIatsJournalFromClause(); + } + + public function addIatsJournalFromClause() { + if ($this->isTableSelected('civicrm_iats_journal')) { + $this->_from .= " + LEFT JOIN civicrm_iats_journal {$this->_aliases['civicrm_iats_journal']} + ON {$this->_aliases['civicrm_contribution']}.invoice_id = {$this->_aliases['civicrm_iats_journal']}.inv \n"; + } + } +} diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/Journal.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/Journal.php index 9bf19576dda..e6c52d52a6a 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/Journal.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/Journal.php @@ -79,6 +79,10 @@ public function __construct() { 'options' => self::$transaction_types, 'type' => CRM_Utils_Type::T_STRING, ), + 'agt' => array( + 'title' => 'Client/Agent code', + 'type' => CRM_Utils_Type::T_STRING, + ), 'rst' => array( 'title' => 'Result string', 'type' => CRM_Utils_Type::T_STRING, diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/Recur.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/Recur.php index 6f46d92e769..c5bbd1b75ee 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/Recur.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/Form/Report/Recur.php @@ -208,6 +208,10 @@ public function __construct() { 'title' => ts('Amount'), 'default' => TRUE, ), + 'financial_type_id' => array( + 'title' => ts('Financial Type'), + 'default' => TRUE, + ), 'contribution_status_id' => array( 'title' => ts('Donation Status'), ), @@ -455,6 +459,10 @@ public function alterDisplay(&$rows) { if ($value = CRM_Utils_Array::value('civicrm_contribution_recur_contribution_status_id', $row)) { $rows[$rowNum]['civicrm_contribution_recur_contribution_status_id'] = self::$contributionStatus[$value]; } + // handle financial type id + if ($value = CRM_Utils_Array::value('civicrm_contribution_recur_financial_type_id', $row)) { + $rows[$rowNum]['civicrm_contribution_recur_financial_type_id'] = self::$financial_types[$value]; + } // Handle processor id. if ($value = CRM_Utils_Array::value('civicrm_contribution_recur_payment_processor_id', $row)) { $rows[$rowNum]['civicrm_contribution_recur_payment_processor_id'] = self::$processors[$value]; diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/iATSService.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/iATSService.php index f65056eb057..6c6d02cae05 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/iATSService.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/CRM/iATS/iATSService.php @@ -639,7 +639,8 @@ public function reasonMessage($code) { return 'The card is over the limit.'; case 'REJECT: 15': - return 'General decline code. Please have cardholder call the number on the back of the card.'; + // return 'General decline code. Please have cardholder call the number on the back of the card.'; + return 'General decline code.'; case 'REJECT: 16': return 'Invalid charge card number. Verify and re-enter credit card information.'; @@ -663,7 +664,8 @@ public function reasonMessage($code) { return 'Charge card expired.'; case 'REJECT: 25': - return 'Capture card. Reported lost or stolen.'; + // return 'Capture card. Reported lost or stolen.'; + return 'Possibly reported lost or stolen.'; case 'REJECT: 26': return 'Invalid transaction, invalid expiry date. Please confirm and retry transaction.'; diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/README.md b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/README.md index 4d309250458..865e2a6c35e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/README.md +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/README.md @@ -24,6 +24,8 @@ Installation This extension follows the standard installation method - if you've got a supported CiviCRM version and you've set up your extensions directory, it'll appear in the Manage Extensions list as 'iATS Payments (com.iatspayments.civicrm)'. Hit Install. +As of CiviCRM 5.x, the iATS extension is distributed with the CiviCRM download. This is generally the right version to install. See https://github.com/iATSPayments/com.iatspayments.civicrm/issues/242 for notes on converting from a previous manual install. + If you need help with installing extensions, try: https://wiki.civicrm.org/confluence/display/CRMDOC/Extensions If you want to try out a particular version directly from github, you probably already know how to do that. diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/IatsPayments/GetJournal.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/IatsPayments/GetJournal.php index 77d00b1f7e6..e228c1f5212 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/IatsPayments/GetJournal.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/IatsPayments/GetJournal.php @@ -9,8 +9,6 @@ * * @param array $params * - * @return array - * * Get entries from iATSPayments in the journal table */ function _civicrm_api3_iats_payments_get_journal_spec(&$params) { @@ -63,7 +61,7 @@ function civicrm_api3_iats_payments_get_journal($params) { 'inv' => 'String', ); $i = 0; - foreach($params as $key => $value) { + foreach ($params as $key => $value) { if (isset($select_params[$key])) { $i++; if (is_string($value)) { @@ -71,7 +69,7 @@ function civicrm_api3_iats_payments_get_journal($params) { $args[$i] = array($value, $select_params[$key]); } elseif (is_array($value)) { - foreach(array_keys($value) as $sql) { + foreach (array_keys($value) as $sql) { $select .= " AND ($key %$i)"; $args[$i] = array($sql, 'String'); } @@ -80,7 +78,7 @@ function civicrm_api3_iats_payments_get_journal($params) { } $limit = 25; if (isset($params['options']['limit'])) { - $limit = (integer) $params['options']['limit']; + $limit = (integer) $params['options']['limit']; } if ($limit > 0) { $i++; @@ -101,18 +99,17 @@ function civicrm_api3_iats_payments_get_journal($params) { /* We index in the transaction_id */ $record = array(); foreach (get_object_vars($dao) as $key => $value) { - if ('N' != $key && (0 !== strpos($key,'_'))) { + if ('N' != $key && (0 !== strpos($key, '_'))) { $record[$key] = $value; } } $key = $dao->tnid; - $values[$key] = $record; + $values[$key] = $record; } } catch (Exception $e) { - CRM_Core_Error::debug_var('params',$params); + CRM_Core_Error::debug_var('params', $params); // throw API_Exception('iATS Payments journalling failed: '. $e->getMessage()); } return civicrm_api3_create_success($values); } - diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/IatsPayments/Journal.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/IatsPayments/Journal.php index b99613ac2ed..a872753cdcd 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/IatsPayments/Journal.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/IatsPayments/Journal.php @@ -9,8 +9,6 @@ * * @param array $params * - * @return array - * * Record an entry from iATSPayments into the journal table */ function _civicrm_api3_iats_payments_journal_spec(&$params) { @@ -36,13 +34,13 @@ function civicrm_api3_iats_payments_journal($params) { //return civicrm_api3_create_success(TRUE, array('test' => TRUE)); try { $data = $params['data']; - $dtm = date('YmdHis',$params['receive_date']); + $dtm = date('YmdHis', $params['receive_date']); $defaults = array( - 'Client Code' => '', + 'Client Code' => '', 'Method of Payment' => '', - 'Comment' => '' + 'Comment' => '', ); - foreach($defaults as $key => $default) { + foreach ($defaults as $key => $default) { $data[$key] = empty($data[$key]) ? $default : $data[$key]; } // There are unique keys on tnid (transaction) and iats_id (journal) @@ -67,13 +65,12 @@ function civicrm_api3_iats_payments_journal($params) { 10 => array($data['Comment'], 'String'), 11 => array($params['status_id'], 'Integer'), ); - $result = CRM_Core_DAO::executeQuery($sql_action ." civicrm_iats_journal + $result = CRM_Core_DAO::executeQuery($sql_action . " civicrm_iats_journal (tnid, iats_id, dtm, agt, cstc, inv, amt, rst, tntyp, cm, status_id) VALUES (%1, $iats_journal_id, %3, %4, %5, %6, %7, %8, %9, %10, %11)", $query_params); } catch (Exception $e) { - CRM_Core_Error::debug_var('params',$params); - // throw CiviCRM_API3_Exception('iATS Payments journalling failed: '. $e->getMessage()); + CRM_Core_Error::debug_var('params', $params); + // throw CiviCRM_API3_Exception('iATS Payments journalling failed: ' . $e->getMessage()); } return civicrm_api3_create_success(); } - diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsrecurringcontributions.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsrecurringcontributions.php index 51b517f4b89..21510bdb671 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsrecurringcontributions.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsrecurringcontributions.php @@ -275,7 +275,7 @@ function civicrm_api3_job_iatsrecurringcontributions($params) { $contribution['financial_type_id'] = $dao->financial_type_id; } // if we have a created a pending contribution record due to a future start time, then recycle that CiviCRM contribution record now. - // Note that the date and amount both could have changed. + // Note that the date and amount both could have changed. // The key is to only match if we find a single pending contribution, with a NULL transaction id, for this recurring schedule. // We'll need to pay attention later that we may or may not already have a contribution id. try { @@ -285,7 +285,9 @@ function civicrm_api3_job_iatsrecurringcontributions($params) { 'contribution_recur_id' => $contribution_recur_id, 'contribution_status_id' => "Pending", )); - $contribution['id'] = $pending_contribution['id']; + if (!empty($pending_contribution['id'])) { + $contribution['id'] = $pending_contribution['id']; + } } catch (Exception $e) { // ignore, we'll proceed normally without a contribution id @@ -331,7 +333,7 @@ function civicrm_api3_job_iatsrecurringcontributions($params) { } // So far so, good ... now create the pending contribution, and save its id // and then try to get the money, and do one of: - // update the contribution to failed, leave as pending for server failure, complete the transaction, + // update the contribution to failed, leave as pending for server failure, complete the transaction, // or update a pending ach/eft with it's transaction id. $result = _iats_process_contribution_payment($contribution, $options, $original_contribution_id); if ($email_failure_report && !empty($contribution['iats_reject_code'])) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.mgd.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.mgd.php index 2ebaa6963c6..21df020af58 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.mgd.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.mgd.php @@ -3,13 +3,13 @@ // The record will be automatically inserted, updated, or deleted from the // database as appropriate. For more details, see "hook_civicrm_managed" at: // http://wiki.civicrm.org/confluence/display/CRMDOC42/Hook+Reference -return array ( +return array( 0 => - array ( + array( 'name' => 'Cron:Job.Iatsreport', 'entity' => 'Job', 'params' => - array ( + array( 'version' => 3, 'name' => 'iATS Payments Get Transaction Journal', 'description' => 'Call into iATS to get transaction journals (for auditing and verifying).', @@ -18,6 +18,6 @@ 'api_action' => 'iatsreport', 'parameters' => '', ), - 'update' => 'never' + 'update' => 'never', ), ); diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.php index 92a23e191c2..90bc3c22ff7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsreport.php @@ -2,8 +2,8 @@ /** * Job.IatsReport API specification (optional) - * - * Pull in the iATS transaction journal and save it in the corresponding table + * + * Pull in the iATS transaction journal and save it in the corresponding table * for local access for easier verification, auditing and reporting. * * @param array $spec description of fields supported by this API call @@ -18,12 +18,6 @@ function _civicrm_api3_job_iatsreport_spec(&$spec) { /** * Job.IatsReport API * - * @param array $params - * @return array API result descriptor - * @see civicrm_api3_create_success - * @see civicrm_api3_create_error - * @throws API_Exception - * Fetch all recent transactions from iATS for the purposes of auditing (in separate jobs). * This addresses multiple needs: * 1. Verify incomplete ACH/EFT contributions. @@ -32,6 +26,11 @@ function _civicrm_api3_job_iatsreport_spec(&$spec) { * 4. Input one-time contributions that did not go through CiviCRM * 5. Audit for remote changes in iATS. * + * @param array $params + * @return array API result descriptor + * @see civicrm_api3_create_success + * @see civicrm_api3_create_error + * @throws API_Exception */ function civicrm_api3_job_iatsreport($params) { @@ -46,13 +45,13 @@ function civicrm_api3_job_iatsreport($params) { )); } catch (CiviCRM_API3_Exception $e) { - throw new API_Exception('Unexpected error getting payment processors: ' . $e->getMessage()); // . "\n" . $e->getTraceAsString()); + throw new API_Exception('Unexpected error getting payment processors: ' . $e->getMessage()); // . "\n" . $e->getTraceAsString()); } if (empty($result['values'])) { return; } $payment_processors = array(); - foreach($result['values'] as $payment_processor) { + foreach ($result['values'] as $payment_processor) { $user_name = $payment_processor['user_name']; $type = $payment_processor['payment_type']; // 1 for cc, 2 for ach/eft $id = $payment_processor['id']; @@ -69,25 +68,25 @@ function civicrm_api3_job_iatsreport($params) { $iats_settings = CRM_Core_BAO_Setting::getItem('iATS Payments Extension', 'iats_settings'); // I also use the setttings to keep track of the last time I imported journal data from iATS. $iats_journal = CRM_Core_BAO_Setting::getItem('iATS Payments Extension', 'iats_journal'); - foreach(array('quick', 'recur', 'series') as $setting) { - $import[$setting] = empty($iats_settings['import_'.$setting]) ? 0 : 1; + foreach (array('quick', 'recur', 'series') as $setting) { + $import[$setting] = empty($iats_settings['import_' . $setting]) ? 0 : 1; } - require_once("CRM/iATS/iATSService.php"); + require_once "CRM/iATS/iATSService.php"; // an array of types => methods => payment status of the records retrieved $process_methods = array( - 1 => array('cc_journal_csv' => 1,'cc_payment_box_journal_csv' => 1, 'cc_payment_box_reject_csv' => 4), - 2 => array('acheft_journal_csv' => 1, 'acheft_payment_box_journal_csv' => 1, 'acheft_payment_box_reject_csv' => 4) + 1 => array('cc_journal_csv' => 1, 'cc_payment_box_journal_csv' => 1, 'cc_payment_box_reject_csv' => 4), + 2 => array('acheft_journal_csv' => 1, 'acheft_payment_box_journal_csv' => 1, 'acheft_payment_box_reject_csv' => 4), ); /* initialize some values so I can report at the end */ // count the number of records from each iats account analysed, and the number of each kind found ('action') $processed = array(); // save all my api result error messages as well $error_log = array(); - foreach($payment_processors as $user_name => $payment_processors_per_user) { + foreach ($payment_processors as $user_name => $payment_processors_per_user) { $processed[$user_name] = array(); foreach ($payment_processors_per_user as $type => $payment_processors_per_user_type) { $processed[$user_name][$type] = array(); - // we might have multiple payment processors by type e.g. SWIPE or separate codes for + // we might have multiple payment processors by type e.g. SWIPE or separate codes for // one-time and recurring contributions, I only want to process once per user_name + type $payment_processor = reset($payment_processors_per_user_type); $process_methods_per_type = $process_methods[$type]; @@ -95,7 +94,7 @@ function civicrm_api3_job_iatsreport($params) { /* the is_test below should always be 0, but I'm leaving it in, in case eventually we want to be verifying tests */ $credentials = iATS_Service_Request::credentials($payment_processor['id'], $payment_processor['is_test']); - foreach($process_methods_per_type as $method => $payment_status_id) { + foreach ($process_methods_per_type as $method => $payment_status_id) { // initialize my counts $processed[$user_name][$type][$method] = 0; // watchdog('civicrm_iatspayments_com', 'pp:
    !pp
    ', array('!pp' => print_r($payment_processor,TRUE)), WATCHDOG_NOTICE); @@ -105,20 +104,22 @@ function civicrm_api3_job_iatsreport($params) { $iats = new iATS_Service_Request($iats_service_params); // For some methods, I only want to check once per day. $skip_method = FALSE; - $journal_setting_key = 'last_update_'.$method; - switch($method) { + $journal_setting_key = 'last_update_' . $method; + switch ($method) { case 'acheft_journal_csv': // special case to get today's transactions, so we're as real-time as we can be - case 'cc_journal_csv': + case 'cc_journal_csv': $request = array( - 'date' => date('Y-m-d').'T23:59:59+00:00', + 'date' => date('Y-m-d') . 'T23:59:59+00:00', 'customerIPAddress' => (function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']), ); break; - default: // box journals (approvals and rejections) only go up to the end of yesterday + + default: + // box journals (approvals and rejections) only go up to the end of yesterday $request = array( 'startIndex' => 0, 'endIndex' => 1000, - 'toDate' => date('Y-m-d',strtotime('-1 day')).'T23:59:59+00:00', + 'toDate' => date('Y-m-d', strtotime('-1 day')) . 'T23:59:59+00:00', 'customerIPAddress' => (function_exists('ip_address') ? ip_address() : $_SERVER['REMOTE_ADDR']), ); // Calculate how far back I want to go, default 2 days ago. @@ -126,50 +127,54 @@ function civicrm_api3_job_iatsreport($params) { // Check when I last downloaded this box journal if (!empty($iats_journal[$journal_setting_key])) { // If I've already done this today, don't do it again. - if (0 === strpos($iats_journal[$journal_setting_key],date('Y-m-d'))) { + if (0 === strpos($iats_journal[$journal_setting_key], date('Y-m-d'))) { $skip_method = TRUE; } - else { // Make sure I fill in any gaps if this cron hasn't run for a while, but no more than a month - $fromDate = min(strtotime($iats_journal[$journal_setting_key]),strtotime('-2 days')); - $fromDate = max($fromDate,strtotime('-30 days')); + else { + // Make sure I fill in any gaps if this cron hasn't run for a while, but no more than a month + $fromDate = min(strtotime($iats_journal[$journal_setting_key]), strtotime('-2 days')); + $fromDate = max($fromDate, strtotime('-30 days')); } } - else { // If I've cleared the settings, then go back a month of data. + else { + // If I've cleared the settings, then go back a month of data. $fromDate = strtotime('-30 days'); } // reset the request fromDate, from the beginning of fromDate's day. - $request['fromDate'] = date('Y-m-d',$fromDate).'T00:00:00+00:00'; + $request['fromDate'] = date('Y-m-d', $fromDate) . 'T00:00:00+00:00'; break; } if (!$skip_method) { $iats_journal[$journal_setting_key] = date('Y-m-d H:i:s'); // make the soap request, should return a csv file - $response = $iats->request($credentials,$request); + $response = $iats->request($credentials, $request); // use my iats object to parse the result into an array of transaction ojects $transactions = $iats->getCSV($response, $method); // for the acheft journal, I also pull the previous 4 days and append, a bit of a hack. if ('acheft_journal_csv' == $method) { for ($days_before = -1; $days_before > -5; $days_before--) { - $request['date'] = date('Y-m-d', strtotime($days_before.' day')) . 'T23:59:59+00:00'; + $request['date'] = date('Y-m-d', strtotime($days_before . ' day')) . 'T23:59:59+00:00'; $response = $iats->request($credentials, $request); $transactions = array_merge($transactions, $iats->getCSV($response, $method)); } } // CRM_Core_Error::debug_var($method, $transactions); - foreach($transactions as $transaction) { + foreach ($transactions as $transaction) { try { $t = get_object_vars($transaction); $t['status_id'] = $payment_status_id; // A few more hacks for the one day journals - switch($method) { - case 'acheft_journal_csv': + switch ($method) { + case 'acheft_journal_csv': $t['data']['Method of Payment'] = 'ACHEFT'; $t['data']['Client Code'] = $credentials['agentCode']; break; - case 'cc_journal_csv': + + case 'cc_journal_csv': $t['data']['Method of Payment'] = $t['data']['CCType']; $t['data']['Client Code'] = $credentials['agentCode']; break; + } civicrm_api3('IatsPayments', 'journal', $t); $processed[$user_name][$type][$method]++; @@ -185,18 +190,18 @@ function civicrm_api3_job_iatsreport($params) { CRM_Core_BAO_Setting::setItem($iats_journal, 'iATS Payments Extension', 'iats_journal'); // watchdog('civicrm_iatspayments_com', 'found:
    !found
    ', array('!found' => print_r($processed,TRUE)), WATCHDOG_NOTICE); $message = ''; - foreach($processed as $user_name => $p) { + foreach ($processed as $user_name => $p) { foreach ($p as $type => $ps) { $prefix = ($type == 1) ? 'cc' : 'acheft'; - $results = - array( + $results + = array( 1 => $user_name, 2 => $prefix, - 3 => $ps[$prefix.'_journal_csv'], - 4 => $ps[$prefix.'_payment_box_journal_csv'], - 5 => $ps[$prefix.'_payment_box_reject_csv'], + 3 => $ps[$prefix . '_journal_csv'], + 4 => $ps[$prefix . '_payment_box_journal_csv'], + 5 => $ps[$prefix . '_payment_box_reject_csv'], ); - $message .= '
    '. ts('For account %1, type %2, processed %3 approvals from the one-day journals, and %4 approval and %5 rejection records from previous days using the box journals.', $results); + $message .= '
    ' . ts('For account %1, type %2, processed %3 approvals from the one-day journals, and %4 approval and %5 rejection records from previous days using the box journals.', $results); } } if (count($error_log) > 0) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsverify.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsverify.php index a2c88d2da10..2145781d63e 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsverify.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/api/v3/Job/Iatsverify.php @@ -51,6 +51,12 @@ function _civicrm_api3_job_iatsverify_spec(&$spec) { /** * Job.IatsVerify API. * + * Look up all incomplete or pending (status = 2) contributions and see if they've been received approved or rejected payments + * at iATS, looked up via the Journal + * Update the corresponding recurring contribution record to status = 1 (or 4) + * This works for both the initial contribution and subsequent contributions of recurring contributions, as well as one offs. + * TODO: what kind of alerts should be provided if it fails? + * * @param array $params * * @return array API result descriptor @@ -59,12 +65,6 @@ function _civicrm_api3_job_iatsverify_spec(&$spec) { * @see civicrm_api3_create_error * * @throws API_Exception - * * Look up all incomplete or pending (status = 2) contributions and see if they've been received approved or rejected payments - * at iATS, looked up via the Journal - * Update the corresponding recurring contribution record to status = 1 (or 4) - * This works for both the initial contribution and subsequent contributions of recurring contributions, as well as one offs. - * TODO: what kind of alerts should be provided if it fails? - * */ function civicrm_api3_job_iatsverify($params) { @@ -80,12 +80,11 @@ function civicrm_api3_job_iatsverify($params) { $processed = array(1 => 0, 4 => 0); // Save all my api error result messages. $error_log = array(); - // $select_params = array( 'sequential' => 1, 'receive_date' => array('>' => "now - $verify_days day"), 'options' => array('limit' => 0), - 'contribution_status_id' => array('NOT IN' => array('Completed', 'Failed')), + 'contribution_status_id' => array('IN' => array('Pending')), 'invoice_id' => array('IS NOT NULL' => 1), 'contribution_test' => 0, 'return' => array('trxn_id', 'invoice_id', 'contribution_recur_id', 'contact_id', 'source'), @@ -146,7 +145,7 @@ function civicrm_api3_job_iatsverify($params) { } catch (CiviCRM_API3_Exception $e) { $is_email_receipt = 0; - $error_log[] = $e->getMessage() ."\n"; + $error_log[] = $e->getMessage() . "\n"; } } $complete['is_email_receipt'] = $is_email_receipt; @@ -155,19 +154,19 @@ function civicrm_api3_job_iatsverify($params) { $contributionResult = civicrm_api3('contribution', 'completetransaction', $complete); } catch (CiviCRM_API3_Exception $e) { - $error_log[] = 'Failed to complete transaction: '. $e->getMessage() ."\n"; + $error_log[] = 'Failed to complete transaction: ' . $e->getMessage() . "\n"; } // Restore source field and trxn_id that completetransaction overwrites civicrm_api3('contribution', 'create', array( - 'id' => $contribution['id'], + 'id' => $contribution['id'], 'source' => $contribution['source'], - 'trxn_id' => $trxn_id + 'trxn_id' => $trxn_id, )); case 4: // failed, just update the contribution status. civicrm_api3('Contribution', 'create', array( 'id' => $contribution['id'], - 'contribution_status_id' => $contribution_status_id + 'contribution_status_id' => $contribution_status_id, )); } // Always log these requests in my cutom civicrm table for auditing type purposes @@ -192,14 +191,14 @@ function civicrm_api3_job_iatsverify($params) { } } catch (CiviCRM_API3_Exception $e) { - $error_log[] = $e->getMessage() ."\n"; + $error_log[] = $e->getMessage() . "\n"; } $message .= '
    ' . ts('Completed with %1 errors.', array( 1 => count($error_log), ) ); - $message .= '
    ' . ts('Processed %1 approvals and %2 rejection records from the previous ' . IATS_VERIFY_DAYS . ' days.', + $message .= '
    ' . ts('Processed %1 approvals and %2 rejection records from the previous ' . IATS_VERIFY_DAYS . ' days.', array( 1 => $processed[1], 2 => $processed[4], diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/composer.json b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/composer.json index 12d66edfd84..74c9b211a76 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/composer.json +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/composer.json @@ -1,5 +1,5 @@ { "name": "iats-payments/civicrm", "description": "CiviCRM extension for iATS Payments", - "license": "AGPL" + "license": "AGPL-3.0" } diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/iats.php b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/iats.php index d6db867634c..65467d75fe6 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/iats.php +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/iats.php @@ -1100,7 +1100,7 @@ function iats_civicrm_buildForm_CRM_Contribute_Form_UpdateSubscription(&$form) { return; } // Only mangle this form for recurring contributions using iATS, (and not the UKDD version) - $payment_processor_type = $form->_paymentProcessor['class_name']; + $payment_processor_type = empty($form->_paymentProcessor) ? substr(get_class($form->_paymentProcessorObj),9) : $form->_paymentProcessor['class_name']; if (0 !== strpos($payment_processor_type, 'Payment_iATSService')) { return; } @@ -1132,6 +1132,12 @@ function iats_civicrm_buildForm_CRM_Contribute_Form_UpdateSubscription(&$form) { catch (CiviCRM_API3_Exception $e) { return; } + try { + $pp = civicrm_api3('PaymentProcessor', 'getsingle', array('id' => $recur['payment_processor_id'])); + } + catch (CiviCRM_API3_Exception $e) { + $pp = array(); + } // Turn off default notification checkbox, because that's a better default. $defaults = array('is_notify' => 0); $edit_fields = array( @@ -1173,8 +1179,8 @@ function iats_civicrm_buildForm_CRM_Contribute_Form_UpdateSubscription(&$form) { // Now add some more fields for display only /* Add in the contact's name */ $form->addElement('static', 'contact', $contact['display_name']); - // get my pp. - $pp_label = $form->_paymentProcessor['name']; + // get my pp, if available. + $pp_label = empty($pp['name']) ? $form->_paymentProcessor['name'] : $pp['name']; $form->addElement('static', 'payment_processor', $pp_label); $label = CRM_Contribute_Pseudoconstant::financialType($recur['financial_type_id']); $form->addElement('static', 'financial_type', $label); diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/info.xml b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/info.xml index 900df21e5cb..55c5172d5b7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/info.xml +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/info.xml @@ -7,21 +7,21 @@ https://github.com/iATSPayments/com.iatspayments.civicrm/issues?state=open https://github.com/iATSPayments/com.iatspayments.civicrm https://github.com/iATSPayments/com.iatspayments.civicrm - https://github.com/iATSPayments/com.iatspayments.civicrm/blob/master/release-notes/1.6.1.md + https://github.com/iATSPayments/com.iatspayments.civicrm/blob/master/release-notes/1.6.2.md https://www.semper-it.com/civicamp-iats-payments-slides - AGPL + AGPL-3.0 Alan Dixon iats@blackflysolutions.ca - 2017-10-17 - 1.6.1 + 2018-08-15 + 1.6.2 stable - 4.7 + 5.0 - This is a CiviCRM 4.7-only release. Please see the current release notes linked above for details. + This release resolves some 5.x compatibility issues, but should continue to work on 4.7.x. Please see the current release notes linked above for details. CRM/iATS diff --git a/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/release-notes/1.6.2.md b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/release-notes/1.6.2.md new file mode 100644 index 00000000000..2f5b84dbe23 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/ext/iatspayments/release-notes/1.6.2.md @@ -0,0 +1,7 @@ +# iATS CiviCRM Extension 1.6.2 + +July 6, 2018 + +This release is an update to the 1.6.1 (4.7+ compatible version) of the iATS payment extension for CiviCRM, primarily for compatibility with the latest 5.x CiviCRM series, which introduced some non-critical feature breakage as noted in various issues. + +Newly added is a slightly modified version of the detailed contribution report that joins it to iATS journal data. This allows administrators to do reconciliation of CiviCRM contributions against transactions reported from iATS. diff --git a/profiles/civicrm_starterkit/modules/civicrm/js/crm.ajax.js b/profiles/civicrm_starterkit/modules/civicrm/js/crm.ajax.js index 9cb5eb601e0..e1742da8fd9 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/js/crm.ajax.js +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/js/model/crm.uf.js b/profiles/civicrm_starterkit/modules/civicrm/js/model/crm.uf.js index a6ab2982aec..4f198c12a9b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/js/model/crm.uf.js +++ b/profiles/civicrm_starterkit/modules/civicrm/js/model/crm.uf.js @@ -32,7 +32,7 @@ * Add a help link to a form label */ function addHelp(title, options) { - return title + ' '; + return title + ' '; } function watchChanges() { diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/Cache/IntegrationTests/LICENSE b/profiles/civicrm_starterkit/modules/civicrm/packages/Cache/IntegrationTests/LICENSE new file mode 100644 index 00000000000..f166f6e87e0 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/packages/Cache/IntegrationTests/LegacySimpleCacheTest.php b/profiles/civicrm_starterkit/modules/civicrm/packages/Cache/IntegrationTests/LegacySimpleCacheTest.php new file mode 100644 index 00000000000..abf59427f09 --- /dev/null +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/Cache/IntegrationTests/LegacySimpleCacheTest.php @@ -0,0 +1,757 @@ +, Tobias Nyholm + * + * 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/profiles/civicrm_starterkit/modules/civicrm/packages/Cache/IntegrationTests/README.md b/profiles/civicrm_starterkit/modules/civicrm/packages/Cache/IntegrationTests/README.md new file mode 100644 index 00000000000..8b6b87a345c --- /dev/null +++ b/profiles/civicrm_starterkit/modules/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/profiles/civicrm_starterkit/modules/civicrm/packages/DB.php b/profiles/civicrm_starterkit/modules/civicrm/packages/DB.php index 2a06764f957..30dd48b3808 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/DB.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/DB.php @@ -790,7 +790,7 @@ static function parseDSN($dsn) $parsed['dbsyntax'] = $str; } - if (!count($dsn)) { + if (empty($dsn)) { return $parsed; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/DB/common.php b/profiles/civicrm_starterkit/modules/civicrm/packages/DB/common.php index afa8359ba9b..ee50c0880d7 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/DB/common.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/DB/common.php @@ -1342,7 +1342,8 @@ function &getRow($query, $params = array(), } } // 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 @@ function &getAll($query, $params = array(), } } - if (sizeof($params) > 0) { + $params = (array) $params; + if (count($params) > 0) { $sth = $this->prepare($query); if (DB::isError($sth)) { diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm.php b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm.php index e5de7720f1f..3931b801c0f 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm.php @@ -854,11 +854,11 @@ function getSubmitValue($elementName) $base = substr($elementName, 0, $pos); $idx = explode('][', str_replace(["['", "']", '["', '"]'], ['[', ']', '[', ']'], substr($elementName, $pos + 1, -1))); if (isset($this->_submitValues[$base])) { - $value = CRM_Utils_Array::recursiveValue($this->_submitValues[$base], $idx); + $value = CRM_Utils_Array::pathGet($this->_submitValues[$base], $idx); } if ((is_array($value) || null === $value) && isset($this->_submitFiles[$base])) { - if (!CRM_Utils_Array::recursiveIsset($this->_submitFiles[$base], array_merge(['name'], $idx))) { + if (!CRM_Utils_Array::pathIsset($this->_submitFiles[$base], array_merge(['name'], $idx))) { $fileValue = NULL; } else { @@ -867,7 +867,7 @@ function getSubmitValue($elementName) foreach ($props as $prop) { $fileValue = HTML_QuickForm::arrayMerge( $fileValue, - $this->_reindexFiles(CRM_Utils_Array::recursiveValue($this->_submitFiles[$base], array_merge([$prop], $idx)), $prop) + $this->_reindexFiles(CRM_Utils_Array::pathGet($this->_submitFiles[$base], array_merge([$prop], $idx)), $prop) ); } } @@ -1264,7 +1264,7 @@ function applyFilter($element, $filter) $this->_submitValues[$elName] = $this->_recursiveFilter($filter, $value); } else { $keys = explode('[', trim(str_replace(["['", "']", '["', '"]'], ['[', '', '[', ''], $elName), '][')); - $this->_submitValues = CRM_Utils_Array::recursiveBuild($keys, $this->_recursiveFilter($filter, $value), $this->_submitValues); + CRM_Utils_Array::pathSet($this->_submitValues, $keys, $this->_recursiveFilter($filter, $value)); } } } @@ -1549,7 +1549,7 @@ function validate() $base = substr($target, 0, $pos); $idx = explode('][', str_replace(["['", "']", '["', '"]'], ['[', ']', '[', ']'], substr($target, $pos + 1, -1))); $idx = array_merge([$base, 'name'], $idx); - $isUpload = CRM_Utils_Array::recursiveIsset($this->_submitFiles, $idx); + $isUpload = CRM_Utils_Array::pathIsset($this->_submitFiles, $idx); } if ($isUpload && (!isset($submitValue['error']) || UPLOAD_ERR_NO_FILE == $submitValue['error'])) { continue 2; diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/Renderer/ArraySmarty.php b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/Renderer/ArraySmarty.php index 980ba694bb2..8aa510a3edc 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/Renderer/ArraySmarty.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/Renderer/ArraySmarty.php @@ -261,9 +261,9 @@ function _storeArray($elAry) unset($elAry['keys']); // where should we put this element... if (is_array($this->_currentGroup) && ('group' != $elAry['type'])) { - $this->_currentGroup = CRM_Utils_Array::recursiveBuild($keys, $elAry, $this->_currentGroup); + CRM_Utils_Array::pathSet($this->_currentGroup, $keys, $elAry); } else { - $this->_ary = CRM_Utils_Array::recursiveBuild($keys, $elAry, $this->_ary); + CRM_Utils_Array::pathSet($this->_ary, $keys, $elAry); } } return; diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/date.php b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/date.php index 040c800f374..9e816e680dd 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/date.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/date.php @@ -159,40 +159,48 @@ function _createElements() $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 @@ function _createElements() $this->_options['minYear'] > $this->_options['maxYear']? -1: 1 ); $emptyText = ts('-year-'); + $ariaLabel .= ts(' year'); break; case 'y': $options = $this->_createOptionList( @@ -210,10 +219,12 @@ function _createElements() ); 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 @@ function _createElements() 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 @@ function _createElements() $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/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/element.php b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/element.php index c5103200341..dd90e77f167 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/element.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/element.php @@ -345,7 +345,7 @@ function _findValue(&$values) return $values[$elementName]; } elseif (strpos($elementName, '[')) { $keys = explode('[', str_replace(']', '', $elementName)); - return CRM_Utils_Array::recursiveValue($values, $keys); + return CRM_Utils_Array::pathGet($values, $keys); } else { return null; } @@ -475,7 +475,9 @@ function _prepareValue($value, $assoc) return array($name => $value); } else { $keys = explode('[', str_replace(']', '', $name)); - return CRM_Utils_Array::recursiveBuild($keys, $value); + $preparedValue = []; + CRM_Utils_Array::pathSet($preparedValue, $keys, $value); + return $preparedValue; } } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/file.php b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/file.php index 220b501c2d5..7365d6129e8 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/file.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/file.php @@ -335,7 +335,7 @@ function _findValue(&$values) $base = substr($elementName, 0, $pos); $idx = explode('][', str_replace(["['", "']", '["', '"]'], ['[', ']', '[', ']'], substr($elementName, $pos + 1, -1))); $idx = array_merge([$base, 'name'], $idx); - if (!CRM_Utils_Array::recursiveIsset($_FILES, $idx)) { + if (!CRM_Utils_Array::pathIsset($_FILES, $idx)) { return NULL; } else { @@ -343,7 +343,7 @@ function _findValue(&$values) $value = []; foreach ($props as $prop) { $idx[1] = $prop; - $value[$prop] = CRM_Utils_Array::recursiveValue($_FILES, $idx); + $value[$prop] = CRM_Utils_Array::pathGet($_FILES, $idx); } return $value; } diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/hierselect.php b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/hierselect.php index 0a1113140af..a3c73808ead 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/hierselect.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/HTML/QuickForm/hierselect.php @@ -230,8 +230,8 @@ function _setOptions() $arrayKeys = []; foreach (array_keys($this->_elements) AS $key) { if (isset($this->_options[$key])) { - if ((empty($arrayKeys)) || CRM_Utils_Array::recursiveIsset($this->_options[$key], $arrayKeys)) { - $array = empty($arrayKeys) ? $this->_options[$key] : CRM_Utils_Array::recursiveValue($this->_options[$key], $arrayKeys); + if ((empty($arrayKeys)) || CRM_Utils_Array::pathIsset($this->_options[$key], $arrayKeys)) { + $array = empty($arrayKeys) ? $this->_options[$key] : CRM_Utils_Array::pathGet($this->_options[$key], $arrayKeys); if (is_array($array)) { $select =& $this->_elements[$key]; $select->_options = array(); @@ -364,6 +364,7 @@ function _setJSArray($grpName, $options, &$js, $optValue = '') if ($js != '') { $js .= ",\n"; } + $options = utf8_encode($options); $js .= '"'.$optValue.'":'.json_encode($options); } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mime.php b/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mime.php index ca815231c59..8ed49b10878 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mime.php +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mime.php @@ -1,4 +1,5 @@ @@ -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 * @copyright 2003-2006 PEAR * @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 @@ function __construct($params = array()) * @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 @@ function setParam($name, $value) * @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 @@ function getParam($name) * 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: (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 @@ function addHTMLImage($file, 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 @@ function addHTMLImage($file, /** * 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 : * - * @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 @@ function addAttachment($file, $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 @@ function addAttachment($file, 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 @@ function &_file2str($file_name) * 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 @@ function &_addHtmlPart(&$obj) * 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 @@ function &_addMixedPart() * 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 @@ function &_addAlternativePart(&$obj) * 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 @@ function &_addHtmlImagePart(&$obj, $value) $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 @@ function &_addAttachmentPart(&$obj, $value) 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 @@ function saveMessage($filename, $params = null, $headers = null, $overwrite = fa } 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 @@ function saveMessage($filename, $params = null, $headers = null, $overwrite = fa } /** - * 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 @@ function saveMessageBody($filename, $params = null) @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 @@ function &get($params = null, $filename = null, $skip_head = false) // * 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 @@ function &get($params = null, $filename = null, $skip_head = false) // * 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 @@ function &get($params = null, $filename = null, $skip_head = false) 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 @@ function &get($params = null, $filename = null, $skip_head = false) * @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 @@ function &headers($xtra_headers = null, $overwrite = false, $skip_content = fals // 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 @@ function &headers($xtra_headers = null, $overwrite = false, $skip_content = fals } 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 @@ function &headers($xtra_headers = null, $overwrite = false, $skip_content = fals * @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 @@ function txtHeaders($xtra_headers = null, $overwrite = false, $skip_content = fa } $ret = ''; - $eol = $this->_build_params['eol']; + $eol = $this->build_params['eol']; foreach ($headers as $key => $val) { if (is_array($val)) { @@ -1126,31 +1151,28 @@ function txtHeaders($xtra_headers = null, $overwrite = false, $skip_content = fa /** * 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 @@ function setContentType($type, $params = array()) } // 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 @@ function setContentType($type, $params = array()) * @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 @@ function setSubject($subject) * @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 @@ function setFrom($email) * @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 @@ function addTo($email) * @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 @@ function addCc($email) * @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 @@ function addBcc($email) * 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 @@ function encodeRecipients($recipients) * @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 @@ function _encodeHeaders($input, $params = array()) $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 @@ function _encodeHeaders($input, $params = array()) * @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 @@ function _basename($filename) * 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 @@ function _contentHeaders() $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 @@ function _contentHeaders() 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 @@ function _contentHeaders() * 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/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mimeDecode.php b/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mimeDecode.php index 4cf1ad7a59f..1a12673c8ab 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mimeDecode.php +++ b/profiles/civicrm_starterkit/modules/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 * @copyright 2003-2006 PEAR * @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 @@ function __construct($input) $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 @@ function __construct($input) * 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 @@ function __construct($input) 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 @@ function decode($params = null) $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 @@ function _decode($headers, $body, $default_ctype = 'text/plain') $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 @@ function _decode($headers, $body, $default_ctype = 'text/plain') } } - 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 @@ function _decode($headers, $body, $default_ctype = 'text/plain') } 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 @@ function _decode($headers, $body, $default_ctype = 'text/plain') $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 @@ function _decode($headers, $body, $default_ctype = 'text/plain') $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 @@ function _decode($headers, $body, $default_ctype = 'text/plain') 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 @@ function _decode($headers, $body, $default_ctype = 'text/plain') 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 @@ function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prep } 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 @@ function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prep $structure->mime_id = $prepend . $mime_number; $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; } - + return $return; } @@ -417,6 +442,11 @@ function _splitBodyHeader($input) 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 @@ function _parseHeaders($input) 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 @@ function _parseHeaders($input) * 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 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 @@ function _parseHeaderValue($input) * @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 @@ function _boundarySplit($input, $boundary) 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 @@ function _boundarySplit($input, $boundary) * @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 @@ function _decodeHeader($input) $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 @@ function _quotedPrintableDecode($input) $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 @@ function _quotedPrintableDecode($input) * 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 @@ function &uudecode($input) /** * 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 @@ function getSendArray() } $to = substr($to,1); return array($to,$header,$this->_body); - } + } /** * Returns a xml copy of the output of diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mimePart.php b/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mimePart.php index dcf3de53c4e..187b1652daa 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/Mail/mimePart.php +++ b/profiles/civicrm_starterkit/modules/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 @@ -48,10 +48,16 @@ * @author Aleksander Machniak * @copyright 2003-2006 PEAR * @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 : + * 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 @@ function __construct($body = '', $params = array()) $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 @@ function __construct($body = '', $params = array()) * @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 @@ function encode($boundary=null) } $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 @@ function encode($boundary=null) } // 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 @@ function encodeToFile($filename, $boundary=null, $skip_head=false) @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 @@ function encodeToFile($filename, $boundary=null, $skip_head=false) * @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 @@ function _encodePartToFile($fh, $boundary=null, $skip_head=false) $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 @@ function _encodePartToFile($fh, $boundary=null, $skip_head=false) * 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 @@ function &addSubpart($body, $params) * @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 @@ function _getEncodedData($data, $encoding) * 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 @@ function _getEncodedDataFromFile($filename, $encoding, $fh=null) 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 @@ function _getEncodedDataFromFile($filename, $encoding, $fh=null) // 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 @@ function _getEncodedDataFromFile($filename, $encoding, $fh=null) * @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 @@ function _quotedPrintableEncode($input , $line_max = 76) $escape = '='; $output = ''; - while (list($idx, $line) = each($lines)) { + foreach ($lines as $idx => $line) { $newline = ''; $i = 0; @@ -626,18 +656,22 @@ function _quotedPrintableEncode($input , $line_max = 76) $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 @@ function _quotedPrintableEncode($input , $line_max = 76) * @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 @@ function _buildHeaderParam($name, $value, $charset=null, $language=null, // 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 @@ function _buildHeaderParam($name, $value, $charset=null, $language=null, $headCount++; } - $headers = implode(';' . $this->_eol, $headers); + $headers = implode(';' . $this->eol, $headers); return $headers; } @@ -723,10 +755,9 @@ function _buildHeaderParam($name, $value, $charset=null, $language=null, * @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 @@ function _buildRFC2047Param($name, $value, $charset, $_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 @@ function _buildRFC2047Param($name, $value, $charset, $_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 @@ function _buildRFC2047Param($name, $value, $charset, * @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 @@ static function encodeHeader($name, $value, $charset='ISO-8859-1', $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 @@ static function encodeHeader($name, $value, $charset='ISO-8859-1', 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 @@ static function encodeHeader($name, $value, $charset='ISO-8859-1', $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 @@ static function encodeHeader($name, $value, $charset='ISO-8859-1', $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 @@ static function encodeHeader($name, $value, $charset='ISO-8859-1', * @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 @@ static function _explodeQuotedString($delimiter, $string) * @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 @@ static function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $e $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 @@ static function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $e } // 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 @@ static function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $e * * @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 @@ function encodeQP($str) // "=", "_", "?" 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 @@ function encodeQP($str) * @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 @@ static function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n") $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 @@ static function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n") * * @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 @@ static function _qpReplaceCallback($matches) * * @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/profiles/civicrm_starterkit/modules/civicrm/packages/jquery/plugins/jquery.menu.js b/profiles/civicrm_starterkit/modules/civicrm/packages/jquery/plugins/jquery.menu.js index 1e142929391..83b78696a31 100755 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/jquery/plugins/jquery.menu.js +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/jquery/plugins/jquery.menu.js @@ -22,7 +22,6 @@ menuDIVElement = $('')[0], menuULElement = $('')[0], menuItemElement = $('
  • ')[0], - arrowElement = $('')[0], $rootDiv = $('
    '), //create main menu div defaults = { // $.Menu options @@ -37,7 +36,7 @@ // $.MenuItem options onClick: null, - arrowSrc: null, + arrowClass: null, addExpando: false, // $.fn.menuFromElement options @@ -132,7 +131,7 @@ t = t.parentNode; //is the found node one of the visible menu elements? - if ( !$(visibleMenus).filter(function(){ return this.$eDIV[0] == t }).length ) + if ( !$(visibleMenus).filter(function(){ return this.$eDIV[0] == t; }).length ) { $.Menu.closeAll(); } @@ -795,11 +794,9 @@ this.subMenu = menu; if ( this.parentMenu && $.inArray(menu, this.parentMenu.subMenus) == -1 ) this.parentMenu.subMenus.push(menu); - if ( this.settings.arrowSrc ) - { - var a = arrowElement.cloneNode(0); - a.setAttribute('src', this.settings.arrowSrc); - this.$eLI[0].firstChild.appendChild(a); + if ( this.settings.arrowClass ) { + var a = $('').addClass(this.settings.arrowClass); + this.$eLI[0].firstChild.appendChild(a[0]); } } } diff --git a/profiles/civicrm_starterkit/modules/civicrm/packages/jquery/plugins/jquery.menu.min.js b/profiles/civicrm_starterkit/modules/civicrm/packages/jquery/plugins/jquery.menu.min.js index d813ee63f43..a231f541c3b 100644 --- a/profiles/civicrm_starterkit/modules/civicrm/packages/jquery/plugins/jquery.menu.min.js +++ b/profiles/civicrm_starterkit/modules/civicrm/packages/jquery/plugins/jquery.menu.min.js @@ -1 +1 @@ -(function(e){var t=[],n=[],r=activeItem=null,i=e('')[0],s=e('')[0],o=e('
  • ')[0],u=e('')[0],a=e('
    '),f={showDelay:200,hideDelay:200,hoverOpenDelay:0,offsetTop:0,offsetLeft:0,minWidth:0,onOpen:null,onClose:null,onClick:null,arrowSrc:null,addExpando:false,copyClassAttr:false};e(function(){a.appendTo("body")});e.extend({MenuCollection:function(e){this.menus=[];this.init(e)}});e.extend(e.MenuCollection,{prototype:{init:function(e){if(e&&e.length){for(var t=0;t-1){if(--o<0)o=u.length-1;e.Menu.closeAll();u[o].show();u[o].setActive();if(u[o].menuItems.length)u[o].menuItems[0].hoverIn(true)}}break;case 38:if(r)r.selectNextItem(-1);break;case 39:if(!r)r=n[0];var a,i=r,f=activeItem?activeItem.subMenu:null;if(i){if(f&&f.menuItems.length){f.show();f.menuItems[0].hoverIn()}else if(i=i.inMenuCollection()){var o,u=i.menuCollection.menus;if((o=e.inArray(i,u))>-1){if(++o>=u.length)o=0;e.Menu.closeAll();u[o].show();u[o].setActive();if(u[o].menuItems.length)u[o].menuItems[0].hoverIn(true)}}}break;case 40:if(!r){if(n.length&&n[0].menuItems.length)n[0].menuItems[0].hoverIn()}else r.selectNextItem();break}if(t.keyCode>36&&t.keyCode<41)return false},closeAll:function(){while(n.length)n[0].hide()},setDefaults:function(t){e.extend(f,t)},prototype:{init:function(){var n=this;if(!this.target)return;else if(this.target instanceof e.MenuItem){this.parentMenuItem=this.target;this.target.addSubMenu(this);this.target=this.target.$eLI}t.push(this);this.$eDIV=e(i.cloneNode(1));this.$eUL=e(s.cloneNode(1));this.$eDIV[0].appendChild(this.$eUL[0]);a[0].appendChild(this.$eDIV[0]);if(!this.parentMenuItem){e(this.target).click(function(e){n.onClick(e)}).hover(function(e){n.setActive();if(n.settings.hoverOpenDelay){n.openTimer=setTimeout(function(){if(!n.visible)n.onClick(e)},n.settings.hoverOpenDelay)}},function(){if(!n.visible)e(this).removeClass("activetarget");if(n.openTimer)clearTimeout(n.openTimer)})}else{this.$eDIV.hover(function(){n.setActive()},function(){})}},setActive:function(){if(!this.parentMenuItem)e(this.target).addClass("activetarget");else this.active=true},addItem:function(t){if(t instanceof e.MenuItem){if(e.inArray(t,this.menuItems)==-1){this.$eUL.append(t.$eLI);this.menuItems.push(t);t.parentMenu=this;if(t.subMenu)this.subMenus.push(t.subMenu)}}else{this.addItem(new e.MenuItem(t,this.settings))}},addItems:function(e){for(var t=0;t-1)this.menuItems.splice(n,1);t.parentMenu=null},hide:function(){if(!this.visible)return;var t,i=e.inArray(this,n);this.$eDIV.hide();if(i>=0)n.splice(i,1);this.visible=this.active=false;e(this.target).removeClass("activetarget");for(t=0;t=r)n=0;else if(n<0)n=r-1}while(this.menuItems[n].separator);this.menuItems[n].hoverIn(true)},inMenuCollection:function(){var e=this;while(e.parentMenuItem)e=e.parentMenuItem.parentMenu;return e.menuCollection?e:null},destroy:function(){var n,r;this.hide();if(!this.parentMenuItem)e(this.target).unbind("click").unbind("mouseover").unbind("mouseout");else this.$eDIV.unbind("mouseover").unbind("mouseout");while(this.menuItems.length){r=this.menuItems[0];r.destroy();delete r}if((n=e.inArray(this,t))>-1)t.splice(n,1);if(this.menuCollection){if((n=e.inArray(this,this.menuCollection.menus))>-1)this.menuCollection.menus.splice(n,1)}this.$eDIV.remove()}}});e.extend({MenuItem:function(t,n){if(typeof t=="string")t={src:t};this.src=t.src||"";this.url=t.url||null;this.urlTarget=t.target||null;this.addClass=t.addClass||null;this.data=t.data||null;this.$eLI=null;this.parentMenu=null;this.subMenu=null;this.settings=e.extend({},f,n);this.active=false;this.enabled=true;this.separator=false;this.init();if(t.subMenu)new e.Menu(this,t.subMenu,n)}});e.extend(e.MenuItem,{prototype:{init:function(){var t,n,r=this.src,i=this;this.$eLI=e(o.cloneNode(1));if(this.addClass)this.$eLI[0].setAttribute("class",this.addClass);if(this.settings.addExpando&&this.data)this.$eLI[0].menuData=this.data;if(r==""){this.$eLI.addClass("menu-separator");this.separator=true}else{n=typeof r=="string";if(n&&this.url)r=e('"+r+"");else if(n||!r.length)r=[r];for(t=0;t