!/ext/greenwich
/ext/greenwich/dist
/ext/greenwich/extern
+!/ext/oauth-client
!/ext/search
!/ext/financialacls
+!/ext/contributioncancelactions
backdrop/
bower_components
CRM/Case/xml/configuration
if (empty($ids) && !empty($includedGroups) &&
is_array($includedGroups)
) {
+ // This is pretty alarming - we 'sometimes' include all included groups
+ // seems problematic per https://lab.civicrm.org/dev/core/-/issues/1879
$ids = $includedGroups;
}
if ($contactID) {
*/
class CRM_Activity_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Activity';
+
/**
* Class constructor.
*
// Add all the actions.
$this->addActions();
+ $this->set('entity', $this->entity);
}
/**
*
* Generated from xml/schema/CRM/Activity/Activity.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:c1b4cc908c0220abf69f57d281eeda95)
+ * (GenCodeChecksum:af42b47e95e8c7f47eb7a3ef53833383)
*/
/**
*/
public static $_log = TRUE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/activity?reset=1&action=add&context=standalone',
+ 'view' => 'civicrm/activity?reset=1&action=view&id=[id]',
+ 'update' => 'civicrm/activity/add?reset=1&action=update&id=[id]',
+ 'delete' => 'civicrm/activity?reset=1&action=delete&id=[id]',
+ ];
+
/**
* Unique Other Activity ID
*
return FALSE;
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return 'civicrm_activity';
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ return 'activity_id';
+ }
+
}
$form->_task = $values['task'];
$ids = [];
- if ($values['radio_ts'] == 'ts_sel') {
- foreach ($values 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);
- }
- }
+ if ($values['radio_ts'] === 'ts_sel') {
+ $ids = $form->getSelectedIDs($values);
}
else {
$queryParams = $form->get('queryParams');
}
$form->_activityHolderIds = $form->_componentIds = $ids;
-
- // Set the context for redirection for any task actions.
- $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form);
- $urlParams = 'force=1';
- if (CRM_Utils_Rule::qfKey($qfKey)) {
- $urlParams .= "&qfKey=$qfKey";
- }
-
- $session = CRM_Core_Session::singleton();
- $searchFormName = strtolower($form->get('searchFormName'));
- if ($searchFormName == 'search') {
- $session->replaceUserContext(CRM_Utils_System::url('civicrm/activity/search', $urlParams));
- }
- else {
- $session->replaceUserContext(CRM_Utils_System::url("civicrm/contact/search/$searchFormName",
- $urlParams
- ));
- }
+ $form->setNextUrl('activity');
}
/**
*/
class CRM_Admin_Form_MailSettings extends CRM_Admin_Form {
+ protected $_testButtonName;
+
/**
* Build the form object.
*/
return;
}
+ $this->_testButtonName = $this->getButtonName('refresh', 'test');
+ $buttons = $this->getElement('buttons')->getElements();
+ $buttons[] = $this->createElement(
+ 'xbutton',
+ $this->_testButtonName,
+ CRM_Core_Page::crmIcon('fa-chain') . ' ' . ts('Save & Test'),
+ ['type' => 'submit', 'class' => 'crm-button']
+ );
+ $this->getElement('buttons')->setElements($buttons);
+
$this->applyFilter('__ALL__', 'trim');
//get the attributes.
* Add local and global form rules.
*/
public function addRules() {
- $this->addFormRule(['CRM_Admin_Form_MailSettings', 'formRule']);
+ $this->addFormRule(['CRM_Admin_Form_MailSettings', 'formRule'], $this);
}
public function getDefaultEntity() {
*
* @param array $fields
* Posted values of the form.
+ * @param array $files
+ * Not used here.
+ * @param CRM_Core_Form $form
+ * This form.
*
* @return array
* list of errors to be posted back to the form
*/
- public static function formRule($fields) {
+ public static function formRule($fields, $files, $form) {
$errors = [];
- // Check for default from email address and organization (domain) name. Force them to change it.
- if ($fields['domain'] == 'EXAMPLE.ORG') {
- $errors['domain'] = ts('Please enter a valid domain for this mailbox account (the part after @).');
+ if ($form->_action != CRM_Core_Action::DELETE) {
+ // Check for default from email address and organization (domain) name. Force them to change it.
+ if ($fields['domain'] == 'EXAMPLE.ORG') {
+ $errors['domain'] = ts('Please enter a valid domain for this mailbox account (the part after @).');
+ }
}
return empty($errors) ? TRUE : $errors;
else {
CRM_Core_Session::setStatus("", ts('Changes Not Saved.'), "info");
}
+
+ if ($this->controller->getButtonName() == $this->_testButtonName) {
+ $test = civicrm_api4('MailSettings', 'testConnection', [
+ 'where' => [['id', '=', $mailSettings->id]],
+ ])->single();
+ CRM_Core_Session::setStatus($test['details'], $test['title'],
+ $test['error'] ? 'error' : 'success');
+ }
}
}
public function postProcess() {
if ($this->_action & CRM_Core_Action::DELETE) {
CRM_Core_BAO_MessageTemplate::del($this->_id);
+
+ $this->postProcessHook();
}
elseif ($this->_action & CRM_Core_Action::VIEW) {
// currently, the above action is used solely for previewing default workflow templates
}
$messageTemplate = MessageTemplate::save()->setDefaults($params)->setRecords([['id' => $this->_id]])->execute()->first();
+
+ // set the id on save, so it can be used in a extension using the posProcess hook
+ $this->_id = $messageTemplate['id'];
+ $this->postProcessHook();
+
CRM_Core_Session::setStatus(ts('The Message Template \'%1\' has been saved.', [1 => $messageTemplate['msg_title']]), ts('Saved'), 'success');
if (isset($this->_submitValues['_qf_MessageTemplates_upload'])) {
}
$this->assign('rows', $allMailSettings);
+
+ $setupActions = CRM_Core_BAO_MailSettings::getSetupActions();
+ if (count($setupActions) > 1 || !isset($setupActions['standard'])) {
+ $this->assign('setupActions', $setupActions);
+ }
}
/**
$this->processContribution($params);
}
elseif ($this->_batchInfo['type_id'] == $batchTypes['Membership']) {
- $this->processMembership($params);
+ $params['actualBatchTotal'] = $this->processMembership($params);
}
// update batch to close status
//process premiums
if (!empty($value['product_name'])) {
if ($value['product_name'][0] > 0) {
- list($products, $options) = CRM_Contribute_BAO_Premium::getPremiumProductInfo();
+ [$products, $options] = CRM_Contribute_BAO_Premium::getPremiumProductInfo();
$value['hidden_Premium'] = 1;
$value['product_option'] = CRM_Utils_Array::value(
* Process membership records.
*
* @param array $params
- * Associated array of submitted values.
+ * Array of submitted values.
*
+ * @return float
+ * batch total monetary amount.
*
- * @return bool
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
*/
- private function processMembership(&$params) {
-
+ private function processMembership(array $params) {
+ $batchTotal = 0;
// get the price set associated with offline membership
$priceSetId = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', 'default_membership_type_amount', 'id', 'name');
$this->_priceSet = $priceSets = current(CRM_Price_BAO_PriceSet::getSetDetail($priceSetId));
$value['total_amount'] = (float) $value['total_amount'];
}
- $params['actualBatchTotal'] += $value['total_amount'];
+ $batchTotal += $value['total_amount'];
unset($value['financial_type']);
unset($value['payment_instrument']);
$this->_params = $params;
$value['is_renew'] = TRUE;
$isPayLater = $params['is_pay_later'] ?? NULL;
- $campaignId = NULL;
- if (isset($this->_values) && is_array($this->_values) && !empty($this->_values)) {
- $campaignId = $this->_params['campaign_id'] ?? NULL;
- if (!array_key_exists('campaign_id', $this->_params)) {
- $campaignId = $this->_values['campaign_id'] ?? NULL;
- }
- }
$formDates = [
'end_date' => $value['membership_end_date'] ?? NULL,
$value['contact_id'], $value['membership_type_id'], FALSE,
//$numTerms should be default to 1.
NULL, NULL, $value['custom'], 1, NULL, FALSE,
- NULL, $membershipSource, $isPayLater, $campaignId, $formDates
+ NULL, $membershipSource, $isPayLater, ['campaign_id' => $value['member_campaign_id'] ?? NULL], $formDates
);
// make contribution entry
CRM_Member_BAO_Membership::recordMembershipContribution($contrbutionParams);
}
else {
- $dateTypes = [
- 'membership_join_date' => 'joinDate',
- 'membership_start_date' => 'startDate',
- 'membership_end_date' => 'endDate',
- ];
-
- $dates = [
- 'join_date',
- 'start_date',
- 'end_date',
- 'reminder_date',
- ];
- foreach ($dateTypes as $dateField => $dateVariable) {
- $$dateVariable = CRM_Utils_Date::processDate($value[$dateField]);
- $fDate[$dateField] = $value[$dateField] ?? NULL;
- }
-
- $calcDates = [];
- $calcDates[$membershipTypeId] = CRM_Member_BAO_MembershipType::getDatesForMembershipType($membershipTypeId,
- $joinDate, $startDate, $endDate
+ $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($membershipTypeId,
+ $value['membership_join_date'] ?? NULL, $value['membership_start_date'] ?? NULL, $value['membership_end_date'] ?? NULL
);
-
- foreach ($calcDates as $memType => $calcDate) {
- foreach ($dates as $d) {
- //first give priority to form values then calDates.
- $date = $value[$d] ?? NULL;
- if (!$date) {
- $date = $calcDate[$d] ?? NULL;
- }
-
- $value[$d] = CRM_Utils_Date::processDate($date);
- }
- }
+ $value['join_date'] = $value['membership_join_date'] ?? $calcDates['join_date'];
+ $value['start_date'] = $value['membership_start_date'] ?? $calcDates['start_date'];
+ $value['end_date'] = $value['membership_end_date'] ?? $calcDates['end_date'];
unset($value['membership_start_date']);
unset($value['membership_end_date']);
- $ids = [];
- // @todo stop passing empty $ids
- $membership = CRM_Member_BAO_Membership::create($value, $ids);
+ $membership = CRM_Member_BAO_Membership::create($value);
}
//process premiums
}
}
}
- return TRUE;
+ return $batchTotal;
}
/**
*/
class CRM_Campaign_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Campaign';
+
/**
* Class constructor.
*
$this->addPages($this->_stateMachine, $action);
// add all the actions
- $config = CRM_Core_Config::singleton();
$this->addActions();
+ $this->set('entity', $this->entity);
}
}
*
* Generated from xml/schema/CRM/Campaign/Campaign.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:a5a49e13e66a5d32b690835a49baf535)
+ * (GenCodeChecksum:1356a7073a6caa15e0da58422c6763b9)
*/
/**
*/
public static $_log = FALSE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/campaign/add?reset=1',
+ 'update' => 'civicrm/campaign/add?reset=1&action=update&id=[id]',
+ 'delete' => 'civicrm/campaign/add?reset=1&action=delete&id=[id]',
+ ];
+
/**
* Unique Campaign ID.
*
$this->_voterIds = $this->_contactIds = $this->_componentIds = $ids;
$this->assign('totalSelectedContacts', count($this->_contactIds));
-
- //set the context for redirection for any task actions
- $session = CRM_Core_Session::singleton();
- $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $this);
- $urlParams = 'force=1';
- if (CRM_Utils_Rule::qfKey($qfKey)) {
- $urlParams .= '&qfKey=' . $qfKey;
- }
- $session->replaceUserContext(CRM_Utils_System::url('civicrm/survey/search', $urlParams));
+ $this->setNextUrl('survey');
}
/**
$selectSQL = "
SELECT %1, contact_a.id, contact_a.display_name
-FROM {$sql['from']}
+{$sql['from']}
";
try {
*/
class CRM_Case_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Case';
+
/**
* Class constructor.
*
$this->addPages($this->_stateMachine, $action);
// add all the actions
- $config = CRM_Core_Config::singleton();
$this->addActions();
+ $this->set('entity', $this->entity);
}
}
CRM_Contact_BAO_Individual::format($params, $contact);
}
- if (strlen($contact->display_name) > 128) {
- $contact->display_name = substr($contact->display_name, 0, 128);
- }
- if (strlen($contact->sort_name) > 128) {
- $contact->sort_name = substr($contact->sort_name, 0, 128);
+ // Note that copyValues() above might already call this, via
+ // CRM_Utils_String::ellipsify(), but e.g. for Individual it gets put
+ // back or altered by Individual::format() just above, so we need to
+ // check again.
+ // Note also orgs will get ellipsified, but if we do that here then
+ // some existing tests on individual fail.
+ // Also api v3 will enforce org naming length by failing, v4 will truncate.
+ if (mb_strlen($contact->display_name, 'UTF-8') > 128) {
+ $contact->display_name = mb_substr($contact->display_name, 0, 128, 'UTF-8');
+ }
+ if (mb_strlen($contact->sort_name, 'UTF-8') > 128) {
+ $contact->sort_name = mb_substr($contact->sort_name, 0, 128, 'UTF-8');
}
$privacy = $params['privacy'] ?? NULL;
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Contact_BAO_DashboardContact extends CRM_Contact_DAO_DashboardContact {
+
+ /**
+ * @param array $record
+ * @return CRM_Contact_DAO_DashboardContact
+ * @throws CRM_Core_Exception
+ */
+ public static function writeRecord(array $record) {
+ self::checkEditPermission($record);
+ return parent::writeRecord($record);
+ }
+
+ /**
+ * @param array $record
+ * @return CRM_Contact_DAO_DashboardContact
+ * @throws CRM_Core_Exception
+ */
+ public static function deleteRecord(array $record) {
+ self::checkEditPermission($record);
+ return parent::deleteRecord($record);
+ }
+
+ /**
+ * Ensure that the current user has permission to create/edit/delete a DashboardContact record
+ *
+ * @param array $record
+ * @throws CRM_Core_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ public static function checkEditPermission(array $record) {
+ if (!empty($record['check_permissions']) && !CRM_Core_Permission::check('administer CiviCRM')) {
+ $cid = !empty($record['id']) ? self::getFieldValue(parent::class, $record['id'], 'contact_id') : $record['contact_id'];
+ if ($cid != CRM_Core_Session::getLoggedInContactID()) {
+ throw new \Civi\API\Exception\UnauthorizedException('You do not have permission to edit the dashboard for this contact.');
+ }
+ }
+ }
+
}
}
/**
- * Defines a new smart group.
+ * Takes a sloppy mismash of params and creates two entities: a Group and a SavedSearch
+ * Currently only used by unit tests.
*
* @param array $params
- * Associative array of parameters.
- *
* @return CRM_Contact_BAO_Group|NULL
- * The new group BAO (if created)
+ * @deprecated
*/
- public static function createSmartGroup(&$params) {
+ public static function createSmartGroup($params) {
if (!empty($params['formValues'])) {
$ssParams = $params;
- unset($ssParams['id']);
- if (isset($ssParams['saved_search_id'])) {
- $ssParams['id'] = $ssParams['saved_search_id'];
+ // Remove group parameters from sloppy mismash
+ unset($ssParams['id'], $ssParams['name'], $ssParams['title'], $ssParams['formValues'], $ssParams['saved_search_id']);
+ if (isset($params['saved_search_id'])) {
+ $ssParams['id'] = $params['saved_search_id'];
}
- $params['form_values'] = $params['formValues'];
- $savedSearch = CRM_Contact_BAO_SavedSearch::create($params);
+ $ssParams['form_values'] = $params['formValues'];
+ $savedSearch = CRM_Contact_BAO_SavedSearch::create($ssParams);
$params['saved_search_id'] = $savedSearch->id;
}
* @param string $parents
* @param string $spacer
* @param bool $titleOnly
+ * @param bool $public
*
* @return array
*/
$groupIDs,
$parents = NULL,
$spacer = '<span class="child-indent"></span>',
- $titleOnly = FALSE
+ $titleOnly = FALSE,
+ $public = FALSE
) {
if (empty($groupIDs)) {
return [];
$groups = [];
$args = [1 => [$groupIdString, 'String']];
$query = "
-SELECT id, title, description, visibility, parents, saved_search_id
+SELECT id, title, frontend_title, description, frontend_description, visibility, parents, saved_search_id
FROM civicrm_group
WHERE id IN $groupIdString
";
$roots = [];
$tree = [];
while ($dao->fetch()) {
+ $title = $dao->title;
+ $description = $dao->description;
+ if ($public) {
+ if (!empty($dao->frontend_title)) {
+ $title = $dao->frontend_title;
+ }
+ if (!empty($dao->frontend_description)) {
+ $description = $dao->frontend_description;
+ }
+ }
if ($dao->parents) {
$parentArray = explode(',', $dao->parents);
$parent = self::filterActiveGroups($parentArray);
$tree[$parent][] = [
'id' => $dao->id,
- 'title' => empty($dao->saved_search_id) ? $dao->title : '* ' . $dao->title,
+ 'title' => empty($dao->saved_search_id) ? $title : '* ' . $title,
'visibility' => $dao->visibility,
- 'description' => $dao->description,
+ 'description' => $description,
];
}
else {
$roots[] = [
'id' => $dao->id,
- 'title' => empty($dao->saved_search_id) ? $dao->title : '* ' . $dao->title,
+ 'title' => empty($dao->saved_search_id) ? $title : '* ' . $title,
'visibility' => $dao->visibility,
- 'description' => $dao->description,
+ 'description' => $description,
];
}
}
*
* @param bool $includeSmartGroups
* Include or Exclude Smart Group(s)
+ * @param bool $public
+ * Are we returning groups for use on a public page.
*
* @return array|int
* the relevant data object values for the contact or the total count when $count is TRUE
$onlyPublicGroups = FALSE,
$excludeHidden = TRUE,
$groupId = NULL,
- $includeSmartGroups = FALSE
+ $includeSmartGroups = FALSE,
+ $public = FALSE
) {
if ($count) {
$select = 'SELECT count(DISTINCT civicrm_group_contact.id)';
$select = 'SELECT
civicrm_group_contact.id as civicrm_group_contact_id,
civicrm_group.title as group_title,
+ civicrm_group.frontend_title as group_public_title,
civicrm_group.visibility as visibility,
civicrm_group_contact.status as status,
civicrm_group.id as group_id,
$id = $dao->civicrm_group_contact_id;
$values[$id]['id'] = $id;
$values[$id]['group_id'] = $dao->group_id;
- $values[$id]['title'] = $dao->group_title;
+ $values[$id]['title'] = ($public && !empty($group->group_public_title) ? $group->group_public_title : $dao->group_title);
$values[$id]['visibility'] = $dao->visibility;
$values[$id]['is_hidden'] = $dao->is_hidden;
switch ($dao->status) {
public static function mergeRelationships($mainId, $otherId, &$sqls) {
// Delete circular relationships
$sqls[] = "DELETE FROM civicrm_relationship
- WHERE (contact_id_a = $mainId AND contact_id_b = $otherId)
- OR (contact_id_b = $mainId AND contact_id_a = $otherId)";
+ WHERE (contact_id_a = $mainId AND contact_id_b = $otherId AND case_id IS NULL)
+ OR (contact_id_b = $mainId AND contact_id_a = $otherId AND case_id IS NULL)";
// Delete relationship from other contact if main contact already has that relationship
$sqls[] = "DELETE r2
FROM civicrm_relationship r1, civicrm_relationship r2
WHERE r1.relationship_type_id = r2.relationship_type_id
AND r1.id <> r2.id
+ AND r1.case_id IS NULL AND r2.case_id IS NULL
AND (
r1.contact_id_a = $mainId AND r2.contact_id_a = $otherId AND r1.contact_id_b = r2.contact_id_b
OR r1.contact_id_b = $mainId AND r2.contact_id_b = $otherId AND r1.contact_id_a = r2.contact_id_a
AND is_current_member = 1";
$result = CRM_Core_DAO::singleValueQuery($query);
if ($result < CRM_Utils_Array::value('max_related', $membershipValues, PHP_INT_MAX)) {
- CRM_Member_BAO_Membership::create($membershipValues);
+ civicrm_api3('Membership', 'create', $membershipValues);
}
return $membershipValues;
}
*/
public static function onHookTriggerInfo($e) {
$relUpdates = self::createInsertUpdateQueries();
+ // Use utf8mb4_bin or utf8_bin, depending on what's in use.
+ $collation = preg_replace('/^(utf8(?:mb4)?)_.*$/', '$1_bin', CRM_Core_BAO_SchemaHandler::getInUseCollation());
+
foreach ($relUpdates as $relUpdate) {
/**
* This trigger runs whenever a "civicrm_relationship" record is inserted or updated.
'sql' => sprintf("\nIF (%s) THEN\n %s;\n END IF;\n",
// Condition
- implode(' OR ', array_map(function ($col) {
- return "(OLD.$col != NEW.$col COLLATE utf8_bin)";
+ implode(' OR ', array_map(function ($col) use ($collation) {
+ return "(OLD.$col != NEW.$col COLLATE $collation)";
}, self::$relTypeWatchFields)),
// Action
}
/**
- * Create a smart group from normalised values.
+ * Create or update SavedSearch record.
*
* @param array $params
*
* @return \CRM_Contact_DAO_SavedSearch
*/
public static function create(&$params) {
- $savedSearch = new CRM_Contact_DAO_SavedSearch();
- $savedSearch->copyValues($params);
- $savedSearch->save();
+ // Auto-create unique name from label if supplied
+ if (empty($params['id']) && empty($params['name']) && !empty($params['label'])) {
+ $name = CRM_Utils_String::munge($params['label']);
+ $existing = Civi\Api4\SavedSearch::get(FALSE)
+ ->addWhere('name', 'LIKE', $name . '%')
+ ->addSelect('name')
+ ->execute()->column('name');
+ $suffix = '';
+ while (in_array($name . $suffix, $existing)) {
+ $suffix = '_' . (1 + str_replace('_', '', $suffix));
+ }
+ $params['name'] = $name . $suffix;
+ }
- return $savedSearch;
+ return self::writeRecord($params);
}
/**
$savedSearch = self::retrieve(['id' => $id]);
// APIv4 search
if (!empty($savedSearch->api_entity)) {
- $groupName = self::getName($id);
- return CRM_Utils_System::url('civicrm/search', NULL, FALSE, "/load/Group/$groupName");
+ return CRM_Utils_System::url('civicrm/admin/search', NULL, FALSE, "/edit/$id");
}
// Classic search builder
if (!empty($savedSearch->mapping_id)) {
*/
class CRM_Contact_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Contact';
+
/**
* Class constructor.
*
// add all the actions
$this->addActions();
+ $this->set('entity', $this->entity);
}
/**
*
* Generated from xml/schema/CRM/Contact/Contact.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:f118596cceae71668861504b7316afa7)
+ * (GenCodeChecksum:25d4fcd3814380d746574c9a17b51134)
*/
/**
*/
public static $_log = TRUE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/contact/add?reset=1&ct=[contact_type]',
+ 'view' => 'civicrm/contact/view?reset=1&cid=[id]',
+ 'update' => 'civicrm/contact/add?reset=1&action=update&cid=[id]',
+ 'delete' => 'civicrm/contact/view/delete?reset=1&delete=1&cid=[id]',
+ ];
+
/**
* Unique Contact ID
*
*
* Generated from xml/schema/CRM/Contact/Group.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:25bcea958ed3b88317a2bbb89169b0a6)
+ * (GenCodeChecksum:e40e779ac8e9bec5ea4d6c55f6f3b863)
*/
/**
*/
public static $_log = TRUE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/group/add?reset=1',
+ ];
+
/**
* Group ID
*
'type' => CRM_Utils_Type::T_STRING,
'title' => ts('Group Title'),
'description' => ts('Name of Group.'),
- 'required' => TRUE,
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'where' => 'civicrm_group.title',
*
* Generated from xml/schema/CRM/Contact/SavedSearch.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:d863f8b0b8659633bc84578e1d6cbf10)
+ * (GenCodeChecksum:2a23a737d07cbfc49ce1d60a642fee3e)
*/
/**
*/
public $id;
+ /**
+ * Unique name of saved search
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Administrative label for search
+ *
+ * @var string
+ */
+ public $label;
+
/**
* Submitted form values for this search
*
'localizable' => 0,
'add' => '1.1',
],
+ 'name' => [
+ 'name' => 'name',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => ts('Saved Search Name'),
+ 'description' => ts('Unique name of saved search'),
+ 'maxlength' => 255,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_saved_search.name',
+ 'default' => 'NULL',
+ 'table_name' => 'civicrm_saved_search',
+ 'entity' => 'SavedSearch',
+ 'bao' => 'CRM_Contact_BAO_SavedSearch',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Text',
+ ],
+ 'add' => '1.0',
+ ],
+ 'label' => [
+ 'name' => 'label',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => ts('Saved Search Label'),
+ 'description' => ts('Administrative label for search'),
+ 'maxlength' => 255,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_saved_search.label',
+ 'default' => 'NULL',
+ 'table_name' => 'civicrm_saved_search',
+ 'entity' => 'SavedSearch',
+ 'bao' => 'CRM_Contact_BAO_SavedSearch',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Text',
+ ],
+ 'add' => '5.32',
+ ],
'form_values' => [
'name' => 'form_values',
'type' => CRM_Utils_Type::T_TEXT,
* @return array
*/
public static function indices($localize = TRUE) {
- $indices = [];
+ $indices = [
+ 'UI_name' => [
+ 'name' => 'UI_name',
+ 'field' => [
+ 0 => 'name',
+ ],
+ 'localizable' => FALSE,
+ 'unique' => TRUE,
+ 'sig' => 'civicrm_saved_search::1::name',
+ ],
+ ];
return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
}
return TRUE;
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return 'civicrm_contact';
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ return 'contact_id';
+ }
+
}
$name = 'name';
}
- $params = ['entity' => 'address'];
+ $params = ['entity' => 'Address'];
if ($name === 'postal_code_suffix') {
$params['label'] = ts('Suffix');
* If used for building tag block.
* @param string $fieldName
* This is used in batch profile(i.e to build multiple blocks).
- *
* @param string $groupElementType
- *
+ * The html type of the element we are adding e.g. checkbox, select
+ * @param bool $public
+ * Is this being used in a public form e.g. Profile.
*/
public static function buildQuickForm(
&$form,
$groupName = 'Group(s)',
$tagName = 'Tag(s)',
$fieldName = NULL,
- $groupElementType = 'checkbox'
+ $groupElementType = 'checkbox',
+ $public = FALSE
) {
if (!isset($form->_tagGroup)) {
$form->_tagGroup = [];
}
if ($groupID || !empty($group)) {
- $groups = CRM_Contact_BAO_Group::getGroupsHierarchy($ids, NULL, '- ');
+ $groups = CRM_Contact_BAO_Group::getGroupsHierarchy($ids, NULL, '- ', FALSE, $public);
$attributes['skiplabel'] = TRUE;
$elements = [];
$groupsOptions = [];
- foreach ($groups as $id => $group) {
+ foreach ($groups as $key => $group) {
+ $id = $group['id'];
// make sure that this group has public visibility
if ($visibility &&
$group['visibility'] == 'User and User Admin Only'
}
if ($groupElementType == 'select') {
- $groupsOptions[$id] = $group;
+ $groupsOptions[$key] = $group;
}
else {
$form->_tagGroup[$fName][$id]['description'] = $group['description'];
// add select for groups
// Get hierarchical listing of groups, respecting ACLs for CRM-16836.
- $groupHierarchy = CRM_Contact_BAO_Group::getGroupsHierarchy($this->_group, NULL, '- ');
+ $groupHierarchy = CRM_Contact_BAO_Group::getGroupsHierarchy($this->_group, NULL, '- ', TRUE);
if (!empty($searchOptions['groups'])) {
$this->addField('group', [
'entity' => 'group_contact',
'label' => ts('in'),
'placeholder' => ts('- any group -'),
'options' => $groupHierarchy,
- 'type' => 'Select2',
]);
}
$config = CRM_Core_Config::singleton();
$form->applyFilter('__ALL__', 'trim');
- $form->add('text',
- 'text',
- ts('Find'),
- TRUE
- );
+ $form->add('text', 'text', ts('Find'), NULL, TRUE);
// also add a select box to allow the search to be constrained
$tables = ['' => ts('All tables')];
NULL,
NULL, TRUE, TRUE,
$this->_onlyPublicGroups,
- NULL, NULL, TRUE
+ NULL, NULL, TRUE, TRUE
);
$in = CRM_Contact_BAO_GroupContact::getContactGroup(
'Added',
NULL, FALSE, TRUE,
$this->_onlyPublicGroups,
- NULL, NULL, TRUE
+ NULL, NULL, TRUE, TRUE
);
$pending = CRM_Contact_BAO_GroupContact::getContactGroup(
'Pending',
NULL, FALSE, TRUE,
$this->_onlyPublicGroups,
- NULL, NULL, TRUE
+ NULL, NULL, TRUE, TRUE
);
$out = CRM_Contact_BAO_GroupContact::getContactGroup(
'Removed',
NULL, FALSE, TRUE,
$this->_onlyPublicGroups,
- NULL, NULL, TRUE
+ NULL, NULL, TRUE, TRUE
);
$this->assign('groupCount', $count);
*/
public function fillupPrevNextCache($sort, $cacheKey, $start = 0, $end = self::CACHE_SIZE) {
$coreSearch = TRUE;
+ // This ensures exceptions are caught in the try-catch.
+ $handling = CRM_Core_TemporaryErrorScope::useException();
// For custom searches, use the contactIDs method
if (is_a($this, 'CRM_Contact_Selector_Custom')) {
$sql = $this->_search->contactIDs($start, $end, $sort, TRUE);
try {
Civi::service('prevnext')->fillWithSql($cacheKey, $sql);
}
- catch (CRM_Core_Exception $e) {
+ catch (\Exception $e) {
if ($coreSearch) {
// in the case of error, try rebuilding cache using full sql which is used for search selector display
// this fixes the bugs reported in CRM-13996 & CRM-14438
public static $_trxnIDs = NULL;
/**
- * Field for all the objects related to this contribution
+ * Field for all the objects related to this contribution.
+ *
+ * This is used from
+ * 1) deprecated function transitionComponents
+ * 2) function to send contribution receipts _assignMessageVariablesToTemplate
+ * 3) some invoice code that is copied from 2
+ * 4) odds & sods that need to be investigated and fixed.
+ *
+ * However, it is no longer used by completeOrder.
*
* @var \CRM_Member_BAO_Membership|\CRM_Event_BAO_Participant[]
+ *
+ * @deprecated
*/
public $_relatedObjects = [];
$contribution->total_amount = $contributionParams['total_amount'] = $input['amount'];
}
- if (!empty($contributionParams['contribution_recur_id'])) {
- $recurringContribution = civicrm_api3('ContributionRecur', 'getsingle', [
- 'id' => $contributionParams['contribution_recur_id'],
- ]);
- if (!empty($recurringContribution['campaign_id'])) {
- // CRM-17718 the campaign id on the contribution recur record should get precedence.
- $contributionParams['campaign_id'] = $recurringContribution['campaign_id'];
- }
- if (!empty($recurringContribution['financial_type_id'])) {
- // CRM-17718 the campaign id on the contribution recur record should get precedence.
- $contributionParams['financial_type_id'] = $recurringContribution['financial_type_id'];
- }
+ $recurringContribution = civicrm_api3('ContributionRecur', 'getsingle', [
+ 'id' => $contributionParams['contribution_recur_id'],
+ ]);
+ if (!empty($recurringContribution['financial_type_id'])) {
+ // CRM-17718 the campaign id on the contribution recur record should get precedence.
+ $contributionParams['financial_type_id'] = $recurringContribution['financial_type_id'];
}
$templateContribution = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution(
$contributionParams['contribution_recur_id'],
else {
$contributionParams['financial_type_id'] = $templateContribution['financial_type_id'];
}
- foreach (['contact_id', 'currency', 'source'] as $fieldName) {
- $contributionParams[$fieldName] = $templateContribution[$fieldName];
+ foreach (['contact_id', 'currency', 'source', 'amount_level', 'address_id'] as $fieldName) {
+ if (isset($templateContribution[$fieldName])) {
+ $contributionParams[$fieldName] = $templateContribution[$fieldName];
+ }
+ }
+ if (!empty($recurringContribution['campaign_id'])) {
+ // CRM-17718 the campaign id on the contribution recur record should get precedence.
+ $contributionParams['campaign_id'] = $recurringContribution['campaign_id'];
+ }
+ if (!isset($contributionParams['campaign_id']) && isset($templateContribution['campaign_id'])) {
+ // Fall back on value from the previous contribution if not passed in as input
+ // or loadable from the recurring contribution.
+ $contributionParams['campaign_id'] = $templateContribution['campaign_id'];
}
-
$contributionParams['source'] = $contributionParams['source'] ?: ts('Recurring contribution');
//CRM-18805 -- Contribution page not recorded on recurring transactions, Recurring contribution payments
$contribution->id = $createContribution['id'];
$contribution->copyCustomFields($templateContribution['id'], $contribution->id);
self::handleMembershipIDOverride($contribution->id, $input);
+ // Add new soft credit against current $contribution.
+ CRM_Contribute_BAO_ContributionRecur::addrecurSoftCredit($contributionParams['contribution_recur_id'], $createContribution['id']);
return $createContribution;
}
}
//not really sure what params might be passed in but lets merge em into values
$values = array_merge($this->_gatherMessageValues($input, $values, $ids), $values);
$values['is_email_receipt'] = !$returnMessageText;
- if (!empty($input['receipt_date'])) {
- $values['receipt_date'] = $input['receipt_date'];
+ foreach (['receipt_date', 'cc_receipt', 'bcc_receipt', 'receipt_from_name', 'receipt_from_email', 'receipt_text'] as $fld) {
+ if (!empty($input[$fld])) {
+ $values[$fld] = $input[$fld];
+ }
}
$template = $this->_assignMessageVariablesToTemplate($values, $input, $returnMessageText);
$contributionResult = civicrm_api3('Contribution', 'create', $contributionParams);
}
- // Add new soft credit against current $contribution.
- if ($recurringContributionID) {
- CRM_Contribute_BAO_ContributionRecur::addrecurSoftCredit($recurringContributionID, $contributionID);
- }
-
$contribution->contribution_status_id = $contributionParams['contribution_status_id'];
CRM_Core_Error::debug_log_message('Contribution record updated successfully');
return $contributeSettings[$name] ?? NULL;
}
- /**
- * This function process contribution related objects.
- *
- * @param int $contributionId
- * @param int $statusId
- * @param int|null $previousStatusId
- *
- * @param string $receiveDate
- *
- * @return null|string
- */
- public static function transitionComponentWithReturnMessage($contributionId, $statusId, $previousStatusId = NULL, $receiveDate = NULL) {
- $statusMsg = NULL;
- if (!$contributionId || !$statusId) {
- return $statusMsg;
- }
-
- $params = [
- 'contribution_id' => $contributionId,
- 'contribution_status_id' => $statusId,
- 'previous_contribution_status_id' => $previousStatusId,
- 'receive_date' => $receiveDate,
- ];
-
- $updateResult = CRM_Contribute_BAO_Contribution::transitionComponents($params);
-
- if (!is_array($updateResult) ||
- !($updatedComponents = CRM_Utils_Array::value('updatedComponents', $updateResult)) ||
- !is_array($updatedComponents) ||
- empty($updatedComponents)
- ) {
- return $statusMsg;
- }
-
- // get the user display name.
- $sql = "
- SELECT display_name as displayName
- FROM civicrm_contact
-LEFT JOIN civicrm_contribution on (civicrm_contribution.contact_id = civicrm_contact.id )
- WHERE civicrm_contribution.id = {$contributionId}";
- $userDisplayName = CRM_Core_DAO::singleValueQuery($sql);
-
- // get the status message for user.
- foreach ($updatedComponents as $componentName => $updatedStatusId) {
-
- if ($componentName == 'CiviMember') {
- $updatedStatusName = CRM_Utils_Array::value($updatedStatusId,
- CRM_Member_PseudoConstant::membershipStatus()
- );
-
- $statusNameMsgPart = 'updated';
- switch ($updatedStatusName) {
- case 'Cancelled':
- case 'Expired':
- $statusNameMsgPart = $updatedStatusName;
- break;
- }
-
- $statusMsg .= "<br />" . ts("Membership for %1 has been %2.", [
- 1 => $userDisplayName,
- 2 => $statusNameMsgPart,
- ]);
- }
-
- if ($componentName == 'CiviEvent') {
- $updatedStatusName = CRM_Utils_Array::value($updatedStatusId,
- CRM_Event_PseudoConstant::participantStatus()
- );
- if ($updatedStatusName == 'Cancelled') {
- $statusMsg .= "<br />" . ts("Event Registration for %1 has been Cancelled.", [1 => $userDisplayName]);
- }
- elseif ($updatedStatusName == 'Registered') {
- $statusMsg .= "<br />" . ts("Event Registration for %1 has been updated.", [1 => $userDisplayName]);
- }
- }
-
- if ($componentName == 'CiviPledge') {
- $updatedStatusName = CRM_Utils_Array::value($updatedStatusId,
- CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name')
- );
- if ($updatedStatusName == 'Cancelled') {
- $statusMsg .= "<br />" . ts("Pledge Payment for %1 has been Cancelled.", [1 => $userDisplayName]);
- }
- elseif ($updatedStatusName == 'Failed') {
- $statusMsg .= "<br />" . ts("Pledge Payment for %1 has been Failed.", [1 => $userDisplayName]);
- }
- elseif ($updatedStatusName == 'Completed') {
- $statusMsg .= "<br />" . ts("Pledge Payment for %1 has been updated.", [1 => $userDisplayName]);
- }
- }
- }
-
- return $statusMsg;
- }
-
/**
* Get the contribution as it is in the database before being updated.
*
* (since it still makes sense to update / cancel
*/
public static function getPaymentProcessorObject($id) {
+ CRM_Core_Error::deprecatedFunctionWarning('Use Civi\Payment\System');
$processor = self::getPaymentProcessor($id);
return is_array($processor) ? $processor['object'] : NULL;
}
* @param int $targetContributionId
*/
public static function copyCustomValues($recurId, $targetContributionId) {
+ CRM_Core_Error::deprecatedFunctionWarning('no alternative');
if ($recurId && $targetContributionId) {
// get the initial contribution id of recur id
$sourceContributionId = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $recurId, 'id', 'contribution_recur_id');
);
// CRM-13848
- CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($financialTypes, CRM_Core_Action::VIEW);
$form->addSelect('financial_type_id',
- ['entity' => 'contribution', 'multiple' => 'multiple', 'context' => 'search', 'options' => $financialTypes]
+ ['entity' => 'contribution', 'multiple' => 'multiple', 'context' => 'search', 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search')]
);
// use contribution_payment_instrument_id instead of payment_instrument_id
*/
class CRM_Contribute_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Contribution';
+
/**
* Class constructor.
*
* @param string $title
* @param bool|int $action
* @param bool $modal
+ *
+ * @throws \CRM_Core_Exception
*/
public function __construct($title = NULL, $action = CRM_Core_Action::NONE, $modal = TRUE) {
$this->_stateMachine = new CRM_Contribute_StateMachine_Search($this, $action);
$this->addPages($this->_stateMachine, $action);
$this->addActions();
+ $this->set('entity', $this->entity);
}
}
*
* Generated from xml/schema/CRM/Contribute/Contribution.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:d937ea0497be1a1aeb1bac09986dd802)
+ * (GenCodeChecksum:600b0cd019cba16cd62aae7463beab77)
*/
/**
*/
public static $_log = TRUE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/contribute/add?reset=1&action=add&context=standalone',
+ 'view' => 'civicrm/contact/view/contribution?reset=1&action=view&id=[id]',
+ 'update' => 'civicrm/contact/view/contribution?reset=1&action=update&id=[id]',
+ 'delete' => 'civicrm/contact/view/contribution?reset=1&action=delete&id=[id]',
+ ];
+
/**
* Contribution ID
*
return FALSE;
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return 'civicrm_contribution';
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ return 'contribution_id';
+ }
+
}
// process associated membership / participant, CRM-4395
if ($contribution->id && $action & CRM_Core_Action::UPDATE) {
- $this->statusMessage[] = CRM_Contribute_BAO_Contribution::transitionComponentWithReturnMessage($contribution->id,
- $contribution->contribution_status_id,
- CRM_Utils_Array::value('contribution_status_id',
- $this->_values
- ),
- $contribution->receive_date
- );
+ CRM_Contribute_BAO_Contribution::transitionComponents([
+ 'contribution_id' => $contribution->id,
+ 'contribution_status_id' => $contribution->contribution_status_id,
+ 'previous_contribution_status_id' => $this->_values['contribution_status_id'] ?? NULL,
+ 'receive_date' => $contribution->receive_date,
+ ]);
}
array_unshift($this->statusMessage, ts('The contribution record has been saved.'));
if (isset($form->_params)) {
$isPayLater = $form->_params['is_pay_later'] ?? NULL;
}
- $campaignId = NULL;
- if (isset($form->_values) && is_array($form->_values) && !empty($form->_values)) {
- $campaignId = $form->_params['campaign_id'] ?? NULL;
- if (!array_key_exists('campaign_id', $form->_params)) {
- $campaignId = $form->_values['campaign_id'] ?? NULL;
- }
- }
+ $memParams = [
+ 'campaign_id' => $form->_params['campaign_id'] ?? ($form->_values['campaign_id'] ?? NULL),
+ ];
// @todo Move this into CRM_Member_BAO_Membership::processMembership
if (!empty($membershipContribution)) {
date('YmdHis'), $membershipParams['cms_contactID'] ?? NULL,
$customFieldsFormatted,
$numTerms, $membershipID, $pending,
- $contributionRecurID, $membershipSource, $isPayLater, $campaignId, [], $membershipContribution,
+ $contributionRecurID, $membershipSource, $isPayLater, $memParams, [], $membershipContribution,
$membershipLineItems
);
foreach ($form->_lineItem[$form->_priceSetId] as & $priceFieldOp) {
if (!empty($priceFieldOp['membership_type_id']) && $membership->membership_type_id == $priceFieldOp['membership_type_id']) {
$membershipOb = $membership;
- $priceFieldOp['start_date'] = $membershipOb->start_date ? CRM_Utils_Date::customFormat($membershipOb->start_date, '%B %E%f, %Y') : '-';
- $priceFieldOp['end_date'] = $membershipOb->end_date ? CRM_Utils_Date::customFormat($membershipOb->end_date, '%B %E%f, %Y') : '-';
+ $priceFieldOp['start_date'] = $membershipOb->start_date ? CRM_Utils_Date::formatDateOnlyLong($membershipOb->start_date) : '-';
+ $priceFieldOp['end_date'] = $membershipOb->end_date ? CRM_Utils_Date::formatDateOnlyLong($membershipOb->end_date) : '-';
}
else {
$priceFieldOp['start_date'] = $priceFieldOp['end_date'] = 'N/A';
$form->_task = $values['task'] ?? NULL;
$ids = [];
- if (isset($values['radio_ts']) && $values['radio_ts'] == 'ts_sel') {
- foreach ($values 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);
- }
- }
+ if (isset($values['radio_ts']) && $values['radio_ts'] === 'ts_sel') {
+ $ids = $form->getSelectedIDs($values);
}
else {
$queryParams = $form->get('queryParams');
$isTest = FALSE;
if (is_array($queryParams)) {
foreach ($queryParams as $fields) {
- if ($fields[0] == 'contribution_test') {
+ if ($fields[0] === 'contribution_test') {
$isTest = TRUE;
break;
}
$form->_contributionIds = $form->_componentIds = $ids;
$form->set('contributionIds', $form->_contributionIds);
-
- //set the context for redirection for any task actions
- $session = CRM_Core_Session::singleton();
-
- $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form);
- $urlParams = 'force=1';
- if (CRM_Utils_Rule::qfKey($qfKey)) {
- $urlParams .= "&qfKey=$qfKey";
- }
-
- $searchFormName = strtolower($form->get('searchFormName'));
- if ($searchFormName == 'search') {
- $session->replaceUserContext(CRM_Utils_System::url('civicrm/contribute/search', $urlParams));
- }
- else {
- $session->replaceUserContext(CRM_Utils_System::url("civicrm/contact/search/$searchFormName",
- $urlParams
- ));
- }
+ $form->setNextUrl('contribute');
}
/**
// @todo add check as to whether the status is updated.
if (!empty($value['contribution_status_id'])) {
// @todo - use completeorder api or make api call do this.
- CRM_Contribute_BAO_Contribution::transitionComponentWithReturnMessage($contribution['id'],
- $value['contribution_status_id'],
- CRM_Utils_Array::value("field[{$contributionID}][contribution_status_id]", $this->_defaultValues),
- $contribution['receive_date']
- );
+ CRM_Contribute_BAO_Contribution::transitionComponents([
+ 'contribution_id' => $contribution['id'],
+ 'contribution_status_id' => $value['contribution_status_id'],
+ 'previous_contribution_status_id' => CRM_Utils_Array::value("field[{$contributionID}][contribution_status_id]", $this->_defaultValues),
+ 'receive_date' => $contribution['receive_date'],
+ ]);
}
}
CRM_Core_Session::setStatus(ts("Your updates have been saved."), ts('Saved'), 'success');
* - Edit
* - Cancel
*
- * @param bool $recurID
+ * @param int $recurID
* @param string $context
*
* @return array
*/
- public static function recurLinks($recurID = FALSE, $context = 'contribution') {
+ public static function recurLinks(int $recurID, $context = 'contribution') {
$links = [
CRM_Core_Action::VIEW => [
'name' => ts('View'),
],
];
- if ($recurID) {
- $paymentProcessorObj = CRM_Contribute_BAO_ContributionRecur::getPaymentProcessorObject($recurID);
- if ($paymentProcessorObj) {
- if ($paymentProcessorObj->supports('cancelRecurring')) {
- unset($links[CRM_Core_Action::DISABLE]['extra'], $links[CRM_Core_Action::DISABLE]['ref']);
- $links[CRM_Core_Action::DISABLE]['url'] = "civicrm/contribute/unsubscribe";
- $links[CRM_Core_Action::DISABLE]['qs'] = "reset=1&crid=%%crid%%&cid=%%cid%%&context={$context}";
- }
+ $paymentProcessorObj = Civi\Payment\System::singleton()->getById(CRM_Contribute_BAO_ContributionRecur::getPaymentProcessorID($recurID));
+ if ($paymentProcessorObj->supports('cancelRecurring')) {
+ unset($links[CRM_Core_Action::DISABLE]['extra'], $links[CRM_Core_Action::DISABLE]['ref']);
+ $links[CRM_Core_Action::DISABLE]['url'] = "civicrm/contribute/unsubscribe";
+ $links[CRM_Core_Action::DISABLE]['qs'] = "reset=1&crid=%%crid%%&cid=%%cid%%&context={$context}";
+ }
- if ($paymentProcessorObj->supports('UpdateSubscriptionBillingInfo')) {
- $links[CRM_Core_Action::RENEW] = [
- 'name' => ts('Change Billing Details'),
- 'title' => ts('Change Billing Details'),
- 'url' => 'civicrm/contribute/updatebilling',
- 'qs' => "reset=1&crid=%%crid%%&cid=%%cid%%&context={$context}",
- ];
- }
+ if ($paymentProcessorObj->supports('UpdateSubscriptionBillingInfo')) {
+ $links[CRM_Core_Action::RENEW] = [
+ 'name' => ts('Change Billing Details'),
+ 'title' => ts('Change Billing Details'),
+ 'url' => 'civicrm/contribute/updatebilling',
+ 'qs' => "reset=1&crid=%%crid%%&cid=%%cid%%&context={$context}",
+ ];
+ }
- if (
- (!CRM_Core_Permission::check('edit contributions') && $context === 'contribution') ||
- (!$paymentProcessorObj->supports('ChangeSubscriptionAmount')
- && !$paymentProcessorObj->supports('EditRecurringContribution')
- )) {
- unset($links[CRM_Core_Action::UPDATE]);
- }
- }
- else {
- unset($links[CRM_Core_Action::DISABLE]);
- unset($links[CRM_Core_Action::UPDATE]);
- }
+ if (
+ (!CRM_Core_Permission::check('edit contributions') && $context === 'contribution') ||
+ (!$paymentProcessorObj->supports('ChangeSubscriptionAmount')
+ && !$paymentProcessorObj->supports('EditRecurringContribution')
+ )) {
+ unset($links[CRM_Core_Action::UPDATE]);
}
return $links;
// Is recurring contribution active?
$recurContributions[$recurId]['is_active'] = !in_array(CRM_Contribute_PseudoConstant::contributionStatus($recurDetail['contribution_status_id'], 'name'), CRM_Contribute_BAO_ContributionRecur::getInactiveStatuses());
if ($recurContributions[$recurId]['is_active']) {
- $actionMask = array_sum(array_keys(self::recurLinks($recurId)));
+ $actionMask = array_sum(array_keys(self::recurLinks((int) $recurId)));
}
else {
$actionMask = CRM_Core_Action::mask([CRM_Core_Permission::VIEW]);
$recurContributions[$recurId]['contribution_status'] = CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_ContributionRecur', 'contribution_status_id', $recurDetail['contribution_status_id']);
}
- $recurContributions[$recurId]['action'] = CRM_Core_Action::formLink(self::recurLinks($recurId), $actionMask,
+ $recurContributions[$recurId]['action'] = CRM_Core_Action::formLink(self::recurLinks((int) $recurId), $actionMask,
[
'cid' => $this->_contactId,
'crid' => $recurId,
$values['recur_status'] = $recurStatus[$values['contribution_status_id']];
$recurRow[$values['id']] = $values;
- $action = array_sum(array_keys(CRM_Contribute_Page_Tab::recurLinks($recur->id, 'dashboard')));
+ $action = array_sum(array_keys(CRM_Contribute_Page_Tab::recurLinks((int) $recur->id, 'dashboard')));
$details = CRM_Contribute_BAO_ContributionRecur::getSubscriptionDetails($recur->id, 'recur');
$hideUpdate = $details->membership_id & $details->auto_renew;
$action -= CRM_Core_Action::UPDATE;
}
- $recurRow[$values['id']]['action'] = CRM_Core_Action::formLink(CRM_Contribute_Page_Tab::recurLinks($recur->id, 'dashboard'),
+ $recurRow[$values['id']]['action'] = CRM_Core_Action::formLink(CRM_Contribute_Page_Tab::recurLinks((int) $recur->id, 'dashboard'),
$action, [
'cid' => $this->_contactId,
'crid' => $values['id'],
* @throws \CiviCRM_API3_Exception
*/
public static function bulkSave($bulkParams, $defaults = []) {
- $addedColumns = $sql = $tables = $customFields = [];
+ $addedColumns = $sql = $customFields = [];
foreach ($bulkParams as $index => $fieldParams) {
$params = array_merge($defaults, $fieldParams);
$customField = self::createCustomFieldRecord($params);
if (!isset($params['custom_group_id'])) {
$params['custom_group_id'] = civicrm_api3('CustomField', 'getvalue', ['id' => $customField->id, 'return' => 'custom_group_id']);
}
- if (!isset($params['table_name'])) {
- if (!isset($tables[$params['custom_group_id']])) {
- $tables[$params['custom_group_id']] = civicrm_api3('CustomGroup', 'getvalue', [
- 'id' => $params['custom_group_id'],
- 'return' => 'table_name',
- ]);
- }
- $params['table_name'] = $tables[$params['custom_group_id']];
- }
- $sql[$params['table_name']][] = $fieldSQL;
- $addedColumns[$params['table_name']][] = $customField->name;
+ $tableName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customField->custom_group_id, 'table_name');
+ $sql[$tableName][] = $fieldSQL;
+ $addedColumns[$tableName][] = $customField->name;
$customFields[$index] = $customField;
}
$logging->fixSchemaDifferencesFor($tableName, ['ADD' => $addedColumns[$tableName]]);
}
- Civi::service('sql_triggers')->rebuild($params['table_name'], TRUE);
+ Civi::service('sql_triggers')->rebuild($tableName, TRUE);
}
CRM_Utils_System::flushCache();
foreach ($customFields as $index => $customField) {
$fieldAttributes += [
'entity' => 'OptionValue',
'placeholder' => $placeholder,
- 'multiple' => $search,
+ 'multiple' => $search ? TRUE : !empty($field->serialize),
'api' => [
'params' => ['option_group_id' => $field->option_group_id, 'is_active' => 1],
],
}
}
elseif (self::isSerialized($customFields[$customFieldId])) {
+ // Select2 v3 returns a comma-separated string.
+ if ($customFields[$customFieldId]['html_type'] == 'Autocomplete-Select' && is_string($value)) {
+ $value = explode(',', $value);
+ }
+
$value = $value ? CRM_Utils_Array::implodePadded($value) : '';
}
$action -= CRM_Core_Action::DELETE;
}
- if (in_array($field->html_type, ['CheckBox', 'Multi-Select'])) {
+ if ($field->html_type == 'CheckBox' || ($field->html_type == 'Select' && $field->serialize == 1)) {
$options[$dao->id]['is_default'] = (isset($defVal) && in_array($dao->value, $defVal));
}
else {
parent::__construct();
}
+ /**
+ * Get a list of setup-actions.
+ *
+ * @return array
+ * List of available actions. See description in the hook-docs.
+ * @see CRM_Utils_Hook::mailSetupActions()
+ */
+ public static function getSetupActions() {
+ $setupActions = [];
+ $setupActions['standard'] = [
+ 'title' => ts('Standard Mail Account'),
+ 'callback' => ['CRM_Core_BAO_MailSettings', 'setupStandardAccount'],
+ ];
+
+ CRM_Utils_Hook::mailSetupActions($setupActions);
+ return $setupActions;
+ }
+
+ public static function setupStandardAccount($setupAction) {
+ return [
+ 'url' => CRM_Utils_System::url('civicrm/admin/mailSettings', 'action=add&reset=1', TRUE, NULL, FALSE),
+ ];
+ }
+
/**
* Return the DAO object containing to the default row of
* civicrm_mail_settings and cache it for further calls
$cond = NULL;
if ($type) {
- $phoneTypeId = array_search($type, CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id'));
+ $phoneTypeId = CRM_Core_PseudoConstant::getKey('CRM_Core_DAO_Phone', 'phone_type_id', $type);
if ($phoneTypeId) {
$cond = " AND civicrm_phone.phone_type_id = $phoneTypeId";
}
return $sql;
}
- /**
- * @deprecated
- *
- * @param array $params
- * @param bool $indexExist
- * @param bool $triggerRebuild
- *
- * @return bool
- */
- public static function alterFieldSQL($params, $indexExist = FALSE, $triggerRebuild = TRUE) {
- CRM_Core_Error::deprecatedFunctionWarning('function no longer in use / supported');
- // lets suppress the required flag, since that can cause sql issue
- $params['required'] = FALSE;
-
- $sql = self::buildFieldChangeSql($params, $indexExist);
-
- // CRM-7007: do not i18n-rewrite this query
- CRM_Core_DAO::executeQuery($sql, [], TRUE, NULL, FALSE, FALSE);
-
- $config = CRM_Core_Config::singleton();
- if ($config->logging) {
- // CRM-16717 not sure why this was originally limited to add.
- // For example custom tables can have field length changes - which need to flow through to logging.
- // Are there any modifies we DON'T was to call this function for (& shouldn't it be clever enough to cope?)
- if ($params['operation'] == 'add' || $params['operation'] == 'modify') {
- $logging = new CRM_Logging_Schema();
- $logging->fixSchemaDifferencesFor($params['table_name'], [trim(strtoupper($params['operation'])) => [$params['name']]]);
- }
- }
-
- if ($triggerRebuild) {
- Civi::service('sql_triggers')->rebuild($params['table_name'], TRUE);
- }
-
- return TRUE;
- }
-
/**
* Delete a CiviCRM-table.
*
}
}
+ $indexType = $createIndexPrefix === 'UI' ? 'UNIQUE' : '';
+
// the index doesn't exist, so create it
// if we're multilingual and the field is internationalised, do it for every locale
// @todo remove is_array check & add multilingual support for combined indexes and add a test.
// entity_id + entity_table which are not multilingual.
if (!is_array($field) && !CRM_Utils_System::isNull($locales) and isset($columns[$table][$fieldName])) {
foreach ($locales as $locale) {
- $queries[] = "CREATE INDEX {$createIndexPrefix}_{$fieldName}{$lengthName}_{$locale} ON {$table} ({$fieldName}_{$locale}{$lengthSize})";
+ $queries[] = "CREATE $indexType INDEX {$createIndexPrefix}_{$fieldName}{$lengthName}_{$locale} ON {$table} ({$fieldName}_{$locale}{$lengthSize})";
}
}
else {
- $queries[] = "CREATE INDEX {$createIndexPrefix}_{$fieldName}{$lengthName} ON {$table} (" . implode(',', (array) $field) . "{$lengthSize})";
+ $queries[] = "CREATE $indexType INDEX {$createIndexPrefix}_{$fieldName}{$lengthName} ON {$table} (" . implode(',', (array) $field) . "{$lengthSize})";
}
}
}
// Disable i18n rewrite.
CRM_Core_DAO::executeQuery($query, $params, TRUE, NULL, FALSE, FALSE);
}
+ // Rebuild triggers and other schema reconciliation if needed.
+ $logging = new CRM_Logging_Schema();
+ $logging->fixSchemaDifferences();
return TRUE;
}
* and format for use with buildProfile. This is the SQL analog of
* formatUFFields().
*
- * @param mix $id
+ * @param int $id
* The id of the UF group or ids of ufgroup.
* @param bool|int $register are we interested in registration fields
* @param int $action
CRM_Contact_Form_Edit_TagsAndGroups::buildQuickForm($form, $contactId,
CRM_Contact_Form_Edit_TagsAndGroups::GROUP,
TRUE, $required,
- $title, NULL, $name
+ $title, NULL, $name, 'checkbox', TRUE
);
}
elseif ($fieldName === 'tag') {
* @param $class
*/
public function loadClass($class) {
- if ($class === 'CiviCRM_API3_Exception') {
+ if ($class === 'CiviCRM_API3_Exception' || $class === 'API_Exception') {
//call internal error class api/Exception first
// allow api/Exception class call external error class
// CiviCRM_API3_Exception
'titlePlural' => $tableXML->titlePlural ?? CRM_Utils_String::pluralize($tableXML->title ?? $titleFromClass),
'icon' => $tableXML->icon ?? NULL,
'add' => $tableXML->add ?? NULL,
+ 'paths' => (array) ($tableXML->paths ?? []),
'labelName' => substr($name, 8),
'className' => $this->classNames[$name],
'bao' => ($useBao ? str_replace('DAO', 'BAO', $this->classNames[$name]) : $this->classNames[$name]),
'=> true,' => '=> TRUE,',
'=> false,' => '=> FALSE,',
'static ::' => 'static::',
- 'use\\' => 'use \\',
+ 'use\\' => 'use ',
];
$contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
$contents = preg_replace('#(\s*)\\/\\*\\*#', "\n\$1/**", $contents);
else {
$maxLength = $field['maxlength'] ?? NULL;
if (!is_array($value) && $maxLength && mb_strlen($value) > $maxLength && empty($field['pseudoconstant'])) {
- Civi::log()->warning(ts('A string for field $dbName has been truncated. The original string was %1', [CRM_Utils_Type::escape($value, 'String')]));
- // The string is too long - what to do what to do? Well losing data is generally bad so lets' truncate
+ // No ts() since this is a sysadmin-y string not seen by general users.
+ Civi::log()->warning('A string for field {dbName} has been truncated. The original string was {value}.', ['dbName' => $dbName, 'value' => $value]);
+ // The string is too long - what to do what to do? Well losing data is generally bad so let's truncate
$value = CRM_Utils_String::ellipsify($value, $maxLength);
}
$this->$dbName = $value;
return array_flip(CRM_Utils_Array::collect('name', static::fields()));
}
+ /**
+ * Returns system paths related to this entity (as defined in the xml schema)
+ *
+ * @return array
+ */
+ public static function getEntityPaths() {
+ return static::$_paths ?? [];
+ }
+
}
* @param string $string
*/
public static function debug_query($string) {
- if (!defined('CIVICRM_DEBUG_LOG_QUERY')) {
- // TODO: When its updated to support getenv(), call CRM_Utils_Constant::value('CIVICRM_DEBUG_LOG_QUERY', FALSE)
- define('CIVICRM_DEBUG_LOG_QUERY', getenv('CIVICRM_DEBUG_LOG_QUERY'));
- }
- if (CIVICRM_DEBUG_LOG_QUERY === 'backtrace') {
+ $debugLogQuery = CRM_Utils_Constant::value('CIVICRM_DEBUG_LOG_QUERY', FALSE);
+ if ($debugLogQuery === 'backtrace') {
CRM_Core_Error::backtrace($string, TRUE);
}
- elseif (CIVICRM_DEBUG_LOG_QUERY) {
- CRM_Core_Error::debug_var('Query', $string, TRUE, TRUE, 'sql_log' . CIVICRM_DEBUG_LOG_QUERY, PEAR_LOG_DEBUG);
+ elseif ($debugLogQuery) {
+ CRM_Core_Error::debug_var('Query', $string, TRUE, TRUE, 'sql_log' . $debugLogQuery, PEAR_LOG_DEBUG);
}
}
/**
* Render an exception as HTML string.
*
- * @param Exception $e
+ * @param Throwable $e
* @return string
* printable HTML text
*/
- public static function formatHtmlException(Exception $e) {
+ public static function formatHtmlException(Throwable $e) {
$msg = '';
// Exception metadata
/**
* Write details of an exception to the log.
*
- * @param Exception $e
+ * @param Throwable $e
* @return string
* printable plain text
*/
- public static function formatTextException(Exception $e) {
+ public static function formatTextException(Throwable $e) {
$msg = get_class($e) . ": \"" . $e->getMessage() . "\"\n";
$ei = $e;
* The field properties, including the entity and context.
* @param bool $required
* If the field is required.
+ * @param string $title
+ * A field title, if applicable.
* @return string
* The placeholder text.
*/
- private static function selectOrAnyPlaceholder($props, $required) {
+ private static function selectOrAnyPlaceholder($props, $required, $title = NULL) {
if (empty($props['entity'])) {
return NULL;
}
- $daoToClass = CRM_Core_DAO_AllCoreTables::daoToClass();
- if (array_key_exists($props['entity'], $daoToClass)) {
- $daoClass = $daoToClass[$props['entity']];
- $tsPlaceholder = $daoClass::getEntityTitle();
- }
- else {
- $tsPlaceholder = ts('option');
+ if (!$title) {
+ $daoToClass = CRM_Core_DAO_AllCoreTables::daoToClass();
+ if (array_key_exists($props['entity'], $daoToClass)) {
+ $daoClass = $daoToClass[$props['entity']];
+ $title = $daoClass::getEntityTitle();
+ }
+ else {
+ $title = ts('option');
+ }
}
if (($props['context'] ?? '') == 'search' && !$required) {
- return ts('- any %1 -', [1 => $tsPlaceholder]);
+ return ts('- any %1 -', [1 => $title]);
}
- return ts('- select %1 -', [1 => $tsPlaceholder]);
+ return ts('- select %1 -', [1 => $title]);
}
/**
}
}
$props += CRM_Utils_Array::value('html', $fieldSpec, []);
+ if (in_array($widget, ['Select', 'Select2'])
+ && !array_key_exists('placeholder', $props)
+ && $placeholder = self::selectOrAnyPlaceholder($props, $required, $label)) {
+ $props['placeholder'] = $placeholder;
+ }
CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
// TODO: refactor switch statement, to separate methods.
case 'Select':
case 'Select2':
$props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
- if (!array_key_exists('placeholder', $props) && $placeholder = self::selectOrAnyPlaceholder($props, $required)) {
- $props['placeholder'] = $placeholder;
- }
// TODO: Add and/or option for fields that store multiple values
return $this->add(strtolower($widget), $name, $label, $options, $required, $props);
* @return HTML_QuickForm_Element
*/
public function addChainSelect($elementName, $settings = []) {
+ $required = $settings['required'] ?? FALSE;
$label = strpos($elementName, 'rovince') ? CRM_Core_DAO_StateProvince::getEntityTitle() : CRM_Core_DAO_County::getEntityTitle();
$props = $settings += [
'control_field' => str_replace(['state_province', 'StateProvince', 'county', 'County'], [
'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
'data-none-prompt' => ts('- N/A -'),
'multiple' => FALSE,
- 'required' => FALSE,
+ 'required' => $required,
'placeholder' => ts('- select %1 -', [1 => $label]),
];
CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
- $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
+ $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2' . ($required ? ' required crm-field-required' : '');
$props['data-select-prompt'] = $props['placeholder'];
$props['data-name'] = $elementName;
// CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
// quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
// which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
- return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
+ return $this->add('select', $elementName, $settings['label'], NULL, $required, $props);
}
/**
*/
public static $entityShortname = NULL;
+ /**
+ * Set where the browser should be directed to next.
+ *
+ * @param string $pathPart
+ *
+ * @throws \CRM_Core_Exception
+ */
+ public function setNextUrl(string $pathPart) {
+ //set the context for redirection for any task actions
+ $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $this);
+ $urlParams = 'force=1';
+ if (CRM_Utils_Rule::qfKey($qfKey)) {
+ $urlParams .= "&qfKey=$qfKey";
+ }
+
+ $session = CRM_Core_Session::singleton();
+ $searchFormName = strtolower($this->get('searchFormName'));
+ if ($searchFormName === 'search') {
+ $session->replaceUserContext(CRM_Utils_System::url('civicrm/' . $pathPart . '/search', $urlParams));
+ }
+ else {
+ $session->replaceUserContext(CRM_Utils_System::url("civicrm/contact/search/$searchFormName",
+ $urlParams
+ ));
+ }
+ }
+
+ /**
+ * Get the ids the user has selected.
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ public function getSelectedIDs(array $values): array {
+ $ids = [];
+ foreach ($values 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);
+ }
+ }
+ return $ids;
+ }
+
/**
* Build all the data structures needed to build the form.
*
}
$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 ";
+ $query->_distinctComponentClause = $form->getDistinctComponentClause();
+ $query->_groupByComponentClause = $form->getGroupByComponentClause();
$result = $query->searchQuery(0, 0, $sortOrder);
- $selector = $form::$entityShortname . '_id';
+ $selector = $form->getEntityAliasField();
while ($result->fetch()) {
$entityIds[] = $result->$selector;
}
}
if (!empty($entityIds)) {
- $form->_componentClause = ' ' . $form::$tableName . '.id IN ( ' . implode(',', $entityIds) . ' ) ';
+ $form->_componentClause = ' ' . $form->getTableName() . '.id IN ( ' . implode(',', $entityIds) . ' ) ';
$form->assign('totalSelected' . ucfirst($form::$entityShortname) . 's', count($entityIds));
}
// FIXME: This is really to handle legacy code that should probably be updated to use $form->_entityIds
$entitySpecificIdsName = '_' . $form::$entityShortname . 'Ids';
$form->$entitySpecificIdsName = $form->_entityIds;
+ $form->setNextUrl($form::$entityShortname);
- //set the context for redirection for any task actions
- $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form);
- $urlParams = 'force=1';
- if (CRM_Utils_Rule::qfKey($qfKey)) {
- $urlParams .= "&qfKey=$qfKey";
- }
-
- $session = CRM_Core_Session::singleton();
- $searchFormName = strtolower($form->get('searchFormName'));
- if ($searchFormName == 'search') {
- $session->replaceUserContext(CRM_Utils_System::url('civicrm/' . $form::$entityShortname . '/search', $urlParams));
- }
- else {
- $session->replaceUserContext(CRM_Utils_System::url("civicrm/contact/search/$searchFormName",
- $urlParams
- ));
- }
}
/**
*/
public function setContactIDs() {
$this->_contactIds = CRM_Core_DAO::getContactIDsFromComponent($this->_entityIds,
- $this::$tableName
+ $this->getTableName()
);
}
return $this->controller->exportValues('Basic');
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ CRM_Core_Error::deprecatedFunctionWarning('function should be overridden');
+ return $this::$tableName;
+ }
+
+ /**
+ * Get the clause for grouping by the component.
+ *
+ * @return string
+ */
+ public function getDistinctComponentClause() {
+ return " ( " . $this->getTableName() . ".id )";
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getGroupByComponentClause() {
+ return " GROUP BY " . $this->getTableName() . ".id ";
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ CRM_Core_Error::deprecatedFunctionWarning('function should be overridden');
+ return $this::$entityShortname . '_id';
+ }
+
}
$longForShortMapping['fr'] = defined("CIVICRM_LANGUAGE_MAPPING_FR") ? CIVICRM_LANGUAGE_MAPPING_FR : 'fr_FR';
$longForShortMapping['pt'] = defined("CIVICRM_LANGUAGE_MAPPING_PT") ? CIVICRM_LANGUAGE_MAPPING_PT : 'pt_PT';
$longForShortMapping['es'] = defined("CIVICRM_LANGUAGE_MAPPING_ES") ? CIVICRM_LANGUAGE_MAPPING_ES : 'es_ES';
+ $longForShortMapping['nl'] = defined("CIVICRM_LANGUAGE_MAPPING_NL") ? CIVICRM_LANGUAGE_MAPPING_NL : 'nl_NL';
}
return $longForShortMapping;
}
'description' => "text COMMENT 'Optional description.'",
],
'civicrm_group' => [
- 'title' => "varchar(255) NOT NULL COMMENT 'Name of Group.'",
+ 'title' => "varchar(255) COMMENT 'Name of Group.'",
'frontend_title' => "varchar(255) DEFAULT NULL COMMENT 'Alternative public title for this Group.'",
'frontend_description' => "text DEFAULT NULL COMMENT 'Alternative public description of the group.'",
],
'civicrm_group' => [
'title' => [
'type' => "Text",
- 'required' => "true",
],
'frontend_title' => [
'type' => "Text",
return $gotText;
case 'contributionPageContinueText':
- if ($params['amount'] <= 0) {
- return ts('To complete this transaction, click the <strong>Continue</strong> button below.');
- }
- if ($this->_paymentProcessor['billing_mode'] == 4) {
- return ts('Click the <strong>Continue</strong> button to go to %1, where you will select your payment method and complete the contribution.', [$this->_paymentProcessor['payment_processor_type']]);
- }
- if ($params['is_payment_to_existing']) {
- return ts('To complete this transaction, click the <strong>Make Payment</strong> button below.');
- }
- return ts('To complete your contribution, click the <strong>Continue</strong> button below.');
+ return ts('Click the <strong>Continue</strong> button to proceed with the payment.');
case 'cancelRecurDetailText':
if ($params['mode'] === 'auto_renew') {
* @param null $entity
* @param string $action
*
- * @return string
+ * @return string|null
+ * @throws \CRM_Core_Exception
*/
public function subscriptionURL($entityID = NULL, $entity = NULL, $action = 'cancel') {
// Set URL
}
/**
- * @param string $component
+ * Main IPN processing function.
*
* @return bool|void
+ *
+ * @throws \CiviCRM_API3_Exception
*/
- public function main($component = 'contribute') {
+ public function main() {
try {
//we only get invoice num as a key player from payment gateway response.
//for ARB we get x_subscription_id and x_subscription_paynum
$x_subscription_id = $this->retrieve('x_subscription_id', 'String');
+ if (!$x_subscription_id) {
+ // Presence of the id means it is approved.
+ return TRUE;
+ }
$ids = $objects = $input = [];
- if ($x_subscription_id) {
- // Presence of the id means it is approved.
- $input['component'] = $component;
-
- // load post vars in $input
- $this->getInput($input, $ids);
-
- // load post ids in $ids
- $this->getIDs($ids, $input);
-
- // Attempt to get payment processor ID from URL
- if (!empty($this->_inputParameters['processor_id'])) {
- $paymentProcessorID = $this->_inputParameters['processor_id'];
- }
- else {
- // 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 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'
- );
- $paymentProcessorID = (int) civicrm_api3('PaymentProcessor', 'getvalue', [
- 'is_test' => 0,
- 'options' => ['limit' => 1],
- 'payment_processor_type_id' => $paymentProcessorTypeID,
- 'return' => 'id',
- ]);
- }
-
- // Check if the contribution exists
- // make sure contribution exists and is valid
+ $input['component'] = 'contribute';
+
+ // load post vars in $input
+ $this->getInput($input, $ids);
+
+ // load post ids in $ids
+ $this->getIDs($ids, $input);
+ $paymentProcessorID = $this->getPaymentProcessorID();
+
+ // Check if the contribution exists
+ // make sure contribution exists and is valid
+ $contribution = new CRM_Contribute_BAO_Contribution();
+ $contribution->id = $ids['contribution'];
+ if (!$contribution->find(TRUE)) {
+ throw new CRM_Core_Exception('Failure: Could not find contribution record for ' . (int) $contribution->id, NULL, ['context' => "Could not find contribution record: {$contribution->id} in IPN request: " . print_r($input, TRUE)]);
+ }
+ $ids['contributionPage'] = $contribution->contribution_page_id;
+
+ // make sure contact exists and is valid
+ // use the contact id from the contribution record as the id in the IPN may not be valid anymore.
+ $contact = new CRM_Contact_BAO_Contact();
+ $contact->id = $contribution->contact_id;
+ $contact->find(TRUE);
+ if ($contact->id != $ids['contact']) {
+ // If the ids do not match then it is possible the contact id in the IPN has been merged into another contact which is why we use the contact_id from the contribution
+ CRM_Core_Error::debug_log_message("Contact ID in IPN {$ids['contact']} not found but contact_id found in contribution {$contribution->contact_id} used instead");
+ echo "WARNING: Could not find contact record: {$ids['contact']}<p>";
+ $ids['contact'] = $contribution->contact_id;
+ }
+
+ $contributionRecur = new CRM_Contribute_BAO_ContributionRecur();
+ $contributionRecur->id = $ids['contributionRecur'];
+ if (!$contributionRecur->find(TRUE)) {
+ throw new CRM_Core_Exception("Could not find contribution recur record: {$ids['ContributionRecur']} in IPN request: " . print_r($input, TRUE));
+ }
+
+ $objects['contact'] = &$contact;
+ $objects['contribution'] = &$contribution;
+
+ $this->loadObjects($input, $ids, $objects, TRUE, $paymentProcessorID);
+
+ // check if first contribution is completed, else complete first contribution
+ $first = TRUE;
+ if ($objects['contribution']->contribution_status_id == 1) {
+ $first = FALSE;
+ //load new contribution object if required.
+ // create a contribution and then get it processed
$contribution = new CRM_Contribute_BAO_Contribution();
- $contribution->id = $ids['contribution'];
- if (!$contribution->find(TRUE)) {
- throw new CRM_Core_Exception('Failure: Could not find contribution record for ' . (int) $contribution->id, NULL, ['context' => "Could not find contribution record: {$contribution->id} in IPN request: " . print_r($input, TRUE)]);
- }
- $ids['contributionPage'] = $contribution->contribution_page_id;
-
- // make sure contact exists and is valid
- // use the contact id from the contribution record as the id in the IPN may not be valid anymore.
- $contact = new CRM_Contact_BAO_Contact();
- $contact->id = $contribution->contact_id;
- $contact->find(TRUE);
- if ($contact->id != $ids['contact']) {
- // If the ids do not match then it is possible the contact id in the IPN has been merged into another contact which is why we use the contact_id from the contribution
- CRM_Core_Error::debug_log_message("Contact ID in IPN {$ids['contact']} not found but contact_id found in contribution {$contribution->contact_id} used instead");
- echo "WARNING: Could not find contact record: {$ids['contact']}<p>";
- $ids['contact'] = $contribution->contact_id;
- }
-
- if (!empty($ids['contributionRecur'])) {
- $contributionRecur = new CRM_Contribute_BAO_ContributionRecur();
- $contributionRecur->id = $ids['contributionRecur'];
- if (!$contributionRecur->find(TRUE)) {
- CRM_Core_Error::debug_log_message("Could not find contribution recur record: {$ids['ContributionRecur']} in IPN request: " . print_r($input, TRUE));
- echo "Failure: Could not find contribution recur record: {$ids['ContributionRecur']}<p>";
- return FALSE;
- }
- }
-
- $objects['contact'] = &$contact;
- $objects['contribution'] = &$contribution;
-
- $this->loadObjects($input, $ids, $objects, TRUE, $paymentProcessorID);
-
- if (!empty($ids['paymentProcessor']) && $objects['contributionRecur']->payment_processor_id != $ids['paymentProcessor']) {
- Civi::log()->warning('Payment Processor does not match the recurring processor id.', ['civi.tag' => 'deprecated']);
- }
-
- if ($component == 'contribute' && $ids['contributionRecur']) {
- // check if first contribution is completed, else complete first contribution
- $first = TRUE;
- if ($objects['contribution']->contribution_status_id == 1) {
- $first = FALSE;
- //load new contribution object if required.
- // create a contribution and then get it processed
- $contribution = new CRM_Contribute_BAO_Contribution();
- $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 = $input['receive_date'];
- $contribution->currency = $objects['contribution']->currency;
- $contribution->amount_level = $objects['contribution']->amount_level;
- $contribution->address_id = $objects['contribution']->address_id;
- $contribution->campaign_id = $objects['contribution']->campaign_id;
- $contribution->_relatedObjects = $objects['contribution']->_relatedObjects;
-
- $objects['contribution'] = &$contribution;
- }
- $input['payment_processor_id'] = $paymentProcessorID;
- return $this->recur($input, [
- 'related_contact' => $ids['related_contact'] ?? NULL,
- 'participant' => !empty($objects['participant']) ? $objects['participant']->id : NULL,
- 'contributionRecur' => !empty($objects['contributionRecur']) ? $objects['contributionRecur']->id : NULL,
- 'contact' => $ids['contact'] ?? NULL,
- 'contributionPage' => $ids['contributionPage'] ?? NULL,
- ], $objects['contributionRecur'], $objects['contribution'], $first);
- }
+ $contribution->contact_id = $ids['contact'];
+ $contribution->contribution_page_id = $ids['contributionPage'];
+ $contribution->contribution_recur_id = $ids['contributionRecur'];
+ $contribution->receive_date = $input['receive_date'];
+ }
+ $input['payment_processor_id'] = $paymentProcessorID;
+ $isFirstOrLastRecurringPayment = $this->recur($input, [
+ 'related_contact' => $ids['related_contact'] ?? NULL,
+ 'participant' => NULL,
+ 'contributionRecur' => $contributionRecur->id,
+ ], $contributionRecur, $contribution, $first);
+
+ if ($isFirstOrLastRecurringPayment) {
+ //send recurring Notification email for user
+ CRM_Contribute_BAO_ContributionPage::recurringNotify(TRUE,
+ $ids['contact'],
+ $ids['contributionPage'],
+ $contributionRecur,
+ (bool) $this->getMembershipID($contribution->id, $contributionRecur->id)
+ );
}
+
return TRUE;
}
catch (CRM_Core_Exception $e) {
// so we just fix the recurring contribution and not change any of
// the existing contributions
// CRM-9036
- return TRUE;
+ return FALSE;
}
// check if contribution is already completed, if so we ignore this ipn
if ($contribution->contribution_status_id == 1) {
CRM_Core_Error::debug_log_message("Returning since contribution has already been handled.");
echo 'Success: Contribution has already been handled<p>';
- return TRUE;
+ return FALSE;
}
CRM_Contribute_BAO_Contribution::completeOrder($input, $ids, $contribution);
-
- if ($isFirstOrLastRecurringPayment) {
- //send recurring Notification email for user
- CRM_Contribute_BAO_ContributionPage::recurringNotify(TRUE,
- $ids['contact'],
- $ids['contributionPage'],
- $recur,
- (bool) $this->getMembershipID($contribution->id, $recur->id)
- );
- }
+ return $isFirstOrLastRecurringPayment;
}
/**
*
* @throws \CRM_Core_Exception
*/
- public function getIDs(&$ids, &$input) {
- $ids['contact'] = $this->retrieve('x_cust_id', 'Integer', FALSE, 0);
+ public function getIDs(&$ids, $input) {
+ $ids['contact'] = (int) $this->retrieve('x_cust_id', 'Integer', FALSE, 0);
$ids['contribution'] = (int) $this->retrieve('x_invoice_num', 'Integer');
-
- // joining with contribution table for extra checks
- $sql = "
- SELECT cr.id, cr.contact_id
- FROM civicrm_contribution_recur cr
-INNER JOIN civicrm_contribution co ON co.contribution_recur_id = cr.id
- WHERE cr.processor_id = '{$input['subscription_id']}' AND
- (cr.contact_id = {$ids['contact']} OR co.id = {$ids['contribution']})
- LIMIT 1";
- $contRecur = CRM_Core_DAO::executeQuery($sql);
- $contRecur->fetch();
- $ids['contributionRecur'] = (int) $contRecur->id;
- if ($ids['contact'] != $contRecur->contact_id) {
- $message = ts("Recurring contribution appears to have been re-assigned from id %1 to %2, continuing with %2.", [1 => $ids['contact'], 2 => $contRecur->contact_id]);
- CRM_Core_Error::debug_log_message($message);
- $ids['contact'] = $contRecur->contact_id;
- }
- if (!$ids['contributionRecur']) {
- $message = ts("Could not find contributionRecur id");
- $log = new CRM_Utils_SystemLogger();
- $log->error('payment_notification', ['message' => $message, 'ids' => $ids, 'input' => $input]);
- throw new CRM_Core_Exception($message);
- }
+ $contributionRecur = $this->getContributionRecurObject($input['subscription_id'], $ids['contact'], $ids['contribution']);
+ $ids['contributionRecur'] = (int) $contributionRecur->id;
+ $ids['contact'] = $contributionRecur->contact_id;
}
/**
return CRM_Core_DAO::singleValueQuery($sql);
}
+ /**
+ * Get the recurring contribution object.
+ *
+ * @param string $processorID
+ * @param int $contactID
+ * @param int $contributionID
+ *
+ * @return \CRM_Core_DAO|\DB_Error|object
+ * @throws \CRM_Core_Exception
+ */
+ protected function getContributionRecurObject(string $processorID, int $contactID, int $contributionID) {
+ // joining with contribution table for extra checks
+ $sql = "
+ SELECT cr.id, cr.contact_id
+ FROM civicrm_contribution_recur cr
+INNER JOIN civicrm_contribution co ON co.contribution_recur_id = cr.id
+ WHERE cr.processor_id = '{$processorID}' AND
+ (cr.contact_id = $contactID OR co.id = $contributionID)
+ LIMIT 1";
+ $contRecur = CRM_Core_DAO::executeQuery($sql);
+ if (!$contRecur->fetch()) {
+ throw new CRM_Core_Exception('Could not find contributionRecur id');
+ }
+ if ($contactID != $contRecur->contact_id) {
+ $message = ts("Recurring contribution appears to have been re-assigned from id %1 to %2, continuing with %2.", [1 => $ids['contact'], 2 => $contRecur->contact_id]);
+ CRM_Core_Error::debug_log_message($message);
+ }
+ return $contRecur;
+ }
+
+ /**
+ * Get the payment processor id.
+ *
+ * @return int
+ *
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
+ */
+ protected function getPaymentProcessorID(): int {
+ // Attempt to get payment processor ID from URL
+ if (!empty($this->_inputParameters['processor_id'])) {
+ return (int) $this->_inputParameters['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 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'
+ );
+ return (int) civicrm_api3('PaymentProcessor', 'getvalue', [
+ 'is_test' => 0,
+ 'options' => ['limit' => 1],
+ 'payment_processor_type_id' => $paymentProcessorTypeID,
+ 'return' => 'id',
+ ]);
+ }
+
}
+--------------------------------------------------------------------+
*/
+use Civi\Api4\Contribution;
+
/**
* Class CRM_Core_Payment_BaseIPN.
*/
CRM_Contribute_BAO_ContributionRecur::addRecurLineItems($objects['contributionRecur']->id, $contribution);
}
- //add new soft credit against current contribution id and
- //copy initial contribution custom fields for recurring contributions
- if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id) {
- CRM_Contribute_BAO_ContributionRecur::addrecurSoftCredit($objects['contributionRecur']->id, $contribution->id);
- CRM_Contribute_BAO_ContributionRecur::copyCustomValues($objects['contributionRecur']->id, $contribution->id);
- }
-
if (!empty($memberships)) {
foreach ($memberships as $membership) {
// @fixme Should we cancel only Pending memberships? per cancelled()
/**
* Process cancelled payment outcome.
*
+ * @deprecated The intended replacement code is
+ *
+ * Contribution::update(FALSE)->setValues([
+ * 'cancel_date' => 'now',
+ * 'contribution_status_id:name' => 'Cancelled',
+ * ])->addWhere('id', '=', $contribution->id)->execute();
+ *
* @param array $objects
*
* @return bool
* @throws \CiviCRM_API3_Exception|\CRM_Core_Exception
*/
public function cancelled($objects) {
+ CRM_Core_Error::deprecatedFunctionWarning('Use Contribution create api to cancel the contribution');
$contribution = &$objects['contribution'];
- $memberships = [];
- if (!empty($objects['membership'])) {
- $memberships = &$objects['membership'];
- if (is_numeric($memberships)) {
- $memberships = [$objects['membership']];
- }
- }
- $addLineItems = FALSE;
if (empty($contribution->id)) {
+ // This code is believed to be unreachable.
+ // this entire function is due to be deprecated in the near future so
+ // this code will live in a deprecated function until it gets removed.
$addLineItems = TRUE;
- }
- $participant = &$objects['participant'];
-
- // CRM-15546
- $contributionStatuses = CRM_Core_PseudoConstant::get('CRM_Contribute_DAO_Contribution', 'contribution_status_id', [
- 'labelColumn' => 'name',
- 'flip' => 1,
- ]);
- $contribution->contribution_status_id = $contributionStatuses['Cancelled'];
- $contribution->cancel_date = self::$_now;
- $contribution->save();
-
- // Add line items for recurring payments.
- if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id && $addLineItems) {
- CRM_Contribute_BAO_ContributionRecur::addRecurLineItems($objects['contributionRecur']->id, $contribution);
- }
-
- //add new soft credit against current $contribution and
- //copy initial contribution custom fields for recurring contributions
- if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id) {
- CRM_Contribute_BAO_ContributionRecur::addrecurSoftCredit($objects['contributionRecur']->id, $contribution->id);
- CRM_Contribute_BAO_ContributionRecur::copyCustomValues($objects['contributionRecur']->id, $contribution->id);
- }
-
- if (!empty($memberships)) {
- foreach ($memberships as $membership) {
- if ($membership) {
- $this->cancelMembership($membership, $membership->status_id);
+ // CRM-15546
+ $contributionStatuses = CRM_Core_PseudoConstant::get('CRM_Contribute_DAO_Contribution', 'contribution_status_id', [
+ 'labelColumn' => 'name',
+ 'flip' => 1,
+ ]);
+ $contribution->contribution_status_id = $contributionStatuses['Cancelled'];
+ $contribution->cancel_date = self::$_now;
+ $contribution->save();
+ // Add line items for recurring payments.
+ if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id && $addLineItems) {
+ CRM_Contribute_BAO_ContributionRecur::addRecurLineItems($objects['contributionRecur']->id, $contribution);
+ }
+ $memberships = [];
+ if (!empty($objects['membership'])) {
+ $memberships = &$objects['membership'];
+ if (is_numeric($memberships)) {
+ $memberships = [$objects['membership']];
}
}
- }
+ if (!empty($memberships)) {
+ foreach ($memberships as $membership) {
+ if ($membership) {
+ $this->cancelMembership($membership, $membership->status_id);
+ }
+ }
+ }
+ $participant = &$objects['participant'];
- if ($participant) {
- $this->cancelParticipant($participant->id);
+ if ($participant) {
+ $this->cancelParticipant($participant->id);
+ }
+ }
+ else {
+ Contribution::update(FALSE)->setValues([
+ 'cancel_date' => 'now',
+ 'contribution_status_id:name' => 'Cancelled',
+ ])->addWhere('id', '=', $contribution->id)->execute();
}
Civi::log()->debug("Setting contribution status to Cancelled");
* Logic to cancel a participant record when the related contribution changes to failed/cancelled.
* @todo This is part of a bigger refactor for dev/core/issues/927 - "duplicate" functionality exists in CRM_Contribute_BAO_Contribution::cancel()
*
+ * @deprecated
+ *
* @param $participantID
*
* @throws \CiviCRM_API3_Exception
* @param boolean $onlyCancelPendingMembership
* Do we only cancel pending memberships? OR memberships in any status? (see CRM-18688)
* @fixme Historically failed() cancelled membership in any status, cancelled() cancelled only pending memberships so we retain that behaviour for now.
- *
+ * @deprecated
*/
private function cancelMembership($membership, $membershipStatusID, $onlyCancelPendingMembership = TRUE) {
+ CRM_Core_Error::deprecatedFunctionWarning('use the api');
// @fixme https://lab.civicrm.org/dev/core/issues/927 Cancelling membership etc is not desirable for all use-cases and we should be able to disable it
// Cancel only Pending memberships
$pendingMembershipStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Pending');
}
return ts('To complete your contribution, click the <strong>Continue</strong> button below.');
+ default:
+ return parent::getText($context, $params);
}
}
+ /**
+ * Does this processor support cancelling recurring contributions through code.
+ *
+ * @return bool
+ */
+ protected function supportsCancelRecurring() {
+ return TRUE;
+ }
+
}
+--------------------------------------------------------------------+
*/
+use Civi\Api4\Contribution;
+
/**
*
* @package CRM
$this->single($input, [
'related_contact' => $ids['related_contact'] ?? NULL,
- 'participant' => !empty($objects['participant']) ? $objects['participant']->id : NULL,
- 'contributionRecur' => !empty($objects['contributionRecur']) ? $objects['contributionRecur']->id : NULL,
+ 'participant' => $ids['participant'] ?? NULL,
+ 'contributionRecur' => $recur->id,
], $objects['contribution'], TRUE);
}
);
}
}
- if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) {
- return;
+ $contribution = new CRM_Contribute_BAO_Contribution();
+ $contribution->id = $ids['contribution'];
+ if (!$contribution->find(TRUE)) {
+ throw new CRM_Core_Exception('Failure: Could not find contribution record for ' . (int) $contribution->id, NULL, ['context' => "Could not find contribution record: {$contribution->id} in IPN request: " . print_r($input, TRUE)]);
+ }
+
+ // make sure contact exists and is valid
+ // use the contact id from the contribution record as the id in the IPN may not be valid anymore.
+ $contact = new CRM_Contact_BAO_Contact();
+ $contact->id = $contribution->contact_id;
+ $contact->find(TRUE);
+ if ($contact->id != $ids['contact']) {
+ // If the ids do not match then it is possible the contact id in the IPN has been merged into another contact which is why we use the contact_id from the contribution
+ CRM_Core_Error::debug_log_message("Contact ID in IPN {$ids['contact']} not found but contact_id found in contribution {$contribution->contact_id} used instead");
+ echo "WARNING: Could not find contact record: {$ids['contact']}<p>";
+ $ids['contact'] = $contribution->contact_id;
+ }
+
+ if (!empty($ids['contributionRecur'])) {
+ $contributionRecur = new CRM_Contribute_BAO_ContributionRecur();
+ $contributionRecur->id = $ids['contributionRecur'];
+ if (!$contributionRecur->find(TRUE)) {
+ CRM_Core_Error::debug_log_message("Could not find contribution recur record: {$ids['ContributionRecur']} in IPN request: " . print_r($input, TRUE));
+ echo "Failure: Could not find contribution recur record: {$ids['ContributionRecur']}<p>";
+ return FALSE;
+ }
+ }
+
+ $objects['contact'] = &$contact;
+ $objects['contribution'] = &$contribution;
+
+ // CRM-19478: handle oddity when p=null is set in place of contribution page ID,
+ if (!empty($ids['contributionPage']) && !is_numeric($ids['contributionPage'])) {
+ // We don't need to worry if about removing contribution page id as it will be set later in
+ // CRM_Contribute_BAO_Contribution::loadRelatedObjects(..) using $objects['contribution']->contribution_page_id
+ unset($ids['contributionPage']);
+ }
+
+ if (!$this->loadObjects($input, $ids, $objects, TRUE, $paymentProcessorID)) {
+ return FALSE;
}
$input['payment_processor_id'] = $paymentProcessorID;
- if ($component == 'contribute') {
- if ($ids['contributionRecur']) {
- // check if first contribution is completed, else complete first contribution
- $first = TRUE;
- $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
- if ($objects['contribution']->contribution_status_id == $completedStatusId) {
- $first = FALSE;
- }
- $this->recur($input, $ids, $objects, $first);
- return;
+ if (!empty($ids['contributionRecur'])) {
+ // check if first contribution is completed, else complete first contribution
+ $first = TRUE;
+ $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
+ if ($objects['contribution']->contribution_status_id == $completedStatusId) {
+ $first = FALSE;
}
+ $this->recur($input, $ids, $objects, $first);
+ return;
}
+
$status = $input['paymentStatus'];
if ($status === 'Denied' || $status === 'Failed' || $status === 'Voided') {
$this->failed($objects);
return;
}
if ($status === 'Refunded' || $status === 'Reversed') {
- $this->cancelled($objects);
+ Contribution::update(FALSE)->setValues([
+ 'cancel_date' => 'now',
+ 'contribution_status_id:name' => 'Cancelled',
+ ])->addWhere('id', '=', $contributionID)->execute();
+ Civi::log()->debug("Setting contribution status to Cancelled");
return;
}
if ($status !== 'Completed') {
}
$this->single($input, [
'related_contact' => $ids['related_contact'] ?? NULL,
- 'participant' => !empty($objects['participant']) ? $objects['participant']->id : NULL,
- 'contributionRecur' => !empty($objects['contributionRecur']) ? $objects['contributionRecur']->id : NULL,
+ 'participant' => $ids['participant'] ?? NULL,
+ 'contributionRecur' => $contributionRecurID,
], $objects['contribution']);
}
catch (CRM_Core_Exception $e) {
}
/**
- * @return null|string
- * @throws \Civi\Payment\Exception\PaymentProcessorException
+ * Get url for users to manage this recurring contribution for this processor.
+ *
+ * @param int $entityID
+ * @param null $entity
+ * @param string $action
+ *
+ * @return string|null
+ * @throws \CRM_Core_Exception
*/
- public function cancelSubscriptionURL() {
+ public function subscriptionURL($entityID = NULL, $entity = NULL, $action = 'cancel') {
if ($this->isPayPalType($this::PAYPAL_STANDARD)) {
+ if ($action !== 'cancel') {
+ return NULL;
+ }
return "{$this->_paymentProcessor['url_site']}cgi-bin/webscr?cmd=_subscr-find&alias=" . urlencode($this->_paymentProcessor['user_name']);
}
- else {
- return NULL;
- }
+ return parent::subscriptionURL($entityID, $entity, $action);
}
/**
+--------------------------------------------------------------------+
*/
+use Civi\Api4\Contribution;
+
/**
*
* @package CRM
$this->single($input, [
'related_contact' => $ids['related_contact'] ?? NULL,
- 'participant' => !empty($objects['participant']) ? $objects['participant']->id : NULL,
- 'contributionRecur' => !empty($objects['contributionRecur']) ? $objects['contributionRecur']->id : NULL,
+ 'participant' => $ids['participant'] ?? NULL,
+ 'contributionRecur' => $recur->id ?? NULL,
], $objects, TRUE, $first);
}
Civi::log()->debug('Returning since contribution status is Pending');
return;
}
- elseif ($status === 'Refunded' || $status === 'Reversed') {
- $this->cancelled($objects);
+ if ($status === 'Refunded' || $status === 'Reversed') {
+ Contribution::update(FALSE)->setValues([
+ 'cancel_date' => 'now',
+ 'contribution_status_id:name' => 'Cancelled',
+ ])->addWhere('id', '=', $contribution->id)->execute();
+ Civi::log()->debug("Setting contribution status to Cancelled");
return;
}
elseif ($status !== 'Completed') {
$input['payment_processor_id'] = $paymentProcessorID;
- //?? how on earth would we not have component be one of these?
- // they are the only valid settings & this IPN file can't even be called without one of them
- // grepping for this class doesn't find other paths to call this class
- if ($this->_component == 'contribute' || $this->_component == 'event') {
- if ($ids['contributionRecur']) {
- // check if first contribution is completed, else complete first contribution
- $first = TRUE;
- $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
- if ($objects['contribution']->contribution_status_id == $completedStatusId) {
- $first = FALSE;
- }
- $this->recur($input, $ids, $objects, $first);
- return;
+ if ($ids['contributionRecur']) {
+ // check if first contribution is completed, else complete first contribution
+ $first = TRUE;
+ $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
+ if ($objects['contribution']->contribution_status_id == $completedStatusId) {
+ $first = FALSE;
}
+ $this->recur($input, $ids, $objects, $first);
+ return;
}
+
$this->single($input, [
'related_contact' => $ids['related_contact'] ?? NULL,
- 'participant' => !empty($objects['participant']) ? $objects['participant']->id : NULL,
- 'contributionRecur' => !empty($objects['contributionRecur']) ? $objects['contributionRecur']->id : NULL,
+ 'participant' => $ids['participant'] ?? NULL,
+ 'contributionRecur' => $ids['contributionRecur'] ?? NULL,
], $objects, FALSE, FALSE);
}
catch (CRM_Core_Exception $e) {
],
];
+ // Dashboard permissions
+ $permissions['dashboard'] = [
+ 'get' => [
+ 'access CiviCRM',
+ ],
+ ];
+ $permissions['dashboard_contact'] = [
+ 'default' => [
+ 'access CiviCRM',
+ ],
+ ];
+
// Profile permissions
$permissions['profile'] = [
// the profile will take care of this
*
* - type: string (markup, template, callback, script, scriptFile, scriptUrl, jquery, style, styleFile, styleUrl)
* - name: string, symbolic identifier for this resource
+ * - aliases: string[], list of alternative names for this resource
* - weight: int, default=1. Lower weights come before higher weights.
* (If two resources have the same weight, then a secondary ordering will be
* used to ensure reproducibility. However, the secondary ordering is
}
$snippet['scriptFileUrls'] = [$res->getUrl($ext, $res->filterMinify($ext, $file), TRUE)];
}
+ if ($snippet['type'] === 'scriptFile' && !isset($snippet['aliases'])) {
+ $snippet['aliases'] = $snippet['scriptFileUrls'];
+ }
if ($snippet['type'] === 'styleFile' && !isset($snippet['styleFileUrls'])) {
/** @var Civi\Core\Themes $theme */
list ($ext, $file) = $snippet['styleFile'];
$snippet['styleFileUrls'] = $theme->resolveUrls($theme->getActiveThemeKey(), $ext, $file);
}
+ if ($snippet['type'] === 'styleFile' && !isset($snippet['aliases'])) {
+ $snippet['aliases'] = $snippet['styleFileUrls'];
+ }
+
+ if (isset($snippet['aliases']) && !is_array($snippet['aliases'])) {
+ $snippet['aliases'] = [$snippet['aliases']];
+ }
$this->snippets[$snippet['name']] = $snippet;
$this->isSorted = FALSE;
* @see CRM_Core_Resources_CollectionInterface::update()
*/
public function update($name, $snippet) {
- $this->snippets[$name] = array_merge($this->snippets[$name], $snippet);
- $this->isSorted = FALSE;
+ foreach ($this->resolveName($name) as $realName) {
+ $this->snippets[$realName] = array_merge($this->snippets[$realName], $snippet);
+ $this->isSorted = FALSE;
+ return $this;
+ }
+
+ Civi::log()->warning('Failed to update resource by name ({name})', [
+ 'name' => $name,
+ ]);
return $this;
}
* @see CRM_Core_Resources_CollectionInterface::get()
*/
public function &get($name) {
- return $this->snippets[$name];
+ foreach ($this->resolveName($name) as $realName) {
+ return $this->snippets[$realName];
+ }
+
+ $null = NULL;
+ return $null;
}
/**
return $this;
}
+ /**
+ * @param string $name
+ * Name or alias.
+ * return array
+ * List of real names.
+ */
+ protected function resolveName($name) {
+ if (isset($this->snippets[$name])) {
+ return [$name];
+ }
+ foreach ($this->snippets as $snippetName => $snippet) {
+ if (isset($snippet['aliases']) && in_array($name, $snippet['aliases'])) {
+ return [$snippetName];
+ }
+ }
+ return [];
+ }
+
/**
* @param $a
* @param $b
$params['is_search_range'] = 0;
}
- if ($params['html_type'] === 'Select') {
+ if ($params['data_type'] !== 'ContactReference' && ($params['html_type'] === 'Select' || $params['html_type'] === 'Autocomplete-Select')) {
$params['serialize'] = $params['serialize'] ? CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND : 'null';
}
else {
$paramsField = ['id' => $this->_fid];
CRM_Core_BAO_CustomField::retrieve($paramsField, $fieldDefaults);
-
if ($fieldDefaults['html_type'] == 'CheckBox'
- || $fieldDefaults['html_type'] == 'Multi-Select'
+ // Multi-Select
+ || ($fieldDefaults['html_type'] == 'Select' && $fieldDefaults['serialize'] == 1)
) {
if (!empty($fieldDefaults['default_value'])) {
$defaultCheckValues = explode(CRM_Core_DAO::VALUE_SEPARATOR,
$customField->find(TRUE) &&
(
$customField->html_type == 'CheckBox' ||
- $customField->html_type == 'Multi-Select'
+ // Multi Value Select
+ ($customField->html_type == 'Select' && $customField->serialize == 1)
)
) {
$defVal = explode(
}
}
+ CRM_Core_BAO_CustomOption::updateValue($customOption->id, $customOption->value);
$customOption->save();
$msg = ts('Your multiple choice option \'%1\' has been saved', [1 => $customOption->label]);
* @return array
* array of all the events that are searched
*/
- public static function &getCompleteInfo(
+ public static function getCompleteInfo(
$start = NULL,
$type = NULL,
$eventId = NULL,
// check 'view event info' permission
//@todo - per CRM-14626 we have resolved that 'view event info' means 'view ALL event info'
- // and passing in the specific permission here will short-circuit the evaluation of permission to
- // see specific events (doesn't seem relevant to this call
- // however, since this function is accessed only by a convoluted call from a joomla block function
- // it seems safer not to touch here. Suggestion is that CRM_Core_Permission::check(array or relevant permissions) would
- // be clearer & safer here
- $permissions = CRM_Core_Permission::event(CRM_Core_Permission::VIEW);
+ if (CRM_Core_Permission::check('view event info')) {
+ $permissions = TRUE;
+ }
+ else {
+ $permissions = CRM_Core_Permission::event(CRM_Core_Permission::VIEW);
+ }
while ($dao->fetch()) {
- if (!empty($permissions) && in_array($dao->event_id, $permissions)) {
+ if (!empty($permissions) && ($permissions === TRUE || in_array($dao->event_id, $permissions))) {
$info = [];
$info['uid'] = "CiviCRM_EventID_{$dao->event_id}_" . md5($config->userFrameworkBaseURL) . $url;
$email = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_UFGroup', $gId, 'notify');
if ($email) {
//get values of corresponding profile fields for notification
- list($profileValues) = self::buildCustomDisplay($gId,
+ [$profileValues] = self::buildCustomDisplay($gId,
NULL,
$contactID,
$template,
*/
class CRM_Event_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Participant';
+
/**
* Class constructor.
*
// add all the actions
$this->addActions($uploadDir, $uploadNames);
+ $this->set('entity', $this->entity);
}
}
*
* Generated from xml/schema/CRM/Event/Event.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:3514f838a27ddbf9bdf6e63ea20aabec)
+ * (GenCodeChecksum:a7abbcbe6a0e5e49e55d61afd013c791)
*/
/**
*/
public static $_log = TRUE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/event/add?reset=1',
+ 'view' => 'civicrm/event/info?reset=1&id=[id]',
+ ];
+
/**
* Event
*
return FALSE;
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return 'civicrm_participant';
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ return 'participant_id';
+ }
+
}
$form->_participantIds = $form->_componentIds = $ids;
- //set the context for redirection for any task actions
- $session = CRM_Core_Session::singleton();
-
- $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form);
- $urlParams = 'force=1';
- if (CRM_Utils_Rule::qfKey($qfKey)) {
- $urlParams .= "&qfKey=$qfKey";
- }
-
- $searchFormName = strtolower($form->get('searchFormName'));
- if ($searchFormName == 'search') {
- $session->replaceUserContext(CRM_Utils_System::url('civicrm/event/search', $urlParams));
- }
- else {
- $session->replaceUserContext(CRM_Utils_System::url("civicrm/contact/search/$searchFormName",
- $urlParams
- ));
- }
+ $form->setNextUrl('event');
}
/**
*
*/
public function listParticipations() {
- $controller = new CRM_Core_Controller_Simple(
- 'CRM_Event_Form_Search',
- ts('Events'),
- NULL,
- FALSE, FALSE, TRUE, FALSE
- );
- $controller->setEmbedded(TRUE);
- $controller->reset();
- $controller->set('context', 'user');
- $controller->set('cid', $this->_contactId);
- $controller->set('force', 1);
- $controller->process();
- $controller->run();
+ $event_rows = [];
+
+ $participants = \Civi\Api4\Participant::get(FALSE)
+ ->addSelect('id', 'contact_id', 'status_id:name', 'status_id:label', 'event.id', 'event.title', 'event.start_date', 'event.end_date')
+ ->addWhere('contact_id', '=', $this->_contactId)
+ ->addOrderBy('event.start_date', 'DESC')
+ ->execute()
+ ->indexBy('id');
+
+ // Flatten the results in the format expected by the template
+ foreach ($participants as $p) {
+ $p['participant_id'] = $p['id'];
+ $p['status'] = $p['status_id:name'];
+ $p['participant_status'] = $p['status_id:label'];
+ $p['event_id'] = $p['event.id'];
+ $p['event_title'] = $p['event.title'];
+ $p['event_start_date'] = $p['event.start_date'];
+ $p['event_end_date'] = $p['event.end_date'];
+
+ $event_rows[] = $p;
+ }
+
+ $this->assign('event_rows', $event_rows);
}
/**
throw new CRM_Core_Exception('Unreachable code');
}
$this->_exportMode = constant('CRM_Export_Form_Select::' . strtoupper($entityShortname) . '_EXPORT');
- $formTaskClassName = "CRM_{$entityShortname}_Form_Task";
-
- if (isset($formTaskClassName::$entityShortname)) {
- $this::$entityShortname = $formTaskClassName::$entityShortname;
- if (isset($formTaskClassName::$tableName)) {
- $this::$tableName = $formTaskClassName::$tableName;
- }
- }
- else {
- $this::$entityShortname = $entityShortname;
- $this::$tableName = CRM_Core_DAO_AllCoreTables::getTableForClass(CRM_Core_DAO_AllCoreTables::getFullName($this->getDAOName()));
- }
+ $this::$entityShortname = strtolower($entityShortname);
$values = $this->getSearchFormValues();
$count = 0;
$this->set('selectAll', $this->_selectAll);
$this->set('exportMode', $this->_exportMode);
$this->set('componentClause', $this->_componentClause);
- $this->set('componentTable', $this->_componentTable);
+ $this->set('componentTable', $this->getTableName());
+ }
+
+ /**
+ * Get the name of the table for the relevant entity.
+ */
+ public function getTableName() {
+ throw new CRM_Core_Exception('should be over-riden');
}
/**
return FALSE;
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return 'civicrm_case';
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ return 'case_id';
+ }
+
}
/**
* Get available Financial Types.
*
+ * This logic is being moved into the financialacls extension.
+ *
+ * Rather than call this function consider using
+ *
+ * $types = \CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search');
+ *
* @param array $financialTypes
* (reference ) an array of financial types
* @param int|string $action
if ($params['total_amount'] < 0 && !empty($params['cancelled_payment_id'])) {
self::reverseAllocationsFromPreviousPayment($params, $trxn->id);
- return $trxn;
}
- list($ftIds, $taxItems) = CRM_Contribute_BAO_Contribution::getLastFinancialItemIds($params['contribution_id']);
+ else {
+ list($ftIds, $taxItems) = CRM_Contribute_BAO_Contribution::getLastFinancialItemIds($params['contribution_id']);
- foreach ($lineItems as $key => $value) {
- if ($value['allocation'] === (float) 0) {
- continue;
- }
+ foreach ($lineItems as $key => $value) {
+ if ($value['allocation'] === (float) 0) {
+ continue;
+ }
- if (!empty($ftIds[$value['price_field_value_id']])) {
- $financialItemID = $ftIds[$value['price_field_value_id']];
- }
- else {
- $financialItemID = self::getNewFinancialItemID($value, $params['trxn_date'], $contribution['contact_id'], $paymentTrxnParams['currency']);
- }
+ if (!empty($ftIds[$value['price_field_value_id']])) {
+ $financialItemID = $ftIds[$value['price_field_value_id']];
+ }
+ else {
+ $financialItemID = self::getNewFinancialItemID($value, $params['trxn_date'], $contribution['contact_id'], $paymentTrxnParams['currency']);
+ }
- $eftParams = [
- 'entity_table' => 'civicrm_financial_item',
- 'financial_trxn_id' => $trxn->id,
- 'entity_id' => $financialItemID,
- 'amount' => $value['allocation'],
- ];
-
- civicrm_api3('EntityFinancialTrxn', 'create', $eftParams);
-
- if (array_key_exists($value['price_field_value_id'], $taxItems)) {
- // @todo - this is expected to be broken - it should be fixed to
- // a) have the getPayableLineItems add the amount to allocate for tax
- // b) call EntityFinancialTrxn directly - per above.
- // - see https://github.com/civicrm/civicrm-core/pull/14763
- $entityParams = [
- 'contribution_total_amount' => $contribution['total_amount'],
- 'trxn_total_amount' => $params['total_amount'],
- 'trxn_id' => $trxn->id,
- 'line_item_amount' => $taxItems[$value['price_field_value_id']]['amount'],
+ $eftParams = [
+ 'entity_table' => 'civicrm_financial_item',
+ 'financial_trxn_id' => $trxn->id,
+ 'entity_id' => $financialItemID,
+ 'amount' => $value['allocation'],
];
- $eftParams['entity_id'] = $taxItems[$value['price_field_value_id']]['financial_item_id'];
- CRM_Contribute_BAO_Contribution::createProportionalEntry($entityParams, $eftParams);
+
+ civicrm_api3('EntityFinancialTrxn', 'create', $eftParams);
+
+ if (array_key_exists($value['price_field_value_id'], $taxItems)) {
+ // @todo - this is expected to be broken - it should be fixed to
+ // a) have the getPayableLineItems add the amount to allocate for tax
+ // b) call EntityFinancialTrxn directly - per above.
+ // - see https://github.com/civicrm/civicrm-core/pull/14763
+ $entityParams = [
+ 'contribution_total_amount' => $contribution['total_amount'],
+ 'trxn_total_amount' => $params['total_amount'],
+ 'trxn_id' => $trxn->id,
+ 'line_item_amount' => $taxItems[$value['price_field_value_id']]['amount'],
+ ];
+ $eftParams['entity_id'] = $taxItems[$value['price_field_value_id']]['financial_item_id'];
+ CRM_Contribute_BAO_Contribution::createProportionalEntry($entityParams, $eftParams);
+ }
}
}
*
* Generated from xml/schema/CRM/Financial/FinancialTrxn.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:857c64b471d1872d98141aefa56aecb6)
+ * (GenCodeChecksum:834a9dbff8acecd70dbb68d640cb1d46)
*/
/**
'bao' => 'CRM_Financial_DAO_FinancialTrxn',
'localizable' => 0,
'FKClassName' => 'CRM_Financial_DAO_PaymentProcessor',
+ 'html' => [
+ 'type' => 'Select',
+ ],
+ 'pseudoconstant' => [
+ 'table' => 'civicrm_payment_processor',
+ 'keyColumn' => 'id',
+ 'labelColumn' => 'name',
+ ],
'add' => '4.3',
],
'financial_trxn_payment_instrument_id' => [
*/
class CRM_Grant_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Grant';
+
/**
* Class constructor.
*
$this->addPages($this->_stateMachine, $action);
// add all the actions
- $config = CRM_Core_Config::singleton();
$this->addActions();
+ $this->set('entity', $this->entity);
}
}
*
* Generated from xml/schema/CRM/Grant/Grant.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:91aecd5b45ba8c5cd6636bb95ddbbfee)
+ * (GenCodeChecksum:febf55259ea92f4dd6a7d1ee4b5ea93a)
*/
/**
*/
public static $_log = TRUE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/grant/add?reset=1&action=add&context=standalone',
+ 'view' => 'contact/view/grant?reset=1&action=view&id=[id]&cid=[contact_id]',
+ 'update' => 'civicrm/contact/view/grant?reset=1&action=update&id=[id]&cid=[contact_id]',
+ 'delete' => 'civicrm/contact/view/grant?reset=1&action=delete&id=[id]&cid=[contact_id]',
+ ];
+
/**
* Unique Grant id
*
return FALSE;
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return 'civicrm_grant';
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ return 'grant_id';
+ }
+
}
$form->_grantIds = $form->_componentIds = $ids;
- //set the context for redirection for any task actions
- $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form);
- $urlParams = 'force=1';
- if (CRM_Utils_Rule::qfKey($qfKey)) {
- $urlParams .= "&qfKey=$qfKey";
- }
-
- $session = CRM_Core_Session::singleton();
- $session->replaceUserContext(CRM_Utils_System::url('civicrm/grant/search', $urlParams));
+ $form->setNextUrl('grant');
}
/**
'required' => TRUE,
],
'description' => ['name' => 'description'],
+ 'frontend_title' => ['name' => 'frontend_title'],
+ 'frontend_description' => ['name' => 'frontend_description'],
];
}
}
$uploadSize = round(($uploadFileSize / (1024 * 1024)), 2);
$form->assign('uploadSize', $uploadSize);
- $form->add('File', 'uploadFile', ts('Import Data File'), 'size=30 maxlength=255', TRUE);
+ $form->add('File', 'uploadFile', ts('Import Data File'), NULL, TRUE);
$form->setMaxFileSize($uploadFileSize);
$form->addRule('uploadFile', ts('File size should be less than %1 MBytes (%2 bytes)', [
1 => $uploadSize,
$this->assign('uploadSize', $uploadSize);
- $this->add('File', 'uploadFile', ts('Import Data File'), 'size=30 maxlength=255', TRUE);
+ $this->add('File', 'uploadFile', ts('Import Data File'), NULL, TRUE);
$this->setMaxFileSize($uploadFileSize);
$this->addRule('uploadFile', ts('File size should be less than %1 MBytes (%2 bytes)', [
1 => $uploadSize,
*
* @param array $tables
* Array of tables to inspect.
+ * @param int $limit
+ * Limit result to x
+ * @param int $offset
+ * Offset result to y
*
* @return array
*/
- public function getAllChangesForConnection($tables) {
- $params = [1 => [$this->log_conn_id, 'String']];
+ public function getAllChangesForConnection($tables, $limit = 0, $offset = 0) {
+ $params = [
+ 1 => [$this->log_conn_id, 'String'],
+ 2 => [$limit, 'Integer'],
+ 3 => [$offset, 'Integer'],
+ ];
+
foreach ($tables as $table) {
if (empty($sql)) {
$sql = " SELECT '{$table}' as table_name, id FROM {$this->db}.log_{$table} WHERE log_conn_id = %1";
$sql .= " UNION SELECT '{$table}' as table_name, id FROM {$this->db}.log_{$table} WHERE log_conn_id = %1";
}
}
+ if ($limit) {
+ $sql .= " LIMIT %2";
+ }
+ if ($offset) {
+ $sql .= " OFFSET %3";
+ }
$diffs = [];
$dao = CRM_Core_DAO::executeQuery($sql, $params);
while ($dao->fetch()) {
if (empty($this->log_date)) {
- $this->log_date = CRM_Core_DAO::singleValueQuery("SELECT log_date FROM {$this->db}.log_{$table} WHERE log_conn_id = %1 LIMIT 1", $params);
+ // look for available table in above query instead of looking for last table. this will avoid multiple loops
+ $this->log_date = CRM_Core_DAO::singleValueQuery("SELECT log_date FROM {$this->db}.log_{$dao->table_name} WHERE log_conn_id = %1 LIMIT 1", $params);
}
$diffs = array_merge($diffs, $this->diffsInTableForId($dao->table_name, $dao->id));
}
return $diffs;
}
+ /**
+ * Get count of all changes made in the connection.
+ *
+ * @param array $tables
+ * Array of tables to inspect.
+ *
+ * @return array
+ */
+ public function getCountOfAllContactChangesForConnection($tables) {
+ $count = 0;
+ $params = [1 => [$this->log_conn_id, 'String']];
+ foreach ($tables as $table) {
+ if (empty($sql)) {
+ $sql = " SELECT '{$table}' as table_name, id FROM {$this->db}.log_{$table} WHERE log_conn_id = %1";
+ }
+ else {
+ $sql .= " UNION SELECT '{$table}' as table_name, id FROM {$this->db}.log_{$table} WHERE log_conn_id = %1";
+ }
+ }
+ $countSQL = " SELECT count(*) as countOfContacts FROM ({$sql}) count";
+ $count = CRM_Core_DAO::singleValueQuery($countSQL, $params);
+ return $count;
+ }
+
/**
* Check that the log record relates to a unique log id.
*
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Logging_ReportDetail extends CRM_Report_Form {
+
+ const ROW_COUNT_LIMIT = 50;
protected $cid;
/**
// populate $rows with only the differences between $changed and $original (skipping certain columns and NULL ↔ empty changes unless raw requested)
$skipped = ['id'];
+ $nRows = $rows = [];
foreach ($this->diffs as $diff) {
$table = $diff['table'];
if (empty($metadata[$table])) {
$to = '';
}
}
-
- $rows[] = ['field' => $field . " (id: {$diff['id']})", 'from' => $from, 'to' => $to];
+ // Rework the results to provide grouping based on the ID
+ // We don't need that field displayed so we will output empty
+ if ($field == 'Modified Date') {
+ $nRows[$diff['id']][] = ['field' => '', 'from' => $from, 'to' => $to];
+ }
+ else {
+ $nRows[$diff['id']][] = ['field' => $field . " (id: {$diff['id']})", 'from' => $from, 'to' => $to];
+ }
}
+ // Transform the output so that we can compact the changes into the proper amount of rows IF trData is holding more than 1 array
+ foreach ($nRows as $trData) {
+ if (count($trData) > 1) {
+ $keys = array_intersect(...array_map('array_keys', $trData));
+ $mergedRes = array_combine($keys, array_map(function ($key) use ($trData) {
+ // If more than 1 entry is found, we are assigning them as subarrays, then the tpls will be responsible for concatenating the results
+ return array_column($trData, $key);
+ }, $keys));
+ $rows[] = $mergedRes;
+ }
+ else {
+ // We always need the first row of that array
+ $rows[] = $trData[0];
+ }
+ }
return $rows;
}
* Calculate all the contact related diffs for the change.
*/
protected function calculateContactDiffs() {
+ $this->_rowsFound = $this->getCountOfAllContactChangesForConnection();
+ // Apply some limits before asking for all contact changes
+ $this->getLimit();
$this->diffs = $this->getAllContactChangesForConnection();
}
}
$this->setDiffer();
try {
- return $this->differ->getAllChangesForConnection($this->tables);
+ return $this->differ->getAllChangesForConnection($this->tables, $this->dblimit, $this->dboffset);
+ }
+ catch (CRM_Core_Exception $e) {
+ CRM_Core_Error::statusBounce($e->getMessage());
+ }
+ }
+
+ /**
+ * Get an count of contacts with changes.
+ *
+ * @return mixed
+ */
+ public function getCountOfAllContactChangesForConnection() {
+ if (empty($this->log_conn_id)) {
+ return [];
+ }
+ $this->setDiffer();
+ try {
+ return $this->differ->getCountOfAllContactChangesForConnection($this->tables);
}
catch (CRM_Core_Exception $e) {
- CRM_Core_Error::statusBounce(ts($e->getMessage()));
+ CRM_Core_Error::statusBounce($e->getMessage());
}
}
$this->altered_name = CRM_Utils_Request::retrieve('alteredName', 'String');
$this->altered_by = CRM_Utils_Request::retrieve('alteredBy', 'String');
$this->altered_by_id = CRM_Utils_Request::retrieve('alteredById', 'Integer');
+ $this->layout = CRM_Utils_Request::retrieve('layout', 'String');
+ }
+
+ /**
+ * Override to set limit
+ * @param int $rowCount
+ */
+ public function limit($rowCount = self::ROW_COUNT_LIMIT) {
+ parent::limit($rowCount);
+ }
+
+ /**
+ * Override to set pager with limit
+ * @param int $rowCount
+ */
+ public function setPager($rowCount = self::ROW_COUNT_LIMIT) {
+ // We should not be rendering the pager in overlay mode
+ if (!isset($this->layout)) {
+ $this->_dashBoardRowCount = $rowCount;
+ $this->_limit = TRUE;
+ parent::setPager($rowCount);
+ }
+ }
+
+ /**
+ * This is a function similar to limit, in fact we copied it as-is and removed
+ * some `set` statements
+ *
+ */
+ public function getLimit($rowCount = self::ROW_COUNT_LIMIT) {
+ if ($this->addPaging) {
+
+ $pageId = CRM_Utils_Request::retrieve('crmPID', 'Integer');
+
+ // @todo all http vars should be extracted in the preProcess
+ // - not randomly in the class
+ if (!$pageId && !empty($_POST)) {
+ if (isset($_POST['PagerBottomButton']) && isset($_POST['crmPID_B'])) {
+ $pageId = max((int) $_POST['crmPID_B'], 1);
+ }
+ elseif (isset($_POST['PagerTopButton']) && isset($_POST['crmPID'])) {
+ $pageId = max((int) $_POST['crmPID'], 1);
+ }
+ unset($_POST['crmPID_B'], $_POST['crmPID']);
+ }
+
+ $pageId = $pageId ? $pageId : 1;
+ $offset = ($pageId - 1) * $rowCount;
+
+ $offset = CRM_Utils_Type::escape($offset, 'Int');
+ $rowCount = CRM_Utils_Type::escape($rowCount, 'Int');
+ $this->_limit = " LIMIT $offset, $rowCount";
+ $this->dblimit = $rowCount;
+ $this->dboffset = $offset;
+ }
}
}
$config->logging = TRUE;
}
if ($config->logging) {
- $this->fixSchemaDifferencesForALL();
+ $this->fixSchemaDifferencesForAll();
}
// invoke the meta trigger creation call
CRM_Core_DAO::triggerRebuild(NULL, TRUE);
$limitString = "LIMIT $offset, $limit";
}
- $isSMSmode = CRM_Core_DAO::getFieldValue('CRM_Mailing_BAO_Mailing', $mailingID, 'sms_provider_id', 'id');
- $additionalJoin = '';
- if (!$isSMSmode) {
- // mailing_recipients added when mailing is submitted in UI by user.
- // if any email is marked on_hold =1 or contact is deceased after mailing is submitted
- // then it should be get skipped while preparing event_queue
- // event_queue list is prepared when mailing job gets started.
- $additionalJoin = " INNER JOIN civicrm_email e ON (r.email_id = e.id AND e.on_hold = 0)
- INNER JOIN civicrm_contact c on (c.id = r.contact_id AND c.is_deceased <> 1 AND c.do_not_email = 0 AND c.is_opt_out = 0)
-";
- }
- else {
- $additionalJoin = "INNER JOIN civicrm_contact c on (c.id = r.contact_id AND c.is_deceased <> 1 AND c.do_not_sms = 0 AND c.is_opt_out = 0)";
- }
+ $isSMSMode = CRM_Core_DAO::getFieldValue('CRM_Mailing_BAO_Mailing', $mailingID, 'sms_provider_id', 'id');
+ $additionalJoin = $isSMSMode ? '' : " INNER JOIN civicrm_email e ON (r.email_id = e.id AND e.on_hold = 0)";
$sql = "
-SELECT r.contact_id, r.email_id, r.phone_id
-FROM civicrm_mailing_recipients r
-{$additionalJoin}
-WHERE r.mailing_id = %1
- $limitString
-";
+ SELECT r.contact_id, r.email_id, r.phone_id
+ FROM civicrm_mailing_recipients r
+ INNER JOIN civicrm_contact c on
+ (c.id = r.contact_id
+ AND c.is_deleted = 0
+ AND c.is_deceased = 0
+ AND c.do_not_" . ($isSMSMode ? 'sms' : 'email') . " = 0
+ AND c.is_opt_out = 0
+ )
+ {$additionalJoin}
+ WHERE r.mailing_id = %1
+ $limitString
+ ";
$params = [1 => [$mailingID, 'Integer']];
return CRM_Core_DAO::executeQuery($sql, $params);
*
* Generated from xml/schema/CRM/Mailing/Mailing.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:9cd784dc86cf4f54983f14440be05239)
+ * (GenCodeChecksum:f1ac9d0a02ed59171d4efdc4209a376c)
*/
/**
*/
public static $_log = FALSE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/a/#/mailing/new',
+ 'update' => 'civicrm/a/#/mailing/[id]',
+ ];
+
/**
* @var int
*/
$do = CRM_Core_DAO::executeQuery("
SELECT grp.id as group_id,
grp.title as title,
+ grp.frontend_title as frontend_title,
+ grp.frontend_description as frontend_description,
grp.description as description
FROM civicrm_group grp
LEFT JOIN civicrm_group_contact gc
$returnGroups = [];
while ($do->fetch()) {
$returnGroups[$do->group_id] = [
- 'title' => $do->title,
- 'description' => $do->description,
+ 'title' => !empty($do->frontend_title) ? $do->frontend_title : $do->title,
+ 'description' => !empty($do->frontend_description) ? $do->frontend_description : $do->description,
];
}
return $returnGroups;
}
else {
while ($do->fetch()) {
- $groups[$do->group_id] = $do->title;
+ $groups[$do->group_id] = !empty($do->frontend_title) ? $do->frontend_title : $do->title;
}
}
$transaction = new CRM_Core_Transaction();
}
$component = CRM_Mailing_BAO_MailingComponent::add($params);
+
+ // set the id after save, so it can be used in a extension using the postProcess hook
+ $this->_id = $component->id;
+
CRM_Core_Session::setStatus(ts('The mailing component \'%1\' has been saved.', [
1 => $component->name,
]), ts('Saved'), 'success');
// make sure requested qroup is accessible and exists
$query = "
-SELECT title, description
+SELECT title, frontend_title, description, frontend_description
FROM civicrm_group
WHERE id={$this->_groupID}
AND visibility != 'User and User Admin Only'
$dao = CRM_Core_DAO::executeQuery($query);
if ($dao->fetch()) {
- $this->assign('groupName', $dao->title);
- CRM_Utils_System::setTitle(ts('Subscribe to Mailing List - %1', [1 => $dao->title]));
+ $this->assign('groupName', !empty($dao->frontend_title) ? $dao->frontend_title : $dao->title);
+ CRM_Utils_System::setTitle(ts('Subscribe to Mailing List - %1', [1 => !empty($dao->frontend_title) ? $dao->frontend_title : $dao->title]));
}
else {
CRM_Core_Error::statusBounce("The specified group is not configured for this action OR The group doesn't exist.");
$groupTypeCondition = CRM_Contact_BAO_Group::groupTypeCondition('Mailing');
$query = "
-SELECT id, title, description
+SELECT id, title, frontend_title, description, frontend_description
FROM civicrm_group
WHERE ( saved_search_id = 0
OR saved_search_id IS NULL )
while ($dao->fetch()) {
$row = [];
$row['id'] = $dao->id;
- $row['title'] = $dao->title;
- $row['description'] = $dao->description;
+ $row['title'] = $dao->frontend_title ?? $dao->title;
+ $row['description'] = $dao->frontend_description ?? $dao->description;
$row['checkbox'] = CRM_Core_Form::CB_PREFIX . $row['id'];
$this->addElement('checkbox',
$row['checkbox'],
protected $keyword = 'mailing';
/**
- * @inheritDoc
* @return array
*/
- public function getInfo() {
- return [
- 'name' => 'CiviMail',
- 'translatedName' => ts('CiviMail'),
- 'title' => ts('CiviCRM Mailing Engine'),
- 'search' => 1,
- 'showActivitiesInCore' => 1,
- ];
- }
-
- /**
- * Get AngularJS modules and their dependencies.
- *
- * @return array
- * list of modules; same format as CRM_Utils_Hook::angularModules(&$angularModules)
- * @see CRM_Utils_Hook::angularModules
- */
- public function getAngularModules() {
- // load angular files only if valid permissions are granted to the user
- if (!CRM_Core_Permission::check('access CiviMail')
- && !CRM_Core_Permission::check('create mailings')
- && !CRM_Core_Permission::check('schedule mailings')
- && !CRM_Core_Permission::check('approve mailings')
- ) {
- return [];
- }
- global $civicrm_root;
-
+ public static function createAngularSettings():array {
$reportIds = [];
$reportTypes = ['detail', 'opened', 'bounce', 'clicks'];
foreach ($reportTypes as $report) {
- $result = civicrm_api3('ReportInstance', 'get', [
+ $rptResult = civicrm_api3('ReportInstance', 'get', [
'sequential' => 1,
'report_id' => 'mailing/' . $report,
]);
- if (!empty($result['values'])) {
- $reportIds[$report] = $result['values'][0]['id'];
+ if (!empty($rptResult['values'])) {
+ $reportIds[$report] = $rptResult['values'][0]['id'];
}
}
- $result = [];
- $result['crmMailing'] = include "$civicrm_root/ang/crmMailing.ang.php";
- $result['crmMailingAB'] = include "$civicrm_root/ang/crmMailingAB.ang.php";
- $result['crmD3'] = include "$civicrm_root/ang/crmD3.ang.php";
$config = CRM_Core_Config::singleton();
$session = CRM_Core_Session::singleton();
]);
$headerfooterList = civicrm_api3('MailingComponent', 'get', $params + [
'is_active' => 1,
- 'return' => ['name', 'component_type', 'is_default', 'body_html', 'body_text'],
+ 'return' => [
+ 'name',
+ 'component_type',
+ 'is_default',
+ 'body_html',
+ 'body_text',
+ ],
]);
$emailAdd = civicrm_api3('Email', 'get', [
$enabledLanguages = CRM_Core_I18n::languages(TRUE);
$isMultiLingual = (count($enabledLanguages) > 1);
// FlexMailer is a refactoring of CiviMail which provides new hooks/APIs/docs. If the sysadmin has opted to enable it, then use that instead of CiviMail.
- $requiredTokens = defined('CIVICRM_FLEXMAILER_HACK_REQUIRED_TOKENS') ? Civi\Core\Resolver::singleton()->call(CIVICRM_FLEXMAILER_HACK_REQUIRED_TOKENS, []) : CRM_Utils_Token::getRequiredTokens();
- CRM_Core_Resources::singleton()
- ->addSetting([
- 'crmMailing' => [
- 'templateTypes' => CRM_Mailing_BAO_Mailing::getTemplateTypes(),
- 'civiMails' => [],
- 'campaignEnabled' => in_array('CiviCampaign', $config->enableComponents),
- 'groupNames' => [],
- // @todo this is not used in core. Remove once Mosaico no longer depends on it.
- 'testGroupNames' => $groupNames['values'],
- 'headerfooterList' => $headerfooterList['values'],
- 'mesTemplate' => $mesTemplate['values'],
- 'emailAdd' => $emailAdd['values'],
- 'mailTokens' => $mailTokens['values'],
- 'contactid' => $contactID,
- 'requiredTokens' => $requiredTokens,
- 'enableReplyTo' => (int) Civi::settings()->get('replyTo'),
- 'disableMandatoryTokensCheck' => (int) Civi::settings()->get('disable_mandatory_tokens_check'),
- 'fromAddress' => $fromAddress['values'],
- 'defaultTestEmail' => civicrm_api3('Contact', 'getvalue', [
- 'id' => 'user_contact_id',
- 'return' => 'email',
- ]),
- 'visibility' => CRM_Utils_Array::makeNonAssociative(CRM_Core_SelectValues::groupVisibility()),
- 'workflowEnabled' => CRM_Mailing_Info::workflowEnabled(),
- 'reportIds' => $reportIds,
- 'enabledLanguages' => $enabledLanguages,
- 'isMultiLingual' => $isMultiLingual,
- ],
- ])
- ->addPermissions([
- 'view all contacts',
- 'edit all contacts',
- 'access CiviMail',
- 'create mailings',
- 'schedule mailings',
- 'approve mailings',
- 'delete in CiviMail',
- 'edit message templates',
- ]);
+ $requiredTokens = defined('CIVICRM_FLEXMAILER_HACK_REQUIRED_TOKENS') ? Civi\Core\Resolver::singleton()
+ ->call(CIVICRM_FLEXMAILER_HACK_REQUIRED_TOKENS,
+ []) : CRM_Utils_Token::getRequiredTokens();
+ $crmMailingSettings = [
+ 'templateTypes' => CRM_Mailing_BAO_Mailing::getTemplateTypes(),
+ 'civiMails' => [],
+ 'campaignEnabled' => in_array('CiviCampaign', $config->enableComponents),
+ 'groupNames' => [],
+ // @todo this is not used in core. Remove once Mosaico no longer depends on it.
+ 'testGroupNames' => $groupNames['values'],
+ 'headerfooterList' => $headerfooterList['values'],
+ 'mesTemplate' => $mesTemplate['values'],
+ 'emailAdd' => $emailAdd['values'],
+ 'mailTokens' => $mailTokens['values'],
+ 'contactid' => $contactID,
+ 'requiredTokens' => $requiredTokens,
+ 'enableReplyTo' => (int) Civi::settings()->get('replyTo'),
+ 'disableMandatoryTokensCheck' => (int) Civi::settings()
+ ->get('disable_mandatory_tokens_check'),
+ 'fromAddress' => $fromAddress['values'],
+ 'defaultTestEmail' => civicrm_api3('Contact', 'getvalue', [
+ 'id' => 'user_contact_id',
+ 'return' => 'email',
+ ]),
+ 'visibility' => CRM_Utils_Array::makeNonAssociative(CRM_Core_SelectValues::groupVisibility()),
+ 'workflowEnabled' => CRM_Mailing_Info::workflowEnabled(),
+ 'reportIds' => $reportIds,
+ 'enabledLanguages' => $enabledLanguages,
+ 'isMultiLingual' => $isMultiLingual,
+ ];
+ return $crmMailingSettings;
+ }
+
+ /**
+ * @inheritDoc
+ * @return array
+ */
+ public function getInfo() {
+ return [
+ 'name' => 'CiviMail',
+ 'translatedName' => ts('CiviMail'),
+ 'title' => ts('CiviCRM Mailing Engine'),
+ 'search' => 1,
+ 'showActivitiesInCore' => 1,
+ ];
+ }
+
+ /**
+ * Get AngularJS modules and their dependencies.
+ *
+ * @return array
+ * list of modules; same format as CRM_Utils_Hook::angularModules(&$angularModules)
+ * @see CRM_Utils_Hook::angularModules
+ */
+ public function getAngularModules() {
+ // load angular files only if valid permissions are granted to the user
+ if (!CRM_Core_Permission::check('access CiviMail')
+ && !CRM_Core_Permission::check('create mailings')
+ && !CRM_Core_Permission::check('schedule mailings')
+ && !CRM_Core_Permission::check('approve mailings')
+ ) {
+ return [];
+ }
+ global $civicrm_root;
+
+ $result = [];
+ $result['crmMailing'] = include "$civicrm_root/ang/crmMailing.ang.php";
+ $result['crmMailingAB'] = include "$civicrm_root/ang/crmMailingAB.ang.php";
+ $result['crmD3'] = include "$civicrm_root/ang/crmD3.ang.php";
return $result;
}
* Name of the settings set from civimail_mail_settings to use (null for default).
*
* @throws Exception
- * @return object
+ * @return CRM_Mailing_MailStore
* mail store implementation for processing CiviMail-bound emails
*/
public static function getStore($name = NULL) {
}
$protocols = CRM_Core_PseudoConstant::get('CRM_Core_DAO_MailSettings', 'protocol', [], 'validate');
- if (empty($protocols[$dao->protocol])) {
- throw new Exception("Empty mail protocol");
+
+ // Prepare normalized/hookable representation of the mail settings.
+ $mailSettings = $dao->toArray();
+ $mailSettings['protocol'] = $protocols[$mailSettings['protocol']] ?? NULL;
+ $protocolDefaults = self::getProtocolDefaults($mailSettings['protocol']);
+ $mailSettings = array_merge($protocolDefaults, $mailSettings);
+
+ CRM_Utils_Hook::alterMailStore($mailSettings);
+
+ if (!empty($mailSettings['factory'])) {
+ return call_user_func($mailSettings['factory'], $mailSettings);
}
+ else {
+ throw new Exception("Unknown protocol {$mailSettings['protocol']}");
+ }
+ }
- switch ($protocols[$dao->protocol]) {
+ /**
+ * @param string $protocol
+ * Ex: 'IMAP', 'Maildir'
+ * @return array
+ * List of properties to merge into the $mailSettings.
+ * The most important property is 'factory' with signature:
+ *
+ * function($mailSettings): CRM_Mailing_MailStore
+ */
+ private static function getProtocolDefaults($protocol) {
+ switch ($protocol) {
case 'IMAP':
- return new CRM_Mailing_MailStore_Imap($dao->server, $dao->username, $dao->password, (bool) $dao->is_ssl, $dao->source);
-
case 'IMAP_XOAUTH2':
- return new CRM_Mailing_MailStore_Imap($dao->server, $dao->username, $dao->password, (bool) $dao->is_ssl, $dao->source, TRUE);
+ return [
+ // For backward compat with pre-release XOAuth2 configurations
+ 'auth' => $protocol === 'IMAP_XOAUTH2' ? 'XOAuth2' : 'Password',
+ // In a simpler world:
+ // 'auth' => 'Password',
+ 'factory' => function($mailSettings) {
+ $useXOAuth2 = ($mailSettings['auth'] === 'XOAuth2');
+ return new CRM_Mailing_MailStore_Imap($mailSettings['server'], $mailSettings['username'], $mailSettings['password'], (bool) $mailSettings['is_ssl'], $mailSettings['source'], $useXOAuth2);
+ },
+ ];
case 'POP3':
- return new CRM_Mailing_MailStore_Pop3($dao->server, $dao->username, $dao->password, (bool) $dao->is_ssl);
+ return [
+ 'factory' => function ($mailSettings) {
+ return new CRM_Mailing_MailStore_Pop3($mailSettings['server'], $mailSettings['username'], $mailSettings['password'], (bool) $mailSettings['is_ssl']);
+ },
+ ];
case 'Maildir':
- return new CRM_Mailing_MailStore_Maildir($dao->source);
+ return [
+ 'factory' => function ($mailSettings) {
+ return new CRM_Mailing_MailStore_Maildir($mailSettings['source']);
+ },
+ ];
case 'Localdir':
- return new CRM_Mailing_MailStore_Localdir($dao->source);
+ return [
+ 'factory' => function ($mailSettings) {
+ return new CRM_Mailing_MailStore_Localdir($mailSettings['source']);
+ },
+ ];
// DO NOT USE the mbox transport for anything other than testing
// in particular, it does not clear the mbox afterwards
-
case 'mbox':
- return new CRM_Mailing_MailStore_Mbox($dao->source);
+ return [
+ 'factory' => function ($mailSettings) {
+ return new CRM_Mailing_MailStore_Mbox($mailSettings['source']);
+ },
+ ];
default:
- throw new Exception("Unknown protocol {$dao->protocol}");
+ return [];
}
}
*/
class CRM_Mailing_Page_AJAX {
+ /**
+ * Kick off the "Add Mail Account" process for some given type of account.
+ *
+ * Ex: 'civicrm/ajax/setupMailAccount?type=standard'
+ * Ex: 'civicrm/ajax/setupMailAccount?type=oauth_1'
+ *
+ * @see CRM_Core_BAO_MailSettings::getSetupActions()
+ * @throws \CRM_Core_Exception
+ */
+ public static function setup() {
+ $type = CRM_Utils_Request::retrieve('type', 'String');
+ $setupActions = CRM_Core_BAO_MailSettings::getSetupActions();
+ $setupAction = $setupActions[$type] ?? NULL;
+ if ($setupAction === NULL) {
+ throw new \CRM_Core_Exception("Cannot setup mail account. Invalid type requested.");
+ }
+
+ $result = call_user_func($setupAction['callback'], $setupAction);
+ if (isset($result['url'])) {
+ CRM_Utils_System::redirect($result['url']);
+ }
+ else {
+ throw new \CRM_Core_Exception("Cannot setup mail account. Setup does not have a URL.");
+ }
+ }
+
/**
* Fetch the template text/html messages
*/
public function run() {
$queue_id = CRM_Utils_Request::retrieveValue('qid', 'Integer');
$url_id = CRM_Utils_Request::retrieveValue('u', 'Integer', NULL, TRUE);
- $url = CRM_Mailing_Event_BAO_TrackableURLOpen::track($queue_id, $url_id);
+ $url = trim(CRM_Mailing_Event_BAO_TrackableURLOpen::track($queue_id, $url_id));
$query_string = $this->extractPassthroughParameters();
if (strlen($query_string) > 0) {
<page_callback>CRM_Mailing_Page_AJAX::getContactMailings</page_callback>
<access_arguments>access CiviCRM</access_arguments>
</item>
+ <item>
+ <path>civicrm/ajax/setupMailAccount</path>
+ <page_callback>CRM_Mailing_Page_AJAX::setup</page_callback>
+ <access_arguments>access CiviCRM,access CiviMail</access_arguments>
+ </item>
<item>
<path>civicrm/mailing/url</path>
<page_callback>CRM_Mailing_Page_Url</page_callback>
* @throws \CiviCRM_API3_Exception
*/
public static function createRelatedMemberships($params, $dao) {
-
+ unset($params['membership_id']);
$membership = new CRM_Member_DAO_Membership();
$membership->id = $dao->id;
// CRM-20966: Do not create membership_payment record for inherited membership.
unset($params['relate_contribution_id']);
- $ids = [];
if (($params['status_id'] == $deceasedStatusId) || ($params['status_id'] == $expiredStatusId)) {
// related membership is not active so does not count towards maximum
if (!self::hasExistingInheritedMembership($params)) {
- CRM_Member_BAO_Membership::create($params);
+ civicrm_api3('Membership', 'create', $params);
}
}
else {
}
/**
- * Build an array of available membership types.
+ * Build an array of available membership types in the current context.
+ *
+ * While core does not do anything context specific extensions may filter
+ * or alter amounts based on user details.
*
* @param CRM_Core_Form $form
* @param array $membershipTypeID
*/
public static function buildMembershipTypeValues($form, $membershipTypeID = [], $activeOnly = FALSE) {
$membershipTypeIDS = (array) $membershipTypeID;
- $membershipTypeValues = CRM_Member_BAO_MembershipType::getPermissionedMembershipTypes();
+ $membershipTypeValues = CRM_Member_BAO_MembershipType::getAllMembershipTypes();
// MembershipTypes are already filtered by domain, filter as appropriate by is_active & a passed in list of ids.
foreach ($membershipTypeValues as $id => $type) {
}
CRM_Utils_Hook::membershipTypeValues($form, $membershipTypeValues);
-
- if (is_numeric($membershipTypeID) &&
- $membershipTypeID > 0
- ) {
- CRM_Core_Error::deprecatedFunctionWarning('Non arrays deprecated');
- return $membershipTypeValues[$membershipTypeID];
- }
return $membershipTypeValues;
}
* @param int $contributionRecurID
* @param $membershipSource
* @param $isPayLater
- * @param int $campaignId
+ * @param array $memParams
* @param array $formDates
* @param null|CRM_Contribute_BAO_Contribution $contribution
* @param array $lineItems
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
- public static function processMembership($contactID, $membershipTypeID, $is_test, $changeToday, $modifiedID, $customFieldsFormatted, $numRenewTerms, $membershipID, $pending, $contributionRecurID, $membershipSource, $isPayLater, $campaignId, $formDates = [], $contribution = NULL, $lineItems = []) {
+ public static function processMembership($contactID, $membershipTypeID, $is_test, $changeToday, $modifiedID, $customFieldsFormatted, $numRenewTerms, $membershipID, $pending, $contributionRecurID, $membershipSource, $isPayLater, $memParams = [], $formDates = [], $contribution = NULL, $lineItems = []) {
$renewalMode = $updateStatusId = FALSE;
$allStatus = CRM_Member_PseudoConstant::membershipStatus();
$format = '%Y%m%d';
array_search('Cancelled', CRM_Member_PseudoConstant::membershipStatus(NULL, " name = 'Cancelled' ", 'name', FALSE, TRUE)),
])) {
- $memParams = [
+ $memParams = array_merge([
'id' => $currentMembership['id'],
'contribution' => $contribution,
'status_id' => $currentMembership['status_id'],
'membership_type_id' => $membershipTypeID,
'max_related' => !empty($membershipTypeDetails['max_related']) ? $membershipTypeDetails['max_related'] : NULL,
'membership_activity_status' => ($pending || $isPayLater) ? 'Scheduled' : 'Completed',
- ];
+ ], $memParams);
if ($contributionRecurID) {
$memParams['contribution_recur_id'] = $contributionRecurID;
}
if (!empty($currentMembership['id'])) {
$ids['membership'] = $currentMembership['id'];
}
- $memParams = $currentMembership;
+ $memParams = array_merge($currentMembership, $memParams);
$memParams['membership_type_id'] = $membershipTypeID;
//set the log start date.
);
// Insert renewed dates for CURRENT membership
- $memParams = [];
$memParams['join_date'] = CRM_Utils_Date::isoToMysql($membership->join_date);
$memParams['start_date'] = $formDates['start_date'] ?? CRM_Utils_Date::isoToMysql($membership->start_date);
$memParams['end_date'] = $formDates['end_date'] ?? NULL;
}
else {
// NEW Membership
- $memParams = [
+ $memParams = array_merge([
'contact_id' => $contactID,
'membership_type_id' => $membershipTypeID,
- ];
+ ], $memParams);
if (!$pending) {
$dates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($membershipTypeID, NULL, NULL, NULL, $numRenewTerms);
}
$params['modified_id'] = $modifiedID ?? $contactID;
- //inherit campaign from contrib page.
- if (isset($campaignId)) {
- $memParams['campaign_id'] = $campaignId;
- }
-
$memParams['contribution'] = $contribution;
$memParams['custom'] = $customFieldsFormatted;
// Load all line items & process all in membership. Don't do in contribution.
return self::getAllMembershipTypes()[$id];
}
- /**
- * Get an array of all membership types the contact is permitted to access.
- *
- * @throws \CiviCRM_API3_Exception
- */
- public static function getPermissionedMembershipTypes() {
- $types = self::getAllMembershipTypes();
- $financialTypes = NULL;
- $financialTypes = CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($financialTypes, CRM_Core_Action::ADD);
- foreach ($types as $id => $type) {
- if (!isset($financialTypes[$type['financial_type_id']])) {
- unset($types[$id]);
- }
- }
- return $types;
- }
-
}
*/
class CRM_Member_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Membership';
+
/**
* Class constructor.
*
$this->addPages($this->_stateMachine, $action);
// add all the actions
- $config = CRM_Core_Config::singleton();
$this->addActions();
+ $this->set('entity', $this->entity);
}
}
*
* Generated from xml/schema/CRM/Member/Membership.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:d80be256fb175b763047883b8694559c)
+ * (GenCodeChecksum:2f2fd321dc15b2e2f453fc600d994c9a)
*/
/**
*/
public static $_log = TRUE;
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = [
+ 'add' => 'civicrm/member/add?reset=1&action=add&context=standalone',
+ 'view' => 'civicrm/contact/view/membership?reset=1&action=view&id=[id]&cid=[contact_id]',
+ 'update' => 'civicrm/contact/view/membership?reset=1&action=update&id=[id]&cid=[contact_id]',
+ 'delete' => 'civicrm/contact/view/membership?reset=1&action=delete&id=[id]&cid=[contact_id]',
+ ];
+
/**
* Membership Id
*
return FALSE;
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return 'civicrm_membership';
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ return 'membership_id';
+ }
+
}
$form->assign('formValues', $formValues);
if (empty($lineItem)) {
- $form->assign('mem_start_date', CRM_Utils_Date::customFormat($membership->start_date, '%B %E%f, %Y'));
+ $form->assign('mem_start_date', CRM_Utils_Date::formatDateOnlyLong($membership->start_date));
if (!CRM_Utils_System::isNull($membership->end_date)) {
- $form->assign('mem_end_date', CRM_Utils_Date::customFormat($membership->end_date, '%B %E%f, %Y'));
+ $form->assign('mem_end_date', CRM_Utils_Date::formatDateOnlyLong($membership->end_date));
}
$form->assign('membership_name', CRM_Member_PseudoConstant::membershipType($membership->membership_type_id));
}
// BEGIN Fix for dev/core/issues/860
// Prepare fee block and call buildAmount hook - based on CRM_Price_BAO_PriceSet::buildPriceSet().
- CRM_Price_BAO_PriceSet::applyACLFinancialTypeStatusToFeeBlock($this->_priceSet['fields']);
CRM_Utils_Hook::buildAmount('membership', $this, $this->_priceSet['fields']);
// END Fix for dev/core/issues/860
$params['contribution_id'] = $this->_onlinePendingContributionId;
$params['componentId'] = $params['id'];
$params['componentName'] = 'contribute';
+ // Only available statuses are Pending and completed so cancel or failed is not possible here.
$result = CRM_Contribute_BAO_Contribution::transitionComponents($params, TRUE);
if (!empty($result) && !empty($params['contribution_id'])) {
$lineItem = [];
$params['receive_date'] = date('Y-m-d H:i:s');
}
$membershipParams = array_merge($params, $membershipTypeValues[$memType]);
- if (!empty($formValues['int_amount'])) {
- $init_amount = [];
- foreach ($formValues as $key => $value) {
- if (strstr($key, 'txt-price')) {
- $init_amount[$key] = $value;
- }
- }
- $membershipParams['init_amount'] = $init_amount;
- }
if (!empty($softParams)) {
$membershipParams['soft_credit'] = $softParams;
$this->addStatusMessage($this->getStatusMessageForUpdate($membership, $endDate));
}
elseif (($this->_action & CRM_Core_Action::ADD)) {
- $this->addStatusMessage($this->getStatusMessageForCreate($endDate, $membershipTypes, $createdMemberships,
+ $this->addStatusMessage($this->getStatusMessageForCreate($endDate, $createdMemberships,
$isRecur, $calcDates));
}
$totalTaxAmount = 0;
foreach ($lineItem[$this->_priceSetId] as & $priceFieldOp) {
if (!empty($priceFieldOp['membership_type_id'])) {
- $priceFieldOp['start_date'] = $membershipTypeValues[$priceFieldOp['membership_type_id']]['start_date'] ? CRM_Utils_Date::customFormat($membershipTypeValues[$priceFieldOp['membership_type_id']]['start_date'], '%B %E%f, %Y') : '-';
- $priceFieldOp['end_date'] = $membershipTypeValues[$priceFieldOp['membership_type_id']]['end_date'] ? CRM_Utils_Date::customFormat($membershipTypeValues[$priceFieldOp['membership_type_id']]['end_date'], '%B %E%f, %Y') : '-';
+ $priceFieldOp['start_date'] = $membershipTypeValues[$priceFieldOp['membership_type_id']]['start_date'] ? CRM_Utils_Date::formatDateOnlyLong($membershipTypeValues[$priceFieldOp['membership_type_id']]['start_date']) : '-';
+ $priceFieldOp['end_date'] = $membershipTypeValues[$priceFieldOp['membership_type_id']]['end_date'] ? CRM_Utils_Date::formatDateOnlyLong($membershipTypeValues[$priceFieldOp['membership_type_id']]['end_date']) : '-';
}
else {
$priceFieldOp['start_date'] = $priceFieldOp['end_date'] = 'N/A';
* Get status message for create action.
*
* @param string $endDate
- * @param array $membershipTypes
* @param array $createdMemberships
* @param bool $isRecur
* @param array $calcDates
*
* @return array|string
*/
- protected function getStatusMessageForCreate($endDate, $membershipTypes, $createdMemberships,
+ protected function getStatusMessageForCreate($endDate, $createdMemberships,
$isRecur, $calcDates) {
// FIX ME: fix status messages
$statusMsg = [];
- foreach ($membershipTypes as $memType => $membershipType) {
- $statusMsg[$memType] = ts('%1 membership for %2 has been added.', [
- 1 => $membershipType,
+ foreach ($this->_memTypeSelected as $membershipTypeID) {
+ $statusMsg[$membershipTypeID] = ts('%1 membership for %2 has been added.', [
+ 1 => $this->allMembershipTypeDetails[$membershipTypeID]['name'],
2 => $this->_memberDisplayName,
]);
- $membership = $createdMemberships[$memType];
+ $membership = $createdMemberships[$membershipTypeID];
$memEndDate = $membership->end_date ?: $endDate;
//get the end date from calculated dates.
if (!$memEndDate && !$isRecur) {
- $memEndDate = $calcDates[$memType]['end_date'] ?? NULL;
+ $memEndDate = $calcDates[$membershipTypeID]['end_date'] ?? NULL;
}
if ($memEndDate && $memEndDate !== 'null') {
- $memEndDate = CRM_Utils_Date::customFormat($memEndDate);
- $statusMsg[$memType] .= ' ' . ts('The new membership End Date is %1.', [1 => $memEndDate]);
+ $memEndDate = CRM_Utils_Date::formatDateOnlyLong($memEndDate);
+ $statusMsg[$membershipTypeID] .= ' ' . ts('The new membership End Date is %1.', [1 => $memEndDate]);
}
}
$statusMsg = implode('<br/>', $statusMsg);
$membership->membership_type_id
));
$this->assign('customValues', $customValues);
- $this->assign('mem_start_date', CRM_Utils_Date::customFormat($membership->start_date));
- $this->assign('mem_end_date', CRM_Utils_Date::customFormat($membership->end_date));
+ $this->assign('mem_start_date', CRM_Utils_Date::formatDateOnlyLong($membership->start_date));
+ $this->assign('mem_end_date', CRM_Utils_Date::formatDateOnlyLong($membership->end_date));
if ($this->_mode) {
$this->assign('address', CRM_Utils_Address::getFormattedBillingAddressFieldsFromParameters(
$this->_params,
}
$form->_memberIds = $form->_componentIds = $ids;
-
- //set the context for redirection for any task actions
- $session = CRM_Core_Session::singleton();
-
- $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form);
- $urlParams = 'force=1';
- if (CRM_Utils_Rule::qfKey($qfKey)) {
- $urlParams .= "&qfKey=$qfKey";
- }
-
- $searchFormName = strtolower($form->get('searchFormName'));
- if ($searchFormName === 'search') {
- $session->replaceUserContext(CRM_Utils_System::url('civicrm/member/search', $urlParams));
- }
- else {
- $session->replaceUserContext(CRM_Utils_System::url("civicrm/contact/search/$searchFormName",
- $urlParams
- ));
- }
+ $form->setNextUrl('member');
}
/**
$params[$key] = $this->parsePseudoConstantField($val, $this->fieldMetadata[$key]);
break;
- case 'member_is_override':
- $params[$key] = CRM_Utils_String::strtobool($val);
- break;
}
if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
if ($customFields[$customFieldID]['data_type'] == 'Date') {
$formatValues = [];
foreach ($params as $key => $field) {
- if ($field == NULL || $field === '') {
+ // ignore empty values or empty arrays etc
+ if (CRM_Utils_System::isNull($field)) {
continue;
}
CRM_Price_BAO_LineItem::getLineItemArray($formatted, NULL, 'membership', $formatted['membership_type_id']);
}
- // @todo stop passing $ids array (and put details in $formatted if required)
- $ids = [
- 'membership' => $formatValues['membership_id'],
- 'userId' => $session->get('userID'),
- ];
- $newMembership = CRM_Member_BAO_Membership::create($formatted, $ids, TRUE);
- if (civicrm_error($newMembership)) {
- array_unshift($values, $newMembership['is_error'] . ' for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.');
- return CRM_Import_Parser::ERROR;
- }
- else {
- $this->_newMemberships[] = $newMembership->id;
- return CRM_Import_Parser::VALID;
- }
+ $newMembership = civicrm_api3('Membership', 'create', $formatted);
+ $this->_newMemberships[] = $newMembership['id'];
+ return CRM_Import_Parser::VALID;
}
else {
array_unshift($values, 'Matching Membership record not found for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.');
$customFields = CRM_Core_BAO_CustomField::getFields('Membership');
foreach ($params as $key => $value) {
- // ignore empty values or empty arrays etc
- if (CRM_Utils_System::isNull($value)) {
- continue;
- }
//Handling Custom Data
if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
$contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'label');
foreach ($result['values'] as $payment) {
- $recurringContributionID = $payment['contribution_id.contribution_recur_id.id'];
+ $recurringContributionID = (int) $payment['contribution_id.contribution_recur_id.id'];
$alreadyProcessed = isset($recurringContributions[$recurringContributionID]);
if ($alreadyProcessed) {
* @param int $recurID
* @param array $recurringContribution
*/
- private function setActionsForRecurringContribution($recurID, &$recurringContribution) {
+ private function setActionsForRecurringContribution(int $recurID, &$recurringContribution) {
$action = array_sum(array_keys(CRM_Contribute_Page_Tab::recurLinks($recurID, 'contribution')));
// no action allowed if it's not active
}
$attrib = ['rows' => 8, 'cols' => 60];
- $this->add('textarea', 'page_text', ts('Your Message'), NULL, FALSE);
+ $this->add('wysiwyg', 'page_text', ts('Your Message'), NULL, FALSE);
$maxAttachments = 1;
CRM_Core_BAO_File::buildAttachment($this, 'civicrm_pcp', $this->_pageId, $maxAttachments);
*/
class CRM_Pledge_Controller_Search extends CRM_Core_Controller {
+ protected $entity = 'Pledge';
+
/**
* Class constructor.
*
$this->addPages($this->_stateMachine, $action);
// add all the actions
- $config = CRM_Core_Config::singleton();
$this->addActions();
+ $this->set('entity', $this->entity);
}
}
return FALSE;
}
+ /**
+ * Get the name of the table for the relevant entity.
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return 'civicrm_pledge';
+ }
+
+ /**
+ * Get the group by clause for the component.
+ *
+ * @return string
+ */
+ public function getEntityAliasField() {
+ return 'pledge_id';
+ }
+
}
/**
* Common pre-processing.
*
- * @param CRM_Core_Form $form
+ * @param CRM_Pledge_Form_Task $form
*/
public static function preProcessCommon(&$form) {
$form->_pledgeIds = [];
}
$form->_pledgeIds = $form->_componentIds = $ids;
-
- // set the context for redirection for any task actions
- $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form);
- $urlParams = 'force=1';
- if (CRM_Utils_Rule::qfKey($qfKey)) {
- $urlParams .= "&qfKey=$qfKey";
- }
-
- $session = CRM_Core_Session::singleton();
- $session->replaceUserContext(CRM_Utils_System::url('civicrm/pledge/search', $urlParams));
+ $form->setNextUrl('pledge');
}
/**
continue;
}
- list($params, $lineItem) = self::getLine($params, $lineItem, $priceSetID, $field, $id);
+ [$params, $lineItem] = self::getLine($params, $lineItem, $priceSetID, $field, $id);
}
$amount_level = [];
$feeBlock = &$form->_priceSet['fields'];
}
- self::applyACLFinancialTypeStatusToFeeBlock($feeBlock);
// Call the buildAmount hook.
CRM_Utils_Hook::buildAmount($component, $form, $feeBlock);
- // CRM-14492 Admin price fields should show up on event registration if user has 'administer CiviCRM' permissions
- $adminFieldVisible = FALSE;
- if (CRM_Core_Permission::check('administer CiviCRM')) {
- $adminFieldVisible = TRUE;
- }
-
- $hideAdminValues = TRUE;
- if (CRM_Core_Permission::check('edit contributions')) {
- $hideAdminValues = FALSE;
- }
-
- foreach ($feeBlock as $id => $field) {
- if (CRM_Utils_Array::value('visibility', $field) == 'public' ||
- (CRM_Utils_Array::value('visibility', $field) == 'admin' && $adminFieldVisible == TRUE) ||
- !$validFieldsOnly
- ) {
- $options = $field['options'] ?? NULL;
- if ($className == 'CRM_Contribute_Form_Contribution_Main' && $component = 'membership') {
- $userid = $form->getVar('_membershipContactID');
- $checklifetime = self::checkCurrentMembership($options, $userid);
- if ($checklifetime) {
- $form->assign('ispricelifetime', TRUE);
- }
- }
-
- $formClasses = ['CRM_Contribute_Form_Contribution', 'CRM_Member_Form_Membership'];
-
- if (!is_array($options) || !in_array($id, $validPriceFieldIds)) {
- continue;
- }
- elseif ($hideAdminValues && !in_array($className, $formClasses)) {
- foreach ($options as $key => $currentOption) {
- if ($currentOption['visibility_id'] == CRM_Price_BAO_PriceField::getVisibilityOptionID('admin')) {
- unset($options[$key]);
- }
- }
- }
- if (!empty($options)) {
- CRM_Price_BAO_PriceField::addQuickFormElement($form,
- 'price_' . $field['id'],
- $field['id'],
- FALSE,
- CRM_Utils_Array::value('is_required', $field, FALSE),
- NULL,
- $options
- );
- }
- }
- }
+ self::addPriceFieldsToForm($form, $feeBlock, $validFieldsOnly, $className, $validPriceFieldIds);
}
/**
* @param array $feeBlock
* Fee block: array of price fields.
*
+ * @deprecated not used in civi universe as at Oct 2020.
+ *
* @return void
*/
public static function applyACLFinancialTypeStatusToFeeBlock(&$feeBlock) {
+ CRM_Core_Error::deprecatedFunctionWarning('enacted in financialtypeacl extension');
if (CRM_Financial_BAO_FinancialType::isACLFinancialTypeStatus()) {
foreach ($feeBlock as $key => $value) {
foreach ($value['options'] as $k => $options) {
return [$params, $lineItem];
}
+ /**
+ * Add the relevant price fields to the form.
+ *
+ * @param \CRM_Core_Form $form
+ * @param array $feeBlock
+ * @param bool $validFieldsOnly
+ * @param string $className
+ * @param array $validPriceFieldIds
+ */
+ protected static function addPriceFieldsToForm(CRM_Core_Form $form, $feeBlock, bool $validFieldsOnly, string $className, array $validPriceFieldIds) {
+ $hideAdminValues = !CRM_Core_Permission::check('edit contributions');
+ // CRM-14492 Admin price fields should show up on event registration if user has 'administer CiviCRM' permissions
+ $adminFieldVisible = CRM_Core_Permission::check('administer CiviCRM');
+ foreach ($feeBlock as $id => $field) {
+ if (CRM_Utils_Array::value('visibility', $field) == 'public' ||
+ (CRM_Utils_Array::value('visibility', $field) == 'admin' && $adminFieldVisible == TRUE) ||
+ !$validFieldsOnly
+ ) {
+ $options = $field['options'] ?? NULL;
+ if ($className == 'CRM_Contribute_Form_Contribution_Main' && $component = 'membership') {
+ $userid = $form->getVar('_membershipContactID');
+ $checklifetime = self::checkCurrentMembership($options, $userid);
+ if ($checklifetime) {
+ $form->assign('ispricelifetime', TRUE);
+ }
+ }
+
+ $formClasses = ['CRM_Contribute_Form_Contribution', 'CRM_Member_Form_Membership'];
+
+ if (!is_array($options) || !in_array($id, $validPriceFieldIds)) {
+ continue;
+ }
+ elseif ($hideAdminValues && !in_array($className, $formClasses)) {
+ foreach ($options as $key => $currentOption) {
+ if ($currentOption['visibility_id'] == CRM_Price_BAO_PriceField::getVisibilityOptionID('admin')) {
+ unset($options[$key]);
+ }
+ }
+ }
+ if (!empty($options)) {
+ CRM_Price_BAO_PriceField::addQuickFormElement($form,
+ 'price_' . $field['id'],
+ $field['id'],
+ FALSE,
+ CRM_Utils_Array::value('is_required', $field, FALSE),
+ NULL,
+ $options
+ );
+ }
+ }
+ }
+ }
+
}
'dbAlias' => "civicrm_contact_target.sort_name",
'default' => TRUE,
],
+ 'contact_target_birth' => [
+ 'name' => 'birth_date',
+ 'title' => ts('Target Birth Date'),
+ 'alias' => 'civicrm_contact_target',
+ 'dbAlias' => "civicrm_contact_target.birth_date",
+ ],
+ 'contact_target_gender' => [
+ 'name' => 'gender_id',
+ 'title' => ts('Target Gender'),
+ 'alias' => 'civicrm_contact_target',
+ 'dbAlias' => "civicrm_contact_target.gender_id",
+ 'default' => TRUE,
+ ],
'contact_source_id' => [
'name' => 'id',
'alias' => 'civicrm_contact_source',
$activityType = CRM_Core_PseudoConstant::activityType(TRUE, TRUE, FALSE, 'label', TRUE);
$activityStatus = CRM_Core_PseudoConstant::activityStatus();
$priority = CRM_Core_PseudoConstant::get('CRM_Activity_DAO_Activity', 'priority_id');
+ $genders = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'gender_id');
$viewLinks = FALSE;
// Would we ever want to retrieve from the form controller??
}
}
+ if (array_key_exists('civicrm_contact_contact_target_gender', $row)) {
+ if ($value = $row['civicrm_contact_contact_target_gender']) {
+ $rows[$rowNum]['civicrm_contact_contact_target_gender'] = $genders[$value];
+ $entryFound = TRUE;
+ }
+ }
+
$entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, 'activity', 'List all activities for this', ';') ? TRUE : $entryFound;
if (!$entryFound) {
'title' => ts('Financial Type'),
'type' => CRM_Utils_Type::T_INT,
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
],
],
'order_bys' => [
'financial_type_id' => [
'title' => ts('Financial Type'),
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
'type' => CRM_Utils_Type::T_INT,
],
'contribution_page_id' => [
'title' => ts('Financial Type'),
'type' => CRM_Utils_Type::T_INT,
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
],
'contribution_status_id' => [
'title' => ts('Contribution Status'),
'financial_type_id' => [
'title' => ts('Financial Type'),
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
'type' => CRM_Utils_Type::T_INT,
],
'frequency_unit' => [
'title' => ts('Financial Type'),
'type' => CRM_Utils_Type::T_INT,
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
),
'contribution_status_id' => array(
'title' => ts('Contribution Status'),
'title' => ts('Financial Type'),
'type' => CRM_Utils_Type::T_INT,
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
],
],
'grouping' => 'softcredit-fields',
'financial_type_id' => [
'title' => ts('Financial Type'),
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
'type' => CRM_Utils_Type::T_INT,
],
'contribution_page_id' => [
'title' => ts('Financial Type'),
'type' => CRM_Utils_Type::T_INT,
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
],
'contribution_status_id' => [
'title' => ts('Contribution Status'),
'title' => ts('Financial Type'),
'type' => CRM_Utils_Type::T_INT,
'operatorType' => CRM_Report_Form::OP_MULTISELECT,
- 'options' => CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes(),
+ 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'),
],
'contribution_status_id' => [
'title' => ts('Contribution Status'),
$this->add('select', 'sms_provider_id',
ts('Select SMS Provider'),
- CRM_Utils_Array::collect('title', CRM_SMS_BAO_Provider::getProviders()),
+ CRM_Utils_Array::collect('title', CRM_SMS_BAO_Provider::getProviders(NULL, ['is_active' => 1])),
TRUE
);
*
* @param CRM_Queue_TaskContext $ctx
* @param string $table
- * @param string|array $column
+ * @param string|array $columns
+ * @param string $prefix
* @return bool
*/
- public static function addIndex($ctx, $table, $column) {
- $tables = [$table => (array) $column];
- CRM_Core_BAO_SchemaHandler::createIndexes($tables);
+ public static function addIndex($ctx, $table, $columns, $prefix = 'index') {
+ $tables = [$table => (array) $columns];
+ CRM_Core_BAO_SchemaHandler::createIndexes($tables, $prefix);
return TRUE;
}
'civicrm_mail_settings', 'is_contact_creation_disabled_if_no_match', "TINYINT DEFAULT 0 NOT NULL COMMENT 'If this option is enabled, CiviCRM will not create new contacts when filing emails'");
}
+ public function upgrade_5_31_beta2($rev) {
+ $this->addTask('Restore null-ity of "civicrm_group.title" field', 'groupTitleRestore');
+ $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
+ }
+
public static function enableEwaySingleExtension(CRM_Queue_TaskContext $ctx) {
$eWAYPaymentProcessorType = CRM_Core_DAO::singleValueQuery("SELECT id FROM civicrm_payment_processor_type WHERE class_name = 'Payment_eWAY'");
if ($eWAYPaymentProcessorType) {
return TRUE;
}
+ /**
+ * The prior task grouptitlefieldExpand went a bit too far in making the `title` NOT NULL.
+ *
+ * @link https://lab.civicrm.org/dev/translation/-/issues/58
+ * @param \CRM_Queue_TaskContext $ctx
+ * @return bool
+ */
+ public static function groupTitleRestore(CRM_Queue_TaskContext $ctx) {
+ $locales = CRM_Core_I18n::getMultilingual();
+ $queries = [];
+ if ($locales) {
+ foreach ($locales as $locale) {
+ $queries[] = "ALTER TABLE civicrm_group CHANGE `title_{$locale}` `title_{$locale}` varchar(255) DEFAULT NULL COMMENT 'Name of Group.'";
+ }
+ }
+ else {
+ $queries[] = "ALTER TABLE civicrm_group CHANGE `title` `title` varchar(255) DEFAULT NULL COMMENT 'Name of Group.'";
+ }
+ foreach ($queries as $query) {
+ CRM_Core_DAO::executeQuery($query, [], TRUE, NULL, FALSE, FALSE);
+ }
+ return TRUE;
+ }
+
}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Upgrade logic for FiveThirtyThree */
+class CRM_Upgrade_Incremental_php_FiveThirtyThree extends CRM_Upgrade_Incremental_Base {
+
+ /**
+ * Compute any messages which should be displayed beforeupgrade.
+ *
+ * Note: This function is called iteratively for each upcoming
+ * revision to the database.
+ *
+ * @param string $preUpgradeMessage
+ * @param string $rev
+ * a version number, e.g. '4.4.alpha1', '4.4.beta3', '4.4.0'.
+ * @param null $currentVer
+ */
+ public function setPreUpgradeMessage(&$preUpgradeMessage, $rev, $currentVer = NULL) {
+ // Example: Generate a pre-upgrade message.
+ // if ($rev == '5.12.34') {
+ // $preUpgradeMessage .= '<p>' . ts('A new permission, "%1", has been added. This permission is now used to control access to the Manage Tags screen.', array(1 => ts('manage tags'))) . '</p>';
+ // }
+ }
+
+ /**
+ * Compute any messages which should be displayed after upgrade.
+ *
+ * @param string $postUpgradeMessage
+ * alterable.
+ * @param string $rev
+ * an intermediate version; note that setPostUpgradeMessage is called repeatedly with different $revs.
+ */
+ public function setPostUpgradeMessage(&$postUpgradeMessage, $rev) {
+ // Example: Generate a post-upgrade message.
+ // if ($rev == '5.12.34') {
+ // $postUpgradeMessage .= '<br /><br />' . ts("By default, CiviCRM now disables the ability to import directly from SQL. To use this feature, you must explicitly grant permission 'import SQL datasource'.");
+ // }
+ }
+
+ /*
+ * 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', [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;
+ // }
+
+}
*/
/**
- * Upgrade logic for FiveThirtyTwo */
+ * Upgrade logic for FiveThirtyTwo
+ */
class CRM_Upgrade_Incremental_php_FiveThirtyTwo extends CRM_Upgrade_Incremental_Base {
/**
// }
}
- /*
- * 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):
+ /**
+ * Install contributioncancelactions extension.
+ *
+ * This feature is restructured as a core extension - which is primarily a code cleanup step but
+ * also permits sites / extensions to disable the core actions to do their own workflows.
+ *
+ * @param \CRM_Queue_TaskContext $ctx
+ *
+ * @return bool
+ *
+ * @throws \CRM_Core_Exception
*/
+ public static function installContributionCancelActions(CRM_Queue_TaskContext $ctx) {
+ // Install via direct SQL manipulation. Note that:
+ // (1) This extension has no activation logic.
+ // (2) On new installs, the extension is activated purely via default SQL INSERT.
+ // (3) Caches are flushed at the end of the upgrade.
+ // ($) Over long term, upgrade steps are more reliable in SQL. API/BAO sometimes don't work mid-upgrade.
+ $insert = CRM_Utils_SQL_Insert::into('civicrm_extension')->row([
+ 'type' => 'module',
+ 'full_name' => 'contributioncancelactions',
+ 'name' => 'contributioncancelactions',
+ 'label' => 'Contribution cancel actions',
+ 'file' => 'contributioncancelactions',
+ 'schema_version' => NULL,
+ 'is_active' => 1,
+ ]);
+ CRM_Core_DAO::executeQuery($insert->usingReplace()->toSQL());
- // /**
- // * Upgrade function.
- // *
- // * @param string $rev
- // */
- // public function upgrade_5_0_x($rev) {
- // $this->addTask(ts('Upgrade DB to %1: SQL', [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.
- // }
+ return TRUE;
+ }
- // public static function taskFoo(CRM_Queue_TaskContext $ctx, ...) {
- // return TRUE;
- // }
+ /**
+ * Upgrade function.
+ *
+ * @param string $rev
+ */
+ public function upgrade_5_32_alpha1($rev) {
+ $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
+ $this->addTask('Add column civicrm_saved_search.name', 'addColumn', 'civicrm_saved_search', 'name', "varchar(255) DEFAULT NULL COMMENT 'Unique name of saved search'");
+ $this->addTask('Add column civicrm_saved_search.label', 'addColumn', 'civicrm_saved_search', 'label', "varchar(255) DEFAULT NULL COMMENT 'Administrative label for search'");
+ $this->addTask('Add index civicrm_saved_search.UI_name', 'addIndex', 'civicrm_saved_search', 'name', 'UI');
+ $this->addTask('Install contribution cancel actions extension', 'installContributionCancelActions');
+ }
}
--- /dev/null
+{* file to handle db changes in 5.31.beta2 during upgrade *}
{* file to handle db changes in 5.32.alpha1 during upgrade *}
+
+-- update italian provinces (pull request #18859)
+UPDATE civicrm_state_province s
+ INNER JOIN civicrm_country c
+ on c.id = s.country_id AND c.name = 'Italy'
+ AND s.abbreviation = 'Bar'
+SET s.abbreviation = 'BT';
+
+UPDATE civicrm_state_province s
+ INNER JOIN civicrm_country c
+ on c.id = s.country_id AND c.name = 'Italy'
+ AND s.abbreviation = 'Fer'
+SET s.abbreviation = 'FM';
+
+UPDATE civicrm_state_province s
+ INNER JOIN civicrm_country c
+ on c.id = s.country_id AND c.name = 'Italy'
+ AND s.abbreviation = 'Mon'
+SET s.abbreviation = 'MB';
--- /dev/null
+{* file to handle db changes in 5.32.beta1 during upgrade *}
--- /dev/null
+{* file to handle db changes in 5.33.alpha1 during upgrade *}
'footer',
// SavedSearch entity
'api_params',
+ // SearchDisplay entity
+ 'settings',
];
$custom = CRM_Core_DAO::executeQuery('SELECT id FROM civicrm_custom_field WHERE html_type = "RichTextEditor"');
while ($custom->fetch()) {
/**
* Determine the value of a constant, if any.
*
- * If the specified constant is undefined, return a default value.
+ * If the specified constant is undefined, check for an environment
+ * variable, defaulting the passed in default value.
*
* @param string $name
* @param mixed $default
* (optional)
* @return mixed
*/
- public static function value($name, $default = NULL) {
+ public static function value(string $name, $default = NULL) {
if (defined($name)) {
return constant($name);
}
- else {
- return $default;
+ if (($value = getenv($name)) !== FALSE) {
+ define($name, $value);
+ return $value;
}
+ return $default;
}
}
}
}
+ /**
+ * Format the field according to the site's preferred date format.
+ *
+ * This is likely to look something like December 31st, 2020.
+ *
+ * @param string $date
+ *
+ * @return string
+ */
+ public static function formatDateOnlyLong(string $date):string {
+ return CRM_Utils_Date::customFormat($date, Civi::settings()->get('dateformatFull'));
+ }
+
/**
* Wrapper for customFormat that takes a timestamp
*
);
}
+ /**
+ * When adding a new "Mail Account" (`MailSettings`), present a menu of setup
+ * options.
+ *
+ * @param array $setupActions
+ * Each item has a symbolic-key, and it has the properties:
+ * - title: string
+ * - callback: string|array, the function which starts the setup process.
+ * The function is expected to return a 'url' for the config screen.
+ * @return mixed
+ */
+ public static function mailSetupActions(&$setupActions) {
+ return self::singleton()->invoke(['setupActions'], $setupActions, self::$_nullObject, self::$_nullObject,
+ self::$_nullObject, self::$_nullObject, self::$_nullObject,
+ 'civicrm_mailSetupActions'
+ );
+ }
+
/**
* This hook is called when composing a mailing. You can include / exclude other groups as needed.
*
);
}
+ /**
+ * This hook is called when loading a mail-store (e.g. IMAP, POP3, or Maildir).
+ *
+ * @param array $params
+ * Most fields correspond to data in the MailSettings entity:
+ * - id: int
+ * - server: string
+ * - username: string
+ * - password: string
+ * - is_ssl: bool
+ * - source: string
+ * - local_part: string
+ *
+ * With a few supplements
+ * - protocol: string, symbolic protocol name (e.g. "IMAP")
+ * - factory: callable, the function which instantiates the driver class
+ * - auth: string, (for some drivers) specify the authentication method (eg "Password" or "XOAuth2")
+ *
+ * @return mixed
+ */
+ public static function alterMailStore(&$params) {
+ return self::singleton()->invoke(['params'], $params, $context,
+ self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject,
+ 'civicrm_alterMailStore'
+ );
+ }
+
/**
* This hook is called when membership status is being calculated.
*
* - url: string (used in lieu of "path"/"query")
* Note: if making "url" CRM_Utils_System::url(), set $htmlize=false
* @return mixed
+ * @deprecated
*/
public static function crudLink($spec, $bao, &$link) {
return self::singleton()->invoke(['spec', 'bao', 'link'], $spec, $bao, $link,
}
if (!empty($valueFormat) && $valueFormat !== '%!i') {
- CRM_Core_Error::deprecatedFunctionWarning('Having a Money Value format other than !%i is deprecated, please report this on the GitLab Issue https://lab.civicrm.org/dev/core/-/issues/1494 with the relevant moneyValueFormat you use.');
+ CRM_Core_Error::deprecatedFunctionWarning('Having a Money Value format other than %!i is deprecated, please report this on the GitLab Issue https://lab.civicrm.org/dev/core/-/issues/1494 with the relevant moneyValueFormat you use.');
}
if ($onlyNumber) {
TRUE
);
$form->registerRule('recaptcha', 'callback', 'validate', 'CRM_Utils_ReCAPTCHA');
+ $form->addRule('g-recaptcha-response', ts('Please go back and complete the CAPTCHA at the bottom of this form.'), 'recaptcha');
if ($form->isSubmitted() && empty($form->_submitValues['g-recaptcha-response'])) {
$form->setElementError(
'g-recaptcha-response',
}
}
+ /**
+ * @param $value
+ * @param CRM_Core_Form $form
+ *
+ * @return mixed
+ */
+ public static function validate($value, $form) {
+ $resp = recaptcha_check_answer(CRM_Core_Config::singleton()->recaptchaPrivateKey,
+ $_SERVER['REMOTE_ADDR'],
+ $_POST['g-recaptcha-response']
+ );
+ return $resp->is_valid;
+ }
+
}
if ($lastLetter == 's' || $lastLetter == 'x' || $lastTwo == 'ch') {
return $str . 'es';
}
- if ($lastLetter == 'y' && $lastTwo != 'ey') {
+ if ($lastLetter == 'y' && !in_array($lastTwo, ['ay', 'ey', 'iy', 'oy', 'uy'])) {
return substr($str, 0, -1) . 'ies';
}
return $str . 's';
* - query: array
* - title: string
* - url: string
+ * @deprecated
*/
public static function createDefaultCrudLink($crudLinkSpec) {
$crudLinkSpec['action'] = CRM_Utils_Array::value('action', $crudLinkSpec, CRM_Core_Action::VIEW);
$e->list[] = 'js/crm.backdrop.js';
}
+ /**
+ * Start a new session.
+ */
+ public function sessionStart() {
+ if (function_exists('backdrop_session_start')) {
+ // https://issues.civicrm.org/jira/browse/CRM-14356
+ if (!(isset($GLOBALS['lazy_session']) && $GLOBALS['lazy_session'] == TRUE)) {
+ backdrop_session_start();
+ }
+ $_SESSION = [];
+ }
+ else {
+ session_start();
+ }
+ }
+
}
}
$res->addSettingsFactory(function () use (&$moduleNames, $angular, $res, $assetParams) {
+ // Merge static settings with the results of settingsFactory functions
+ $settingsByModule = $angular->getResources($moduleNames, 'settings', 'settings');
+ foreach ($angular->getResources($moduleNames, 'settingsFactory', 'settingsFactory') as $moduleName => $factory) {
+ $settingsByModule[$moduleName] = array_merge($settingsByModule[$moduleName] ?? [], $factory());
+ }
+ // Add clientside permissions
+ $permissions = [];
+ $toCheck = $angular->getResources($moduleNames, 'permissions', 'permissions');
+ foreach ($toCheck as $perms) {
+ foreach ((array) $perms as $perm) {
+ if (!isset($permissions[$perm])) {
+ $permissions[$perm] = \CRM_Core_Permission::check($perm);
+ }
+ }
+ }
// TODO optimization; client-side caching
- $result = array_merge($angular->getResources($moduleNames, 'settings', 'settings'), [
+ return array_merge($settingsByModule, ['permissions' => $permissions], [
'resourceUrls' => \CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(),
'angular' => [
'modules' => $moduleNames,
'bundleUrl' => \Civi::service('asset_builder')->getUrl('angular-modules.json', $assetParams),
],
]);
- return $result;
});
$res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, $this->getRegion(), FALSE);
* This will be mapped to "~/moduleName" by crmResource.
* - settings: array(string $key => mixed $value)
* List of settings to preload.
+ * - settingsFactory: callable
+ * Callback function to fetch settings.
+ * - permissions: array
+ * List of permissions to make available client-side
+ * - requires: array
+ * List of other modules required
*/
protected $modules = NULL;
$angularModules = array_merge($angularModules, $component->getAngularModules());
}
\CRM_Utils_Hook::angularModules($angularModules);
- foreach (array_keys($angularModules) as $module) {
- if (!isset($angularModules[$module]['basePages'])) {
- $angularModules[$module]['basePages'] = ['civicrm/a'];
+ foreach ($angularModules as $module => $info) {
+ // Merge in defaults
+ $angularModules[$module] += ['basePages' => ['civicrm/a']];
+ // Validate settingsFactory callables
+ if (isset($info['settingsFactory'])) {
+ // To keep the cache small, we want `settingsFactory` to contain the string names of class & function, not an object
+ if (!is_array($info['settingsFactory']) && !is_string($info['settingsFactory'])) {
+ throw new \CRM_Core_Exception($module . ' settingsFactory must be a callable array or string');
+ }
+ // To keep the cache small, convert full object to just the class name
+ if (is_array($info['settingsFactory']) && is_object($info['settingsFactory'][0])) {
+ $angularModules[$module]['settingsFactory'][0] = get_class($info['settingsFactory'][0]);
+ }
}
}
$this->modules = $this->resolvePatterns($angularModules);
break;
case 'settings':
+ case 'settingsFactory':
case 'requires':
+ case 'permissions':
if (!empty($module[$resType])) {
$result[$moduleName] = $module[$resType];
}
$entities[$fieldName] = [
'name' => $fieldName,
'title' => $customEntity['title'],
- 'titlePlural' => $customEntity['title'],
- 'description' => ts('Custom group for %1', [1 => $baseEntity::getInfo()['titlePlural']]),
+ 'title_plural' => $customEntity['title'],
+ 'description' => ts('Custom group for %1', [1 => $baseEntity::getInfo()['title_plural']]),
'see' => [
'https://docs.civicrm.org/user/en/latest/organising-your-data/creating-custom-fields/#multiple-record-fieldsets',
'\\Civi\\Api4\\CustomGroup',
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\MailSettings;
+
+use Civi\Api4\Generic\BasicBatchAction;
+
+class TestConnection extends BasicBatchAction {
+
+ public function __construct($entityName, $actionName) {
+ parent::__construct($entityName, $actionName, ['id', 'name']);
+ }
+
+ /**
+ * @param array $item
+ * @return array
+ */
+ protected function doTask($item) {
+ try {
+ $mailStore = \CRM_Mailing_MailStore::getStore($item['name']);
+ }
+ catch (\Throwable $t) {
+ \Civi::log()->warning('MailSettings: Failed to establish test connection', [
+ 'exception' => $t,
+ ]);
+
+ return [
+ 'title' => ts("Failed to connect"),
+ 'details' => $t->getMessage() . "\n" . ts('(See log for more details.)'),
+ 'error' => TRUE,
+ ];
+ }
+
+ if (empty($mailStore)) {
+ return [
+ 'title' => ts("Failed to connect"),
+ 'details' => ts('The mail service was not instantiated.'),
+ 'error' => TRUE,
+ ];
+ }
+
+ $limitTestCount = 5;
+ try {
+ $msgs = $mailStore->fetchNext($limitTestCount);
+ }
+ catch (\Throwable $t) {
+ \Civi::log()->warning('MailSettings: Failed to read test message', [
+ 'exception' => $t,
+ ]);
+ return [
+ 'title' => ts('Failed to read test message'),
+ 'details' => $t->getMessage() . "\n" . ts('(See log for more details.)'),
+ 'error' => TRUE,
+ ];
+ }
+
+ if (count($msgs) === 0) {
+ return [
+ 'title' => ts('Connection succeeded.'),
+ 'details' => ts('No new messages found.'),
+ 'error' => FALSE,
+ ];
+ }
+ else {
+ return [
+ 'title' => ts('Connection succeeded.'),
+ 'details' => ts('Found at least %1 new messages.', [
+ 1 => count($msgs),
+ ]),
+ 'error' => FALSE,
+ ];
+ }
+ }
+
+}
],
[
'name' => 'title',
- 'description' => 'Localized title',
+ 'description' => 'Localized title (singular)',
+ ],
+ [
+ 'name' => 'title_plural',
+ 'description' => 'Localized title (plural)',
],
[
'name' => 'type',
'name' => 'dao',
'description' => 'Class name for dao-based entities',
],
+ [
+ 'name' => 'paths',
+ 'data_type' => 'Array',
+ 'description' => 'System paths for accessing this entity',
+ ],
[
'name' => 'see',
'data_type' => 'Array',
return static::getEntityName();
}
+ /**
+ * Overridable function to return menu paths related to this entity.
+ *
+ * @return array
+ */
+ protected static function getEntityPaths() {
+ return [];
+ }
+
/**
* Magic method to return the action object for an api.
*
$info = [
'name' => static::getEntityName(),
'title' => static::getEntityTitle(),
- 'titlePlural' => static::getEntityTitle(TRUE),
+ 'title_plural' => static::getEntityTitle(TRUE),
'type' => self::stripNamespace(get_parent_class(static::class)),
+ 'paths' => static::getEntityPaths(),
];
$reflection = new \ReflectionClass(static::class);
$info += ReflectionUtils::getCodeDocs($reflection, NULL, ['entity' => $info['name']]);
$info = parent::getInfo();
$dao = \CRM_Core_DAO_AllCoreTables::getFullName($info['name']);
if ($dao) {
+ $info['paths'] = $dao::getEntityPaths();
$info['icon'] = $dao::$_icon;
$info['dao'] = $dao;
}
return array_pop($items);
}
+ /**
+ * Return the one-and-only result record.
+ *
+ * If there are too many or too few results, then throw an exception.
+ *
+ * @return array
+ * @throws \API_Exception
+ */
+ public function single() {
+ $result = NULL;
+ foreach ($this as $values) {
+ if ($result === NULL) {
+ $result = $values;
+ }
+ else {
+ throw new \API_Exception("Expected to find one {$this->entity} record, but there were multiple.");
+ }
+ }
+
+ if ($result === NULL) {
+ throw new \API_Exception("Expected to find one {$this->entity} record, but there were zero.");
+ }
+
+ return $result;
+ }
+
/**
* @param int $index
* @return array|null
*/
class MailSettings extends Generic\DAOEntity {
+ /**
+ * Check whether the mail store is accessible.
+ *
+ * @param bool $checkPermissions
+ * @return \Civi\Api4\Action\MailSettings\TestConnection
+ */
+ public static function testConnection($checkPermissions = TRUE) {
+ $action = new \Civi\Api4\Action\MailSettings\TestConnection(static::class, __FUNCTION__);
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
}
}
$hic = \CRM_Utils_API_HTMLInputCoder::singleton();
- if (!$hic->isSkippedField($fieldSpec['name'])) {
+ if (!$hic->isSkippedField($fieldSpec['name']) && is_string($value)) {
$value = $hic->encodeValue($value);
}
}
$host = $db_config['server'];
}
if (empty($db_config['ssl_params'])) {
- $conn = @mysqli_connect($host, $db_config['username'], $db_config['password'], $db_config['database'], !empty($db_config['port']) ? $db_config['port'] : NULL);
+ $conn = @mysqli_connect($host, $db_config['username'], $db_config['password'], $db_config['database'], !empty($db_config['port']) ? $db_config['port'] : NULL, $db_config['socket'] ?? NULL);
}
else {
$conn = NULL;
$db_config['ssl_params']['capath'] ?? NULL,
$db_config['ssl_params']['cipher'] ?? NULL
);
- if (@mysqli_real_connect($init, $host, $db_config['username'], $db_config['password'], $db_config['database'], (!empty($db_config['port']) ? $db_config['port'] : NULL), NULL, MYSQLI_CLIENT_SSL)) {
+ if (@mysqli_real_connect($init, $host, $db_config['username'], $db_config['password'], $db_config['database'], (!empty($db_config['port']) ? $db_config['port'] : NULL), $db_config['socket'] ?? NULL, MYSQLI_CLIENT_SSL)) {
$conn = $init;
}
}
*
* @param int $id
*
- * @return \CRM_Core_Payment|NULL
+ * @return \CRM_Core_Payment
*
- * @throws \CiviCRM_API3_Exception
+ * @throws \CiviCRM_API3_Exception|\CRM_Core_Exception
*/
public function getById($id) {
- if ($id == 0) {
+ if (isset($this->cache[$id])) {
+ return $this->cache[$id];
+ }
+ if ((int) $id === 0) {
return new \CRM_Core_Payment_Manual();
}
$processor = civicrm_api3('payment_processor', 'getsingle', ['id' => $id, 'is_test' => NULL]);
- return self::getByProcessor($processor);
+ return $this->getByProcessor($processor);
}
/**
</span>
</div>
<div ng-if="!$ctrl.conjunctions[clause[0]]" class="api4-input-group">
- <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{data: $ctrl.fields, allowClear: true, placeholder: 'Field'}" />
- <select class="form-control api4-operator" ng-model="clause[1]" ng-options="o for o in $ctrl.operators" ></select>
+ <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{data: $ctrl.fields, allowClear: true, placeholder: 'Field'}" ng-change="$ctrl.changeClauseField(clause, index)" />
+ <select class="form-control api4-operator" ng-model="clause[1]" ng-options="o for o in $ctrl.operators" ng-change="$ctrl.changeClauseOperator(clause)" ></select>
<input class="form-control" ng-model="clause[2]" api4-exp-value="{field: clause[0], op: clause[1], format: $ctrl.format}" />
</div>
<fieldset class="clearfix" ng-if="$ctrl.conjunctions[clause[0]]">
if (lastLetter === 's' || lastLetter === 'x' || lastTwo === 'ch') {
return str + 'es';
}
- if (lastLetter === 'y' && lastTwo !== 'ey') {
+ if (lastLetter === 'y' && !_.includes(['ay', 'ey', 'iy', 'oy', 'uy'], lastTwo)) {
return str.slice(0, -1) + 'ies';
}
return str + 's';
],
'css' => ['ang/crmMailing.css'],
'partials' => ['ang/crmMailing'],
+ 'settingsFactory' => ['CRM_Mailing_Info', 'createAngularSettings'],
'requires' => ['crmUtil', 'crmAttachment', 'crmAutosave', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService', 'crmResource'],
+ 'permissions' => [
+ 'view all contacts',
+ 'edit all contacts',
+ 'access CiviMail',
+ 'create mailings',
+ 'schedule mailings',
+ 'approve mailings',
+ 'delete in CiviMail',
+ 'edit message templates',
+ ],
];
* // is the json encoded result
* echo $api;
* ```
+ *
+ * For remote calls, you may need to set the UserAgent and Referer strings for some environments (eg WordFence)
+ * Add 'referer' and 'useragent' to the initialisation config:
+ *
+ * ```
+ * $api = new civicrm_api3 (['server' => 'http://example.org',
+ * 'api_key'=>'theusersecretkey',
+ * 'key'=>'thesitesecretkey',
+ * 'referer'=>'https://my_site',
+ * 'useragent'=>'curl']);
+ * ```
*/
class civicrm_api3 {
else {
die("\nFATAL:param['api_key] missing\n");
}
+ $this->referer = !empty($config['referer']) ? $config['referer'] : '';
+ $this->useragent = !empty($config['useragent']) ? $config['useragent'] : 'curl';
return;
}
if (!empty($config) && !empty($config['conf_path'])) {
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
+ curl_setopt($ch, CURLOPT_USERAGENT, $this->useragent);
+ if ($this->referer) {
+ curl_setopt($ch, CURLOPT_REFERER, $this->referer);
+ }
$result = curl_exec($ch);
// CiviCRM expects to get back a CiviCRM error object.
if (curl_errno($ch)) {
if (in_array($result['data_type'], $legacyDataTypes)) {
$result['html_type'] = array_search($result['data_type'], $legacyDataTypes);
}
- if (!empty($result['serialize'])) {
+ if (!empty($result['serialize']) && $result['html_type'] !== 'Autocomplete-Select') {
$result['html_type'] = str_replace('Select', 'Multi-Select', $result['html_type']);
}
}
$item = reset($lineItems['line_item']);
$entity = str_replace('civicrm_', '', $item['entity_table']);
}
+
if ($entityParams) {
- if (in_array($entity, ['participant', 'membership'])) {
+ $supportedEntity = TRUE;
+ switch ($entity) {
+ case 'participant':
+ $entityParams['status_id'] = 'Pending from incomplete transaction';
+ break;
+
+ case 'membership':
+ $entityParams['status_id'] = 'Pending';
+ break;
+
+ default:
+ // Don't create any related entities. We might want to support eg. Pledge one day?
+ $supportedEntity = FALSE;
+ break;
+ }
+ if ($supportedEntity) {
$entityParams['skipLineItem'] = TRUE;
- $entityParams['status_id'] = ($entity === 'participant' ? 'Pending from incomplete transaction' : 'Pending');
$entityResult = civicrm_api3($entity, 'create', $entityParams);
$params['contribution_mode'] = $entity;
$entityIds[] = $params[$entity . '_id'] = $entityResult['id'];
$items['entity_id'] = $entityResult['id'];
}
}
- else {
- // pledge payment
- }
}
+
if (empty($priceSetID)) {
$item = reset($lineItems['line_item']);
$priceSetID = (int) civicrm_api3('PriceField', 'getvalue', [
$contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
$params['contribution_status_id'] = array_search('Cancelled', $contributionStatuses);
$result = civicrm_api3('Contribution', 'create', $params);
- CRM_Contribute_BAO_Contribution::transitionComponents($params);
return civicrm_api3_create_success($result['values'], $params, 'Order', 'cancel');
}
## Prune local data
$MYSQLCMD -e "DROP TABLE IF EXISTS civicrm_install_canary; DELETE FROM civicrm_cache; DELETE FROM civicrm_setting;"
-$MYSQLCMD -e "DELETE FROM civicrm_extension WHERE full_name NOT IN ('sequentialcreditnotes', 'eventcart', 'greenwich', 'search', 'flexmailer', 'financialacls');"
+$MYSQLCMD -e "DELETE FROM civicrm_extension WHERE full_name NOT IN ('sequentialcreditnotes', 'eventcart', 'greenwich', 'search', 'flexmailer', 'financialacls', 'contributioncancelactions');"
TABLENAMES=$( echo "show tables like 'civicrm_%'" | $MYSQLCMD | grep ^civicrm_ | xargs )
cd $CIVISOURCEDIR/sql
"cweagans/composer-patches": "~1.0",
"pear/log": "1.13.2",
"adrienrn/php-mimetyper": "0.2.2",
- "civicrm/composer-downloads-plugin": "^2.0",
+ "civicrm/composer-downloads-plugin": "^3.0",
"league/csv": "^9.2",
+ "league/oauth2-client": "^2.4",
+ "league/oauth2-google": "^3.0",
"tplaner/when": "~3.0.0",
"xkerman/restricted-unserialize": "~1.1",
"typo3/phar-stream-wrapper": "^2 || ^3.0",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "5d304686f3edec04284e52a859321e0a",
+ "content-hash": "7e6ef8d4248bce0f976048cabc185289",
"packages": [
{
"name": "adrienrn/php-mimetyper",
},
{
"name": "civicrm/composer-downloads-plugin",
- "version": "v2.1.1",
+ "version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/civicrm/composer-downloads-plugin.git",
- "reference": "8722bc7d547315be39397a3078bb51ee053ca269"
+ "reference": "3aabb6d259a86158d01829fc2c62a2afb9618877"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/civicrm/composer-downloads-plugin/zipball/8722bc7d547315be39397a3078bb51ee053ca269",
- "reference": "8722bc7d547315be39397a3078bb51ee053ca269",
+ "url": "https://api.github.com/repos/civicrm/composer-downloads-plugin/zipball/3aabb6d259a86158d01829fc2c62a2afb9618877",
+ "reference": "3aabb6d259a86158d01829fc2c62a2afb9618877",
"shasum": ""
},
"require": {
- "composer-plugin-api": "^1.1",
+ "composer-plugin-api": "^1.1 || ^2.0",
"php": ">=5.6",
"togos/gitignore": "~1.1.1"
},
"require-dev": {
- "composer/composer": "~1.0",
+ "composer/composer": "~1.0 || ~2.0",
"friendsofphp/php-cs-fixer": "^2.3",
"phpunit/phpunit": "^5.7",
"totten/process-helper": "^1.0.1"
}
],
"description": "Composer plugin for downloading additional files within any composer package.",
- "time": "2019-08-28T00:33:51+00:00"
+ "time": "2020-11-02T04:00:42+00:00"
},
{
"name": "cweagans/composer-patches",
- "version": "1.6.5",
+ "version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/cweagans/composer-patches.git",
- "reference": "2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3"
+ "reference": "ae02121445ad75f4eaff800cc532b5e6233e2ddf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3",
- "reference": "2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3",
+ "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/ae02121445ad75f4eaff800cc532b5e6233e2ddf",
+ "reference": "ae02121445ad75f4eaff800cc532b5e6233e2ddf",
"shasum": ""
},
"require": {
- "composer-plugin-api": "^1.0",
+ "composer-plugin-api": "^1.0 || ^2.0",
"php": ">=5.3.0"
},
"require-dev": {
- "composer/composer": "~1.0",
+ "composer/composer": "~1.0 || ~2.0",
"phpunit/phpunit": "~4.6"
},
"type": "composer-plugin",
}
],
"description": "Provides a way to patch Composer packages.",
- "time": "2018-05-11T18:00:16+00:00"
+ "time": "2020-09-30T17:56:20+00:00"
},
{
"name": "dflydev/apache-mime-types",
],
"time": "2019-06-07T06:24:33+00:00"
},
+ {
+ "name": "league/oauth2-client",
+ "version": "2.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/oauth2-client.git",
+ "reference": "badb01e62383430706433191b82506b6df24ad98"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/badb01e62383430706433191b82506b6df24ad98",
+ "reference": "badb01e62383430706433191b82506b6df24ad98",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.0 || ^7.0",
+ "paragonie/random_compat": "^1 || ^2 || ^9.99",
+ "php": "^5.6 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.3",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpunit/phpunit": "^5.7 || ^6.0 || ^9.3",
+ "squizlabs/php_codesniffer": "^2.3 || ^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\OAuth2\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alex Bilbie",
+ "email": "hello@alexbilbie.com",
+ "homepage": "http://www.alexbilbie.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Woody Gilk",
+ "homepage": "https://github.com/shadowhand",
+ "role": "Contributor"
+ }
+ ],
+ "description": "OAuth 2.0 Client Library",
+ "keywords": [
+ "Authentication",
+ "SSO",
+ "authorization",
+ "identity",
+ "idp",
+ "oauth",
+ "oauth2",
+ "single sign on"
+ ],
+ "time": "2020-10-28T02:03:40+00:00"
+ },
+ {
+ "name": "league/oauth2-google",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/oauth2-google.git",
+ "reference": "18d1889897a8b18d85ecadacf74c9274d678d943"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/oauth2-google/zipball/18d1889897a8b18d85ecadacf74c9274d678d943",
+ "reference": "18d1889897a8b18d85ecadacf74c9274d678d943",
+ "shasum": ""
+ },
+ "require": {
+ "league/oauth2-client": "^2.0"
+ },
+ "require-dev": {
+ "eloquent/phony-phpunit": "^2.0",
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^6.0",
+ "squizlabs/php_codesniffer": "^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\OAuth2\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Woody Gilk",
+ "email": "woody.gilk@gmail.com",
+ "homepage": "http://shadowhand.me"
+ }
+ ],
+ "description": "Google OAuth 2.0 Client Provider for The PHP League OAuth2-Client",
+ "keywords": [
+ "Authentication",
+ "authorization",
+ "client",
+ "google",
+ "oauth",
+ "oauth2"
+ ],
+ "time": "2020-07-24T15:16:12+00:00"
+ },
{
"name": "marcj/topsort",
"version": "1.1.0",
"description": "CSS Autoprefixer written in pure PHP.",
"time": "2019-11-26T09:55:37+00:00"
},
+ {
+ "name": "paragonie/random_compat",
+ "version": "v9.99.100",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/random_compat.git",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">= 7"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*|5.*",
+ "vimeo/psalm": "^1"
+ },
+ "suggest": {
+ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+ },
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+ "keywords": [
+ "csprng",
+ "polyfill",
+ "pseudorandom",
+ "random"
+ ],
+ "time": "2020-10-15T08:29:30+00:00"
+ },
{
"name": "pclzip/pclzip",
"version": "2.8.2",
"portable",
"shim"
],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
"time": "2020-05-12T16:47:27+00:00"
},
{
name : Justin Freeman
organization: Agileware
+- github : jvos
+
- github : kainuk
name : Klaas Eikelboom
organization: CiviCoop
organization: Web Access
jira : kurund
+- github : lalgwebdev
+ name : Tony Maynard-Smith
+
- github : larssg-wildsight
name : Lars Sanders-Green
organization: Wildsight
organization: Circle Interactive
jira : marshCircle
+- github : masetto
+ organization: PERORA SRL
+ name : Samuele Masetto
+
- github : mathavanveda
name : Mathavan Veeramuthu
organization: Veda Consulting
name : Martin Hansen
organization: PeaceWorks Technology Solutions
+- github : maxtsero
+ name : Max Tsero
+ organization: Atomic Development
+
- github : mc0e
name : Andrew McNaughton
jira : mc0e
organization: Electronic Frontier Foundation
jira : mfb
+- github : mglaman
+ name : Matt Glaman
+ organization: Centarro
+
- github : mgribaudo
name : Marcello Gribaudo
jira : mgribaudo
- name : Steve Binkowski
jira : s.bink
+- github : shaneonabike
+ name : Abeilles en Vélo / Bees on a bike
+
- name : Sheila Burkett
organization: Spry Digital
jira : saburkett
echo ext/financialacls
echo ext/afform
echo ext/greenwich
+ echo ext/contributioncancelactions
}
## Copy all packages
$routeProvider.when('/', {
controller: 'AfformStandalonePageCtrl',
template: function() {
- return '<div ' + CRM.afform.open + '="{}"></div>';
+ return '<div id="bootstrap-theme" ' + CRM.afform.open + '="{}"></div>';
}
});
})
<div crm-ui-debug="layout"></div>
<div crm-ui-debug="entities"></div>
<div crm-ui-debug="meta"></div>
-<div id="bootstrap-theme">
+<div>
<div id="afGuiEditor">
<div id="afGuiEditor-palette" ng-include="'~/afGuiEditor/palette.html'"></div>
<div id="afGuiEditor-canvas" ng-include="'~/afGuiEditor/canvas.html'"></div>
--- /dev/null
+Package: contributioncancelactions
+Copyright (C) 2020, CiviCRM <info@civicrm.org>
+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. <http://fsf.org/>
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 <http://www.gnu.org/licenses/>.
+
+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
+<http://www.gnu.org/licenses/>.
--- /dev/null
+# contributioncancelactions
+
+![Screenshot](/images/screenshot.png)
+
+This extension cancels memberships, participation records,
+and pledge payments when the related
+contribution is cancelled. If you do not wish them to be cancelled
+then disable the extension. If you want more complex logic
+you may be able to achieve it using civirules or another extension.
+
+This code was part of the core code prior to 5.32 at which point
+it was separated to an optional extension.
+
+The extension is licensed under [AGPL-3.0](LICENSE.txt).
+
+## Requirements
+
+* PHP v7.1+
+* CiviCRM 5.32+
+
+## Installation (Web UI)
+
+This extension ships with core - you can enable or disable in the extensions UI.
+
--- /dev/null
+<?php
+
+// AUTO-GENERATED FILE -- Civix may overwrite any changes made to this file
+
+/**
+ * The ExtensionUtil class provides small stubs for accessing resources of this
+ * extension.
+ */
+class CRM_Contributioncancelactions_ExtensionUtil {
+ const SHORT_NAME = "contributioncancelactions";
+ const LONG_NAME = "contributioncancelactions";
+ const CLASS_PREFIX = "CRM_Contributioncancelactions";
+
+ /**
+ * Translate a string using the extension's domain.
+ *
+ * If the extension doesn't have a specific translation
+ * for the string, fallback to the default translations.
+ *
+ * @param string $text
+ * Canonical message text (generally en_US).
+ * @param array $params
+ * @return string
+ * Translated text.
+ * @see ts
+ */
+ public static function ts($text, $params = []) {
+ if (!array_key_exists('domain', $params)) {
+ $params['domain'] = [self::LONG_NAME, NULL];
+ }
+ return ts($text, $params);
+ }
+
+ /**
+ * Get the URL of a resource file (in this extension).
+ *
+ * @param string|NULL $file
+ * Ex: NULL.
+ * Ex: 'css/foo.css'.
+ * @return string
+ * Ex: 'http://example.org/sites/default/ext/org.example.foo'.
+ * Ex: 'http://example.org/sites/default/ext/org.example.foo/css/foo.css'.
+ */
+ public static function url($file = NULL) {
+ if ($file === NULL) {
+ return rtrim(CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME), '/');
+ }
+ return CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME, $file);
+ }
+
+ /**
+ * Get the path of a resource file (in this extension).
+ *
+ * @param string|NULL $file
+ * Ex: NULL.
+ * Ex: 'css/foo.css'.
+ * @return string
+ * Ex: '/var/www/example.org/sites/default/ext/org.example.foo'.
+ * Ex: '/var/www/example.org/sites/default/ext/org.example.foo/css/foo.css'.
+ */
+ public static function path($file = NULL) {
+ // return CRM_Core_Resources::singleton()->getPath(self::LONG_NAME, $file);
+ return __DIR__ . ($file === NULL ? '' : (DIRECTORY_SEPARATOR . $file));
+ }
+
+ /**
+ * Get the name of a class within this extension.
+ *
+ * @param string $suffix
+ * Ex: 'Page_HelloWorld' or 'Page\\HelloWorld'.
+ * @return string
+ * Ex: 'CRM_Foo_Page_HelloWorld'.
+ */
+ public static function findClass($suffix) {
+ return self::CLASS_PREFIX . '_' . str_replace('\\', '_', $suffix);
+ }
+
+}
+
+use CRM_Contributioncancelactions_ExtensionUtil as E;
+
+/**
+ * (Delegated) Implements hook_civicrm_config().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config
+ */
+function _contributioncancelactions_civix_civicrm_config(&$config = NULL) {
+ static $configured = FALSE;
+ if ($configured) {
+ return;
+ }
+ $configured = TRUE;
+
+ $template =& CRM_Core_Smarty::singleton();
+
+ $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR;
+ $extDir = $extRoot . 'templates';
+
+ if (is_array($template->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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_xmlMenu
+ */
+function _contributioncancelactions_civix_civicrm_xmlMenu(&$files) {
+ foreach (_contributioncancelactions_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) {
+ $files[] = $file;
+ }
+}
+
+/**
+ * Implements hook_civicrm_install().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install
+ */
+function _contributioncancelactions_civix_civicrm_install() {
+ _contributioncancelactions_civix_civicrm_config();
+ if ($upgrader = _contributioncancelactions_civix_upgrader()) {
+ $upgrader->onInstall();
+ }
+}
+
+/**
+ * Implements hook_civicrm_postInstall().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall
+ */
+function _contributioncancelactions_civix_civicrm_postInstall() {
+ _contributioncancelactions_civix_civicrm_config();
+ if ($upgrader = _contributioncancelactions_civix_upgrader()) {
+ if (is_callable([$upgrader, 'onPostInstall'])) {
+ $upgrader->onPostInstall();
+ }
+ }
+}
+
+/**
+ * Implements hook_civicrm_uninstall().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall
+ */
+function _contributioncancelactions_civix_civicrm_uninstall() {
+ _contributioncancelactions_civix_civicrm_config();
+ if ($upgrader = _contributioncancelactions_civix_upgrader()) {
+ $upgrader->onUninstall();
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_enable().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
+ */
+function _contributioncancelactions_civix_civicrm_enable() {
+ _contributioncancelactions_civix_civicrm_config();
+ if ($upgrader = _contributioncancelactions_civix_upgrader()) {
+ if (is_callable([$upgrader, 'onEnable'])) {
+ $upgrader->onEnable();
+ }
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_disable().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable
+ * @return mixed
+ */
+function _contributioncancelactions_civix_civicrm_disable() {
+ _contributioncancelactions_civix_civicrm_config();
+ if ($upgrader = _contributioncancelactions_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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade
+ */
+function _contributioncancelactions_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
+ if ($upgrader = _contributioncancelactions_civix_upgrader()) {
+ return $upgrader->onUpgrade($op, $queue);
+ }
+}
+
+/**
+ * @return CRM_Contributioncancelactions_Upgrader
+ */
+function _contributioncancelactions_civix_upgrader() {
+ if (!file_exists(__DIR__ . '/CRM/Contributioncancelactions/Upgrader.php')) {
+ return NULL;
+ }
+ else {
+ return CRM_Contributioncancelactions_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 string $dir base dir
+ * @param string $pattern , glob pattern, eg "*.txt"
+ *
+ * @return array
+ */
+function _contributioncancelactions_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 (_contributioncancelactions_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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed
+ */
+function _contributioncancelactions_civix_civicrm_managed(&$entities) {
+ $mgdFiles = _contributioncancelactions_civix_find_files(__DIR__, '*.mgd.php');
+ sort($mgdFiles);
+ foreach ($mgdFiles as $file) {
+ $es = include $file;
+ foreach ($es as $e) {
+ if (empty($e['module'])) {
+ $e['module'] = E::LONG_NAME;
+ }
+ if (empty($e['params']['version'])) {
+ $e['params']['version'] = '3';
+ }
+ $entities[] = $e;
+ }
+ }
+}
+
+/**
+ * (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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_caseTypes
+ */
+function _contributioncancelactions_civix_civicrm_caseTypes(&$caseTypes) {
+ if (!is_dir(__DIR__ . '/xml/case')) {
+ return;
+ }
+
+ foreach (_contributioncancelactions_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));
+ throw new CRM_Core_Exception($errorMessage);
+ }
+ $caseTypes[$name] = [
+ 'module' => E::LONG_NAME,
+ '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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules
+ */
+function _contributioncancelactions_civix_civicrm_angularModules(&$angularModules) {
+ if (!is_dir(__DIR__ . '/ang')) {
+ return;
+ }
+
+ $files = _contributioncancelactions_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'] = E::LONG_NAME;
+ }
+ $angularModules[$name] = $module;
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_themes().
+ *
+ * Find any and return any files matching "*.theme.php"
+ */
+function _contributioncancelactions_civix_civicrm_themes(&$themes) {
+ $files = _contributioncancelactions_civix_glob(__DIR__ . '/*.theme.php');
+ foreach ($files as $file) {
+ $themeMeta = include $file;
+ if (empty($themeMeta['name'])) {
+ $themeMeta['name'] = preg_replace(':\.theme\.php$:', '', basename($file));
+ }
+ if (empty($themeMeta['ext'])) {
+ $themeMeta['ext'] = E::LONG_NAME;
+ }
+ $themes[$themeMeta['name']] = $themeMeta;
+ }
+}
+
+/**
+ * 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
+ */
+function _contributioncancelactions_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 to parent of this item, e.g. 'my_extension/submenu'
+ * 'Mailing', or 'Administer/System Settings'
+ * @param array $item - the item to insert (parent/child attributes will be
+ * filled for you)
+ *
+ * @return bool
+ */
+function _contributioncancelactions_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 = _contributioncancelactions_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item);
+ }
+ }
+ return $found;
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_navigationMenu().
+ */
+function _contributioncancelactions_civix_navigationMenu(&$nodes) {
+ if (!is_callable(['CRM_Core_BAO_Navigation', 'fixNavigationMenu'])) {
+ _contributioncancelactions_civix_fixNavigationMenu($nodes);
+ }
+}
+
+/**
+ * Given a navigation menu, generate navIDs for any items which are
+ * missing them.
+ */
+function _contributioncancelactions_civix_fixNavigationMenu(&$nodes) {
+ $maxNavID = 1;
+ array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) {
+ if ($key === 'navID') {
+ $maxNavID = max($maxNavID, $item);
+ }
+ });
+ _contributioncancelactions_civix_fixNavigationMenuItems($nodes, $maxNavID, NULL);
+}
+
+function _contributioncancelactions_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'])) {
+ _contributioncancelactions_civix_fixNavigationMenuItems($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']);
+ }
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_alterSettingsFolders().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterSettingsFolders
+ */
+function _contributioncancelactions_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
+ $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings';
+ if (!in_array($settingsDir, $metaDataFolders) && is_dir($settingsDir)) {
+ $metaDataFolders[] = $settingsDir;
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_entityTypes().
+ *
+ * Find any *.entityType.php files, merge their content, and return.
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+ */
+function _contributioncancelactions_civix_civicrm_entityTypes(&$entityTypes) {
+ $entityTypes = array_merge($entityTypes, []);
+}
--- /dev/null
+<?php
+
+require_once 'contributioncancelactions.civix.php';
+// phpcs:disable
+use CRM_Contributioncancelactions_ExtensionUtil as E;
+// phpcs:enable
+use Civi\Api4\LineItem;
+use Civi\Api4\Participant;
+
+/**
+ * Implements hook_civicrm_preProcess().
+ *
+ * This enacts the following
+ * - find and cancel any related pending memberships
+ * - (not yet implemented) find and cancel any related pending participant records
+ * - (not yet implemented) find any related pledge payment records. Remove the contribution id.
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_post
+ */
+function contributioncancelactions_civicrm_post($op, $objectName, $objectId, $objectRef) {
+ if ($op === 'edit' && $objectName === 'Contribution') {
+ if ('Cancelled' === CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $objectRef->contribution_status_id)) {
+ contributioncancelactions_cancel_related_pending_memberships((int) $objectId);
+ contributioncancelactions_cancel_related_pending_participant_records((int) $objectId);
+ contributioncancelactions_update_related_pledge((int) $objectId, (int) $objectRef->contribution_status_id);
+ }
+ }
+}
+
+/**
+ * Update any related pledge when a contribution is cancelled.
+ *
+ * This updates the status of the pledge and amount paid.
+ *
+ * The functionality should probably be give more thought in that it currently
+ * does not un-assign the contribution id from the pledge payment. However,
+ * at time of writing the goal is to move rather than fix functionality.
+ *
+ * @param int $contributionID
+ * @param int $contributionStatusID
+ *
+ * @throws CiviCRM_API3_Exception
+ */
+function contributioncancelactions_update_related_pledge(int $contributionID, int $contributionStatusID) {
+ $pledgePayments = civicrm_api3('PledgePayment', 'get', ['contribution_id' => $contributionID])['values'];
+ if (!empty($pledgePayments)) {
+ $pledgePaymentIDS = array_keys($pledgePayments);
+ $pledgePayment = reset($pledgePayments);
+ CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($pledgePayment['pledge_id'], $pledgePaymentIDS, $contributionStatusID);
+ }
+}
+
+/**
+ * Find and cancel any pending participant records.
+ *
+ * @param int $contributionID
+ * @throws CiviCRM_API3_Exception
+ */
+function contributioncancelactions_cancel_related_pending_participant_records($contributionID): void {
+ $pendingStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Pending'");
+ $waitingStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Waiting'");
+ $cancellableParticipantRecords = civicrm_api3('ParticipantPayment', 'get', [
+ 'contribution_id' => $contributionID,
+ 'participant_id.status_id' => ['IN' => array_merge(array_keys($pendingStatuses), array_keys($waitingStatuses))],
+ ])['values'];
+ if (empty($cancellableParticipantRecords)) {
+ return;
+ }
+ Participant::update(FALSE)
+ ->addWhere('id', 'IN', array_keys($cancellableParticipantRecords))
+ ->setValues(['status_id:name' => 'Cancelled'])
+ ->execute();
+}
+
+/**
+ * Find and cancel any pending memberships.
+ *
+ * @param int $contributionID
+ * @throws API_Exception
+ * @throws CiviCRM_API3_Exception
+ */
+function contributioncancelactions_cancel_related_pending_memberships($contributionID): void {
+ $connectedMemberships = (array) LineItem::get(FALSE)->setWhere([
+ ['contribution_id', '=', $contributionID],
+ ['entity_table', '=', 'civicrm_membership'],
+ ])->execute()->indexBy('entity_id');
+
+ if (empty($connectedMemberships)) {
+ return;
+ }
+ // @todo we don't have v4 membership api yet so v3 for now.
+ $connectedMemberships = array_keys(civicrm_api3('Membership', 'get', [
+ 'status_id' => 'Pending',
+ 'id' => ['IN' => array_keys($connectedMemberships)],
+ ])['values']);
+ if (empty($connectedMemberships)) {
+ return;
+ }
+ foreach ($connectedMemberships as $membershipID) {
+ civicrm_api3('Membership', 'create', ['status_id' => 'Cancelled', 'id' => $membershipID, 'is_override' => 1]);
+ }
+}
--- /dev/null
+<?xml version="1.0"?>
+<extension key="contributioncancelactions" type="module">
+ <file>contributioncancelactions</file>
+ <name>Contribution cancel actions</name>
+ <description>This extension cancels memberships, participation records, and pledge payments when the related contribution is cancelled.</description>
+ <license>AGPL-3.0</license>
+ <maintainer>
+ <author>CiviCRM</author>
+ <email>info@civicrm.org</email>
+ </maintainer>
+ <urls>
+ <url desc="Main Extension Page">http://civicrm.org</url>
+ <url desc="Documentation">http://civicrm.org</url>
+ <url desc="Support">http://civicrm.org</url>
+ <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
+ </urls>
+ <releaseDate>2020-10-12</releaseDate>
+ <version>1.0</version>
+ <develStage>stable</develStage>
+ <compatibility>
+ <ver>5.32</ver>
+ </compatibility>
+ <tags>
+ <tag>mgmt:hidden</tag>
+ </tags>
+ <comments>This code has been moved from core to a separate extension in 5.32</comments>
+ <classloader>
+ <psr4 prefix="Civi\" path="Civi"/>
+ </classloader>
+ <civix>
+ <namespace>CRM/Contributioncancelactions</namespace>
+ </civix>
+</extension>
--- /dev/null
+<?xml version="1.0"?>
+<phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/phpunit/bootstrap.php">
+ <testsuites>
+ <testsuite name="My Test Suite">
+ <directory>./tests/phpunit</directory>
+ </testsuite>
+ </testsuites>
+ <filter>
+ <whitelist>
+ <directory suffix=".php">./</directory>
+ </whitelist>
+ </filter>
+ <listeners>
+ <listener class="Civi\Test\CiviTestListener">
+ <arguments/>
+ </listener>
+ </listeners>
+</phpunit>
--- /dev/null
+<?php
+
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+use Civi\Api4\Contact;
+use Civi\Api4\MembershipType;
+use Civi\Api4\RelationshipType;
+use Civi\Api4\Relationship;
+use Civi\Api4\Event;
+use Civi\Api4\PriceField;
+use Civi\Api4\Participant;
+
+/**
+ * FIXME - Add test description.
+ *
+ * Tips:
+ * - With HookInterface, you may implement CiviCRM hooks directly in the test class.
+ * Simply create corresponding functions (e.g. "hook_civicrm_post(...)" or similar).
+ * - With TransactionalInterface, any data changes made by setUp() or test****() functions will
+ * rollback automatically -- as long as you don't manipulate schema or truncate tables.
+ * If this test needs to manipulate schema or truncate tables, then either:
+ * a. Do all that using setupHeadless() and Civi\Test.
+ * b. Disable TransactionalInterface, and handle all setup/teardown yourself.
+ *
+ * @group headless
+ */
+class CancelTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ use \Civi\Test\Api3TestTrait;
+
+ /**
+ * Created ids.
+ *
+ * @var array
+ */
+ protected $ids = [];
+
+ /**
+ * The setupHeadless function runs at the start of each test case, right before
+ * the headless environment reboots.
+ *
+ * It should perform any necessary steps required for putting the database
+ * in a consistent baseline -- such as loading schema and extensions.
+ *
+ * The utility `\Civi\Test::headless()` provides a number of helper functions
+ * for managing this setup, and it includes optimizations to avoid redundant
+ * setup work.
+ *
+ * @see \Civi\Test
+ */
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()
+ ->installMe(__DIR__)
+ ->apply();
+ }
+
+ /**
+ * Test that a cancel from paypal pro results in an order being cancelled.
+ *
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ public function testPaypalProCancel() {
+ $this->createContact();
+ $this->createMembershipType();
+ Relationship::create()->setValues([
+ 'contact_id_a' => $this->ids['contact'][0],
+ 'contact_id_b' => Contact::create()->setValues(['first_name' => 'Bugs', 'last_name' => 'Bunny'])->execute()->first()['id'],
+ 'relationship_type_id' => RelationshipType::get()->addWhere('name_a_b', '=', 'AB')->execute()->first()['id'],
+ ])->execute();
+
+ $this->createMembershipOrder();
+
+ $memberships = $this->callAPISuccess('Membership', 'get')['values'];
+ $this->assertCount(2, $memberships);
+
+ $ipn = new CRM_Core_Payment_PayPalProIPN([
+ 'rp_invoice_id' => http_build_query([
+ 'b' => $this->ids['Contribution'][0],
+ 'm' => 'contribute',
+ 'i' => 'zyx',
+ 'c' => $this->ids['contact'][0],
+ ]),
+ 'mc_gross' => 200,
+ 'payment_status' => 'Refunded',
+ 'processor_id' => $this->createPaymentProcessor(),
+ ]);
+ $ipn->main();
+ $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
+ $this->callAPISuccessGetCount('Membership', ['status_id' => 'Cancelled'], 2);
+ }
+
+ /**
+ * Create an order with more than one membership.
+ *
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ protected function createMembershipOrder() {
+ $priceFieldID = $this->callAPISuccessGetValue('price_field', [
+ 'return' => 'id',
+ 'label' => 'Membership Amount',
+ 'options' => ['limit' => 1, 'sort' => 'id DESC'],
+ ]);
+ $generalPriceFieldValueID = $this->callAPISuccessGetValue('price_field_value', [
+ 'return' => 'id',
+ 'label' => 'General',
+ 'options' => ['limit' => 1, 'sort' => 'id DESC'],
+ ]);
+
+ $orderID = $this->callAPISuccess('Order', 'create', [
+ 'financial_type_id' => 'Member Dues',
+ 'contact_id' => $this->ids['contact'][0],
+ 'is_test' => 0,
+ 'payment_instrument_id' => 'Credit card',
+ 'receive_date' => '2019-07-25 07:34:23',
+ 'invoice_id' => 'zyx',
+ 'line_items' => [
+ [
+ 'params' => [
+ 'contact_id' => $this->ids['contact'][0],
+ 'source' => 'Payment',
+ 'membership_type_id' => 'General',
+ // This is interim needed while we improve the BAO - if the test passes without it it can go!
+ 'skipStatusCal' => TRUE,
+ ],
+ 'line_item' => [
+ [
+ 'label' => 'General',
+ 'qty' => 1,
+ 'unit_price' => 200,
+ 'line_total' => 200,
+ 'financial_type_id' => 1,
+ 'entity_table' => 'civicrm_membership',
+ 'price_field_id' => $priceFieldID,
+ 'price_field_value_id' => $generalPriceFieldValueID,
+ ],
+ ],
+ ],
+ ],
+ ])['id'];
+ $this->ids['Contribution'][0] = $orderID;
+ }
+
+ /**
+ * Create the general membership type.
+ *
+ * @throws \API_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ protected function createMembershipType(): void {
+ MembershipType::create()->setValues([
+ 'name' => 'General',
+ 'duration_unit' => 'year',
+ 'duration_interval' => 1,
+ 'period_type' => 'rolling',
+ 'member_of_contact_id' => 1,
+ 'domain_id' => 1,
+ 'financial_type_id' => 2,
+ 'relationship_type_id' => RelationshipType::create(FALSE)->setValues(['name_a_b' => 'AB', 'name_b_a' => 'BA'])->execute()->first()['id'],
+ 'relationship_direction' => 'a_b',
+ 'is_active' => 1,
+ 'sequential' => 1,
+ 'visibility' => 'Public',
+ ])->execute();
+ }
+
+ /**
+ * Create a payment processor.
+ *
+ * @param array $params
+ *
+ * @return int
+ * @throws \CRM_Core_Exception
+ */
+ public function createPaymentProcessor($params = []) {
+ $params = array_merge([
+ 'name' => 'demo',
+ 'domain_id' => CRM_Core_Config::domainID(),
+ 'payment_processor_type_id' => 'PayPal',
+ 'is_active' => 1,
+ 'is_default' => 0,
+ 'is_test' => 1,
+ 'user_name' => 'sunil._1183377782_biz_api1.webaccess.co.in',
+ 'password' => '1183377788',
+ 'signature' => 'APixCoQ-Zsaj-u3IH7mD5Do-7HUqA9loGnLSzsZga9Zr-aNmaJa3WGPH',
+ 'url_site' => 'https://www.sandbox.paypal.com/',
+ 'url_api' => 'https://api-3t.sandbox.paypal.com/',
+ 'url_button' => 'https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif',
+ 'class_name' => 'Payment_PayPalImpl',
+ 'billing_mode' => 3,
+ 'financial_type_id' => 1,
+ 'financial_account_id' => 12,
+ // Credit card = 1 so can pass 'by accident'.
+ 'payment_instrument_id' => 'Debit Card',
+ ], $params);
+ if (!is_numeric($params['payment_processor_type_id'])) {
+ // really the api should handle this through getoptions but it's not exactly api call so lets just sort it
+ //here
+ $params['payment_processor_type_id'] = $this->callAPISuccess('payment_processor_type', 'getvalue', [
+ 'name' => $params['payment_processor_type_id'],
+ 'return' => 'id',
+ ], 'integer');
+ }
+ $result = $this->callAPISuccess('payment_processor', 'create', $params);
+ return (int) $result['id'];
+ }
+
+ /**
+ * Test that a cancel from paypal pro results in an order being cancelled.
+ *
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ */
+ public function testPaypalStandardCancel() {
+ $this->createContact();
+ $orderID = $this->createEventOrder();
+ $ipn = new CRM_Core_Payment_PayPalIPN([
+ 'mc_gross' => 200,
+ 'contactID' => $this->ids['contact'][0],
+ 'contributionID' => $orderID,
+ 'module' => 'event',
+ 'invoice' => 123,
+ 'eventID' => $this->ids['event'][0],
+ 'participantID' => Participant::get()->addWhere('event_id', '=', $this->ids['event'][0])->addSelect('id')->execute()->first()['id'],
+ 'payment_status' => 'Refunded',
+ 'processor_id' => $this->createPaymentProcessor(['payment_processor_type_id' => 'PayPal_Standard']),
+ ]);
+ $ipn->main();
+ $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
+ $this->callAPISuccessGetCount('Participant', ['status_id' => 'Cancelled'], 1);
+ }
+
+ /**
+ * Test cancel order api
+ * @throws API_Exception
+ * @throws CRM_Core_Exception
+ */
+ public function testCancelOrderWithParticipant() {
+ $this->createContact();
+ $orderID = $this->createEventOrder();
+ $this->callAPISuccess('Order', 'cancel', ['contribution_id' => $orderID]);
+ $this->callAPISuccess('Order', 'get', ['contribution_id' => $orderID]);
+ $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
+ $this->callAPISuccessGetCount('Participant', ['status_id' => 'Cancelled'], 1);
+ }
+
+ /**
+ * Test cancel order api when a pledge is linked.
+ *
+ * The pledge status should be updated. I believe the contribution should also be unlinked but
+ * the goal at this point is no change.
+ *
+ * @throws CRM_Core_Exception
+ * @throws API_Exception
+ */
+ public function testCancelOrderWithPledge() {
+ $this->createContact();
+ $pledgeID = (int) $this->callAPISuccess('Pledge', 'create', ['contact_id' => $this->ids['contact'][0], 'amount' => 4, 'installments' => 2, 'frequency_unit' => 'month', 'original_installment_amount' => 2, 'create_date' => 'now', 'financial_type_id' => 'Donation', 'start_date' => '+5 days'])['id'];
+ $orderID = (int) $this->callAPISuccess('Order', 'create', ['contact_id' => $this->ids['contact'][0], 'total_amount' => 2, 'financial_type_id' => 'Donation', 'api.Payment.create' => ['total_amount' => 2]])['id'];
+ $pledgePayments = $this->callAPISuccess('PledgePayment', 'get')['values'];
+ $this->callAPISuccess('PledgePayment', 'create', ['id' => key($pledgePayments), 'pledge_id' => $pledgeID, 'contribution_id' => $orderID, 'status_id' => 'Completed', 'actual_amount' => 2]);
+ $beforePledge = $this->callAPISuccessGetSingle('Pledge', ['id' => $pledgeID]);
+ $this->assertEquals(2, $beforePledge['pledge_total_paid']);
+ $this->callAPISuccess('Order', 'cancel', ['contribution_id' => $orderID]);
+
+ $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
+ $afterPledge = $this->callAPISuccessGetSingle('Pledge', ['id' => $pledgeID]);
+ $this->assertEquals('', $afterPledge['pledge_total_paid']);
+ }
+
+ /**
+ * Create an event and an order for a participant in that event.
+ *
+ * @return int
+ * @throws API_Exception
+ * @throws CRM_Core_Exception
+ */
+ protected function createEventOrder() {
+ $this->ids['event'][0] = (int) Event::create()->setValues(['title' => 'Event', 'start_date' => 'tomorrow', 'event_type_id:name' => 'Workshop'])->execute()->first()['id'];
+ $order = $this->callAPISuccess('Order', 'create', [
+ 'contact_id' => $this->ids['contact'][0],
+ 'financial_type_id' => 'Donation',
+ 'invoice_id' => 123,
+ 'line_items' => [
+ [
+ 'line_item' => [
+ [
+ 'line_total' => 5,
+ 'qty' => 1,
+ 'financial_type_id' => 1,
+ 'entity_table' => 'civicrm_participant',
+ 'price_field_id' => PriceField::get()->addSelect('id')->addWhere('name', '=', 'contribution_amount')->execute()->first()['id'],
+ ],
+ ],
+ 'params' => [
+ 'contact_id' => $this->ids['contact'][0],
+ 'event_id' => $this->ids['event'][0],
+ ],
+ ],
+ ],
+ ]);
+ return (int) $order['id'];
+ }
+
+ /**
+ * Create a contact for use in the test.
+ *
+ * @throws API_Exception
+ */
+ protected function createContact(): void {
+ $this->ids['contact'][0] = Civi\Api4\Contact::create()->setValues(['first_name' => 'Brer', 'last_name' => 'Rabbit'])->execute()->first()['id'];
+ }
+
+}
--- /dev/null
+<?php
+
+ini_set('memory_limit', '2G');
+ini_set('safe_mode', 0);
+// phpcs:disable
+eval(cv('php:boot --level=classloader', 'phpcode'));
+// phpcs:enable
+// Allow autoloading of PHPUnit helper classes in this extension.
+$loader = new \Composer\Autoload\ClassLoader();
+$loader->add('CRM_', __DIR__);
+$loader->add('Civi\\', __DIR__);
+$loader->add('api_', __DIR__);
+$loader->add('api\\', __DIR__);
+$loader->register();
+
+/**
+ * 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 = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR);
+ $oldOutput = getenv('CV_OUTPUT');
+ putenv("CV_OUTPUT=json");
+
+ // Execute `cv` in the original folder. This is a work-around for
+ // phpunit/codeception, which seem to manipulate PWD.
+ $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd);
+
+ $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)");
+ }
+}
}
+/**
+ * Remove un.
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_buildAmount
+ *
+ * @param string $component
+ * @param \CRM_Core_Form $form
+ * @param array $feeBlock
+ */
+function financialacls_civicrm_buildAmount($component, $form, &$feeBlock) {
+ if (CRM_Financial_BAO_FinancialType::isACLFinancialTypeStatus()) {
+ foreach ($feeBlock as $key => $value) {
+ foreach ($value['options'] as $k => $options) {
+ if (!CRM_Core_Permission::check('add contributions of type ' . CRM_Contribute_PseudoConstant::financialType($options['financial_type_id']))) {
+ unset($feeBlock[$key]['options'][$k]);
+ }
+ }
+ if (empty($feeBlock[$key]['options'])) {
+ unset($feeBlock[$key]);
+ }
+ }
+ }
+}
+
+/**
+ * Remove unpermitted membership types from selection availability..
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_membershipTypeValues
+ *
+ * @param \CRM_Core_Form $form
+ * @param array $membershipTypeValues
+ */
+function financialacls_civicrm_membershipTypeValues($form, &$membershipTypeValues) {
+ $financialTypes = NULL;
+ $financialTypes = CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($financialTypes, CRM_Core_Action::ADD);
+ foreach ($membershipTypeValues as $id => $type) {
+ if (!isset($financialTypes[$type['financial_type_id']])) {
+ unset($membershipTypeValues[$id]);
+ }
+ }
+}
+
+/**
+ * Remove unpermitted financial types from field Options in search context.
+ *
+ * Search context is described as
+ * 'search' => "search: searchable options are returned; labels are translated.",
+ * So this is appropriate to removing the options from search screens.
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_fieldOptions
+ *
+ * @param string $entity
+ * @param string $field
+ * @param array $options
+ * @param array $params
+ */
+function financialacls_civicrm_fieldOptions($entity, $field, &$options, $params) {
+ if ($entity === 'Contribution' && $field === 'financial_type_id' && $params['context'] === 'search') {
+ $action = CRM_Core_Action::VIEW;
+ // At this stage we are only considering the view action. Code from
+ // CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes().
+ $actions = [
+ CRM_Core_Action::VIEW => 'view',
+ CRM_Core_Action::UPDATE => 'edit',
+ CRM_Core_Action::ADD => 'add',
+ CRM_Core_Action::DELETE => 'delete',
+ ];
+ $cacheKey = 'available_types_' . $action;
+ if (!isset(\Civi::$statics['CRM_Financial_BAO_FinancialType'][$cacheKey])) {
+ foreach ($options as $finTypeId => $type) {
+ if (!CRM_Core_Permission::check($actions[$action] . ' contributions of type ' . $type)) {
+ unset($options[$finTypeId]);
+ }
+ }
+ \Civi::$statics['CRM_Financial_BAO_FinancialType'][$cacheKey] = $options;
+ }
+ $options = \Civi::$statics['CRM_Financial_BAO_FinancialType'][$cacheKey];
+ }
+}
+
// --- Functions below this ship commented out. Uncomment as required. ---
/**
--- /dev/null
+<?php
+
+namespace Civi\Financialacls;
+
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+use Civi\Test\ContactTestTrait;
+use Civi\Test\Api3TestTrait;
+
+/**
+ * @group headless
+ */
+class BaseTestClass extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ use ContactTestTrait;
+ use Api3TestTrait;
+
+ /**
+ * @return \Civi\Test\CiviEnvBuilder
+ * @throws \CRM_Extension_Exception_ParseException
+ */
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()
+ ->installMe(__DIR__)
+ ->apply();
+ }
+
+ /**
+ * Set ACL permissions, overwriting any existing ones.
+ *
+ * @param array $permissions
+ * Array of permissions e.g ['access CiviCRM','access CiviContribute'],
+ */
+ protected function setPermissions(array $permissions) {
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = $permissions;
+ if (isset(\Civi::$statics['CRM_Financial_BAO_FinancialType'])) {
+ unset(\Civi::$statics['CRM_Financial_BAO_FinancialType']);
+ }
+ }
+
+ protected function setupLoggedInUserWithLimitedFinancialTypeAccess(): void {
+ $this->setPermissions([
+ 'access CiviCRM',
+ 'access CiviContribute',
+ 'edit contributions',
+ 'delete in CiviContribute',
+ 'view contributions of type Donation',
+ 'delete contributions of type Donation',
+ 'add contributions of type Donation',
+ 'edit contributions of type Donation',
+ ]);
+ \Civi::settings()->set('acl_financial_type', TRUE);
+ $this->createLoggedInUser();
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Civi\Financialacls;
+
+use Civi\Api4\PriceField;
+use Civi\Api4\PriceSet;
+use Civi\Api4\PriceFieldValue;
+
+// I fought the Autoloader and the autoloader won.
+require_once 'BaseTestClass.php';
+
+/**
+ * Test that that financial acls are applied in the context of buildAmountHook.
+ *
+ * @group headless
+ */
+class BuildAmountHookTest extends BaseTestClass {
+
+ /**
+ * Test api applies permissions on line item actions (delete & get).
+ */
+ public function testBuildAmount() {
+ $priceSet = PriceSet::create()->setValues(['name' => 'test', 'title' => 'test', 'extends' => 'CiviMember'])->execute()->first();
+ PriceField::create()->setValues([
+ 'financial_type_id:name' => 'Donation',
+ 'name' => 'donation',
+ 'label' => 'donation',
+ 'price_set_id' => $priceSet['id'],
+ 'html_type' => 'Select',
+ ])->addChain('field_values', PriceFieldValue::save()->setRecords([
+ ['financial_type_id:name' => 'Donation', 'name' => 'a', 'label' => 'a', 'amount' => 1],
+ ['financial_type_id:name' => 'Member Dues', 'name' => 'b', 'label' => 'b', 'amount' => 2],
+ ])->setDefaults(['price_field_id' => '$id']))->execute();
+ $this->setupLoggedInUserWithLimitedFinancialTypeAccess();
+ $form = new \CRM_Member_Form_Membership();
+ $form->controller = new \CRM_Core_Controller();
+ $form->set('priceSetId', $priceSet['id']);
+ \CRM_Price_BAO_PriceSet::buildPriceSet($form);
+ $priceField = reset($form->_priceSet['fields']);
+ $this->assertCount(1, $priceField['options']);
+ $this->assertEquals('a', reset($priceField['options'])['name']);
+ }
+
+}
<?php
-use CRM_Financialacls_ExtensionUtil as E;
-use Civi\Test\HeadlessInterface;
-use Civi\Test\HookInterface;
-use Civi\Test\TransactionalInterface;
+namespace Civi\Financialacls;
+
use Civi\Api4\PriceField;
+// I fought the Autoloader and the autoloader won.
+require_once 'BaseTestClass.php';
+
/**
* FIXME - Add test description.
*
*
* @group headless
*/
-class LineItemTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
-
- use Civi\Test\ContactTestTrait;
- use Civi\Test\Api3TestTrait;
-
- /**
- * @return \Civi\Test\CiviEnvBuilder
- * @throws \CRM_Extension_Exception_ParseException
- */
- public function setUpHeadless() {
- // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
- // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
- return \Civi\Test::headless()
- ->installMe(__DIR__)
- ->apply();
- }
+class LineItemTest extends BaseTestClass {
/**
* Test api applies permissions on line item actions (delete & get).
+ *
* @dataProvider versionThreeAndFour
*/
public function testLineItemApiPermissions($version) {
[
'line_item' => [
[
- 'financial_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', 'Donation'),
+ 'financial_type_id' => \CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', 'Donation'),
'line_total' => 40,
'price_field_id' => $defaultPriceFieldID,
'qty' => 1,
],
[
- 'financial_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', 'Member Dues'),
+ 'financial_type_id' => \CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', 'Member Dues'),
'line_total' => 50,
'price_field_id' => $defaultPriceFieldID,
'qty' => 1,
]);
$this->_apiversion = $version;
- $this->setPermissions([
- 'access CiviCRM',
- 'access CiviContribute',
- 'edit contributions',
- 'delete in CiviContribute',
- 'view contributions of type Donation',
- 'delete contributions of type Donation',
- 'add contributions of type Donation',
- 'edit contributions of type Donation',
- ]);
- Civi::settings()->set('acl_financial_type', TRUE);
- $this->createLoggedInUser();
+ $this->setupLoggedInUserWithLimitedFinancialTypeAccess();
$lineItems = $this->callAPISuccess('LineItem', 'get', ['sequential' => TRUE])['values'];
$this->assertCount(2, $lineItems);
$this->callAPISuccess('LineItem', 'Create', ['id' => $line['id'], 'check_permissions' => TRUE, 'financial_type_id' => 'Donation']);
}
- /**
- * Set ACL permissions, overwriting any existing ones.
- *
- * @param array $permissions
- * Array of permissions e.g ['access CiviCRM','access CiviContribute'],
- */
- protected function setPermissions($permissions) {
- CRM_Core_Config::singleton()->userPermissionClass->permissions = $permissions;
- if (isset(\Civi::$statics['CRM_Financial_BAO_FinancialType'])) {
- unset(\Civi::$statics['CRM_Financial_BAO_FinancialType']);
- }
- }
-
/**
* @return mixed
* @throws \API_Exception
--- /dev/null
+<?php
+
+namespace Civi\Financialacls;
+
+use Civi\Api4\MembershipType;
+
+// I fought the Autoloader and the autoloader won.
+require_once 'BaseTestClass.php';
+
+/**
+ * @group headless
+ */
+class MembershipTypesTest extends BaseTestClass {
+
+ /**
+ * Test buildMembershipTypes.
+ */
+ public function testMembershipTypesHook() {
+ $types = MembershipType::save(FALSE)->setRecords([
+ ['name' => 'Forbidden', 'financial_type_id:name' => 'Member Dues'],
+ ['name' => 'Go for it', 'financial_type_id:name' => 'Donation'],
+ ])->setDefaults(['period_type' => 'rolling', 'member_of_contact_id' => 1])->execute()->indexBy('name');
+ $this->setupLoggedInUserWithLimitedFinancialTypeAccess();
+ $permissionedTypes = \CRM_Member_BAO_Membership::buildMembershipTypeValues(new \CRM_Member_Form_Membership());
+ $this->assertEquals([$types['Go for it']['id']], array_keys($permissionedTypes));
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Civi\Financialacls;
+
+// I fought the Autoloader and the autoloader won.
+require_once 'BaseTestClass.php';
+
+/**
+ * @group headless
+ */
+class OptionsTest extends BaseTestClass {
+
+ /**
+ * Test buildMembershipTypes.
+ */
+ public function testBuildOptions() {
+ $this->setupLoggedInUserWithLimitedFinancialTypeAccess();
+ $options = \CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes();
+ $this->assertEquals(['Donation'], array_merge($options));
+ $builtOptions = \CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search');
+ $this->assertEquals(['Donation'], array_merge($builtOptions));
+ }
+
+}
--- /dev/null
+<?php
+
+class CRM_OAuth_Angular {
+
+ public static function getSettings() {
+ $s = [];
+
+ $s['redirectUrl'] = \CRM_OAuth_BAO_OAuthClient::getRedirectUri();
+ $s['providers'] = civicrm_api4('OAuthProvider', 'get', [])->indexBy('name');
+
+ return $s;
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+class CRM_OAuth_BAO_OAuthClient extends CRM_OAuth_DAO_OAuthClient {
+
+ /**
+ * Create a new OAuthClient based on array-data
+ *
+ * @param array $params key-value pairs
+ * @return CRM_OAuth_DAO_OAuthClient|NULL
+ *
+ * public static function create($params) {
+ * $className = 'CRM_OAuth_DAO_OAuthClient';
+ * $entityName = 'OAuthClient';
+ * $hook = empty($params['id']) ? 'create' : 'edit';
+ *
+ * CRM_Utils_Hook::pre($hook, $entityName, CRM_Utils_Array::value('id', $params), $params);
+ * $instance = new $className();
+ * $instance->copyValues($params);
+ * $instance->save();
+ * CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance);
+ *
+ * return $instance;
+ * } */
+
+ /**
+ * @return array
+ * ~~Ex: ['my_provider' => 'My Provider']~~
+ * Ex: ['my_provider' => 'my_provider']
+ */
+ public static function getProviders() {
+ if (!isset(Civi::$statics[__FUNCTION__])) {
+ if (!class_exists('\Civi\Api4\OAuthProvider')) {
+ return [];
+ }
+ $ps = Civi\Api4\OAuthProvider::get(FALSE)
+ ->setSelect(['name', 'title'])
+ ->execute();
+ $titles = [];
+ foreach ($ps as $p) {
+ $titles[$p['name']] = $p['name'];
+ // $titles[$p['name']] = $p['title'];
+ }
+ Civi::$statics[__FUNCTION__] = $titles;
+ }
+ return Civi::$statics[__FUNCTION__];
+ }
+
+ /**
+ * Determine the "redirect_uri". When using authorization-code flow, the
+ * OAuth2 provider will redirect back to our "redirect_uri".
+ *
+ * @return string
+ */
+ public static function getRedirectUri() {
+ return \Civi::settings()->get('oauthClientRedirectUrl') ?:
+ \CRM_Utils_System::url('civicrm/oauth-client/return', NULL, TRUE, NULL, FALSE);
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+class CRM_OAuth_BAO_OAuthSysToken extends CRM_OAuth_DAO_OAuthSysToken {
+
+ private static $returnFields = ['id', 'client_id', 'expires', 'tag'];
+
+ /**
+ * Create a new OAuthSysToken based on array-data
+ *
+ * @param array $params key-value pairs
+ * @return CRM_OAuth_DAO_OAuthSysToken|NULL
+ *
+ * public static function create($params) {
+ * $className = 'CRM_OAuth_DAO_OAuthSysToken';
+ * $entityName = 'OAuthSysToken';
+ * $hook = empty($params['id']) ? 'create' : 'edit';
+ *
+ * CRM_Utils_Hook::pre($hook, $entityName, CRM_Utils_Array::value('id', $params), $params);
+ * $instance = new $className();
+ * $instance->copyValues($params);
+ * $instance->save();
+ * CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance);
+ *
+ * return $instance;
+ * } */
+
+ /**
+ * Redact the content of a token.
+ *
+ * This is useful for processes which must internally use the entire token
+ * record -- but then report on their progress to a permissioned party.
+ *
+ * @param array $tokenRecord
+ * @return array
+ */
+ public static function redact($tokenRecord) {
+ if (!\CRM_Core_Permission::check('manage OAuth client secrets')) {
+ return \CRM_Utils_Array::subset($tokenRecord, self::$returnFields);
+ }
+ else {
+ return $tokenRecord;
+ }
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ *
+ * Generated from oauth-client/xml/schema/CRM/OAuth/OAuthClient.xml
+ * DO NOT EDIT. Generated by CRM_Core_CodeGen
+ * (GenCodeChecksum:7487cf595064832b3d55188b3e48bffc)
+ */
+use CRM_OAuth_ExtensionUtil as E;
+
+/**
+ * Database access object for the OAuthClient entity.
+ */
+class CRM_OAuth_DAO_OAuthClient extends CRM_Core_DAO {
+ const EXT = E::LONG_NAME;
+ const TABLE_ADDED = '5.32';
+
+ /**
+ * Static instance to hold the table name.
+ *
+ * @var string
+ */
+ public static $_tableName = 'civicrm_oauth_client';
+
+ /**
+ * Should CiviCRM log any modifications to this table in the civicrm_log table.
+ *
+ * @var bool
+ */
+ public static $_log = FALSE;
+
+ /**
+ * Internal Client ID
+ *
+ * @var int
+ */
+ public $id;
+
+ /**
+ * Provider
+ *
+ * @var string
+ */
+ public $provider;
+
+ /**
+ * Client ID
+ *
+ * @var string
+ */
+ public $guid;
+
+ /**
+ * Client Secret
+ *
+ * @var text
+ */
+ public $secret;
+
+ /**
+ * Extra override options for the service (JSON)
+ *
+ * @var text
+ */
+ public $options;
+
+ /**
+ * Is the client currently enabled?
+ *
+ * @var bool
+ */
+ public $is_active;
+
+ /**
+ * When the client was created.
+ *
+ * @var timestamp
+ */
+ public $created_date;
+
+ /**
+ * When the client was created or modified.
+ *
+ * @var timestamp
+ */
+ public $modified_date;
+
+ /**
+ * Class constructor.
+ */
+ public function __construct() {
+ $this->__table = 'civicrm_oauth_client';
+ parent::__construct();
+ }
+
+ /**
+ * Returns localized title of this entity.
+ *
+ * @param bool $plural
+ * Whether to return the plural version of the title.
+ */
+ public static function getEntityTitle($plural = FALSE) {
+ return $plural ? E::ts('OAuth Clients') : E::ts('OAuth Client');
+ }
+
+ /**
+ * Returns all the column names of this table
+ *
+ * @return array
+ */
+ public static function &fields() {
+ if (!isset(Civi::$statics[__CLASS__]['fields'])) {
+ Civi::$statics[__CLASS__]['fields'] = [
+ 'id' => [
+ 'name' => 'id',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'title' => E::ts('Internal Client ID'),
+ 'description' => E::ts('Internal Client ID'),
+ 'where' => 'civicrm_oauth_client.id',
+ 'table_name' => 'civicrm_oauth_client',
+ 'entity' => 'OAuthClient',
+ 'bao' => 'CRM_OAuth_DAO_OAuthClient',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'provider' => [
+ 'name' => 'provider',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Provider'),
+ 'description' => E::ts('Provider'),
+ 'required' => TRUE,
+ 'maxlength' => 128,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_oauth_client.provider',
+ 'table_name' => 'civicrm_oauth_client',
+ 'entity' => 'OAuthClient',
+ 'bao' => 'CRM_OAuth_DAO_OAuthClient',
+ 'localizable' => 0,
+ 'pseudoconstant' => [
+ 'callback' => 'CRM_OAuth_BAO_OAuthClient::getProviders',
+ ],
+ 'add' => '5.32',
+ ],
+ 'guid' => [
+ 'name' => 'guid',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Client ID'),
+ 'description' => E::ts('Client ID'),
+ 'required' => TRUE,
+ 'maxlength' => 128,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_oauth_client.guid',
+ 'table_name' => 'civicrm_oauth_client',
+ 'entity' => 'OAuthClient',
+ 'bao' => 'CRM_OAuth_DAO_OAuthClient',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'secret' => [
+ 'name' => 'secret',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Client Secret'),
+ 'description' => E::ts('Client Secret'),
+ 'where' => 'civicrm_oauth_client.secret',
+ 'table_name' => 'civicrm_oauth_client',
+ 'entity' => 'OAuthClient',
+ 'bao' => 'CRM_OAuth_DAO_OAuthClient',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'options' => [
+ 'name' => 'options',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Options'),
+ 'description' => E::ts('Extra override options for the service (JSON)'),
+ 'where' => 'civicrm_oauth_client.options',
+ 'table_name' => 'civicrm_oauth_client',
+ 'entity' => 'OAuthClient',
+ 'bao' => 'CRM_OAuth_DAO_OAuthClient',
+ 'localizable' => 0,
+ 'serialize' => self::SERIALIZE_JSON,
+ 'add' => '5.32',
+ ],
+ 'is_active' => [
+ 'name' => 'is_active',
+ 'type' => CRM_Utils_Type::T_BOOLEAN,
+ 'title' => E::ts('Is Active'),
+ 'description' => E::ts('Is the client currently enabled?'),
+ 'required' => TRUE,
+ 'where' => 'civicrm_oauth_client.is_active',
+ 'default' => '1',
+ 'table_name' => 'civicrm_oauth_client',
+ 'entity' => 'OAuthClient',
+ 'bao' => 'CRM_OAuth_DAO_OAuthClient',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'created_date' => [
+ 'name' => 'created_date',
+ 'type' => CRM_Utils_Type::T_TIMESTAMP,
+ 'title' => E::ts('Created Date'),
+ 'description' => E::ts('When the client was created.'),
+ 'required' => TRUE,
+ 'where' => 'civicrm_oauth_client.created_date',
+ 'default' => 'CURRENT_TIMESTAMP',
+ 'table_name' => 'civicrm_oauth_client',
+ 'entity' => 'OAuthClient',
+ 'bao' => 'CRM_OAuth_DAO_OAuthClient',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'modified_date' => [
+ 'name' => 'modified_date',
+ 'type' => CRM_Utils_Type::T_TIMESTAMP,
+ 'title' => E::ts('Modified Date'),
+ 'description' => E::ts('When the client was created or modified.'),
+ 'required' => TRUE,
+ 'where' => 'civicrm_oauth_client.modified_date',
+ 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+ 'table_name' => 'civicrm_oauth_client',
+ 'entity' => 'OAuthClient',
+ 'bao' => 'CRM_OAuth_DAO_OAuthClient',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ ];
+ CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
+ }
+ return Civi::$statics[__CLASS__]['fields'];
+ }
+
+ /**
+ * Return a mapping from field-name to the corresponding key (as used in fields()).
+ *
+ * @return array
+ * Array(string $name => string $uniqueName).
+ */
+ public static function &fieldKeys() {
+ if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) {
+ Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields()));
+ }
+ return Civi::$statics[__CLASS__]['fieldKeys'];
+ }
+
+ /**
+ * Returns the names of this table
+ *
+ * @return string
+ */
+ public static function getTableName() {
+ return self::$_tableName;
+ }
+
+ /**
+ * Returns if this table needs to be logged
+ *
+ * @return bool
+ */
+ public function getLog() {
+ return self::$_log;
+ }
+
+ /**
+ * Returns the list of fields that can be imported
+ *
+ * @param bool $prefix
+ *
+ * @return array
+ */
+ public static function &import($prefix = FALSE) {
+ $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'oauth_client', $prefix, []);
+ return $r;
+ }
+
+ /**
+ * Returns the list of fields that can be exported
+ *
+ * @param bool $prefix
+ *
+ * @return array
+ */
+ public static function &export($prefix = FALSE) {
+ $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'oauth_client', $prefix, []);
+ return $r;
+ }
+
+ /**
+ * Returns the list of indices
+ *
+ * @param bool $localize
+ *
+ * @return array
+ */
+ public static function indices($localize = TRUE) {
+ $indices = [
+ 'UI_provider' => [
+ 'name' => 'UI_provider',
+ 'field' => [
+ 0 => 'provider',
+ ],
+ 'localizable' => FALSE,
+ 'sig' => 'civicrm_oauth_client::0::provider',
+ ],
+ 'UI_guid' => [
+ 'name' => 'UI_guid',
+ 'field' => [
+ 0 => 'guid',
+ ],
+ 'localizable' => FALSE,
+ 'sig' => 'civicrm_oauth_client::0::guid',
+ ],
+ ];
+ return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ *
+ * Generated from oauth-client/xml/schema/CRM/OAuth/OAuthSysToken.xml
+ * DO NOT EDIT. Generated by CRM_Core_CodeGen
+ * (GenCodeChecksum:1b0fa60330b4ea4a6d30bd972ccf3633)
+ */
+use CRM_OAuth_ExtensionUtil as E;
+
+/**
+ * Database access object for the OAuthSysToken entity.
+ */
+class CRM_OAuth_DAO_OAuthSysToken extends CRM_Core_DAO {
+ const EXT = E::LONG_NAME;
+ const TABLE_ADDED = '5.32';
+
+ /**
+ * Static instance to hold the table name.
+ *
+ * @var string
+ */
+ public static $_tableName = 'civicrm_oauth_systoken';
+
+ /**
+ * Should CiviCRM log any modifications to this table in the civicrm_log table.
+ *
+ * @var bool
+ */
+ public static $_log = FALSE;
+
+ /**
+ * Token ID
+ *
+ * @var int
+ */
+ public $id;
+
+ /**
+ * The tag specifies how this token will be used.
+ *
+ * @var string
+ */
+ public $tag;
+
+ /**
+ * Client ID
+ *
+ * @var int
+ */
+ public $client_id;
+
+ /**
+ * Ex: authorization_code
+ *
+ * @var string
+ */
+ public $grant_type;
+
+ /**
+ * List of scopes addressed by this token
+ *
+ * @var text
+ */
+ public $scopes;
+
+ /**
+ * Ex: Bearer or MAC
+ *
+ * @var string
+ */
+ public $token_type;
+
+ /**
+ * Token to present when accessing resources
+ *
+ * @var text
+ */
+ public $access_token;
+
+ /**
+ * Expiration time for the access_token (seconds since epoch)
+ *
+ * @var int
+ */
+ public $expires;
+
+ /**
+ * Token to present when refreshing the access_token
+ *
+ * @var text
+ */
+ public $refresh_token;
+
+ /**
+ * Identifier for the resource owner. Structure varies by service.
+ *
+ * @var string
+ */
+ public $resource_owner_name;
+
+ /**
+ * Cached details describing the resource owner
+ *
+ * @var text
+ */
+ public $resource_owner;
+
+ /**
+ * List of scopes addressed by this token
+ *
+ * @var text
+ */
+ public $error;
+
+ /**
+ * The token response data, per AccessToken::jsonSerialize
+ *
+ * @var text
+ */
+ public $raw;
+
+ /**
+ * When the client was created.
+ *
+ * @var timestamp
+ */
+ public $created_date;
+
+ /**
+ * When the client was created or modified.
+ *
+ * @var timestamp
+ */
+ public $modified_date;
+
+ /**
+ * Class constructor.
+ */
+ public function __construct() {
+ $this->__table = 'civicrm_oauth_systoken';
+ parent::__construct();
+ }
+
+ /**
+ * Returns localized title of this entity.
+ *
+ * @param bool $plural
+ * Whether to return the plural version of the title.
+ */
+ public static function getEntityTitle($plural = FALSE) {
+ return $plural ? E::ts('OAuth Sys Tokens') : E::ts('OAuth Sys Token');
+ }
+
+ /**
+ * Returns foreign keys and entity references.
+ *
+ * @return array
+ * [CRM_Core_Reference_Interface]
+ */
+ public static function getReferenceColumns() {
+ if (!isset(Civi::$statics[__CLASS__]['links'])) {
+ Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__);
+ Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'client_id', 'civicrm_oauth_client', 'id');
+ CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']);
+ }
+ return Civi::$statics[__CLASS__]['links'];
+ }
+
+ /**
+ * Returns all the column names of this table
+ *
+ * @return array
+ */
+ public static function &fields() {
+ if (!isset(Civi::$statics[__CLASS__]['fields'])) {
+ Civi::$statics[__CLASS__]['fields'] = [
+ 'id' => [
+ 'name' => 'id',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'title' => E::ts('Token ID'),
+ 'description' => E::ts('Token ID'),
+ 'required' => TRUE,
+ 'where' => 'civicrm_oauth_systoken.id',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'tag' => [
+ 'name' => 'tag',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Tag'),
+ 'description' => E::ts('The tag specifies how this token will be used.'),
+ 'maxlength' => 128,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_oauth_systoken.tag',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'client_id' => [
+ 'name' => 'client_id',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'title' => E::ts('Client ID'),
+ 'description' => E::ts('Client ID'),
+ 'where' => 'civicrm_oauth_systoken.client_id',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'FKClassName' => 'CRM_OAuth_DAO_OAuthClient',
+ 'add' => '5.32',
+ ],
+ 'grant_type' => [
+ 'name' => 'grant_type',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Grant type'),
+ 'description' => E::ts('Ex: authorization_code'),
+ 'maxlength' => 31,
+ 'size' => CRM_Utils_Type::MEDIUM,
+ 'where' => 'civicrm_oauth_systoken.grant_type',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'scopes' => [
+ 'name' => 'scopes',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Scopes'),
+ 'description' => E::ts('List of scopes addressed by this token'),
+ 'where' => 'civicrm_oauth_systoken.scopes',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'serialize' => self::SERIALIZE_SEPARATOR_BOOKEND,
+ 'add' => '5.32',
+ ],
+ 'token_type' => [
+ 'name' => 'token_type',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Token Type'),
+ 'description' => E::ts('Ex: Bearer or MAC'),
+ 'maxlength' => 128,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_oauth_systoken.token_type',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'access_token' => [
+ 'name' => 'access_token',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Access Token'),
+ 'description' => E::ts('Token to present when accessing resources'),
+ 'where' => 'civicrm_oauth_systoken.access_token',
+ 'permission' => [
+ [
+ 'manage OAuth client secrets',
+ ],
+ ],
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'expires' => [
+ 'name' => 'expires',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'title' => E::ts('Expiration time'),
+ 'description' => E::ts('Expiration time for the access_token (seconds since epoch)'),
+ 'where' => 'civicrm_oauth_systoken.expires',
+ 'default' => '0',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '4.7',
+ ],
+ 'refresh_token' => [
+ 'name' => 'refresh_token',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Refresh Token'),
+ 'description' => E::ts('Token to present when refreshing the access_token'),
+ 'where' => 'civicrm_oauth_systoken.refresh_token',
+ 'permission' => [
+ [
+ 'manage OAuth client secrets',
+ ],
+ ],
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'resource_owner_name' => [
+ 'name' => 'resource_owner_name',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Resource Owner Name'),
+ 'description' => E::ts('Identifier for the resource owner. Structure varies by service.'),
+ 'maxlength' => 128,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_oauth_systoken.resource_owner_name',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'resource_owner' => [
+ 'name' => 'resource_owner',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Resource Owner'),
+ 'description' => E::ts('Cached details describing the resource owner'),
+ 'where' => 'civicrm_oauth_systoken.resource_owner',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'serialize' => self::SERIALIZE_JSON,
+ 'add' => '5.32',
+ ],
+ 'error' => [
+ 'name' => 'error',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Error'),
+ 'description' => E::ts('List of scopes addressed by this token'),
+ 'where' => 'civicrm_oauth_systoken.error',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'serialize' => self::SERIALIZE_JSON,
+ 'add' => '5.32',
+ ],
+ 'raw' => [
+ 'name' => 'raw',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Raw token'),
+ 'description' => E::ts('The token response data, per AccessToken::jsonSerialize'),
+ 'where' => 'civicrm_oauth_systoken.raw',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'serialize' => self::SERIALIZE_JSON,
+ 'add' => '5.32',
+ ],
+ 'created_date' => [
+ 'name' => 'created_date',
+ 'type' => CRM_Utils_Type::T_TIMESTAMP,
+ 'title' => E::ts('Created Date'),
+ 'description' => E::ts('When the client was created.'),
+ 'required' => FALSE,
+ 'where' => 'civicrm_oauth_systoken.created_date',
+ 'default' => 'CURRENT_TIMESTAMP',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ 'modified_date' => [
+ 'name' => 'modified_date',
+ 'type' => CRM_Utils_Type::T_TIMESTAMP,
+ 'title' => E::ts('Modified Date'),
+ 'description' => E::ts('When the client was created or modified.'),
+ 'required' => FALSE,
+ 'where' => 'civicrm_oauth_systoken.modified_date',
+ 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+ 'table_name' => 'civicrm_oauth_systoken',
+ 'entity' => 'OAuthSysToken',
+ 'bao' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'localizable' => 0,
+ 'add' => '5.32',
+ ],
+ ];
+ CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
+ }
+ return Civi::$statics[__CLASS__]['fields'];
+ }
+
+ /**
+ * Return a mapping from field-name to the corresponding key (as used in fields()).
+ *
+ * @return array
+ * Array(string $name => string $uniqueName).
+ */
+ public static function &fieldKeys() {
+ if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) {
+ Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields()));
+ }
+ return Civi::$statics[__CLASS__]['fieldKeys'];
+ }
+
+ /**
+ * Returns the names of this table
+ *
+ * @return string
+ */
+ public static function getTableName() {
+ return self::$_tableName;
+ }
+
+ /**
+ * Returns if this table needs to be logged
+ *
+ * @return bool
+ */
+ public function getLog() {
+ return self::$_log;
+ }
+
+ /**
+ * Returns the list of fields that can be imported
+ *
+ * @param bool $prefix
+ *
+ * @return array
+ */
+ public static function &import($prefix = FALSE) {
+ $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'oauth_systoken', $prefix, []);
+ return $r;
+ }
+
+ /**
+ * Returns the list of fields that can be exported
+ *
+ * @param bool $prefix
+ *
+ * @return array
+ */
+ public static function &export($prefix = FALSE) {
+ $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'oauth_systoken', $prefix, []);
+ return $r;
+ }
+
+ /**
+ * Returns the list of indices
+ *
+ * @param bool $localize
+ *
+ * @return array
+ */
+ public static function indices($localize = TRUE) {
+ $indices = [
+ 'UI_tag' => [
+ 'name' => 'UI_tag',
+ 'field' => [
+ 0 => 'tag',
+ ],
+ 'localizable' => FALSE,
+ 'sig' => 'civicrm_oauth_systoken::0::tag',
+ ],
+ ];
+ return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
+ }
+
+}
--- /dev/null
+<?php
+
+class CRM_OAuth_MailSetup {
+
+ /**
+ * Return a list of setup-options based on OAuth2 services.
+ *
+ * @see CRM_Utils_Hook::mailSetupActions()
+ */
+ public static function buildSetupLinks() {
+ $clients = Civi\Api4\OAuthClient::get(0)->addWhere('is_active', '=', 1)->execute();
+ $providers = Civi\Api4\OAuthProvider::get(0)->execute()->indexBy('name');
+
+ $setupActions = [];
+ foreach ($clients as $client) {
+ $provider = $providers[$client['provider']] ?? NULL;
+ if ($provider === NULL) {
+ continue;
+ }
+ // v api OptionValue.get option_group_id=mail_protocol
+ if (!empty($provider['mailSettingsTemplate'])) {
+ $setupActions['oauth_' . $client['id']] = [
+ 'title' => sprintf('%s (ID #%s)', $provider['title'] ?? $provider['name'] ?? ts('OAuth2'), $client['id']),
+ 'callback' => ['CRM_OAuth_MailSetup', 'setup'],
+ 'oauth_client_id' => $client['id'],
+ ];
+ }
+ }
+
+ return $setupActions;
+ }
+
+ /**
+ * When a user chooses to add one of our mail options, we kick off
+ * the authorization-code workflow.
+ *
+ * @param array $setupAction
+ * The chosen descriptor from mailSetupActions.
+ * @return array
+ * With keys:
+ * - url: string, the final URL to go to.
+ * @see CRM_Utils_Hook::mailSetupActions()
+ */
+ public static function setup($setupAction) {
+ $authCode = Civi\Api4\OAuthClient::authorizationCode(0)
+ ->addWhere('id', '=', $setupAction['oauth_client_id'])
+ ->setStorage('OAuthSysToken')
+ ->setTag('MailSettings:setup')
+ ->setPrompt('select_account')
+ ->execute()
+ ->single();
+
+ return [
+ 'url' => $authCode['url'],
+ ];
+ }
+
+ /**
+ * When the user returns with a token, we add a new record to
+ * civicrm_mail_settings with defaults and redirect to the edit screen.
+ *
+ * @param array $token
+ * OAuthSysToken
+ * @param string $nextUrl
+ */
+ public static function onReturn($token, &$nextUrl) {
+ if ($token['tag'] !== 'MailSettings:setup') {
+ return;
+ }
+
+ $client = \Civi\Api4\OAuthClient::get(0)->addWhere('id', '=', $token['client_id'])->execute()->single();
+ $provider = \Civi\Api4\OAuthProvider::get(0)->addWhere('name', '=', $client['provider'])->execute()->single();
+
+ $vars = ['token' => $token, 'client' => $client, 'provider' => $provider];
+ $mailSettings = civicrm_api4('MailSettings', 'create', [
+ 'values' => self::evalArrayTemplate($provider['mailSettingsTemplate'], $vars),
+ ])->single();
+
+ \Civi\Api4\OAuthSysToken::update(0)
+ ->addWhere('id', '=', $token['id'])
+ ->setValues(['tag' => 'MailSettings:' . $mailSettings['id']])
+ ->execute();
+
+ CRM_Core_Session::setStatus(
+ ts('Here are the account defaults we detected for %1. Please check them carefully.', [
+ 1 => $mailSettings['name'],
+ ]),
+ ts('Account created!'),
+ 'info'
+ );
+
+ $nextUrl = CRM_Utils_System::url('civicrm/admin/mailSettings', [
+ 'action' => 'update',
+ 'id' => $mailSettings['id'],
+ 'reset' => 1,
+ ], TRUE, NULL, FALSE);
+ }
+
+ /**
+ * @param array $template
+ * List of key-value expressions.
+ * Ex: ['name' => '{{person.first}} {{person.last}}']
+ * Expressions begin with the dotted-name of a variable.
+ * Optionally, the value may be piped through other functions
+ * @param array $vars
+ * Array tree of data to interpolate.
+ * @return array
+ * The template array, with '{{...}}' expressions evaluated.
+ */
+ public static function evalArrayTemplate($template, $vars) {
+ $filters = [
+ 'getMailDomain' => function($v) {
+ $parts = explode('@', $v);
+ return $parts[1] ?? NULL;
+ },
+ 'getMailUser' => function($v) {
+ $parts = explode('@', $v);
+ return $parts[0] ?? NULL;
+ },
+ ];
+
+ $lookupVars = function($m) use ($vars, $filters) {
+ $parts = explode('|', $m[1]);
+ $value = (string) CRM_Utils_Array::pathGet($vars, explode('.', array_shift($parts)));
+ foreach ($parts as $part) {
+ if (isset($filters[$part])) {
+ $value = $filters[$part]($value);
+ }
+ else {
+ $value = NULL;
+ }
+ }
+ return $value;
+ };
+
+ $values = [];
+ foreach ($template as $key => $value) {
+ $values[$key] = is_string($value)
+ ? preg_replace_callback(';{{([a-zA-Z0-9_\.\|]+)}};', $lookupVars, $value)
+ : $value;
+ }
+ return $values;
+ }
+
+ /**
+ * If we have a stored token for this for this, then use it.
+ *
+ * @see CRM_Utils_Hook::alterMailStore()
+ */
+ public static function alterMailStore(&$mailSettings) {
+ $token = civicrm_api4('OAuthSysToken', 'refresh', [
+ 'checkPermissions' => FALSE,
+ 'where' => [['tag', '=', 'MailSettings:' . $mailSettings['id']]],
+ 'orderBy' => ['id' => 'DESC'],
+ ])->first();
+
+ if ($token === NULL) {
+ return;
+ }
+ // Not certain if 'refresh' will complain about staleness. Doesn't hurt to double-check.
+ if (empty($token['access_token']) || $token['expires'] < CRM_Utils_Time::getTimeRaw()) {
+ throw new \OAuthException("Found invalid token for mail store #" . $mailSettings['id']);
+ }
+
+ $mailSettings['auth'] = 'XOAuth2';
+ $mailSettings['password'] = $token['access_token'];
+ }
+
+}
--- /dev/null
+<?php
+use CRM_OAuth_ExtensionUtil as E;
+
+class CRM_OAuth_Page_Return extends CRM_Core_Page {
+
+ const TTL = 3600;
+
+ public function run() {
+ $json = function ($d) {
+ return json_encode($d, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ };
+
+ $state = self::loadState(CRM_Utils_Request::retrieve('state', 'String'));
+ if (CRM_Core_Permission::check('manage OAuth client')) {
+ $this->assign('state', $state);
+ $this->assign('stateJson', $json($state ?? NULL));
+ }
+
+ if (CRM_Utils_Request::retrieve('error', 'String')) {
+ CRM_Utils_System::setTitle(ts('OAuth Error'));
+ $error = CRM_Utils_Array::subset($_GET, ['error', 'error_description', 'error_uri']);
+ $event = \Civi\Core\Event\GenericHookEvent::create([
+ 'error' => $error['error'] ?? NULL,
+ 'description' => $error['description'] ?? NULL,
+ 'uri' => $error['uri'] ?? NULL,
+ 'state' => $state,
+ ]);
+ Civi::dispatcher()->dispatch('hook_civicrm_oauthReturnError', $event);
+
+ Civi::log()->info('OAuth returned error', [
+ 'error' => $error,
+ 'state' => $state,
+ ]);
+
+ $this->assign('error', $error ?? NULL);
+ }
+ elseif ($authCode = CRM_Utils_Request::retrieve('code', 'String')) {
+ $client = \Civi\Api4\OAuthClient::get(0)->addWhere('id', '=', $state['clientId'])->execute()->single();
+ $tokenRecord = Civi::service('oauth2.token')->init([
+ 'client' => $client,
+ 'scope' => $state['scopes'],
+ 'tag' => $state['tag'],
+ 'storage' => $state['storage'],
+ 'grant_type' => 'authorization_code',
+ 'cred' => ['code' => $authCode],
+ ]);
+
+ $nextUrl = $state['landingUrl'] ?? NULL;
+ $event = \Civi\Core\Event\GenericHookEvent::create([
+ 'token' => $tokenRecord,
+ 'nextUrl' => &$nextUrl,
+ ]);
+ Civi::dispatcher()->dispatch('hook_civicrm_oauthReturn', $event);
+ if ($nextUrl !== NULL) {
+ CRM_Utils_System::redirect($nextUrl);
+ }
+
+ CRM_Utils_System::setTitle(ts('OAuth Token Created'));
+ if (CRM_Core_Permission::check('manage OAuth client')) {
+ $this->assign('token', CRM_OAuth_BAO_OAuthSysToken::redact($tokenRecord));
+ $this->assign('tokenJson', $json(CRM_OAuth_BAO_OAuthSysToken::redact($tokenRecord)));
+ }
+ }
+ else {
+ throw new \Civi\OAuth\OAuthException("OAuth: Unrecognized return request");
+ }
+
+ parent::run();
+ }
+
+ /**
+ * @param array $stateData
+ * @return string
+ * State token / identifier
+ */
+ public static function storeState($stateData):string {
+ $stateId = \CRM_Utils_String::createRandom(20, \CRM_Utils_String::ALPHANUMERIC);
+
+ if (PHP_SAPI === 'cli') {
+ // CLI doesn't have a real session, so we can't defend as deeply. However,
+ // it's also quite uncommon to run authorizationCode in CLI.
+ \Civi::cache('session')->set('OAuthStates_' . $stateId, $stateData, self::TTL);
+ return 'c_' . $stateId;
+ }
+ else {
+ // Storing in the bona fide session binds us to the cookie
+ $session = \CRM_Core_Session::singleton();
+ $session->createScope('OAuthStates');
+ $session->set($stateId, $stateData, 'OAuthStates');
+ return 'w_' . $stateId;
+ }
+ }
+
+ /**
+ * Restore from the $stateId.
+ *
+ * @param string $stateId
+ * @return mixed
+ * @throws \Civi\OAuth\OAuthException
+ */
+ public static function loadState($stateId) {
+ list ($type, $id) = explode('_', $stateId);
+ switch ($type) {
+ case 'w':
+ $state = \CRM_Core_Session::singleton()->get($id, 'OAuthStates');
+ break;
+
+ case 'c':
+ $state = \Civi::cache('session')->get('OAuthStates_' . $id);
+ break;
+
+ default:
+ throw new \Civi\OAuth\OAuthException("OAuth: Received invalid or expired state");
+ }
+
+ if (!isset($state['time']) || $state['time'] + self::TTL < CRM_Utils_Time::getTimeRaw()) {
+ throw new \Civi\OAuth\OAuthException("OAuth: Received invalid or expired state");
+ }
+
+ return $state;
+ }
+
+}
--- /dev/null
+<?php
+use CRM_OAuth_ExtensionUtil as E;
+
+/**
+ * Collection of upgrade steps.
+ */
+class CRM_OAuth_Upgrader extends CRM_OAuth_Upgrader_Base {
+
+ // By convention, functions that look like "function upgrade_NNNN()" are
+ // upgrade tasks. They are executed in order (like Drupal's hook_update_N).
+
+ /**
+ * @see CRM_Utils_Hook::install()
+ */
+ public function install() {
+ $domainId = CRM_Core_Config::domainID();
+ civicrm_api3('Navigation', 'create', [
+ 'sequential' => 1,
+ 'domain_id' => $domainId,
+ 'url' => "civicrm/admin/oauth",
+ 'permission' => "manage OAuth client",
+ 'label' => "OAuth",
+ 'permission_operator' => "OR",
+ 'has_separator' => 0,
+ 'is_active' => 1,
+ 'parent_id' => "System Settings",
+ ]);
+ }
+
+ /**
+ * Example: Run an external SQL script when the module is installed.
+ *
+ * public function install() {
+ * $this->executeSqlFile('sql/myinstall.sql');
+ * }
+ *
+ * /**
+ * 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),
+ // ));
+ // }
+
+ /**
+ * Example: Run an external SQL script when the module is uninstalled.
+ */
+ // public function uninstall() {
+ // $this->executeSqlFile('sql/myuninstall.sql');
+ // }
+
+ /**
+ * 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(E::ts('Process first step'), 'processPart1', $arg1, $arg2);
+ // $this->addTask(E::ts('Process second step'), 'processPart2', $arg3, $arg4);
+ // $this->addTask(E::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 = E::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;
+ // }
+
+}
--- /dev/null
+<?php
+
+// AUTO-GENERATED FILE -- Civix may overwrite any changes made to this file
+use CRM_OAuth_ExtensionUtil as E;
+
+/**
+ * Base class which provides helpers to execute upgrade logic
+ */
+class CRM_OAuth_Upgrader_Base {
+
+ /**
+ * @var CRM_OAuth_Upgrader_Base
+ */
+ public static $instance;
+
+ /**
+ * @var CRM_Queue_TaskContext
+ */
+ protected $ctx;
+
+ /**
+ * @var string
+ * eg 'com.example.myextension'
+ */
+ protected $extensionName;
+
+ /**
+ * @var string
+ * full path to the extension's source tree
+ */
+ protected $extensionDir;
+
+ /**
+ * @var array
+ * sorted numerically
+ */
+ private $revisions;
+
+ /**
+ * @var bool
+ * Flag to clean up extension revision data in civicrm_setting
+ */
+ private $revisionStorageIsDeprecated = FALSE;
+
+ /**
+ * Obtain a reference to the active upgrade handler.
+ */
+ public static function instance() {
+ if (!self::$instance) {
+ self::$instance = new CRM_OAuth_Upgrader(
+ 'oauth-client',
+ E::path()
+ );
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Adapter that lets you add normal (non-static) member functions to the queue.
+ *
+ * Note: Each upgrader instance should only be associated with one
+ * task-context; otherwise, this will be non-reentrant.
+ *
+ * ```
+ * CRM_OAuth_Upgrader_Base::_queueAdapter($ctx, 'methodName', 'arg1', 'arg2');
+ * ```
+ */
+ public static function _queueAdapter() {
+ $instance = self::instance();
+ $args = func_get_args();
+ $instance->ctx = array_shift($args);
+ $instance->queue = $instance->ctx->queue;
+ $method = array_shift($args);
+ return call_user_func_array([$instance, $method], $args);
+ }
+
+ /**
+ * CRM_OAuth_Upgrader_Base constructor.
+ *
+ * @param $extensionName
+ * @param $extensionDir
+ */
+ 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 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;
+ }
+
+ /**
+ * Run the sql commands in the specified file.
+ *
+ * @param string $tplFile
+ * The SQL file path (relative to this extension's dir).
+ * Ex: "sql/mydata.mysql.tpl".
+ *
+ * @return bool
+ * @throws \CRM_Core_Exception
+ */
+ 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 syntactic sugar for queueing several tasks that
+ * run different queries
+ *
+ * @return bool
+ */
+ public function executeSql($query, $params = []) {
+ // FIXME verify that we raise an exception on error
+ CRM_Core_DAO::executeQuery($query, $params);
+ return TRUE;
+ }
+
+ /**
+ * Syntactic 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.
+ *
+ * @param CRM_Queue_Queue $queue
+ */
+ public function enqueuePendingRevisions(CRM_Queue_Queue $queue) {
+ $this->queue = $queue;
+
+ $currentRevision = $this->getCurrentRevision();
+ foreach ($this->getRevisions() as $revision) {
+ if ($revision > $currentRevision) {
+ $title = E::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 = \Civi::settings()->get($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://docs.civicrm.org/dev/en/latest/hooks/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://docs.civicrm.org/dev/en/latest/hooks/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://docs.civicrm.org/dev/en/latest/hooks/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://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
+ */
+ public function onEnable() {
+ // stub for possible future use
+ if (is_callable([$this, 'enable'])) {
+ $this->enable();
+ }
+ }
+
+ /**
+ * @see https://docs.civicrm.org/dev/en/latest/hooks/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:
+ }
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\Api4\Action\OAuthClient;
+
+use Civi\OAuth\OAuthTokenFacade;
+use Civi\OAuth\OAuthException;
+
+/**
+ * Class AbstractGrantAction
+ * @package Civi\Api4\Action\OAuthClient
+ *
+ * @method $this setStorage(string $storage)
+ * @method string getStorage()
+ * @method $this setTag(string $tag)
+ * @method string getTag()
+ */
+abstract class AbstractGrantAction extends \Civi\Api4\Generic\AbstractBatchAction {
+
+ /**
+ * List of permissions to request from the OAuth service.
+ *
+ * If none specified, uses a default based on the client and provider.
+ *
+ * @var array|null
+ */
+ protected $scopes = NULL;
+
+ /**
+ * Where to store tokens once they are received.
+ *
+ * @var string
+ */
+ protected $storage = 'OAuthSysToken';
+
+ /**
+ * Optionally tag the new token with a symbolic/freeform label. This tag can be
+ * used by automated mechanism to lookup/select a token.
+ *
+ * @var string|null
+ */
+ protected $tag = NULL;
+
+ /**
+ * The active client definition.
+ *
+ * @var array|null
+ * @see \Civi\Api4\OAuthClient::get()
+ */
+ private $clientDef = NULL;
+
+ public function __construct($entityName, $actionName) {
+ parent::__construct($entityName, $actionName, ['*']);
+ }
+
+ /**
+ * @throws \API_Exception
+ */
+ protected function validate() {
+ if (!preg_match(OAuthTokenFacade::STORAGE_TYPES, $this->storage)) {
+ throw new \API_Exception("Invalid token storage ($this->storage)");
+ }
+ }
+
+ /**
+ * Look up the definition for the desired client.
+ *
+ * @return array
+ * The OAuthClient details
+ * @see \Civi\Api4\OAuthClient::get()
+ * @throws OAuthException
+ */
+ protected function getClientDef():array {
+ if ($this->clientDef !== NULL) {
+ return $this->clientDef;
+ }
+
+ $records = $this->getBatchRecords();
+ if (count($records) !== 1) {
+ throw new OAuthException(sprintf("OAuth: Failed to locate client. Expected 1 client, but found %d clients.", count($records)));
+ }
+
+ $this->clientDef = array_shift($records);
+ return $this->clientDef;
+ }
+
+ /**
+ * @return \League\OAuth2\Client\Provider\AbstractProvider
+ */
+ protected function createLeagueProvider() {
+ $localOptions = [];
+ if ($this->scopes !== NULL) {
+ $localOptions['scopes'] = $this->scopes;
+ }
+ return \Civi::service('oauth2.league')->createProvider($this->getClientDef(), $localOptions);
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getScopes() {
+ return $this->scopes;
+ }
+
+ /**
+ * @param array|string|null $scopes
+ */
+ public function setScopes($scopes) {
+ $this->scopes = is_string($scopes) ? [$scopes] : $scopes;
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Civi\Api4\Action\OAuthClient;
+
+use Civi\Api4\Generic\Result;
+use Civi\OAuth\OAuthException;
+
+/**
+ * Class AuthorizationCode
+ * @package Civi\Api4\Action\OAuthClient
+ *
+ * In this workflow, we seek permission from the browser-user to access
+ * resources on their behalf. The result will be stored as a token.
+ *
+ * This API call merely *initiates* the workflow. It returns a fully-formed `url` for the
+ * authorization service. You should redirect the user to this URL.
+ *
+ * ```
+ * $result = civicrm_api4('OAuthClient', 'authorizationCode', [
+ * 'where' => [['id', '=', 123],
+ * ]);
+ * $startUrl = $result->first()['url'];
+ * CRM_Utils_System::redirect($startUrl);
+ * ```
+ *
+ * @method $this setLandingUrl(string $landingUrl)
+ * @method string getLandingUrl()
+ * @method $this setPrompt(string $prompt)
+ * @method string getPrompt()
+ *
+ * @link https://tools.ietf.org/html/rfc6749#section-4.1
+ */
+class AuthorizationCode extends AbstractGrantAction {
+
+ /**
+ * If a user successfully completes the authentication, where should they go?
+ *
+ * This value will be stored in a way that is bound to the user session and
+ * OAuth-request.
+ *
+ * @var string|null
+ */
+ protected $landingUrl = NULL;
+
+ /**
+ * @var string
+ * Ex: 'none', 'consent', 'select_account'
+ *
+ * @see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
+ * @see https://developers.google.com/identity/protocols/oauth2/web-server
+ */
+ protected $prompt = NULL;
+
+ /**
+ * Tee-up the authorization request.
+ *
+ * @param \Civi\Api4\Generic\Result $result
+ */
+ public function _run(Result $result) {
+ $this->validate();
+
+ /** @var \League\OAuth2\Client\Provider\GenericProvider $provider */
+ $provider = $this->createLeagueProvider();
+
+ // NOTE: If we don't set scopes, then getAuthorizationUrl() would implicitly use getDefaultScopes().
+ // We aim to store the effective list, but the protocol doesn't guarantee a notification of
+ // effective list.
+ $scopes = $this->getScopes() ?: $this->callProtected($provider, 'getDefaultScopes');
+
+ $stateId = \CRM_OAuth_Page_Return::storeState([
+ 'time' => \CRM_Utils_Time::getTimeRaw(),
+ 'clientId' => $this->getClientDef()['id'],
+ 'landingUrl' => $this->getLandingUrl(),
+ 'storage' => $this->getStorage(),
+ 'scopes' => $scopes,
+ 'tag' => $this->getTag(),
+ ]);
+ $authOptions = [
+ 'state' => $stateId,
+ 'scope' => $scopes,
+ ];
+ if ($this->prompt !== NULL) {
+ $authOptions['prompt'] = $this->prompt;
+ }
+ $result[] = [
+ 'url' => $provider->getAuthorizationUrl($authOptions),
+ ];
+ }
+
+ protected function validate() {
+ parent::validate();
+ if ($this->landingUrl) {
+ $landingUrlParsed = parse_url($this->landingUrl);
+ $landingUrlIp = gethostbyname($landingUrlParsed['host']);
+ $allowedBases = [
+ \Civi::paths()->getVariable('cms.root', 'url'),
+ \Civi::paths()->getVariable('civicrm.root', 'url'),
+ ];
+ $ok = max(array_map(function($allowed) use ($landingUrlParsed, $landingUrlIp) {
+ $allowedParsed = parse_url($allowed);
+ $allowedIp = gethostbyname($allowedParsed['host']);
+ $ok = $landingUrlIp === $allowedIp && $landingUrlParsed['scheme'] == $allowedParsed['scheme'];
+ return (int) $ok;
+ }, $allowedBases));
+ if (!$ok) {
+ throw new OAuthException("Cannot initiate OAuth. Unsupported landing URL.");
+ }
+ }
+ }
+
+ /**
+ * Call a protected method.
+ *
+ * @param mixed $obj
+ * @param string $method
+ * @param array $args
+ * @return mixed
+ */
+ protected function callProtected($obj, $method, $args = []) {
+ $r = new \ReflectionMethod(get_class($obj), $method);
+ $r->setAccessible(TRUE);
+ return $r->invokeArgs($obj, $args);
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Civi\Api4\Action\OAuthClient;
+
+use Civi\Api4\Generic\Result;
+
+/**
+ * Class AuthorizationCode
+ * @package Civi\Api4\Action\OAuthClient
+ *
+ * In this workflow, we seek permission to access resources by relaying
+ * a username and password.
+ *
+ * ```
+ * $result = civicrm_api4('OAuthClient', 'clientCredential', [
+ * 'where' => [['id', '=', 123],
+ * 'storage' => 'OAuthSysToken',
+ * ]);
+ * ```
+ *
+ * If successful, the result will be a (redacted) token.
+ *
+ * @link https://tools.ietf.org/html/rfc6749#section-4.4
+ */
+class ClientCredential extends AbstractGrantAction {
+
+ public function _run(Result $result) {
+ $this->validate();
+
+ $tokenRecord = \Civi::service('oauth2.token')->init([
+ 'client' => $this->getClientDef(),
+ 'scope' => $this->getScopes(),
+ 'storage' => $this->getStorage(),
+ 'tag' => $this->getTag(),
+ 'grant_type' => 'client_credentials',
+ ]);
+
+ $result[] = \CRM_OAuth_BAO_OAuthSysToken::redact($tokenRecord);
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\Api4\Action\OAuthClient;
+
+class Create extends \Civi\Api4\Generic\DAOCreateAction {
+
+ /**
+ * @inheritdoc
+ */
+ protected function validateValues() {
+ // Hrm, parent doesn't validate <callback> PC's by default.
+ if (isset($this->values['provider'])) {
+ $ps = \CRM_OAuth_BAO_OAuthClient::getProviders();
+ if (!isset($ps[$this->values['provider']])) {
+ throw new \API_Exception("Invalid provider name: " . $this->values['provider']);
+ }
+ }
+ parent::validateValues();
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\Api4\Action\OAuthClient;
+
+class Update extends \Civi\Api4\Generic\DAOUpdateAction {
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatWriteValues(&$record) {
+ $result = parent::formatWriteValues($record);
+
+ // Hrm, parent doesn't validate <callback> PC's by default.
+ if (isset($this->values['provider'])) {
+ $ps = \CRM_OAuth_BAO_OAuthClient::getProviders();
+ if (!isset($ps[$this->values['provider']])) {
+ throw new \API_Exception("Invalid provider name: " . $this->values['provider']);
+ }
+ }
+
+ return $result;
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Civi\Api4\Action\OAuthClient;
+
+use Civi\Api4\Generic\Result;
+
+/**
+ * Class AuthorizationCode
+ * @package Civi\Api4\Action\OAuthClient
+ *
+ * In this workflow, we seek permission to access resources by relaying
+ * a username and password.
+ *
+ * ```
+ * $result = civicrm_api4('OAuthClient', 'userPassword', [
+ * 'where' => [['id', '=', 123],
+ * 'username' => 'johndoe',
+ * 'password' => 'abcd1234',
+ * 'storage' => 'OAuthSysToken',
+ * ]);
+ * ```
+ *
+ * If successful, the result will be a (redacted) token.
+ *
+ * @method $this setUsername(string $username)
+ * @method string getUsername()
+ * @method $this setPassword(string $password)
+ * @method string getPassword()
+ *
+ * @link https://tools.ietf.org/html/rfc6749#section-4.3
+ */
+class UserPassword extends AbstractGrantAction {
+
+ /**
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * @var string
+ */
+ protected $password;
+
+ public function _run(Result $result) {
+ $this->validate();
+
+ $tokenRecord = \Civi::service('oauth2.token')->init([
+ 'client' => $this->getClientDef(),
+ 'scope' => $this->getScopes(),
+ 'storage' => $this->getStorage(),
+ 'tag' => $this->getTag(),
+ 'grant_type' => 'password',
+ 'cred' => [
+ 'username' => $this->getUsername(),
+ 'password' => $this->getPassword(),
+ ],
+ ]);
+
+ $result[] = \CRM_OAuth_BAO_OAuthSysToken::redact($tokenRecord);
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\Api4\Action\OAuthSysToken;
+
+use Civi\Api4\Generic\BasicBatchAction;
+
+/**
+ * Class Refresh
+ * @package Civi\Api4\Action\OAuthSysToken
+ *
+ * When preparing to connect to a remote OAuth system, use the "refresh" action
+ * to simultaneously refresh and return the auth token.
+ *
+ * Note that it is possible to refresh a token without having permission to view
+ * or edit the specific secrets involved. The result will adjust according to permissions:
+ *
+ * - If permission-checks are disabled, or if you have permission to manage secrets,
+ * then this will return the full token record.
+ * - If permission-checks are active and you only have access to "refresh" (but
+ * not to secrets), it will return a minimalist record to indicate completion.
+ *
+ * @method $this setThreshold(int $limit)
+ * @method int getThreshold()
+ */
+class Refresh extends BasicBatchAction {
+
+ /**
+ * Refresh records if they are within the given threshold for expiration.
+ *
+ * Ex: If your token is approaching expiration in 5 seconds, and if your
+ * threshold is 60 seconds, then the token will refresh. But if your token
+ * still has 5 minutes, then there's no need to refresh.
+ *
+ * A negative threshold will always refresh.
+ *
+ * @var int
+ */
+ protected $threshold = 60;
+
+ private $syncFields = ['access_token', 'refresh_token', 'expires', 'token_type'];
+ private $writeFields = ['access_token', 'refresh_token', 'expires', 'token_type', 'raw'];
+ private $providers = [];
+
+ public function __construct($entityName, $actionName) {
+ parent::__construct($entityName, $actionName, '*');
+ }
+
+ protected function doTask($row) {
+ if ($this->threshold >= 0 && \CRM_Utils_Time::getTimeRaw() < $row['expires'] - $this->threshold) {
+ return $this->filterReturn($row);
+ }
+
+ $provider = $this->getProvider($row['client_id']);
+ $newToken = $provider->getAccessToken('refresh_token', [
+ 'refresh_token' => $row['refresh_token'],
+ ]);
+
+ $raw = $newToken->jsonSerialize();
+ $row['raw'] = $raw;
+ foreach ($this->syncFields as $field) {
+ if (isset($raw[$field])) {
+ $row[$field] = $raw[$field];
+ }
+ }
+
+ civicrm_api4($this->getEntityName(), 'update', [
+ // You may have permission to refresh even if you can't inspect/update secrets directly.
+ 'checkPermissions' => FALSE,
+ 'where' => [['id', '=', $row['id']]],
+ 'values' => \CRM_Utils_Array::subset($row, $this->writeFields),
+ ])->single();
+
+ return $this->filterReturn($row);
+ }
+
+ protected function getProvider($clientId) {
+ if (!isset($this->providers[$clientId])) {
+ $client = \Civi\Api4\OAuthClient::get(0)->addWhere('id', '=', $clientId)->execute()->single();
+ $this->providers[$clientId] = \Civi::service('oauth2.league')->createProvider($client);
+ }
+ return $this->providers[$clientId];
+ }
+
+ protected function filterReturn($tokenRecord) {
+ return $this->checkPermissions ? \CRM_OAuth_BAO_OAuthSysToken::redact($tokenRecord) : $tokenRecord;
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\Api4;
+
+use Civi\Api4\Action\OAuthClient\Create;
+use Civi\Api4\Action\OAuthClient\Update;
+
+/**
+ * OAuthClient entity.
+ *
+ * Provided by the OAuth Client extension.
+ *
+ * @package Civi\Api4
+ */
+class OAuthClient extends Generic\DAOEntity {
+
+ public static function create($checkPermissions = TRUE) {
+ $action = new Create(static::class, __FUNCTION__);
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
+ public static function update($checkPermissions = TRUE) {
+ $action = new Update(static::class, __FUNCTION__);
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * Initiate the "Authorization Code" workflow.
+ *
+ * @param bool $checkPermissions
+ * @return \Civi\Api4\Action\OAuthClient\AuthorizationCode
+ */
+ public static function authorizationCode($checkPermissions = TRUE) {
+ $action = new \Civi\Api4\Action\OAuthClient\AuthorizationCode(static::class, __FUNCTION__);
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * Request access with client credentials
+ *
+ * @param bool $checkPermissions
+ * @return \Civi\Api4\Action\OAuthClient\ClientCredential
+ */
+ public static function clientCredential($checkPermissions = TRUE) {
+ $action = new \Civi\Api4\Action\OAuthClient\ClientCredential(static::class, __FUNCTION__);
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * Request access with a username and password.
+ *
+ * @param bool $checkPermissions
+ * @return \Civi\Api4\Action\OAuthClient\UserPassword
+ */
+ public static function userPassword($checkPermissions = TRUE) {
+ $action = new \Civi\Api4\Action\OAuthClient\UserPassword(static::class, __FUNCTION__);
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
+ public static function permissions() {
+ return [
+ 'meta' => ['access CiviCRM'],
+ 'default' => ['manage OAuth client'],
+ ];
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Civi\Api4;
+
+use Civi\Core\Event\GenericHookEvent;
+use Civi\OAuth\CiviGenericProvider;
+
+class OAuthProvider extends Generic\AbstractEntity {
+
+ const TTL = 600;
+
+ /**
+ * @param bool $checkPermissions
+ * @return Generic\BasicGetAction
+ */
+ public static function get($checkPermissions = TRUE) {
+ $action = new Generic\BasicGetAction('OAuthProvider', __FUNCTION__, function () {
+ $cache = \Civi::cache('long');
+ if (!$cache->has('OAuthProvider_list')) {
+ $providers = [];
+ $event = GenericHookEvent::create([
+ 'providers' => &$providers,
+ ]);
+ \Civi::dispatcher()->dispatch('hook_civicrm_oauthProviders', $event);
+
+ foreach ($providers as $name => &$provider) {
+ if ($provider['name'] !== $name) {
+ throw new \API_Exception(sprintf("Mismatched OAuth provider names: \"%s\" vs \"%s\"",
+ $provider['name'], $name));
+ }
+ if (!isset($provider['class'])) {
+ $provider['class'] = CiviGenericProvider::class;
+ }
+ }
+
+ $cache->set('OAuthProvider_list', $providers, self::TTL);
+ }
+ return $cache->get('OAuthProvider_list');
+ });
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * @param bool $checkPermissions
+ * @return Generic\BasicGetFieldsAction
+ */
+ public static function getFields($checkPermissions = TRUE) {
+ $action = new Generic\BasicGetFieldsAction('OAuthProvider', __FUNCTION__, function () {
+ return [
+ [
+ 'name' => 'name',
+ ],
+ [
+ 'name' => 'title',
+ ],
+ [
+ 'name' => 'class',
+ ],
+ [
+ 'name' => 'options',
+ ],
+ ];
+ });
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * @return array
+ */
+ public static function permissions() {
+ return [
+ "meta" => ["access CiviCRM"],
+ "get" => ["access CiviCRM"],
+ "default" => ["administer CiviCRM"],
+ ];
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\Api4;
+
+/**
+ * OAuthSysToken entity.
+ *
+ * Provided by the OAuth Client extension.
+ *
+ * @package Civi\Api4
+ */
+class OAuthSysToken extends Generic\DAOEntity {
+
+ /**
+ * Load and conditionally refresh a stored token.
+ *
+ * @param bool $checkPermissions
+ * @return \Civi\Api4\Action\OAuthSysToken\Refresh
+ */
+ public static function refresh($checkPermissions = TRUE) {
+ $action = new \Civi\Api4\Action\OAuthSysToken\Refresh(static::class, __FUNCTION__);
+ return $action->setCheckPermissions($checkPermissions);
+ }
+
+ public static function permissions() {
+ return [
+ 'meta' => ['access CiviCRM'],
+ 'default' => ['manage OAuth client'],
+ 'delete' => ['manage OAuth client'],
+ 'get' => ['manage OAuth client'],
+ 'refresh' => ['manage OAuth client'],
+ 'create' => ['manage OAuth client secrets'],
+ 'update' => ['manage OAuth client secrets'],
+ // In theory, there might be cases to 'create' or 'update' an OAuthSysToken
+ // without access to its secrets, but you should think through the
+ // lifecycle/errors/permissions. For now, easier to limit 'create'/update'.
+ ];
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\OAuth;
+
+use League\OAuth2\Client\Token\AccessToken;
+
+/**
+ * Class CiviGenericProvider
+ * @package Civi\OAuth
+ *
+ * This is a variant of "GenericProvider" which tries to support some newer
+ * conventions out-of-the-box.
+ *
+ * - By default, do not send "approval_prompt" for auth-code requests. Providers
+ * may prefer "prompt" nowadays.
+ * - Allow one to fetch claims about the resource-owner from the `id_token`
+ * supported by OpenID Connect. This reduces the need for extra round-trips
+ * and proprietary scopes+URLs. To use this, set the the option:
+ *
+ * "urlResourceOwnerDetails": "{{use_id_token}}",
+ */
+class CiviGenericProvider extends \League\OAuth2\Client\Provider\GenericProvider {
+
+ protected function getAuthorizationParameters(array $options) {
+ $newOptions = parent::getAuthorizationParameters($options);
+ if (!isset($options['approval_prompt'])) {
+ // GenericProvider insists on filling in "approval_prompt", but this seems
+ // to be disfavored nowadays b/c OpenID Connect defines "prompt".
+ unset($newOptions['approval_prompt']);
+ }
+ return $newOptions;
+ }
+
+ /**
+ * Requests resource owner details.
+ *
+ * @param \League\OAuth2\Client\Token\AccessToken $token
+ * @return mixed
+ */
+ protected function fetchResourceOwnerDetails(AccessToken $token) {
+ $url = $this->getResourceOwnerDetailsUrl($token);
+
+ // If there is no resource-owner URL, and if there is an id_token, use it.
+ if ($url === '{{use_id_token}}') {
+ $tokenData = $token->jsonSerialize();
+ if (isset($tokenData['id_token'])) {
+ $idToken = $this->decodeUnauthenticatedJwt($tokenData['id_token']);
+
+ // As long as id_token comes from a secure source, we can skip signature check.
+ // Which is fortunate... because we don't what key to check against...
+ if (!preg_match(';^https:;', $this->getBaseAccessTokenUrl([]))) {
+ throw new \RuntimeException("Cannot decode ID token from insecure source.");
+ }
+
+ return $idToken['payload'];
+ }
+ }
+
+ return parent::fetchResourceOwnerDetails($token);
+ }
+
+ private function decodeUnauthenticatedJwt($t) {
+ list ($header, $payload) = explode('.', $t);
+
+ return [
+ 'header' => json_decode(base64_decode($header), 1),
+ 'payload' => json_decode(base64_decode($payload), 1),
+ ];
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\OAuth;
+
+class OAuthException extends \CRM_Core_Exception {
+
+}
--- /dev/null
+<?php
+
+namespace Civi\OAuth;
+
+class OAuthLeagueFacade {
+
+ /**
+ * @param array $clientDef
+ * The OAuthClient record. This may be a full record, or it may be
+ * brief stub with 'id' or 'provider'. (In which case, it will look for
+ * exactly one matching client.)
+ * @return \League\OAuth2\Client\Provider\AbstractProvider
+ */
+ public function createProvider($clientDef) {
+ list ($class, $options) = $this->createProviderOptions($clientDef);
+ return new $class($options);
+ }
+
+ /**
+ * @param array $clientDef
+ * The OAuthClient record. This may be a full record, or it may be
+ * brief stub with 'id' or 'provider'. (In which case, it will look for
+ * exactly one matching client.)
+ * @return array
+ */
+ public function createProviderOptions($clientDef) {
+ $clientDef = $this->resolveSingleRef('OAuthClient', $clientDef, ['id', 'provider'], ['secret', 'guid']);
+ $providerDef = \Civi\Api4\OAuthProvider::get(0)
+ ->addWhere('name', '=', $clientDef['provider'])
+ ->execute()
+ ->single();
+
+ $class = $providerDef['class'];
+
+ $localOptions = [];
+ $localOptions['clientId'] = $clientDef['guid'];
+ $localOptions['clientSecret'] = $clientDef['secret'];
+ // NOTE: If we ever have frontend users, this may need to change.
+ $localOptions['redirectUri'] = \CRM_OAuth_BAO_OAuthClient::getRedirectUri();
+ $options = array_merge(
+ $providerDef['options'] ?? [],
+ $clientDef['options'] ?? [],
+ $localOptions
+ );
+
+ return [$class, $options];
+ }
+
+ /**
+ * Create an instance of the PHP League's OAuth2 client for interacting with
+ * a given token.
+ *
+ * @param array $tokenRecord
+ * @return array
+ * An array with properties:
+ * - provider: League\OAuth2\Client\Provider\AbstractProvider
+ * - token: League\OAuth2\Client\Token\AccessTokenInterface
+ * @throws \Civi\OAuth\OAuthException
+ */
+ public function create($tokenRecord) {
+ $tokenRecord = $this->resolveSingleRef('OAuthSysToken', $tokenRecord, ['id'], ['client_id', 'raw']);
+ $provider = $this->createProvider(['id' => $tokenRecord['client_id']]);
+ $token = new \League\OAuth2\Client\Token\AccessToken($tokenRecord['raw']);
+ return [
+ 'provider' => $provider,
+ 'token' => $token,
+ ];
+ }
+
+ /**
+ * Given a $record, determine if it is complete enough for usage. If not,
+ * attempt to load the full record. Throw an exception if we don't find it.
+ *
+ * @param string $entity
+ * The of record that we want to load. (APIv4 entity)
+ * @param array $record
+ * A complete or partial API record
+ * @param array $lookupFields
+ * A list of key fields that can be used to lookup records.
+ * @param array $requireFields
+ * A list of data fields that we need to have.
+ * @return array
+ * @throws \Civi\OAuth\OAuthException
+ */
+ protected function resolveSingleRef($entity, $record, $lookupFields, $requireFields) {
+ $requireFields = array_unique(array_merge($lookupFields, $requireFields));
+ $hasReqs = TRUE;
+ foreach ($requireFields as $field) {
+ $hasReqs = $hasReqs && isset($record[$field]);
+ }
+
+ if ($hasReqs) {
+ return $record;
+ }
+
+ $where = [];
+ foreach ($lookupFields as $field) {
+ if (isset($record[$field])) {
+ $where[] = [$field, '=', $record[$field]];
+
+ }
+ }
+
+ if (empty($where)) {
+ throw new OAuthException("Incomplete reference to $entity. Must have at least one of these fields: " . implode(',', $lookupFields));
+ }
+
+ return civicrm_api4($entity, 'get', [
+ 'where' => $where,
+ 'checkPermissions' => FALSE,
+ ])->single();
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Civi\OAuth;
+
+use League\OAuth2\Client\Provider\ResourceOwnerInterface;
+
+class OAuthTokenFacade {
+
+ const STORAGE_TYPES = ';^OAuthSysToken$;';
+
+ /**
+ * Request and store a token.
+ *
+ * @param array $options
+ * With some mix of the following:
+ * - client: array, the OAuthClient record
+ * - scope: array|string|null, list of scopes to request. if omitted, inherit default from client/provider
+ * - storage: string, default: "OAuthSysToken"
+ * - tag: string|null, a symbolic/freeform identifier for looking-up tokens
+ * - grant_type: string, ex "authorization_code", "client_credentials", "password"
+ * - cred: array, extra credentialing options to pass to the "token" URL (via getAccessToken($tokenOptions)),
+ * eg "username", "password", "code"
+ * @return array
+ * @throws \API_Exception
+ * @see \League\OAuth2\Client\Provider\AbstractProvider::getAccessToken()
+ */
+ public function init($options) {
+ $options['storage'] = $options['storage'] ?? 'OAuthSysToken';
+ if (!preg_match(self::STORAGE_TYPES, $options['storage'])) {
+ throw new \API_Exception("Invalid token storage ({$options['storage']})");
+ }
+
+ /** @var \League\OAuth2\Client\Provider\GenericProvider $provider */
+ $provider = \Civi::service('oauth2.league')->createProvider($options['client']);
+ $scopeSeparator = $this->callProtected($provider, 'getScopeSeparator');
+
+ $sendOptions = $options['cred'] ?? [];
+ if (isset($options['scope']) && $options['scope'] !== NULL) {
+ switch ($options['grant_type']) {
+ case 'authorization_code':
+ // already sent.
+ break;
+
+ default:
+ $sendOptions['scope'] = $this->implodeScopes($scopeSeparator, $options['scope']);
+ }
+ }
+
+ /** @var \League\OAuth2\Client\Token\AccessToken $accessToken */
+ $accessToken = $provider->getAccessToken($options['grant_type'], $sendOptions);
+ $values = $accessToken->getValues();
+
+ $tokenRecord = [
+ 'client_id' => $options['client']['id'],
+ 'grant_type' => $options['grant_type'],
+ 'tag' => $options['tag'] ?? NULL,
+ 'scopes' => $this->splitScopes($scopeSeparator, $values['scope'] ?? $options['scope'] ?? NULL),
+ 'token_type' => $values['token_type'] ?? NULL,
+ 'access_token' => $accessToken->getToken(),
+ 'refresh_token' => $accessToken->getRefreshToken(),
+ 'expires' => $accessToken->getExpires(),
+ 'raw' => $accessToken->jsonSerialize(),
+ ];
+ try {
+ $owner = $provider->getResourceOwner($accessToken);
+ $tokenRecord['resource_owner_name'] = $this->findName($owner);
+ $tokenRecord['resource_owner'] = $owner->toArray();
+ }
+ catch (\Throwable $e) {
+ \Civi::log()->warning("Failed to resolve resource_owner");
+ }
+
+ return civicrm_api4($options['storage'], 'create', [
+ 'checkPermissions' => FALSE,
+ 'values' => $tokenRecord,
+ ])->single();
+ }
+
+ /**
+ * Call a protected method.
+ *
+ * @param mixed $obj
+ * @param string $method
+ * @param array $args
+ * @return mixed
+ */
+ protected function callProtected($obj, $method, $args = []) {
+ $r = new \ReflectionMethod(get_class($obj), $method);
+ $r->setAccessible(TRUE);
+ return $r->invokeArgs($obj, $args);
+ }
+
+ /**
+ * @param string $delim
+ * @param string|array|null $scopes
+ * @return array|null
+ */
+ protected function splitScopes($delim, $scopes) {
+ if ($scopes === NULL || is_array($scopes)) {
+ return $scopes;
+ }
+ if ($scopes === '') {
+ return [];
+ }
+ if (is_string($scopes)) {
+ return explode($delim, $scopes);
+ }
+ \Civi::log()->warning("Failed to explode scopes", [
+ 'scopes' => $scopes,
+ ]);
+ return NULL;
+ }
+
+ protected function implodeScopes($delim, $scopes) {
+ if ($scopes === NULL || is_string($scopes)) {
+ return $scopes;
+ }
+ if (is_array($scopes)) {
+ return implode($delim, $scopes);
+ }
+ \Civi::log()->warning("Failed to implode scopes", [
+ 'scopes' => $scopes,
+ ]);
+ return NULL;
+ }
+
+ protected function findName(ResourceOwnerInterface $owner) {
+ $values = $owner->toArray();
+ $fields = ['upn', 'userPrincipalName', 'mail', 'email', 'id'];
+ foreach ($fields as $field) {
+ if (isset($values[$field])) {
+ return $values[$field];
+ }
+ }
+ return $owner->getId();
+ }
+
+}
--- /dev/null
+Package: oauth-client
+Copyright (C) 2020, Tim Otten <info@civicrm.org>
+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. <http://fsf.org/>
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 <http://www.gnu.org/licenses/>.
+
+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
+<http://www.gnu.org/licenses/>.
--- /dev/null
+# oauth-client
+
+![Screenshot](/images/screenshot.png)
+
+(*FIXME: In one or two paragraphs, describe what the extension does and why one would download it. *)
+
+The extension is licensed under [AGPL-3.0](LICENSE.txt).
+
+## Requirements
+
+* PHP v7.0+
+* CiviCRM (*FIXME: Version number*)
+
+## Installation (Web UI)
+
+This extension has not yet been published for installation via the web UI.
+
+## Installation (CLI, Zip)
+
+Sysadmins and developers may download the `.zip` file for this extension and
+install it with the command-line tool [cv](https://github.com/civicrm/cv).
+
+```bash
+cd <extension-dir>
+cv dl oauth-client@https://github.com/FIXME/oauth-client/archive/master.zip
+```
+
+## Installation (CLI, Git)
+
+Sysadmins and developers may clone the [Git](https://en.wikipedia.org/wiki/Git) repo for this extension and
+install it with the command-line tool [cv](https://github.com/civicrm/cv).
+
+```bash
+git clone https://github.com/FIXME/oauth-client.git
+cv en oauth_client
+```
+
+## Usage
+
+(* FIXME: Where would a new user navigate to get started? What changes would they see? *)
+
+## Known Issues
+
+(* FIXME *)
--- /dev/null
+<div oauth-util-import="CRM.oauthUtil.providers" to="theProviders"></div>
+
+<div id="bootstrap-theme">
+ <div ng-if="!routeParams.provider">
+ <div oauth-provider-list></div>
+ </div>
+
+ <div ng-if="routeParams.provider">
+ <div oauth-provider-detail="{provider: theProviders[routeParams.provider]}"></div>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+{
+ "title": "OAuth2 Client Administration",
+ "server_route": "civicrm/admin/oauth",
+ "permission": "manage OAuth client"
+}
--- /dev/null
+<div oauth-util-import="CRM.oauthUtil.redirectUrl" to="redirectUrl"></div>
+<div class="help">
+ <p>{{ts('Please register with your web-service provider first. Be sure to:')}}</p>
+ <ul>
+ <li>
+ <p>
+ {{ts('Configure the "Redirect URL":')}}
+ </p>
+ <pre>{{redirectUrl}}</pre>
+ <div ng-if="redirectUrl.startsWith('http:/') && !redirectUrl.match('//(localhost|127\.0\.0\.1)')">
+ <p>
+ {{ts('WARNING: Most web-service providers require "https://" URLs. Alternatively, "http://" may be accepted for strictly local URLs ("localhost" or "127.0.0.1").')}}
+ </p>
+ <p>
+ {{ts('If you are doing development or testing on a local HTTP virtual-host, then consider a work-around like "bin/local-redir.sh".')}}
+ </p>
+ </div>
+ </li>
+
+ <li ng-if="options.provider.options.scopes.length > 0">
+ <p>
+ {{ts('Configure the scopes:')}}
+ </p>
+ <pre>{{options.provider.options.scopes.join("\n")}}</pre>
+ </li>
+
+ <li>
+ {{ts('Determine the client credentials ("Client ID" and "Client Secret").')}}
+ </li>
+ </ul>
+ <p>{{ts('Finally, copy the client credentials below:')}}</p>
+</div>
\ No newline at end of file
--- /dev/null
+<div oauth-util-import="CRM.oauthUtil.providers" to="theProviders"></div>
+
+<div ng-form="create" class="form-horizontal">
+ <div class="form-group">
+ <div>
+ <label for="guid">{{ts('Client ID')}}:</label>
+ <input class="form-control" ng-model="options.client.guid" type="text" id="guid" />
+ </div>
+ <div>
+ <label for="secret">{{ts('Client Secret')}}:</label>
+ <input class="form-control" ng-model="options.client.secret" type="text" id="secret" />
+ </div>
+ </div>
+</div>
--- /dev/null
+<div oauth-util-import="CRM.oauthUtil.providers" to="theProviders"></div>
+
+<div ng-form="update" class="form-horizontal">
+ <div class="form-group">
+ <div>
+ <label for="provider{{options.client.id}}">{{ts('Provider')}}:</label>
+ <input class="form-control" ng-model="options.client.provider" disabled id="provider{{options.client.id}}">
+ </div>
+ <div>
+ <label for="id{{options.client.id}}">{{ts('Client ID (Private)')}}:</label>
+ <input class="form-control" ng-model="options.client.id" type="text" id="id{{options.client.id}}" disabled/>
+ </div>
+ <div>
+ <label for="guid{{options.client.id}}">{{ts('Client ID (Public)')}}:</label>
+ <input class="form-control" ng-model="options.client.guid" type="text" id="guid{{options.client.id}}"/>
+ </div>
+ <div>
+ <label for="secret{{options.client.id}}">{{ts('Client Secret')}}:</label>
+ <input class="form-control" ng-model="options.client.secret" type="text" id="secret{{options.client.id}}"/>
+ </div>
+ <div ng-if="options.client.created_date">
+ <label for="created_date{{options.client.id}}">{{ts('Created Date')}}:</label>
+ <input class="form-control" ng-model="options.client.created_date" disabled
+ id="created_date{{options.client.id}}">
+ </div>
+ <div ng-if="options.client.modified_date">
+ <label for="modified_date{{options.client.id}}">{{ts('Modified Date')}}:</label>
+ <input class="form-control" ng-model="options.client.modified_date" disabled
+ id="modified_date{{options.client.id}}">
+ </div>
+ </div>
+</div>
--- /dev/null
+<div
+ af-api4="['OAuthClient', 'get', {select: ['id','provider','guid'], orderBy: {provider:'ASC'}}]"
+ af-api4-ctrl="listCtrl">
+
+ <div ng-if="apiData.result.length == 0">
+ {{ts('There are no clients!')}}
+ </div>
+
+ <table>
+ <thead>
+ <tr>
+ <th>{{ts('ID')}}</th>
+ <th>{{ts('Provider')}}</th>
+ <th>{{ts('GUID')}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="availClient in listCtrl.result">
+ <td>
+ <a ng-href="#!/?id={{availClient.id}}">{{availClient.id}}</a>
+ </td>
+ <td>{{availClient.provider}}</td>
+ <td>{{availClient.guid}}</td>
+ <td>
+ <!--
+ <a af-api4-action="['Afform', 'revert', {where: [['name','=', availClient.name]]}]"
+ af-api4-start-msg="ts('Reverting...')"
+ af-api4-success-msg="ts('Reverted')"
+ af-api4-success="listCtrl.refresh()"
+ class="btn btn-xs btn-default"
+ ng-if="availClient.has_local && availClient.has_base"
+ >{{ts('Revert')}}</a>
+ -->
+ <a af-api4-action="['OAuthClient', 'delete', {where: [['id','=', availClient.id]]}]"
+ af-api4-start-msg="ts('Deleting...')"
+ af-api4-success-msg="ts('Deleted')"
+ af-api4-success="listCtrl.refresh()"
+ class="btn btn-xs btn-default"
+ >{{ts('Delete')}}</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+</div>
--- /dev/null
+<div af-api4-ctrl="tokens" af-api4="['OAuthSysToken', 'get', {'where': [['client_id', '=', options.clientId]]}]">
+</div>
+<div ng-if="tokens.result.length == 0">
+ {{ts('No tokens found')}}
+</div>
+
+<table class="table" ng-if="tokens.result.length > 0">
+ <tr>
+ <th>{{ts('ID')}}</th>
+ <th>{{ts('Tag')}}</th>
+ <th>{{ts('On Behalf Of')}}</th>
+ <th>{{ts('Scopes')}}</th>
+ <th>{{ts('Created Date')}}</th>
+ <th>{{ts('Actions')}}</th>
+ </tr>
+ <tr ng-repeat="token in tokens.result">
+ <td>{{token.id}}</td>
+ <td>{{token.tag}}</td>
+ <td>{{token.resource_owner_name}}</td>
+ <td>{{token.scopes.join(" ")}}</td>
+ <td>{{token.created_date}}</td>
+ <td>
+ <div class="btn-group">
+ <a class="btn btn-default"
+ ng-if="token.access_token"
+ ng-href="{{crmUrl('civicrm/admin/oauth-jwt-debug#!/', {id: token.id})}}"
+ target="_blank"
+ >{{ts('Inspect')}}</a>
+
+ <a class="btn btn-danger"
+ af-api4-action="['OAuthSysToken', 'delete', {where: [['id', '=', token.id]]}]"
+ af-api4-start-msg="ts('Deleting...')"
+ af-api4-success-msg="ts('Deleted')"
+ af-api4-success="tokens.refresh()"
+ >{{ts('Delete')}}</a>
+ </div>
+ </td>
+ </tr>
+</table>
--- /dev/null
+<div id="bootstrap-theme">
+ <div ng-init="data = {tokenId: routeParams.id, token: null}"></div>
+
+ <p>This is a temporary debug page. It requires super user permissions or <code>manage oauth client secrets</code>.</p>
+
+ <p>Some (but not all) OAuth2 tokens are based on <a href="https://en.wikipedia.org/wiki/JSON_Web_Token">JWT</a>. If a token is based on JWT, then we can examine its content to learn more about what the token signifies. This may help with debugging token-access issues.</p>
+
+ <div af-api4-ctrl="tokens" af-api4="['OAuthSysToken', 'get', {'where': [['id', '=', routeParams.id]]}]"></div>
+
+ <div ng-if="tokens.result.length == 0">
+ No tokens found.
+ </div>
+
+ <div ng-repeat="token in tokens.result">
+
+ <h3>Token Record</h3>
+
+ <pre>{{token|json}}</pre>
+
+ <h3>Access Token: Raw</h3>
+
+ <pre>{{token.access_token}}</pre>
+
+ <h3>Access Token: JWT Payload</h3>
+
+ (This will only display meaningful data if the token is based on JWT.)
+
+ <pre>{{token.access_token|unvalidatedJwtDecode|json}}</pre>
+
+ </div>
+
+</div>
\ No newline at end of file
--- /dev/null
+{
+ "title": "OAuth2 JWT Debug",
+ "requires": ["unvalidatedJwtDecode", "afCore"],
+ "server_route": "civicrm/admin/oauth-jwt-debug",
+ "permission": "manage OAuth client secrets"
+}
\ No newline at end of file
--- /dev/null
+<h1>{{options.provider.title}}</h1>
+
+<div af-api4-ctrl="theClients" af-api4="['OAuthClient', 'get', {where: [['provider','=',options.provider.name]]}]"></div>
+
+<div ng-if="!theClients.loading">
+ <div class="panel panel-info" ng-init="selected = {tab: theClients.result.length > 0 ? 'client_' + theClients.result[0].id : 'new'}">
+ <ul class="panel-heading nav nav-tabs">
+ <li role="presentation" ng-repeat="theClient in theClients.result" ng-class="{active: selected.tab === 'client_' + theClient.id}"><a ng-click="selected.tab = 'client_' + theClient.id">{{ts('Client #%1', {1: theClient.id})}}</a></li>
+ <li role="presentation" ng-class="{active: selected.tab === 'new'}"><a ng-click="selected.tab = 'new'">{{ts('Register Client')}}</a></li>
+ <li role="presentation" ng-class="{active: selected.tab === 'details'}"><a ng-click="selected.tab = 'details'">{{ts('Details')}}</a></li>
+ </ul>
+
+ <div class="panel-body" ng-if="selected.tab === 'details'">
+ <pre>{{options.provider|json}}</pre>
+ </div>
+
+ <div class="panel-body" ng-repeat="resultClient in theClients.result" ng-if="selected.tab === 'client_'+resultClient.id">
+ <div ng-form="editClientForm">
+ <h4>{{ts('Tokens')}}</h4>
+
+ <div oauth-client-tokens="{clientId: resultClient.id}"></div>
+
+ <div class="btn-group" oauth-util-grant-ctrl="granter">
+ <a class="btn btn-primary" ng-click="granter.authCode(resultClient.id)">{{ts('Add (Auth Code)')}}</a>
+ </div>
+
+ <h4>{{ts('Properties')}}</h4>
+
+ <div oauth-client-editor="{client: resultClient}"></div>
+ <div class="btn-group">
+ <a class="btn btn-primary"
+ af-api4-action="['OAuthClient', 'update', {where: [['id', '=', resultClient.id]], values:resultClient}]">{{ts('Save')}}</a>
+ <a class="btn btn-danger"
+ af-api4-action="['OAuthClient', 'delete', {where: [['id', '=', resultClient.id]]}]"
+ af-api4-success="selected.tab = 'details'; theClients.refresh()"
+ >{{ts('Delete')}}</a>
+ </div>
+
+ </div>
+ </div>
+
+ <div class="panel-body" ng-if="selected.tab === 'new'" ng-form="newClientForm" ng-init="theNew = {provider: options.provider.name}">
+ <div oauth-client-create-help="{provider: options.provider}"></div>
+ <div crm-ui-debug="theNew"></div>
+ <div oauth-client-creator="{client: theNew}"></div>
+ <div class="btn-group">
+ <a class="btn btn-primary"
+ af-api4-action="['OAuthClient', 'create', {values:theNew}]"
+ af-api4-success="theNew = {provider: options.provider.name}; theClients.refresh(); selected.tab = 'client_' + response[0].id"
+ >{{ts('Add')}}</a>
+ </div>
+ </div>
+ </div>
+
+</div>
--- /dev/null
+<div oauth-util-import="CRM.oauthUtil.providers" to="theProviders"></div>
+
+<div class="help">
+ <p>
+ {{ts('CiviCRM may be configured as a client that interacts with remote web-services, such as Google Mail or Microsoft Exchange. Please choose the type of web-service you wish to connect to:')}}
+ </p>
+
+ <!--
+ To do so, you must first register with the service to obtain credentials (Client ID and Client Secret). Copy the assigned credentials below.
+ -->
+</div>
+
+<table>
+ <thead>
+ <tr>
+ <th>{{ts('Name')}}</th>
+ <th>{{ts('Title')}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="provider in theProviders">
+ <td>
+ <a ng-href="#!/?provider={{provider.name}}">{{provider.name}}</a>
+ </td>
+ <td>
+ <a ng-href="#!/?provider={{provider.name}}">{{provider.title}}</a>
+ </td>
+ </tr>
+ </tbody>
+</table>
--- /dev/null
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// \https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules/n
+return [
+ 'js' => [
+ 'ang/oauthUtil.js',
+ // 'ang/oauthUtil/*.js',
+ // 'ang/oauthUtil/*/*.js',
+ ],
+ // 'css' => ['ang/oauthUtil.css'],
+ // 'partials' => ['ang/oauthUtil'],
+ // 'requires' => ['crmUi', 'crmUtil'],
+ 'settings' => [],
+ 'settingsFactory' => ['CRM_OAuth_Angular', 'getSettings'],
+ 'exports' => [
+ 'oauth-util-import' => 'A',
+ 'oauth-util-grant-ctrl' => 'A',
+ ],
+];
--- /dev/null
+(function(angular, $, _) {
+ angular.module('oauthUtil', CRM.angRequires('oauthUtil'));
+ // Import data from the 'CRM.foo' settings.
+ // Ex: <div oauth-util-import="CRM.oauthUtil.providers" to="theProviders" />
+ angular.module('oauthUtil').directive('oauthUtilImport', function() {
+ return {
+ restrict: 'EA',
+ scope: {
+ to: '=',
+ oauthUtilImport: '@'
+ },
+ controller: function($scope, $parse) {
+ $scope.to = $parse($scope.oauthUtilImport)({CRM: CRM});
+ }
+ };
+ });
+ angular.module('oauthUtil').directive('oauthUtilGrantCtrl', function() {
+ return {
+ restrict: 'EA',
+ scope: {
+ oauthUtilGrantCtrl: '='
+ },
+ controllerAs: 'oauthUtilGrantCtrl',
+ controller: function($scope, $parse, crmBlocker, crmApi4, crmStatus) {
+ var block = crmBlocker();
+ var ctrl = this;
+ ctrl.authCode = function(clientId) {
+ var confirmOpt = {
+ message: ts('You are about to be redirected to an external site.'),
+ options: {no: ts('Cancel'), yes: ts('Continue')}
+ };
+ CRM.confirm(confirmOpt)
+ .on('crmConfirm:yes', function(){
+ var going = crmApi4('OAuthClient', 'authorizationCode', {
+ 'landingUrl': window.location.href,
+ 'where': [['id', '=', clientId]]
+ }).then(function(r){
+ window.location = r[0].url;
+ });
+ return block(crmStatus({start: ts('Redirecting...'), success: ts('Redirecting...')}, going));
+ });
+ };
+
+ $scope.oauthUtilGrantCtrl = this;
+ }
+ };
+ });
+})(angular, CRM.$, CRM._);
--- /dev/null
+<?php
+return [
+ 'js' => [
+ 'ang/unvalidatedJwtDecode.js',
+ ],
+ 'requires' => [
+ 'crmUi',
+ 'crmUtil',
+ ],
+];
--- /dev/null
+(function(angular, $, _) {
+ angular.module('unvalidatedJwtDecode', CRM.angRequires('unvalidatedJwtDecode'));
+ angular.module('unvalidatedJwtDecode').filter('unvalidatedJwtDecode', function() {
+ return function(token) {
+ if (!token) return null;
+ var payload = token.split('.')[1];
+ var tokenData = url_base64_decode(payload);
+ try {
+ return JSON.parse(tokenData);
+ } catch (e) {
+ return tokenData;
+ }
+ };
+ });
+
+ function url_base64_decode(str) {
+ var output = str.replace(/-/g, '+').replace(/_/g, '/');
+ switch (output.length % 4) {
+ case 0:
+ break;
+ case 2:
+ output += '==';
+ break;
+ case 3:
+ output += '=';
+ break;
+ default:
+ throw 'Illegal base64url string!';
+ }
+ return decodeURIComponent(window.escape(atob(output)));
+ }
+})(angular, CRM.$, CRM._);
--- /dev/null
+<?php
+
+if (PHP_SAPI !== 'cli-server') {
+ throw new \Exception(sprintf("Cannot redirect. The script %s must be launched PHP standalone mode (ex: %s).",
+ basename(__FILE__), 'DEST=http://example.com/ php -S localhost:3000'));
+}
+
+function buildInputUrl($s) {
+ $ssl = !empty($s['HTTPS']) && strtolower($s['HTTPS']) != 'off';
+ $url = ($ssl ? 'https' : 'http') . '://' . $s['HTTP_HOST'] . $s['REQUEST_URI'];
+ return $url;
+}
+
+function buildRedirectUrl($get) {
+ $url = getenv('DEST');
+ if (empty($url)) {
+ throw new \Exception(sprintf("Cannot redirect. The script %s requires environment variable %s.",
+ basename(__FILE__), 'DEST'));
+ }
+ $query = http_build_query($get);
+ $delim = strpos($url, '?') === FALSE ? '?' : '&';
+ return $url . ($query === '' ? '' : $delim . $query);
+}
+
+$inputUrl = buildInputUrl($_SERVER);
+$redirectUrl = buildRedirectUrl($_GET);
+error_log(sprintf("Redirect:\n from: %s\n to: %s\n", $inputUrl, $redirectUrl));
+header('Location: ' . $redirectUrl);
--- /dev/null
+#!/bin/bash
+set -e
+
+## Most OAuth2 providers allow you to register local dev sites if the "Redirect URL"
+## looks like "http://localhost:NNNN" or "http://127.0.0.1:NNNN".
+##
+## It is common in the CiviCRM community to develop on local virtual-hosts like "http://example.local".
+## These URLs cannot be directly registered with most OAuth2 providers.
+##
+## To resolve this either (1) enable HTTPS locally or (2) setup an intermediate redirect, e.g.
+##
+## http://localhost:3000/my-return ==> http://example.local/civicrm/oauth/return
+## https://public.example.com/my-return ==> http://example.local/civicrm/oauth/return
+##
+## The script "local-redir.sh" can help you setup an intermediate redirect. It will:
+##
+## 1. Launch a temporary HTTP service on "http://localhost:3000".
+## 2. Configure CiviCRM to work with "http://localhost:3000".
+##
+################################################################################
+
+## usage: local-redir.sh [ip-or-host[:port]]
+##
+## example#1: local-redir.sh
+## example#2: local-redir.sh 127.0.0.1
+## example#3: local-redir.sh localhost:8000
+
+###############################################################################
+## Bootstrap
+
+## Determine the absolute path of the directory with the file
+## usage: absdirname <file-path>
+function absdirname() {
+ pushd $(dirname $0) >> /dev/null
+ pwd
+ popd >> /dev/null
+}
+
+BINDIR=$(absdirname "$0")
+REDIRPHP="$BINDIR/local-redir.php"
+
+###############################################################################
+## Main
+
+BIND=${1:-localhost:3000}
+DEST=$(cv url -I civicrm/oauth-client/return)
+
+echo "local-redir.sh: Setup redirect proxy"
+echo
+echo "Intermediate URL: http://$BIND"
+echo "Canonical URL: $DEST"
+echo
+echo "Update CiviCRM settings:"
+cv api setting.create oauthClientRedirectUrl="http://$BIND"
+
+export DEST
+php -S "$BIND" "$REDIRPHP"
+
+echo "Shutting down"
+echo
+echo "Reverting CiviCRM settings: oauthClientRedirectUrl"
+cv ev 'Civi::settings()->revert("oauthClientRedirectUrl");'
--- /dev/null
+<?xml version="1.0"?>
+<extension key="oauth-client" type="module">
+ <file>oauth_client</file>
+ <name>OAuth Client</name>
+ <description>Connect CiviCRM to remote OAuth 2 services</description>
+ <license>AGPL-3.0</license>
+ <maintainer>
+ <author>Tim Otten</author>
+ <email>info@civicrm.org</email>
+ </maintainer>
+ <urls>
+ <url desc="Main Extension Page">http://FIXME</url>
+ <url desc="Documentation">http://FIXME</url>
+ <url desc="Support">http://FIXME</url>
+ <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
+ </urls>
+ <releaseDate>2020-10-23</releaseDate>
+ <version>1.0</version>
+ <tags>
+ <tag>mgmt:hidden</tag>
+ </tags>
+ <develStage>alpha</develStage>
+ <compatibility>
+ <ver>5.0</ver>
+ </compatibility>
+ <requires>
+ <ext version="~4.5">org.civicrm.afform</ext>
+ </requires>
+ <comments>This is a new, undeveloped module</comments>
+ <classloader>
+ <psr4 prefix="Civi\" path="Civi"/>
+ </classloader>
+ <civix>
+ <namespace>CRM/OAuth</namespace>
+ </civix>
+</extension>
--- /dev/null
+<?php
+
+// AUTO-GENERATED FILE -- Civix may overwrite any changes made to this file
+
+/**
+ * The ExtensionUtil class provides small stubs for accessing resources of this
+ * extension.
+ */
+class CRM_OAuth_ExtensionUtil {
+ const SHORT_NAME = 'oauth_client';
+ const LONG_NAME = 'oauth-client';
+ const CLASS_PREFIX = 'CRM_OAuth';
+
+ /**
+ * Translate a string using the extension's domain.
+ *
+ * If the extension doesn't have a specific translation
+ * for the string, fallback to the default translations.
+ *
+ * @param string $text
+ * Canonical message text (generally en_US).
+ * @param array $params
+ * @return string
+ * Translated text.
+ * @see ts
+ */
+ public static function ts($text, $params = []) {
+ if (!array_key_exists('domain', $params)) {
+ $params['domain'] = [self::LONG_NAME, NULL];
+ }
+ return ts($text, $params);
+ }
+
+ /**
+ * Get the URL of a resource file (in this extension).
+ *
+ * @param string|NULL $file
+ * Ex: NULL.
+ * Ex: 'css/foo.css'.
+ * @return string
+ * Ex: 'http://example.org/sites/default/ext/org.example.foo'.
+ * Ex: 'http://example.org/sites/default/ext/org.example.foo/css/foo.css'.
+ */
+ public static function url($file = NULL) {
+ if ($file === NULL) {
+ return rtrim(CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME), '/');
+ }
+ return CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME, $file);
+ }
+
+ /**
+ * Get the path of a resource file (in this extension).
+ *
+ * @param string|NULL $file
+ * Ex: NULL.
+ * Ex: 'css/foo.css'.
+ * @return string
+ * Ex: '/var/www/example.org/sites/default/ext/org.example.foo'.
+ * Ex: '/var/www/example.org/sites/default/ext/org.example.foo/css/foo.css'.
+ */
+ public static function path($file = NULL) {
+ // return CRM_Core_Resources::singleton()->getPath(self::LONG_NAME, $file);
+ return __DIR__ . ($file === NULL ? '' : (DIRECTORY_SEPARATOR . $file));
+ }
+
+ /**
+ * Get the name of a class within this extension.
+ *
+ * @param string $suffix
+ * Ex: 'Page_HelloWorld' or 'Page\\HelloWorld'.
+ * @return string
+ * Ex: 'CRM_Foo_Page_HelloWorld'.
+ */
+ public static function findClass($suffix) {
+ return self::CLASS_PREFIX . '_' . str_replace('\\', '_', $suffix);
+ }
+
+}
+
+use CRM_OAuth_ExtensionUtil as E;
+
+/**
+ * (Delegated) Implements hook_civicrm_config().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config
+ */
+function _oauth_client_civix_civicrm_config(&$config = NULL) {
+ static $configured = FALSE;
+ if ($configured) {
+ return;
+ }
+ $configured = TRUE;
+
+ $template =& CRM_Core_Smarty::singleton();
+
+ $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR;
+ $extDir = $extRoot . 'templates';
+
+ if (is_array($template->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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_xmlMenu
+ */
+function _oauth_client_civix_civicrm_xmlMenu(&$files) {
+ foreach (_oauth_client_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) {
+ $files[] = $file;
+ }
+}
+
+/**
+ * Implements hook_civicrm_install().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install
+ */
+function _oauth_client_civix_civicrm_install() {
+ _oauth_client_civix_civicrm_config();
+ if ($upgrader = _oauth_client_civix_upgrader()) {
+ $upgrader->onInstall();
+ }
+}
+
+/**
+ * Implements hook_civicrm_postInstall().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall
+ */
+function _oauth_client_civix_civicrm_postInstall() {
+ _oauth_client_civix_civicrm_config();
+ if ($upgrader = _oauth_client_civix_upgrader()) {
+ if (is_callable([$upgrader, 'onPostInstall'])) {
+ $upgrader->onPostInstall();
+ }
+ }
+}
+
+/**
+ * Implements hook_civicrm_uninstall().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall
+ */
+function _oauth_client_civix_civicrm_uninstall() {
+ _oauth_client_civix_civicrm_config();
+ if ($upgrader = _oauth_client_civix_upgrader()) {
+ $upgrader->onUninstall();
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_enable().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
+ */
+function _oauth_client_civix_civicrm_enable() {
+ _oauth_client_civix_civicrm_config();
+ if ($upgrader = _oauth_client_civix_upgrader()) {
+ if (is_callable([$upgrader, 'onEnable'])) {
+ $upgrader->onEnable();
+ }
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_disable().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable
+ * @return mixed
+ */
+function _oauth_client_civix_civicrm_disable() {
+ _oauth_client_civix_civicrm_config();
+ if ($upgrader = _oauth_client_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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade
+ */
+function _oauth_client_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
+ if ($upgrader = _oauth_client_civix_upgrader()) {
+ return $upgrader->onUpgrade($op, $queue);
+ }
+}
+
+/**
+ * @return CRM_OAuth_Upgrader
+ */
+function _oauth_client_civix_upgrader() {
+ if (!file_exists(__DIR__ . '/CRM/OAuth/Upgrader.php')) {
+ return NULL;
+ }
+ else {
+ return CRM_OAuth_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 string $dir base dir
+ * @param string $pattern , glob pattern, eg "*.txt"
+ *
+ * @return array
+ */
+function _oauth_client_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 (_oauth_client_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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed
+ */
+function _oauth_client_civix_civicrm_managed(&$entities) {
+ $mgdFiles = _oauth_client_civix_find_files(__DIR__, '*.mgd.php');
+ sort($mgdFiles);
+ foreach ($mgdFiles as $file) {
+ $es = include $file;
+ foreach ($es as $e) {
+ if (empty($e['module'])) {
+ $e['module'] = E::LONG_NAME;
+ }
+ if (empty($e['params']['version'])) {
+ $e['params']['version'] = '3';
+ }
+ $entities[] = $e;
+ }
+ }
+}
+
+/**
+ * (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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_caseTypes
+ */
+function _oauth_client_civix_civicrm_caseTypes(&$caseTypes) {
+ if (!is_dir(__DIR__ . '/xml/case')) {
+ return;
+ }
+
+ foreach (_oauth_client_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));
+ throw new CRM_Core_Exception($errorMessage);
+ }
+ $caseTypes[$name] = [
+ 'module' => E::LONG_NAME,
+ '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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules
+ */
+function _oauth_client_civix_civicrm_angularModules(&$angularModules) {
+ if (!is_dir(__DIR__ . '/ang')) {
+ return;
+ }
+
+ $files = _oauth_client_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'] = E::LONG_NAME;
+ }
+ $angularModules[$name] = $module;
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_themes().
+ *
+ * Find any and return any files matching "*.theme.php"
+ */
+function _oauth_client_civix_civicrm_themes(&$themes) {
+ $files = _oauth_client_civix_glob(__DIR__ . '/*.theme.php');
+ foreach ($files as $file) {
+ $themeMeta = include $file;
+ if (empty($themeMeta['name'])) {
+ $themeMeta['name'] = preg_replace(':\.theme\.php$:', '', basename($file));
+ }
+ if (empty($themeMeta['ext'])) {
+ $themeMeta['ext'] = E::LONG_NAME;
+ }
+ $themes[$themeMeta['name']] = $themeMeta;
+ }
+}
+
+/**
+ * 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
+ */
+function _oauth_client_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 to parent of this item, e.g. 'my_extension/submenu'
+ * 'Mailing', or 'Administer/System Settings'
+ * @param array $item - the item to insert (parent/child attributes will be
+ * filled for you)
+ *
+ * @return bool
+ */
+function _oauth_client_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 = _oauth_client_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item);
+ }
+ }
+ return $found;
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_navigationMenu().
+ */
+function _oauth_client_civix_navigationMenu(&$nodes) {
+ if (!is_callable(['CRM_Core_BAO_Navigation', 'fixNavigationMenu'])) {
+ _oauth_client_civix_fixNavigationMenu($nodes);
+ }
+}
+
+/**
+ * Given a navigation menu, generate navIDs for any items which are
+ * missing them.
+ */
+function _oauth_client_civix_fixNavigationMenu(&$nodes) {
+ $maxNavID = 1;
+ array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) {
+ if ($key === 'navID') {
+ $maxNavID = max($maxNavID, $item);
+ }
+ });
+ _oauth_client_civix_fixNavigationMenuItems($nodes, $maxNavID, NULL);
+}
+
+function _oauth_client_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'])) {
+ _oauth_client_civix_fixNavigationMenuItems($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']);
+ }
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_alterSettingsFolders().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterSettingsFolders
+ */
+function _oauth_client_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
+ $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings';
+ if (!in_array($settingsDir, $metaDataFolders) && is_dir($settingsDir)) {
+ $metaDataFolders[] = $settingsDir;
+ }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_entityTypes().
+ *
+ * Find any *.entityType.php files, merge their content, and return.
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+ */
+function _oauth_client_civix_civicrm_entityTypes(&$entityTypes) {
+ $entityTypes = array_merge($entityTypes, [
+ 'CRM_OAuth_DAO_OAuthClient' => [
+ 'name' => 'OAuthClient',
+ 'class' => 'CRM_OAuth_DAO_OAuthClient',
+ 'table' => 'civicrm_oauth_client',
+ ],
+ 'CRM_OAuth_DAO_OAuthSysToken' => [
+ 'name' => 'OAuthSysToken',
+ 'class' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'table' => 'civicrm_oauth_systoken',
+ ],
+ ]);
+}
--- /dev/null
+<?php
+
+require_once 'oauth_client.civix.php';
+// phpcs:disable
+use CRM_OauthClient_ExtensionUtil as E;
+// phpcs:enable
+
+/**
+ * Implements hook_civicrm_config().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config/
+ */
+function oauth_client_civicrm_config(&$config) {
+ _oauth_client_civix_civicrm_config($config);
+}
+
+/**
+ * Implements hook_civicrm_xmlMenu().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_xmlMenu
+ */
+function oauth_client_civicrm_xmlMenu(&$files) {
+ _oauth_client_civix_civicrm_xmlMenu($files);
+}
+
+/**
+ * Implements hook_civicrm_install().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install
+ */
+function oauth_client_civicrm_install() {
+ _oauth_client_civix_civicrm_install();
+}
+
+/**
+ * Implements hook_civicrm_postInstall().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall
+ */
+function oauth_client_civicrm_postInstall() {
+ _oauth_client_civix_civicrm_postInstall();
+}
+
+/**
+ * Implements hook_civicrm_uninstall().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall
+ */
+function oauth_client_civicrm_uninstall() {
+ _oauth_client_civix_civicrm_uninstall();
+}
+
+/**
+ * Implements hook_civicrm_enable().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
+ */
+function oauth_client_civicrm_enable() {
+ _oauth_client_civix_civicrm_enable();
+}
+
+/**
+ * Implements hook_civicrm_disable().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable
+ */
+function oauth_client_civicrm_disable() {
+ _oauth_client_civix_civicrm_disable();
+}
+
+/**
+ * Implements hook_civicrm_upgrade().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade
+ */
+function oauth_client_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
+ return _oauth_client_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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed
+ */
+function oauth_client_civicrm_managed(&$entities) {
+ _oauth_client_civix_civicrm_managed($entities);
+}
+
+/**
+ * Implements hook_civicrm_permission().
+ *
+ * @see CRM_Utils_Hook::permission()
+ * @see CRM_Core_Permission::getCorePermissions()
+ */
+function oauth_client_civicrm_permission(&$permissions) {
+ $prefix = ts('CiviCRM') . ': ';
+ $permissions['manage OAuth client'] = [
+ $prefix . ts('manage OAuth client'),
+ ts('Create and delete OAuth client connections'),
+ ];
+ $permissions['manage OAuth client secrets'] = [
+ $prefix . ts('manage OAuth client secrets'),
+ ts('Access OAuth secrets'),
+ ];
+}
+
+/**
+ * Implements hook_civicrm_caseTypes().
+ *
+ * Generate a list of case-types.
+ *
+ * Note: This hook only runs in CiviCRM 4.4+.
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_caseTypes
+ */
+function oauth_client_civicrm_caseTypes(&$caseTypes) {
+ _oauth_client_civix_civicrm_caseTypes($caseTypes);
+}
+
+/**
+ * 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 https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules
+ */
+function oauth_client_civicrm_angularModules(&$angularModules) {
+ _oauth_client_civix_civicrm_angularModules($angularModules);
+}
+
+/**
+ * Implements hook_civicrm_alterSettingsFolders().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterSettingsFolders
+ */
+function oauth_client_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
+ _oauth_client_civix_civicrm_alterSettingsFolders($metaDataFolders);
+}
+
+/**
+ * Implements hook_civicrm_entityTypes().
+ *
+ * Declare entity types provided by this module.
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+ */
+function oauth_client_civicrm_entityTypes(&$entityTypes) {
+ _oauth_client_civix_civicrm_entityTypes($entityTypes);
+}
+
+/**
+ * Implements hook_civicrm_thems().
+ */
+function oauth_client_civicrm_themes(&$themes) {
+ _oauth_client_civix_civicrm_themes($themes);
+}
+
+// --- Functions below this ship commented out. Uncomment as required. ---
+
+/**
+ * Implements hook_civicrm_preProcess().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_preProcess
+ */
+//function oauth_client_civicrm_preProcess($formName, &$form) {
+//
+//}
+
+/**
+ * Implements hook_civicrm_navigationMenu().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_navigationMenu
+ */
+//function oauth_client_civicrm_navigationMenu(&$menu) {
+// _oauth_client_civix_insert_navigation_menu($menu, 'Mailings', array(
+// 'label' => E::ts('New subliminal message'),
+// 'name' => 'mailing_subliminal_message',
+// 'url' => 'civicrm/mailing/subliminal',
+// 'permission' => 'access CiviMail',
+// 'operator' => 'OR',
+// 'separator' => 0,
+// ));
+// _oauth_client_civix_navigationMenu($menu);
+//}
+
+/**
+ * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
+ */
+function oauth_client_civicrm_container($container) {
+ $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
+ $container->setDefinition('oauth2.league', new \Symfony\Component\DependencyInjection\Definition(
+ \Civi\OAuth\OAuthLeagueFacade::class, []))->setPublic(TRUE);
+ $container->setDefinition('oauth2.token', new \Symfony\Component\DependencyInjection\Definition(
+ \Civi\OAuth\OAuthTokenFacade::class, []))->setPublic(TRUE);
+}
+
+/**
+ * Implements hook_civicrm_oauthProviders().
+ */
+function oauth_client_civicrm_oauthProviders(&$providers) {
+ $ingest = function($pat) use (&$providers) {
+ $files = (array) glob($pat);
+ foreach ($files as $file) {
+ if (!defined('CIVICRM_TEST') && preg_match(';\.test\.json$;', $file)) {
+ continue;
+ }
+ $name = preg_replace(';\.(dist\.|test\.|)json$;', '', basename($file));
+ $provider = json_decode(file_get_contents($file), 1);
+ $provider['name'] = $name;
+ $providers[$name] = $provider;
+ }
+ };
+
+ $ingest(__DIR__ . '/providers/*.json');
+ $localDir = Civi::paths()->getPath('[civicrm.private]/oauth-providers');
+ if (file_exists($localDir)) {
+ $ingest($localDir . '/*.json');
+ }
+}
+
+/**
+ * Implements hook_civicrm_mailSetupActions().
+ *
+ * @see CRM_Utils_Hook::mailSetupActions()
+ */
+function oauth_client_civicrm_mailSetupActions(&$setupActions) {
+ $setupActions = array_merge($setupActions, CRM_OAuth_MailSetup::buildSetupLinks());
+}
+
+/**
+ * Implements hook_civicrm_oauthReturn().
+ */
+function oauth_client_civicrm_oauthReturn($token, &$nextUrl) {
+ CRM_OAuth_MailSetup::onReturn($token, $nextUrl);
+}
+
+/**
+ * Implements hook_civicrm_alterMailStore().
+ *
+ * @see CRM_Utils_Hook::alterMailStore()
+ */
+function oauth_client_civicrm_alterMailStore(&$mailSettings) {
+ CRM_OAuth_MailSetup::alterMailStore($mailSettings);
+}
--- /dev/null
+<?xml version="1.0"?>
+<phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/phpunit/bootstrap.php">
+ <testsuites>
+ <testsuite name="My Test Suite">
+ <directory>./tests/phpunit</directory>
+ </testsuite>
+ </testsuites>
+ <filter>
+ <whitelist>
+ <directory suffix=".php">./</directory>
+ </whitelist>
+ </filter>
+ <listeners>
+ <listener class="Civi\Test\CiviTestListener">
+ <arguments/>
+ </listener>
+ </listeners>
+</phpunit>
--- /dev/null
+{
+ "title": "Google Mail",
+ "class": "League\\OAuth2\\Client\\Provider\\Google",
+ "options": {
+ "urlAuthorize": "https://accounts.google.com/o/oauth2/v2/auth",
+ "urlAccessToken": "https://www.googleapis.com/oauth2/v4/token",
+ "urlResourceOwnerDetails": "https://openidconnect.googleapis.com/v1/userinfo",
+ "accessType": "offline",
+ "scopeSeparator": " ",
+ "scopes": [
+ "https://mail.google.com/",
+ "openid"
+ ]
+ },
+ "mailSettingsTemplate": {
+ "name": "{{token.resource_owner.email}}",
+ "domain": "{{token.resource_owner.email|getMailDomain}}",
+ "localpart": null,
+ "return_path": null,
+ "protocol:name": "IMAP",
+ "server": "imap.gmail.com",
+ "username": "{{token.resource_owner.email}}",
+ "password": null,
+ "is_ssl": true
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "title": "Microsoft Exchange Online",
+ "options": {
+ "urlAuthorize": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
+ "urlAccessToken": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+ "urlResourceOwnerDetails": "{{use_id_token}}",
+ "scopeSeparator": " ",
+ "scopes": [
+ "https://outlook.office.com/IMAP.AccessAsUser.All",
+ "https://outlook.office.com/POP.AccessAsUser.All",
+ "https://outlook.office.com/SMTP.Send",
+ "openid",
+ "email",
+ "offline_access"
+ ]
+ },
+ "mailSettingsTemplate": {
+ "name": "{{token.resource_owner.email}}",
+ "domain": "{{token.resource_owner.email|getMailDomain}}",
+ "localpart": null,
+ "return_path": null,
+ "protocol:name": "IMAP",
+ "server": "outlook.office365.com",
+ "username": "{{token.resource_owner.email}}",
+ "password": null,
+ "is_ssl": true
+ }
+}
--- /dev/null
+{
+ "title": "First Test Example",
+ "options": {
+ "urlAuthorize": "https://example.com/one/auth",
+ "urlAccessToken": "https://example.com/one/token",
+ "urlResourceOwnerDetails": "https://example.com/one/owner",
+ "scopes": ["scope-1-foo", "scope-1-bar"]
+ }
+}
--- /dev/null
+{
+ "name": "test_example_2",
+ "title": "Second Test Example",
+ "class": "My\\Example2",
+ "options": {
+ "urlAuthorize": "https://example.com/two",
+ "scopes": ["scope-2-foo", "scope-2-bar"]
+ }
+}
--- /dev/null
+<?php
+return [
+ 'oauthClientRedirectUrl' => [
+ 'group_name' => 'Developer Preferences',
+ 'group' => 'developer',
+ 'name' => 'fatalErrorHandler',
+ 'type' => 'String',
+ 'quick_form_type' => 'Element',
+ 'html_type' => 'text',
+ 'default' => NULL,
+ 'add' => '5.32',
+ 'title' => ts('Redirect URL'),
+ 'is_domain' => 1,
+ 'is_contact' => 0,
+ 'description' => ts('Override the redirect URL for OAuth2 requests. This is an absolute URL which should be equivalent to "civicrm/oauth-client/return".'),
+ ],
+];
--- /dev/null
+-- +--------------------------------------------------------------------+
+-- | Copyright CiviCRM LLC. All rights reserved. |
+-- | |
+-- | This work is published under the GNU AGPLv3 license with some |
+-- | permitted exceptions and without any warranty. For full license |
+-- | and copyright information, see https://civicrm.org/licensing |
+-- +--------------------------------------------------------------------+
+--
+-- Generated from schema.tpl
+-- DO NOT EDIT. Generated by CRM_Core_CodeGen
+--
+
+
+-- +--------------------------------------------------------------------+
+-- | Copyright CiviCRM LLC. All rights reserved. |
+-- | |
+-- | This work is published under the GNU AGPLv3 license with some |
+-- | permitted exceptions and without any warranty. For full license |
+-- | and copyright information, see https://civicrm.org/licensing |
+-- +--------------------------------------------------------------------+
+--
+-- Generated from drop.tpl
+-- DO NOT EDIT. Generated by CRM_Core_CodeGen
+--
+-- /*******************************************************
+-- *
+-- * Clean up the exisiting tables
+-- *
+-- *******************************************************/
+
+SET FOREIGN_KEY_CHECKS=0;
+
+DROP TABLE IF EXISTS `civicrm_oauth_systoken`;
+DROP TABLE IF EXISTS `civicrm_oauth_client`;
+
+SET FOREIGN_KEY_CHECKS=1;
+-- /*******************************************************
+-- *
+-- * Create new tables
+-- *
+-- *******************************************************/
+
+-- /*******************************************************
+-- *
+-- * civicrm_oauth_client
+-- *
+-- *******************************************************/
+CREATE TABLE `civicrm_oauth_client` (
+
+
+ `id` int unsigned AUTO_INCREMENT COMMENT 'Internal Client ID',
+ `provider` varchar(128) NOT NULL COMMENT 'Provider',
+ `guid` varchar(128) NOT NULL COMMENT 'Client ID',
+ `secret` text COMMENT 'Client Secret',
+ `options` text COMMENT 'Extra override options for the service (JSON)',
+ `is_active` tinyint NOT NULL DEFAULT 1 COMMENT 'Is the client currently enabled?',
+ `created_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When the client was created.',
+ `modified_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'When the client was created or modified.'
+,
+ PRIMARY KEY (`id`)
+
+ , INDEX `UI_provider`(
+ provider
+ )
+ , INDEX `UI_guid`(
+ guid
+ )
+
+
+) ;
+
+-- /*******************************************************
+-- *
+-- * civicrm_oauth_systoken
+-- *
+-- *******************************************************/
+CREATE TABLE `civicrm_oauth_systoken` (
+
+
+ `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Token ID',
+ `tag` varchar(128) COMMENT 'The tag specifies how this token will be used.',
+ `client_id` int unsigned COMMENT 'Client ID',
+ `grant_type` varchar(31) COMMENT 'Ex: authorization_code',
+ `scopes` text COMMENT 'List of scopes addressed by this token',
+ `token_type` varchar(128) COMMENT 'Ex: Bearer or MAC',
+ `access_token` text COMMENT 'Token to present when accessing resources',
+ `expires` int unsigned DEFAULT 0 COMMENT 'Expiration time for the access_token (seconds since epoch)',
+ `refresh_token` text COMMENT 'Token to present when refreshing the access_token',
+ `resource_owner_name` varchar(128) COMMENT 'Identifier for the resource owner. Structure varies by service.',
+ `resource_owner` text COMMENT 'Cached details describing the resource owner',
+ `error` text COMMENT 'List of scopes addressed by this token',
+ `raw` text COMMENT 'The token response data, per AccessToken::jsonSerialize',
+ `created_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When the client was created.',
+ `modified_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'When the client was created or modified.'
+,
+ PRIMARY KEY (`id`)
+
+ , INDEX `UI_tag`(
+ tag
+ )
+
+, CONSTRAINT FK_civicrm_oauth_systoken_client_id FOREIGN KEY (`client_id`) REFERENCES `civicrm_oauth_client`(`id`) ON DELETE CASCADE
+) ;
+
+
\ No newline at end of file
--- /dev/null
+-- +--------------------------------------------------------------------+
+-- | Copyright CiviCRM LLC. All rights reserved. |
+-- | |
+-- | This work is published under the GNU AGPLv3 license with some |
+-- | permitted exceptions and without any warranty. For full license |
+-- | and copyright information, see https://civicrm.org/licensing |
+-- +--------------------------------------------------------------------+
+--
+-- Generated from drop.tpl
+-- DO NOT EDIT. Generated by CRM_Core_CodeGen
+--
+-- /*******************************************************
+-- *
+-- * Clean up the exisiting tables
+-- *
+-- *******************************************************/
+
+SET FOREIGN_KEY_CHECKS=0;
+
+DROP TABLE IF EXISTS `civicrm_oauth_systoken`;
+DROP TABLE IF EXISTS `civicrm_oauth_client`;
+
+SET FOREIGN_KEY_CHECKS=1;
\ No newline at end of file
--- /dev/null
+{if $error}
+ <div class="crm-accordion-wrapper">
+ <div class="crm-accordion-header">
+ {ts}OAuth Error Details{/ts}
+ </div>
+ <div class="crm-accordion-body">
+ <ul>
+ <li><strong>{ts}Error type:{/ts}</strong> {$error.error|escape:'html'}</li>
+ <li><strong>{ts}Error description:{/ts}</strong>
+ <pre>{$error.error_description|escape:'html'}</pre>
+ </li>
+ <li><strong>{ts}Error URI:{/ts}</strong> <code>{$error.error_uri|escape:'html'}</code></li>
+ </ul>
+ </div>
+ </div>
+{else}
+ <p>{ts}An OAuth token was created!{/ts}</p>
+ <p>{ts}There is no clear "next step", so this may be a new integration. Please update the integration to define a next step via "hook_civicrm_oauthReturn" or "landingUrl".{/ts}</p>
+{/if}
+
+{if $stateJson}
+ <div class="crm-accordion-wrapper collapsed">
+ <div class="crm-accordion-header">
+ {ts}OAuth State{/ts}
+ </div>
+ <div class="crm-accordion-body">
+ <pre>{$stateJson}</pre>
+ </div>
+ </div>
+{/if}
+
+{if $tokenJson}
+ <div class="crm-accordion-wrapper collapsed">
+ <div class="crm-accordion-header">
+ {ts}OAuth Token{/ts}
+ </div>
+ <div class="crm-accordion-body">
+ <pre>{$tokenJson}</pre>
+ </div>
+ </div>
+{/if}
--- /dev/null
+<?php
+
+use CRM_OAuth_ExtensionUtil as E;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * Test helper functions in CRM_OAuth_MailSetup.
+ *
+ * @group headless
+ */
+class CRM_OAuth_MailSetupTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()->install('oauth-client')->apply();
+ }
+
+ public function setUp() {
+ parent::setUp();
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ }
+
+ public function testEvalArrayTemplate() {
+ $vars = array(
+ 'token' => [
+ 'client_id' => 10,
+ 'resource_owner' => ['mail' => 'foo@bar.com'],
+ ],
+ 'client' => [
+ 'id' => 1,
+ 'provider' => 'ms-exchange',
+ 'guid' => 'abcd-1234-efgh-5678',
+ 'secret' => '8765-hgfe-4321-dcba',
+ 'options' => NULL,
+ 'is_active' => TRUE,
+ 'created_date' => '2020-10-29 10:11:12',
+ 'modified_date' => '2020-10-29 10:11:12',
+ ],
+ 'provider' => [
+ 'name' => 'foozball',
+ 'title' => 'Foozball Association',
+ 'options' => [
+ 'urlAuthorize' => 'https://login.example.com/common/oauth2/v2.0/authorize',
+ 'urlAccessToken' => 'https://login.example.com/common/oauth2/v2.0/token',
+ 'urlResourceOwnerDetails' => 'https://resource.example.com/v9.0/me',
+ 'scopeSeparator' => ' ',
+ 'scopes' => [],
+ ],
+ 'mailSettingsTemplate' => [
+ 'name' => '{{provider.title}}: {{token.resource_owner.mail}}',
+ 'domain' => '{{token.resource_owner.mail|getMailDomain}}',
+ 'localpart' => NULL,
+ 'return_path' => NULL,
+ 'protocol:name' => 'IMAP',
+ 'server' => 'imap.foozball.com',
+ 'username' => '{{token.resource_owner.mail}}',
+ 'password' => NULL,
+ 'is_ssl' => TRUE,
+ ],
+ 'class' => 'Civi\\OAuth\\CiviGenericProvider',
+ ],
+ );
+ $expected = [
+ 'name' => 'Foozball Association: foo@bar.com',
+ 'domain' => 'bar.com',
+ 'localpart' => NULL,
+ 'return_path' => NULL,
+ 'protocol:name' => 'IMAP',
+ 'server' => 'imap.foozball.com',
+ 'username' => 'foo@bar.com',
+ 'password' => '',
+ 'is_ssl' => TRUE,
+ ];
+ $actual = \CRM_OAuth_MailSetup::evalArrayTemplate($vars['provider']['mailSettingsTemplate'], $vars);
+ $this->assertEquals($expected, $actual);
+ $this->assertTrue($actual['localpart'] === NULL);
+ }
+
+}
--- /dev/null
+<?php
+
+use CRM_OAuth_ExtensionUtil as E;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * Test the "grant" methods (authorizationCode, clientCredential, etc).
+ *
+ * @group headless
+ */
+class api_v4_OAuthClientGrantTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()->install('oauth-client')->apply();
+ }
+
+ public function setUp() {
+ parent::setUp();
+ $this->assertEquals(0, CRM_Core_DAO::singleValueQuery('SELECT count(*) FROM civicrm_oauth_client'));
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * Basic sanity check - create, read, and delete a client.
+ */
+ public function testAuthorizationCode() {
+ $usePerms = function($ps) {
+ $base = ['access CiviCRM'];
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = array_merge($base, $ps);
+ };
+
+ $usePerms(['manage OAuth client']);
+ $client = $this->createClient();
+
+ $usePerms(['manage OAuth client']);
+ $result = Civi\Api4\OAuthClient::authorizationCode()->addWhere('id', '=', $client['id'])->execute();
+ $this->assertEquals(1, $result->count());
+ foreach ($result as $ac) {
+ $url = parse_url($ac['url']);
+ $this->assertEquals('example.com', $url['host']);
+ $this->assertEquals('/one/auth', $url['path']);
+ \parse_str($url['query'], $actualQuery);
+ $this->assertEquals('code', $actualQuery['response_type']);
+ $this->assertRegExp(';^[cs]_[a-zA-Z0-9]+$;', $actualQuery['state']);
+ $this->assertEquals('scope-1-foo,scope-1-bar', $actualQuery['scope']);
+ // ? // $this->assertEquals('auto', $actualQuery['approval_prompt']);
+ $this->assertEquals('example-id', $actualQuery['client_id']);
+ $this->assertRegExp(';civicrm/oauth-client/return;', $actualQuery['redirect_uri']);
+ }
+ }
+
+ private function createClient(): array {
+ $create = Civi\Api4\OAuthClient::create()->setValues([
+ 'provider' => 'test_example_1',
+ 'guid' => "example-id",
+ 'secret' => "example-secret",
+ ])->execute();
+ $this->assertEquals(1, $create->count());
+ $client = $create->first();
+ $this->assertTrue(!empty($client['id']));
+ return $client;
+ }
+
+}
--- /dev/null
+<?php
+
+use CRM_OAuth_ExtensionUtil as E;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * Create, read, and destroy OAuth clients.
+ *
+ * @group headless
+ */
+class api_v4_OAuthClientTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()->install('oauth-client')->apply();
+ }
+
+ public function setUp() {
+ parent::setUp();
+ $this->assertEquals(0, CRM_Core_DAO::singleValueQuery('SELECT count(*) FROM civicrm_oauth_client'));
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * Basic sanity check - create, read, and delete a client.
+ */
+ public function testBasic() {
+ $random = CRM_Utils_String::createRandom(16, CRM_Utils_String::ALPHANUMERIC);
+ $usePerms = function($ps) {
+ $base = ['access CiviCRM'];
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = array_merge($base, $ps);
+ };
+
+ $usePerms(['manage OAuth client']);
+ $create = Civi\Api4\OAuthClient::create()->setValues([
+ 'provider' => 'test_example_1',
+ 'guid' => "example-id-$random" ,
+ 'secret' => "example-secret-$random",
+ ])->execute();
+ $this->assertEquals(1, $create->count());
+ $client = $create->first();
+ $this->assertEquals("example-id-$random", $client['guid']);
+ $this->assertEquals("example-secret-$random", $client['secret']);
+
+ $usePerms(['manage OAuth client']);
+ // If we can tighten perm model: $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $get = Civi\Api4\OAuthClient::get(0)->addWhere('guid', '=', "example-id-$random")->execute();
+ $this->assertEquals(1, $get->count());
+ $client = $get->first();
+ $this->assertEquals("example-id-$random", $client['guid']);
+ $this->assertEquals("example-secret-$random", $client['secret']);
+
+ $usePerms(['manage OAuth client']);
+ Civi\Api4\OAuthClient::delete(0)->addWhere('guid', '=', "example-id-$random")->execute();
+ $get = Civi\Api4\OAuthClient::get(0)->addWhere('guid', '=', "example-id-$random")->execute();
+ $this->assertEquals(0, $get->count());
+ }
+
+ public function testCreateBadProvider() {
+ $random = CRM_Utils_String::createRandom(16, CRM_Utils_String::ALPHANUMERIC);
+ $usePerms = function($ps) {
+ $base = ['access CiviCRM'];
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = array_merge($base, $ps);
+ };
+
+ $usePerms(['manage OAuth client']);
+ try {
+ Civi\Api4\OAuthClient::create()->setValues([
+ 'provider' => 'test_example_does_not_exist',
+ 'guid' => "example-id-$random" ,
+ 'secret' => "example-secret-$random",
+ ])->execute();
+ $this->fail("Expected exception: invalid provider");
+ }
+ catch (API_Exception $e) {
+ $this->assertRegExp(';Invalid provider;', $e->getMessage());
+ }
+ }
+
+ public function testUpdateBadProvider() {
+ $random = CRM_Utils_String::createRandom(16, CRM_Utils_String::ALPHANUMERIC);
+ $usePerms = function($ps) {
+ $base = ['access CiviCRM'];
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = array_merge($base, $ps);
+ };
+
+ $usePerms(['manage OAuth client']);
+ $created = Civi\Api4\OAuthClient::create()->setValues([
+ 'provider' => 'test_example_1',
+ 'guid' => "example-id-$random" ,
+ 'secret' => "example-secret-$random",
+ ])->execute();
+
+ try {
+ Civi\Api4\OAuthClient::update()
+ ->addWhere('id', '=', $created->first()['id'])
+ ->setValues(['provider' => 'test_example_does_not_exist'])
+ ->execute();
+ $this->fail("Expected exception: invalid provider");
+ }
+ catch (API_Exception $e) {
+ $this->assertRegExp(';Invalid provider;', $e->getMessage());
+ }
+
+ Civi\Api4\OAuthClient::update()
+ ->addWhere('id', '=', $created->first()['id'])
+ ->setValues(['provider:name' => 'test_example_2'])
+ ->execute();
+ }
+
+}
--- /dev/null
+<?php
+use CRM_OAuth_ExtensionUtil as E;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * Read list of OAuth providers
+ *
+ * @group headless
+ */
+class api_v4_OAuthProviderTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()->install('oauth-client')->apply();
+ }
+
+ public function setUp() {
+ parent::setUp();
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * Create, read, and destroy token - with full access to secrets.
+ */
+ public function testGet() {
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM'];
+
+ $examples = Civi\Api4\OAuthProvider::get()
+ ->addWhere('name', 'LIKE', 'test_example%')
+ ->addOrderBy('name', 'DESC')
+ ->execute();
+ $this->assertEquals(2, $examples->count());
+
+ $this->assertEquals('Civi\OAuth\CiviGenericProvider', $examples->last()['class']);
+ $this->assertEquals('My\Example2', $examples->first()['class']);
+ $this->assertEquals('https://example.com/one/auth', $examples->last()['options']['urlAuthorize']);
+ $this->assertEquals('https://example.com/two', $examples->first()['options']['urlAuthorize']);
+ }
+
+}
--- /dev/null
+<?php
+
+use CRM_OAuth_ExtensionUtil as E;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * Create, read, and destroy OAuth tokens.
+ *
+ * @group headless
+ */
+class api_v4_OAuthSysTokenTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()->install('oauth-client')->apply();
+ }
+
+ public function setUp() {
+ parent::setUp();
+ $this->assertEquals(0, CRM_Core_DAO::singleValueQuery('SELECT count(*) FROM civicrm_oauth_client'));
+ $this->assertEquals(0, CRM_Core_DAO::singleValueQuery('SELECT count(*) FROM civicrm_oauth_systoken'));
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * Create, read, and destroy token - with full access to secrets.
+ */
+ public function testFullApiAccess() {
+ $random = CRM_Utils_String::createRandom(16, CRM_Utils_String::ALPHANUMERIC);
+ $usePerms = function($ps) {
+ $base = ['access CiviCRM'];
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = array_merge($base, $ps);
+ };
+
+ $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $createClient = Civi\Api4\OAuthClient::create()->setValues([
+ 'provider' => 'test_example_1',
+ 'guid' => "example-id-$random" ,
+ 'secret' => "example-secret-$random",
+ ])->execute();
+ $client = $createClient->first();
+ $this->assertTrue(is_numeric($client['id']));
+
+ $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $createToken = Civi\Api4\OAuthSysToken::create()->setValues([
+ 'client_id' => $client['id'],
+ 'access_token' => "example-access-token-$random",
+ 'refresh_token' => "example-refresh-token-$random",
+ ])->execute();
+ $token = $createToken->first();
+ $this->assertTrue(is_numeric($token['id']));
+ $this->assertEquals($client['id'], $token['client_id']);
+ $this->assertEquals("example-access-token-$random", $token['access_token']);
+ $this->assertEquals("example-refresh-token-$random", $token['refresh_token']);
+
+ $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $getTokens = Civi\Api4\OAuthSysToken::get()->execute();
+ $this->assertEquals(1, count($getTokens));
+ // ^^ Started at 0, added 1.
+ $token = $getTokens->first();
+ $this->assertEquals($client['id'], $token['client_id']);
+ $this->assertEquals("example-access-token-$random", $token['access_token']);
+ $this->assertEquals("example-refresh-token-$random", $token['refresh_token']);
+
+ $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $updateToken = Civi\Api4\OAuthSysToken::update()
+ ->setWhere([['client.guid', '=', "example-id-$random"]])
+ ->setValues(['access_token' => "revised-access-token-$random"])
+ ->execute();
+
+ $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $getTokens = Civi\Api4\OAuthSysToken::get()->execute();
+ $this->assertEquals(1, count($getTokens));
+ $token = $getTokens->first();
+ $this->assertEquals($client['id'], $token['client_id']);
+ $this->assertEquals("revised-access-token-$random", $token['access_token']);
+ $this->assertEquals("example-refresh-token-$random", $token['refresh_token']);
+ }
+
+ /**
+ * Create, read, and destroy a token - with limited API access (cannot access token secrets).
+ */
+ public function testLimitedApiAccess() {
+ $random = CRM_Utils_String::createRandom(16, CRM_Utils_String::ALPHANUMERIC);
+ $usePerms = function($ps) {
+ $base = ['access CiviCRM'];
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = array_merge($base, $ps);
+ };
+
+ $usePerms(['manage OAuth client']);
+ $createClient = Civi\Api4\OAuthClient::create()->setValues([
+ 'provider' => 'test_example_1',
+ 'guid' => "example-id-$random" ,
+ 'secret' => "example-secret-$random",
+ ])->execute();
+ $client = $createClient->first();
+ $this->assertTrue(is_numeric($client['id']));
+
+ // User has some access to tokens -- but secret fields are off limits.
+ try {
+ $usePerms(['manage OAuth client']);
+ Civi\Api4\OAuthSysToken::create()->setValues([
+ 'client_id' => $client['id'],
+ 'access_token' => "ignored-access-token-$random",
+ 'refresh_token' => "ignored-refresh-token-$random",
+ ])->execute();
+ $this->fail('Expected exception - User should not be able to write secret values.');
+ }
+ catch (\Civi\API\Exception\UnauthorizedException $e) {
+ // OK
+ }
+
+ // Tokens with secret values can still be created by system services.
+ $usePerms(['manage OAuth client']);
+ $createTokenFull = Civi\Api4\OAuthSysToken::create(FALSE)->setValues([
+ 'client_id' => $client['id'],
+ 'access_token' => "example-access-token-$random",
+ 'refresh_token' => "example-refresh-token-$random",
+ ])->execute();
+ $token = $createTokenFull->first();
+ $this->assertTrue(is_numeric($token['id']));
+ $this->assertEquals($client['id'], $token['client_id']);
+ $this->assertEquals("example-access-token-$random", $token['access_token']);
+ $this->assertEquals("example-refresh-token-$random", $token['refresh_token']);
+
+ $usePerms(['manage OAuth client']);
+ $getTokens = Civi\Api4\OAuthSysToken::get()->execute();
+ $this->assertEquals(1, count($getTokens));
+ // ^^ Started at 0, added 1.
+ $token = $getTokens->first();
+ $this->assertEquals($client['id'], $token['client_id']);
+ $this->assertArrayNotHasKey('access_token', $token);
+ $this->assertArrayNotHasKey('refresh_token', $token);
+
+ $usePerms(['manage OAuth client']);
+ try {
+ Civi\Api4\OAuthSysToken::update()
+ ->setWhere([['client.guid', '=', "example-id-$random"]])
+ ->setValues(['access_token' => "revised-access-token-$random"])
+ ->execute();
+ $this->fail('Expected exception - User should not be able to write secret values.');
+ }
+ catch (\Civi\API\Exception\UnauthorizedException $e) {
+ // OK
+ }
+
+ $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $getTokens = Civi\Api4\OAuthSysToken::get()->execute();
+ $this->assertEquals(1, count($getTokens));
+ $token = $getTokens->first();
+ $this->assertEquals($client['id'], $token['client_id']);
+ $this->assertEquals("example-access-token-$random", $token['access_token']);
+ $this->assertEquals("example-refresh-token-$random", $token['refresh_token']);
+ }
+
+ public function testGetByScope() {
+ $random = CRM_Utils_String::createRandom(16, CRM_Utils_String::ALPHANUMERIC);
+ $usePerms = function($ps) {
+ $base = ['access CiviCRM'];
+ \CRM_Core_Config::singleton()->userPermissionClass->permissions = array_merge($base, $ps);
+ };
+
+ $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $createClient = Civi\Api4\OAuthClient::create()->setValues([
+ 'provider' => 'test_example_1',
+ 'guid' => "example-id-$random" ,
+ 'secret' => "example-secret-$random",
+ ])->execute();
+ $client = $createClient->first();
+ $this->assertTrue(is_numeric($client['id']));
+
+ $usePerms(['manage OAuth client', 'manage OAuth client secrets']);
+ $createToken = Civi\Api4\OAuthSysToken::create()->setValues([
+ 'client_id' => $client['id'],
+ 'access_token' => "example-access-token-$random",
+ 'refresh_token' => "example-refresh-token-$random",
+ 'scopes' => ['foo', 'bar'],
+ ])->execute();
+ $token = $createToken->first();
+ $this->assertTrue(is_numeric($token['id']));
+ $this->assertEquals($client['id'], $token['client_id']);
+ $this->assertEquals("example-access-token-$random", $token['access_token']);
+ $this->assertEquals("example-refresh-token-$random", $token['refresh_token']);
+ $this->assertEquals(['foo', 'bar'], $token['scopes']);
+
+ $usePerms(['manage OAuth client']);
+ $getTokens = Civi\Api4\OAuthSysToken::get()
+ ->addWhere('client.provider', '=', 'test_example_1')
+ ->addWhere('scopes', 'CONTAINS', 'foo')
+ ->execute();
+ $this->assertEquals(1, count($getTokens));
+ $this->assertEquals($createToken->first()['id'], $getTokens->first()['id']);
+
+ $usePerms(['manage OAuth client']);
+ $getTokens = Civi\Api4\OAuthSysToken::get()
+ ->addWhere('client.provider', '=', 'test_example_1')
+ ->addWhere('scopes', 'CONTAINS', 'nada')
+ ->execute();
+ $this->assertEquals(0, count($getTokens));
+
+ $usePerms(['manage OAuth client']);
+ $getTokens = Civi\Api4\OAuthSysToken::get()
+ ->addWhere('client.provider', '=', 'test_example_2')
+ ->addWhere('scopes', 'CONTAINS', 'foo')
+ ->execute();
+ $this->assertEquals(0, count($getTokens));
+ }
+
+}
--- /dev/null
+<?php
+
+ini_set('memory_limit', '2G');
+ini_set('safe_mode', 0);
+define('CIVICRM_TEST', 1);
+// phpcs:disable
+eval(cv('php:boot --level=classloader', 'phpcode'));
+// phpcs:enable
+// Allow autoloading of PHPUnit helper classes in this extension.
+$loader = new \Composer\Autoload\ClassLoader();
+$loader->add('CRM_', __DIR__);
+$loader->add('Civi\\', __DIR__);
+$loader->add('api_', __DIR__);
+$loader->add('api\\', __DIR__);
+$loader->register();
+
+/**
+ * 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 = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR);
+ $oldOutput = getenv('CV_OUTPUT');
+ putenv("CV_OUTPUT=json");
+
+ // Execute `cv` in the original folder. This is a work-around for
+ // phpunit/codeception, which seem to manipulate PWD.
+ $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd);
+
+ $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)");
+ }
+}
--- /dev/null
+<?xml version="1.0"?>
+<menu>
+ <item>
+ <path>civicrm/oauth-client/return</path>
+ <page_callback>CRM_OAuth_Page_Return</page_callback>
+ <title>Return</title>
+ <access_arguments>access CiviCRM</access_arguments>
+ </item>
+</menu>
--- /dev/null
+<?php
+// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at:
+// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+return [
+ [
+ 'name' => 'OAuthClient',
+ 'class' => 'CRM_OAuth_DAO_OAuthClient',
+ 'table' => 'civicrm_oauth_client',
+ ],
+];
--- /dev/null
+<table>
+ <base>CRM/OAuth</base>
+ <class>OAuthClient</class>
+ <name>civicrm_oauth_client</name>
+ <add>5.32</add>
+ <field>
+ <name>id</name>
+ <title>Internal Client ID</title>
+ <type>int unsigned</type>
+ <comment>Internal Client ID</comment>
+ <add>5.32</add>
+ </field>
+ <primaryKey>
+ <name>id</name>
+ <autoincrement>true</autoincrement>
+ </primaryKey>
+
+ <field>
+ <name>provider</name>
+ <title>Provider</title>
+ <type>varchar</type>
+ <length>128</length>
+ <pseudoconstant>
+ <callback>CRM_OAuth_BAO_OAuthClient::getProviders</callback>
+ </pseudoconstant>
+ <required>true</required>
+ <comment>Provider</comment>
+ <add>5.32</add>
+ </field>
+ <index>
+ <name>UI_provider</name>
+ <fieldName>provider</fieldName>
+ <add>5.32</add>
+ </index>
+
+ <field>
+ <name>guid</name>
+ <title>Client ID</title>
+ <type>varchar</type>
+ <length>128</length>
+ <required>true</required>
+ <comment>Client ID</comment>
+ <add>5.32</add>
+ </field>
+ <index>
+ <name>UI_guid</name>
+ <fieldName>guid</fieldName>
+ <add>5.32</add>
+ </index>
+
+ <field>
+ <name>secret</name>
+ <title>Client Secret</title>
+ <type>text</type>
+ <comment>Client Secret</comment>
+ <add>5.32</add>
+ <!-- Would prefer this be write-only for std admin, and read-write with special/elevated perm -->
+ <!--<permission>-->
+ <!--<or>manage OAuth client secrets</or>-->
+ <!--</permission>-->
+ </field>
+
+ <field>
+ <name>options</name>
+ <type>text</type>
+ <comment>Extra override options for the service (JSON)</comment>
+ <!-- Ex: urlAuthorize, urlAccessToken, urlResourceOwnerDetails, scopes -->
+ <serialize>JSON</serialize>
+ <add>5.32</add>
+ </field>
+
+ <!-- Lifecycle -->
+
+ <field>
+ <name>is_active</name>
+ <title>Is Active</title>
+ <type>boolean</type>
+ <default>1</default>
+ <required>true</required>
+ <comment>Is the client currently enabled?</comment>
+ <add>5.32</add>
+ </field>
+ <field>
+ <name>created_date</name>
+ <type>timestamp</type>
+ <comment>When the client was created.</comment>
+ <required>true</required>
+ <default>CURRENT_TIMESTAMP</default>
+ <add>5.32</add>
+ </field>
+ <field>
+ <name>modified_date</name>
+ <type>timestamp</type>
+ <comment>When the client was created or modified.</comment>
+ <required>true</required>
+ <default>CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP</default>
+ <add>5.32</add>
+ </field>
+
+</table>
--- /dev/null
+<?php
+// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at:
+// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+return [
+ [
+ 'name' => 'OAuthSysToken',
+ 'class' => 'CRM_OAuth_DAO_OAuthSysToken',
+ 'table' => 'civicrm_oauth_systoken',
+ ],
+];
--- /dev/null
+<table>
+ <base>CRM/OAuth</base>
+ <class>OAuthSysToken</class>
+ <name>civicrm_oauth_systoken</name>
+ <add>5.32</add>
+ <field>
+ <name>id</name>
+ <title>Token ID</title>
+ <type>int unsigned</type>
+ <required>true</required>
+ <comment>Token ID</comment>
+ <add>5.32</add>
+ </field>
+ <primaryKey>
+ <name>id</name>
+ <autoincrement>true</autoincrement>
+ </primaryKey>
+
+ <!-- Details based on how the token was requested -->
+
+ <field>
+ <name>tag</name>
+ <title>Tag</title>
+ <type>varchar</type>
+ <length>128</length>
+ <comment>The tag specifies how this token will be used.</comment>
+ <add>5.32</add>
+ </field>
+ <index>
+ <name>UI_tag</name>
+ <fieldName>tag</fieldName>
+ <add>5.32</add>
+ </index>
+
+ <field>
+ <name>client_id</name>
+ <title>Client ID</title>
+ <type>int unsigned</type>
+ <comment>Client ID</comment>
+ <add>5.32</add>
+ </field>
+ <foreignKey>
+ <name>client_id</name>
+ <table>civicrm_oauth_client</table>
+ <key>id</key>
+ <add>5.32</add>
+ <onDelete>CASCADE</onDelete>
+ </foreignKey>
+
+ <field>
+ <name>grant_type</name>
+ <title>Grant type</title>
+ <type>varchar</type>
+ <length>31</length>
+ <!-- FIXME: Pseudoconstant -->
+ <comment>Ex: authorization_code</comment>
+ <add>5.32</add>
+ </field>
+
+ <field>
+ <name>scopes</name>
+ <type>text</type>
+ <comment>List of scopes addressed by this token</comment>
+ <serialize>SEPARATOR_BOOKEND</serialize>
+ <add>5.32</add>
+ </field>
+
+ <!-- Data provided by the authentication server -->
+
+ <field>
+ <name>token_type</name>
+ <title>Token Type</title>
+ <type>varchar</type>
+ <length>128</length>
+ <comment>Ex: Bearer or MAC</comment>
+ <add>5.32</add>
+ </field>
+
+ <field>
+ <name>access_token</name>
+ <title>Access Token</title>
+ <type>text</type>
+ <!-- text or varchar? In theory, if the auth svc uses JWT, tokens can get long -->
+ <permission>
+ <or>manage OAuth client secrets</or>
+ </permission>
+ <comment>Token to present when accessing resources</comment>
+ <add>5.32</add>
+ </field>
+
+ <field>
+ <name>expires</name>
+ <type>int unsigned</type>
+ <title>Expiration time</title>
+ <default>0</default>
+ <comment>Expiration time for the access_token (seconds since epoch)</comment>
+ <add>4.7</add>
+ </field>
+
+ <field>
+ <name>refresh_token</name>
+ <title>Refresh Token</title>
+ <type>text</type>
+ <!-- text or varchar? In theory, if the auth svc uses JWT, tokens can get long -->
+ <permission>
+ <or>manage OAuth client secrets</or>
+ </permission>
+ <comment>Token to present when refreshing the access_token</comment>
+ <add>5.32</add>
+ </field>
+
+ <field>
+ <name>resource_owner_name</name>
+ <title>Resource Owner Name</title>
+ <type>varchar</type>
+ <length>128</length>
+ <comment>Identifier for the resource owner. Structure varies by service.</comment>
+ <add>5.32</add>
+ </field>
+
+ <field>
+ <name>resource_owner</name>
+ <title>Resource Owner</title>
+ <type>text</type>
+ <comment>Cached details describing the resource owner</comment>
+ <serialize>JSON</serialize>
+ <add>5.32</add>
+ </field>
+
+ <field>
+ <name>error</name>
+ <type>text</type>
+ <comment>List of scopes addressed by this token</comment>
+ <serialize>JSON</serialize>
+ <add>5.32</add>
+ </field>
+
+ <field>
+ <name>raw</name>
+ <title>Raw token</title>
+ <type>text</type>
+ <serialize>JSON</serialize>
+ <comment>The token response data, per AccessToken::jsonSerialize</comment>
+ <add>5.32</add>
+ </field>
+
+ <!-- Lifecycle -->
+
+ <field>
+ <name>created_date</name>
+ <type>timestamp</type>
+ <comment>When the client was created.</comment>
+ <required>false</required>
+ <default>CURRENT_TIMESTAMP</default>
+ <add>5.32</add>
+ </field>
+ <field>
+ <name>modified_date</name>
+ <type>timestamp</type>
+ <comment>When the client was created or modified.</comment>
+ <required>false</required>
+ <default>CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP</default>
+ <add>5.32</add>
+ </field>
+
+</table>
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Search Display BAO
+ */
+class CRM_Search_BAO_SearchDisplay extends CRM_Search_DAO_SearchDisplay {
+
+}
--- /dev/null
+<?php
+
+/**
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ *
+ * Generated from org.civicrm.search/xml/schema/CRM/Search/SearchDisplay.xml
+ * DO NOT EDIT. Generated by CRM_Core_CodeGen
+ * (GenCodeChecksum:ac28cede0407e2e1bf2273b7ca6421d4)
+ */
+use CRM_Search_ExtensionUtil as E;
+
+/**
+ * Database access object for the SearchDisplay entity.
+ */
+class CRM_Search_DAO_SearchDisplay extends CRM_Core_DAO {
+ const EXT = E::LONG_NAME;
+ const TABLE_ADDED = '';
+
+ /**
+ * Static instance to hold the table name.
+ *
+ * @var string
+ */
+ public static $_tableName = 'civicrm_search_display';
+
+ /**
+ * Should CiviCRM log any modifications to this table in the civicrm_log table.
+ *
+ * @var bool
+ */
+ public static $_log = TRUE;
+
+ /**
+ * Unique SearchDisplay ID
+ *
+ * @var int
+ */
+ public $id;
+
+ /**
+ * Unique name for identifying search display
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Label for identifying search display to administrators
+ *
+ * @var string
+ */
+ public $label;
+
+ /**
+ * FK to saved search table.
+ *
+ * @var int
+ */
+ public $saved_search_id;
+
+ /**
+ * Type of display
+ *
+ * @var string
+ */
+ public $type;
+
+ /**
+ * Configuration data for the search display
+ *
+ * @var text
+ */
+ public $settings;
+
+ /**
+ * Class constructor.
+ */
+ public function __construct() {
+ $this->__table = 'civicrm_search_display';
+ parent::__construct();
+ }
+
+ /**
+ * Returns localized title of this entity.
+ *
+ * @param bool $plural
+ * Whether to return the plural version of the title.
+ */
+ public static function getEntityTitle($plural = FALSE) {
+ return $plural ? E::ts('Search Displays') : E::ts('Search Display');
+ }
+
+ /**
+ * Returns foreign keys and entity references.
+ *
+ * @return array
+ * [CRM_Core_Reference_Interface]
+ */
+ public static function getReferenceColumns() {
+ if (!isset(Civi::$statics[__CLASS__]['links'])) {
+ Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__);
+ Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'saved_search_id', 'civicrm_saved_search', 'id');
+ CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']);
+ }
+ return Civi::$statics[__CLASS__]['links'];
+ }
+
+ /**
+ * Returns all the column names of this table
+ *
+ * @return array
+ */
+ public static function &fields() {
+ if (!isset(Civi::$statics[__CLASS__]['fields'])) {
+ Civi::$statics[__CLASS__]['fields'] = [
+ 'id' => [
+ 'name' => 'id',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'title' => E::ts('Search Display ID'),
+ 'description' => E::ts('Unique SearchDisplay ID'),
+ 'required' => TRUE,
+ 'where' => 'civicrm_search_display.id',
+ 'table_name' => 'civicrm_search_display',
+ 'entity' => 'SearchDisplay',
+ 'bao' => 'CRM_Search_DAO_SearchDisplay',
+ 'localizable' => 0,
+ 'add' => '1.0',
+ ],
+ 'name' => [
+ 'name' => 'name',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Search Display Name'),
+ 'description' => E::ts('Unique name for identifying search display'),
+ 'required' => TRUE,
+ 'maxlength' => 255,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_search_display.name',
+ 'table_name' => 'civicrm_search_display',
+ 'entity' => 'SearchDisplay',
+ 'bao' => 'CRM_Search_DAO_SearchDisplay',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Text',
+ ],
+ 'add' => '1.0',
+ ],
+ 'label' => [
+ 'name' => 'label',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Search Display Label'),
+ 'description' => E::ts('Label for identifying search display to administrators'),
+ 'required' => TRUE,
+ 'maxlength' => 255,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_search_display.label',
+ 'table_name' => 'civicrm_search_display',
+ 'entity' => 'SearchDisplay',
+ 'bao' => 'CRM_Search_DAO_SearchDisplay',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Text',
+ ],
+ 'add' => '1.0',
+ ],
+ 'saved_search_id' => [
+ 'name' => 'saved_search_id',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'title' => E::ts('Saved Search ID'),
+ 'description' => E::ts('FK to saved search table.'),
+ 'required' => TRUE,
+ 'where' => 'civicrm_search_display.saved_search_id',
+ 'table_name' => 'civicrm_search_display',
+ 'entity' => 'SearchDisplay',
+ 'bao' => 'CRM_Search_DAO_SearchDisplay',
+ 'localizable' => 0,
+ 'FKClassName' => 'CRM_Contact_DAO_SavedSearch',
+ 'add' => '1.0',
+ ],
+ 'type' => [
+ 'name' => 'type',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Search Display Type'),
+ 'description' => E::ts('Type of display'),
+ 'required' => TRUE,
+ 'maxlength' => 128,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_search_display.type',
+ 'table_name' => 'civicrm_search_display',
+ 'entity' => 'SearchDisplay',
+ 'bao' => 'CRM_Search_DAO_SearchDisplay',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Select',
+ ],
+ 'pseudoconstant' => [
+ 'optionGroupName' => 'search_display_type',
+ 'optionEditPath' => 'civicrm/admin/options/search_display_type',
+ ],
+ 'add' => '1.0',
+ ],
+ 'settings' => [
+ 'name' => 'settings',
+ 'type' => CRM_Utils_Type::T_TEXT,
+ 'title' => E::ts('Search Display Settings'),
+ 'description' => E::ts('Configuration data for the search display'),
+ 'where' => 'civicrm_search_display.settings',
+ 'default' => 'NULL',
+ 'table_name' => 'civicrm_search_display',
+ 'entity' => 'SearchDisplay',
+ 'bao' => 'CRM_Search_DAO_SearchDisplay',
+ 'localizable' => 0,
+ 'serialize' => self::SERIALIZE_JSON,
+ 'add' => '1.0',
+ ],
+ ];
+ CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
+ }
+ return Civi::$statics[__CLASS__]['fields'];
+ }
+
+ /**
+ * Return a mapping from field-name to the corresponding key (as used in fields()).
+ *
+ * @return array
+ * Array(string $name => string $uniqueName).
+ */
+ public static function &fieldKeys() {
+ if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) {
+ Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields()));
+ }
+ return Civi::$statics[__CLASS__]['fieldKeys'];
+ }
+
+ /**
+ * Returns the names of this table
+ *
+ * @return string
+ */
+ public static function getTableName() {
+ return self::$_tableName;
+ }
+
+ /**
+ * Returns if this table needs to be logged
+ *
+ * @return bool
+ */
+ public function getLog() {
+ return self::$_log;
+ }
+
+ /**
+ * Returns the list of fields that can be imported
+ *
+ * @param bool $prefix
+ *
+ * @return array
+ */
+ public static function &import($prefix = FALSE) {
+ $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'search_display', $prefix, []);
+ return $r;
+ }
+
+ /**
+ * Returns the list of fields that can be exported
+ *
+ * @param bool $prefix
+ *
+ * @return array
+ */
+ public static function &export($prefix = FALSE) {
+ $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'search_display', $prefix, []);
+ return $r;
+ }
+
+ /**
+ * Returns the list of indices
+ *
+ * @param bool $localize
+ *
+ * @return array
+ */
+ public static function indices($localize = TRUE) {
+ $indices = [
+ 'UI_saved_search__id_name' => [
+ 'name' => 'UI_saved_search__id_name',
+ 'field' => [
+ 0 => 'saved_search_id',
+ 1 => 'name',
+ ],
+ 'localizable' => FALSE,
+ 'unique' => TRUE,
+ 'sig' => 'civicrm_search_display::1::saved_search_id::name',
+ ],
+ ];
+ return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Angular base page for search admin
+ */
+class CRM_Search_Page_Admin extends CRM_Core_Page {
+
+ public function run() {
+ $breadCrumb = [
+ 'title' => ts('Search Kit'),
+ 'url' => CRM_Utils_System::url('civicrm/admin/search', NULL, FALSE, '/list'),
+ ];
+ CRM_Utils_System::appendBreadCrumb([$breadCrumb]);
+
+ $schema = \Civi\Search\Admin::getSchema();
+
+ // If user does not have permission to search any entity, bye bye.
+ if (!$schema) {
+ CRM_Utils_System::permissionDenied();
+ }
+
+ // Add client-side vars for the search UI
+ $vars = [
+ 'schema' => $schema,
+ 'links' => \Civi\Search\Admin::getLinks(array_column($schema, 'name')),
+ ];
+
+ Civi::resources()
+ ->addBundle('bootstrap3')
+ ->addVars('search', $vars);
+
+ // Load angular module
+ $loader = new Civi\Angular\AngularLoader();
+ $loader->setPageName('civicrm/admin/search');
+ $loader->useApp([
+ 'defaultRoute' => '/list',
+ ]);
+ $loader->load();
+ parent::run();
+ }
+
+}
+++ /dev/null
-<?php
-
-class CRM_Search_Page_Ang extends CRM_Core_Page {
- /**
- * @var string[]
- */
- private $loadOptions = ['id', 'name', 'label', 'description', 'color', 'icon'];
-
- /**
- * @var array
- */
- private $schema = [];
-
- /**
- * @var string[]
- */
- private $allowedEntities = [];
-
- public function run() {
- $breadCrumb = [
- 'title' => ts('Search'),
- 'url' => CRM_Utils_System::url('civicrm/search'),
- ];
- CRM_Utils_System::appendBreadCrumb([$breadCrumb]);
-
- $this->getSchema();
-
- // If user does not have permission to search any entity, bye bye.
- if (!$this->allowedEntities) {
- CRM_Utils_System::permissionDenied();
- }
-
- // Add client-side vars for the search UI
- $vars = [
- 'operators' => CRM_Utils_Array::makeNonAssociative($this->getOperators()),
- 'schema' => $this->schema,
- 'links' => $this->getLinks(),
- 'loadOptions' => $this->loadOptions,
- 'actions' => $this->getActions(),
- 'functions' => CRM_Api4_Page_Api4Explorer::getSqlFunctions(),
- ];
-
- Civi::resources()
- ->addPermissions(['edit groups', 'administer reserved groups'])
- ->addBundle('bootstrap3')
- ->addVars('search', $vars);
-
- // Load angular module
- $loader = new Civi\Angular\AngularLoader();
- $loader->setModules(['search']);
- $loader->setPageName('civicrm/search');
- $loader->useApp([
- 'defaultRoute' => '/create/Contact',
- ]);
- $loader->load();
- parent::run();
- }
-
- /**
- * @return string[]
- */
- private function getOperators() {
- return [
- '=' => '=',
- '!=' => '≠',
- '>' => '>',
- '<' => '<',
- '>=' => '≥',
- '<=' => '≤',
- 'CONTAINS' => ts('Contains'),
- 'IN' => ts('Is In'),
- 'NOT IN' => ts('Not In'),
- 'LIKE' => ts('Is Like'),
- 'NOT LIKE' => ts('Not Like'),
- 'BETWEEN' => ts('Is Between'),
- 'NOT BETWEEN' => ts('Not Between'),
- 'IS NULL' => ts('Is Null'),
- 'IS NOT NULL' => ts('Not Null'),
- ];
- }
-
- /**
- * Populates $this->schema & $this->allowedEntities
- */
- private function getSchema() {
- $schema = \Civi\Api4\Entity::get()
- ->addSelect('name', 'title', 'titlePlural', 'description', 'icon')
- ->addWhere('name', '!=', 'Entity')
- ->addOrderBy('titlePlural')
- ->setChain([
- 'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']],
- ])->execute();
- $getFields = ['name', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'fk_entity'];
- foreach ($schema as $entity) {
- // Skip if entity doesn't have a 'get' action or the user doesn't have permission to use get
- if ($entity['get']) {
- // Get fields and pre-load options for certain prominent entities
- $loadOptions = in_array($entity['name'], ['Contact', 'Group']) ? $this->loadOptions : FALSE;
- if ($loadOptions) {
- $entity['optionsLoaded'] = TRUE;
- }
- $entity['fields'] = civicrm_api4($entity['name'], 'getFields', [
- 'select' => $getFields,
- 'where' => [['permission', 'IS NULL']],
- 'orderBy' => ['label'],
- 'loadOptions' => $loadOptions,
- ]);
- // Get the names of params this entity supports (minus some obvious ones)
- $params = $entity['get'][0];
- CRM_Utils_Array::remove($params, 'checkPermissions', 'debug', 'chain', 'language');
- unset($entity['get']);
- $this->schema[] = ['params' => array_keys($params)] + array_filter($entity);
- $this->allowedEntities[] = $entity['name'];
- }
- }
- }
-
- /**
- * @return array
- */
- private function getLinks() {
- $results = [];
- $keys = array_flip(['alias', 'entity', 'joinType']);
- foreach (civicrm_api4('Entity', 'getLinks', ['where' => [['entity', 'IN', $this->allowedEntities]]], ['entity' => 'links']) as $entity => $links) {
- $entityLinks = [];
- foreach ($links as $link) {
- if (!empty($link['entity']) && in_array($link['entity'], $this->allowedEntities)) {
- // Use entity.alias as array key to avoid duplicates
- $entityLinks[$link['entity'] . $link['alias']] = array_intersect_key($link, $keys);
- }
- }
- $results[$entity] = array_values($entityLinks);
- }
- return array_filter($results);
- }
-
- /**
- * @return array[]
- */
- private function getActions() {
- // Note: the placeholder %1 will be replaced with entity name on the clientside
- $actions = [
- 'export' => [
- 'title' => ts('Export %1'),
- 'icon' => 'fa-file-excel-o',
- 'entities' => array_keys(CRM_Export_BAO_Export::getComponents()),
- 'crmPopup' => [
- 'path' => "'civicrm/export/standalone'",
- 'query' => "{entity: entity, id: ids.join(',')}",
- ],
- ],
- 'update' => [
- 'title' => ts('Update %1'),
- 'icon' => 'fa-save',
- 'entities' => [],
- 'uiDialog' => ['templateUrl' => '~/search/crmSearchActions/crmSearchActionUpdate.html'],
- ],
- 'delete' => [
- 'title' => ts('Delete %1'),
- 'icon' => 'fa-trash',
- 'entities' => [],
- 'uiDialog' => ['templateUrl' => '~/search/crmSearchActions/crmSearchActionDelete.html'],
- ],
- ];
-
- // Check permissions for update & delete actions
- foreach ($this->allowedEntities as $entity) {
- $result = civicrm_api4($entity, 'getActions', [
- 'where' => [['name', 'IN', ['update', 'delete']]],
- ], ['name']);
- foreach ($result as $action) {
- // Contacts have their own delete action
- if (!($entity === 'Contact' && $action === 'delete')) {
- $actions[$action]['entities'][] = $entity;
- }
- }
- }
-
- // Add contact tasks which support standalone mode (with a 'url' property)
- $contactTasks = CRM_Contact_Task::permissionedTaskTitles(CRM_Core_Permission::getPermission());
- foreach (CRM_Contact_Task::tasks() as $id => $task) {
- if (isset($contactTasks[$id]) && !empty($task['url'])) {
- $actions['contact.' . $id] = [
- 'title' => $task['title'],
- 'entities' => ['Contact'],
- 'icon' => $task['icon'] ?? 'fa-gear',
- 'crmPopup' => [
- 'path' => "'{$task['url']}'",
- 'query' => "{cids: ids.join(',')}",
- ],
- ];
- }
- }
-
- return $actions;
- }
-
-}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Angular base page for search admin
+ */
+class CRM_Search_Page_Search extends CRM_Core_Page {
+
+ public function run() {
+
+ Civi::resources()->addBundle('bootstrap3');
+
+ // Load angular module
+ $loader = new Civi\Angular\AngularLoader();
+ $loader->setPageName('civicrm/search');
+ $loader->useApp();
+ $loader->load();
+
+ parent::run();
+ }
+
+}
->addValue('parent_id:name', 'Search')
->addValue('label', E::ts('Search Kit'))
->addValue('name', 'search_kit')
- ->addValue('url', 'civicrm/search')
+ ->addValue('url', 'civicrm/admin/search')
->addValue('icon', 'crm-i fa-search-plus')
->addValue('has_separator', 2)
->addValue('weight', 99)
->execute();
}
+ public function upgrade_1000() {
+ $this->ctx->log->info('Applying update 1000 - install schema.');
+ // For early, early adopters who installed the extension pre-beta
+ if (!CRM_Core_DAO::singleValueQuery("SHOW TABLES LIKE 'civicrm_search_display'")) {
+ $this->executeSqlFile('sql/auto_install.sql');
+ }
+ CRM_Core_DAO::executeQuery("UPDATE civicrm_navigation SET url = 'civicrm/admin/search', name = 'search_kit' WHERE url = 'civicrm/search'");
+ return TRUE;
+ }
+
}
--- /dev/null
+<?php
+namespace Civi\Api4;
+
+/**
+ * SearchDisplay entity.
+ *
+ * Provided by the Search Kit extension.
+ *
+ * @package Civi\Api4
+ */
+class SearchDisplay extends Generic\DAOEntity {
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class SearchDisplayCreationSpecProvider implements Generic\SpecProviderInterface {
+
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('name')->setRequired(FALSE)->setRequiredIf('empty($values.label)');
+ $spec->getFieldByName('label')->setRequired(FALSE)->setRequiredIf('empty($values.name)');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'SearchDisplay' && $action === 'create';
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Search;
+
+/**
+ * Class Tasks
+ * @package Civi\Search
+ */
+class Actions {
+
+ /**
+ * @return array
+ */
+ public static function getActionSettings():array {
+ return [
+ 'tasks' => self::getTasks(),
+ 'groupOptions' => self::getGroupOptions(),
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public static function getGroupOptions():array {
+ return \Civi\Api4\Group::getFields(FALSE)
+ ->setLoadOptions(['id', 'label'])
+ ->addWhere('name', 'IN', ['group_type', 'visibility'])
+ ->execute()
+ ->indexBy('name')
+ ->column('options');
+ }
+
+ /**
+ * @return array
+ */
+ public static function getTasks():array {
+ // Note: the placeholder %1 will be replaced with entity name on the clientside
+ $tasks = [
+ 'export' => [
+ 'title' => ts('Export %1'),
+ 'icon' => 'fa-file-excel-o',
+ 'entities' => array_keys(\CRM_Export_BAO_Export::getComponents()),
+ 'crmPopup' => [
+ 'path' => "'civicrm/export/standalone'",
+ 'query' => "{entity: entity, id: ids.join(',')}",
+ ],
+ ],
+ 'update' => [
+ 'title' => ts('Update %1'),
+ 'icon' => 'fa-save',
+ 'entities' => [],
+ 'uiDialog' => ['templateUrl' => '~/crmSearchActions/crmSearchActionUpdate.html'],
+ ],
+ 'delete' => [
+ 'title' => ts('Delete %1'),
+ 'icon' => 'fa-trash',
+ 'entities' => [],
+ 'uiDialog' => ['templateUrl' => '~/crmSearchActions/crmSearchActionDelete.html'],
+ ],
+ ];
+
+ // Add contact tasks which support standalone mode (with a 'url' property)
+ $contactTasks = \CRM_Contact_Task::permissionedTaskTitles(\CRM_Core_Permission::getPermission());
+ foreach (\CRM_Contact_Task::tasks() as $id => $task) {
+ if (isset($contactTasks[$id]) && !empty($task['url']) && $task['url'] !== 'civicrm/task/delete-contact') {
+ $tasks['contact.' . $id] = [
+ 'title' => $task['title'],
+ 'entities' => ['Contact'],
+ 'icon' => $task['icon'] ?? 'fa-gear',
+ 'crmPopup' => [
+ 'path' => "'{$task['url']}'",
+ 'query' => "{cids: ids.join(',')}",
+ ],
+ ];
+ }
+ }
+
+ return $tasks;
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Search;
+
+/**
+ * Class Admin
+ * @package Civi\Search
+ */
+class Admin {
+
+ /**
+ * @return array
+ */
+ public static function getAdminSettings():array {
+ return [
+ 'operators' => \CRM_Utils_Array::makeNonAssociative(self::getOperators()),
+ 'functions' => \CRM_Api4_Page_Api4Explorer::getSqlFunctions(),
+ 'displayTypes' => Display::getDisplayTypes(['name', 'label', 'description', 'icon']),
+ ];
+ }
+
+ /**
+ * @return string[]
+ */
+ public static function getOperators():array {
+ return [
+ '=' => '=',
+ '!=' => '≠',
+ '>' => '>',
+ '<' => '<',
+ '>=' => '≥',
+ '<=' => '≤',
+ 'CONTAINS' => ts('Contains'),
+ 'IN' => ts('Is In'),
+ 'NOT IN' => ts('Not In'),
+ 'LIKE' => ts('Is Like'),
+ 'NOT LIKE' => ts('Not Like'),
+ 'BETWEEN' => ts('Is Between'),
+ 'NOT BETWEEN' => ts('Not Between'),
+ 'IS NULL' => ts('Is Null'),
+ 'IS NOT NULL' => ts('Not Null'),
+ ];
+ }
+
+ /**
+ * Fetch all entities the current user has permission to `get`
+ * @return array
+ */
+ public static function getSchema() {
+ $schema = [];
+ $entities = \Civi\Api4\Entity::get()
+ ->addSelect('name', 'title', 'title_plural', 'description', 'icon', 'paths')
+ ->addWhere('name', '!=', 'Entity')
+ ->addOrderBy('title_plural')
+ ->setChain([
+ 'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']],
+ ])->execute();
+ $getFields = ['name', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'fk_entity'];
+ foreach ($entities as $entity) {
+ // Skip if entity doesn't have a 'get' action or the user doesn't have permission to use get
+ if ($entity['get']) {
+ // Add paths (but only RUD actions) with translated titles
+ foreach ($entity['paths'] as $action => $path) {
+ unset($entity['paths'][$action]);
+ switch ($action) {
+ case 'view':
+ $title = ts('View %1', [1 => $entity['title']]);
+ break;
+
+ case 'update':
+ $title = ts('Edit %1', [1 => $entity['title']]);
+ break;
+
+ case 'delete':
+ $title = ts('Delete %1', [1 => $entity['title']]);
+ break;
+
+ default:
+ continue 2;
+ }
+ $entity['paths'][] = [
+ 'path' => $path,
+ 'title' => $title,
+ 'action' => $action,
+ ];
+ }
+ $entity['fields'] = civicrm_api4($entity['name'], 'getFields', [
+ 'select' => $getFields,
+ 'where' => [['name', 'NOT IN', ['api_key', 'hash']]],
+ 'orderBy' => ['label'],
+ ]);
+ $params = $entity['get'][0];
+ // Entity must support at least these params or it is too weird for search kit
+ if (!array_diff(['select', 'where', 'orderBy', 'limit', 'offset'], array_keys($params))) {
+ \CRM_Utils_Array::remove($params, 'checkPermissions', 'debug', 'chain', 'language', 'select', 'where', 'orderBy', 'limit', 'offset');
+ unset($entity['get']);
+ $schema[] = ['params' => array_keys($params)] + array_filter($entity);
+ }
+ }
+ }
+ return $schema;
+ }
+
+ /**
+ * @param array $allowedEntities
+ * @return array
+ */
+ public static function getLinks(array $allowedEntities) {
+ $results = [];
+ $keys = array_flip(['alias', 'entity', 'joinType']);
+ foreach (civicrm_api4('Entity', 'getLinks', ['where' => [['entity', 'IN', $allowedEntities]]], ['entity' => 'links']) as $entity => $links) {
+ $entityLinks = [];
+ foreach ($links as $link) {
+ if (!empty($link['entity']) && in_array($link['entity'], $allowedEntities)) {
+ // Use entity.alias as array key to avoid duplicates
+ $entityLinks[$link['entity'] . $link['alias']] = array_intersect_key($link, $keys);
+ }
+ }
+ $results[$entity] = array_values($entityLinks);
+ }
+ return array_filter($results);
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Search;
+
+/**
+ * Class Display
+ * @package Civi\Search
+ */
+class Display {
+
+ /**
+ * @return array
+ */
+ public static function getPageSettings():array {
+ return [
+ 'displayTypes' => self::getDisplayTypes(['name']),
+ ];
+ }
+
+ /**
+ * @param array $props
+ * @return array
+ */
+ public static function getDisplayTypes(array $props):array {
+ try {
+ return \Civi\Api4\SearchDisplay::getFields(FALSE)
+ ->setLoadOptions($props)
+ ->addWhere('name', '=', 'type')
+ ->execute()
+ ->first()['options'];
+ }
+ catch (\Exception $e) {
+ return [];
+ }
+ }
+
+}
--- /dev/null
+<?php
+// Search actions module - provides dropdown menu with bulk actions to take on selected rows.
+return [
+ 'js' => [
+ 'ang/crmSearchActions.module.js',
+ 'ang/crmSearchActions/*.js',
+ 'ang/crmSearchActions/*/*.js',
+ ],
+ 'partials' => [
+ 'ang/crmSearchActions',
+ ],
+ 'basePages' => [],
+ 'requires' => ['crmUi', 'crmUtil', 'dialogService', 'api4', 'crmSearchKit'],
+ 'settingsFactory' => ['\Civi\Search\Actions', 'getActionSettings'],
+ 'permissions' => ['edit groups', 'administer reserved groups'],
+];
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ // Declare module
+ angular.module('crmSearchActions', CRM.angRequires('crmSearchActions'));
+
+})(angular, CRM.$, CRM._);
(function(angular, $, _) {
"use strict";
- angular.module('search').controller('SaveSmartGroup', function ($scope, $element, $timeout, crmApi4, dialogService, searchMeta) {
+ angular.module('crmSearchActions').controller('SaveSmartGroup', function ($scope, $element, $timeout, crmApi4, dialogService, searchMeta) {
var ts = $scope.ts = CRM.ts(),
- model = $scope.model,
- joins = _.pluck((model.api_params.join || []), 0),
- entityCount = {};
+ model = $scope.model;
$scope.groupEntityRefParams = {
entity: 'Group',
api: {
placeholder: ts('Select existing group')
}
};
- // Find all possible search columns that could serve as contact_id for the smart group
- $scope.columns = _.transform([model.api_entity].concat(joins), function(columns, joinExpr) {
- var joinName = joinExpr.split(' AS '),
- entityName = joinName[0],
- entity = searchMeta.getEntity(entityName),
- prefix = joinName[1] ? joinName[1] + '.' : '';
- _.each(entity.fields, function(field) {
- if ((entityName === 'Contact' && field.name === 'id') || field.fk_entity === 'Contact') {
- columns.push({
- id: prefix + field.name,
- text: entity.titlePlural + (entityCount[entityName] ? ' ' + entityCount[entityName] : '') + ': ' + field.label,
- icon: entity.icon
- });
- }
- });
- entityCount[entityName] = 1 + (entityCount[entityName] || 1);
- });
+ $scope.columns = searchMeta.getSmartGroupColumns(model.api_entity, model.api_params);
if (!$scope.columns.length) {
CRM.alert(ts('Cannot create smart group; search does not include any contacts.'), ts('Error'));
$scope.perm = {
administerReservedGroups: CRM.checkPerm('administer reserved groups')
};
- $scope.groupFields = _.indexBy(_.find(CRM.vars.search.schema, {name: 'Group'}).fields, 'name');
+ $scope.groupOptions = CRM.crmSearchActions.groupOptions;
$element.on('change', '#api-save-search-select-group', function() {
if ($(this).val()) {
$scope.$apply(function() {
(function(angular, $, _) {
"use strict";
- angular.module('search').controller('crmSearchActionDelete', function($scope, crmApi4, dialogService, searchMeta) {
+ angular.module('crmSearchActions').controller('crmSearchActionDelete', function($scope, crmApi4, dialogService) {
var ts = $scope.ts = CRM.ts(),
model = $scope.model,
ctrl = $scope.$ctrl = this;
- this.entity = searchMeta.getEntity(model.entity);
+ this.entityTitle = model.ids.length === 1 ? model.entityInfo.title : model.entityInfo.title_plural;
this.cancel = function() {
dialogService.cancel('crmSearchAction');
<div id="bootstrap-theme">
<div ng-controller="crmSearchActionDelete">
- <p>{{:: ts('Are you sure you want to delete %1 %2?', {1: model.ids.length, 2: $ctrl.entity.title}) }}</p>
+ <p><strong>{{:: ts('Are you sure you want to delete %1 %2?', {1: model.ids.length, 2: $ctrl.entityTitle}) }}</strong></p>
<hr />
<div class="buttons pull-right">
<button type="button" ng-click="$ctrl.cancel()" class="btn btn-danger">{{:: ts('Cancel') }}</button>
- <button ng-click="$ctrl.delete()" class="btn btn-primary">{{:: ts('Delete %1 %2', {1: model.ids.length, 2: $ctrl.entity.title}) }}</button>
+ <button ng-click="$ctrl.delete()" class="btn btn-primary">{{:: ts('Delete %1', {1: $ctrl.entityTitle}) }}</button>
</div>
</div>
</div>
(function(angular, $, _) {
"use strict";
- angular.module('search').controller('crmSearchActionUpdate', function ($scope, $timeout, crmApi4, dialogService, searchMeta) {
+ angular.module('crmSearchActions').controller('crmSearchActionUpdate', function ($scope, $timeout, crmApi4, dialogService) {
var ts = $scope.ts = CRM.ts(),
model = $scope.model,
ctrl = $scope.$ctrl = this;
- this.entity = searchMeta.getEntity(model.entity);
+ this.entityTitle = model.ids.length === 1 ? model.entityInfo.title : model.entityInfo.title_plural;
this.values = [];
this.add = null;
+ this.fields = null;
- function fieldInUse(fieldName) {
- return _.includes(_.collect(ctrl.values, 0), fieldName);
- }
+ crmApi4(model.entity, 'getFields', {action: 'update', loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon']})
+ .then(function(fields) {
+ ctrl.fields = fields;
+ });
this.updateField = function(index) {
// Debounce the onchange event using timeout
});
};
+ this.getField = function(fieldName) {
+ return _.where(ctrl.fields, {name: fieldName})[0];
+ };
+
+ function fieldInUse(fieldName) {
+ return _.includes(_.collect(ctrl.values, 0), fieldName);
+ }
+
this.availableFields = function() {
- var results = _.transform(ctrl.entity.fields, function(result, item) {
+ var results = _.transform(ctrl.fields, function(result, item) {
var formatted = {id: item.name, text: item.label, description: item.description};
if (fieldInUse(item.name)) {
formatted.disabled = true;
<div id="bootstrap-theme">
<div ng-controller="crmSearchActionUpdate">
+ <p><strong>{{:: ts('Update the %1 selected %2 with the following values:', {1: model.ids.length, 2: $ctrl.entityTitle}) }}</strong></p>
<div class="form-inline" ng-repeat="clause in $ctrl.values" >
<input class="form-control" ng-change="$ctrl.updateField($index)" ng-model="clause[0]" crm-ui-select="{data: $ctrl.availableFields, allowClear: true, placeholder: 'Field'}" />
- <input class="form-control" ng-model="clause[1]" crm-search-value="{field: clause[0]}" />
+ <input class="form-control" ng-model="clause[1]" crm-search-value="{field: $ctrl.getField(clause[0])}" />
</div>
<div class="form-inline">
- <input class="form-control twenty" style="width: 15em;" ng-model="$ctrl.add" ng-change="$ctrl.addField()" crm-ui-select="{data: $ctrl.availableFields, placeholder: ts('Add Value')}"/>
+ <input class="form-control twenty" style="width: 15em;" ng-model="$ctrl.add" ng-change="$ctrl.addField()" ng-disabled="!$ctrl.fields" ng-class="{loading: !$ctrl.fields}" crm-ui-select="{data: $ctrl.availableFields, placeholder: ts('Add Value')}"/>
</div>
<hr />
<div class="buttons pull-right">
<button type="button" ng-click="$ctrl.cancel()" class="btn btn-danger">{{:: ts('Cancel') }}</button>
- <button ng-click="$ctrl.save()" class="btn btn-primary" ng-disabled="!$ctrl.values.length">{{:: ts('Update %1 %2', {1: model.ids.length, 2: (model.ids.length === 1 ? $ctrl.entity.title : $ctrl.entity.titlePlural)}) }}</button>
+ <button ng-click="$ctrl.save()" class="btn btn-primary" ng-disabled="!$ctrl.values.length">{{:: ts('Update %1', {1: $ctrl.entityTitle}) }}</button>
</div>
</div>
</div>
(function(angular, $, _) {
"use strict";
- angular.module('search').component('crmSearchActions', {
+ angular.module('crmSearchActions').component('crmSearchActions', {
bindings: {
entity: '<',
refresh: '&',
ids: '<'
},
- templateUrl: '~/search/crmSearchActions.html',
- controller: function($scope, crmApi4, dialogService, searchMeta) {
+ templateUrl: '~/crmSearchActions/crmSearchActions.html',
+ controller: function($scope, crmApi4, dialogService) {
var ts = $scope.ts = CRM.ts(),
- ctrl = this;
+ ctrl = this,
+ initialized = false,
+ unwatchIDs = $scope.$watch('$ctrl.ids.length', watchIDs);
- this.$onInit = function() {
- var entityTitle = searchMeta.getEntity(ctrl.entity).titlePlural;
- if (!ctrl.actions) {
- var actions = _.transform(_.cloneDeep(CRM.vars.search.actions), function (actions, action) {
+ function watchIDs() {
+ if (ctrl.ids && ctrl.ids.length && !initialized) {
+ unwatchIDs();
+ initialized = true;
+ initialize();
+ }
+ }
+
+ function initialize() {
+ crmApi4({
+ entityInfo: ['Entity', 'get', {select: ['name', 'title', 'title_plural'], where: [['name', '=', ctrl.entity]]}, 0],
+ allowed: [ctrl.entity, 'getActions', {where: [['name', 'IN', ['update', 'delete']]]}, ['name']]
+ }).then(function(result) {
+ ctrl.entityInfo = result.entityInfo;
+ _.each(result.allowed, function(action) {
+ CRM.crmSearchActions.tasks[action].entities.push(ctrl.entity);
+ });
+ var actions = _.transform(_.cloneDeep(CRM.crmSearchActions.tasks), function(actions, action) {
if (_.includes(action.entities, ctrl.entity)) {
- action.title = action.title.replace('%1', entityTitle);
+ action.title = action.title.replace('%1', ctrl.entityInfo.title_plural);
actions.push(action);
}
}, []);
ctrl.actions = _.sortBy(actions, 'title');
- }
- };
+ });
+ }
this.isActionAllowed = function(action) {
return !action.number || $scope.eval('' + $ctrl.ids.length + action.number);
}
var data = {
ids: ctrl.ids,
- entity: ctrl.entity
+ entity: ctrl.entity,
+ entityInfo: ctrl.entityInfo
};
// If action uses a crmPopup form
if (action.crmPopup) {
<div class="btn-group" title="{{:: ts('Perform action on selected items.') }}">
- <button type="button" ng-disabled="!$ctrl.ids.length" ng-click="$ctrl.init()" class="btn form-control dropdown-toggle btn-default" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ <button type="button" ng-disabled="!$ctrl.ids.length" class="btn form-control dropdown-toggle btn-default" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{:: ts('Action') }} <span class="caret"></span>
</button>
<ul class="dropdown-menu" ng-if=":: $ctrl.actions">
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('crmSearchActions').directive('saveSmartGroup', function() {
+ return {
+ bindToController: {
+ load: '<',
+ entity: '<',
+ params: '<'
+ },
+ restrict: 'A',
+ controller: function ($scope, $element, dialogService) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ $scope.saveGroup = function () {
+ var model = {
+ title: '',
+ description: '',
+ visibility: 'User and User Admin Only',
+ group_type: [],
+ id: ctrl.load ? ctrl.load.id : null,
+ api_entity: ctrl.entity,
+ api_params: _.cloneDeep(angular.extend({}, ctrl.params, {version: 4}))
+ };
+ delete model.api_params.orderBy;
+ if (ctrl.load && ctrl.load.api_params && ctrl.load.api_params.select && ctrl.load.api_params.select[0]) {
+ model.api_params.select.unshift(ctrl.load.api_params.select[0]);
+ }
+ var options = CRM.utils.adjustDialogDefaults({
+ autoOpen: false,
+ title: ts('Save smart group')
+ });
+ dialogService.open('saveSearchDialog', '~/crmSearchActions/saveSmartGroup.html', model, options);
+ };
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
<textarea class="form-control" ng-model="model.description"></textarea>
<div class="form-inline">
<label>{{:: ts('Group Type:') }} </label>
- <div class="checkbox" ng-repeat="option in groupFields.group_type.options track by option.id">
+ <div class="checkbox" ng-repeat="option in groupOptions.group_type track by option.id">
<label>
<input type="checkbox" checklist-model="model.group_type" checklist-value="option.id">
{{ option.label }}
</div>
<div class="form-inline">
<label>{{:: ts('Visibility:') }}</label>
- <select class="form-control" ng-model="model.visibility" ng-options="item.id as item.label for item in groupFields.visibility.options track by item.id" crm-ui-select></select>
+ <select class="form-control" ng-model="model.visibility" ng-options="item.id as item.label for item in groupOptions.visibility track by item.id" crm-ui-select></select>
</div>
<hr />
<div class="buttons pull-right">
--- /dev/null
+<?php
+// Search Admin module - for composing & saving searches & displays.
+return [
+ 'js' => [
+ 'ang/crmSearchAdmin.module.js',
+ 'ang/crmSearchAdmin/*.js',
+ 'ang/crmSearchAdmin/*/*.js',
+ ],
+ 'css' => [
+ 'css/*.css',
+ ],
+ 'partials' => [
+ 'ang/crmSearchAdmin',
+ ],
+ 'basePages' => ['civicrm/admin/search'],
+ 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'crmSearchDisplay', 'crmSearchActions', 'crmSearchKit'],
+ 'settingsFactory' => ['\Civi\Search\Admin', 'getAdminSettings'],
+];
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ // Shared between router and searchMeta service
+ var searchEntity,
+ undefined;
+
+ // Declare module and route/controller/services
+ angular.module('crmSearchAdmin', CRM.angRequires('crmSearchAdmin'))
+
+ .config(function($routeProvider) {
+ $routeProvider.when('/list', {
+ controller: 'searchList',
+ templateUrl: '~/crmSearchAdmin/searchList.html',
+ resolve: {
+ // Load data for lists
+ savedSearches: function(crmApi4) {
+ return crmApi4('SavedSearch', 'get', {
+ select: [
+ 'id',
+ 'name',
+ 'label',
+ 'api_entity',
+ 'form_values',
+ 'GROUP_CONCAT(display.name ORDER BY display.id) AS display_name',
+ 'GROUP_CONCAT(display.label ORDER BY display.id) AS display_label',
+ 'GROUP_CONCAT(display.type:icon ORDER BY display.id) AS display_icon',
+ 'GROUP_CONCAT(group.title) AS groups'
+ ],
+ join: [['SearchDisplay AS display'], ['Group AS group']],
+ where: [['api_entity', 'IS NOT NULL']],
+ groupBy: ['id']
+ });
+ }
+ }
+ });
+ $routeProvider.when('/create/:entity', {
+ controller: 'searchCreate',
+ template: '<crm-search-admin saved-search="$ctrl.savedSearch"></crm-search-admin>',
+ });
+ $routeProvider.when('/edit/:id', {
+ controller: 'searchEdit',
+ template: '<crm-search-admin saved-search="$ctrl.savedSearch"></crm-search-admin>',
+ resolve: {
+ // Load saved search
+ savedSearch: function($route, crmApi4) {
+ var params = $route.current.params;
+ return crmApi4('SavedSearch', 'get', {
+ where: [['id', '=', params.id]],
+ chain: {
+ groups: ['Group', 'get', {select: ['id', 'title', 'description', 'visibility', 'group_type'], where: [['saved_search_id', '=', '$id']]}],
+ displays: ['SearchDisplay', 'get', {where: [['saved_search_id', '=', '$id']]}]
+ }
+ }, 0);
+ }
+ }
+ });
+ })
+
+ // Controller for creating a new search
+ .controller('searchCreate', function($scope, $routeParams, $location) {
+ searchEntity = $routeParams.entity;
+ $scope.$ctrl = this;
+ this.savedSearch = {
+ api_entity: searchEntity,
+ };
+ // Changing entity will refresh the angular page
+ $scope.$watch('$ctrl.savedSearch.api_entity', function(newEntity, oldEntity) {
+ if (newEntity && oldEntity && newEntity !== oldEntity) {
+ $location.url('/create/' + newEntity);
+ }
+ });
+ })
+
+ // Controller for editing a SavedSearch
+ .controller('searchEdit', function($scope, savedSearch) {
+ searchEntity = savedSearch.api_entity;
+ this.savedSearch = savedSearch;
+ $scope.$ctrl = this;
+ })
+
+ .factory('searchMeta', function() {
+ function getEntity(entityName) {
+ if (entityName) {
+ return _.find(CRM.vars.search.schema, {name: entityName});
+ }
+ }
+ function getField(fieldName, entityName) {
+ var dotSplit = fieldName.split('.'),
+ joinEntity = dotSplit.length > 1 ? dotSplit[0] : null,
+ name = _.last(dotSplit).split(':')[0],
+ field;
+ // Custom fields contain a dot in their fieldname
+ // If 3 segments, the first is the joinEntity and the last 2 are the custom field
+ if (dotSplit.length === 3) {
+ name = dotSplit[1] + '.' + name;
+ }
+ // If 2 segments, it's ambiguous whether this is a custom field or joined field. Search the main entity first.
+ if (dotSplit.length === 2) {
+ field = _.find(getEntity(entityName).fields, {name: dotSplit[0] + '.' + name});
+ if (field) {
+ field.entity = entityName;
+ return field;
+ }
+ }
+ if (joinEntity) {
+ entityName = _.find(CRM.vars.search.links[entityName], {alias: joinEntity}).entity;
+ }
+ field = _.find(getEntity(entityName).fields, {name: name});
+ if (field) {
+ field.entity = entityName;
+ return field;
+ }
+ }
+ return {
+ getEntity: getEntity,
+ getField: getField,
+ parseExpr: function(expr) {
+ var result = {fn: null, modifier: ''},
+ fieldName = expr,
+ bracketPos = expr.indexOf('(');
+ if (bracketPos >= 0) {
+ var parsed = expr.substr(bracketPos).match(/[ ]?([A-Z]+[ ]+)?([\w.:]+)/);
+ fieldName = parsed[2];
+ result.fn = _.find(CRM.crmSearchAdmin.functions, {name: expr.substring(0, bracketPos)});
+ result.modifier = _.trim(parsed[1]);
+ }
+ result.field = expr ? getField(fieldName, searchEntity) : undefined;
+ if (result.field) {
+ var split = fieldName.split(':'),
+ prefixPos = split[0].lastIndexOf(result.field.name);
+ result.path = split[0];
+ result.prefix = prefixPos > 0 ? result.path.substring(0, prefixPos) : '';
+ result.suffix = !split[1] ? '' : ':' + split[1];
+ }
+ return result;
+ },
+ // Find all possible search columns that could serve as contact_id for a smart group
+ getSmartGroupColumns: function(api_entity, api_params) {
+ var joins = _.pluck((api_params.join || []), 0),
+ entityCount = {};
+ return _.transform([api_entity].concat(joins), function(columns, joinExpr) {
+ var joinName = joinExpr.split(' AS '),
+ entityName = joinName[0],
+ entity = getEntity(entityName),
+ prefix = joinName[1] ? joinName[1] + '.' : '';
+ _.each(entity.fields, function(field) {
+ if ((entityName === 'Contact' && field.name === 'id') || field.fk_entity === 'Contact') {
+ columns.push({
+ id: prefix + field.name,
+ text: entity.title_plural + (entityCount[entityName] ? ' ' + entityCount[entityName] : '') + ': ' + field.label,
+ icon: entity.icon
+ });
+ }
+ });
+ entityCount[entityName] = 1 + (entityCount[entityName] || 1);
+ });
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
{{:: ts('Auto') }}
</button>
</div>
- <crm-search-actions entity="$ctrl.entity" ids="$ctrl.selectedRows" refresh="$ctrl.refreshPage()"></crm-search-actions>
- <div class="btn-group pull-right">
- <button type="button" class="btn btn-default form-control dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- <i class="crm-i fa-save"></i> {{:: ts('Create')}}
- <span class="caret"></span>
- </button>
- <ul class="dropdown-menu">
- <li ng-if=":: $ctrl.perm.editGroups">
- <a href ng-click="saveGroup()">{{:: ts('Smart Group') }}</a>
- </li>
- </ul>
- </div>
+ <crm-search-actions entity="$ctrl.savedSearch.api_entity" ids="$ctrl.selectedRows" refresh="$ctrl.refreshPage()"></crm-search-actions>
</div>
<div class="crm-flex-box">
<div>
- <div class="form-inline">
- <label for="crm-search-main-entity">{{:: ts('Search for') }}</label>
- <input id="crm-search-main-entity" class="form-control" ng-model="$ctrl.entity" crm-ui-select="::{allowClear: false, data: entities}" />
- </div>
<div ng-if=":: $ctrl.paramExists('join')">
- <fieldset ng-repeat="join in $ctrl.params.join">
+ <fieldset ng-repeat="join in $ctrl.savedSearch.api_params.join">
<div class="form-inline">
<label for="crm-search-join-{{ $index }}">{{:: ts('With') }}</label>
<input id="crm-search-join-{{ $index }}" class="form-control" ng-model="join[0]" crm-ui-select="{placeholder: ' ', data: getJoinEntities}" ng-change="changeJoin($index)" />
</fieldset>
</div>
<fieldset ng-if=":: $ctrl.paramExists('groupBy')">
- <div class="form-inline" ng-repeat="groupBy in $ctrl.params.groupBy">
+ <div class="form-inline" ng-repeat="groupBy in $ctrl.savedSearch.api_params.groupBy">
<label for="crm-search-groupBy-{{ $index }}">{{:: ts('Group By') }}</label>
- <input id="crm-search-groupBy-{{ $index }}" class="form-control" ng-model="$ctrl.params.groupBy[$index]" crm-ui-select="{placeholder: ' ', data: fieldsForGroupBy}" ng-change="changeGroupBy($index)" />
+ <input id="crm-search-groupBy-{{ $index }}" class="form-control" ng-model="$ctrl.savedSearch.api_params.groupBy[$index]" crm-ui-select="{placeholder: ' ', data: fieldsForGroupBy}" ng-change="changeGroupBy($index)" />
<hr>
</div>
<div class="form-inline">
<input id="crm-search-add-groupBy" class="form-control crm-action-menu fa-plus" ng-model="controls.groupBy" crm-ui-select="{placeholder: ts('Group By'), data: fieldsForGroupBy}" ng-change="addParam('groupBy')"/>
</div>
- <fieldset id="crm-search-build-group-aggregate" ng-if="$ctrl.params.groupBy.length" class="crm-collapsible collapsed">
+ <fieldset id="crm-search-build-group-aggregate" ng-if="$ctrl.savedSearch.api_params.groupBy.length" class="crm-collapsible collapsed">
<legend class="collapsible-title">{{:: ts('Aggregate fields') }}</legend>
<div>
- <fieldset ng-repeat="col in $ctrl.params.select" ng-if="$ctrl.canAggregate(col)">
- <crm-search-function expr="$ctrl.params.select[$index]" cat="'aggregate'"></crm-search-function>
+ <fieldset ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-if="$ctrl.canAggregate(col)">
+ <crm-search-function expr="$ctrl.savedSearch.api_params.select[$index]" cat="'aggregate'"></crm-search-function>
</fieldset>
</div>
</fieldset>
</fieldset>
</div>
<div>
- <div class="navbar-form clearfix" ng-if="$ctrl.load">
- <div class="form-group pull-right">
- <label>{{ $ctrl.load.title }}</label>
- <button class="btn btn-default" ng-disabled="$ctrl.load.saved" ng-click="saveGroup()">{{ $ctrl.load.saved ? ts('Saved') : ts('Save') }}</button>
- </div>
- </div>
<fieldset class="api4-clause-fieldset">
- <crm-search-clause clauses="$ctrl.params.where" format="string" op="AND" label="{{ ts('Where') }}" fields="fieldsForWhere" ></crm-search-clause>
+ <crm-search-clause clauses="$ctrl.savedSearch.api_params.where" format="string" op="AND" label="{{ ts('Where') }}" fields="fieldsForWhere" ></crm-search-clause>
</fieldset>
- <fieldset ng-if="$ctrl.paramExists('having') && $ctrl.params.groupBy.length" class="api4-clause-fieldset">
- <crm-search-clause clauses="$ctrl.params.having" format="string" op="AND" label="{{ ts('Filter') }}" fields="fieldsForHaving" ></crm-search-clause>
+ <fieldset ng-if="$ctrl.paramExists('having') && $ctrl.savedSearch.api_params.groupBy.length" class="api4-clause-fieldset">
+ <crm-search-clause clauses="$ctrl.savedSearch.api_params.having" format="string" op="AND" label="{{ ts('Filter') }}" fields="fieldsForHaving" ></crm-search-clause>
</fieldset>
</div>
</div>
<table>
<thead>
- <tr ng-model="$ctrl.params.select" ui-sortable="{axis: 'x'}">
+ <tr ng-model="$ctrl.savedSearch.api_params.select" ui-sortable="sortableColumnOptions">
<th class="crm-search-result-select">
<input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" ng-disabled="!(loading === false && !loadingAllRows && $ctrl.results[$ctrl.page] && $ctrl.results[$ctrl.page][0].id)">
</th>
- <th ng-repeat="col in $ctrl.params.select" ng-click="setOrderBy(col, $event)" title="{{:: ts('Drag to reorder columns, click to sort results (shift-click to sort by multiple).')}}">
+ <th ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-click="setOrderBy(col, $event)" title="{{:: ts('Drag to reorder columns, click to sort results (shift-click to sort by multiple).') }}">
<i class="crm-i {{ getOrderBy(col) }}"></i>
- <span>{{ $ctrl.getFieldLabel(col) }}</span>
- <a href class="crm-hover-button" title="{{:: ts('Clear') }}" ng-click="$ctrl.clearParam('select', $index)"><i class="crm-i fa-times" aria-hidden="true"></i></a>
+ <span ng-class="{'crm-draggable': $index || !$ctrl.groupExists}">{{ $ctrl.getFieldLabel(col) }}</span>
+ <span ng-switch="$index || !$ctrl.groupExists ? 'sortable' : 'locked'">
+ <i ng-switch-when="locked" class="crm-i fa-lock" aria-hidden="true"></i>
+ <a href ng-switch-default class="crm-hover-button" title="{{:: ts('Clear') }}" ng-click="$ctrl.clearParam('select', $index)"><i class="crm-i fa-times" aria-hidden="true"></i></a>
+ </span>
</th>
<th class="form-inline">
<input class="form-control crm-action-menu fa-plus" ng-model="controls.select" crm-ui-select="::{data: fieldsForSelect, placeholder: ts('Add')}" ng-change="addParam('select')">
<td>
<input type="checkbox" ng-checked="isRowSelected(row)" ng-click="selectRow(row)" ng-disabled="!(loading === false && !loadingAllRows && row.id)">
</td>
- <td ng-repeat="col in $ctrl.params.select">
- {{ formatResult(row, col) }}
- </td>
+ <td ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-bind-html="formatResult(row, col)"></td>
<td></td>
</tr>
</tbody>
(function(angular, $, _) {
"use strict";
- angular.module('search').component('crmSearch', {
+ angular.module('crmSearchAdmin').component('crmSearchAdmin', {
bindings: {
- entity: '=',
- load: '<'
+ savedSearch: '<'
},
- templateUrl: '~/search/crmSearch.html',
+ templateUrl: '~/crmSearchAdmin/crmSearchAdmin.html',
controller: function($scope, $element, $timeout, crmApi4, dialogService, searchMeta, formatForSelect2) {
var ts = $scope.ts = CRM.ts(),
ctrl = this;
this.selectedRows = [];
this.limit = CRM.cache.get('searchPageSize', 30);
this.page = 1;
- this.params = {};
+ this.displayTypes = _.indexBy(CRM.crmSearchAdmin.displayTypes, 'name');
// After a search this.results is an object of result arrays keyed by page,
// Initially this.results is an empty string because 1: it's falsey (unlike an empty object) and 2: it doesn't throw an error if you try to access undefined properties (unlike null)
this.results = '';
this.rowCount = false;
+ this.allRowsSelected = false;
// Have the filters (WHERE, HAVING, GROUP BY, JOIN) changed?
this.stale = true;
- this.allRowsSelected = false;
- $scope.controls = {};
+ $scope.controls = {tab: 'compose'};
$scope.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}];
- $scope.entities = formatForSelect2(CRM.vars.search.schema, 'name', 'titlePlural', ['description', 'icon']);
+ $scope.groupOptions = CRM.crmSearchActions.groupOptions;
+ $scope.entities = formatForSelect2(CRM.vars.search.schema, 'name', 'title_plural', ['description', 'icon']);
this.perm = {
editGroups: CRM.checkPerm('edit groups')
};
- this.getEntity = searchMeta.getEntity;
+ this.$onInit = function() {
+ this.entityTitle = searchMeta.getEntity(this.savedSearch.api_entity).title_plural;
+
+ this.savedSearch.displays = this.savedSearch.displays || [];
+ this.savedSearch.groups = this.savedSearch.groups || [];
+ this.groupExists = !!this.savedSearch.groups.length;
+
+ if (!this.savedSearch.api_params) {
+ this.savedSearch.api_params = {
+ version: 4,
+ select: getDefaultSelect(),
+ orderBy: {},
+ where: [],
+ };
+ }
+
+ $scope.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect);
+
+ $scope.$watch('$ctrl.savedSearch.api_params.where', onChangeFilters, true);
+
+ if (this.paramExists('groupBy')) {
+ this.savedSearch.api_params.groupBy = this.savedSearch.api_params.groupBy || [];
+ $scope.$watchCollection('$ctrl.savedSearch.api_params.groupBy', onChangeFilters);
+ }
+
+ if (this.paramExists('join')) {
+ this.savedSearch.api_params.join = this.savedSearch.api_params.join || [];
+ $scope.$watch('$ctrl.savedSearch.api_params.join', onChangeFilters, true);
+ }
+
+ if (this.paramExists('having')) {
+ this.savedSearch.api_params.having = this.savedSearch.api_params.having || [];
+ $scope.$watch('$ctrl.savedSearch.api_params.having', onChangeFilters, true);
+ }
+
+ $scope.$watch('$ctrl.savedSearch', onChangeAnything, true);
+
+ // After watcher runs for the first time and messes up the status, set it correctly
+ $timeout(function() {
+ $scope.status = ctrl.savedSearch && ctrl.savedSearch.id ? 'saved' : 'unsaved';
+ });
+
+ loadFieldOptions();
+ };
+
+ function onChangeAnything() {
+ $scope.status = 'unsaved';
+ }
+
+ this.save = function() {
+ if (!validate()) {
+ return;
+ }
+ $scope.status = 'saving';
+ var params = _.cloneDeep(ctrl.savedSearch),
+ apiCalls = {},
+ chain = {};
+ if (ctrl.groupExists) {
+ chain.groups = ['Group', 'save', {defaults: {saved_search_id: '$id'}, records: params.groups}];
+ delete params.groups;
+ } else if (params.id) {
+ apiCalls.deleteGroup = ['Group', 'delete', {where: [['saved_search_id', '=', params.id]]}];
+ }
+ if (params.displays && params.displays.length) {
+ chain.displays = ['SearchDisplay', 'replace', {where: [['saved_search_id', '=', '$id']], records: params.displays}];
+ } else if (params.id) {
+ apiCalls.deleteDisplays = ['SearchDisplay', 'delete', {where: [['saved_search_id', '=', params.id]]}];
+ }
+ delete params.displays;
+ apiCalls.saved = ['SavedSearch', 'save', {records: [params], chain: chain}, 0];
+ crmApi4(apiCalls).then(function(results) {
+ // Set new status to saved unless the user changed something in the interim
+ var newStatus = $scope.status === 'unsaved' ? 'unsaved' : 'saved';
+ ctrl.savedSearch.id = results.saved.id;
+ if (results.saved.groups && results.saved.groups.length) {
+ ctrl.savedSearch.groups[0].id = results.saved.groups[0].id;
+ }
+ ctrl.savedSearch.displays = results.saved.displays || [];
+ // Wait until after onChangeAnything to update status
+ $timeout(function() {
+ $scope.status = newStatus;
+ });
+ });
+ };
this.paramExists = function(param) {
- return _.includes(searchMeta.getEntity(ctrl.entity).params, param);
+ return _.includes(searchMeta.getEntity(ctrl.savedSearch.api_entity).params, param);
+ };
+
+ this.addDisplay = function(type) {
+ ctrl.savedSearch.displays.push({
+ type: type,
+ label: ''
+ });
+ $scope.selectTab('display_' + (ctrl.savedSearch.displays.length - 1));
+ };
+
+ this.removeDisplay = function(index) {
+ var display = ctrl.savedSearch.displays[index];
+ if (display.id) {
+ display.trashed = !display.trashed;
+ if ($scope.controls.tab === ('display_' + index) && display.trashed) {
+ $scope.selectTab('compose');
+ } else if (!display.trashed) {
+ $scope.selectTab('display_' + index);
+ }
+ } else {
+ $scope.selectTab('compose');
+ ctrl.savedSearch.displays.splice(index, 1);
+ }
+ };
+
+ this.addGroup = function() {
+ ctrl.savedSearch.groups.push({
+ title: '',
+ description: '',
+ visibility: 'User and User Admin Only',
+ group_type: []
+ });
+ ctrl.groupExists = true;
+ $scope.selectTab('group');
+ };
+
+ $scope.selectTab = function(tab) {
+ if (tab === 'group') {
+ $scope.smartGroupColumns = searchMeta.getSmartGroupColumns(ctrl.savedSearch.api_entity, ctrl.savedSearch.api_params);
+ var smartGroupColumns = _.map($scope.smartGroupColumns, 'id');
+ if (smartGroupColumns.length && !_.includes(smartGroupColumns, ctrl.savedSearch.api_params.select[0])) {
+ ctrl.savedSearch.api_params.select.unshift(smartGroupColumns[0]);
+ }
+ }
+ ctrl.savedSearch.api_params.select = _.uniq(ctrl.savedSearch.api_params.select);
+ $scope.controls.tab = tab;
+ };
+
+ this.removeGroup = function() {
+ ctrl.groupExists = !ctrl.groupExists;
+ if (!ctrl.groupExists && (!ctrl.savedSearch.groups.length || !ctrl.savedSearch.groups[0].id)) {
+ ctrl.savedSearch.groups.length = 0;
+ }
+ if ($scope.controls.tab === 'group') {
+ $scope.selectTab('compose');
+ }
};
$scope.getJoinEntities = function() {
- var joinEntities = _.transform(CRM.vars.search.links[ctrl.entity], function(joinEntities, link) {
+ var joinEntities = _.transform(CRM.vars.search.links[ctrl.savedSearch.api_entity], function(joinEntities, link) {
var entity = searchMeta.getEntity(link.entity);
if (entity) {
joinEntities.push({
id: link.entity + ' AS ' + link.alias,
- text: entity.titlePlural,
+ text: entity.title_plural,
description: '(' + link.alias + ')',
icon: entity.icon
});
// Debounce the onchange event using timeout
$timeout(function() {
if ($scope.controls.join) {
- ctrl.params.join = ctrl.params.join || [];
- ctrl.params.join.push([$scope.controls.join, false]);
+ ctrl.savedSearch.api_params.join = ctrl.savedSearch.api_params.join || [];
+ ctrl.savedSearch.api_params.join.push([$scope.controls.join, false]);
loadFieldOptions();
}
$scope.controls.join = '';
};
$scope.changeJoin = function(idx) {
- if (ctrl.params.join[idx][0]) {
- ctrl.params.join[idx].length = 2;
+ if (ctrl.savedSearch.api_params.join[idx][0]) {
+ ctrl.savedSearch.api_params.join[idx].length = 2;
loadFieldOptions();
} else {
ctrl.clearParam('join', idx);
};
$scope.changeGroupBy = function(idx) {
- if (!ctrl.params.groupBy[idx]) {
+ if (!ctrl.savedSearch.api_params.groupBy[idx]) {
ctrl.clearParam('groupBy', idx);
}
// Remove aggregate functions when no grouping
- if (!ctrl.params.groupBy.length) {
- _.each(ctrl.params.select, function(col, pos) {
+ if (!ctrl.savedSearch.api_params.groupBy.length) {
+ _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
if (_.contains(col, '(')) {
var info = searchMeta.parseExpr(col);
if (info.fn.category === 'aggregate') {
- ctrl.params.select[pos] = info.path + info.suffix;
+ ctrl.savedSearch.api_params.select[pos] = info.path + info.suffix;
}
}
});
}
};
+ function validate() {
+ var errors = [],
+ errorEl,
+ label,
+ tab;
+ if (!ctrl.savedSearch.label) {
+ errorEl = '#crm-saved-search-label';
+ label = ts('Search Label');
+ errors.push(ts('%1 is a required field.', {1: label}));
+ }
+ if (ctrl.groupExists && !ctrl.savedSearch.groups[0].title) {
+ errorEl = '#crm-search-admin-group-title';
+ label = ts('Group Title');
+ errors.push(ts('%1 is a required field.', {1: label}));
+ tab = 'group';
+ }
+ _.each(ctrl.savedSearch.displays, function(display, index) {
+ if (!display.trashed && !display.label) {
+ errorEl = '#crm-search-admin-display-label';
+ label = ts('Display Label');
+ errors.push(ts('%1 is a required field.', {1: label}));
+ tab = 'display_' + index;
+ }
+ });
+ if (errors.length) {
+ if (tab) {
+ $scope.selectTab(tab);
+ }
+ $(errorEl).crmError(errors.join('<br>'), ts('Error Saving'), {expires: 5000});
+ }
+ return !errors.length;
+ }
+
/**
* Called when clicking on a column header
* @param col
$scope.setOrderBy = function(col, $event) {
var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC';
if (!$event.shiftKey) {
- ctrl.params.orderBy = {};
+ ctrl.savedSearch.api_params.orderBy = {};
}
- ctrl.params.orderBy[col] = dir;
+ ctrl.savedSearch.api_params.orderBy[col] = dir;
if (ctrl.results) {
ctrl.refreshPage();
}
* @returns {string}
*/
$scope.getOrderBy = function(col) {
- var dir = ctrl.params.orderBy && ctrl.params.orderBy[col];
+ var dir = ctrl.savedSearch.api_params.orderBy && ctrl.savedSearch.api_params.orderBy[col];
if (dir) {
return 'fa-sort-' + dir.toLowerCase();
}
};
$scope.addParam = function(name) {
- if ($scope.controls[name] && !_.contains(ctrl.params[name], $scope.controls[name])) {
- ctrl.params[name].push($scope.controls[name]);
+ if ($scope.controls[name] && !_.contains(ctrl.savedSearch.api_params[name], $scope.controls[name])) {
+ ctrl.savedSearch.api_params[name].push($scope.controls[name]);
if (name === 'groupBy') {
// Expand the aggregate block
$timeout(function() {
// Deletes an item from an array param
this.clearParam = function(name, idx) {
- ctrl.params[name].splice(idx, 1);
+ ctrl.savedSearch.api_params[name].splice(idx, 1);
};
// Prevent visual jumps in results table height during loading
// Ensure all non-grouped columns are aggregated if using GROUP BY
function aggregateGroupByColumns() {
- if (ctrl.params.groupBy.length) {
- _.each(ctrl.params.select, function(col, pos) {
+ if (ctrl.savedSearch.api_params.groupBy.length) {
+ _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
if (!_.contains(col, '(') && ctrl.canAggregate(col)) {
- ctrl.params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(' + col + ')';
+ ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(' + col + ')';
}
});
}
// Debounced callback for loadResults
function _loadResultsCallback() {
// Multiply limit to read 2 pages at once & save ajax requests
- var params = angular.merge({debug: true, limit: ctrl.limit * 2}, ctrl.params);
+ var params = _.merge(_.cloneDeep(ctrl.savedSearch.api_params), {debug: true, limit: ctrl.limit * 2});
+ // Select the ids of joined entities (helps with displaying links)
+ _.each(params.join, function(join) {
+ var idField = join[0].split(' AS ')[1] + '.id';
+ if (!_.includes(params.select, idField) && !ctrl.canAggregate(idField)) {
+ params.select.push(idField);
+ }
+ });
lockTableHeight();
$scope.error = false;
if (ctrl.stale) {
params.select.push('row_count');
}
params.offset = ctrl.limit * (ctrl.page - 1);
- crmApi4(ctrl.entity, 'get', params).then(function(success) {
+ crmApi4(ctrl.savedSearch.api_entity, 'get', params).then(function(success) {
if (ctrl.stale) {
ctrl.results = {};
}
})
.finally(function() {
if (ctrl.debug) {
- ctrl.debug.params = JSON.stringify(_.extend({version: 4}, ctrl.params), null, 2);
+ ctrl.debug.params = JSON.stringify(params, null, 2);
if (ctrl.debug.timeIndex) {
ctrl.debug.timeIndex = Number.parseFloat(ctrl.debug.timeIndex).toPrecision(2);
}
function onChangeSelect(newSelect, oldSelect) {
// When removing a column from SELECT, also remove from ORDER BY
- _.each(_.difference(_.keys(ctrl.params.orderBy), newSelect), function(col) {
- delete ctrl.params.orderBy[col];
+ _.each(_.difference(_.keys(ctrl.savedSearch.api_params.orderBy), newSelect), function(col) {
+ delete ctrl.savedSearch.api_params.orderBy[col];
});
// Re-arranging or removing columns doesn't merit a refresh, only adding columns does
if (!oldSelect || _.difference(newSelect, oldSelect).length) {
}
}
if (ctrl.load) {
- ctrl.load.saved = false;
+ ctrl.saved = false;
}
}
ctrl.stale = true;
ctrl.selectedRows.length = 0;
if (ctrl.load) {
- ctrl.load.saved = false;
+ ctrl.saved = false;
}
if (ctrl.autoSearch) {
ctrl.refreshAll();
}
// If more than one page of results, use ajax to fetch all ids
$scope.loadingAllRows = true;
- var params = _.cloneDeep(ctrl.params);
+ var params = _.cloneDeep(ctrl.savedSearch.api_params);
params.select = ['id'];
- crmApi4(ctrl.entity, 'get', params, ['id']).then(function(ids) {
+ crmApi4(ctrl.savedSearch.api_entity, 'get', params, ['id']).then(function(ids) {
$scope.loadingAllRows = false;
ctrl.selectedRows = _.toArray(ids);
});
// Is a column eligible to use an aggregate function?
this.canAggregate = function(col) {
+ // If the query does not use grouping, never
+ if (!ctrl.savedSearch.api_params.groupBy.length) {
+ return false;
+ }
var info = searchMeta.parseExpr(col);
// If the column is used for a groupBy, no
- if (ctrl.params.groupBy.indexOf(info.path) > -1) {
+ if (ctrl.savedSearch.api_params.groupBy.indexOf(info.path) > -1) {
return false;
}
// If the entity this column belongs to is being grouped by id, then also no
- return ctrl.params.groupBy.indexOf(info.prefix + 'id') < 0;
+ return ctrl.savedSearch.api_params.groupBy.indexOf(info.prefix + 'id') < 0;
};
- $scope.formatResult = function formatResult(row, col) {
+ $scope.formatResult = function(row, col) {
var info = searchMeta.parseExpr(col),
key = info.fn ? (info.fn.name + ':' + info.path + info.suffix) : col,
value = row[key];
if (info.fn && info.fn.name === 'COUNT') {
return value;
}
+ // Output user-facing name/label fields as a link, if possible
+ if (info.field && _.includes(['display_name', 'title', 'label', 'subject'], info.field.name) && !info.fn && typeof value === 'string') {
+ var link = getEntityUrl(row, info);
+ if (link) {
+ return '<a href="' + _.escape(link.url) + '" title="' + _.escape(link.title) + '">' + formatFieldValue(info.field, value) + '</a>';
+ }
+ }
return formatFieldValue(info.field, value);
};
+ // Attempts to construct a view url for a given entity
+ function getEntityUrl(row, info) {
+ var entity = searchMeta.getEntity(info.field.entity),
+ path = _.result(_.findWhere(entity.paths, {action: 'view'}), 'path');
+ // Only proceed if the path metadata exists for this entity
+ if (path) {
+ // Replace tokens in the path (e.g. [id])
+ var tokens = path.match(/\[\w*]/g) || [],
+ replacements = _.transform(tokens, function(replacements, token) {
+ var fieldName = info.prefix + token.slice(1, token.length - 1);
+ if (row[fieldName]) {
+ replacements.push(row[fieldName]);
+ }
+ });
+ // Only proceed if the row contains all the necessary data to resolve tokens
+ if (tokens.length === replacements.length) {
+ _.each(tokens, function(token, index) {
+ path = path.replace(token, replacements[index]);
+ });
+ return {url: CRM.url(path), title: path.title};
+ }
+ }
+ }
+
function formatFieldValue(field, value) {
- var type = field.data_type;
+ var type = field.data_type,
+ result = value;
if (_.isArray(value)) {
return _.map(value, function(val) {
return formatFieldValue(field, val);
}).join(', ');
}
if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) {
- return CRM.utils.formatDate(value, null, type === 'Timestamp');
+ result = CRM.utils.formatDate(value, null, type === 'Timestamp');
}
else if (type === 'Boolean' && typeof value === 'boolean') {
- return value ? ts('Yes') : ts('No');
+ result = value ? ts('Yes') : ts('No');
}
else if (type === 'Money' && typeof value === 'number') {
- return CRM.formatMoney(value);
+ result = CRM.formatMoney(value);
}
- return value;
+ return _.escape(result);
}
$scope.fieldsForGroupBy = function() {
return {results: getAllFields('', function(key) {
- return _.contains(ctrl.params.groupBy, key);
+ return _.contains(ctrl.savedSearch.api_params.groupBy, key);
})
};
};
$scope.fieldsForSelect = function() {
return {results: getAllFields(':label', function(key) {
- return _.contains(ctrl.params.select, key);
+ return _.contains(ctrl.savedSearch.api_params.select, key);
})
};
};
};
$scope.fieldsForHaving = function() {
- return {results: _.transform(ctrl.params.select, function(fields, name) {
+ return {results: _.transform(ctrl.savedSearch.api_params.select, function(fields, name) {
fields.push({id: name, text: ctrl.getFieldLabel(name)});
})};
};
+ $scope.sortableColumnOptions = {
+ axis: 'x',
+ handle: '.crm-draggable',
+ update: function(e, ui) {
+ // Don't allow items to be moved to position 0 if locked
+ if (!ui.item.sortable.dropindex && ctrl.groupExists) {
+ ui.item.sortable.cancel();
+ }
+ }
+ };
+
+ // Sets the default select clause based on commonly-named fields
function getDefaultSelect() {
- return _.filter(['id', 'display_name', 'label', 'title', 'location_type_id:label'], function(field) {
- return !!searchMeta.getField(field, ctrl.entity);
+ var whitelist = ['id', 'name', 'subject', 'display_name', 'label', 'title'];
+ return _.transform(searchMeta.getEntity(ctrl.savedSearch.api_entity).fields, function(select, field) {
+ if (_.includes(whitelist, field.name) || _.includes(field.name, '_type_id')) {
+ select.push(field.name + (field.options ? ':label' : ''));
+ }
});
}
}, []);
}
- var mainEntity = searchMeta.getEntity(ctrl.entity),
+ var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
result = [{
- text: mainEntity.titlePlural,
+ text: mainEntity.title_plural,
icon: mainEntity.icon,
- children: formatFields(ctrl.entity, '')
+ children: formatFields(ctrl.savedSearch.api_entity, '')
}];
- _.each(ctrl.params.join, function(join) {
+ _.each(ctrl.savedSearch.api_params.join, function(join) {
var joinName = join[0].split(' AS '),
joinEntity = searchMeta.getEntity(joinName[0]);
result.push({
- text: joinEntity.titlePlural + ' (' + joinName[1] + ')',
+ text: joinEntity.title_plural + ' (' + joinName[1] + ')',
icon: joinEntity.icon,
children: formatFields(joinEntity.name, joinName[1] + '.')
});
* Sets an optionsLoaded property on each entity to avoid duplicate requests
*/
function loadFieldOptions() {
- var mainEntity = searchMeta.getEntity(ctrl.entity),
+ var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
entities = {};
function enqueue(entity) {
entity.optionsLoaded = false;
entities[entity.name] = [entity.name, 'getFields', {
- loadOptions: CRM.vars.search.loadOptions,
+ loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'],
where: [['options', '!=', false]],
select: ['options']
}, {name: 'options'}];
if (typeof mainEntity.optionsLoaded === 'undefined') {
enqueue(mainEntity);
}
- _.each(ctrl.params.join, function(join) {
+ _.each(ctrl.savedSearch.api_params.join, function(join) {
var joinName = join[0].split(' AS '),
joinEntity = searchMeta.getEntity(joinName[0]);
if (typeof joinEntity.optionsLoaded === 'undefined') {
}
}
- this.$onInit = function() {
- $scope.$bindToRoute({
- expr: '$ctrl.params.select',
- param: 'select',
- format: 'json',
- default: getDefaultSelect()
- });
- $scope.$watchCollection('$ctrl.params.select', onChangeSelect);
-
- $scope.$bindToRoute({
- expr: '$ctrl.params.orderBy',
- param: 'orderBy',
- format: 'json',
- default: {}
- });
-
- $scope.$bindToRoute({
- expr: '$ctrl.params.where',
- param: 'where',
- format: 'json',
- default: [],
- deep: true
- });
- $scope.$watch('$ctrl.params.where', onChangeFilters, true);
-
- if (this.paramExists('groupBy')) {
- $scope.$bindToRoute({
- expr: '$ctrl.params.groupBy',
- param: 'groupBy',
- format: 'json',
- default: []
- });
- }
- $scope.$watchCollection('$ctrl.params.groupBy', onChangeFilters);
-
- if (this.paramExists('join')) {
- $scope.$bindToRoute({
- expr: '$ctrl.params.join',
- param: 'join',
- format: 'json',
- default: [],
- deep: true
- });
- }
- $scope.$watch('$ctrl.params.join', onChangeFilters, true);
-
- if (this.paramExists('having')) {
- $scope.$bindToRoute({
- expr: '$ctrl.params.having',
- param: 'having',
- format: 'json',
- default: [],
- deep: true
- });
- }
- $scope.$watch('$ctrl.params.having', onChangeFilters, true);
-
- if (this.load) {
- this.params = this.load.api_params;
- $timeout(function() {
- ctrl.load.saved = true;
- });
- }
-
- loadFieldOptions();
- };
-
- $scope.saveGroup = function() {
- var model = {
- title: '',
- description: '',
- visibility: 'User and User Admin Only',
- group_type: [],
- id: ctrl.load ? ctrl.load.id : null,
- api_entity: ctrl.entity,
- api_params: _.cloneDeep(angular.extend({}, ctrl.params, {version: 4}))
- };
- delete model.api_params.orderBy;
- if (ctrl.load && ctrl.load.api_params && ctrl.load.api_params.select && ctrl.load.api_params.select[0]) {
- model.api_params.select.unshift(ctrl.load.api_params.select[0]);
- }
- var options = CRM.utils.adjustDialogDefaults({
- autoOpen: false,
- title: ts('Save smart group')
- });
- dialogService.open('saveSearchDialog', '~/search/saveSmartGroup.html', model, options)
- .then(function() {
- if (ctrl.load) {
- ctrl.load.saved = true;
- }
- });
- };
}
});
--- /dev/null
+<div id="bootstrap-theme" class="crm-search">
+ <h1 crm-page-title>{{ $ctrl.entityTitle + ': ' + $ctrl.savedSearch.label }}</h1>
+ <div crm-ui-debug="$ctrl.savedSearch"></div>
+
+ <!--This warning will show if bootstrap is unavailable. Normally it will be hidden by the bootstrap .collapse class.-->
+ <div class="messages warning no-popup collapse">
+ <p>
+ <i class="crm-i fa-exclamation-triangle" aria-hidden="true"></i>
+ <strong>{{:: ts('Bootstrap theme not found.') }}</strong>
+ </p>
+ <p>{{:: ts('This screen may not work correctly without a bootstrap-based theme such as Shoreditch installed.') }}</p>
+ </div>
+
+ <form>
+ <div class="crm-flex-box">
+ <div class="nav-stacked">
+ <input id="crm-saved-search-label" class="form-control" ng-model="$ctrl.savedSearch.label" type="text" required placeholder="{{ ts('Untitled Search') }}" />
+ </div>
+ <div class="crm-flex-4 form-inline">
+ <label for="crm-search-main-entity">{{:: ts('Search for:') }}</label>
+ <input id="crm-search-main-entity" class="form-control" ng-model="$ctrl.savedSearch.api_entity" crm-ui-select="::{allowClear: false, data: entities}" ng-disabled="$ctrl.savedSearch.id" />
+ <div class="btn-group btn-group-md pull-right">
+ <button type="submit" class="btn" ng-class="{'btn-primary': status === 'unsaved', 'btn-warning': status === 'saving', 'btn-success': status === 'saved'}" ng-disabled="status !== 'unsaved'" ng-click="$ctrl.save()">
+ <i class="crm-i" ng-class="{'fa-check': status !== 'saving', 'fa-spin fa-spinner': status === 'saving'}"></i>
+ <span ng-if="status === 'saved'">{{ ts('Saved') }}</span>
+ <span ng-if="status === 'unsaved'">{{ ts('Save') }}</span>
+ <span ng-if="status === 'saving'">{{ ts('Saving...') }}</span>
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="crm-flex-box">
+ <ul class="nav nav-pills nav-stacked" ng-include="'~/crmSearchAdmin/tabs.html'"></ul>
+ <div class="crm-flex-4" ng-switch="controls.tab">
+ <div ng-switch-when="compose">
+ <div ng-include="'~/crmSearchAdmin/compose/criteria.html'"></div>
+ <div ng-include="'~/crmSearchAdmin/compose/controls.html'"></div>
+ <div ng-include="'~/crmSearchAdmin/compose/debug.html'" ng-if="$ctrl.debug"></div>
+ <div ng-include="'~/crmSearchAdmin/compose/results.html'" class="crm-search-results"></div>
+ <div ng-include="'~/crmSearchAdmin/compose/pager.html'"></div>
+ </div>
+ <div ng-switch-when="group">
+ <fieldset ng-include="'~/crmSearchAdmin/group.html'"></fieldset>
+ </div>
+ <div ng-switch-default>
+ <div ng-repeat="display in $ctrl.savedSearch.displays" ng-if="controls.tab === ('display_' + $index)">
+ <crm-search-admin-display display="display" saved-search="$ctrl.savedSearch"></crm-search-admin-display>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('crmSearchAdmin').component('crmSearchAdminDisplay', {
+ bindings: {
+ savedSearch: '<',
+ display: '<'
+ },
+ template: function() {
+ // Dynamic template generates switch condition for each display type
+ var html =
+ '<div ng-include="\'~/crmSearchAdmin/crmSearchAdminDisplay.html\'"></div>\n' +
+ '<div ng-switch="$ctrl.display.type">\n';
+ _.each(CRM.crmSearchAdmin.displayTypes, function(type) {
+ html +=
+ '<div ng-switch-when="' + type.name + '">\n' +
+ ' <search-admin-display-' + type.name + ' api-entity="$ctrl.savedSearch.api_entity" api-params="$ctrl.savedSearch.api_params" display="$ctrl.display"></search-admin-display-' + type.name + '>\n' +
+ ' <hr>\n' +
+ ' <button type="button" class="btn btn-{{ !$ctrl.stale ? \'success\' : $ctrl.preview ? \'warning\' : \'primary\' }}" ng-click="$ctrl.previewDisplay()" ng-disabled="!$ctrl.stale">\n' +
+ ' <i class="crm-i ' + type.icon + '"></i>' +
+ ' {{ $ctrl.preview && $ctrl.stale ? ts("Refresh") : ts("Preview") }}\n' +
+ ' </button>\n' +
+ ' <hr>\n' +
+ ' <div ng-if="$ctrl.preview">\n' +
+ ' <crm-search-display-' + type.name + ' api-entity="$ctrl.savedSearch.api_entity" api-params="$ctrl.savedSearch.api_params" settings="$ctrl.display.settings"></crm-search-display-' + type.name + '>\n' +
+ ' </div>\n' +
+ '</div>\n';
+ });
+ html += '</div>';
+ return html;
+ },
+ controller: function($scope, $timeout) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ this.preview = this.stale = false;
+
+ this.previewDisplay = function() {
+ ctrl.preview = !ctrl.preview;
+ ctrl.stale = false;
+ if (!ctrl.preview) {
+ $timeout(function() {
+ ctrl.preview = true;
+ }, 100);
+ }
+ };
+
+ $scope.$watch('$ctrl.display.settings', function() {
+ ctrl.stale = true;
+ }, true);
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<fieldset>
+ <div class="form-inline">
+ <label for="crm-search-admin-display-label">{{:: ts('Name:') }} <span class="crm-marker">*</span></label>
+ <input id="crm-search-admin-display-label" type="text" class="form-control" ng-model="$ctrl.display.label" required placeholder="{{ ts('Untitled') }}"/>
+ <label class="pull-right">{{:: $ctrl.displayTypes[$ctrl.display.type].label }}</label>
+ </div>
+</fieldset>
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('crmSearchAdmin').component('crmSearchAdminLinkSelect', {
+ bindings: {
+ column: '<',
+ links: '<'
+ },
+ templateUrl: '~/crmSearchAdmin/crmSearchAdminLinkSelect.html',
+ controller: function ($scope, $element, $timeout) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ function onChange() {
+ var val = $('select', $element).val();
+ if (val !== ctrl.column.link) {
+ var link = ctrl.getLink(val);
+ if (link) {
+ ctrl.column.link = link.path;
+ ctrl.column.title = link.title;
+ } else if (val === 'civicrm/') {
+ ctrl.column.link = val;
+ $timeout(function() {
+ $('input', $element).focus();
+ });
+ } else {
+ ctrl.column.link = '';
+ ctrl.column.title = '';
+ }
+ }
+ }
+
+ this.$onInit = function() {
+ $('select', $element).on('change', function() {
+ $scope.$apply(onChange);
+ });
+ };
+
+ this.getLink = function(path) {
+ return _.findWhere(ctrl.links, {path: path});
+ };
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<select class="form-control">
+ <option value="" ng-selected="!$ctrl.column.link" >{{ ts('None') }}</option>
+ <option ng-repeat="link in $ctrl.links" value="{{ link.path }}" ng-selected="$ctrl.column.link === link.path">
+ {{ link.title }}
+ </option>
+ <option value="civicrm/" ng-selected="$ctrl.column.link && !$ctrl.getLink($ctrl.column.link)">
+ {{ ts('Other...') }}
+ </option>
+</select>
+<input class="form-control" type="text" ng-model="$ctrl.column.link" ng-model-options="{updateOn: 'blur'}" ng-show="$ctrl.column.link && !$ctrl.getLink($ctrl.column.link)" />
(function(angular, $, _) {
"use strict";
- angular.module('search').component('crmSearchClause', {
+ angular.module('crmSearchAdmin').component('crmSearchClause', {
bindings: {
fields: '<',
clauses: '<',
label: '@',
deleteGroup: '&'
},
- templateUrl: '~/search/crmSearchClause.html',
- controller: function ($scope, $element, $timeout) {
+ templateUrl: '~/crmSearchAdmin/crmSearchClause.html',
+ controller: function ($scope, $element, $timeout, searchMeta) {
var ts = $scope.ts = CRM.ts(),
- ctrl = this;
+ ctrl = this,
+ meta = {};
this.conjunctions = {AND: ts('And'), OR: ts('Or'), NOT: ts('Not')};
- this.operators = CRM.vars.search.operators;
+ this.operators = CRM.crmSearchAdmin.operators;
this.sortOptions = {
axis: 'y',
connectWith: '.api4-clause-group-sortable',
ctrl.hasParent = !!$element.attr('delete-group');
};
+ this.getField = function(expr) {
+ if (!meta[expr]) {
+ meta[expr] = searchMeta.parseExpr(expr);
+ }
+ return meta[expr].field;
+ };
+
+ this.getOptionKey = function(expr) {
+ if (!meta[expr]) {
+ meta[expr] = searchMeta.parseExpr(expr);
+ }
+ return meta[expr].suffix ? meta[expr].suffix.slice(1) : 'id';
+ };
+
this.addGroup = function(op) {
ctrl.clauses.push([op, []]);
};
<div ng-if="!$ctrl.conjunctions[clause[0]]" class="api4-input-group">
<input class="form-control" ng-model="clause[0]" crm-ui-select="{data: $ctrl.fields, allowClear: true, placeholder: 'Field'}" ng-change="$ctrl.changeClauseField(clause, index)" />
<select class="form-control api4-operator" ng-model="clause[1]" ng-options="o.key as o.value for o in $ctrl.operators" ng-change="$ctrl.changeClauseOperator(clause)" ></select>
- <input class="form-control" ng-model="clause[2]" crm-search-value="{field: clause[0], op: clause[1], format: $ctrl.format}" />
+ <input class="form-control" ng-model="clause[2]" crm-search-value="{field: $ctrl.getField(clause[0]), optionKey: $ctrl.getOptionKey(clause[0]), op: clause[1], format: $ctrl.format}" />
</div>
<fieldset class="clearfix" ng-if="$ctrl.conjunctions[clause[0]]">
<crm-search-clause clauses="clause[1]" format="{{ $ctrl.format }}" op="{{ clause[0] }}" fields="$ctrl.fields" delete-group="$ctrl.deleteRow(index)" ></crm-search-clause>
(function(angular, $, _) {
"use strict";
- angular.module('search').component('crmSearchFunction', {
+ angular.module('crmSearchAdmin').component('crmSearchFunction', {
bindings: {
expr: '=',
cat: '<'
},
- templateUrl: '~/search/crmSearchFunction.html',
+ templateUrl: '~/crmSearchAdmin/crmSearchFunction.html',
controller: function($scope, formatForSelect2, searchMeta) {
var ts = $scope.ts = CRM.ts(),
ctrl = this;
this.$onInit = function() {
- ctrl.functions = formatForSelect2(_.where(CRM.vars.search.functions, {category: ctrl.cat}), 'name', 'title');
+ ctrl.functions = formatForSelect2(_.where(CRM.crmSearchAdmin.functions, {category: ctrl.cat}), 'name', 'title');
var fieldInfo = searchMeta.parseExpr(ctrl.expr);
ctrl.path = fieldInfo.path + fieldInfo.suffix;
ctrl.field = fieldInfo.field;
};
function initFunction() {
- ctrl.fnInfo = _.find(CRM.vars.search.functions, {name: ctrl.fn});
+ ctrl.fnInfo = _.find(CRM.crmSearchAdmin.functions, {name: ctrl.fn});
if (ctrl.fnInfo && _.includes(ctrl.fnInfo.params[0].prefix, 'DISTINCT')) {
ctrl.modifierAllowed = true;
}
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('crmSearchAdmin').component('searchAdminDisplayTable', {
+ bindings: {
+ display: '<',
+ apiEntity: '<',
+ apiParams: '<'
+ },
+ require: {
+ crmSearchAdmin: '^crmSearchAdmin'
+ },
+ templateUrl: '~/crmSearchAdmin/displays/searchAdminDisplayTable.html',
+ controller: function($scope, searchMeta) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ function fieldToColumn(fieldExpr) {
+ var info = searchMeta.parseExpr(fieldExpr);
+ return {
+ expr: fieldExpr,
+ label: ctrl.getFieldLabel(fieldExpr),
+ dataType: (info.fn && info.fn.name === 'COUNT') ? 'Integer' : info.field.data_type
+ };
+ }
+
+ this.sortableOptions = {
+ connectWith: '.crm-search-admin-edit-columns',
+ containment: '.crm-search-admin-edit-columns-wrapper'
+ };
+
+ this.removeCol = function(index) {
+ ctrl.hiddenColumns.push(ctrl.display.settings.columns[index]);
+ ctrl.display.settings.columns.splice(index, 1);
+ };
+
+ this.restoreCol = function(index) {
+ ctrl.display.settings.columns.push(ctrl.hiddenColumns[index]);
+ ctrl.hiddenColumns.splice(index, 1);
+ };
+
+ this.$onInit = function () {
+ ctrl.getFieldLabel = ctrl.crmSearchAdmin.getFieldLabel;
+ if (!ctrl.display.settings) {
+ ctrl.display.settings = {
+ limit: 20,
+ pager: true
+ };
+ }
+ if (!ctrl.display.settings.columns) {
+ ctrl.display.settings.columns = _.transform(ctrl.apiParams.select, function(columns, fieldExpr) {
+ columns.push(fieldToColumn(fieldExpr));
+ });
+ ctrl.hiddenColumns = [];
+ } else {
+ var activeColumns = _.collect(ctrl.display.settings.columns, 'expr');
+ ctrl.hiddenColumns = _.transform(ctrl.apiParams.select, function(hiddenColumns, fieldExpr) {
+ if (!_.includes(activeColumns, fieldExpr)) {
+ hiddenColumns.push(fieldToColumn(fieldExpr));
+ }
+ });
+ _.each(activeColumns, function(fieldExpr, index) {
+ if (!_.includes(ctrl.apiParams.select, fieldExpr)) {
+ ctrl.display.settings.columns.splice(index, 1);
+ }
+ });
+ }
+ ctrl.links = _.cloneDeep(searchMeta.getEntity(ctrl.apiEntity).paths || []);
+ _.each(ctrl.apiParams.join, function(join) {
+ var joinName = join[0].split(' AS '),
+ joinEntity = searchMeta.getEntity(joinName[0]);
+ _.each(joinEntity.paths, function(path) {
+ var link = _.cloneDeep(path);
+ link.path = link.path.replace(/\[/g, '[' + joinName[1] + '.');
+ ctrl.links.push(link);
+ });
+ });
+ };
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<fieldset>
+ <div class="form-inline">
+ <label for="crm-search-admin-table-limit">{{ ts('Results to display (0 for no limit):') }}</label>
+ <input id="crm-search-admin-table-limit" type="number" min="0" step="1" class="form-control" ng-model="$ctrl.display.settings.limit">
+ <label><input type="checkbox" ng-model="$ctrl.display.settings.pager"> {{ ts('Use Pager') }}</label>
+ <label><input type="checkbox" ng-model="$ctrl.display.settings.actions"> {{ ts('Enable Actions') }}</label>
+ </div>
+</fieldset>
+<div class="crm-flex-box crm-search-admin-edit-columns-wrapper">
+ <fieldset class="crm-search-admin-edit-columns" ng-model="$ctrl.display.settings.columns" ui-sortable="$ctrl.sortableOptions">
+ <legend>{{:: ts('Columns') }}</legend>
+ <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
+ <legend>{{ $ctrl.getFieldLabel(col.expr) }}</legend>
+ <div class="form-inline">
+ <label>{{ ts('Label:') }}</label> <input class="form-control" type="text" ng-model="col.label" />
+ <button class="btn-xs pull-right" ng-click="$ctrl.removeCol($index)" title="{{:: ts('Hide') }}">
+ <i class="crm-i fa-ban"></i>
+ </button>
+ </div>
+ <div class="form-inline">
+ <label>{{ ts('Link:') }}</label>
+ <crm-search-admin-link-select column="col" links="$ctrl.links"></crm-search-admin-link-select>
+ </div>
+ <div class="form-inline">
+ <label>{{ ts('Tooltip:') }}</label>
+ <input class="form-control" type="text" ng-model="col.title" />
+ </div>
+ </fieldset>
+ </fieldset>
+ <fieldset class="crm-search-admin-edit-columns" ng-model="$ctrl.hiddenColumns" ui-sortable="$ctrl.sortableOptions">
+ <legend>{{:: ts('Hidden Columns') }}</legend>
+ <fieldset ng-repeat="col in $ctrl.hiddenColumns" class="crm-draggable">
+ <legend>{{ $ctrl.getFieldLabel(col.expr) }}</legend>
+ <div class="form-inline">
+ <label>{{ ts('Label:') }}</label> <input disabled class="form-control" type="text" ng-model="col.label" />
+ <button class="btn-xs pull-right" ng-click="$ctrl.restoreCol($index)" title="{{:: ts('Show') }}">
+ <i class="crm-i fa-undo"></i>
+ </button>
+ </div>
+ </fieldset>
+ </fieldset>
+</div>
--- /dev/null
+<div class="alert alert-warning" ng-show="!smartGroupColumns.length">
+ {{:: ts('Unable to create smart group because this search does not include any contacts.') }}
+</div>
+
+<div class="form-inline">
+ <label for="crm-search-admin-group-title">{{ ts('Group Title:') }} <span class="crm-marker">*</span></label>
+ <input id="crm-search-admin-group-title" class="form-control" placeholder="{{:: ts('Untitled') }}" ng-model="$ctrl.savedSearch.groups[0].title" ng-disabled="!smartGroupColumns.length" ng-required="smartGroupColumns.length">
+ <label for="api-save-search-select-column">{{:: ts('Contact Column:') }}</label>
+ <input id="api-save-search-select-column" ng-model="$ctrl.savedSearch.api_params.select[0]" class="form-control" crm-ui-select="{data: smartGroupColumns}"/>
+</div>
+<fieldset ng-show="smartGroupColumns.length">
+ <label>{{:: ts('Description:') }}</label>
+ <textarea class="form-control" ng-model="$ctrl.savedSearch.groups[0].description"></textarea>
+ <div class="form-inline">
+ <label>{{:: ts('Group Type:') }} </label>
+ <div class="checkbox" ng-repeat="option in groupOptions.group_type track by option.id">
+ <label>
+ <input type="checkbox" checklist-model="$ctrl.savedSearch.groups[0].group_type" checklist-value="option.id">
+ {{ option.label }}
+ </label>
+ </div>
+ </div>
+ <div class="form-inline">
+ <label>{{:: ts('Visibility:') }}</label>
+ <select class="form-control" ng-model="$ctrl.savedSearch.groups[0].visibility" ng-options="item.id as item.label for item in groupOptions.visibility track by item.id" crm-ui-select></select>
+ </div>
+</fieldset>
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('crmSearchAdmin').controller('searchList', function($scope, savedSearches, crmApi4) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = $scope.$ctrl = this;
+ this.savedSearches = savedSearches;
+ this.entityTitles = _.transform(CRM.vars.search.schema, function(titles, entity) {
+ titles[entity.name] = entity.title_plural;
+ }, {});
+
+ this.searchPath = window.location.href.split('#')[0].replace('civicrm/admin/search', 'civicrm/search');
+
+ this.deleteSearch = function(search) {
+ var index = _.findIndex(savedSearches, {id: search.id});
+ if (index > -1) {
+ crmApi4([
+ ['Group', 'delete', {where: [['saved_search_id', '=', search.id]]}],
+ ['SavedSearch', 'delete', {where: [['id', '=', search.id]]}]
+ ]);
+ savedSearches.splice(index, 1);
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<div id="bootstrap-theme" class="crm-search">
+ <h1 crm-page-title>{{:: ts('Saved Searches') }}</h1>
+ <div class="form-inline">
+ <a class="btn btn-primary pull-right" href="#/create/Contact/">
+ <i class="crm-i fa-plus"></i>
+ {{:: ts('New Search') }}
+ </a>
+ </div>
+ <table>
+ <thead>
+ <tr>
+ <th>{{:: ts('ID') }}</th>
+ <th>{{:: ts('Label') }}</th>
+ <th>{{:: ts('For') }}</th>
+ <th>{{:: ts('Displays') }}</th>
+ <th>{{:: ts('Smart Group') }}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="search in $ctrl.savedSearches">
+ <td>{{ search.id }}</td>
+ <td>{{ search.label }}</td>
+ <td>{{ $ctrl.entityTitles[search.api_entity] }}</td>
+ <td>
+ <div class="btn-group">
+ <button type="button" disabled ng-if="!search.display_name" class="btn btn-xs dropdown-toggle btn-primary-outline">
+ {{:: ts('0 Displays') }}
+ </button>
+ <button type="button" ng-if="search.display_name" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{:: search.display_name.length === 1 ? ts('1 Display') : ts('%1 Displays', {1: search.display_name.length}) }} <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu" ng-if=":: search.display_name.length">
+ <li ng-repeat="display_name in search.display_name">
+ <a href="{{:: $ctrl.searchPath + '#/display/' + search.name + '/' + display_name }}"><i class="fa {{:: search.display_icon[$index] }}"></i> {{:: search.display_label[$index] }}</a>
+ </li>
+ </ul>
+ </div>
+ </td>
+ <td>{{ search.groups.join(', ') }}</td>
+ <td class="text-right">
+ <a class="btn btn-xs btn-default" href="#/edit/{{ search.id }}">{{:: ts('Edit') }}</a>
+ <a href class="btn btn-xs btn-danger" crm-confirm="{type: 'delete', obj: search}" on-yes="$ctrl.deleteSearch(search)">{{:: ts('Delete') }}</a>
+ </td>
+ </tr>
+ <tr ng-if="$ctrl.savedSearches.length === 0">
+ <td colspan="9">
+ <p class="messages status no-popup text-center">
+ {{:: ts('No saved searches.')}}
+ </p>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
--- /dev/null
+<li role="presentation" ng-class="{active: controls.tab === 'compose'}">
+ <a href ng-click="selectTab('compose')">
+ <i class="crm-i fa-gears"></i>
+ {{ ts('Compose Search') }}
+ </a>
+</li>
+<li role="presentation" ng-class="{active: controls.tab === 'group'}" ng-if="$ctrl.savedSearch.groups.length" title="{{ !$ctrl.groupExists ? ts('Group will be deleted.') : '' }}">
+ <a href ng-click="selectTab('group')" ng-disabled="!$ctrl.groupExists">
+ <i class="crm-i fa-users"></i>
+ {{:: ts('Smart Group:') }} {{ $ctrl.savedSearch.groups[0].title }}
+ </a>
+ <button class="btn-xs btn-danger-outline crm-search-delete-display" ng-click="$ctrl.removeGroup()" title="{{ $ctrl.groupExists ? ts('Delete') : ts('Undelete') }}">
+ <i class="crm-i fa-{{ $ctrl.groupExists ? 'trash' : 'undo' }}"></i>
+ </button>
+</li>
+<li role="presentation" ng-repeat="display in $ctrl.savedSearch.displays" ng-class="{active: controls.tab === ('display_' + $index)}" title="{{ display.trashed ? ts('Display will be deleted.') : '' }}">
+ <a href ng-click="selectTab('display_' + $index)" ng-disabled="display.trashed">
+ <i class="crm-i {{ $ctrl.displayTypes[display.type].icon }}"></i>
+ {{ display.label || ts('Untitled') }}
+ </a>
+ <button class="btn-xs btn-danger-outline crm-search-delete-display" ng-click="$ctrl.removeDisplay($index)" title="{{ display.trashed ? ts('Undelete') : ts('Delete') }}">
+ <i class="crm-i fa-{{ display.trashed ? 'undo' : 'trash' }}"></i>
+ </button>
+</li>
+<li role="presentation">
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ <i class="crm-i fa-plus"></i> {{:: ts('Add...') }} <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li ng-if="!$ctrl.savedSearch.groups.length">
+ <a href ng-click="$ctrl.addGroup()">
+ <i class="crm-i fa-users"></i>
+ {{:: ts('Smart Group') }}
+ </a>
+ </li>
+ <li class="dropdown-header">{{ ts('Display:') }}</li>
+ <li ng-repeat="type in ::$ctrl.displayTypes">
+ <a href ng-click="$ctrl.addDisplay(type.name)">
+ <i class="crm-i {{:: type.icon }}"></i>
+ {{:: type.label }}
+ </a>
+ </li>
+ </ul>
+</li>
--- /dev/null
+<?php
+// Search Display module - for rendering search displays.
+return [
+ 'js' => [
+ 'ang/crmSearchDisplay.module.js',
+ 'ang/crmSearchDisplay/*.js',
+ 'ang/crmSearchDisplay/*/*.js',
+ ],
+ 'partials' => [
+ 'ang/crmSearchDisplay',
+ ],
+ 'basePages' => [],
+ 'requires' => ['ngSanitize', 'crmUi', 'api4', 'crmSearchActions', 'ui.bootstrap'],
+ 'exports' => [
+ 'crm-search-display-table' => 'E',
+ ],
+];
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ // Declare module
+ angular.module('crmSearchDisplay', CRM.angRequires('crmSearchDisplay'));
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('crmSearchDisplay').component('crmSearchDisplayTable', {
+ bindings: {
+ apiEntity: '<',
+ apiParams: '<',
+ settings: '<',
+ filters: '<'
+ },
+ templateUrl: '~/crmSearchDisplay/crmSearchDisplayTable.html',
+ controller: function($scope, crmApi4) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ this.page = 1;
+ this.selectedRows = [];
+ this.allRowsSelected = false;
+
+ this.$onInit = function() {
+ this.orderBy = _.cloneDeep(this.apiParams.orderBy || {});
+ this.limit = parseInt(ctrl.settings.limit || 0, 10);
+ this.columns = _.cloneDeep(ctrl.settings.columns);
+ _.each(ctrl.columns, function(col, num) {
+ var index = ctrl.apiParams.select.indexOf(col.expr);
+ if (_.includes(col.expr, '(') && !_.includes(col.expr, ' AS ')) {
+ col.expr += ' AS column_' + num;
+ ctrl.apiParams.select[index] += ' AS column_' + num;
+ }
+ col.key = _.last(col.expr.split(' AS '));
+ });
+ };
+
+ this.getResults = function() {
+ var params = _.merge(_.cloneDeep(ctrl.apiParams), {limit: ctrl.limit, offset: (ctrl.page - 1) * ctrl.limit, orderBy: ctrl.orderBy});
+ if (_.isEmpty(params.where)) {
+ params.where = [];
+ }
+ // Select the ids of joined entities (helps with displaying links)
+ _.each(params.join, function(join) {
+ var joinEntity = join[0].split(' AS ')[1],
+ idField = joinEntity + '.id';
+ if (!_.includes(params.select, idField) && !canAggregate('id', joinEntity + '.')) {
+ params.select.push(idField);
+ }
+ });
+ _.each(ctrl.filters, function(value, key) {
+ if (value) {
+ params.where.push([key, 'CONTAINS', value]);
+ }
+ });
+ if (ctrl.settings.pager) {
+ params.select.push('row_count');
+ }
+ crmApi4(ctrl.apiEntity, 'get', params).then(function(results) {
+ ctrl.results = results;
+ ctrl.rowCount = results.count;
+ });
+ };
+
+ $scope.$watch('$ctrl.filters', ctrl.getResults, true);
+
+ /**
+ * Returns crm-i icon class for a sortable column
+ * @param col
+ * @returns {string}
+ */
+ $scope.getOrderBy = function(col) {
+ var dir = ctrl.orderBy && ctrl.orderBy[col.key];
+ if (dir) {
+ return 'fa-sort-' + dir.toLowerCase();
+ }
+ return 'fa-sort disabled';
+ };
+
+ /**
+ * Called when clicking on a column header
+ * @param col
+ * @param $event
+ */
+ $scope.setOrderBy = function(col, $event) {
+ var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC';
+ if (!$event.shiftKey) {
+ ctrl.orderBy = {};
+ }
+ ctrl.orderBy[col.key] = dir;
+ ctrl.getResults();
+ };
+
+ $scope.formatResult = function(row, col) {
+ var value = row[col.key];
+ return formatFieldValue(row, col, value);
+ };
+
+ function formatFieldValue(row, col, value) {
+ var type = col.dataType,
+ result = value;
+ if (_.isArray(value)) {
+ return _.map(value, function(val) {
+ return formatFieldValue(col, val);
+ }).join(', ');
+ }
+ if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) {
+ result = CRM.utils.formatDate(value, null, type === 'Timestamp');
+ }
+ else if (type === 'Boolean' && typeof value === 'boolean') {
+ result = value ? ts('Yes') : ts('No');
+ }
+ else if (type === 'Money' && typeof value === 'number') {
+ result = CRM.formatMoney(value);
+ }
+ result = _.escape(result);
+ if (col.link) {
+ result = '<a href="' + getUrl(col.link, row) + '">' + result + '</a>';
+ }
+ return result;
+ }
+
+ function getUrl(link, row) {
+ var url = replaceTokens(link, row);
+ if (url.slice(0, 1) !== '/' && url.slice(0, 4) !== 'http') {
+ url = CRM.url(url);
+ }
+ return _.escape(url);
+ }
+
+ function replaceTokens(str, data) {
+ _.each(data, function(value, key) {
+ str = str.replace('[' + key + ']', value);
+ });
+ return str;
+ }
+
+ function canAggregate(fieldName, prefix) {
+ // If the query does not use grouping, never
+ if (!ctrl.apiParams.groupBy.length) {
+ return false;
+ }
+ // If the column is used for a groupBy, no
+ if (ctrl.apiParams.groupBy.indexOf(prefix + fieldName) > -1) {
+ return false;
+ }
+ // If the entity this column belongs to is being grouped by id, then also no
+ return ctrl.apiParams.groupBy.indexOf(prefix + 'id') < 0;
+ }
+
+ $scope.selectAllRows = function() {
+ // Deselect all
+ if (ctrl.allRowsSelected) {
+ ctrl.allRowsSelected = false;
+ ctrl.selectedRows.length = 0;
+ return;
+ }
+ // Select all
+ ctrl.allRowsSelected = true;
+ if (ctrl.page === 1 && ctrl.results[1].length < ctrl.limit) {
+ ctrl.selectedRows = _.pluck(ctrl.results[1], 'id');
+ return;
+ }
+ // If more than one page of results, use ajax to fetch all ids
+ $scope.loadingAllRows = true;
+ var params = _.cloneDeep(ctrl.apiParams);
+ params.select = ['id'];
+ crmApi4(ctrl.apiEntity, 'get', params, ['id']).then(function(ids) {
+ $scope.loadingAllRows = false;
+ ctrl.selectedRows = _.toArray(ids);
+ });
+ };
+
+ $scope.selectRow = function(row) {
+ var index = ctrl.selectedRows.indexOf(row.id);
+ if (index < 0) {
+ ctrl.selectedRows.push(row.id);
+ ctrl.allRowsSelected = (ctrl.rowCount === ctrl.selectedRows.length);
+ } else {
+ ctrl.allRowsSelected = false;
+ ctrl.selectedRows.splice(index, 1);
+ }
+ };
+
+ $scope.isRowSelected = function(row) {
+ return ctrl.allRowsSelected || _.includes(ctrl.selectedRows, row.id);
+ };
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<div class="form-inline" ng-if="$ctrl.settings.actions">
+ <crm-search-actions entity="$ctrl.apiEntity" ids="$ctrl.selectedRows" refresh="$ctrl.getResults()"></crm-search-actions>
+</div>
+<table>
+ <thead>
+ <tr>
+ <th class="crm-search-result-select" ng-if="$ctrl.settings.actions">
+ <input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" >
+ </th>
+ <th ng-repeat="col in $ctrl.columns" ng-click="setOrderBy(col, $event)" title="{{:: ts('Click to sort results (shift-click to sort by multiple).') }}">
+ <i class="crm-i {{ getOrderBy(col) }}"></i>
+ <span>{{ col.label }}</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="row in $ctrl.results">
+ <td ng-if="$ctrl.settings.actions">
+ <input type="checkbox" ng-checked="isRowSelected(row)" ng-click="selectRow(row)" ng-disabled="!(!loadingAllRows && row.id)">
+ </td>
+ <td ng-repeat="col in $ctrl.columns" ng-bind-html="formatResult(row, col)" title="{{:: col.title }}">
+ </td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+<div class="text-center" ng-if="$ctrl.rowCount && $ctrl.settings.pager">
+ <ul uib-pagination
+ class="pagination"
+ boundary-links="true"
+ total-items="$ctrl.rowCount"
+ ng-model="$ctrl.page"
+ ng-change="$ctrl.getResults()"
+ items-per-page="$ctrl.limit"
+ max-size="6"
+ force-ellipses="true"
+ previous-text="‹"
+ next-text="›"
+ first-text="«"
+ last-text="»"
+ ></ul>
+</div>
--- /dev/null
+<?php
+// Search kit - base module with utilities & services.
+return [
+ 'js' => [
+ 'ang/crmSearchKit.module.js',
+ 'ang/crmSearchKit/*.js',
+ 'ang/crmSearchKit/*/*.js',
+ ],
+ 'partials' => [
+ 'ang/crmSearchKit',
+ ],
+ 'basePages' => [],
+ 'requires' => ['api4'],
+];
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ // Declare module
+ angular.module('crmSearchKit', CRM.angRequires('crmSearchKit'))
+
+ // Reformat an array of objects for compatibility with select2
+ // Todo this probably belongs in core
+ .factory('formatForSelect2', function() {
+ return function(input, key, label, extra) {
+ return _.transform(input, function(result, item) {
+ var formatted = {id: item[key], text: item[label]};
+ if (extra) {
+ _.merge(formatted, _.pick(item, extra));
+ }
+ result.push(formatted);
+ }, []);
+ };
+ });
+
+})(angular, CRM.$, CRM._);
(function(angular, $, _) {
"use strict";
- angular.module('search').directive('crmSearchValue', function($interval, searchMeta, formatForSelect2) {
+ angular.module('crmSearchKit').directive('crmSearchValue', function($interval, formatForSelect2) {
return {
scope: {
data: '=crmSearchValue'
scope.$watchCollection('data', function(data) {
destroyWidget();
- var field = searchMeta.parseExpr(data.field).field;
- if (field) {
- var optionKey = data.field.split(':')[1] || 'id';
- makeWidget(field, data.op, optionKey);
+ if (data.field) {
+ makeWidget(data.field, data.op, data.optionKey || 'id');
}
});
}
--- /dev/null
+<?php
+// Autoloader data for SearchDisplay module.
+return [
+ 'js' => [
+ 'ang/crmSearchPage.module.js',
+ 'ang/crmSearchPage/*.js',
+ 'ang/crmSearchPage/*/*.js',
+ ],
+ 'partials' => [
+ 'ang/crmSearchPage',
+ ],
+ 'basePages' => ['civicrm/search'],
+ 'requires' => ['ngRoute', 'api4', 'crmUi', 'crmSearchDisplay'],
+ 'settingsFactory' => ['\Civi\Search\Display', 'getPageSettings'],
+];
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ // Declare module
+ angular.module('crmSearchPage', CRM.angRequires('crmSearchPage'))
+
+ .config(function($routeProvider) {
+ // Load & render a SearchDisplay
+ $routeProvider.when('/display/:savedSearchName/:displayName', {
+ controller: 'crmSearchPageDisplay',
+ // Dynamic template generates the directive for each display type
+ template: function() {
+ var html =
+ '<h1 crm-page-title>{{:: $ctrl.display.label }}</h1>\n' +
+ '<div ng-switch="$ctrl.display.type" id="bootstrap-theme">\n';
+ _.each(CRM.crmSearchPage.displayTypes, function(type) {
+ html +=
+ ' <div ng-switch-when="' + type.name + '">\n' +
+ ' <crm-search-display-' + type.name + ' api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" settings="$ctrl.display.settings"></crm-search-display-' + type.name + '>\n' +
+ ' </div>\n';
+ });
+ html += '</div>';
+ return html;
+ },
+ resolve: {
+ // Load saved search display
+ display: function($route, crmApi4) {
+ var params = $route.current.params;
+ return crmApi4('SearchDisplay', 'get', {
+ where: [['name', '=', params.displayName], ['saved_search.name', '=', params.savedSearchName]],
+ select: ['*', 'saved_search.api_entity', 'saved_search.api_params']
+ }, 0);
+ }
+ }
+ });
+ })
+
+ // Controller for displaying a search
+ .controller('crmSearchPageDisplay', function($scope, $routeParams, $location, display) {
+ this.display = display;
+ this.apiEntity = display['saved_search.api_entity'];
+ this.apiParams = display['saved_search.api_params'];
+ $scope.$ctrl = this;
+ });
+
+})(angular, CRM.$, CRM._);
+++ /dev/null
-<?php
-// Autoloader data for search builder.
-return [
- 'js' => [
- 'ang/*.js',
- 'ang/search/*.js',
- 'ang/search/*/*.js',
- ],
- 'css' => [
- 'css/*.css',
- ],
- 'partials' => [
- 'ang/search',
- ],
- 'basePages' => [],
- 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'crmRouteBinder', 'ui.sortable', 'ui.bootstrap', 'dialogService', 'api4'],
-];
+++ /dev/null
-(function(angular, $, _) {
- "use strict";
-
- // Shared between router and searchMeta service
- var searchEntity,
- // For loading saved search
- savedSearch,
- undefined;
-
- // Declare module and route/controller/services
- angular.module('search', CRM.angRequires('search'))
-
- .config(function($routeProvider) {
- $routeProvider.when('/:mode/:entity/:name?', {
- controller: 'searchRoute',
- template: '<div id="bootstrap-theme" class="crm-search"><crm-search ng-if="$ctrl.mode === \'create\'" entity="$ctrl.entity" load=":: $ctrl.savedSearch"></crm-search></div>',
- reloadOnSearch: false,
- resolve: {
- // For paths like /load/Group/MySmartGroup, load the group, stash it in the savedSearch variable, and then redirect
- // For paths like /create/Contact, return the stashed savedSearch if present
- savedSearch: function($route, $location, $timeout, crmApi4) {
- var retrievedSearch = savedSearch,
- getParams,
- params = $route.current.params;
- savedSearch = undefined;
- switch (params.mode) {
- case 'create':
- return retrievedSearch;
-
- case 'load':
- // Load savedSearch by `id` (the SavedSearch entity doesn't have `name`)
- if (params.entity === 'SavedSearch' && /^\d+$/.test(params.name)) {
- getParams = {
- where: [['id', '=', params.name]]
- };
- }
- // Load attached entity (e.g. Smart Groups) with a join via saved_search_id
- else if (params.entity === 'Group' && params.name) {
- getParams = {
- select: ['id', 'title', 'saved_search_id', 'saved_search.*'],
- where: [['name', '=', params.name]]
- };
- }
- // In theory savedSearches could be attached to something other than groups, but for now that's not supported
- else {
- throw 'Failed to load ' + params.entity;
- }
- return crmApi4(params.entity, 'get', getParams, 0).then(function(retrieved) {
- savedSearch = retrieved;
- savedSearch.type = params.entity;
- if (params.entity !== 'SavedSearch') {
- savedSearch.api_entity = retrieved['saved_search.api_entity'];
- savedSearch.api_params = retrieved['saved_search.api_params'];
- savedSearch.form_values = retrieved['saved_search.form_values'];
- }
- $timeout(function() {
- $location.url('/create/' + savedSearch.api_entity);
- });
- });
- }
- }
- }
- });
- })
-
- // Controller binds entity to route
- .controller('searchRoute', function($scope, $routeParams, $location, savedSearch) {
- searchEntity = this.entity = $routeParams.entity;
- this.mode = $routeParams.mode;
- this.savedSearch = savedSearch;
- $scope.$ctrl = this;
-
- // Changing entity will refresh the angular page
- $scope.$watch('$ctrl.entity', function(newEntity, oldEntity) {
- if (newEntity && oldEntity && newEntity !== oldEntity) {
- $location.url('/create/' + newEntity);
- }
- });
- })
-
- .factory('searchMeta', function() {
- function getEntity(entityName) {
- if (entityName) {
- return _.find(CRM.vars.search.schema, {name: entityName});
- }
- }
- function getField(fieldName, entityName) {
- var dotSplit = fieldName.split('.'),
- joinEntity = dotSplit.length > 1 ? dotSplit[0] : null,
- name = _.last(dotSplit).split(':')[0];
- // Custom fields contain a dot in their fieldname
- // If 3 segments, the first is the joinEntity and the last 2 are the custom field
- if (dotSplit.length === 3) {
- name = dotSplit[1] + '.' + name;
- }
- // If 2 segments, it's ambiguous whether this is a custom field or joined field. Search the main entity first.
- if (dotSplit.length === 2) {
- var field = _.find(getEntity(entityName).fields, {name: dotSplit[0] + '.' + name});
- if (field) {
- return field;
- }
- }
- if (joinEntity) {
- entityName = _.find(CRM.vars.search.links[entityName], {alias: joinEntity}).entity;
- }
- return _.find(getEntity(entityName).fields, {name: name});
- }
- return {
- getEntity: getEntity,
- getField: getField,
- parseExpr: function(expr) {
- var result = {fn: null, modifier: ''},
- fieldName = expr,
- bracketPos = expr.indexOf('(');
- if (bracketPos >= 0) {
- var parsed = expr.substr(bracketPos).match(/[ ]?([A-Z]+[ ]+)?([\w.:]+)/);
- fieldName = parsed[2];
- result.fn = _.find(CRM.vars.search.functions, {name: expr.substring(0, bracketPos)});
- result.modifier = _.trim(parsed[1]);
- }
- result.field = expr ? getField(fieldName, searchEntity) : undefined;
- if (result.field) {
- var split = fieldName.split(':'),
- prefixPos = split[0].lastIndexOf(result.field.name);
- result.path = split[0];
- result.prefix = prefixPos > 0 ? result.path.substring(0, prefixPos) : '';
- result.suffix = !split[1] ? '' : ':' + split[1];
- }
- return result;
- }
- };
- })
-
- // Reformat an array of objects for compatibility with select2
- // Todo this probably belongs in core
- .factory('formatForSelect2', function() {
- return function(input, key, label, extra) {
- return _.transform(input, function(result, item) {
- var formatted = {id: item[key], text: item[label]};
- if (extra) {
- _.merge(formatted, _.pick(item, extra));
- }
- result.push(formatted);
- }, []);
- };
- });
-
-})(angular, CRM.$, CRM._);
+++ /dev/null
-<div id="bootstrap-theme" class="crm-search">
- <h1 crm-page-title>{{:: ts('Search for %1', {1: $ctrl.getEntity($ctrl.entity).titlePlural}) }}</h1>
-
- <!--This warning will show if bootstrap is unavailable. Normally it will be hidden by the bootstrap .collapse class.-->
- <div class="messages warning no-popup collapse">
- <p>
- <i class="crm-i fa-exclamation-triangle" aria-hidden="true"></i>
- <strong>{{:: ts('Bootstrap theme not found.') }}</strong>
- </p>
- <p>{{:: ts('This screen may not work correctly without a bootstrap-based theme such as Shoreditch installed.') }}</p>
- </div>
-
- <form>
- <div ng-include="'~/search/crmSearch/criteria.html'"></div>
- <div ng-include="'~/search/crmSearch/controls.html'"></div>
- <div ng-include="'~/search/crmSearch/debug.html'" ng-if="$ctrl.debug"></div>
- <div ng-include="'~/search/crmSearch/results.html'" class="crm-search-results"></div>
- <div ng-include="'~/search/crmSearch/pager.html'"></div>
- </form>
-</div>
.crm-flex-box > .crm-flex-4 {
flex: 4;
}
+.crm-draggable {
+ cursor: move;
+}
+
#bootstrap-theme #crm-search-results-page-size {
width: 5em;
}
#bootstrap-theme .crm-search-results {
min-height: 200px;
}
-.crm-search-results thead th[ng-repeat] {
- cursor: pointer;
+
+#bootstrap-theme.crm-search .nav-stacked {
+ margin-left: 0;
+ margin-right: 20px;
}
-.crm-search-results thead th[ng-repeat] > span {
- cursor: move;
+
+#bootstrap-theme.crm-search ul.nav-stacked {
+ margin-top: 20px;
+}
+
+#bootstrap-theme.crm-search input.ng-invalid {
+ border-color: #8A1F11;
+}
+#bootstrap-theme.crm-search input.ng-invalid::placeholder {
+ color: #8A1F11;
+}
+
+#bootstrap-theme.crm-search ul.nav-stacked li {
+ cursor: default;
+}
+
+#bootstrap-theme.crm-search ul.nav-stacked li a[disabled] {
+ text-decoration: line-through !important;
+ color: grey;
+ cursor: default;
+ pointer-events: none;
}
#bootstrap-theme.crm-search fieldset {
#bootstrap-theme.crm-search th.crm-search-result-select {
padding-right: 10px;
}
+
+#bootstrap-theme .crm-search-delete-display {
+ position: absolute;
+ right: 0;
+ top: 0;
+}
<url desc="Issues">https://lab.civicrm.org/dev/report/-/issues</url>
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
</urls>
- <releaseDate>2020-11-07</releaseDate>
- <version>1.0.beta1</version>
+ <releaseDate>2020-12-02</releaseDate>
+ <version>1.0.beta2</version>
<develStage>beta</develStage>
<compatibility>
<ver>5.31</ver>
--- /dev/null
+<?php
+// Adds option group for SearchDisplay.type
+
+return [
+ [
+ 'name' => 'SearchDisplayType',
+ 'entity' => 'OptionGroup',
+ 'params' => [
+ 'name' => 'search_display_type',
+ 'title' => 'Search Display Type',
+ ],
+ ],
+ [
+ 'name' => 'SearchDisplayType:table',
+ 'entity' => 'OptionValue',
+ 'params' => [
+ 'option_group_id' => 'search_display_type',
+ 'name' => 'table',
+ 'value' => 'table',
+ 'label' => 'Table',
+ 'icon' => 'fa-table',
+ ],
+ ],
+];
* extension.
*/
class CRM_Search_ExtensionUtil {
- const SHORT_NAME = "search";
- const LONG_NAME = "org.civicrm.search";
- const CLASS_PREFIX = "CRM_Search";
+ const SHORT_NAME = 'search';
+ const LONG_NAME = 'org.civicrm.search';
+ const CLASS_PREFIX = 'CRM_Search';
/**
* Translate a string using the extension's domain.
* @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
*/
function _search_civix_civicrm_entityTypes(&$entityTypes) {
- $entityTypes = array_merge($entityTypes, []);
+ $entityTypes = array_merge($entityTypes, [
+ 'CRM_Search_DAO_SearchDisplay' => [
+ 'name' => 'SearchDisplay',
+ 'class' => 'CRM_Search_DAO_SearchDisplay',
+ 'table' => 'civicrm_search_display',
+ ],
+ ]);
}
}
/**
- * Implements hook_civicrm_thems().
+ * Implements hook_civicrm_themes().
*/
function search_civicrm_themes(&$themes) {
_search_civix_civicrm_themes($themes);
}
+
+/**
+ * Implements hook_civicrm_pre().
+ */
+function search_civicrm_pre($op, $entity, $id, &$params) {
+ // Supply default name/label when creating new SearchDisplay
+ if ($entity === 'SearchDisplay' && $op === 'create') {
+ if (empty($params['label'])) {
+ $params['label'] = $params['name'];
+ }
+ elseif (empty($params['name'])) {
+ $params['name'] = \CRM_Utils_String::munge($params['label']);
+ }
+ }
+}
+
+/**
+ * Injects settings data to search displays embedded in afforms
+ *
+ * @param \Civi\Angular\Manager $angular
+ * @see CRM_Utils_Hook::alterAngular()
+ */
+function search_civicrm_alterAngular($angular) {
+ $changeSet = \Civi\Angular\ChangeSet::create('searchSettings')
+ ->alterHtml(';\\.aff\\.html$;', function($doc, $path) {
+ $displayTypes = array_column(\Civi\Search\Display::getDisplayTypes(['name']), 'name');
+
+ if ($displayTypes) {
+ $componentNames = 'crm-search-display-' . implode(', crm-search-display-', $displayTypes);
+ foreach (pq($componentNames, $doc) as $component) {
+ $searchName = pq($component)->attr('search-name');
+ $displayName = pq($component)->attr('display-name');
+ if ($searchName && $displayName) {
+ $display = \Civi\Api4\SearchDisplay::get(FALSE)
+ ->addWhere('name', '=', $displayName)
+ ->addWhere('saved_search.name', '=', $searchName)
+ ->addSelect('settings', 'saved_search.api_entity', 'saved_search.api_params')
+ ->execute()->first();
+ if ($display) {
+ pq($component)->attr('settings', CRM_Utils_JS::encode($display['settings'] ?? []));
+ pq($component)->attr('api-entity', CRM_Utils_JS::encode($display['saved_search.api_entity']));
+ pq($component)->attr('api-params', CRM_Utils_JS::encode($display['saved_search.api_params']));
+ }
+ }
+ }
+ }
+ });
+ $angular->add($changeSet);
+
+}
--- /dev/null
+-- +--------------------------------------------------------------------+
+-- | Copyright CiviCRM LLC. All rights reserved. |
+-- | |
+-- | This work is published under the GNU AGPLv3 license with some |
+-- | permitted exceptions and without any warranty. For full license |
+-- | and copyright information, see https://civicrm.org/licensing |
+-- +--------------------------------------------------------------------+
+--
+-- Generated from schema.tpl
+-- DO NOT EDIT. Generated by CRM_Core_CodeGen
+--
+
+
+-- +--------------------------------------------------------------------+
+-- | Copyright CiviCRM LLC. All rights reserved. |
+-- | |
+-- | This work is published under the GNU AGPLv3 license with some |
+-- | permitted exceptions and without any warranty. For full license |
+-- | and copyright information, see https://civicrm.org/licensing |
+-- +--------------------------------------------------------------------+
+--
+-- Generated from drop.tpl
+-- DO NOT EDIT. Generated by CRM_Core_CodeGen
+--
+-- /*******************************************************
+-- *
+-- * Clean up the exisiting tables
+-- *
+-- *******************************************************/
+
+SET FOREIGN_KEY_CHECKS=0;
+
+DROP TABLE IF EXISTS `civicrm_search_display`;
+
+SET FOREIGN_KEY_CHECKS=1;
+-- /*******************************************************
+-- *
+-- * Create new tables
+-- *
+-- *******************************************************/
+
+-- /*******************************************************
+-- *
+-- * civicrm_search_display
+-- *
+-- * Search Kit - saved search displays
+-- *
+-- *******************************************************/
+CREATE TABLE `civicrm_search_display` (
+
+
+ `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique SearchDisplay ID',
+ `name` varchar(255) NOT NULL COMMENT 'Unique name for identifying search display',
+ `label` varchar(255) NOT NULL COMMENT 'Label for identifying search display to administrators',
+ `saved_search_id` int unsigned NOT NULL COMMENT 'FK to saved search table.',
+ `type` varchar(128) NOT NULL COMMENT 'Type of display',
+ `settings` text DEFAULT NULL COMMENT 'Configuration data for the search display'
+,
+ PRIMARY KEY (`id`)
+
+ , UNIQUE INDEX `UI_saved_search__id_name`(
+ saved_search_id
+ , name
+ )
+
+, CONSTRAINT FK_civicrm_search_display_saved_search_id FOREIGN KEY (`saved_search_id`) REFERENCES `civicrm_saved_search`(`id`) ON DELETE CASCADE
+) ;
+
+
\ No newline at end of file
--- /dev/null
+-- +--------------------------------------------------------------------+
+-- | Copyright CiviCRM LLC. All rights reserved. |
+-- | |
+-- | This work is published under the GNU AGPLv3 license with some |
+-- | permitted exceptions and without any warranty. For full license |
+-- | and copyright information, see https://civicrm.org/licensing |
+-- +--------------------------------------------------------------------+
+--
+-- Generated from drop.tpl
+-- DO NOT EDIT. Generated by CRM_Core_CodeGen
+--
+-- /*******************************************************
+-- *
+-- * Clean up the exisiting tables
+-- *
+-- *******************************************************/
+
+SET FOREIGN_KEY_CHECKS=0;
+
+DROP TABLE IF EXISTS `civicrm_search_display`;
+
+SET FOREIGN_KEY_CHECKS=1;
\ No newline at end of file
<menu>
<item>
<path>civicrm/search</path>
- <page_callback>CRM_Search_Page_Ang</page_callback>
+ <page_callback>CRM_Search_Page_Search</page_callback>
<access_arguments>access CiviCRM</access_arguments>
</item>
+ <item>
+ <path>civicrm/admin/search</path>
+ <page_callback>CRM_Search_Page_Admin</page_callback>
+ <access_arguments>administer CiviCRM</access_arguments>
+ </item>
</menu>
--- /dev/null
+<?php
+// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at:
+// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+return [
+ [
+ 'name' => 'SearchDisplay',
+ 'class' => 'CRM_Search_DAO_SearchDisplay',
+ 'table' => 'civicrm_search_display',
+ ],
+];
--- /dev/null
+<?xml version="1.0" encoding="iso-8859-1" ?>
+
+<table>
+ <base>CRM/Search</base>
+ <class>SearchDisplay</class>
+ <name>civicrm_search_display</name>
+ <comment>Search Kit - saved search displays</comment>
+ <log>true</log>
+
+ <field>
+ <name>id</name>
+ <title>Search Display ID</title>
+ <type>int unsigned</type>
+ <required>true</required>
+ <comment>Unique SearchDisplay ID</comment>
+ <add>1.0</add>
+ </field>
+ <primaryKey>
+ <name>id</name>
+ <autoincrement>true</autoincrement>
+ <add>1.0</add>
+ </primaryKey>
+
+ <field>
+ <name>name</name>
+ <title>Search Display Name</title>
+ <comment>Unique name for identifying search display</comment>
+ <required>true</required>
+ <type>varchar</type>
+ <length>255</length>
+ <html>
+ <type>Text</type>
+ </html>
+ <add>1.0</add>
+ </field>
+
+ <field>
+ <name>label</name>
+ <title>Search Display Label</title>
+ <comment>Label for identifying search display to administrators</comment>
+ <required>true</required>
+ <type>varchar</type>
+ <length>255</length>
+ <html>
+ <type>Text</type>
+ </html>
+ <add>1.0</add>
+ </field>
+
+ <field>
+ <name>saved_search_id</name>
+ <type>int unsigned</type>
+ <title>Saved Search ID</title>
+ <comment>FK to saved search table.</comment>
+ <required>true</required>
+ <add>1.0</add>
+ </field>
+ <foreignKey>
+ <name>saved_search_id</name>
+ <table>civicrm_saved_search</table>
+ <key>id</key>
+ <add>1.0</add>
+ <onDelete>CASCADE</onDelete>
+ </foreignKey>
+
+ <index>
+ <name>UI_saved_search__id_name</name>
+ <fieldName>saved_search_id</fieldName>
+ <fieldName>name</fieldName>
+ <unique>true</unique>
+ <add>1.0</add>
+ </index>
+
+ <field>
+ <name>type</name>
+ <title>Search Display Type</title>
+ <required>true</required>
+ <type>varchar</type>
+ <length>128</length>
+ <comment>Type of display</comment>
+ <pseudoconstant>
+ <optionGroupName>search_display_type</optionGroupName>
+ </pseudoconstant>
+ <add>1.0</add>
+ <html>
+ <type>Select</type>
+ </html>
+ </field>
+
+ <field>
+ <name>settings</name>
+ <type>text</type>
+ <title>Search Display Settings</title>
+ <comment>Configuration data for the search display</comment>
+ <serialize>JSON</serialize>
+ <default>NULL</default>
+ <add>1.0</add>
+ </field>
+
+</table>
opts = placeholder || placeholder === '' ? '' : '[value!=""]';
$elect.find('option' + opts).remove();
var newOptions = CRM.utils.renderOptions(options, val);
+ if (options.length == 0) {
+ $elect.removeClass('required');
+ } else if ($elect.hasClass('crm-field-required') && !$elect.hasClass('required')) {
+ $elect.addClass('required');
+ }
if (typeof placeholder === 'string') {
if ($elect.is('[multiple]')) {
select.attr('placeholder', placeholder);
var extra = {
expires: 0
- };
+ }, label;
if ($(this).length) {
if (title === '') {
- var label = $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
+ label = $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
if (label.length) {
label.addClass('crm-error');
var $label = label.clone();
ele.one('change', function () {
if (msg && msg.close) msg.close();
ele.removeClass('crm-error');
- label.removeClass('crm-error');
+ if (label) {
+ label.removeClass('crm-error');
+ }
});
}, 1000);
}
* https://github.com/civicrm/civicrm-joomla
* https://github.com/civicrm/civicrm-wordpress
+## CiviCRM 5.32.0
+
+Released December 2, 2020
+
+- **[Synopsis](release-notes/5.32.0.md#synopsis)**
+- **[Features](release-notes/5.32.0.md#features)**
+- **[Bugs resolved](release-notes/5.32.0.md#bugs)**
+- **[Miscellany](release-notes/5.32.0.md#misc)**
+- **[Credits](release-notes/5.32.0.md#credits)**
+- **[Feedback](release-notes/5.32.0.md#feedback)**
+
+## CiviCRM 5.31.0
+
+Released November 4, 2020
+
+- **[Synopsis](release-notes/5.31.0.md#synopsis)**
+- **[Features](release-notes/5.31.0.md#features)**
+- **[Bugs resolved](release-notes/5.31.0.md#bugs)**
+- **[Miscellany](release-notes/5.31.0.md#misc)**
+- **[Credits](release-notes/5.31.0.md#credits)**
+- **[Feedback](release-notes/5.31.0.md#feedback)**
+
+## CiviCRM 5.30.1
+
+Released October 21, 2020
+
+- **[Synopsis](release-notes/5.30.1.md#synopsis)**
+- **[Bugs resolved](release-notes/5.30.1.md#bugs)**
+- **[Credits](release-notes/5.30.1.md#credits)**
+- **[Feedback](release-notes/5.30.1.md#feedback)**
+
## CiviCRM 5.30.0
Released October 7, 2020
--- /dev/null
+# CiviCRM 5.30.1
+
+Released October 21, 2020
+
+- **[Synopsis](#synopsis)**
+- **[Bugs resolved](#bugs)**
+- **[Credits](#credits)**
+- **[Feedback](#feedback)**
+
+## <a name="synopsis"></a>Synopsis
+
+| *Does this version...?* | |
+| --------------------------------------------------------------- | -------- |
+| **Change the database schema?** | **yes** |
+| Alter the API? | no |
+| Require attention to configuration options? | no |
+| Fix problems installing or upgrading to a previous version? | no |
+| Introduce features? | no |
+| **Fix bugs?** | **yes** |
+
+## <a name="bugs"></a>Bugs resolved
+
+* **_CiviMail_: Recently deleted contacts still receive email ([dev/core#2119](https://lab.civicrm.org/dev/core/-/issues/2119): [#18763](https://github.com/civicrm/civicrm-core/pull/18763))**
+* **_Contact Dashboard_: Authenticated user cannot see their own events ([dev/event#43](https://lab.civicrm.org/dev/event/-/issues/43): [#18758](https://github.com/civicrm/civicrm-core/pull/18758))**
+* **_Groups_: Styling error when selecting groups ([dev/core#2105](https://lab.civicrm.org/dev/core/-/issues/2105): [#18719](https://github.com/civicrm/civicrm-core/pull/18719))**
+* **_Relationships_: Error editing "Relationship Types" when using utf8mb4 ([#18721](https://github.com/civicrm/civicrm-core/pull/18721), [#18751](https://github.com/civicrm/civicrm-core/pull/18751))**
+
+## <a name="credits"></a>Credits
+
+This release was developed by the following authors and reviewers:
+
+Wikimedia Foundation - Eileen McNaughton; Rar9; Megaphone Technology Consulting - Jon
+Goldberg; MJCO - Mikey O'Toole; Lighthouse Consulting and Design - Brian Shaughnessy; JMA
+Consulting - Seamus Lee, Monish Deb; Fuzion - Luke Stewart; Dave D; Coop SymbioTIC -
+Mathieu Lutfy; CiviCRM - Coleman Watts, Tim Otten; Artful Robot - Rich Lott; Andy Clarke;
+
+## <a name="feedback"></a>Feedback
+
+These release notes are edited by Tim Otten and Andrew Hunt. If you'd like to
+provide feedback on them, please login to https://chat.civicrm.org/civicrm and
+contact `@agh1`.
--- /dev/null
+# CiviCRM 5.31.0
+
+Released November 4, 2020
+
+- **[Synopsis](#synopsis)**
+- **[Features](#features)**
+- **[Bugs resolved](#bugs)**
+- **[Miscellany](#misc)**
+- **[Credits](#credits)**
+- **[Feedback](#feedback)**
+
+## <a name="synopsis"></a>Synopsis
+
+| *Does this version...?* | |
+|:--------------------------------------------------------------- |:-------:|
+| Fix security vulnerabilities? | no |
+| **Change the database schema?** | **yes** |
+| **Alter the API?** | **yes** |
+| **Require attention to configuration options?** | **yes** |
+| **Fix problems installing or upgrading to a previous version?** | **yes** |
+| **Introduce features?** | **yes** |
+| **Fix bugs?** | **yes** |
+
+## <a name="features"></a>Features
+
+### Core CiviCRM
+
+- **Implement more nuanced "Administer CiviCRM" permisions
+ ([16482](https://github.com/civicrm/civicrm-core/pull/16482) and
+ [18671](https://github.com/civicrm/civicrm-core/pull/18671))**
+
+ Actions that required the "Administer CiviCRM" permission now require one of
+ two separate permissions: "administer CiviCRM system" and "administer CiviCRM
+ data". The "Administer CiviCRM" permission still exists, and users having it
+ are treated as implicitly having both of the new permissions.
+
+ However, it is now possible to grant permission to configure profiles,
+ scheduled reminders, and set admin-only price options independently of
+ granting permission to configure scheduled jobs, install extensions, and view
+ the system check. An organization might grant the former to senior staff and
+ the latter to technical staff.
+
+- **Buttonrama ([18410](https://github.com/civicrm/civicrm-core/pull/18410),
+ [18820](https://github.com/civicrm/civicrm-core/pull/18820),
+ [18834](https://github.com/civicrm/civicrm-core/pull/18834),
+ [18799](https://github.com/civicrm/civicrm-core/pull/18799), and
+ [307](https://github.com/civicrm/civicrm-packages/pull/307))**
+
+ This ensures icons and text within buttons are aligned vertically, and it
+ makes form buttons appear consistent with links that are rendered to appear
+ like buttons.
+
+ Specifically, most buttons are now `<button>` elements rather than `<input
+ type="button">`, and button styling now applies to the button itself rather
+ than a wrapper. Extension and theme developers should confirm that CSS and
+ DOM selectors accurately identify the intended button elements.
+
+- **Custom field form reform
+ ([18419](https://github.com/civicrm/civicrm-core/pull/18419))**
+
+ This improves the form for creating or updating custom fields by improving
+ validation, making defaults easier to select, and allowing more flexibility
+ around changing the widget type.
+
+- **Add higher-level support for "bundles" and "collections" of resources
+ ([18247](https://github.com/civicrm/civicrm-core/pull/18247))**
+
+ "Resources" refers to CSS, Javascript, and DOM variables that developers can
+ add to certain pages. These can now be bundled together to reduce redundant
+ code and can be modified with the new `hook_civicrm_alterBundle()`.
+
+ The core styles and other resources are now included as bundles, allowing them
+ to be modified in a standard way.
+
+ In addition, page regions, bundles, and (to some extent) page resources are
+ now treated as "collections" which share a common interface for adding and
+ retrieving individual resources.
+
+- **Bootstrap3 CSS
+ ([dev/user-interface#27](https://lab.civicrm.org/dev/user-interface/-/issues/27):
+ [18354](https://github.com/civicrm/civicrm-core/pull/18354),
+ [18465](https://github.com/civicrm/civicrm-core/pull/18465),
+ [18550](https://github.com/civicrm/civicrm-core/pull/18550),
+ [18583](https://github.com/civicrm/civicrm-core/pull/18583), and
+ [18579](https://github.com/civicrm/civicrm-core/pull/18579))**
+
+ CiviCRM introduced a theming system several years ago, but the existing look
+ and feel was left as a "default", with the *de facto* theming code spread
+ throughout the application. The new themes have generally used Bootstrap 3 as
+ a user interface framework.
+
+ A handful of new CiviCRM features have been developed that depend on Bootstrap
+ 3 for core functionality or at least for basic look and feel. Sites lacking a
+ newer theme need a way to load at least a minimal set of Bootstrap 3 code for
+ these features to be functional and attractive.
+
+ This change introduces a new CiviCRM theme extension, named "Greenwich", that
+ is enabled and hidden by default for all sites. It provides Bootstrap 3 when
+ needed. In addition, it can serve as a vehicle for moving theming code out of
+ core.
+
+- **APIv4 Search: Improve GROUP_CONCAT with :label prefix
+ ([18572](https://github.com/civicrm/civicrm-core/pull/18572))**
+
+ This improves the Search UI by exposing the DISTINCT modifier and fixing
+ currency formatting.
+
+- **Search ext: rename to Search Kit, mark as beta
+ ([18672](https://github.com/civicrm/civicrm-core/pull/18672))**
+
+ An extension to replace the search user interface has been included, but
+ hidden, in CiviCRM for several months. Named Search Kit, it is now available
+ to be enabled for sites.
+
+- **Search extension: edit smart groups
+ ([18431](https://github.com/civicrm/civicrm-core/pull/18431))**
+
+ Search Kit can now edit smart groups. When installed, the "edit smart group
+ criteria" link will open the classic search forms or Search Kit as
+ appropriate.
+
+- **Search ext: support complex joins & HAVING clause in api4 smart groups
+ ([18644](https://github.com/civicrm/civicrm-core/pull/18644))**
+
+ Improves the new search extension and APIv4 smart groups in core to support
+ any entity that can join with Contact, including full support for calculated
+ fields and the HAVING clause.
+
+- **Select field fixes for screen reader
+ ([17675](https://github.com/civicrm/civicrm-core/pull/17675),
+ [18873](https://github.com/civicrm/civicrm-core/pull/18873),
+ [18889](https://github.com/civicrm/civicrm-core/pull/18889))**
+
+ The placeholder text for select drop-down fields now reflects the field label.
+ The Select2 widget makes it difficult for screen readers to identify the
+ field's label, so this helps identify the field for users who rely on screen
+ readers.
+
+- **Add modified_date to list of activity tokens
+ ([18611](https://github.com/civicrm/civicrm-core/pull/18611))**
+
+ Adds Modified date to the list of available activity tokens.
+
+- **Add an 'Execute Now' button to the job log
+ ([18593](https://github.com/civicrm/civicrm-core/pull/18593))**
+
+ Adds an "Execute Now" button to the Job log to make it easier to rerun a
+ scheduled job if needed.
+
+- **Send email to contacts when clicking on their email address on the contact's
+ card ([dev/core#1790](https://lab.civicrm.org/dev/core/-/issues/1790):
+ [18623](https://github.com/civicrm/civicrm-core/pull/18623))**
+
+ Improves the contact card by making the email a link which takes the user to a
+ form to send an email to that contact.
+
+- **Ability to Search Smart or Normal Group using additional filter on Manage
+ Group page ([dev/report#45](https://lab.civicrm.org/dev/report/-/issues/45):
+ [18379](https://github.com/civicrm/civicrm-core/pull/18379) and
+ [18246](https://github.com/civicrm/civicrm-core/pull/18246))**
+
+ Adds a filter "Group Type" to the Manage Groups page which can be used
+ to filter by normal or smart groups.
+
+- **Ability to Send Invoice with modified subject and CC it
+ ([dev/user-interface#30](https://lab.civicrm.org/dev/user-interface/-/issues/30):
+ [18286](https://github.com/civicrm/civicrm-core/pull/18286))**
+
+ Adds the ability to edit the subject and cc fields when emailing an invoice
+ from a contribution.
+
+- **Add ability to segment query logs
+ ([dev/core#2032](https://lab.civicrm.org/dev/core/-/issues/2032):
+ [18471](https://github.com/civicrm/civicrm-core/pull/18471) and
+ [309](https://github.com/civicrm/civicrm-packages/pull/309))**
+
+ SQL queries can be sent to a debugging log when the `CIVICRM_DEBUG_LOG_QUERY`
+ environment variable is set. Now, the value of that variable can specify a
+ file name for the log.
+
+### CiviContribute
+
+- **Move ACls on LineItem create to financialacls core extension
+ ([18339](https://github.com/civicrm/civicrm-core/pull/18339))**
+
+ Simplifies the code base by moving the financial ACL handling from the
+ LineItem BAO to the financialacls core extension.
+
+- **Convert core processors to use Guzzle and bring them under CI (Work Towards
+ [dev/financial#143](https://lab.civicrm.org/dev/financial/-/issues/143):
+ [18350](https://github.com/civicrm/civicrm-core/pull/18350))**
+
+ The PayPal Pro payment processor integration now uses the Guzzle library for
+ HTTP requests. This improves consistency and allows for unit testing of the
+ request handling.
+
+- **Migrate Eway(Single Currency) Payment Processor Type out into its own
+ extension ([18349](https://github.com/civicrm/civicrm-core/pull/18349))**
+
+ The Eway payment processor is now a separate extension, albeit shipped with
+ core.
+
+- **Make 'Record Payment' & 'Record Refund' visible regardless of whether the
+ balance 'requires' one
+ ([dev/financial#86](https://lab.civicrm.org/dev/financial/-/issues/86):
+ [18417](https://github.com/civicrm/civicrm-core/pull/18417))**
+
+ Makes it so the "Record Payment" and "Record Refund" links on Contributions
+ are always visible.
+
+- **Alter the default of send notification to contributor checkbox on cancel or
+ edit recurring to off
+ ([dev/core#1986](https://lab.civicrm.org/dev/core/-/issues/1986):
+ [18537](https://github.com/civicrm/civicrm-core/pull/18537))**
+
+ The "notify contributor" checkbox on the form to cancel or edit a recurring
+ donation is now unchecked by default.
+
+- **PCP action links support for hook_civicrm_links
+ ([dev/core#2061](https://lab.civicrm.org/dev/core/-/issues/2061):
+ [18570](https://github.com/civicrm/civicrm-core/pull/18570))**
+
+ This allows `hook_civicrm_links` to be used by extension developers to modify the
+ list of actions offered to personal campaign page creators.
+
+- **Add Line Item v4 API
+ ([dev/core#1980](https://lab.civicrm.org/dev/core/-/issues/1980):
+ [18388](https://github.com/civicrm/civicrm-core/pull/18388) and
+ [18352](https://github.com/civicrm/civicrm-core/pull/18352))**
+
+ Adds the "Line Item" entity to APIv4.
+
+- **Improve metadata on LineItem DAO
+ ([18521](https://github.com/civicrm/civicrm-core/pull/18521))**
+
+ Adds labels to line item meta data.
+
+### CiviMail
+
+- **Add options to Mail Account settings to improve inbound mail processing
+ ([18624](https://github.com/civicrm/civicrm-core/pull/18624))**
+
+ Two new options are added to the Mail Account settings form to improve inbound
+ email processing:
+
+ 1. 'Skip emails which do not have a Case ID or Case token'
+ 2. 'Do not create new contacts when filing emails'
+
+- **Change wording on the Opt Out and Unsubscribe pages
+ ([18338](https://github.com/civicrm/civicrm-core/pull/18338))**
+
+ Improves messaging to end user on Opt Out and Unsubscribe pages.
+
+### CiviMember
+
+- **Add custom field groups to Membership Contribution Detail report
+ ([dev/report#49](https://lab.civicrm.org/dev/report/-/issues/49):
+ [18420](https://github.com/civicrm/civicrm-core/pull/18420))**
+
+ Contact custom fields are now available on the Membership Contribution Detail
+ report.
+
+### Drupal Integration
+
+- **Drupal 9 deprecations
+ ([dev/drupal#138](https://lab.civicrm.org/dev/drupal/-/issues/138):
+ [18461](https://github.com/civicrm/civicrm-core/pull/18461))**
+
+ Drupal now allows for smooth upgrades between major versions by gradually
+ introducing new API functions and deprecating others through the cycle of a
+ major version. A new major version starts by simply removing deprecated
+ function from the latest release of the prior major version.
+
+ This removes the use of a number of functions that have been deprecated since
+ Drupal 8.5 and are removed in Drupal 9. The result is that CiviCRM 5.31 is no
+ longer compatible with Drupal versions prior to 8.5 but is compatible with
+ Drupal 9.
+
+- **Finish allowing use of SSL to connect to database
+ (Work Towards [dev/core#1926](https://lab.civicrm.org/dev/core/-/issues/1926):
+ [18264](https://github.com/civicrm/civicrm-core/pull/18264))**
+
+ The setup screen now attempts to identify if the Drupal database connection
+ uses SSL and fills the configuration options to match.
+
+- **drush civicrm-ext-list add ext's status filter and show version number
+ ([597](https://github.com/civicrm/civicrm-drupal/pull/597))**
+
+ Extends the drush command `drush civicrm-ext-list` so that users can:
+
+ - Filter by the extension's status (installed, uninstalled, disabled)
+ - Show the extension version number in the result list
+ - Use the `--out` option to print results as json or as a pretty table
+
+## <a name="bugs"></a>Bugs resolved
+
+### Core CiviCRM
+
+- **"Network Error" when sorting contact search results by City, Postcode or
+ Country ([dev/core#2132](https://lab.civicrm.org/dev/core/-/issues/2132):
+ [18857](https://github.com/civicrm/civicrm-core/pull/18857))**
+
+- **Public contribution form and Checksums: billing information not loaded if
+ using multiple processors
+ ([dev/core#334](https://lab.civicrm.org/dev/core/-/issues/334):
+ [18642](https://github.com/civicrm/civicrm-core/pull/18642))**
+
+ Custom data tables for contacts are now created with the charset and collation
+ to match the `civicrm_contact` table.
+
+ This resolves a bug when the billing information is not filled when visiting a
+ contribution page with multiple payment processors via a checksum link.
+
+- **Deadlocks on acl_cache
+ (Work Towards [dev/core#1486](https://lab.civicrm.org/dev/core/-/issues/1486):
+ [18403](https://github.com/civicrm/civicrm-core/pull/18403))**
+
+ Removes foreign keys from the ACL cache tables as they are likely to
+ hinder performance.
+
+- **APIv4 - revisit required parameters on location entities
+ ([dev/core#2044](https://lab.civicrm.org/dev/core/-/issues/2044):
+ [18575](https://github.com/civicrm/civicrm-core/pull/18575))**
+
+ Makes it so that APIv4 can be used for creating event locations by making
+ contact_id optional for the Address, Phone and Email entities.
+
+- **Eliminate "No extensions available for this version of CiviCRM"
+ ([dev/core#2063](https://lab.civicrm.org/dev/core/-/issues/2063):
+ [18596](https://github.com/civicrm/civicrm-core/pull/18596))**
+
+ Fixes warning thrown when no public extensions directory is found.
+
+- **Group ids in profile fields are not correct
+ ([dev/core#2125](https://lab.civicrm.org/dev/core/-/issues/2125):
+ [18776](https://github.com/civicrm/civicrm-core/pull/18776))**
+
+ Fixes DB errors when using the groups field in a profile.
+
+- **Drupal 7 + 9 Groups dont show in edit with version 5.30.1
+ ([dev/core#2136](https://lab.civicrm.org/dev/core/-/issues/2136):
+ [18831](https://github.com/civicrm/civicrm-core/pull/18831))**
+
+- **Rebuild triggers after utf8mb4 conversion
+ ([18751](https://github.com/civicrm/civicrm-core/pull/18751))**
+
+ When the `System.utf8conversion` API call is run, this ensures that the
+ triggers for relationship_cache are updated to use the right encoding.
+
+- **Remove explicit COLLATE utf8_bin from RelationshipCache trigger
+ ([18721](https://github.com/civicrm/civicrm-core/pull/18721))**
+
+ Ensure that relationship types can be edited after switching to utf8mb4.
+
+- **Fix way of identifying custom serialized fields
+ ([18360](https://github.com/civicrm/civicrm-core/pull/18360))**
+
+ Removes references to field types that no longer exist, specifically the
+ 'Multi-Select', 'Multi-Select State/Province', and 'Multi-Select Country',
+ custom field types, which were all removed in 5.27.
+
+- **Exclude api4 from IDS check
+ ([18695](https://github.com/civicrm/civicrm-core/pull/18695))**
+
+ Fixes false-positive "suspicious activity" warnings in the IDS (Intrusion
+ Detection System) when using APIv4.
+
+- **Fix complexity on cache key
+ ([18650](https://github.com/civicrm/civicrm-core/pull/18650))**
+
+ Ensures that the cacheKey does not cross-populate values from different users.
+
+- **Use title instead name in status message
+ ([18406](https://github.com/civicrm/civicrm-core/pull/18406))**
+
+ Fixes help text when an option group is saved to show the title of the option
+ group instead of the name.
+
+- **Export fix on long custom fields
+ ([18146](https://github.com/civicrm/civicrm-core/pull/18146))**
+
+ This resolves problems exporting custom fields that have long option values.
+
+- **Contact form task delete php spelling fix
+ ([18399](https://github.com/civicrm/civicrm-core/pull/18399))**
+
+- **Component Titles are not translated on the Configuration Checklist page
+ ([dev/translation#54](https://lab.civicrm.org/dev/translation/-/issues/54):
+ [18690](https://github.com/civicrm/civicrm-core/pull/18690))**
+
+- **Error when viewing contact merged to permanently deleted contact
+ ([dev/core#1838](https://lab.civicrm.org/dev/core/-/issues/1838):
+ [18564](https://github.com/civicrm/civicrm-core/pull/18564))**
+
+ When one contact is merged to another contact, the first contact remains in
+ the trash and refers to the second. However, there was a bug preventing that
+ first contact from being viewed if the second contact was deleted permanently.
+
+- **E_WARNING when editing custom field with trigger-based logging turned on
+ ([dev/core#1989](https://lab.civicrm.org/dev/core/-/issues/1989):
+ [18386](https://github.com/civicrm/civicrm-core/pull/18386))**
+
+- **Northern Ireland / Wales counties are out of date
+ ([dev/core#2027](https://lab.civicrm.org/dev/core/-/issues/2027):
+ [18470](https://github.com/civicrm/civicrm-core/pull/18470))**
+
+- **Multiple email activity cc recipients get scrunched together in recorded
+ activity details field
+ ([dev/core#2040](https://lab.civicrm.org/dev/core/-/issues/2040):
+ [18504](https://github.com/civicrm/civicrm-core/pull/18504))**
+
+- **When exporting for composer-style deployment, exclude the `.gitignore` file
+ ([18673](https://github.com/civicrm/civicrm-core/pull/18673))**
+
+- **Fix patently silly code
+ ([18652](https://github.com/civicrm/civicrm-core/pull/18652))**
+
+- **Fix cache bypass
+ ([18643](https://github.com/civicrm/civicrm-core/pull/18643))**
+
+- **Fix bug in primary handling where TRUE rather than 1 used
+ ([18598](https://github.com/civicrm/civicrm-core/pull/18598))**
+
+- **Greenwich - fix conflict btw bootstrap & jQuery UI button
+ ([18696](https://github.com/civicrm/civicrm-core/pull/18696))**
+
+- **Remove double exception handling in repeattransaction
+ ([18594](https://github.com/civicrm/civicrm-core/pull/18594))**
+
+- **Preferred Language in a profile doesn't show/behave as required when so
+ configured ([dev/core#1883](https://lab.civicrm.org/dev/core/-/issues/1883):
+ [18595](https://github.com/civicrm/civicrm-core/pull/18595))**
+
+- **Fix deprecation notice
+ ([18541](https://github.com/civicrm/civicrm-core/pull/18541))**
+
+- **Replace '&' to 'and' in button label
+ ([18405](https://github.com/civicrm/civicrm-core/pull/18405))**
+
+- **Performance - meta issue for hunting down memory leaks
+ ([dev/core#2073](https://lab.civicrm.org/dev/core/-/issues/2073):
+ [18640](https://github.com/civicrm/civicrm-core/pull/18640),
+ [18701](https://github.com/civicrm/civicrm-core/pull/18701),
+ [18699](https://github.com/civicrm/civicrm-core/pull/18699),
+ [18702](https://github.com/civicrm/civicrm-core/pull/18702),
+ [18700](https://github.com/civicrm/civicrm-core/pull/18700),
+ [18692](https://github.com/civicrm/civicrm-core/pull/18692),
+ [18693](https://github.com/civicrm/civicrm-core/pull/18693),
+ [18633](https://github.com/civicrm/civicrm-core/pull/18633),
+ [18641](https://github.com/civicrm/civicrm-core/pull/18641) and
+ [18632](https://github.com/civicrm/civicrm-core/pull/18632))**
+
+- **E_NOTICE viewing an activity that has no details contents
+ ([dev/core#2075](https://lab.civicrm.org/dev/core/-/issues/2075):
+ [18637](https://github.com/civicrm/civicrm-core/pull/18637))**
+
+- **Undefined index on contact's activity tab when there's an activity that has
+ no With Contact
+ ([dev/core#2090](https://lab.civicrm.org/dev/core/-/issues/2090):
+ [18669](https://github.com/civicrm/civicrm-core/pull/18669))**
+
+- **Undefined index 'class' on new individual form
+ ([dev/core#2093](https://lab.civicrm.org/dev/core/-/issues/2093):
+ [18678](https://github.com/civicrm/civicrm-core/pull/18678))**
+
+- **Deprecation warnings when making thank-you letters
+ ([dev/core#2108](https://lab.civicrm.org/dev/core/-/issues/2108):
+ [18717](https://github.com/civicrm/civicrm-core/pull/18717) and
+ [18716](https://github.com/civicrm/civicrm-core/pull/18716))**
+
+ This affects all PDF letters.
+
+- **Fix the output of the full text custom search form
+ ([18890](https://github.com/civicrm/civicrm-core/pull/18890))**
+
+ This resolves styling issues on the full text search form.
+
+- **For countries without a province N/A is not accepted as a state in a profile
+ ([dev/core#2149](https://lab.civicrm.org/dev/core/-/issues/2149):
+ [18877](https://github.com/civicrm/civicrm-core/pull/18877))**
+
+- **IN operator not working in Search
+ ([dev/core#2147](https://lab.civicrm.org/dev/core/-/issues/2147):
+ [18898](https://github.com/civicrm/civicrm-core/pull/18898))**
+
+ This changes the group search field in the basic "Find Contacts" search back
+ to a plain select drop-down rather than a Select2 widget.
+
+### CiviCampaign
+
+- **Fix default report permissions when creating reports from CiviCampaign
+ ([18493](https://github.com/civicrm/civicrm-core/pull/18493))**
+
+ Ensures that CiviCampaign report titles are not accessible to users without
+ proper permissions to view the report.
+
+### CiviCase
+
+- **Incorrect comparison of status_id when changing status of linked cases
+ ([dev/core#1979](https://lab.civicrm.org/dev/core/-/issues/1979):
+ [18309](https://github.com/civicrm/civicrm-core/pull/18309))**
+
+### CiviContribute
+
+- **View Payment owned by Different contact on Membership and Participant View.
+ ([dev/report#48](https://lab.civicrm.org/dev/report/-/issues/48):
+ [18281](https://github.com/civicrm/civicrm-core/pull/18281))**
+
+ This ensures that related payments are displayed when viewing a Membership or
+ Participant even if they come from a different contact.
+
+- **Dropdown for country seems to have reverted to a regular select instead of
+ select2 ([dev/core#2030](https://lab.civicrm.org/dev/core/-/issues/2030):
+ [18533](https://github.com/civicrm/civicrm-core/pull/18533))**
+
+ This resolves a bug in the display of the Country field in the billing address
+ section of a contribution page.
+
+- **Fix formatLocaleNumericRoundedByCurrency
+ ([18409](https://github.com/civicrm/civicrm-core/pull/18409))**
+
+ Ensures that currencies are rounded to the correct decimal point instead of
+ always 2 decimal points.
+
+- **change civicrm_price_set.min_amount to float
+ ([18677](https://github.com/civicrm/civicrm-core/pull/18677))**
+
+ Updates the price set minimum amount field to be float (not int).
+
+- **Incorrect rounding up with priceset fields
+ ([dev/core#2003](https://lab.civicrm.org/dev/core/-/issues/2003):
+ [18297](https://github.com/civicrm/civicrm-core/pull/18297) and
+ [18416](https://github.com/civicrm/civicrm-core/pull/18416))**
+
+ Ensures the amount is saved to the database correctly for price field values
+ when a value is entered longer than two decimals.
+
+- **Display url_site and url_recur based on if the form elements exist
+ ([18324](https://github.com/civicrm/civicrm-core/pull/18324))**
+
+ Ensures developers can remove fields from the payment processor configuration
+ form using the build form hook.
+
+- **LineItem pre Hook non-standard on edit
+ ([dev/core#1994](https://lab.civicrm.org/dev/core/-/issues/1994):
+ [18340](https://github.com/civicrm/civicrm-core/pull/18340))**
+
+ This resolves a bug in the entity ID sent to `hook_civicrm_pre` when editing
+ line items.
+
+- **Performance - do not retrieve soft credits & pcps when not required
+ ([dev/core#2056](https://lab.civicrm.org/dev/core/-/issues/2056):
+ [18556](https://github.com/civicrm/civicrm-core/pull/18556))**
+
+- **Remove ajax timeout from contribution page on behalf of
+ ([18140](https://github.com/civicrm/civicrm-core/pull/18140))**
+
+- **property bag's setAmount should ensure dot decimal point
+ ([18429](https://github.com/civicrm/civicrm-core/pull/18429))**
+
+### CiviEvent
+
+- **Contact Dashboard does not show event registrations for non-admins
+ ([dev/event#43](https://lab.civicrm.org/dev/event/-/issues/43):
+ [18758](https://github.com/civicrm/civicrm-core/pull/18758))**
+
+- **Set participant status notification to false by default
+ ([18544](https://github.com/civicrm/civicrm-core/pull/18544))**
+
+ The "Send Notification" checkbox is now always unchecked when editing a participant's status. Previously it would be checked by default when changing a status to "Cancelled" or from "Waitlist" or "Pending waitlist".
+
+- **ParticipantListing Report: only display the View link for web, unhardcode
+ others ([18704](https://github.com/civicrm/civicrm-core/pull/18704))**
+
+ Ensures that when exporting the Participant Listing report the view links are
+ not included.
+
+- **Scheduled reminder: "Additional recipients" receive reminders under
+ circumstances where they ought not to
+ ([dev/core#1590](https://lab.civicrm.org/dev/core/-/issues/1590):
+ [17641](https://github.com/civicrm/civicrm-core/pull/17641))**
+
+ Ensures that "Additional recipients" do not receive reminders for deleted
+ events.
+
+- **Email & Phone storage issues in event location
+ ([dev/core#1973](https://lab.civicrm.org/dev/core/-/issues/1973):
+ [18488](https://github.com/civicrm/civicrm-core/pull/18488))**
+
+ Ensures second email and phone values are saved for event locations.
+
+- **Creating new event without email fails
+ ([dev/core#2096](https://lab.civicrm.org/dev/core/-/issues/2096):
+ [18710](https://github.com/civicrm/civicrm-core/pull/18710))**
+
+- **Changing address on event hangs
+ ([dev/core#2102](https://lab.civicrm.org/dev/core/-/issues/2102):
+ [18713](https://github.com/civicrm/civicrm-core/pull/18713))**
+
+### CiviGrant
+
+- **Grant dashboard counts trashed contacts
+ ([dev/core#2009](https://lab.civicrm.org/dev/core/-/issues/2009):
+ [18428](https://github.com/civicrm/civicrm-core/pull/18428))**
+
+### CiviMail
+
+- **Possible regression on deleted contacts
+ ([dev/core#2119](https://lab.civicrm.org/dev/core/-/issues/2119):
+ [18763](https://github.com/civicrm/civicrm-core/pull/18763))**
+
+ Ensures contacts deleted after a mailing is created do not get the mailing.
+
+### CiviMember
+
+- **Membership Renewal form re 'fixMembershipBeforeRenew'
+ ([dev/membership#27](https://lab.civicrm.org/dev/membership/-/issues/27):
+ [18621](https://github.com/civicrm/civicrm-core/pull/18621))**
+
+ The status of an existing membership is now recalculated prior to loading the
+ renewal form. This allows an accurate status to be displayed and used for
+ calculating renewal dates.
+
+- **Membership status does not get updated during membership import when status
+ override is set
+ ([dev/membership#30](https://lab.civicrm.org/dev/membership/-/issues/30):
+ [18821](https://github.com/civicrm/civicrm-core/pull/18821))**
+
+- **Bug When Restoring Overridden Status on Related Memberships
+ ([dev/core#1854](https://lab.civicrm.org/dev/core/-/issues/1854):
+ [17742](https://github.com/civicrm/civicrm-core/pull/17742))**
+
+ Ensures related memberships do not get deleted when running the membership
+ status calculation scheduled job.
+
+- **Fix for ongoing issues with static upsetting the apple cart
+ ([18245](https://github.com/civicrm/civicrm-core/pull/18245))**
+
+ Ensures that inherited relationships are created more reliably.
+
+- **Multiple Memberships Status Not updated when payment status changed from
+ pending to Completed
+ ([dev/core#1942](https://lab.civicrm.org/dev/core/-/issues/1942):
+ [18232](https://github.com/civicrm/civicrm-core/pull/18232))**
+
+- **Make period_type mandatory for MembershipType
+ ([18395](https://github.com/civicrm/civicrm-core/pull/18395))**
+
+### Backdrop Integration
+
+- **Check if BACKDROP_ROOT is defined already
+ ([18545](https://github.com/civicrm/civicrm-core/pull/18545))**
+
+ Fixes the "Constant BACKDROP_ROOT already defined..." notice.
+
+### Drupal Integration
+
+- **Make symfony aliased services public
+ ([18443](https://github.com/civicrm/civicrm-core/pull/18443))**
+
+ Fixes a warning on the extensions form for Drupal 8 sites.
+
+- **Tarball includes a symlink that goes nowhere, which causes alternate drupal
+ install method to fail
+ ([dev/core#1393](https://lab.civicrm.org/dev/core/-/issues/1393):
+ [18472](https://github.com/civicrm/civicrm-core/pull/18472),
+ [18605](https://github.com/civicrm/civicrm-core/pull/18605) and
+ [18659](https://github.com/civicrm/civicrm-core/pull/18659))**
+
+- **Exception handling - 'Allowed memory size' exhasted issues
+ ([dev/drupal#119](https://lab.civicrm.org/dev/drupal/-/issues/119):
+ [18610](https://github.com/civicrm/civicrm-core/pull/18610))**
+
+ Avoid crashes from recursion on unhandled exceptions (most often an issue in
+ Drupal 8).
+
+- **inheritLocale regression
+ ([dev/translation#51](https://lab.civicrm.org/dev/translation/-/issues/51):
+ [18447](https://github.com/civicrm/civicrm-core/pull/18447))**
+
+ Ensures that CiviCRM in multilingual mode respects the Drupal language.
+
+- **Do not block user incase 'Require approval' is checked
+ ([18329](https://github.com/civicrm/civicrm-core/pull/18329))**
+
+ Ensures users created via a profile are set to active in Drupal8 to prevent
+ issues with the email verification step.
+
+- **Fix customGroup getTableNameByEntityName to recognize all entities
+ ([18546](https://github.com/civicrm/civicrm-core/pull/18546))**
+
+ This ensures that all entities are recognized by Webform integration.
+
+- **Custom field values not showing in Drupal 7 Views filter
+ ([dev/core#1929](https://lab.civicrm.org/dev/core/-/issues/1929):
+ [611](https://github.com/civicrm/civicrm-drupal/pull/611))**
+
+- **Fix theme configuration section on Display preference and improve
+ `isFrontendPage` function for Drupal CMS
+ ([dev/core#1987](https://lab.civicrm.org/dev/core/-/issues/1987):
+ [18396](https://github.com/civicrm/civicrm-core/pull/18396) and
+ [18397](https://github.com/civicrm/civicrm-core/pull/18397))**
+
+- **Drupal 7 - Groups children now get shown with SPAN CSS error
+ ([dev/core#2105](https://lab.civicrm.org/dev/core/-/issues/2105):
+ [18719](https://github.com/civicrm/civicrm-core/pull/18719))**
+
+- **composer.json - Update compile-lib and compile-plugin
+ ([18670](https://github.com/civicrm/civicrm-core/pull/18670))**
+
+## <a name="misc"></a>Miscellany
+
+- **Take the guesswork out of rendering clientside CRM variables
+ ([18262](https://github.com/civicrm/civicrm-core/pull/18262))**
+
+- **Improve consistency of metadata type declarations
+ ([18147](https://github.com/civicrm/civicrm-core/pull/18147))**
+
+- **Use eventID rather than the object in completeTransaction
+ ([18358](https://github.com/civicrm/civicrm-core/pull/18358))**
+
+- **Load event title from participantID
+ ([18376](https://github.com/civicrm/civicrm-core/pull/18376))**
+
+- **Fix Invoice class to not call validateData
+ ([18372](https://github.com/civicrm/civicrm-core/pull/18372))**
+
+- **Finish deprecating BaseIPN->completeTransaction
+ ([18381](https://github.com/civicrm/civicrm-core/pull/18381))**
+
+- **Add postAssert to check payments and contributions are valid on all tests.
+ ([18317](https://github.com/civicrm/civicrm-core/pull/18317))**
+
+- **Switch frontend contribution form to cached/non-deprecated functions for
+ membershipTypes
+ ([18404](https://github.com/civicrm/civicrm-core/pull/18404))**
+
+- **Ensure DAO base class contains functions to be removed from generated files
+ ([18492](https://github.com/civicrm/civicrm-core/pull/18492))**
+
+- **Switch backend membership form to use non-deprecated/cached functions to get
+ membership types
+ ([18427](https://github.com/civicrm/civicrm-core/pull/18427))**
+
+- **Fix civi version for greenwich
+ ([18542](https://github.com/civicrm/civicrm-core/pull/18542))**
+
+- **Switch to passing payment_processor_id as input param to completeOrder
+ ([18528](https://github.com/civicrm/civicrm-core/pull/18528))**
+
+- **Switch membership BAO to use non-deprecated cached functions to get
+ membershipType details
+ ([18515](https://github.com/civicrm/civicrm-core/pull/18515))**
+
+- **Separate export into separate classes to allow unravelling of component
+ handling (Member)
+ ([18512](https://github.com/civicrm/civicrm-core/pull/18512))**
+
+- **Simplify CRM_Core_BAO_Location::createLocBlock by moving eventLocation
+ specific handling back to the class
+ ([18578](https://github.com/civicrm/civicrm-core/pull/18578))**
+
+- **Update the post-upgrade thank you message to include URLs to CiviCRM
+ contributors, CiviCRM members and minor rewrite
+ ([18559](https://github.com/civicrm/civicrm-core/pull/18559))**
+
+- **Simplify call to loadRelatedObjects in repeat/completetransaction
+ ([18613](https://github.com/civicrm/civicrm-core/pull/18613))**
+
+- **Move membership tab add/submit membership buttons to PHP layer
+ ([18143](https://github.com/civicrm/civicrm-core/pull/18143))**
+
+- **Remove extraneous UF match queries
+ ([dev/core#2087](https://lab.civicrm.org/dev/core/-/issues/2087):
+ [18667](https://github.com/civicrm/civicrm-core/pull/18667) and
+ [18675](https://github.com/civicrm/civicrm-core/pull/18675))**
+
+- **Can't send SMS to mailing group whose parent isn't a mailing group (Clean up
+ [dev/core#2053](https://lab.civicrm.org/dev/core/-/issues/2053):
+ [18698](https://github.com/civicrm/civicrm-core/pull/18698))**
+
+- **Merge - ensure location entities remaining on deleted contacts have
+ is_primary integrity
+ (Clean up [dev/core#2047](https://lab.civicrm.org/dev/core/-/issues/2047):
+ [18499](https://github.com/civicrm/civicrm-core/pull/18499) and
+ [18500](https://github.com/civicrm/civicrm-core/pull/18500))**
+
+- **[cq] Do not pass by reference where avoidable
+ (Work Towards [dev/core#2043](https://lab.civicrm.org/dev/core/-/issues/2043):
+ [18484](https://github.com/civicrm/civicrm-core/pull/18484) and
+ [18485](https://github.com/civicrm/civicrm-core/pull/18485))**
+
+- **Fix the Test Result (1 failure / -190)
+ E2E.Core.PrevNextTest.testDeleteByCacheKey recurring test issue
+ ([dev/core#2029](https://lab.civicrm.org/dev/core/-/issues/2029):
+ [18587](https://github.com/civicrm/civicrm-core/pull/18587) and
+ [18565](https://github.com/civicrm/civicrm-core/pull/18565))**
+
+- **SyntaxConformance::testSqlOperators cleanup fix - ensure entities are
+ deleted ([18569](https://github.com/civicrm/civicrm-core/pull/18569))**
+
+- **Move afform to be a core extension
+ ([dev/core#2000](https://lab.civicrm.org/dev/core/-/issues/2000):
+ [18423](https://github.com/civicrm/civicrm-core/pull/18423))**
+
+- **Upgrade Angular from 1.5 => 1.8
+ ([dev/core#1818](https://lab.civicrm.org/dev/core/-/issues/1818):
+ [18635](https://github.com/civicrm/civicrm-core/pull/18635))**
+
+- **Extraneous queries - activities (Work Towards
+ [dev/core#2057](https://lab.civicrm.org/dev/core/-/issues/2057):
+ [18625](https://github.com/civicrm/civicrm-core/pull/18625),
+ [18566](https://github.com/civicrm/civicrm-core/pull/18566),
+ [18609](https://github.com/civicrm/civicrm-core/pull/18609),
+ [18636](https://github.com/civicrm/civicrm-core/pull/18636) and
+ [18567](https://github.com/civicrm/civicrm-core/pull/18567))**
+
+- **Eliminate unused query on CRM_Core_BAO_CustomQuery::_construct
+ ([dev/core#2079](https://lab.civicrm.org/dev/core/-/issues/2079):
+ [18654](https://github.com/civicrm/civicrm-core/pull/18654),
+ [18653](https://github.com/civicrm/civicrm-core/pull/18653),
+ [18665](https://github.com/civicrm/civicrm-core/pull/18665),
+ [18664](https://github.com/civicrm/civicrm-core/pull/18664),
+ [18656](https://github.com/civicrm/civicrm-core/pull/18656),
+ [18657](https://github.com/civicrm/civicrm-core/pull/18657) and
+ [18655](https://github.com/civicrm/civicrm-core/pull/18655))**
+
+- **Rationalise BAO create vs add functions (Work Towards
+ [dev/core#2046](https://lab.civicrm.org/dev/core/-/issues/2046):
+ [18682](https://github.com/civicrm/civicrm-core/pull/18682),
+ [18658](https://github.com/civicrm/civicrm-core/pull/18658),
+ [18495](https://github.com/civicrm/civicrm-core/pull/18495),
+ [18588](https://github.com/civicrm/civicrm-core/pull/18588),
+ [18661](https://github.com/civicrm/civicrm-core/pull/18661),
+ [18607](https://github.com/civicrm/civicrm-core/pull/18607) and
+ [18606](https://github.com/civicrm/civicrm-core/pull/18606))**
+
+- **Address extraneous location queries
+ ([dev/core#2039](https://lab.civicrm.org/dev/core/-/issues/2039):
+ [18496](https://github.com/civicrm/civicrm-core/pull/18496),
+ [18498](https://github.com/civicrm/civicrm-core/pull/18498),
+ [18497](https://github.com/civicrm/civicrm-core/pull/18497),
+ [18494](https://github.com/civicrm/civicrm-core/pull/18494),
+ [18501](https://github.com/civicrm/civicrm-core/pull/18501),
+ [18480](https://github.com/civicrm/civicrm-core/pull/18480),
+ [18489](https://github.com/civicrm/civicrm-core/pull/18489),
+ [18684](https://github.com/civicrm/civicrm-core/pull/18684) and
+ [18663](https://github.com/civicrm/civicrm-core/pull/18663))**
+
+- **Remove unused functions (Work Towards
+ [dev/core#2017](https://lab.civicrm.org/dev/core/-/issues/2017):
+ [18662](https://github.com/civicrm/civicrm-core/pull/18662),
+ [18430](https://github.com/civicrm/civicrm-core/pull/18430),
+ [18433](https://github.com/civicrm/civicrm-core/pull/18433),
+ [18463](https://github.com/civicrm/civicrm-core/pull/18463),
+ [18458](https://github.com/civicrm/civicrm-core/pull/18458))**
+
+- **Remove unneccessary isoToDate function
+ ([dev/core#1921](https://lab.civicrm.org/dev/core/-/issues/1921):
+ [18422](https://github.com/civicrm/civicrm-core/pull/18422),
+ [18576](https://github.com/civicrm/civicrm-core/pull/18576),
+ [18359](https://github.com/civicrm/civicrm-core/pull/18359),
+ [18469](https://github.com/civicrm/civicrm-core/pull/18469),
+ [18456](https://github.com/civicrm/civicrm-core/pull/18456),
+ [18457](https://github.com/civicrm/civicrm-core/pull/18457),
+ [18374](https://github.com/civicrm/civicrm-core/pull/18374),
+ [18383](https://github.com/civicrm/civicrm-core/pull/18383) and
+ [18468](https://github.com/civicrm/civicrm-core/pull/18468))**
+
+- **Deprecate BaseIPN functions validateData & LoadObject (Work Towards
+ [dev/financial#148](https://lab.civicrm.org/dev/financial/-/issues/148):
+ [18479](https://github.com/civicrm/civicrm-core/pull/18479) and
+ [18571](https://github.com/civicrm/civicrm-core/pull/18571))**
+
+- **Separate out Search participant register form from backoffice form
+ ([dev/event#42](https://lab.civicrm.org/dev/event/-/issues/42):
+ [18486](https://github.com/civicrm/civicrm-core/pull/18486))**
+
+- **Add try catch to main loops on core ipn classes
+ ([18384](https://github.com/civicrm/civicrm-core/pull/18384))**
+
+- **Rename variable $key to $participantID to make it clear what it is
+ ([18371](https://github.com/civicrm/civicrm-core/pull/18371))**
+
+- **Stop passing / using object when all we need is the id
+ ([18331](https://github.com/civicrm/civicrm-core/pull/18331))**
+
+- **Minor code cleanup - this is only ever called from one place so component is
+ always event ([18343](https://github.com/civicrm/civicrm-core/pull/18343))**
+
+- **Membership form test cleanup, date cleanup on form
+ ([18413](https://github.com/civicrm/civicrm-core/pull/18413))**
+
+- **Search ext: misc cleanup & fixes
+ ([18723](https://github.com/civicrm/civicrm-core/pull/18723))**
+
+- **Switch to non-deprecated/cached functions for membership pricesets
+ ([18568](https://github.com/civicrm/civicrm-core/pull/18568))**
+
+- **Fix parameters for MembershipTest
+ ([18467](https://github.com/civicrm/civicrm-core/pull/18467))**
+
+- **Update code comments
+ ([18460](https://github.com/civicrm/civicrm-core/pull/18460))**
+
+- **Pass in activity type rather than calculate it
+ ([18450](https://github.com/civicrm/civicrm-core/pull/18450))**
+
+- **Move definition of userName to where it is used and remove an unused
+ parameter ([18452](https://github.com/civicrm/civicrm-core/pull/18452))**
+
+- **Minor code simplification on date handling in getMembershipStatusByDate
+ ([18421](https://github.com/civicrm/civicrm-core/pull/18421))**
+
+- **Offer singular entity titles
+ ([18434](https://github.com/civicrm/civicrm-core/pull/18434))**
+
+- **(REF) GenerateData - Make it possible to call this via PHP
+ ([18491](https://github.com/civicrm/civicrm-core/pull/18491))**
+
+- **[REF] Simplify array construction
+ ([18432](https://github.com/civicrm/civicrm-core/pull/18432))**
+
+- **[REF] minor tidy up on membershipStatus::create & add
+ ([18435](https://github.com/civicrm/civicrm-core/pull/18435))**
+
+- **[REF] Folllow up cleanup - remove now unused param
+ ([18438](https://github.com/civicrm/civicrm-core/pull/18438))**
+
+- **[REF] Start the process of separating the search action from the participant
+ form ([18464](https://github.com/civicrm/civicrm-core/pull/18464))**
+
+- **[Ref] Code simplification - remove conditional chunk
+ ([18445](https://github.com/civicrm/civicrm-core/pull/18445))**
+
+- **[ref] Extract failContribution code
+ ([18418](https://github.com/civicrm/civicrm-core/pull/18418))**
+
+- **[REF] Fix visibility of afform_scanner container service for Symfony …
+ ([18505](https://github.com/civicrm/civicrm-core/pull/18505))**
+
+- **[REF] Refactor price field form to allow for unit testing of the form
+ ([18414](https://github.com/civicrm/civicrm-core/pull/18414))**
+
+- **[REF] Minor readability fix
+ ([18415](https://github.com/civicrm/civicrm-core/pull/18415))**
+
+- **[REF] change deprecated function to API4 call
+ ([18076](https://github.com/civicrm/civicrm-core/pull/18076))**
+
+- **[NFC] Cleanup in test class
+ ([18539](https://github.com/civicrm/civicrm-core/pull/18539))**
+
+- **[REF] Remove now used parameter & make function protected
+ ([18543](https://github.com/civicrm/civicrm-core/pull/18543))**
+
+- **[REF] Consolidate input params that are primarily used for the membership
+ entity action to an array
+ ([18451](https://github.com/civicrm/civicrm-core/pull/18451))**
+
+- **[REF] Extract the code to determine the DAO name into a functions
+ ([18513](https://github.com/civicrm/civicrm-core/pull/18513))**
+
+- **[REF] Fix deprecated array and string offset access using curly brace…
+ ([18529](https://github.com/civicrm/civicrm-core/pull/18529))**
+
+- **[REF] Code cleanup on membership renewal & test
+ ([18365](https://github.com/civicrm/civicrm-core/pull/18365))**
+
+- **[REF] Improve the human readable name of the eway upgrade step to be …
+ ([18401](https://github.com/civicrm/civicrm-core/pull/18401))**
+
+- **[REF] Simplify loading of related objects in transition components
+ ([18373](https://github.com/civicrm/civicrm-core/pull/18373))**
+
+- **[REF] simplify interaction with objects in complete order
+ ([18385](https://github.com/civicrm/civicrm-core/pull/18385))**
+
+- **[REF] Mark CRM_Contribute_BAO_Contribution_Utils::formatAmount deprec…
+ ([18387](https://github.com/civicrm/civicrm-core/pull/18387))**
+
+- **[REF] Swap out CRM_Utils_Array::value() - partial pull out from PR 18207
+ ([18391](https://github.com/civicrm/civicrm-core/pull/18391))**
+
+- **[REF] Remove unused lines from loadObjects
+ ([18389](https://github.com/civicrm/civicrm-core/pull/18389))**
+
+- **[REF] Ensure that all bundle container services are public for Symfon…
+ ([18368](https://github.com/civicrm/civicrm-core/pull/18368))**
+
+- **[REF] Parse ids before sending to single function (minor simplification)
+ ([18630](https://github.com/civicrm/civicrm-core/pull/18630))**
+
+- **[REF] Hide eway extension in UI and only install it if the original e…
+ ([18377](https://github.com/civicrm/civicrm-core/pull/18377))**
+
+- **[REF] Simplify logic on calling self::updateContributionStatus
+ ([18357](https://github.com/civicrm/civicrm-core/pull/18357))**
+
+- **[REF] Fix adding in the accessKey based on the button array
+ ([18674](https://github.com/civicrm/civicrm-core/pull/18674))**
+
+- **[REF] Add in frontend fields for title and description of group Schem…
+ ([18599](https://github.com/civicrm/civicrm-core/pull/18599))**
+
+- **[REF] Remove unused taskName variable
+ ([18590](https://github.com/civicrm/civicrm-core/pull/18590))**
+
+- **[REF] IPN - move unshared chunk of code out of shared function
+ ([18600](https://github.com/civicrm/civicrm-core/pull/18600))**
+
+- **[REF] Paypal std ipn Move not-actually shared-code out of shared code
+ function ([18536](https://github.com/civicrm/civicrm-core/pull/18536))**
+
+- **[REF] Remove some unused params, move one to where it is used
+ ([18614](https://github.com/civicrm/civicrm-core/pull/18614))**
+
+- **[Ref] Extract getOrderParams
+ ([18617](https://github.com/civicrm/civicrm-core/pull/18617))**
+
+- **REF Filter params in completetransaction
+ ([18321](https://github.com/civicrm/civicrm-core/pull/18321))**
+
+- **[REF] Fix compatability with Drupal 9 installing of var_dumper
+ ([18679](https://github.com/civicrm/civicrm-core/pull/18679))**
+
+- **[REF] Add test for existing Participant batch update cancel and fix to not
+ call BaseIPN->cancelled
+ ([18318](https://github.com/civicrm/civicrm-core/pull/18318))**
+
+- **[REF] Use helper function to check if multiLingual
+ ([604](https://github.com/civicrm/civicrm-drupal/pull/604))**
+
+- **[REF] Update Versions file and remove Net_URL class as doesn't appear…
+ ([310](https://github.com/civicrm/civicrm-packages/pull/310))**
+
+- **[REF] Remove Eway Libraries and XML_Util as they are now shipped as p…
+ ([306](https://github.com/civicrm/civicrm-packages/pull/306))**
+
+- **[REF] Add in css classes to make the save and preview button on the C…
+ ([18647](https://github.com/civicrm/civicrm-core/pull/18647))**
+
+- **[REF] Move daoName generation so we don't need to pass the variable name
+ ([18552](https://github.com/civicrm/civicrm-core/pull/18552))**
+
+- **[REF] Very minor cleanup
+ ([18604](https://github.com/civicrm/civicrm-core/pull/18604))**
+
+- **[REF] Fix Event location to create it's locations directly rather than via
+ shared methods ([18586](https://github.com/civicrm/civicrm-core/pull/18586))**
+
+- **[REF] Consolidate retrieval of searchFormValues
+ ([18591](https://github.com/civicrm/civicrm-core/pull/18591))**
+
+- **[REF] Include recently added core extensions into distmaker
+ ([18597](https://github.com/civicrm/civicrm-core/pull/18597))**
+
+- **[Ref] Extract getFormValues
+ ([18510](https://github.com/civicrm/civicrm-core/pull/18510))**
+
+- **[REF] Remove checks as to whether entityShortName is in the component array
+ ([18538](https://github.com/civicrm/civicrm-core/pull/18538))**
+
+- **[Ref] Merge code - Move determination about location type to the
+ getDAOForLocation…
+ ([18562](https://github.com/civicrm/civicrm-core/pull/18562))**
+
+- **[REF] Remove unreachable lines
+ ([18535](https://github.com/civicrm/civicrm-core/pull/18535))**
+
+- **[REF] Remove wrangling on activityType param
+ ([18558](https://github.com/civicrm/civicrm-core/pull/18558))**
+
+- **[REF] Finally remove deprecated ids handling
+ ([18557](https://github.com/civicrm/civicrm-core/pull/18557))**
+
+- **[REF] Update composer compile plugin to latest version
+ ([18553](https://github.com/civicrm/civicrm-core/pull/18553))**
+
+- **(REF) Make it easier for extensions to define basic bundles
+ ([18660](https://github.com/civicrm/civicrm-core/pull/18660))**
+
+- **[REF] Follow up cleanup from Event Location
+ ([18608](https://github.com/civicrm/civicrm-core/pull/18608))**
+
+- **(REF) Switch to composer-compile-lib
+ ([18646](https://github.com/civicrm/civicrm-core/pull/18646))**
+
+- **[REF] Remove XML_Util dependancy within ewaysingle extension
+ ([18676](https://github.com/civicrm/civicrm-core/pull/18676))**
+
+- **Remove long-deprecated hook_civicrm_tabs
+ ([18503](https://github.com/civicrm/civicrm-core/pull/18503))**
+
+- **Remove redundant custom field types
+ ([18378](https://github.com/civicrm/civicrm-core/pull/18378) and
+ ([622](https://github.com/civicrm/civicrm-drupal/pull/622))**
+
+- **Remove unnecessary call to 'validateData' from pdf generator
+ ([18367](https://github.com/civicrm/civicrm-core/pull/18367))**
+
+- **Remove unnecessary debug from tests which messes up array output
+ ([18446](https://github.com/civicrm/civicrm-core/pull/18446))**
+
+- **Remove error handling from loadObjects
+ ([18393](https://github.com/civicrm/civicrm-core/pull/18393))**
+
+- **Remove deprecated code lines
+ ([18490](https://github.com/civicrm/civicrm-core/pull/18490))**
+
+- **Remove CRM_Contact_BAO_Contact::getPrimaryOpenId
+ ([18424](https://github.com/civicrm/civicrm-core/pull/18424))**
+
+- **Remove inaccessible call to baseIPN failed
+ ([18369](https://github.com/civicrm/civicrm-core/pull/18369))**
+
+- **Remove pass-by-ref in PaypalProIPN::single
+ ([18337](https://github.com/civicrm/civicrm-core/pull/18337))**
+
+- **Remove obsolete load-bootstrap.js
+ ([18551](https://github.com/civicrm/civicrm-core/pull/18551))**
+
+- **Remove deprecated ids param
+ ([18375](https://github.com/civicrm/civicrm-core/pull/18375))**
+
+- **Remove unused deprecated handling for partial_amount_to_pay
+ ([18328](https://github.com/civicrm/civicrm-core/pull/18328))**
+
+- **Minor test data fix up - ensure domain contact's email is primary
+ ([18561](https://github.com/civicrm/civicrm-core/pull/18561))**
+
+- **Minor test fix
+ ([18560](https://github.com/civicrm/civicrm-core/pull/18560))**
+
+- **Preliminary cleanup on test
+ ([18346](https://github.com/civicrm/civicrm-core/pull/18346))**
+
+- **Fix test to use validateAllContributions
+ ([18348](https://github.com/civicrm/civicrm-core/pull/18348))**
+
+- **Afform Tests - Fix extension tests when run via `civi-test-run`
+ ([18511](https://github.com/civicrm/civicrm-core/pull/18511))**
+
+- **Test fix - use valid membership type
+ ([18507](https://github.com/civicrm/civicrm-core/pull/18507))**
+
+- **Test cleanup fix
+ ([18601](https://github.com/civicrm/civicrm-core/pull/18601))**
+
+- **[Civi\Test] Fix test output noise
+ ([18638](https://github.com/civicrm/civicrm-core/pull/18638))**
+
+- **[Test framework] Wrong group id in mailing test setup
+ ([18626](https://github.com/civicrm/civicrm-core/pull/18626))**
+
+- **Add test to cover existing v3 api setting of tax_amount on line items
+ ([18351](https://github.com/civicrm/civicrm-core/pull/18351))**
+
+- **Add unit test that ultimately failed to hit the desired code but does add
+ cover ([18708](https://github.com/civicrm/civicrm-core/pull/18708))**
+
+- **[NFC/Test] Unit test activity-contact variations
+ ([18619](https://github.com/civicrm/civicrm-core/pull/18619))**
+
+- **[NFC/Test] Unit test for target contacts on Bulk Email when mailing in
+ batches ([18584](https://github.com/civicrm/civicrm-core/pull/18584))**
+
+- **[NFC] Remove trailing whitespace
+ ([18476](https://github.com/civicrm/civicrm-core/pull/18476))**
+
+- **[NFC] Aim to reduce memory usage in create single value alter test by…
+ ([18394](https://github.com/civicrm/civicrm-core/pull/18394))**
+
+- **[NFC/Test framework] Make class name match file name
+ ([18392](https://github.com/civicrm/civicrm-core/pull/18392))**
+
+- **[NFC] Minor cleanup - use strict comparison where possible
+ ([18573](https://github.com/civicrm/civicrm-core/pull/18573))**
+
+- **[NFC] Enable APIv4 Testing on the statusPrefence API Tests
+ ([18366](https://github.com/civicrm/civicrm-core/pull/18366))**
+
+- **NFC Clarify what CRM_Price_BAO_Priceset::getMembershipCount does
+ ([18426](https://github.com/civicrm/civicrm-core/pull/18426))**
+
+- **[NFC] Enable APIv4 testing on the Fin ACL Extension Line Item test
+ ([18478](https://github.com/civicrm/civicrm-core/pull/18478))**
+
+- **Enotice fix ([18620](https://github.com/civicrm/civicrm-core/pull/18620))**
+
+- **Enotice fix ([18707](https://github.com/civicrm/civicrm-core/pull/18707))**
+
+- **Update civicrm_handler_field_contact_image.inc
+ ([625](https://github.com/civicrm/civicrm-drupal/pull/625))**
+
+ Fixes a notice.
+
+- **Update civicrm_handler_field_pseudo_constant.inc
+ ([626](https://github.com/civicrm/civicrm-drupal/pull/626))**
+
+ Fixes a notice.
+
+- **Update composer-download-plugin to v3.0.0 to support usage of composer 2.x
+ ([18899](https://github.com/civicrm/civicrm-core/pull/18899))**
+
+## <a name="credits"></a>Credits
+
+This release was developed by the following code authors:
+
+AGH Strategies - Alice Frumin, Andrew Hunt; Agileware - Justin Freeman; Bastien
+Ho; Blackfly Solutions - Alan Dixon; CEDC - Laryn Kragt Bakker; Christian Wach;
+Circle Interactive - Pradeep Nayak; CiviCRM - Coleman Watts, Tim Otten;
+CiviDesk - Sunil Pawar; CompuCorp - Camilo Rodriguez, Ivan; Coop SymbioTIC -
+Mathieu Lutfy; Dave D; iXiam - Luciano Spiegel; JMA Consulting - Monish Deb,
+Seamus Lee; John Kingsnorth; Lighthouse Consulting and Design - Brian
+Shaughnessy; Megaphone Technology Consulting - Dennis P. Osorio, Jon Goldberg;
+MJW Consulting - Matthew Wire; QED42 - Swastik Pareek; Richard van Oosterhout;
+Semper IT - Karin Gerritsen; Squiffle Consulting - Aidan Saunders; Tadpole
+Collective - Kevin Cristiano; Wikimedia Foundation - Eileen McNaughton
+
+Most authors also reviewed code for this release; in addition, the following
+reviewers contributed their comments:
+
+Abeilles en Vélo / Bees on a bike; Artful Robot - Rich Lott; Betty Dolfing;
+CiviCoop - Jaap Jansma; CiviCRM - Josh Gowans; CiviDesk - Nicolas Ganivet,
+Yashodha Chaku; CompuCorp - René Olivo; Freeform Solutions - Herb van den Dool;
+Fuzion - Jitendra Purohit, Luke Stewart; Irene Meisel; JMA Consulting - Joe
+Murray; Lemniscus - Noah Miller; MJCO - Mikey O'Toole; Tony Maynard-Smith;
+Wikimedia Foundation - Maggie Epps
+
+## <a name="feedback"></a>Feedback
+
+These release notes are edited by Alice Frumin and Andrew Hunt. If you'd like
+to provide feedback on them, please log in to https://chat.civicrm.org/civicrm
+and contact `@agh1`.
--- /dev/null
+# CiviCRM 5.32.0
+
+Released December 2, 2020
+
+- **[Synopsis](#synopsis)**
+- **[Features](#features)**
+- **[Bugs resolved](#bugs)**
+- **[Miscellany](#misc)**
+- **[Credits](#credits)**
+- **[Feedback](#feedback)**
+
+## <a name="synopsis"></a>Synopsis
+
+| *Does this version...?* | |
+|:--------------------------------------------------------------- |:-------:|
+| Fix security vulnerabilities? | no |
+| **Change the database schema?** | **yes** |
+| **Alter the API?** | **yes** |
+| Require attention to configuration options? | no |
+| **Fix problems installing or upgrading to a previous version?** | **yes** |
+| **Introduce features?** | **yes** |
+| **Fix bugs?** | **yes** |
+
+## <a name="features"></a>Features
+
+### Core CiviCRM
+
+- **Display public title and description on profiles and unsubscribe/subscribe
+ forms as appropriate if set
+ ([18645](https://github.com/civicrm/civicrm-core/pull/18645))**
+
+ Starts to make use of the new front end title and description fields on
+ unsubscribe, subscribe and user dashboard pages.
+
+- **Allow custom fields of type Autocomplete-Select to be multivalued
+ ([18449](https://github.com/civicrm/civicrm-core/pull/18449))**
+
+ Adds support for multi-select for auto-complete custom fields.
+
+- **Add more columns for Activity Report (Work towards
+ [dev/core#2104](https://lab.civicrm.org/dev/core/-/issues/2104):
+ [18827](https://github.com/civicrm/civicrm-core/pull/18827) and
+ [18840](https://github.com/civicrm/civicrm-core/pull/18840))**
+
+ Improves the Activity Report by adding columns for:
+ - Birth Date of the target contact
+ - Gender of the target contact
+
+- **More accurate language around social media sharing
+ ([18743](https://github.com/civicrm/civicrm-core/pull/18743))**
+
+ Improves user experience by clarifying language around social media sharing.
+
+- **Adds performance improvement when browsing the report logs
+ ([18851](https://github.com/civicrm/civicrm-core/pull/18851))**
+
+ Improves performance when browsing the logs.
+
+- **Clean up search actions in core to make them available from search builder
+ (Work Towards [dev/core#2066](https://lab.civicrm.org/dev/core/-/issues/2066):
+ [18773](https://github.com/civicrm/civicrm-core/pull/18773),
+ [18783](https://github.com/civicrm/civicrm-core/pull/18783),
+ [18768](https://github.com/civicrm/civicrm-core/pull/18768) and
+ [18767](https://github.com/civicrm/civicrm-core/pull/18767))**
+
+ Preliminary cleanup of code to move towards making search actions available
+ from search builder.
+
+- **Lotsa new features for the Search Kit extension
+ ([18876](https://github.com/civicrm/civicrm-core/pull/18876))**
+
+ Integrates Search Kit/Afform. Adds a standalone page for viewing search kit
+ displays. Adds first search kit display type "table". Adds search kit display
+ entity and UI. Adds CRUD form for managing saved search kit searches.
+
+- **Search ext: Add links to search admin and improve links in displays
+ ([18909](https://github.com/civicrm/civicrm-core/pull/18909))**
+
+ Improves links in search kit results.
+
+- **SavedSearch - Add name and label columns
+ ([18809](https://github.com/civicrm/civicrm-core/pull/18809))**
+
+ Adds 2 database columns: `name` and `label` for the SavedSearch entity. This
+ is needed by the new Search Kit extension.
+
+- **Add entity paths to schema & APIv4
+ ([18887](https://github.com/civicrm/civicrm-core/pull/18887))**
+
+ Adds metadata to some entities (hopefully more will be added in the
+ future) for the paths at which they can be created/updated/viewed/deleted. The
+ metadata is accessed via the DAO, and exposed to APIv4.
+
+- **Use standard names for entity paths and add a few more paths
+ ([18915](https://github.com/civicrm/civicrm-core/pull/18915))**
+
+ Standardizes entity paths.
+
+- **APIv4 - Add `$result->single()` helper
+ ([18871](https://github.com/civicrm/civicrm-core/pull/18871))**
+
+ Improves developer-experience when using APIv4 by adding a helper function to
+ retrieve just one result.
+
+- **Update CRM_Utils_Constant::value to support env variables
+ ([18806](https://github.com/civicrm/civicrm-core/pull/18806))**
+
+ Extends the ability to set a variable using env to any variable accessed via
+ CRM_Utils_Constant::value.
+
+- **Angular Loader: Allow modules to specify permissions to add client-side
+ ([18754](https://github.com/civicrm/civicrm-core/pull/18754))**
+
+ Makes it possible for angular modules to define permissions.
+
+- **AngularLoader: Support 'settingsFactory' callbacks in angular modules.
+ ([18731](https://github.com/civicrm/civicrm-core/pull/18731))**
+
+ Allows Angular modules with complex/expensive data to provide it with a
+ callback, which will only be invoked if the module is actively loaded on the
+ page.
+
+### CiviContribute
+
+- **Use of <th> tags for labels on Contribution Amounts tab gives them unique
+ styling
+ ([dev/user-interface#34](https://lab.civicrm.org/dev/user-interface/-/issues/34):
+ [18850](https://github.com/civicrm/civicrm-core/pull/18850))**
+
+ Makes the look and feel when configuring Contribution Forms more consistent.
+
+- **[Meta] Does CiviCRM have a single defined application HTTP entry point which
+ routes all requests?
+ (Work Towards [dev/cloud-native#16](https://lab.civicrm.org/dev/cloud-native/-/issues/16):
+ [17969](https://github.com/civicrm/civicrm-core/pull/17969))**
+
+ Migrates the Contribution Page widget extern url to use the normal CMS routing
+ mechanism.
+
+- **Add UI metadata for payment_processor_id on financialTrxn
+ ([18917](https://github.com/civicrm/civicrm-core/pull/18917))**
+
+ Improve metadata for `financialTrxn`.
+
+### CiviMail
+
+- **OAuth2 administration (email focus) (Work Towards
+ [dev/core#2141](https://lab.civicrm.org/dev/core/-/issues/2141):
+ [18902](https://github.com/civicrm/civicrm-core/pull/18902),
+ [18914](https://github.com/civicrm/civicrm-core/pull/18914),
+ [18885](https://github.com/civicrm/civicrm-core/pull/18885) and
+ [18908](https://github.com/civicrm/civicrm-core/pull/18908))**
+
+ Adds a new hook `hook_civicrm_alterMailStore` which can be used to add or
+ modify a driver. Adds a hidden `oauth-client` extension for connecting to
+ OAuth2-based-web-services. Adds a UI to "Add Mail Account". Adds two
+ libraries: "league/oauth2-client" and "league/oauth2-google".
+
+- **MailSettings - Add button+API for testing a connection
+ ([18911](https://github.com/civicrm/civicrm-core/pull/18911))**
+
+ Adds a button (and API action) to the Edit Mail Account form to test the
+ connection.
+
+- **Add postProcess hook to MessageTemplates
+ ([18807](https://github.com/civicrm/civicrm-core/pull/18807))**
+
+ Makes it so extension developers can use the 'postProcess' hook to access
+ 'MessageTemplates'.
+
+### WordPress Integration
+
+- **MySQL socket while using CiviCRM installer
+ ([dev/wordpress#35](https://lab.civicrm.org/dev/wordpress/-/issues/35):
+ [18913](https://github.com/civicrm/civicrm-core/pull/18913))**
+
+ Ensures `civicrm-setup` can handle database connections with unix sockets.
+
+## <a name="bugs"></a>Bugs resolved
+
+### Core CiviCRM
+
+- **Fix main contact uf url on merge screen
+ ([18742](https://github.com/civicrm/civicrm-core/pull/18742))**
+
+ Ensures the UF url of the main contact on the merge screen points to the
+ correct user.
+
+- **Fix sendconfirmation api to override receipt params
+ ([18789](https://github.com/civicrm/civicrm-core/pull/18789))**
+
+ Ensures params in `sendconfirmation` API take precedence over receipt params.
+
+- **"Non-static method CRM_Contact_Page_AJAX::pdfFormat() should not be called
+ statically" when changing the page format on print/merge document
+ ([dev/core#2110](https://lab.civicrm.org/dev/core/-/issues/2110):
+ [18726](https://github.com/civicrm/civicrm-core/pull/18726))**
+
+ Fixes notices when generating PDFs.
+
+- **updated italian provinces
+ ([18859](https://github.com/civicrm/civicrm-core/pull/18859))**
+
+ Ensures that the right abbreviations are used for Italian provinces.
+
+- **APIv4 Activity::update() causes target contacts and assignees to be deleted
+ ([dev/core#1428](https://lab.civicrm.org/dev/core/-/issues/1428):
+ [18720](https://github.com/civicrm/civicrm-core/pull/18720))**
+
+ Adds a test.
+
+- **APIv4 - Fix type coersion of non-string input
+ ([18860](https://github.com/civicrm/civicrm-core/pull/18860))**
+
+ Fixes APIv4 to not accidentally change non-string input to string.
+
+- **Participant Calculate/Fees: fix ts usage, simplify wording
+ ([18874](https://github.com/civicrm/civicrm-core/pull/18874))**
+
+ Fixes an incorrect use of ts, which assumes how strings can be concatenated.
+ It also makes it difficult to use Word Replacements.
+
+- **Cancel first contribution associated to membership, cancels the membership
+ ([dev/core#927](https://lab.civicrm.org/dev/core/-/issues/927):
+ [18814](https://github.com/civicrm/civicrm-core/pull/18814),
+ [18812](https://github.com/civicrm/civicrm-core/pull/18812),
+ [18853](https://github.com/civicrm/civicrm-core/pull/18853),
+ [18786](https://github.com/civicrm/civicrm-core/pull/18786),
+ [18881](https://github.com/civicrm/civicrm-core/pull/18881),
+ [18813](https://github.com/civicrm/civicrm-core/pull/18813) and
+ [18784](https://github.com/civicrm/civicrm-core/pull/18784))**
+
+- **When default changed for an alphanumeric multi-select custom field
+ defaulting breaks for that field on backend
+ forms.([dev/core#2139](https://lab.civicrm.org/dev/core/-/issues/2139):
+ [18907](https://github.com/civicrm/civicrm-core/pull/18907))**
+
+- **Long cyrillic names give error Data too long for column sort_name when
+ saving a contact
+ ([dev/core#2146](https://lab.civicrm.org/dev/core/-/issues/2146):
+ [18862](https://github.com/civicrm/civicrm-core/pull/18862))**
+
+- **Incorrect use of quotes and escape and ts in CRM_Core_DAO::copyValues
+ ([dev/core#2148](https://lab.civicrm.org/dev/core/-/issues/2148):
+ [18864](https://github.com/civicrm/civicrm-core/pull/18864))**
+
+- **CiviCRM reCAPTCHA Util not validating user tokens on form submission
+ ([dev/core#2150](https://lab.civicrm.org/dev/core/-/issues/2150):
+ [18872](https://github.com/civicrm/civicrm-core/pull/18872) and
+ [311](https://github.com/civicrm/civicrm-packages/pull/311))**
+
+- **Fix the Test Result (1 failure / -190)
+ E2E.Core.PrevNextTest.testDeleteByCacheKey recurring test issue
+ ([dev/core#2029](https://lab.civicrm.org/dev/core/-/issues/2029):
+ [18846](https://github.com/civicrm/civicrm-core/pull/18846))**
+
+- **Merge - ensure location entities remaining on deleted contacts have
+ is_primary integrity
+ ([dev/core#2047](https://lab.civicrm.org/dev/core/-/issues/2047):
+ [18555](https://github.com/civicrm/civicrm-core/pull/18555))**
+
+- **Eliminate unused query on CRM_Core_BAO_CustomQuery::_construct
+ ([dev/core#2079](https://lab.civicrm.org/dev/core/-/issues/2079):
+ [18668](https://github.com/civicrm/civicrm-core/pull/18668))**
+
+- **get log date from tables available in query with data instead of last table
+ ([18868](https://github.com/civicrm/civicrm-core/pull/18868))**
+
+- **Search ext: Fix validation and saving on search admin screen
+ ([18919](https://github.com/civicrm/civicrm-core/pull/18919))**
+
+- **CRM_Core_Error::formatFooException - Don't bomb on 'Error'
+ ([18910](https://github.com/civicrm/civicrm-core/pull/18910))**
+
+- **ClassLoader - Fix autoloading of `API_Exception`
+ ([18870](https://github.com/civicrm/civicrm-core/pull/18870))**
+
+- **SavedSearch: add UI_name index to upgrade script
+ ([18811](https://github.com/civicrm/civicrm-core/pull/18811))**
+
+- **Fix pluralize function for words like 'display'
+ ([18778](https://github.com/civicrm/civicrm-core/pull/18778))**
+
+- **class.api.php: In remote api calls, allow referer and useragent to be set.
+ ([18400](https://github.com/civicrm/civicrm-core/pull/18400))**
+
+- **Typo in call to fixSchemaDifferencesForAll
+ ([18762](https://github.com/civicrm/civicrm-core/pull/18762))**
+
+- **Rationalise date formatting
+ ([18805](https://github.com/civicrm/civicrm-core/pull/18805))**
+
+### CiviCampaign
+
+- **Fix campaign_id handling for batch entry
+ ([18792](https://github.com/civicrm/civicrm-core/pull/18792))**
+
+ Fixes a bug whereby `campaign_id` is not updated on batch entry if it has been
+ added to the profile.
+
+### CiviCase
+
+- **Merging contacts removes case roles
+ ([dev/core#2152](https://lab.civicrm.org/dev/core/-/issues/2152):
+ [18884](https://github.com/civicrm/civicrm-core/pull/18884))**
+
+### CiviContribute
+
+- **finish 'this round' of completeOrder cleanup
+ ([dev/financial#152](https://lab.civicrm.org/dev/financial/-/issues/152):
+ [18631](https://github.com/civicrm/civicrm-core/pull/18631),
+ [18734](https://github.com/civicrm/civicrm-core/pull/18734),
+ [18732](https://github.com/civicrm/civicrm-core/pull/18732),
+ [18733](https://github.com/civicrm/civicrm-core/pull/18733),
+ [18728](https://github.com/civicrm/civicrm-core/pull/18728),
+ [18629](https://github.com/civicrm/civicrm-core/pull/18629),
+ [18835](https://github.com/civicrm/civicrm-core/pull/18835),
+ [18730](https://github.com/civicrm/civicrm-core/pull/18730),
+ [18729](https://github.com/civicrm/civicrm-core/pull/18729).
+ [18737](https://github.com/civicrm/civicrm-core/pull/18737),
+ [18735](https://github.com/civicrm/civicrm-core/pull/18735),
+ [18744](https://github.com/civicrm/civicrm-core/pull/18744),
+ [18736](https://github.com/civicrm/civicrm-core/pull/18736) and
+ [18815](https://github.com/civicrm/civicrm-core/pull/18815))**
+
+ Cleans up how `completeOrder` deals with `payment_processor_id` (solely as an
+ input param).
+
+- **Thank-you letter incorrect contribution currency
+ ([dev/financial#111](https://lab.civicrm.org/dev/financial/-/issues/111):
+ [18714](https://github.com/civicrm/civicrm-core/pull/18714) and
+ [18715](https://github.com/civicrm/civicrm-core/pull/18715))**
+
+ Ensures that when a contribution is made using a currency other than the
+ default currency, the contribution tokens: {contribution.total_amount}
+ {contribution.fee_amount} {contribution.net_amount} correctly display the
+ currency.
+
+- **PayPal payment processor uses deprecated methods, breaking functionality
+ (Work Towards [dev/core#2034](https://lab.civicrm.org/dev/core/-/issues/2034):
+ [18540](https://github.com/civicrm/civicrm-core/pull/18540))**
+
+ Fixes the PayPal standard cancel url.
+
+- **Declare support for cancelRecurring in manual processor
+ ([18804](https://github.com/civicrm/civicrm-core/pull/18804))**
+
+ Ensure that when no processor id is present the cancel form is loaded (as
+ opposed to the enable-disable form).
+
+- **Refund status not set correctly when cancelled_payment_id is set
+ ([dev/financial#156](https://lab.civicrm.org/dev/financial/-/issues/156):
+ [18930](https://github.com/civicrm/civicrm-core/pull/18930))**
+
+- **Contribution confirmation page should not display the name of payment
+ processor type ([17568](https://github.com/civicrm/civicrm-core/pull/17568))**
+
+- **PCP 'Your Message' should use WYSIWYG
+ ([18411](https://github.com/civicrm/civicrm-core/pull/18411))**
+
+### CiviEvent
+
+- **.ical files not populating correctly for sites with ACL's configured for
+ events ([dev/core#1879](https://lab.civicrm.org/dev/core/-/issues/1879):
+ [18712](https://github.com/civicrm/civicrm-core/pull/18712))**
+
+### CiviMail
+
+- **Set id after save for the mailing component in the postProcess
+ ([18808](https://github.com/civicrm/civicrm-core/pull/18808))**
+
+- **"Notice: Undefined index: domain" when deleting a mail account
+ ([dev/core#2166](https://lab.civicrm.org/dev/core/-/issues/2166):
+ [18927](https://github.com/civicrm/civicrm-core/pull/18927))**
+
+- **crmMailing - Only load Angular settings if they're needed
+ ([18749](https://github.com/civicrm/civicrm-core/pull/18749))**
+
+### CiviSMS
+
+- **Error in the selected phone to send an SMS when the Mobile type label is
+ modified ([dev/core#2138](https://lab.civicrm.org/dev/core/-/issues/2138):
+ [18842](https://github.com/civicrm/civicrm-core/pull/18842))**
+
+- **Show only Active SMS provider List on Mass SMS form
+ ([18867](https://github.com/civicrm/civicrm-core/pull/18867))**
+
+### Backdrop Integration
+
+- **Override sessionStart function and use backdrop functions as appropriate
+ (related to
+ [backdrop#116](https://github.com/civicrm/civicrm-backdrop/issues/116):
+ [18745](https://github.com/civicrm/civicrm-core/pull/18745))**
+
+ Overrides the backdrop Session start function in DrupalBase.php to use the
+ backdrop specific functions
+
+- **Resolve #110 Sync repo with CiviCRM-Drupal repo
+ ([115](https://github.com/civicrm/civicrm-backdrop/pull/115))**
+
+### Drupal Integration
+
+- **Do not manually construct the site path during Drupal8+ setup
+ ([dev/core#2140](https://lab.civicrm.org/dev/core/-/issues/2140):
+ [18843](https://github.com/civicrm/civicrm-core/pull/18843))**
+
+- **D8 Install checks run via Drupal Status Report - gives misleading warnings.
+ ([dev/drupal#137](https://lab.civicrm.org/dev/drupal/-/issues/137):
+ [18581](https://github.com/civicrm/civicrm-core/pull/18581))**
+
+### Joomla Integration
+
+- **[Joomla 4.0] CiviCRM cannot be installed on Joomla 4.0 alpha
+ ([dev/joomla#14](https://lab.civicrm.org/dev/joomla/-/issues/14):
+ [52](https://github.com/civicrm/civicrm-joomla/pull/52))**
+
+### Wordpress Integration
+
+- **Protect against undefined index query in heartbeat callback
+ ([220](https://github.com/civicrm/civicrm-wordpress/pull/220))**
+
+## <a name="misc"></a>Miscellany
+
+- **Schema handler fixes
+ ([18932](https://github.com/civicrm/civicrm-core/pull/18932))**
+
+- **[cq] Do not pass by reference where avoidable
+ ([dev/core#2043](https://lab.civicrm.org/dev/core/-/issues/2043):
+ [18802](https://github.com/civicrm/civicrm-core/pull/18802))**
+
+- **Move financialACLs to a core extension (Work Towards
+ [dev/core#2115](https://lab.civicrm.org/dev/core/-/issues/2115):
+ [18738](https://github.com/civicrm/civicrm-core/pull/18738) and
+ [18740](https://github.com/civicrm/civicrm-core/pull/18740))**
+
+- **Move call to update related pledges on contribution cancel to extension
+ ([18894](https://github.com/civicrm/civicrm-core/pull/18894))**
+
+- **Move filtering of unpermitted options for reports/ search select to
+ financialacl extension
+ ([18849](https://github.com/civicrm/civicrm-core/pull/18849))**
+
+- **Move CRM_Member_BAO_MembershipType::getPermissionedMembershipTypes to
+ financial acl extension
+ ([18848](https://github.com/civicrm/civicrm-core/pull/18848))**
+
+- **Replace BAO calls with api calls in test class
+ ([18798](https://github.com/civicrm/civicrm-core/pull/18798))**
+
+- **Switch to calling api
+ ([18797](https://github.com/civicrm/civicrm-core/pull/18797))**
+
+- **Switch to calling the api
+ ([18796](https://github.com/civicrm/civicrm-core/pull/18796))**
+
+- **Extract setNextUrl
+ ([18750](https://github.com/civicrm/civicrm-core/pull/18750))**
+
+- **Deprecate hook_civicrm_crudLink
+ ([18888](https://github.com/civicrm/civicrm-core/pull/18888))**
+
+- **Fix extension generated DAO files to pass civilint
+ ([18879](https://github.com/civicrm/civicrm-core/pull/18879))**
+
+- **Hack away at false negative test fails
+ ([18892](https://github.com/civicrm/civicrm-core/pull/18892))**
+
+- **Remove always-true IF
+ ([18803](https://github.com/civicrm/civicrm-core/pull/18803))**
+
+- **Remove always true if
+ ([18801](https://github.com/civicrm/civicrm-core/pull/18801))**
+
+- **Remove always-true & otherwise silly if
+ ([18883](https://github.com/civicrm/civicrm-core/pull/18883))**
+
+- **Remove IPN reference to _relatedObjects, deprecate property
+ ([18895](https://github.com/civicrm/civicrm-core/pull/18895))**
+
+- **Remove deprecated code
+ ([18903](https://github.com/civicrm/civicrm-core/pull/18903))**
+
+- **Remove a few lines of deprecated code
+ ([18826](https://github.com/civicrm/civicrm-core/pull/18826))**
+
+- **Remove instances of variable variables
+ ([18791](https://github.com/civicrm/civicrm-core/pull/18791))**
+
+- **Remove meaningless legacy code
+ ([18856](https://github.com/civicrm/civicrm-core/pull/18856))**
+
+- **Refactor entityParams in Order.Create API so it is easier to
+ understand/modify
+ ([18306](https://github.com/civicrm/civicrm-core/pull/18306))**
+
+- **[REF] Minor simplification - don't use a variable for table name
+ ([18651](https://github.com/civicrm/civicrm-core/pull/18651))**
+
+- **[REF] Remove silly if
+ ([18897](https://github.com/civicrm/civicrm-core/pull/18897))**
+
+- **[Ref] Move sending the email back out of the recur function
+ ([18852](https://github.com/civicrm/civicrm-core/pull/18852))**
+
+- **[Ref] Use direct version of participant id
+ ([18882](https://github.com/civicrm/civicrm-core/pull/18882))**
+
+- **[Ref] Simplify params
+ ([18896](https://github.com/civicrm/civicrm-core/pull/18896))**
+
+- **[REF] Simplify use of shared code.
+ ([18900](https://github.com/civicrm/civicrm-core/pull/18900))**
+
+- **[REF] Minor extraction
+ ([18829](https://github.com/civicrm/civicrm-core/pull/18829))**
+
+- **[REF] Determine values where they are needed rather than passing them
+ around (in tested function)
+ ([18837](https://github.com/civicrm/civicrm-core/pull/18837))**
+
+- **[REF] Include contributioncancelactions extension in dismaker and reg…
+ ([18825](https://github.com/civicrm/civicrm-core/pull/18825))**
+
+- **[REF] Extract handling for loading contribution recur object.
+ ([18746](https://github.com/civicrm/civicrm-core/pull/18746))**
+
+- **[REF] Replace long if block with early return
+ ([18747](https://github.com/civicrm/civicrm-core/pull/18747))**
+
+- **[REF] Upgrade DomPDF to v0.8.6
+ ([18688](https://github.com/civicrm/civicrm-core/pull/18688))**
+
+- **[REF] Separate export form classes out & simplify task handling
+ ([18589](https://github.com/civicrm/civicrm-core/pull/18589))**
+
+- **[REF] Search ext: Reorganize code into modules
+ ([18775](https://github.com/civicrm/civicrm-core/pull/18775))**
+
+- **[Ref] Minor code extraction
+ ([18739](https://github.com/civicrm/civicrm-core/pull/18739))**
+
+- **[Test] - Fix some tests to call API not BAO
+ ([18795](https://github.com/civicrm/civicrm-core/pull/18795))**
+
+- **[Test] Ensure all APIv4 entities have basic info
+ ([18727](https://github.com/civicrm/civicrm-core/pull/18727))**
+
+- **Test for event#43
+ ([18761](https://github.com/civicrm/civicrm-core/pull/18761))**
+
+- **Add test for recurring links and clean up method of retrieving recurring
+ ([18790](https://github.com/civicrm/civicrm-core/pull/18790))**
+
+- **unit test for #18306 - order create api test for contribution
+ ([18785](https://github.com/civicrm/civicrm-core/pull/18785))**
+
+- **(NFC) Fix typo in Money valueFormat depretation warning
+ ([18886](https://github.com/civicrm/civicrm-core/pull/18886))**
+
+- **(NFC) Make assertions in PrevNextTest more skimmable
+ ([dev/core#2029](https://lab.civicrm.org/dev/core/-/issues/2029):
+ [18822](https://github.com/civicrm/civicrm-core/pull/18822))**
+
+## <a name="credits"></a>Credits
+
+This release was developed by the following code authors:
+
+AGH Strategies - Alice Frumin, Andrew Hunt; Agileware - Francis Whittle, Justin
+Freeman, Pengyi Zhang; Andrew Thompson; Christian Wach; Circle Interactive -
+Pradeep Nayak; CiviCoop - Jaap Jansma; CiviCRM - Coleman Watts, Tim Otten;
+CiviDesk - Sunil Pawar, Yashodha Chaku; CiviFirst - John Kirk; CompuCorp -
+Debarshi Bhaumik; Coop SymbioTIC - Mathieu Lutfy; Dave D; Diego Muñio; Freeform
+Solutions - Herb van den Dool; Fuzion - Jitendra Purohit; iXiam - Luciano
+Spiegel, Vangelis Pantazis; JMA Consulting - Monish Deb, Seamus Lee; John
+Kingsnorth; Megaphone Technology Consulting - Jon Goldberg; mglaman; MJW
+Consulting - Matthew Wire; Nicol Wistreich; PERORA SRL- Samuele Masetto;
+Progressive Technology Project - Jamie McClelland; Richard van Oosterhout;
+Squiffle Consulting - Aidan Saunders; Wikimedia Foundation - Eileen McNaughton
+
+Most authors also reviewed code for this release; in addition, the following
+reviewers contributed their comments:
+
+Artful Robot - Rich Lott; Atomic Development - Max Tsero; Australian Greens -
+John Twyman; Centarro - Matt Glaman; Fuzion - Luke Stewart; Greenpeace Central
+and Eastern Europe - Patrick Figel; JMA Consulting - Joe Murray; jvos;
+Lighthouse Consulting and Design - Brian Shaughnessy; Megaphone Technology
+Consulting - Jon Goldberg; MJCO - Mikey O'Toole; Semper IT - Karin Gerritsen;
+Tadpole Collective - Kevin Cristiano;
+
+## <a name="feedback"></a>Feedback
+
+These release notes are edited by Alice Frumin and Andrew Hunt. If you'd like
+to provide feedback on them, please log in to https://chat.civicrm.org/civicrm
+and contact `@agh1`.
<p><?php echo ts('By default, CiviCRM uses the same database as your website. You may install on a separate database if you need more fine-grained control over permissions, replication, hardware capacity, etc.'); ?></p>
<p><?php echo ts('<strong>Example</strong>: <code>%1</code>', array(1 => 'mysql://admin:secret@localhost/civicrm')); ?></p>
<p><?php echo ts('<strong>Example</strong>: <code>%1</code>', array(1 => 'mysql://admin:secret@127.0.0.1:3306/otherdb')); ?></p>
+ <p><?php echo ts('<strong>Example</strong>: <code>%1</code>', array(1 => 'mysql://admin:secret@unix(/var/lib/mysql/mysql.sock)/otherdb')); ?></p>
</div>
</td>
</tr>
_corereqadapter_addMessages($e, 'system', $systemMsgs);
\Civi\Setup::log()->info(sprintf('[%s] Run Requirements::checkDatabase()', basename(__FILE__)));
- list ($host, $port) = \Civi\Setup\DbUtil::decodeHostPort($model->db['server']);
+ list ($host, $port, $socket) = \Civi\Setup\DbUtil::decodeHostPort($model->db['server']);
$dbMsgs = $r->checkDatabase(array(
'host' => $host,
'port' => $port,
+ 'socket' => $socket,
'username' => $model->db['username'],
'password' => $model->db['password'],
'database' => $model->db['database'],
// Compute DSN.
global $databases;
$ssl_params = \Civi\Setup\DrupalUtil::guessSslParams($databases['default']['default']);
+ // @todo Does Backdrop support unixsocket in config? Set 'server' => 'unix(/path/to/socket.sock)'
$model->db = $model->cmsDb = array(
'server' => \Civi\Setup\DbUtil::encodeHostPort($databases['default']['default']['host'], $databases['default']['default']['port'] ?: NULL),
'username' => $databases['default']['default']['username'],
// Compute DSN.
global $databases;
$ssl_params = \Civi\Setup\DrupalUtil::guessSslParams($databases['default']['default']);
+ // @todo Does Drupal support unixsocket in config? Set 'server' => 'unix(/path/to/socket.sock)'
$model->db = $model->cmsDb = array(
'server' => \Civi\Setup\DbUtil::encodeHostPort($databases['default']['default']['host'], $databases['default']['default']['port'] ?: NULL),
'username' => $databases['default']['default']['username'],
// Compute settingsPath.
$siteDir = \Civi\Setup\DrupalUtil::getDrupalSiteDir($cmsPath);
- $model->settingsPath = implode(DIRECTORY_SEPARATOR, [$cmsPath, 'sites', $siteDir, 'civicrm.settings.php']);
+ $model->settingsPath = implode(DIRECTORY_SEPARATOR, [$cmsPath, $siteDir, 'civicrm.settings.php']);
if (($loadGenerated = \Drupal\Core\Site\Settings::get('civicrm_load_generated', NULL)) !== NULL) {
$model->loadGenerated = $loadGenerated;
// Compute DSN.
$connectionOptions = \Drupal::database()->getConnectionOptions();
$ssl_params = \Civi\Setup\DrupalUtil::guessSslParams($connectionOptions);
+ // @todo Does Drupal support unixsocket in config? Set 'server' => 'unix(/path/to/socket.sock)'
$model->db = $model->cmsDb = array(
'server' => \Civi\Setup\DbUtil::encodeHostPort($connectionOptions['host'], $connectionOptions['port'] ?: NULL),
'username' => $connectionOptions['username'],
$model->templateCompilePath = implode(DIRECTORY_SEPARATOR, [$uploadDir['basedir'], 'civicrm', 'templates_c']);
// Compute DSN.
+ list(/*$host*/, /*$port*/, $socket) = Civi\Setup\DbUtil::decodeHostPort(DB_HOST);
$model->db = $model->cmsDb = array(
- 'server' => DB_HOST,
+ 'server' => $socket ? sprintf('unix(%s)', $socket) : DB_HOST,
'username' => DB_USER,
'password' => DB_PASSWORD,
'database' => DB_NAME,
$e->addInfo('system', 'settingsPath', sprintf('The settingsPath is defined.'));
}
+ // If Civi is already installed, Drupal 8's status report page also calls us
+ // and so we need to modify the check slightly since we want the reverse
+ // conditions.
+ $installed = \Civi\Setup::instance()->checkInstalled();
+ $alreadyInstalled = $installed->isSettingInstalled() || $installed->isDatabaseInstalled();
+
if (!\Civi\Setup\FileUtil::isCreateable($m->settingsPath)) {
- $e->addError('system', 'settingsWritable', sprintf('The settings file "%s" cannot be created. Ensure the parent folder is writable.', $m->settingsPath));
+ if ($alreadyInstalled) {
+ $e->addInfo('system', 'settingsWritable', sprintf('The settings file "%s" is protected from writing.', $m->settingsPath));
+ }
+ else {
+ $e->addError('system', 'settingsWritable', sprintf('The settings file "%s" cannot be created. Ensure the parent folder is writable.', $m->settingsPath));
+ }
}
else {
- $e->addInfo('system', 'settingsWritable', sprintf('The settings file "%s" can be created.', $m->settingsPath));
+ if ($alreadyInstalled) {
+ // Note if we were to output an error, we wouldn't be able to use
+ // `cv core:install` to do an in-place reinstall since it would fail
+ // requirements checks.
+ $e->addWarning('system', 'settingsWritable', sprintf('The settings file "%s" should not be writable.', $m->settingsPath));
+ }
+ else {
+ $e->addInfo('system', 'settingsWritable', sprintf('The settings file "%s" can be created.', $m->settingsPath));
+ }
}
});
*/
public static function parseDsn($dsn) {
$parsed = parse_url($dsn);
+ // parse_url parses 'mysql://admin:secret@unix(/var/lib/mysql/mysql.sock)/otherdb' like:
+ // [
+ // 'host' => 'unix(',
+ // 'path' => '/var/lib/mysql/mysql.sock)/otherdb',
+ // ...
+ // ]
+ if ($parsed['host'] == 'unix(') {
+ preg_match('/(unix\(.*\))(\/(.+)?)?$/', $dsn, $matches);
+ $server = $matches[1];
+ $database = $matches[3] ?? NULL;
+ }
+ else {
+ $server = self::encodeHostPort($parsed['host'], $parsed['port'] ?? NULL);
+ $database = $parsed['path'] ? ltrim($parsed['path'], '/') : NULL;
+ }
+
return array(
- 'server' => self::encodeHostPort($parsed['host'], $parsed['port'] ?? NULL),
+ 'server' => $server,
'username' => $parsed['user'] ?: NULL,
'password' => $parsed['pass'] ?: NULL,
- 'database' => $parsed['path'] ? ltrim($parsed['path'], '/') : NULL,
+ 'database' => $database,
'ssl_params' => self::parseSSL($parsed['query'] ?? NULL),
);
}
* @return \mysqli
*/
public static function softConnect($db) {
- list($host, $port) = self::decodeHostPort($db['server']);
+ list($host, $port, $socket) = self::decodeHostPort($db['server']);
if (empty($db['ssl_params'])) {
- $conn = @mysqli_connect($host, $db['username'], $db['password'], $db['database'], $port);
+ $conn = @mysqli_connect($host, $db['username'], $db['password'], $db['database'], $port, $socket);
}
else {
$conn = NULL;
$db['ssl_params']['capath'] ?? NULL,
$db['ssl_params']['cipher'] ?? NULL
);
- // @todo socket parameter, but if you're using sockets do you need SSL?
- if (@mysqli_real_connect($init, $host, $db['username'], $db['password'], $db['database'], $port, NULL, MYSQLI_CLIENT_SSL)) {
+ if (@mysqli_real_connect($init, $host, $db['username'], $db['password'], $db['database'], $port, $socket, MYSQLI_CLIENT_SSL)) {
$conn = $init;
}
}
* Ex: '127.0.0.1:123'
* Ex: '[1234:abcd]'
* Ex: '[1234:abcd]:123'
+ * Ex: 'localhost:/path/to/socket.sock
+ * Ex: 'unix(/path/to/socket.sock)
* @return array
- * Combination: [0 => string $host, 1 => numeric|NULL $port].
- * Ex: ['localhost', NULL].
- * Ex: ['127.0.0.1', 3306]
+ * Combination: [0 => string $host, 1 => numeric|NULL $port, 2 => string|NULL].
+ * Ex: ['localhost', NULL, NULL].
+ * Ex: ['127.0.0.1', 3306, NULL]
*/
public static function decodeHostPort($host) {
- $hostParts = explode(':', $host);
- if (count($hostParts) > 1 && strrpos($host, ']') !== strlen($host) - 1) {
- $port = array_pop($hostParts);
- $host = implode(':', $hostParts);
+ $port = NULL;
+ $socket = NULL;
+ if (preg_match('/^unix\(([^)]+)\)$/', $host, $matches) === 1) {
+ $host = 'localhost';
+ $socket = $matches[1];
}
else {
- $port = NULL;
+ $hostParts = explode(':', $host);
+ if (count($hostParts) > 1 && strrpos($host, ']') !== strlen($host) - 1) {
+ $portOrSocket = array_pop($hostParts);
+ if (substr($portOrSocket, /*start*/ 0, /*length*/ 1) == '/') {
+ $socket = $portOrSocket;
+ }
+ else {
+ $port = $portOrSocket;
+ }
+ $host = implode(':', $hostParts);
+ }
}
- return array($host, $port);
+ return array($host, $port, $socket);
}
/**
return basename(conf_path());
}
elseif (class_exists('Drupal')) {
- return basename(\Drupal::service('site.path'));
+ return \Drupal::service('site.path');
}
else {
throw new \Exception('Cannot detect path under Drupal "sites/".');
LOCK TABLES `civicrm_domain` WRITE;
/*!40000 ALTER TABLE `civicrm_domain` DISABLE KEYS */;
-INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `version`, `contact_id`, `locales`, `locale_custom_strings`) VALUES (1,'Default Domain Name',NULL,'5.32.alpha1',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}');
+INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `version`, `contact_id`, `locales`, `locale_custom_strings`) VALUES (1,'Default Domain Name',NULL,'5.33.alpha1',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}');
/*!40000 ALTER TABLE `civicrm_domain` ENABLE KEYS */;
UNLOCK TABLES;
LOCK TABLES `civicrm_extension` WRITE;
/*!40000 ALTER TABLE `civicrm_extension` DISABLE KEYS */;
-INSERT INTO `civicrm_extension` (`id`, `type`, `full_name`, `name`, `label`, `file`, `schema_version`, `is_active`) VALUES (1,'module','sequentialcreditnotes','Sequential credit notes','Sequential credit notes','sequentialcreditnotes',NULL,1),(2,'module','greenwich','Theme: Greenwich','Theme: Greenwich','greenwich',NULL,1),(3,'module','eventcart','Event cart','Event cart','eventcart',NULL,1),(4,'module','financialacls','Financial ACLs','Financial ACLs','financialacls',NULL,1);
+INSERT INTO `civicrm_extension` (`id`, `type`, `full_name`, `name`, `label`, `file`, `schema_version`, `is_active`) VALUES (1,'module','sequentialcreditnotes','Sequential credit notes','Sequential credit notes','sequentialcreditnotes',NULL,1),(2,'module','greenwich','Theme: Greenwich','Theme: Greenwich','greenwich',NULL,1),(3,'module','eventcart','Event cart','Event cart','eventcart',NULL,1),(4,'module','financialacls','Financial ACLs','Financial ACLs','financialacls',NULL,1),(5,'module','contributioncancelactions','Contribution cancel actions','Contribution cancel actions','contributioncancelactions',NULL,1);
/*!40000 ALTER TABLE `civicrm_extension` ENABLE KEYS */;
UNLOCK TABLES;
LOCK TABLES `civicrm_state_province` WRITE;
/*!40000 ALTER TABLE `civicrm_state_province` DISABLE KEYS */;
-INSERT INTO `civicrm_state_province` (`id`, `name`, `abbreviation`, `country_id`) VALUES (1000,'Alabama','AL',1228),(1001,'Alaska','AK',1228),(1002,'Arizona','AZ',1228),(1003,'Arkansas','AR',1228),(1004,'California','CA',1228),(1005,'Colorado','CO',1228),(1006,'Connecticut','CT',1228),(1007,'Delaware','DE',1228),(1008,'Florida','FL',1228),(1009,'Georgia','GA',1228),(1010,'Hawaii','HI',1228),(1011,'Idaho','ID',1228),(1012,'Illinois','IL',1228),(1013,'Indiana','IN',1228),(1014,'Iowa','IA',1228),(1015,'Kansas','KS',1228),(1016,'Kentucky','KY',1228),(1017,'Louisiana','LA',1228),(1018,'Maine','ME',1228),(1019,'Maryland','MD',1228),(1020,'Massachusetts','MA',1228),(1021,'Michigan','MI',1228),(1022,'Minnesota','MN',1228),(1023,'Mississippi','MS',1228),(1024,'Missouri','MO',1228),(1025,'Montana','MT',1228),(1026,'Nebraska','NE',1228),(1027,'Nevada','NV',1228),(1028,'New Hampshire','NH',1228),(1029,'New Jersey','NJ',1228),(1030,'New Mexico','NM',1228),(1031,'New York','NY',1228),(1032,'North Carolina','NC',1228),(1033,'North Dakota','ND',1228),(1034,'Ohio','OH',1228),(1035,'Oklahoma','OK',1228),(1036,'Oregon','OR',1228),(1037,'Pennsylvania','PA',1228),(1038,'Rhode Island','RI',1228),(1039,'South Carolina','SC',1228),(1040,'South Dakota','SD',1228),(1041,'Tennessee','TN',1228),(1042,'Texas','TX',1228),(1043,'Utah','UT',1228),(1044,'Vermont','VT',1228),(1045,'Virginia','VA',1228),(1046,'Washington','WA',1228),(1047,'West Virginia','WV',1228),(1048,'Wisconsin','WI',1228),(1049,'Wyoming','WY',1228),(1050,'District of Columbia','DC',1228),(1052,'American Samoa','AS',1228),(1053,'Guam','GU',1228),(1055,'Northern Mariana Islands','MP',1228),(1056,'Puerto Rico','PR',1228),(1057,'Virgin Islands','VI',1228),(1058,'United States Minor Outlying Islands','UM',1228),(1059,'Armed Forces Europe','AE',1228),(1060,'Armed Forces Americas','AA',1228),(1061,'Armed Forces Pacific','AP',1228),(1100,'Alberta','AB',1039),(1101,'British Columbia','BC',1039),(1102,'Manitoba','MB',1039),(1103,'New Brunswick','NB',1039),(1104,'Newfoundland and Labrador','NL',1039),(1105,'Northwest Territories','NT',1039),(1106,'Nova Scotia','NS',1039),(1107,'Nunavut','NU',1039),(1108,'Ontario','ON',1039),(1109,'Prince Edward Island','PE',1039),(1110,'Quebec','QC',1039),(1111,'Saskatchewan','SK',1039),(1112,'Yukon Territory','YT',1039),(1200,'Maharashtra','MM',1101),(1201,'Karnataka','KA',1101),(1202,'Andhra Pradesh','AP',1101),(1203,'Arunachal Pradesh','AR',1101),(1204,'Assam','AS',1101),(1205,'Bihar','BR',1101),(1206,'Chhattisgarh','CH',1101),(1207,'Goa','GA',1101),(1208,'Gujarat','GJ',1101),(1209,'Haryana','HR',1101),(1210,'Himachal Pradesh','HP',1101),(1211,'Jammu and Kashmir','JK',1101),(1212,'Jharkhand','JH',1101),(1213,'Kerala','KL',1101),(1214,'Madhya Pradesh','MP',1101),(1215,'Manipur','MN',1101),(1216,'Meghalaya','ML',1101),(1217,'Mizoram','MZ',1101),(1218,'Nagaland','NL',1101),(1219,'Orissa','OR',1101),(1220,'Punjab','PB',1101),(1221,'Rajasthan','RJ',1101),(1222,'Sikkim','SK',1101),(1223,'Tamil Nadu','TN',1101),(1224,'Tripura','TR',1101),(1225,'Uttarakhand','UT',1101),(1226,'Uttar Pradesh','UP',1101),(1227,'West Bengal','WB',1101),(1228,'Andaman and Nicobar Islands','AN',1101),(1229,'Dadra and Nagar Haveli','DN',1101),(1230,'Daman and Diu','DD',1101),(1231,'Delhi','DL',1101),(1232,'Lakshadweep','LD',1101),(1233,'Pondicherry','PY',1101),(1300,'mazowieckie','MZ',1172),(1301,'pomorskie','PM',1172),(1302,'dolnośląskie','DS',1172),(1303,'kujawsko-pomorskie','KP',1172),(1304,'lubelskie','LU',1172),(1305,'lubuskie','LB',1172),(1306,'łódzkie','LD',1172),(1307,'małopolskie','MA',1172),(1308,'opolskie','OP',1172),(1309,'podkarpackie','PK',1172),(1310,'podlaskie','PD',1172),(1311,'śląskie','SL',1172),(1312,'świętokrzyskie','SK',1172),(1313,'warmińsko-mazurskie','WN',1172),(1314,'wielkopolskie','WP',1172),(1315,'zachodniopomorskie','ZP',1172),(1500,'Abu Zaby','AZ',1225),(1501,'\'Ajman','AJ',1225),(1502,'Al Fujayrah','FU',1225),(1503,'Ash Shariqah','SH',1225),(1504,'Dubayy','DU',1225),(1505,'Ra\'s al Khaymah','RK',1225),(1506,'Dac Lac','33',1233),(1507,'Umm al Qaywayn','UQ',1225),(1508,'Badakhshan','BDS',1001),(1509,'Badghis','BDG',1001),(1510,'Baghlan','BGL',1001),(1511,'Balkh','BAL',1001),(1512,'Bamian','BAM',1001),(1513,'Farah','FRA',1001),(1514,'Faryab','FYB',1001),(1515,'Ghazni','GHA',1001),(1516,'Ghowr','GHO',1001),(1517,'Helmand','HEL',1001),(1518,'Herat','HER',1001),(1519,'Jowzjan','JOW',1001),(1520,'Kabul','KAB',1001),(1521,'Kandahar','KAN',1001),(1522,'Kapisa','KAP',1001),(1523,'Khowst','KHO',1001),(1524,'Konar','KNR',1001),(1525,'Kondoz','KDZ',1001),(1526,'Laghman','LAG',1001),(1527,'Lowgar','LOW',1001),(1528,'Nangrahar','NAN',1001),(1529,'Nimruz','NIM',1001),(1530,'Nurestan','NUR',1001),(1531,'Oruzgan','ORU',1001),(1532,'Paktia','PIA',1001),(1533,'Paktika','PKA',1001),(1534,'Parwan','PAR',1001),(1535,'Samangan','SAM',1001),(1536,'Sar-e Pol','SAR',1001),(1537,'Takhar','TAK',1001),(1538,'Wardak','WAR',1001),(1539,'Zabol','ZAB',1001),(1540,'Berat','BR',1002),(1541,'Bulqizë','BU',1002),(1542,'Delvinë','DL',1002),(1543,'Devoll','DV',1002),(1544,'Dibër','DI',1002),(1545,'Durrës','DR',1002),(1546,'Elbasan','EL',1002),(1547,'Fier','FR',1002),(1548,'Gramsh','GR',1002),(1549,'Gjirokastër','GJ',1002),(1550,'Has','HA',1002),(1551,'Kavajë','KA',1002),(1552,'Kolonjë','ER',1002),(1553,'Korçë','KO',1002),(1554,'Krujë','KR',1002),(1555,'Kuçovë','KC',1002),(1556,'Kukës','KU',1002),(1557,'Kurbin','KB',1002),(1558,'Lezhë','LE',1002),(1559,'Librazhd','LB',1002),(1560,'Lushnjë','LU',1002),(1561,'Malësi e Madhe','MM',1002),(1562,'Mallakastër','MK',1002),(1563,'Mat','MT',1002),(1564,'Mirditë','MR',1002),(1565,'Peqin','PQ',1002),(1566,'Përmet','PR',1002),(1567,'Pogradec','PG',1002),(1568,'Pukë','PU',1002),(1569,'Sarandë','SR',1002),(1570,'Skrapar','SK',1002),(1571,'Shkodër','SH',1002),(1572,'Tepelenë','TE',1002),(1573,'Tiranë','TR',1002),(1574,'Tropojë','TP',1002),(1575,'Vlorë','VL',1002),(1576,'Erevan','ER',1011),(1577,'Aragacotn','AG',1011),(1578,'Ararat','AR',1011),(1579,'Armavir','AV',1011),(1580,'Gegarkunik\'','GR',1011),(1581,'Kotayk\'','KT',1011),(1582,'Lory','LO',1011),(1583,'Sirak','SH',1011),(1584,'Syunik\'','SU',1011),(1585,'Tavus','TV',1011),(1586,'Vayoc Jor','VD',1011),(1587,'Bengo','BGO',1006),(1588,'Benguela','BGU',1006),(1589,'Bie','BIE',1006),(1590,'Cabinda','CAB',1006),(1591,'Cuando-Cubango','CCU',1006),(1592,'Cuanza Norte','CNO',1006),(1593,'Cuanza Sul','CUS',1006),(1594,'Cunene','CNN',1006),(1595,'Huambo','HUA',1006),(1596,'Huila','HUI',1006),(1597,'Luanda','LUA',1006),(1598,'Lunda Norte','LNO',1006),(1599,'Lunda Sul','LSU',1006),(1600,'Malange','MAL',1006),(1601,'Moxico','MOX',1006),(1602,'Namibe','NAM',1006),(1603,'Uige','UIG',1006),(1604,'Zaire','ZAI',1006),(1605,'Capital federal','C',1010),(1606,'Buenos Aires','B',1010),(1607,'Catamarca','K',1010),(1608,'Cordoba','X',1010),(1609,'Corrientes','W',1010),(1610,'Chaco','H',1010),(1611,'Chubut','U',1010),(1612,'Entre Rios','E',1010),(1613,'Formosa','P',1010),(1614,'Jujuy','Y',1010),(1615,'La Pampa','L',1010),(1616,'Mendoza','M',1010),(1617,'Misiones','N',1010),(1618,'Neuquen','Q',1010),(1619,'Rio Negro','R',1010),(1620,'Salta','A',1010),(1621,'San Juan','J',1010),(1622,'San Luis','D',1010),(1623,'Santa Cruz','Z',1010),(1624,'Santa Fe','S',1010),(1625,'Santiago del Estero','G',1010),(1626,'Tierra del Fuego','V',1010),(1627,'Tucuman','T',1010),(1628,'Burgenland','1',1014),(1629,'Kärnten','2',1014),(1630,'Niederösterreich','3',1014),(1631,'Oberösterreich','4',1014),(1632,'Salzburg','5',1014),(1633,'Steiermark','6',1014),(1634,'Tirol','7',1014),(1635,'Vorarlberg','8',1014),(1636,'Wien','9',1014),(1637,'Australian Antarctic Territory','AAT',1008),(1638,'Australian Capital Territory','ACT',1013),(1639,'Northern Territory','NT',1013),(1640,'New South Wales','NSW',1013),(1641,'Queensland','QLD',1013),(1642,'South Australia','SA',1013),(1643,'Tasmania','TAS',1013),(1644,'Victoria','VIC',1013),(1645,'Western Australia','WA',1013),(1646,'Naxcivan','NX',1015),(1647,'Ali Bayramli','AB',1015),(1648,'Baki','BA',1015),(1649,'Ganca','GA',1015),(1650,'Lankaran','LA',1015),(1651,'Mingacevir','MI',1015),(1652,'Naftalan','NA',1015),(1653,'Saki','SA',1015),(1654,'Sumqayit','SM',1015),(1655,'Susa','SS',1015),(1656,'Xankandi','XA',1015),(1657,'Yevlax','YE',1015),(1658,'Abseron','ABS',1015),(1659,'Agcabadi','AGC',1015),(1660,'Agdam','AGM',1015),(1661,'Agdas','AGS',1015),(1662,'Agstafa','AGA',1015),(1663,'Agsu','AGU',1015),(1664,'Astara','AST',1015),(1665,'Babak','BAB',1015),(1666,'Balakan','BAL',1015),(1667,'Barda','BAR',1015),(1668,'Beylagan','BEY',1015),(1669,'Bilasuvar','BIL',1015),(1670,'Cabrayll','CAB',1015),(1671,'Calilabad','CAL',1015),(1672,'Culfa','CUL',1015),(1673,'Daskasan','DAS',1015),(1674,'Davaci','DAV',1015),(1675,'Fuzuli','FUZ',1015),(1676,'Gadabay','GAD',1015),(1677,'Goranboy','GOR',1015),(1678,'Goycay','GOY',1015),(1679,'Haciqabul','HAC',1015),(1680,'Imisli','IMI',1015),(1681,'Ismayilli','ISM',1015),(1682,'Kalbacar','KAL',1015),(1683,'Kurdamir','KUR',1015),(1684,'Lacin','LAC',1015),(1685,'Lerik','LER',1015),(1686,'Masalli','MAS',1015),(1687,'Neftcala','NEF',1015),(1688,'Oguz','OGU',1015),(1689,'Ordubad','ORD',1015),(1690,'Qabala','QAB',1015),(1691,'Qax','QAX',1015),(1692,'Qazax','QAZ',1015),(1693,'Qobustan','QOB',1015),(1694,'Quba','QBA',1015),(1695,'Qubadli','QBI',1015),(1696,'Qusar','QUS',1015),(1697,'Saatli','SAT',1015),(1698,'Sabirabad','SAB',1015),(1699,'Sadarak','SAD',1015),(1700,'Sahbuz','SAH',1015),(1701,'Salyan','SAL',1015),(1702,'Samaxi','SMI',1015),(1703,'Samkir','SKR',1015),(1704,'Samux','SMX',1015),(1705,'Sarur','SAR',1015),(1706,'Siyazan','SIY',1015),(1707,'Tartar','TAR',1015),(1708,'Tovuz','TOV',1015),(1709,'Ucar','UCA',1015),(1710,'Xacmaz','XAC',1015),(1711,'Xanlar','XAN',1015),(1712,'Xizi','XIZ',1015),(1713,'Xocali','XCI',1015),(1714,'Xocavand','XVD',1015),(1715,'Yardimli','YAR',1015),(1716,'Zangilan','ZAN',1015),(1717,'Zaqatala','ZAQ',1015),(1718,'Zardab','ZAR',1015),(1719,'Federacija Bosna i Hercegovina','BIH',1026),(1720,'Republika Srpska','SRP',1026),(1721,'Bagerhat zila','05',1017),(1722,'Bandarban zila','01',1017),(1723,'Barguna zila','02',1017),(1724,'Barisal zila','06',1017),(1725,'Bhola zila','07',1017),(1726,'Bogra zila','03',1017),(1727,'Brahmanbaria zila','04',1017),(1728,'Chandpur zila','09',1017),(1729,'Chittagong zila','10',1017),(1730,'Chuadanga zila','12',1017),(1731,'Comilla zila','08',1017),(1732,'Cox\'s Bazar zila','11',1017),(1733,'Dhaka zila','13',1017),(1734,'Dinajpur zila','14',1017),(1735,'Faridpur zila','15',1017),(1736,'Feni zila','16',1017),(1737,'Gaibandha zila','19',1017),(1738,'Gazipur zila','18',1017),(1739,'Gopalganj zila','17',1017),(1740,'Habiganj zila','20',1017),(1741,'Jaipurhat zila','24',1017),(1742,'Jamalpur zila','21',1017),(1743,'Jessore zila','22',1017),(1744,'Jhalakati zila','25',1017),(1745,'Jhenaidah zila','23',1017),(1746,'Khagrachari zila','29',1017),(1747,'Khulna zila','27',1017),(1748,'Kishorganj zila','26',1017),(1749,'Kurigram zila','28',1017),(1750,'Kushtia zila','30',1017),(1751,'Lakshmipur zila','31',1017),(1752,'Lalmonirhat zila','32',1017),(1753,'Madaripur zila','36',1017),(1754,'Magura zila','37',1017),(1755,'Manikganj zila','33',1017),(1756,'Meherpur zila','39',1017),(1757,'Moulvibazar zila','38',1017),(1758,'Munshiganj zila','35',1017),(1759,'Mymensingh zila','34',1017),(1760,'Naogaon zila','48',1017),(1761,'Narail zila','43',1017),(1762,'Narayanganj zila','40',1017),(1763,'Narsingdi zila','42',1017),(1764,'Natore zila','44',1017),(1765,'Nawabganj zila','45',1017),(1766,'Netrakona zila','41',1017),(1767,'Nilphamari zila','46',1017),(1768,'Noakhali zila','47',1017),(1769,'Pabna zila','49',1017),(1770,'Panchagarh zila','52',1017),(1771,'Patuakhali zila','51',1017),(1772,'Pirojpur zila','50',1017),(1773,'Rajbari zila','53',1017),(1774,'Rajshahi zila','54',1017),(1775,'Rangamati zila','56',1017),(1776,'Rangpur zila','55',1017),(1777,'Satkhira zila','58',1017),(1778,'Shariatpur zila','62',1017),(1779,'Sherpur zila','57',1017),(1780,'Sirajganj zila','59',1017),(1781,'Sunamganj zila','61',1017),(1782,'Sylhet zila','60',1017),(1783,'Tangail zila','63',1017),(1784,'Thakurgaon zila','64',1017),(1785,'Antwerpen','VAN',1020),(1786,'Brabant Wallon','WBR',1020),(1787,'Hainaut','WHT',1020),(1788,'Liege','WLG',1020),(1789,'Limburg','VLI',1020),(1790,'Luxembourg','WLX',1020),(1791,'Namur','WNA',1020),(1792,'Oost-Vlaanderen','VOV',1020),(1793,'Vlaams-Brabant','VBR',1020),(1794,'West-Vlaanderen','VWV',1020),(1795,'Bale','BAL',1034),(1796,'Bam','BAM',1034),(1797,'Banwa','BAN',1034),(1798,'Bazega','BAZ',1034),(1799,'Bougouriba','BGR',1034),(1800,'Boulgou','BLG',1034),(1801,'Boulkiemde','BLK',1034),(1802,'Comoe','COM',1034),(1803,'Ganzourgou','GAN',1034),(1804,'Gnagna','GNA',1034),(1805,'Gourma','GOU',1034),(1806,'Houet','HOU',1034),(1807,'Ioba','IOB',1034),(1808,'Kadiogo','KAD',1034),(1809,'Kenedougou','KEN',1034),(1810,'Komondjari','KMD',1034),(1811,'Kompienga','KMP',1034),(1812,'Kossi','KOS',1034),(1813,'Koulpulogo','KOP',1034),(1814,'Kouritenga','KOT',1034),(1815,'Kourweogo','KOW',1034),(1816,'Leraba','LER',1034),(1817,'Loroum','LOR',1034),(1818,'Mouhoun','MOU',1034),(1819,'Nahouri','NAO',1034),(1820,'Namentenga','NAM',1034),(1821,'Nayala','NAY',1034),(1822,'Noumbiel','NOU',1034),(1823,'Oubritenga','OUB',1034),(1824,'Oudalan','OUD',1034),(1825,'Passore','PAS',1034),(1826,'Poni','PON',1034),(1827,'Sanguie','SNG',1034),(1828,'Sanmatenga','SMT',1034),(1829,'Seno','SEN',1034),(1830,'Siasili','SIS',1034),(1831,'Soum','SOM',1034),(1832,'Sourou','SOR',1034),(1833,'Tapoa','TAP',1034),(1834,'Tui','TUI',1034),(1835,'Yagha','YAG',1034),(1836,'Yatenga','YAT',1034),(1837,'Ziro','ZIR',1034),(1838,'Zondoma','ZON',1034),(1839,'Zoundweogo','ZOU',1034),(1840,'Blagoevgrad','01',1033),(1841,'Burgas','02',1033),(1842,'Dobrich','08',1033),(1843,'Gabrovo','07',1033),(1844,'Haskovo','26',1033),(1845,'Yambol','28',1033),(1846,'Kardzhali','09',1033),(1847,'Kyustendil','10',1033),(1848,'Lovech','11',1033),(1849,'Montana','12',1033),(1850,'Pazardzhik','13',1033),(1851,'Pernik','14',1033),(1852,'Pleven','15',1033),(1853,'Plovdiv','16',1033),(1854,'Razgrad','17',1033),(1855,'Ruse','18',1033),(1856,'Silistra','19',1033),(1857,'Sliven','20',1033),(1858,'Smolyan','21',1033),(1859,'Sofia','23',1033),(1860,'Stara Zagora','24',1033),(1861,'Shumen','27',1033),(1862,'Targovishte','25',1033),(1863,'Varna','03',1033),(1864,'Veliko Tarnovo','04',1033),(1865,'Vidin','05',1033),(1866,'Vratsa','06',1033),(1867,'Al Hadd','01',1016),(1868,'Al Manamah','03',1016),(1869,'Al Mintaqah al Gharbiyah','10',1016),(1870,'Al Mintagah al Wusta','07',1016),(1871,'Al Mintaqah ash Shamaliyah','05',1016),(1872,'Al Muharraq','02',1016),(1873,'Ar Rifa','09',1016),(1874,'Jidd Hafs','04',1016),(1875,'Madluat Jamad','12',1016),(1876,'Madluat Isa','08',1016),(1877,'Mintaqat Juzur tawar','11',1016),(1878,'Sitrah','06',1016),(1879,'Bubanza','BB',1036),(1880,'Bujumbura','BJ',1036),(1881,'Bururi','BR',1036),(1882,'Cankuzo','CA',1036),(1883,'Cibitoke','CI',1036),(1884,'Gitega','GI',1036),(1885,'Karuzi','KR',1036),(1886,'Kayanza','KY',1036),(1887,'Makamba','MA',1036),(1888,'Muramvya','MU',1036),(1889,'Mwaro','MW',1036),(1890,'Ngozi','NG',1036),(1891,'Rutana','RT',1036),(1892,'Ruyigi','RY',1036),(1893,'Alibori','AL',1022),(1894,'Atakora','AK',1022),(1895,'Atlantique','AQ',1022),(1896,'Borgou','BO',1022),(1897,'Collines','CO',1022),(1898,'Donga','DO',1022),(1899,'Kouffo','KO',1022),(1900,'Littoral','LI',1022),(1901,'Mono','MO',1022),(1902,'Oueme','OU',1022),(1903,'Plateau','PL',1022),(1904,'Zou','ZO',1022),(1905,'Belait','BE',1032),(1906,'Brunei-Muara','BM',1032),(1907,'Temburong','TE',1032),(1908,'Tutong','TU',1032),(1909,'Cochabamba','C',1025),(1910,'Chuquisaca','H',1025),(1911,'El Beni','B',1025),(1912,'La Paz','L',1025),(1913,'Oruro','O',1025),(1914,'Pando','N',1025),(1915,'Potosi','P',1025),(1916,'Tarija','T',1025),(1917,'Acre','AC',1029),(1918,'Alagoas','AL',1029),(1919,'Amazonas','AM',1029),(1920,'Amapa','AP',1029),(1921,'Bahia','BA',1029),(1922,'Ceara','CE',1029),(1923,'Distrito Federal','DF',1029),(1924,'Espirito Santo','ES',1029),(1926,'Goias','GO',1029),(1927,'Maranhao','MA',1029),(1928,'Minas Gerais','MG',1029),(1929,'Mato Grosso do Sul','MS',1029),(1930,'Mato Grosso','MT',1029),(1931,'Para','PA',1029),(1932,'Paraiba','PB',1029),(1933,'Pernambuco','PE',1029),(1934,'Piaui','PI',1029),(1935,'Parana','PR',1029),(1936,'Rio de Janeiro','RJ',1029),(1937,'Rio Grande do Norte','RN',1029),(1938,'Rondonia','RO',1029),(1939,'Roraima','RR',1029),(1940,'Rio Grande do Sul','RS',1029),(1941,'Santa Catarina','SC',1029),(1942,'Sergipe','SE',1029),(1943,'Sao Paulo','SP',1029),(1944,'Tocantins','TO',1029),(1945,'Acklins and Crooked Islands','AC',1212),(1946,'Bimini','BI',1212),(1947,'Cat Island','CI',1212),(1948,'Exuma','EX',1212),(1955,'Inagua','IN',1212),(1957,'Long Island','LI',1212),(1959,'Mayaguana','MG',1212),(1960,'New Providence','NP',1212),(1962,'Ragged Island','RI',1212),(1966,'Bumthang','33',1024),(1967,'Chhukha','12',1024),(1968,'Dagana','22',1024),(1969,'Gasa','GA',1024),(1970,'Ha','13',1024),(1971,'Lhuentse','44',1024),(1972,'Monggar','42',1024),(1973,'Paro','11',1024),(1974,'Pemagatshel','43',1024),(1975,'Punakha','23',1024),(1976,'Samdrup Jongkha','45',1024),(1977,'Samtee','14',1024),(1978,'Sarpang','31',1024),(1979,'Thimphu','15',1024),(1980,'Trashigang','41',1024),(1981,'Trashi Yangtse','TY',1024),(1982,'Trongsa','32',1024),(1983,'Tsirang','21',1024),(1984,'Wangdue Phodrang','24',1024),(1985,'Zhemgang','34',1024),(1986,'Central','CE',1027),(1987,'Ghanzi','GH',1027),(1988,'Kgalagadi','KG',1027),(1989,'Kgatleng','KL',1027),(1990,'Kweneng','KW',1027),(1991,'Ngamiland','NG',1027),(1992,'North-East','NE',1027),(1993,'North-West','NW',1027),(1994,'South-East','SE',1027),(1995,'Southern','SO',1027),(1996,'Brèsckaja voblasc\'','BR',1019),(1997,'Homel\'skaja voblasc\'','HO',1019),(1998,'Hrodzenskaja voblasc\'','HR',1019),(1999,'Mahilëuskaja voblasc\'','MA',1019),(2000,'Minskaja voblasc\'','MI',1019),(2001,'Vicebskaja voblasc\'','VI',1019),(2002,'Belize','BZ',1021),(2003,'Cayo','CY',1021),(2004,'Corozal','CZL',1021),(2005,'Orange Walk','OW',1021),(2006,'Stann Creek','SC',1021),(2007,'Toledo','TOL',1021),(2008,'Kinshasa','KN',1050),(2011,'Equateur','EQ',1050),(2014,'Kasai-Oriental','KE',1050),(2016,'Maniema','MA',1050),(2017,'Nord-Kivu','NK',1050),(2019,'Sud-Kivu','SK',1050),(2020,'Bangui','BGF',1042),(2021,'Bamingui-Bangoran','BB',1042),(2022,'Basse-Kotto','BK',1042),(2023,'Haute-Kotto','HK',1042),(2024,'Haut-Mbomou','HM',1042),(2025,'Kemo','KG',1042),(2026,'Lobaye','LB',1042),(2027,'Mambere-Kadei','HS',1042),(2028,'Mbomou','MB',1042),(2029,'Nana-Grebizi','KB',1042),(2030,'Nana-Mambere','NM',1042),(2031,'Ombella-Mpoko','MP',1042),(2032,'Ouaka','UK',1042),(2033,'Ouham','AC',1042),(2034,'Ouham-Pende','OP',1042),(2035,'Sangha-Mbaere','SE',1042),(2036,'Vakaga','VR',1042),(2037,'Brazzaville','BZV',1051),(2038,'Bouenza','11',1051),(2039,'Cuvette','8',1051),(2040,'Cuvette-Ouest','15',1051),(2041,'Kouilou','5',1051),(2042,'Lekoumou','2',1051),(2043,'Likouala','7',1051),(2044,'Niari','9',1051),(2045,'Plateaux','14',1051),(2046,'Pool','12',1051),(2047,'Sangha','13',1051),(2048,'Aargau','AG',1205),(2049,'Appenzell Innerrhoden','AI',1205),(2050,'Appenzell Ausserrhoden','AR',1205),(2051,'Bern','BE',1205),(2052,'Basel-Landschaft','BL',1205),(2053,'Basel-Stadt','BS',1205),(2054,'Fribourg','FR',1205),(2055,'Geneva','GE',1205),(2056,'Glarus','GL',1205),(2057,'Graubunden','GR',1205),(2058,'Jura','JU',1205),(2059,'Luzern','LU',1205),(2060,'Neuchatel','NE',1205),(2061,'Nidwalden','NW',1205),(2062,'Obwalden','OW',1205),(2063,'Sankt Gallen','SG',1205),(2064,'Schaffhausen','SH',1205),(2065,'Solothurn','SO',1205),(2066,'Schwyz','SZ',1205),(2067,'Thurgau','TG',1205),(2068,'Ticino','TI',1205),(2069,'Uri','UR',1205),(2070,'Vaud','VD',1205),(2071,'Valais','VS',1205),(2072,'Zug','ZG',1205),(2073,'Zurich','ZH',1205),(2074,'18 Montagnes','06',1054),(2075,'Agnebi','16',1054),(2076,'Bas-Sassandra','09',1054),(2077,'Denguele','10',1054),(2078,'Haut-Sassandra','02',1054),(2079,'Lacs','07',1054),(2080,'Lagunes','01',1054),(2081,'Marahoue','12',1054),(2082,'Moyen-Comoe','05',1054),(2083,'Nzi-Comoe','11',1054),(2084,'Savanes','03',1054),(2085,'Sud-Bandama','15',1054),(2086,'Sud-Comoe','13',1054),(2087,'Vallee du Bandama','04',1054),(2088,'Worodouqou','14',1054),(2089,'Zanzan','08',1054),(2090,'Aisen del General Carlos Ibanez del Campo','AI',1044),(2091,'Antofagasta','AN',1044),(2092,'Araucania','AR',1044),(2093,'Atacama','AT',1044),(2094,'Bio-Bio','BI',1044),(2095,'Coquimbo','CO',1044),(2096,'Libertador General Bernardo O\'Higgins','LI',1044),(2097,'Los Lagos','LL',1044),(2098,'Magallanes','MA',1044),(2099,'Maule','ML',1044),(2100,'Santiago Metropolitan','SM',1044),(2101,'Tarapaca','TA',1044),(2102,'Valparaiso','VS',1044),(2103,'Adamaoua','AD',1038),(2104,'Centre','CE',1038),(2105,'East','ES',1038),(2106,'Far North','EN',1038),(2107,'North','NO',1038),(2108,'South','SW',1038),(2109,'South-West','SW',1038),(2110,'West','OU',1038),(2111,'Beijing','11',1045),(2112,'Chongqing','50',1045),(2113,'Shanghai','31',1045),(2114,'Tianjin','12',1045),(2115,'Anhui','34',1045),(2116,'Fujian','35',1045),(2117,'Gansu','62',1045),(2118,'Guangdong','44',1045),(2119,'Guizhou','52',1045),(2120,'Hainan','46',1045),(2121,'Hebei','13',1045),(2122,'Heilongjiang','23',1045),(2123,'Henan','41',1045),(2124,'Hubei','42',1045),(2125,'Hunan','43',1045),(2126,'Jiangsu','32',1045),(2127,'Jiangxi','36',1045),(2128,'Jilin','22',1045),(2129,'Liaoning','21',1045),(2130,'Qinghai','63',1045),(2131,'Shaanxi','61',1045),(2132,'Shandong','37',1045),(2133,'Shanxi','14',1045),(2134,'Sichuan','51',1045),(2135,'Taiwan','71',1045),(2136,'Yunnan','53',1045),(2137,'Zhejiang','33',1045),(2138,'Guangxi','45',1045),(2139,'Neia Mongol (mn)','15',1045),(2140,'Xinjiang','65',1045),(2141,'Xizang','54',1045),(2142,'Hong Kong','91',1045),(2143,'Macau','92',1045),(2144,'Distrito Capital de Bogotá','DC',1048),(2145,'Amazonea','AMA',1048),(2146,'Antioquia','ANT',1048),(2147,'Arauca','ARA',1048),(2148,'Atlántico','ATL',1048),(2149,'Bolívar','BOL',1048),(2150,'Boyacá','BOY',1048),(2151,'Caldea','CAL',1048),(2152,'Caquetá','CAQ',1048),(2153,'Casanare','CAS',1048),(2154,'Cauca','CAU',1048),(2155,'Cesar','CES',1048),(2156,'Córdoba','COR',1048),(2157,'Cundinamarca','CUN',1048),(2158,'Chocó','CHO',1048),(2159,'Guainía','GUA',1048),(2160,'Guaviare','GUV',1048),(2161,'La Guajira','LAG',1048),(2162,'Magdalena','MAG',1048),(2163,'Meta','MET',1048),(2164,'Nariño','NAR',1048),(2165,'Norte de Santander','NSA',1048),(2166,'Putumayo','PUT',1048),(2167,'Quindio','QUI',1048),(2168,'Risaralda','RIS',1048),(2169,'San Andrés, Providencia y Santa Catalina','SAP',1048),(2170,'Santander','SAN',1048),(2171,'Sucre','SUC',1048),(2172,'Tolima','TOL',1048),(2173,'Valle del Cauca','VAC',1048),(2174,'Vaupés','VAU',1048),(2175,'Vichada','VID',1048),(2176,'Alajuela','A',1053),(2177,'Cartago','C',1053),(2178,'Guanacaste','G',1053),(2179,'Heredia','H',1053),(2180,'Limon','L',1053),(2181,'Puntarenas','P',1053),(2182,'San Jose','SJ',1053),(2183,'Camagey','09',1056),(2184,'Ciego de `vila','08',1056),(2185,'Cienfuegos','06',1056),(2186,'Ciudad de La Habana','03',1056),(2187,'Granma','12',1056),(2188,'Guantanamo','14',1056),(2189,'Holquin','11',1056),(2190,'La Habana','02',1056),(2191,'Las Tunas','10',1056),(2192,'Matanzas','04',1056),(2193,'Pinar del Rio','01',1056),(2194,'Sancti Spiritus','07',1056),(2195,'Santiago de Cuba','13',1056),(2196,'Villa Clara','05',1056),(2197,'Isla de la Juventud','99',1056),(2198,'Pinar del Roo','PR',1056),(2199,'Ciego de Avila','CA',1056),(2200,'Camagoey','CG',1056),(2201,'Holgun','HO',1056),(2202,'Sancti Spritus','SS',1056),(2203,'Municipio Especial Isla de la Juventud','IJ',1056),(2204,'Boa Vista','BV',1040),(2205,'Brava','BR',1040),(2206,'Calheta de Sao Miguel','CS',1040),(2207,'Fogo','FO',1040),(2208,'Maio','MA',1040),(2209,'Mosteiros','MO',1040),(2210,'Paul','PA',1040),(2211,'Porto Novo','PN',1040),(2212,'Praia','PR',1040),(2213,'Ribeira Grande','RG',1040),(2214,'Sal','SL',1040),(2215,'Sao Domingos','SD',1040),(2216,'Sao Filipe','SF',1040),(2217,'Sao Nicolau','SN',1040),(2218,'Sao Vicente','SV',1040),(2219,'Tarrafal','TA',1040),(2220,'Ammochostos Magusa','04',1057),(2221,'Keryneia','06',1057),(2222,'Larnaka','03',1057),(2223,'Lefkosia','01',1057),(2224,'Lemesos','02',1057),(2225,'Pafos','05',1057),(2226,'Jihočeský kraj','JC',1058),(2227,'Jihomoravský kraj','JM',1058),(2228,'Karlovarský kraj','KA',1058),(2229,'Královéhradecký kraj','KR',1058),(2230,'Liberecký kraj','LI',1058),(2231,'Moravskoslezský kraj','MO',1058),(2232,'Olomoucký kraj','OL',1058),(2233,'Pardubický kraj','PA',1058),(2234,'Plzeňský kraj','PL',1058),(2235,'Praha, hlavní město','PR',1058),(2236,'Středočeský kraj','ST',1058),(2237,'Ústecký kraj','US',1058),(2238,'Vysočina','VY',1058),(2239,'Zlínský kraj','ZL',1058),(2240,'Baden-Württemberg','BW',1082),(2241,'Bayern','BY',1082),(2242,'Bremen','HB',1082),(2243,'Hamburg','HH',1082),(2244,'Hessen','HE',1082),(2245,'Niedersachsen','NI',1082),(2246,'Nordrhein-Westfalen','NW',1082),(2247,'Rheinland-Pfalz','RP',1082),(2248,'Saarland','SL',1082),(2249,'Schleswig-Holstein','SH',1082),(2250,'Berlin','BE',1082),(2251,'Brandenburg','BB',1082),(2252,'Mecklenburg-Vorpommern','MV',1082),(2253,'Sachsen','SN',1082),(2254,'Sachsen-Anhalt','ST',1082),(2255,'Thüringen','TH',1082),(2256,'Ali Sabiah','AS',1060),(2257,'Dikhil','DI',1060),(2258,'Djibouti','DJ',1060),(2259,'Obock','OB',1060),(2260,'Tadjoura','TA',1060),(2261,'Frederiksberg','147',1059),(2262,'Copenhagen City','101',1059),(2263,'Copenhagen','015',1059),(2264,'Frederiksborg','020',1059),(2265,'Roskilde','025',1059),(2266,'Vestsjælland','030',1059),(2267,'Storstrøm','035',1059),(2268,'Bornholm','040',1059),(2269,'Fyn','042',1059),(2270,'South Jutland','050',1059),(2271,'Ribe','055',1059),(2272,'Vejle','060',1059),(2273,'Ringkjøbing','065',1059),(2274,'Århus','070',1059),(2275,'Viborg','076',1059),(2276,'North Jutland','080',1059),(2277,'Distrito Nacional (Santo Domingo)','01',1062),(2278,'Azua','02',1062),(2279,'Bahoruco','03',1062),(2280,'Barahona','04',1062),(2281,'Dajabón','05',1062),(2282,'Duarte','06',1062),(2283,'El Seybo [El Seibo]','08',1062),(2284,'Espaillat','09',1062),(2285,'Hato Mayor','30',1062),(2286,'Independencia','10',1062),(2287,'La Altagracia','11',1062),(2288,'La Estrelleta [Elias Pina]','07',1062),(2289,'La Romana','12',1062),(2290,'La Vega','13',1062),(2291,'Maroia Trinidad Sánchez','14',1062),(2292,'Monseñor Nouel','28',1062),(2293,'Monte Cristi','15',1062),(2294,'Monte Plata','29',1062),(2295,'Pedernales','16',1062),(2296,'Peravia','17',1062),(2297,'Puerto Plata','18',1062),(2298,'Salcedo','19',1062),(2299,'Samaná','20',1062),(2300,'San Cristóbal','21',1062),(2301,'San Pedro de Macorís','23',1062),(2302,'Sánchez Ramírez','24',1062),(2303,'Santiago','25',1062),(2304,'Santiago Rodríguez','26',1062),(2305,'Valverde','27',1062),(2306,'Adrar','01',1003),(2307,'Ain Defla','44',1003),(2308,'Ain Tmouchent','46',1003),(2309,'Alger','16',1003),(2310,'Annaba','23',1003),(2311,'Batna','05',1003),(2312,'Bechar','08',1003),(2313,'Bejaia','06',1003),(2314,'Biskra','07',1003),(2315,'Blida','09',1003),(2316,'Bordj Bou Arreridj','34',1003),(2317,'Bouira','10',1003),(2318,'Boumerdes','35',1003),(2319,'Chlef','02',1003),(2320,'Constantine','25',1003),(2321,'Djelfa','17',1003),(2322,'El Bayadh','32',1003),(2323,'El Oued','39',1003),(2324,'El Tarf','36',1003),(2325,'Ghardaia','47',1003),(2326,'Guelma','24',1003),(2327,'Illizi','33',1003),(2328,'Jijel','18',1003),(2329,'Khenchela','40',1003),(2330,'Laghouat','03',1003),(2331,'Mascara','29',1003),(2332,'Medea','26',1003),(2333,'Mila','43',1003),(2334,'Mostaganem','27',1003),(2335,'Msila','28',1003),(2336,'Naama','45',1003),(2337,'Oran','31',1003),(2338,'Ouargla','30',1003),(2339,'Oum el Bouaghi','04',1003),(2340,'Relizane','48',1003),(2341,'Saida','20',1003),(2342,'Setif','19',1003),(2343,'Sidi Bel Abbes','22',1003),(2344,'Skikda','21',1003),(2345,'Souk Ahras','41',1003),(2346,'Tamanghasset','11',1003),(2347,'Tebessa','12',1003),(2348,'Tiaret','14',1003),(2349,'Tindouf','37',1003),(2350,'Tipaza','42',1003),(2351,'Tissemsilt','38',1003),(2352,'Tizi Ouzou','15',1003),(2353,'Tlemcen','13',1003),(2354,'Azuay','A',1064),(2355,'Bolivar','B',1064),(2356,'Canar','F',1064),(2357,'Carchi','C',1064),(2358,'Cotopaxi','X',1064),(2359,'Chimborazo','H',1064),(2360,'El Oro','O',1064),(2361,'Esmeraldas','E',1064),(2362,'Galapagos','W',1064),(2363,'Guayas','G',1064),(2364,'Imbabura','I',1064),(2365,'Loja','L',1064),(2366,'Los Rios','R',1064),(2367,'Manabi','M',1064),(2368,'Morona-Santiago','S',1064),(2369,'Napo','N',1064),(2370,'Orellana','D',1064),(2371,'Pastaza','Y',1064),(2372,'Pichincha','P',1064),(2373,'Sucumbios','U',1064),(2374,'Tungurahua','T',1064),(2375,'Zamora-Chinchipe','Z',1064),(2376,'Harjumaa','37',1069),(2377,'Hiiumaa','39',1069),(2378,'Ida-Virumaa','44',1069),(2379,'Jõgevamaa','49',1069),(2380,'Järvamaa','51',1069),(2381,'Läänemaa','57',1069),(2382,'Lääne-Virumaa','59',1069),(2383,'Põlvamaa','65',1069),(2384,'Pärnumaa','67',1069),(2385,'Raplamaa','70',1069),(2386,'Saaremaa','74',1069),(2387,'Tartumaa','7B',1069),(2388,'Valgamaa','82',1069),(2389,'Viljandimaa','84',1069),(2390,'Võrumaa','86',1069),(2391,'Ad Daqahllyah','DK',1065),(2392,'Al Bahr al Ahmar','BA',1065),(2393,'Al Buhayrah','BH',1065),(2394,'Al Fayym','FYM',1065),(2395,'Al Gharbiyah','GH',1065),(2396,'Al Iskandarlyah','ALX',1065),(2397,'Al Isma illyah','IS',1065),(2398,'Al Jizah','GZ',1065),(2399,'Al Minuflyah','MNF',1065),(2400,'Al Minya','MN',1065),(2401,'Al Qahirah','C',1065),(2402,'Al Qalyublyah','KB',1065),(2403,'Al Wadi al Jadid','WAD',1065),(2404,'Ash Sharqiyah','SHR',1065),(2405,'As Suways','SUZ',1065),(2406,'Aswan','ASN',1065),(2407,'Asyut','AST',1065),(2408,'Bani Suwayf','BNS',1065),(2409,'Bur Sa\'id','PTS',1065),(2410,'Dumyat','DT',1065),(2411,'Janub Sina\'','JS',1065),(2412,'Kafr ash Shaykh','KFS',1065),(2413,'Matruh','MT',1065),(2414,'Qina','KN',1065),(2415,'Shamal Sina\'','SIN',1065),(2416,'Suhaj','SHG',1065),(2417,'Anseba','AN',1068),(2418,'Debub','DU',1068),(2419,'Debubawi Keyih Bahri [Debub-Keih-Bahri]','DK',1068),(2420,'Gash-Barka','GB',1068),(2421,'Maakel [Maekel]','MA',1068),(2422,'Semenawi Keyih Bahri [Semien-Keih-Bahri]','SK',1068),(2423,'Álava','VI',1198),(2424,'Albacete','AB',1198),(2425,'Alicante','A',1198),(2426,'Almería','AL',1198),(2427,'Asturias','O',1198),(2428,'Ávila','AV',1198),(2429,'Badajoz','BA',1198),(2430,'Baleares','PM',1198),(2431,'Barcelona','B',1198),(2432,'Burgos','BU',1198),(2433,'Cáceres','CC',1198),(2434,'Cádiz','CA',1198),(2435,'Cantabria','S',1198),(2436,'Castellón','CS',1198),(2437,'Ciudad Real','CR',1198),(2438,'Cuenca','CU',1198),(2439,'Girona [Gerona]','GE',1198),(2440,'Granada','GR',1198),(2441,'Guadalajara','GU',1198),(2442,'Guipúzcoa','SS',1198),(2443,'Huelva','H',1198),(2444,'Huesca','HU',1198),(2445,'Jaén','J',1198),(2446,'La Coruña','C',1198),(2447,'La Rioja','LO',1198),(2448,'Las Palmas','GC',1198),(2449,'León','LE',1198),(2450,'Lleida [Lérida]','L',1198),(2451,'Lugo','LU',1198),(2452,'Madrid','M',1198),(2453,'Málaga','MA',1198),(2454,'Murcia','MU',1198),(2455,'Navarra','NA',1198),(2456,'Ourense','OR',1198),(2457,'Palencia','P',1198),(2458,'Pontevedra','PO',1198),(2459,'Salamanca','SA',1198),(2460,'Santa Cruz de Tenerife','TF',1198),(2461,'Segovia','SG',1198),(2462,'Sevilla','SE',1198),(2463,'Soria','SO',1198),(2464,'Tarragona','T',1198),(2465,'Teruel','TE',1198),(2466,'Valencia','V',1198),(2467,'Valladolid','VA',1198),(2468,'Vizcaya','BI',1198),(2469,'Zamora','ZA',1198),(2470,'Zaragoza','Z',1198),(2471,'Ceuta','CE',1198),(2472,'Melilla','ML',1198),(2473,'Addis Ababa','AA',1070),(2474,'Dire Dawa','DD',1070),(2475,'Afar','AF',1070),(2476,'Amara','AM',1070),(2477,'Benshangul-Gumaz','BE',1070),(2478,'Gambela Peoples','GA',1070),(2479,'Harari People','HA',1070),(2480,'Oromia','OR',1070),(2481,'Somali','SO',1070),(2482,'Southern Nations, Nationalities and Peoples','SN',1070),(2483,'Tigrai','TI',1070),(2490,'Eastern','E',1074),(2491,'Northern','N',1074),(2492,'Western','W',1074),(2493,'Rotuma','R',1074),(2494,'Chuuk','TRK',1141),(2495,'Kosrae','KSA',1141),(2496,'Pohnpei','PNI',1141),(2497,'Yap','YAP',1141),(2498,'Ain','01',1076),(2499,'Aisne','02',1076),(2500,'Allier','03',1076),(2501,'Alpes-de-Haute-Provence','04',1076),(2502,'Alpes-Maritimes','06',1076),(2503,'Ardèche','07',1076),(2504,'Ardennes','08',1076),(2505,'Ariège','09',1076),(2506,'Aube','10',1076),(2507,'Aude','11',1076),(2508,'Aveyron','12',1076),(2509,'Bas-Rhin','67',1076),(2510,'Bouches-du-Rhône','13',1076),(2511,'Calvados','14',1076),(2512,'Cantal','15',1076),(2513,'Charente','16',1076),(2514,'Charente-Maritime','17',1076),(2515,'Cher','18',1076),(2516,'Corrèze','19',1076),(2517,'Corse-du-Sud','20A',1076),(2518,'Côte-d\'Or','21',1076),(2519,'Côtes-d\'Armor','22',1076),(2520,'Creuse','23',1076),(2521,'Deux-Sèvres','79',1076),(2522,'Dordogne','24',1076),(2523,'Doubs','25',1076),(2524,'Drôme','26',1076),(2525,'Essonne','91',1076),(2526,'Eure','27',1076),(2527,'Eure-et-Loir','28',1076),(2528,'Finistère','29',1076),(2529,'Gard','30',1076),(2530,'Gers','32',1076),(2531,'Gironde','33',1076),(2532,'Haut-Rhin','68',1076),(2533,'Haute-Corse','20B',1076),(2534,'Haute-Garonne','31',1076),(2535,'Haute-Loire','43',1076),(2536,'Haute-Saône','70',1076),(2537,'Haute-Savoie','74',1076),(2538,'Haute-Vienne','87',1076),(2539,'Hautes-Alpes','05',1076),(2540,'Hautes-Pyrénées','65',1076),(2541,'Hauts-de-Seine','92',1076),(2542,'Hérault','34',1076),(2543,'Indre','36',1076),(2544,'Ille-et-Vilaine','35',1076),(2545,'Indre-et-Loire','37',1076),(2546,'Isère','38',1076),(2547,'Landes','40',1076),(2548,'Loir-et-Cher','41',1076),(2549,'Loire','42',1076),(2550,'Loire-Atlantique','44',1076),(2551,'Loiret','45',1076),(2552,'Lot','46',1076),(2553,'Lot-et-Garonne','47',1076),(2554,'Lozère','48',1076),(2555,'Maine-et-Loire','49',1076),(2556,'Manche','50',1076),(2557,'Marne','51',1076),(2558,'Mayenne','53',1076),(2559,'Meurthe-et-Moselle','54',1076),(2560,'Meuse','55',1076),(2561,'Morbihan','56',1076),(2562,'Moselle','57',1076),(2563,'Nièvre','58',1076),(2564,'Nord','59',1076),(2565,'Oise','60',1076),(2566,'Orne','61',1076),(2567,'Paris','75',1076),(2568,'Pas-de-Calais','62',1076),(2569,'Puy-de-Dôme','63',1076),(2570,'Pyrénées-Atlantiques','64',1076),(2571,'Pyrénées-Orientales','66',1076),(2572,'Rhône','69',1076),(2573,'Saône-et-Loire','71',1076),(2574,'Sarthe','72',1076),(2575,'Savoie','73',1076),(2576,'Seine-et-Marne','77',1076),(2577,'Seine-Maritime','76',1076),(2578,'Seine-Saint-Denis','93',1076),(2579,'Somme','80',1076),(2580,'Tarn','81',1076),(2581,'Tarn-et-Garonne','82',1076),(2582,'Val d\'Oise','95',1076),(2583,'Territoire de Belfort','90',1076),(2584,'Val-de-Marne','94',1076),(2585,'Var','83',1076),(2586,'Vaucluse','84',1076),(2587,'Vendée','85',1076),(2588,'Vienne','86',1076),(2589,'Vosges','88',1076),(2590,'Yonne','89',1076),(2591,'Yvelines','78',1076),(2592,'Aberdeen City','ABE',1226),(2593,'Aberdeenshire','ABD',1226),(2594,'Angus','ANS',1226),(2595,'Co Antrim','ANT',1226),(2597,'Argyll and Bute','AGB',1226),(2598,'Co Armagh','ARM',1226),(2606,'Bedfordshire','BDF',1226),(2612,'Blaenau Gwent','BGW',1226),(2620,'Bristol, City of','BST',1226),(2622,'Buckinghamshire','BKM',1226),(2626,'Cambridgeshire','CAM',1226),(2634,'Cheshire','CHS',1226),(2635,'Clackmannanshire','CLK',1226),(2639,'Cornwall','CON',1226),(2643,'Cumbria','CMA',1226),(2647,'Derbyshire','DBY',1226),(2648,'Co Londonderry','DRY',1226),(2649,'Devon','DEV',1226),(2651,'Dorset','DOR',1226),(2652,'Co Down','DOW',1226),(2654,'Dumfries and Galloway','DGY',1226),(2655,'Dundee City','DND',1226),(2657,'County Durham','DUR',1226),(2659,'East Ayrshire','EAY',1226),(2660,'East Dunbartonshire','EDU',1226),(2661,'East Lothian','ELN',1226),(2662,'East Renfrewshire','ERW',1226),(2663,'East Riding of Yorkshire','ERY',1226),(2664,'East Sussex','ESX',1226),(2665,'Edinburgh, City of','EDH',1226),(2666,'Na h-Eileanan Siar','ELS',1226),(2668,'Essex','ESS',1226),(2669,'Falkirk','FAL',1226),(2670,'Co Fermanagh','FER',1226),(2671,'Fife','FIF',1226),(2674,'Glasgow City','GLG',1226),(2675,'Gloucestershire','GLS',1226),(2678,'Gwynedd','GWN',1226),(2682,'Hampshire','HAM',1226),(2687,'Herefordshire','HEF',1226),(2688,'Hertfordshire','HRT',1226),(2689,'Highland','HED',1226),(2692,'Inverclyde','IVC',1226),(2694,'Isle of Wight','IOW',1226),(2699,'Kent','KEN',1226),(2705,'Lancashire','LAN',1226),(2709,'Leicestershire','LEC',1226),(2712,'Lincolnshire','LIN',1226),(2723,'Midlothian','MLN',1226),(2726,'Moray','MRY',1226),(2734,'Norfolk','NFK',1226),(2735,'North Ayrshire','NAY',1226),(2738,'North Lanarkshire','NLK',1226),(2742,'North Yorkshire','NYK',1226),(2743,'Northamptonshire','NTH',1226),(2744,'Northumberland','NBL',1226),(2746,'Nottinghamshire','NTT',1226),(2747,'Oldham','OLD',1226),(2748,'Omagh','OMH',1226),(2749,'Orkney Islands','ORR',1226),(2750,'Oxfordshire','OXF',1226),(2752,'Perth and Kinross','PKN',1226),(2757,'Powys','POW',1226),(2761,'Renfrewshire','RFW',1226),(2766,'Rutland','RUT',1226),(2770,'Scottish Borders','SCB',1226),(2773,'Shetland Islands','ZET',1226),(2774,'Shropshire','SHR',1226),(2777,'Somerset','SOM',1226),(2778,'South Ayrshire','SAY',1226),(2779,'South Gloucestershire','SGC',1226),(2780,'South Lanarkshire','SLK',1226),(2785,'Staffordshire','STS',1226),(2786,'Stirling','STG',1226),(2791,'Suffolk','SFK',1226),(2793,'Surrey','SRY',1226),(2804,'Vale of Glamorgan, The','VGL',1226),(2811,'Warwickshire','WAR',1226),(2813,'West Dunbartonshire','WDU',1226),(2814,'West Lothian','WLN',1226),(2815,'West Sussex','WSX',1226),(2818,'Wiltshire','WIL',1226),(2823,'Worcestershire','WOR',1226),(2826,'Ashanti','AH',1083),(2827,'Brong-Ahafo','BA',1083),(2828,'Greater Accra','AA',1083),(2829,'Upper East','UE',1083),(2830,'Upper West','UW',1083),(2831,'Volta','TV',1083),(2832,'Banjul','B',1213),(2833,'Lower River','L',1213),(2834,'MacCarthy Island','M',1213),(2835,'North Bank','N',1213),(2836,'Upper River','U',1213),(2837,'Beyla','BE',1091),(2838,'Boffa','BF',1091),(2839,'Boke','BK',1091),(2840,'Coyah','CO',1091),(2841,'Dabola','DB',1091),(2842,'Dalaba','DL',1091),(2843,'Dinguiraye','DI',1091),(2844,'Dubreka','DU',1091),(2845,'Faranah','FA',1091),(2846,'Forecariah','FO',1091),(2847,'Fria','FR',1091),(2848,'Gaoual','GA',1091),(2849,'Guekedou','GU',1091),(2850,'Kankan','KA',1091),(2851,'Kerouane','KE',1091),(2852,'Kindia','KD',1091),(2853,'Kissidougou','KS',1091),(2854,'Koubia','KB',1091),(2855,'Koundara','KN',1091),(2856,'Kouroussa','KO',1091),(2857,'Labe','LA',1091),(2858,'Lelouma','LE',1091),(2859,'Lola','LO',1091),(2860,'Macenta','MC',1091),(2861,'Mali','ML',1091),(2862,'Mamou','MM',1091),(2863,'Mandiana','MD',1091),(2864,'Nzerekore','NZ',1091),(2865,'Pita','PI',1091),(2866,'Siguiri','SI',1091),(2867,'Telimele','TE',1091),(2868,'Tougue','TO',1091),(2869,'Yomou','YO',1091),(2870,'Region Continental','C',1067),(2871,'Region Insular','I',1067),(2872,'Annobon','AN',1067),(2873,'Bioko Norte','BN',1067),(2874,'Bioko Sur','BS',1067),(2875,'Centro Sur','CS',1067),(2876,'Kie-Ntem','KN',1067),(2877,'Litoral','LI',1067),(2878,'Wele-Nzas','WN',1067),(2879,'Achaïa','13',1085),(2880,'Aitolia-Akarnania','01',1085),(2881,'Argolis','11',1085),(2882,'Arkadia','12',1085),(2883,'Arta','31',1085),(2884,'Attiki','A1',1085),(2885,'Chalkidiki','64',1085),(2886,'Chania','94',1085),(2887,'Chios','85',1085),(2888,'Dodekanisos','81',1085),(2889,'Drama','52',1085),(2890,'Evros','71',1085),(2891,'Evrytania','05',1085),(2892,'Evvoia','04',1085),(2893,'Florina','63',1085),(2894,'Fokis','07',1085),(2895,'Fthiotis','06',1085),(2896,'Grevena','51',1085),(2897,'Ileia','14',1085),(2898,'Imathia','53',1085),(2899,'Ioannina','33',1085),(2900,'Irakleion','91',1085),(2901,'Karditsa','41',1085),(2902,'Kastoria','56',1085),(2903,'Kavalla','55',1085),(2904,'Kefallinia','23',1085),(2905,'Kerkyra','22',1085),(2906,'Kilkis','57',1085),(2907,'Korinthia','15',1085),(2908,'Kozani','58',1085),(2909,'Kyklades','82',1085),(2910,'Lakonia','16',1085),(2911,'Larisa','42',1085),(2912,'Lasithion','92',1085),(2913,'Lefkas','24',1085),(2914,'Lesvos','83',1085),(2915,'Magnisia','43',1085),(2916,'Messinia','17',1085),(2917,'Pella','59',1085),(2918,'Preveza','34',1085),(2919,'Rethymnon','93',1085),(2920,'Rodopi','73',1085),(2921,'Samos','84',1085),(2922,'Serrai','62',1085),(2923,'Thesprotia','32',1085),(2924,'Thessaloniki','54',1085),(2925,'Trikala','44',1085),(2926,'Voiotia','03',1085),(2927,'Xanthi','72',1085),(2928,'Zakynthos','21',1085),(2929,'Agio Oros','69',1085),(2930,'Alta Verapaz','AV',1090),(2931,'Baja Verapaz','BV',1090),(2932,'Chimaltenango','CM',1090),(2933,'Chiquimula','CQ',1090),(2934,'El Progreso','PR',1090),(2935,'Escuintla','ES',1090),(2936,'Guatemala','GU',1090),(2937,'Huehuetenango','HU',1090),(2938,'Izabal','IZ',1090),(2939,'Jalapa','JA',1090),(2940,'Jutiapa','JU',1090),(2941,'Peten','PE',1090),(2942,'Quetzaltenango','QZ',1090),(2943,'Quiche','QC',1090),(2944,'Retalhuleu','RE',1090),(2945,'Sacatepequez','SA',1090),(2946,'San Marcos','SM',1090),(2947,'Santa Rosa','SR',1090),(2948,'Sololá','SO',1090),(2949,'Suchitepequez','SU',1090),(2950,'Totonicapan','TO',1090),(2951,'Zacapa','ZA',1090),(2952,'Bissau','BS',1092),(2953,'Bafata','BA',1092),(2954,'Biombo','BM',1092),(2955,'Bolama','BL',1092),(2956,'Cacheu','CA',1092),(2957,'Gabu','GA',1092),(2958,'Oio','OI',1092),(2959,'Quloara','QU',1092),(2960,'Tombali S','TO',1092),(2961,'Barima-Waini','BA',1093),(2962,'Cuyuni-Mazaruni','CU',1093),(2963,'Demerara-Mahaica','DE',1093),(2964,'East Berbice-Corentyne','EB',1093),(2965,'Essequibo Islands-West Demerara','ES',1093),(2966,'Mahaica-Berbice','MA',1093),(2967,'Pomeroon-Supenaam','PM',1093),(2968,'Potaro-Siparuni','PT',1093),(2969,'Upper Demerara-Berbice','UD',1093),(2970,'Upper Takutu-Upper Essequibo','UT',1093),(2971,'Atlantida','AT',1097),(2972,'Colon','CL',1097),(2973,'Comayagua','CM',1097),(2974,'Copan','CP',1097),(2975,'Cortes','CR',1097),(2976,'Choluteca','CH',1097),(2977,'El Paraiso','EP',1097),(2978,'Francisco Morazan','FM',1097),(2979,'Gracias a Dios','GD',1097),(2980,'Intibuca','IN',1097),(2981,'Islas de la Bahia','IB',1097),(2982,'Lempira','LE',1097),(2983,'Ocotepeque','OC',1097),(2984,'Olancho','OL',1097),(2985,'Santa Barbara','SB',1097),(2986,'Valle','VA',1097),(2987,'Yoro','YO',1097),(2988,'Bjelovarsko-bilogorska zupanija','07',1055),(2989,'Brodsko-posavska zupanija','12',1055),(2990,'Dubrovacko-neretvanska zupanija','19',1055),(2991,'Istarska zupanija','18',1055),(2992,'Karlovacka zupanija','04',1055),(2993,'Koprivnickco-krizevacka zupanija','06',1055),(2994,'Krapinako-zagorska zupanija','02',1055),(2995,'Licko-senjska zupanija','09',1055),(2996,'Medimurska zupanija','20',1055),(2997,'Osjecko-baranjska zupanija','14',1055),(2998,'Pozesko-slavonska zupanija','11',1055),(2999,'Primorsko-goranska zupanija','08',1055),(3000,'Sisacko-moelavacka Iupanija','03',1055),(3001,'Splitako-dalmatinska zupanija','17',1055),(3002,'Sibenako-kninska zupanija','15',1055),(3003,'Varaidinska zupanija','05',1055),(3004,'VirovitiEko-podravska zupanija','10',1055),(3005,'VuRovarako-srijemska zupanija','16',1055),(3006,'Zadaraka','13',1055),(3007,'Zagrebacka zupanija','01',1055),(3008,'Grande-Anse','GA',1094),(3009,'Nord-Est','NE',1094),(3010,'Nord-Ouest','NO',1094),(3011,'Ouest','OU',1094),(3012,'Sud','SD',1094),(3013,'Sud-Est','SE',1094),(3014,'Budapest','BU',1099),(3015,'Bács-Kiskun','BK',1099),(3016,'Baranya','BA',1099),(3017,'Békés','BE',1099),(3018,'Borsod-Abaúj-Zemplén','BZ',1099),(3019,'Csongrád','CS',1099),(3020,'Fejér','FE',1099),(3021,'Győr-Moson-Sopron','GS',1099),(3022,'Hajdu-Bihar','HB',1099),(3023,'Heves','HE',1099),(3024,'Jász-Nagykun-Szolnok','JN',1099),(3025,'Komárom-Esztergom','KE',1099),(3026,'Nográd','NO',1099),(3027,'Pest','PE',1099),(3028,'Somogy','SO',1099),(3029,'Szabolcs-Szatmár-Bereg','SZ',1099),(3030,'Tolna','TO',1099),(3031,'Vas','VA',1099),(3032,'Veszprém','VE',1099),(3033,'Zala','ZA',1099),(3034,'Békéscsaba','BC',1099),(3035,'Debrecen','DE',1099),(3036,'Dunaújváros','DU',1099),(3037,'Eger','EG',1099),(3038,'Győr','GY',1099),(3039,'Hódmezővásárhely','HV',1099),(3040,'Kaposvár','KV',1099),(3041,'Kecskemét','KM',1099),(3042,'Miskolc','MI',1099),(3043,'Nagykanizsa','NK',1099),(3044,'Nyiregyháza','NY',1099),(3045,'Pécs','PS',1099),(3046,'Salgótarján','ST',1099),(3047,'Sopron','SN',1099),(3048,'Szeged','SD',1099),(3049,'Székesfehérvár','SF',1099),(3050,'Szekszárd','SS',1099),(3051,'Szolnok','SK',1099),(3052,'Szombathely','SH',1099),(3053,'Tatabánya','TB',1099),(3054,'Zalaegerszeg','ZE',1099),(3055,'Bali','BA',1102),(3056,'Kepulauan Bangka Belitung','BB',1102),(3057,'Banten','BT',1102),(3058,'Bengkulu','BE',1102),(3059,'Gorontalo','GO',1102),(3060,'Papua Barat','PB',1102),(3061,'Jambi','JA',1102),(3062,'Jawa Barat','JB',1102),(3063,'Jawa Tengah','JT',1102),(3064,'Jawa Timur','JI',1102),(3065,'Kalimantan Barat','KB',1102),(3066,'Kalimantan Timur','KI',1102),(3067,'Kalimantan Selatan','KS',1102),(3068,'Kepulauan Riau','KR',1102),(3069,'Lampung','LA',1102),(3070,'Maluku','MA',1102),(3071,'Maluku Utara','MU',1102),(3072,'Nusa Tenggara Barat','NB',1102),(3073,'Nusa Tenggara Timur','NT',1102),(3074,'Papua','PA',1102),(3075,'Riau','RI',1102),(3076,'Sulawesi Selatan','SN',1102),(3077,'Sulawesi Tengah','ST',1102),(3078,'Sulawesi Tenggara','SG',1102),(3079,'Sulawesi Utara','SA',1102),(3080,'Sumatra Barat','SB',1102),(3081,'Sumatra Selatan','SS',1102),(3082,'Sumatera Utara','SU',1102),(3083,'DKI Jakarta','JK',1102),(3084,'Aceh','AC',1102),(3085,'DI Yogyakarta','YO',1102),(3086,'Cork','C',1105),(3087,'Clare','CE',1105),(3088,'Cavan','CN',1105),(3089,'Carlow','CW',1105),(3090,'Dublin','D',1105),(3091,'Donegal','DL',1105),(3092,'Galway','G',1105),(3093,'Kildare','KE',1105),(3094,'Kilkenny','KK',1105),(3095,'Kerry','KY',1105),(3096,'Longford','LD',1105),(3097,'Louth','LH',1105),(3098,'Limerick','LK',1105),(3099,'Leitrim','LM',1105),(3100,'Laois','LS',1105),(3101,'Meath','MH',1105),(3102,'Monaghan','MN',1105),(3103,'Mayo','MO',1105),(3104,'Offaly','OY',1105),(3105,'Roscommon','RN',1105),(3106,'Sligo','SO',1105),(3107,'Tipperary','TA',1105),(3108,'Waterford','WD',1105),(3109,'Westmeath','WH',1105),(3110,'Wicklow','WW',1105),(3111,'Wexford','WX',1105),(3112,'HaDarom','D',1106),(3113,'HaMerkaz','M',1106),(3114,'HaZafon','Z',1106),(3115,'Haifa','HA',1106),(3116,'Tel-Aviv','TA',1106),(3117,'Jerusalem','JM',1106),(3118,'Al Anbar','AN',1104),(3119,'Al Ba,rah','BA',1104),(3120,'Al Muthanna','MU',1104),(3121,'Al Qadisiyah','QA',1104),(3122,'An Najef','NA',1104),(3123,'Arbil','AR',1104),(3124,'As Sulaymaniyah','SW',1104),(3125,'At Ta\'mim','TS',1104),(3126,'Babil','BB',1104),(3127,'Baghdad','BG',1104),(3128,'Dahuk','DA',1104),(3129,'Dhi Qar','DQ',1104),(3130,'Diyala','DI',1104),(3131,'Karbala\'','KA',1104),(3132,'Maysan','MA',1104),(3133,'Ninawa','NI',1104),(3134,'Salah ad Din','SD',1104),(3135,'Wasit','WA',1104),(3136,'Ardabil','03',1103),(3137,'Azarbayjan-e Gharbi','02',1103),(3138,'Azarbayjan-e Sharqi','01',1103),(3139,'Bushehr','06',1103),(3140,'Chahar Mahall va Bakhtiari','08',1103),(3141,'Esfahan','04',1103),(3142,'Fars','14',1103),(3143,'Gilan','19',1103),(3144,'Golestan','27',1103),(3145,'Hamadan','24',1103),(3146,'Hormozgan','23',1103),(3147,'Iiam','05',1103),(3148,'Kerman','15',1103),(3149,'Kermanshah','17',1103),(3150,'Khorasan','09',1103),(3151,'Khuzestan','10',1103),(3152,'Kohjiluyeh va Buyer Ahmad','18',1103),(3153,'Kordestan','16',1103),(3154,'Lorestan','20',1103),(3155,'Markazi','22',1103),(3156,'Mazandaran','21',1103),(3157,'Qazvin','28',1103),(3158,'Qom','26',1103),(3159,'Semnan','12',1103),(3160,'Sistan va Baluchestan','13',1103),(3161,'Tehran','07',1103),(3162,'Yazd','25',1103),(3163,'Zanjan','11',1103),(3164,'Austurland','7',1100),(3165,'Hofuoborgarsvaeoi utan Reykjavikur','1',1100),(3166,'Norourland eystra','6',1100),(3167,'Norourland vestra','5',1100),(3168,'Reykjavik','0',1100),(3169,'Suourland','8',1100),(3170,'Suournes','2',1100),(3171,'Vestfirolr','4',1100),(3172,'Vesturland','3',1100),(3173,'Agrigento','AG',1107),(3174,'Alessandria','AL',1107),(3175,'Ancona','AN',1107),(3176,'Aosta','AO',1107),(3177,'Arezzo','AR',1107),(3178,'Ascoli Piceno','AP',1107),(3179,'Asti','AT',1107),(3180,'Avellino','AV',1107),(3181,'Bari','BA',1107),(3182,'Belluno','BL',1107),(3183,'Benevento','BN',1107),(3184,'Bergamo','BG',1107),(3185,'Biella','BI',1107),(3186,'Bologna','BO',1107),(3187,'Bolzano','BZ',1107),(3188,'Brescia','BS',1107),(3189,'Brindisi','BR',1107),(3190,'Cagliari','CA',1107),(3191,'Caltanissetta','CL',1107),(3192,'Campobasso','CB',1107),(3193,'Caserta','CE',1107),(3194,'Catania','CT',1107),(3195,'Catanzaro','CZ',1107),(3196,'Chieti','CH',1107),(3197,'Como','CO',1107),(3198,'Cosenza','CS',1107),(3199,'Cremona','CR',1107),(3200,'Crotone','KR',1107),(3201,'Cuneo','CN',1107),(3202,'Enna','EN',1107),(3203,'Ferrara','FE',1107),(3204,'Firenze','FI',1107),(3205,'Foggia','FG',1107),(3206,'Forlì-Cesena','FC',1107),(3207,'Frosinone','FR',1107),(3208,'Genova','GE',1107),(3209,'Gorizia','GO',1107),(3210,'Grosseto','GR',1107),(3211,'Imperia','IM',1107),(3212,'Isernia','IS',1107),(3213,'L\'Aquila','AQ',1107),(3214,'La Spezia','SP',1107),(3215,'Latina','LT',1107),(3216,'Lecce','LE',1107),(3217,'Lecco','LC',1107),(3218,'Livorno','LI',1107),(3219,'Lodi','LO',1107),(3220,'Lucca','LU',1107),(3221,'Macerata','MC',1107),(3222,'Mantova','MN',1107),(3223,'Massa-Carrara','MS',1107),(3224,'Matera','MT',1107),(3225,'Messina','ME',1107),(3226,'Milano','MI',1107),(3227,'Modena','MO',1107),(3228,'Napoli','NA',1107),(3229,'Novara','NO',1107),(3230,'Nuoro','NU',1107),(3231,'Oristano','OR',1107),(3232,'Padova','PD',1107),(3233,'Palermo','PA',1107),(3234,'Parma','PR',1107),(3235,'Pavia','PV',1107),(3236,'Perugia','PG',1107),(3237,'Pesaro e Urbino','PU',1107),(3238,'Pescara','PE',1107),(3239,'Piacenza','PC',1107),(3240,'Pisa','PI',1107),(3241,'Pistoia','PT',1107),(3242,'Pordenone','PN',1107),(3243,'Potenza','PZ',1107),(3244,'Prato','PO',1107),(3245,'Ragusa','RG',1107),(3246,'Ravenna','RA',1107),(3247,'Reggio Calabria','RC',1107),(3248,'Reggio Emilia','RE',1107),(3249,'Rieti','RI',1107),(3250,'Rimini','RN',1107),(3251,'Roma','RM',1107),(3252,'Rovigo','RO',1107),(3253,'Salerno','SA',1107),(3254,'Sassari','SS',1107),(3255,'Savona','SV',1107),(3256,'Siena','SI',1107),(3257,'Siracusa','SR',1107),(3258,'Sondrio','SO',1107),(3259,'Taranto','TA',1107),(3260,'Teramo','TE',1107),(3261,'Terni','TR',1107),(3262,'Torino','TO',1107),(3263,'Trapani','TP',1107),(3264,'Trento','TN',1107),(3265,'Treviso','TV',1107),(3266,'Trieste','TS',1107),(3267,'Udine','UD',1107),(3268,'Varese','VA',1107),(3269,'Venezia','VE',1107),(3270,'Verbano-Cusio-Ossola','VB',1107),(3271,'Vercelli','VC',1107),(3272,'Verona','VR',1107),(3273,'Vibo Valentia','VV',1107),(3274,'Vicenza','VI',1107),(3275,'Viterbo','VT',1107),(3276,'Aichi','23',1109),(3277,'Akita','05',1109),(3278,'Aomori','02',1109),(3279,'Chiba','12',1109),(3280,'Ehime','38',1109),(3281,'Fukui','18',1109),(3282,'Fukuoka','40',1109),(3283,'Fukusima','07',1109),(3284,'Gifu','21',1109),(3285,'Gunma','10',1109),(3286,'Hiroshima','34',1109),(3287,'Hokkaido','01',1109),(3288,'Hyogo','28',1109),(3289,'Ibaraki','08',1109),(3290,'Ishikawa','17',1109),(3291,'Iwate','03',1109),(3292,'Kagawa','37',1109),(3293,'Kagoshima','46',1109),(3294,'Kanagawa','14',1109),(3295,'Kochi','39',1109),(3296,'Kumamoto','43',1109),(3297,'Kyoto','26',1109),(3298,'Mie','24',1109),(3299,'Miyagi','04',1109),(3300,'Miyazaki','45',1109),(3301,'Nagano','20',1109),(3302,'Nagasaki','42',1109),(3303,'Nara','29',1109),(3304,'Niigata','15',1109),(3305,'Oita','44',1109),(3306,'Okayama','33',1109),(3307,'Okinawa','47',1109),(3308,'Osaka','27',1109),(3309,'Saga','41',1109),(3310,'Saitama','11',1109),(3311,'Shiga','25',1109),(3312,'Shimane','32',1109),(3313,'Shizuoka','22',1109),(3314,'Tochigi','09',1109),(3315,'Tokushima','36',1109),(3316,'Tokyo','13',1109),(3317,'Tottori','31',1109),(3318,'Toyama','16',1109),(3319,'Wakayama','30',1109),(3320,'Yamagata','06',1109),(3321,'Yamaguchi','35',1109),(3322,'Yamanashi','19',1109),(3323,'Clarendon','CN',1108),(3324,'Hanover','HR',1108),(3325,'Kingston','KN',1108),(3326,'Portland','PD',1108),(3327,'Saint Andrew','AW',1108),(3328,'Saint Ann','AN',1108),(3329,'Saint Catherine','CE',1108),(3330,'Saint Elizabeth','EH',1108),(3331,'Saint James','JS',1108),(3332,'Saint Mary','MY',1108),(3333,'Saint Thomas','TS',1108),(3334,'Trelawny','TY',1108),(3335,'Westmoreland','WD',1108),(3336,'Ajln','AJ',1110),(3337,'Al \'Aqaba','AQ',1110),(3338,'Al Balqa\'','BA',1110),(3339,'Al Karak','KA',1110),(3340,'Al Mafraq','MA',1110),(3341,'Amman','AM',1110),(3342,'At Tafilah','AT',1110),(3343,'Az Zarga','AZ',1110),(3344,'Irbid','JR',1110),(3345,'Jarash','JA',1110),(3346,'Ma\'an','MN',1110),(3347,'Madaba','MD',1110),(3353,'Bishkek','GB',1117),(3354,'Batken','B',1117),(3355,'Chu','C',1117),(3356,'Jalal-Abad','J',1117),(3357,'Naryn','N',1117),(3358,'Osh','O',1117),(3359,'Talas','T',1117),(3360,'Ysyk-Kol','Y',1117),(3361,'Krong Kaeb','23',1037),(3362,'Krong Pailin','24',1037),(3363,'Xrong Preah Sihanouk','18',1037),(3364,'Phnom Penh','12',1037),(3365,'Baat Dambang','2',1037),(3366,'Banteay Mean Chey','1',1037),(3367,'Rampong Chaam','3',1037),(3368,'Kampong Chhnang','4',1037),(3369,'Kampong Spueu','5',1037),(3370,'Kampong Thum','6',1037),(3371,'Kampot','7',1037),(3372,'Kandaal','8',1037),(3373,'Kach Kong','9',1037),(3374,'Krachoh','10',1037),(3375,'Mondol Kiri','11',1037),(3376,'Otdar Mean Chey','22',1037),(3377,'Pousaat','15',1037),(3378,'Preah Vihear','13',1037),(3379,'Prey Veaeng','14',1037),(3380,'Rotanak Kiri','16',1037),(3381,'Siem Reab','17',1037),(3382,'Stueng Traeng','19',1037),(3383,'Svaay Rieng','20',1037),(3384,'Taakaev','21',1037),(3385,'Gilbert Islands','G',1113),(3386,'Line Islands','L',1113),(3387,'Phoenix Islands','P',1113),(3388,'Anjouan Ndzouani','A',1049),(3389,'Grande Comore Ngazidja','G',1049),(3390,'Moheli Moili','M',1049),(3391,'Kaesong-si','KAE',1114),(3392,'Nampo-si','NAM',1114),(3393,'Pyongyang-ai','PYO',1114),(3394,'Chagang-do','CHA',1114),(3395,'Hamgyongbuk-do','HAB',1114),(3396,'Hamgyongnam-do','HAN',1114),(3397,'Hwanghaebuk-do','HWB',1114),(3398,'Hwanghaenam-do','HWN',1114),(3399,'Kangwon-do','KAN',1114),(3400,'Pyonganbuk-do','PYB',1114),(3401,'Pyongannam-do','PYN',1114),(3402,'Yanggang-do','YAN',1114),(3403,'Najin Sonbong-si','NAJ',1114),(3404,'Seoul Teugbyeolsi','11',1115),(3405,'Busan Gwang\'yeogsi','26',1115),(3406,'Daegu Gwang\'yeogsi','27',1115),(3407,'Daejeon Gwang\'yeogsi','30',1115),(3408,'Gwangju Gwang\'yeogsi','29',1115),(3409,'Incheon Gwang\'yeogsi','28',1115),(3410,'Ulsan Gwang\'yeogsi','31',1115),(3411,'Chungcheongbugdo','43',1115),(3412,'Chungcheongnamdo','44',1115),(3413,'Gang\'weondo','42',1115),(3414,'Gyeonggido','41',1115),(3415,'Gyeongsangbugdo','47',1115),(3416,'Gyeongsangnamdo','48',1115),(3417,'Jejudo','49',1115),(3418,'Jeonrabugdo','45',1115),(3419,'Jeonranamdo','46',1115),(3420,'Al Ahmadi','AH',1116),(3421,'Al Farwanlyah','FA',1116),(3422,'Al Jahrah','JA',1116),(3423,'Al Kuwayt','KU',1116),(3424,'Hawalli','HA',1116),(3425,'Almaty','ALA',1111),(3426,'Astana','AST',1111),(3427,'Almaty oblysy','ALM',1111),(3428,'Aqmola oblysy','AKM',1111),(3429,'Aqtobe oblysy','AKT',1111),(3430,'Atyrau oblyfiy','ATY',1111),(3431,'Batys Quzaqstan oblysy','ZAP',1111),(3432,'Mangghystau oblysy','MAN',1111),(3433,'Ongtustik Quzaqstan oblysy','YUZ',1111),(3434,'Pavlodar oblysy','PAV',1111),(3435,'Qaraghandy oblysy','KAR',1111),(3436,'Qostanay oblysy','KUS',1111),(3437,'Qyzylorda oblysy','KZY',1111),(3438,'Shyghys Quzaqstan oblysy','VOS',1111),(3439,'Soltustik Quzaqstan oblysy','SEV',1111),(3440,'Zhambyl oblysy Zhambylskaya oblast\'','ZHA',1111),(3441,'Vientiane','VT',1118),(3442,'Attapu','AT',1118),(3443,'Bokeo','BK',1118),(3444,'Bolikhamxai','BL',1118),(3445,'Champasak','CH',1118),(3446,'Houaphan','HO',1118),(3447,'Khammouan','KH',1118),(3448,'Louang Namtha','LM',1118),(3449,'Louangphabang','LP',1118),(3450,'Oudomxai','OU',1118),(3451,'Phongsali','PH',1118),(3452,'Salavan','SL',1118),(3453,'Savannakhet','SV',1118),(3454,'Xaignabouli','XA',1118),(3455,'Xiasomboun','XN',1118),(3456,'Xekong','XE',1118),(3457,'Xiangkhoang','XI',1118),(3458,'Beirut','BA',1120),(3459,'Beqaa','BI',1120),(3460,'Mount Lebanon','JL',1120),(3461,'North Lebanon','AS',1120),(3462,'South Lebanon','JA',1120),(3463,'Nabatieh','NA',1120),(3464,'Ampara','52',1199),(3465,'Anuradhapura','71',1199),(3466,'Badulla','81',1199),(3467,'Batticaloa','51',1199),(3468,'Colombo','11',1199),(3469,'Galle','31',1199),(3470,'Gampaha','12',1199),(3471,'Hambantota','33',1199),(3472,'Jaffna','41',1199),(3473,'Kalutara','13',1199),(3474,'Kandy','21',1199),(3475,'Kegalla','92',1199),(3476,'Kilinochchi','42',1199),(3477,'Kurunegala','61',1199),(3478,'Mannar','43',1199),(3479,'Matale','22',1199),(3480,'Matara','32',1199),(3481,'Monaragala','82',1199),(3482,'Mullaittivu','45',1199),(3483,'Nuwara Eliya','23',1199),(3484,'Polonnaruwa','72',1199),(3485,'Puttalum','62',1199),(3486,'Ratnapura','91',1199),(3487,'Trincomalee','53',1199),(3488,'VavunLya','44',1199),(3489,'Bomi','BM',1122),(3490,'Bong','BG',1122),(3491,'Grand Basaa','GB',1122),(3492,'Grand Cape Mount','CM',1122),(3493,'Grand Gedeh','GG',1122),(3494,'Grand Kru','GK',1122),(3495,'Lofa','LO',1122),(3496,'Margibi','MG',1122),(3497,'Maryland','MY',1122),(3498,'Montserrado','MO',1122),(3499,'Nimba','NI',1122),(3500,'Rivercess','RI',1122),(3501,'Sinoe','SI',1122),(3502,'Berea','D',1121),(3503,'Butha-Buthe','B',1121),(3504,'Leribe','C',1121),(3505,'Mafeteng','E',1121),(3506,'Maseru','A',1121),(3507,'Mohale\'s Hoek','F',1121),(3508,'Mokhotlong','J',1121),(3509,'Qacha\'s Nek','H',1121),(3510,'Quthing','G',1121),(3511,'Thaba-Tseka','K',1121),(3512,'Alytaus Apskritis','AL',1125),(3513,'Kauno Apskritis','KU',1125),(3514,'Klaipėdos Apskritis','KL',1125),(3515,'Marijampolės Apskritis','MR',1125),(3516,'Panevėžio Apskritis','PN',1125),(3517,'Šiaulių Apskritis','SA',1125),(3518,'Tauragės Apskritis','TA',1125),(3519,'Telšių Apskritis','TE',1125),(3520,'Utenos Apskritis','UT',1125),(3521,'Vilniaus Apskritis','VL',1125),(3522,'Diekirch','D',1126),(3523,'GreveNmacher','G',1126),(3550,'Daugavpils','DGV',1119),(3551,'Jelgava','JEL',1119),(3552,'Jūrmala','JUR',1119),(3553,'Liepāja','LPX',1119),(3554,'Rēzekne','REZ',1119),(3555,'Rīga','RIX',1119),(3556,'Ventspils','VEN',1119),(3557,'Ajdābiyā','AJ',1123),(3558,'Al Buţnān','BU',1123),(3559,'Al Hizām al Akhdar','HZ',1123),(3560,'Al Jabal al Akhdar','JA',1123),(3561,'Al Jifārah','JI',1123),(3562,'Al Jufrah','JU',1123),(3563,'Al Kufrah','KF',1123),(3564,'Al Marj','MJ',1123),(3565,'Al Marqab','MB',1123),(3566,'Al Qaţrūn','QT',1123),(3567,'Al Qubbah','QB',1123),(3568,'Al Wāhah','WA',1123),(3569,'An Nuqaţ al Khams','NQ',1123),(3570,'Ash Shāţi\'','SH',1123),(3571,'Az Zāwiyah','ZA',1123),(3572,'Banghāzī','BA',1123),(3573,'Banī Walīd','BW',1123),(3574,'Darnah','DR',1123),(3575,'Ghadāmis','GD',1123),(3576,'Gharyān','GR',1123),(3577,'Ghāt','GT',1123),(3578,'Jaghbūb','JB',1123),(3579,'Mişrātah','MI',1123),(3580,'Mizdah','MZ',1123),(3581,'Murzuq','MQ',1123),(3582,'Nālūt','NL',1123),(3583,'Sabhā','SB',1123),(3584,'Şabrātah Şurmān','SS',1123),(3585,'Surt','SR',1123),(3586,'Tājūrā\' wa an Nawāhī al Arbāh','TN',1123),(3587,'Ţarābulus','TB',1123),(3588,'Tarhūnah-Masallātah','TM',1123),(3589,'Wādī al hayāt','WD',1123),(3590,'Yafran-Jādū','YJ',1123),(3591,'Agadir','AGD',1146),(3592,'Aït Baha','BAH',1146),(3593,'Aït Melloul','MEL',1146),(3594,'Al Haouz','HAO',1146),(3595,'Al Hoceïma','HOC',1146),(3596,'Assa-Zag','ASZ',1146),(3597,'Azilal','AZI',1146),(3598,'Beni Mellal','BEM',1146),(3599,'Ben Sllmane','BES',1146),(3600,'Berkane','BER',1146),(3601,'Boujdour','BOD',1146),(3602,'Boulemane','BOM',1146),(3603,'Casablanca [Dar el Beïda]','CAS',1146),(3604,'Chefchaouene','CHE',1146),(3605,'Chichaoua','CHI',1146),(3606,'El Hajeb','HAJ',1146),(3607,'El Jadida','JDI',1146),(3608,'Errachidia','ERR',1146),(3609,'Essaouira','ESI',1146),(3610,'Es Smara','ESM',1146),(3611,'Fès','FES',1146),(3612,'Figuig','FIG',1146),(3613,'Guelmim','GUE',1146),(3614,'Ifrane','IFR',1146),(3615,'Jerada','JRA',1146),(3616,'Kelaat Sraghna','KES',1146),(3617,'Kénitra','KEN',1146),(3618,'Khemisaet','KHE',1146),(3619,'Khenifra','KHN',1146),(3620,'Khouribga','KHO',1146),(3621,'Laâyoune (EH)','LAA',1146),(3622,'Larache','LAP',1146),(3623,'Marrakech','MAR',1146),(3624,'Meknsès','MEK',1146),(3625,'Nador','NAD',1146),(3626,'Ouarzazate','OUA',1146),(3627,'Oued ed Dahab (EH)','OUD',1146),(3628,'Oujda','OUJ',1146),(3629,'Rabat-Salé','RBA',1146),(3630,'Safi','SAF',1146),(3631,'Sefrou','SEF',1146),(3632,'Settat','SET',1146),(3633,'Sidl Kacem','SIK',1146),(3634,'Tanger','TNG',1146),(3635,'Tan-Tan','TNT',1146),(3636,'Taounate','TAO',1146),(3637,'Taroudannt','TAR',1146),(3638,'Tata','TAT',1146),(3639,'Taza','TAZ',1146),(3640,'Tétouan','TET',1146),(3641,'Tiznit','TIZ',1146),(3642,'Gagauzia, Unitate Teritoriala Autonoma','GA',1142),(3643,'Chisinau','CU',1142),(3644,'Stinga Nistrului, unitatea teritoriala din','SN',1142),(3645,'Balti','BA',1142),(3646,'Cahul','CA',1142),(3647,'Edinet','ED',1142),(3648,'Lapusna','LA',1142),(3649,'Orhei','OR',1142),(3650,'Soroca','SO',1142),(3651,'Taraclia','TA',1142),(3652,'Tighina [Bender]','TI',1142),(3653,'Ungheni','UN',1142),(3654,'Antananarivo','T',1129),(3655,'Antsiranana','D',1129),(3656,'Fianarantsoa','F',1129),(3657,'Mahajanga','M',1129),(3658,'Toamasina','A',1129),(3659,'Toliara','U',1129),(3660,'Ailinglapalap','ALL',1135),(3661,'Ailuk','ALK',1135),(3662,'Arno','ARN',1135),(3663,'Aur','AUR',1135),(3664,'Ebon','EBO',1135),(3665,'Eniwetok','ENI',1135),(3666,'Jaluit','JAL',1135),(3667,'Kili','KIL',1135),(3668,'Kwajalein','KWA',1135),(3669,'Lae','LAE',1135),(3670,'Lib','LIB',1135),(3671,'Likiep','LIK',1135),(3672,'Majuro','MAJ',1135),(3673,'Maloelap','MAL',1135),(3674,'Mejit','MEJ',1135),(3675,'Mili','MIL',1135),(3676,'Namorik','NMK',1135),(3677,'Namu','NMU',1135),(3678,'Rongelap','RON',1135),(3679,'Ujae','UJA',1135),(3680,'Ujelang','UJL',1135),(3681,'Utirik','UTI',1135),(3682,'Wotho','WTN',1135),(3683,'Wotje','WTJ',1135),(3684,'Bamako','BK0',1133),(3685,'Gao','7',1133),(3686,'Kayes','1',1133),(3687,'Kidal','8',1133),(3688,'Xoulikoro','2',1133),(3689,'Mopti','5',1133),(3690,'S69ou','4',1133),(3691,'Sikasso','3',1133),(3692,'Tombouctou','6',1133),(3693,'Ayeyarwady','07',1035),(3694,'Bago','02',1035),(3695,'Magway','03',1035),(3696,'Mandalay','04',1035),(3697,'Sagaing','01',1035),(3698,'Tanintharyi','05',1035),(3699,'Yangon','06',1035),(3700,'Chin','14',1035),(3701,'Kachin','11',1035),(3702,'Kayah','12',1035),(3703,'Kayin','13',1035),(3704,'Mon','15',1035),(3705,'Rakhine','16',1035),(3706,'Shan','17',1035),(3707,'Ulaanbaatar','1',1144),(3708,'Arhangay','073',1144),(3709,'Bayanhongor','069',1144),(3710,'Bayan-Olgiy','071',1144),(3711,'Bulgan','067',1144),(3712,'Darhan uul','037',1144),(3713,'Dornod','061',1144),(3714,'Dornogov,','063',1144),(3715,'DundgovL','059',1144),(3716,'Dzavhan','057',1144),(3717,'Govi-Altay','065',1144),(3718,'Govi-Smber','064',1144),(3719,'Hentiy','039',1144),(3720,'Hovd','043',1144),(3721,'Hovsgol','041',1144),(3722,'Omnogovi','053',1144),(3723,'Orhon','035',1144),(3724,'Ovorhangay','055',1144),(3725,'Selenge','049',1144),(3726,'Shbaatar','051',1144),(3727,'Tov','047',1144),(3728,'Uvs','046',1144),(3729,'Nouakchott','NKC',1137),(3730,'Assaba','03',1137),(3731,'Brakna','05',1137),(3732,'Dakhlet Nouadhibou','08',1137),(3733,'Gorgol','04',1137),(3734,'Guidimaka','10',1137),(3735,'Hodh ech Chargui','01',1137),(3736,'Hodh el Charbi','02',1137),(3737,'Inchiri','12',1137),(3738,'Tagant','09',1137),(3739,'Tiris Zemmour','11',1137),(3740,'Trarza','06',1137),(3741,'Beau Bassin-Rose Hill','BR',1138),(3742,'Curepipe','CU',1138),(3743,'Port Louis','PU',1138),(3744,'Quatre Bornes','QB',1138),(3745,'Vacosa-Phoenix','VP',1138),(3746,'Black River','BL',1138),(3747,'Flacq','FL',1138),(3748,'Grand Port','GP',1138),(3749,'Moka','MO',1138),(3750,'Pamplemousses','PA',1138),(3751,'Plaines Wilhems','PW',1138),(3752,'Riviere du Rempart','RP',1138),(3753,'Savanne','SA',1138),(3754,'Agalega Islands','AG',1138),(3755,'Cargados Carajos Shoals','CC',1138),(3756,'Rodrigues Island','RO',1138),(3757,'Male','MLE',1132),(3758,'Alif','02',1132),(3759,'Baa','20',1132),(3760,'Dhaalu','17',1132),(3761,'Faafu','14',1132),(3762,'Gaaf Alif','27',1132),(3763,'Gaefu Dhaalu','28',1132),(3764,'Gnaviyani','29',1132),(3765,'Haa Alif','07',1132),(3766,'Haa Dhaalu','23',1132),(3767,'Kaafu','26',1132),(3768,'Laamu','05',1132),(3769,'Lhaviyani','03',1132),(3770,'Meemu','12',1132),(3771,'Noonu','25',1132),(3772,'Raa','13',1132),(3773,'Seenu','01',1132),(3774,'Shaviyani','24',1132),(3775,'Thaa','08',1132),(3776,'Vaavu','04',1132),(3777,'Balaka','BA',1130),(3778,'Blantyre','BL',1130),(3779,'Chikwawa','CK',1130),(3780,'Chiradzulu','CR',1130),(3781,'Chitipa','CT',1130),(3782,'Dedza','DE',1130),(3783,'Dowa','DO',1130),(3784,'Karonga','KR',1130),(3785,'Kasungu','KS',1130),(3786,'Likoma Island','LK',1130),(3787,'Lilongwe','LI',1130),(3788,'Machinga','MH',1130),(3789,'Mangochi','MG',1130),(3790,'Mchinji','MC',1130),(3791,'Mulanje','MU',1130),(3792,'Mwanza','MW',1130),(3793,'Mzimba','MZ',1130),(3794,'Nkhata Bay','NB',1130),(3795,'Nkhotakota','NK',1130),(3796,'Nsanje','NS',1130),(3797,'Ntcheu','NU',1130),(3798,'Ntchisi','NI',1130),(3799,'Phalomba','PH',1130),(3800,'Rumphi','RU',1130),(3801,'Salima','SA',1130),(3802,'Thyolo','TH',1130),(3803,'Zomba','ZO',1130),(3804,'Aguascalientes','AGU',1140),(3805,'Baja California','BCN',1140),(3806,'Baja California Sur','BCS',1140),(3807,'Campeche','CAM',1140),(3808,'Coahuila','COA',1140),(3809,'Colima','COL',1140),(3810,'Chiapas','CHP',1140),(3811,'Chihuahua','CHH',1140),(3812,'Durango','DUR',1140),(3813,'Guanajuato','GUA',1140),(3814,'Guerrero','GRO',1140),(3815,'Hidalgo','HID',1140),(3816,'Jalisco','JAL',1140),(3817,'Mexico','MEX',1140),(3818,'Michoacin','MIC',1140),(3819,'Morelos','MOR',1140),(3820,'Nayarit','NAY',1140),(3821,'Nuevo Leon','NLE',1140),(3822,'Oaxaca','OAX',1140),(3823,'Puebla','PUE',1140),(3824,'Queretaro','QUE',1140),(3825,'Quintana Roo','ROO',1140),(3826,'San Luis Potosi','SLP',1140),(3827,'Sinaloa','SIN',1140),(3828,'Sonora','SON',1140),(3829,'Tabasco','TAB',1140),(3830,'Tamaulipas','TAM',1140),(3831,'Tlaxcala','TLA',1140),(3832,'Veracruz','VER',1140),(3833,'Yucatan','YUC',1140),(3834,'Zacatecas','ZAC',1140),(3835,'Wilayah Persekutuan Kuala Lumpur','14',1131),(3836,'Wilayah Persekutuan Labuan','15',1131),(3837,'Wilayah Persekutuan Putrajaya','16',1131),(3838,'Johor','01',1131),(3839,'Kedah','02',1131),(3840,'Kelantan','03',1131),(3841,'Melaka','04',1131),(3842,'Negeri Sembilan','05',1131),(3843,'Pahang','06',1131),(3844,'Perak','08',1131),(3845,'Perlis','09',1131),(3846,'Pulau Pinang','07',1131),(3847,'Sabah','12',1131),(3848,'Sarawak','13',1131),(3849,'Selangor','10',1131),(3850,'Terengganu','11',1131),(3851,'Maputo','MPM',1147),(3852,'Cabo Delgado','P',1147),(3853,'Gaza','G',1147),(3854,'Inhambane','I',1147),(3855,'Manica','B',1147),(3856,'Numpula','N',1147),(3857,'Niaaea','A',1147),(3858,'Sofala','S',1147),(3859,'Tete','T',1147),(3860,'Zambezia','Q',1147),(3861,'Caprivi','CA',1148),(3862,'Erongo','ER',1148),(3863,'Hardap','HA',1148),(3864,'Karas','KA',1148),(3865,'Khomas','KH',1148),(3866,'Kunene','KU',1148),(3867,'Ohangwena','OW',1148),(3868,'Okavango','OK',1148),(3869,'Omaheke','OH',1148),(3870,'Omusati','OS',1148),(3871,'Oshana','ON',1148),(3872,'Oshikoto','OT',1148),(3873,'Otjozondjupa','OD',1148),(3874,'Niamey','8',1156),(3875,'Agadez','1',1156),(3876,'Diffa','2',1156),(3877,'Dosso','3',1156),(3878,'Maradi','4',1156),(3879,'Tahoua','S',1156),(3880,'Tillaberi','6',1156),(3881,'Zinder','7',1156),(3882,'Abuja Federal Capital Territory','FC',1157),(3883,'Abia','AB',1157),(3884,'Adamawa','AD',1157),(3885,'Akwa Ibom','AK',1157),(3886,'Anambra','AN',1157),(3887,'Bauchi','BA',1157),(3888,'Bayelsa','BY',1157),(3889,'Benue','BE',1157),(3890,'Borno','BO',1157),(3891,'Cross River','CR',1157),(3892,'Delta','DE',1157),(3893,'Ebonyi','EB',1157),(3894,'Edo','ED',1157),(3895,'Ekiti','EK',1157),(3896,'Enugu','EN',1157),(3897,'Gombe','GO',1157),(3898,'Imo','IM',1157),(3899,'Jigawa','JI',1157),(3900,'Kaduna','KD',1157),(3901,'Kano','KN',1157),(3902,'Katsina','KT',1157),(3903,'Kebbi','KE',1157),(3904,'Kogi','KO',1157),(3905,'Kwara','KW',1157),(3906,'Lagos','LA',1157),(3907,'Nassarawa','NA',1157),(3908,'Niger','NI',1157),(3909,'Ogun','OG',1157),(3910,'Ondo','ON',1157),(3911,'Osun','OS',1157),(3912,'Oyo','OY',1157),(3913,'Rivers','RI',1157),(3914,'Sokoto','SO',1157),(3915,'Taraba','TA',1157),(3916,'Yobe','YO',1157),(3917,'Zamfara','ZA',1157),(3918,'Boaco','BO',1155),(3919,'Carazo','CA',1155),(3920,'Chinandega','CI',1155),(3921,'Chontales','CO',1155),(3922,'Esteli','ES',1155),(3923,'Jinotega','JI',1155),(3924,'Leon','LE',1155),(3925,'Madriz','MD',1155),(3926,'Managua','MN',1155),(3927,'Masaya','MS',1155),(3928,'Matagalpa','MT',1155),(3929,'Nueva Segovia','NS',1155),(3930,'Rio San Juan','SJ',1155),(3931,'Rivas','RI',1155),(3932,'Atlantico Norte','AN',1155),(3933,'Atlantico Sur','AS',1155),(3934,'Drente','DR',1152),(3935,'Flevoland','FL',1152),(3936,'Friesland','FR',1152),(3937,'Gelderland','GL',1152),(3938,'Groningen','GR',1152),(3939,'Noord-Brabant','NB',1152),(3940,'Noord-Holland','NH',1152),(3941,'Overijssel','OV',1152),(3942,'Utrecht','UT',1152),(3943,'Zuid-Holland','ZH',1152),(3944,'Zeeland','ZL',1152),(3945,'Akershus','02',1161),(3946,'Aust-Agder','09',1161),(3947,'Buskerud','06',1161),(3948,'Finnmark','20',1161),(3949,'Hedmark','04',1161),(3950,'Hordaland','12',1161),(3951,'Møre og Romsdal','15',1161),(3952,'Nordland','18',1161),(3953,'Nord-Trøndelag','17',1161),(3954,'Oppland','05',1161),(3955,'Oslo','03',1161),(3956,'Rogaland','11',1161),(3957,'Sogn og Fjordane','14',1161),(3958,'Sør-Trøndelag','16',1161),(3959,'Telemark','06',1161),(3960,'Troms','19',1161),(3961,'Vest-Agder','10',1161),(3962,'Vestfold','07',1161),(3963,'Østfold','01',1161),(3964,'Jan Mayen','22',1161),(3965,'Svalbard','21',1161),(3966,'Auckland','AUK',1154),(3967,'Bay of Plenty','BOP',1154),(3968,'Canterbury','CAN',1154),(3969,'Gisborne','GIS',1154),(3970,'Hawkes Bay','HKB',1154),(3971,'Manawatu-Wanganui','MWT',1154),(3972,'Marlborough','MBH',1154),(3973,'Nelson','NSN',1154),(3974,'Northland','NTL',1154),(3975,'Otago','OTA',1154),(3976,'Southland','STL',1154),(3977,'Taranaki','TKI',1154),(3978,'Tasman','TAS',1154),(3979,'Waikato','WKO',1154),(3980,'Wellington','WGN',1154),(3981,'West Coast','WTC',1154),(3982,'Ad Dakhillyah','DA',1162),(3983,'Al Batinah','BA',1162),(3984,'Al Janblyah','JA',1162),(3985,'Al Wusta','WU',1162),(3986,'Ash Sharqlyah','SH',1162),(3987,'Az Zahirah','ZA',1162),(3988,'Masqat','MA',1162),(3989,'Musandam','MU',1162),(3990,'Bocas del Toro','1',1166),(3991,'Cocle','2',1166),(3992,'Chiriqui','4',1166),(3993,'Darien','5',1166),(3994,'Herrera','6',1166),(3995,'Loa Santoa','7',1166),(3996,'Panama','8',1166),(3997,'Veraguas','9',1166),(3998,'Comarca de San Blas','Q',1166),(3999,'El Callao','CAL',1169),(4000,'Ancash','ANC',1169),(4001,'Apurimac','APU',1169),(4002,'Arequipa','ARE',1169),(4003,'Ayacucho','AYA',1169),(4004,'Cajamarca','CAJ',1169),(4005,'Cuzco','CUS',1169),(4006,'Huancavelica','HUV',1169),(4007,'Huanuco','HUC',1169),(4008,'Ica','ICA',1169),(4009,'Junin','JUN',1169),(4010,'La Libertad','LAL',1169),(4011,'Lambayeque','LAM',1169),(4012,'Lima','LIM',1169),(4013,'Loreto','LOR',1169),(4014,'Madre de Dios','MDD',1169),(4015,'Moquegua','MOQ',1169),(4016,'Pasco','PAS',1169),(4017,'Piura','PIU',1169),(4018,'Puno','PUN',1169),(4019,'San Martin','SAM',1169),(4020,'Tacna','TAC',1169),(4021,'Tumbes','TUM',1169),(4022,'Ucayali','UCA',1169),(4023,'National Capital District (Port Moresby)','NCD',1167),(4024,'Chimbu','CPK',1167),(4025,'Eastern Highlands','EHG',1167),(4026,'East New Britain','EBR',1167),(4027,'East Sepik','ESW',1167),(4028,'Enga','EPW',1167),(4029,'Gulf','GPK',1167),(4030,'Madang','MPM',1167),(4031,'Manus','MRL',1167),(4032,'Milne Bay','MBA',1167),(4033,'Morobe','MPL',1167),(4034,'New Ireland','NIK',1167),(4035,'North Solomons','NSA',1167),(4036,'Santaun','SAN',1167),(4037,'Southern Highlands','SHM',1167),(4038,'Western Highlands','WHM',1167),(4039,'West New Britain','WBK',1167),(4040,'Abra','ABR',1170),(4041,'Agusan del Norte','AGN',1170),(4042,'Agusan del Sur','AGS',1170),(4043,'Aklan','AKL',1170),(4044,'Albay','ALB',1170),(4045,'Antique','ANT',1170),(4046,'Apayao','APA',1170),(4047,'Aurora','AUR',1170),(4048,'Basilan','BAS',1170),(4049,'Bataan','BAN',1170),(4050,'Batanes','BTN',1170),(4051,'Batangas','BTG',1170),(4052,'Benguet','BEN',1170),(4053,'Biliran','BIL',1170),(4054,'Bohol','BOH',1170),(4055,'Bukidnon','BUK',1170),(4056,'Bulacan','BUL',1170),(4057,'Cagayan','CAG',1170),(4058,'Camarines Norte','CAN',1170),(4059,'Camarines Sur','CAS',1170),(4060,'Camiguin','CAM',1170),(4061,'Capiz','CAP',1170),(4062,'Catanduanes','CAT',1170),(4063,'Cavite','CAV',1170),(4064,'Cebu','CEB',1170),(4065,'Compostela Valley','COM',1170),(4066,'Davao','DAV',1170),(4067,'Davao del Sur','DAS',1170),(4068,'Davao Oriental','DAO',1170),(4069,'Eastern Samar','EAS',1170),(4070,'Guimaras','GUI',1170),(4071,'Ifugao','IFU',1170),(4072,'Ilocos Norte','ILN',1170),(4073,'Ilocos Sur','ILS',1170),(4074,'Iloilo','ILI',1170),(4075,'Isabela','ISA',1170),(4076,'Kalinga-Apayso','KAL',1170),(4077,'Laguna','LAG',1170),(4078,'Lanao del Norte','LAN',1170),(4079,'Lanao del Sur','LAS',1170),(4080,'La Union','LUN',1170),(4081,'Leyte','LEY',1170),(4082,'Maguindanao','MAG',1170),(4083,'Marinduque','MAD',1170),(4084,'Masbate','MAS',1170),(4085,'Mindoro Occidental','MDC',1170),(4086,'Mindoro Oriental','MDR',1170),(4087,'Misamis Occidental','MSC',1170),(4088,'Misamis Oriental','MSR',1170),(4089,'Mountain Province','MOU',1170),(4090,'Negroe Occidental','NEC',1170),(4091,'Negros Oriental','NER',1170),(4092,'North Cotabato','NCO',1170),(4093,'Northern Samar','NSA',1170),(4094,'Nueva Ecija','NUE',1170),(4095,'Nueva Vizcaya','NUV',1170),(4096,'Palawan','PLW',1170),(4097,'Pampanga','PAM',1170),(4098,'Pangasinan','PAN',1170),(4099,'Quezon','QUE',1170),(4100,'Quirino','QUI',1170),(4101,'Rizal','RIZ',1170),(4102,'Romblon','ROM',1170),(4103,'Sarangani','SAR',1170),(4104,'Siquijor','SIG',1170),(4105,'Sorsogon','SOR',1170),(4106,'South Cotabato','SCO',1170),(4107,'Southern Leyte','SLE',1170),(4108,'Sultan Kudarat','SUK',1170),(4109,'Sulu','SLU',1170),(4110,'Surigao del Norte','SUN',1170),(4111,'Surigao del Sur','SUR',1170),(4112,'Tarlac','TAR',1170),(4113,'Tawi-Tawi','TAW',1170),(4114,'Western Samar','WSA',1170),(4115,'Zambales','ZMB',1170),(4116,'Zamboanga del Norte','ZAN',1170),(4117,'Zamboanga del Sur','ZAS',1170),(4118,'Zamboanga Sibiguey','ZSI',1170),(4119,'Islamabad Federal Capital Area','IS',1163),(4120,'Baluchistan','BA',1163),(4121,'Khyber Pakhtun Khawa','NW',1163),(4122,'Sindh','SD',1163),(4123,'Federally Administered Tribal Areas','TA',1163),(4124,'Azad Kashmir','JK',1163),(4125,'Gilgit-Baltistan','NA',1163),(4126,'Aveiro','01',1173),(4127,'Beja','02',1173),(4128,'Braga','03',1173),(4129,'Bragança','04',1173),(4130,'Castelo Branco','05',1173),(4131,'Coimbra','06',1173),(4132,'Évora','07',1173),(4133,'Faro','08',1173),(4134,'Guarda','09',1173),(4135,'Leiria','10',1173),(4136,'Lisboa','11',1173),(4137,'Portalegre','12',1173),(4138,'Porto','13',1173),(4139,'Santarém','14',1173),(4140,'Setúbal','15',1173),(4141,'Viana do Castelo','16',1173),(4142,'Vila Real','17',1173),(4143,'Viseu','18',1173),(4144,'Região Autónoma dos Açores','20',1173),(4145,'Região Autónoma da Madeira','30',1173),(4146,'Asuncion','ASU',1168),(4147,'Alto Paraguay','16',1168),(4148,'Alto Parana','10',1168),(4149,'Amambay','13',1168),(4150,'Boqueron','19',1168),(4151,'Caeguazu','5',1168),(4152,'Caazapl','6',1168),(4153,'Canindeyu','14',1168),(4154,'Concepcion','1',1168),(4155,'Cordillera','3',1168),(4156,'Guaira','4',1168),(4157,'Itapua','7',1168),(4158,'Miaiones','8',1168),(4159,'Neembucu','12',1168),(4160,'Paraguari','9',1168),(4161,'Presidente Hayes','15',1168),(4162,'San Pedro','2',1168),(4163,'Ad Dawhah','DA',1175),(4164,'Al Ghuwayriyah','GH',1175),(4165,'Al Jumayliyah','JU',1175),(4166,'Al Khawr','KH',1175),(4167,'Al Wakrah','WA',1175),(4168,'Ar Rayyan','RA',1175),(4169,'Jariyan al Batnah','JB',1175),(4170,'Madinat ash Shamal','MS',1175),(4171,'Umm Salal','US',1175),(4172,'Bucuresti','B',1176),(4173,'Alba','AB',1176),(4174,'Arad','AR',1176),(4175,'Argeș','AG',1176),(4176,'Bacău','BC',1176),(4177,'Bihor','BH',1176),(4178,'Bistrița-Năsăud','BN',1176),(4179,'Botoșani','BT',1176),(4180,'Brașov','BV',1176),(4181,'Brăila','BR',1176),(4182,'Buzău','BZ',1176),(4183,'Caraș-Severin','CS',1176),(4184,'Călărași','CL',1176),(4185,'Cluj','CJ',1176),(4186,'Constanța','CT',1176),(4187,'Covasna','CV',1176),(4188,'Dâmbovița','DB',1176),(4189,'Dolj','DJ',1176),(4190,'Galați','GL',1176),(4191,'Giurgiu','GR',1176),(4192,'Gorj','GJ',1176),(4193,'Harghita','HR',1176),(4194,'Hunedoara','HD',1176),(4195,'Ialomița','IL',1176),(4196,'Iași','IS',1176),(4197,'Ilfov','IF',1176),(4198,'Maramureș','MM',1176),(4199,'Mehedinți','MH',1176),(4200,'Mureș','MS',1176),(4201,'Neamț','NT',1176),(4202,'Olt','OT',1176),(4203,'Prahova','PH',1176),(4204,'Satu Mare','SM',1176),(4205,'Sălaj','SJ',1176),(4206,'Sibiu','SB',1176),(4207,'Suceava','SV',1176),(4208,'Teleorman','TR',1176),(4209,'Timiș','TM',1176),(4210,'Tulcea','TL',1176),(4211,'Vaslui','VS',1176),(4212,'Vâlcea','VL',1176),(4213,'Vrancea','VN',1176),(4214,'Adygeya, Respublika','AD',1177),(4215,'Altay, Respublika','AL',1177),(4216,'Bashkortostan, Respublika','BA',1177),(4217,'Buryatiya, Respublika','BU',1177),(4218,'Chechenskaya Respublika','CE',1177),(4219,'Chuvashskaya Respublika','CU',1177),(4220,'Dagestan, Respublika','DA',1177),(4221,'Ingushskaya Respublika','IN',1177),(4222,'Kabardino-Balkarskaya','KB',1177),(4223,'Kalmykiya, Respublika','KL',1177),(4224,'Karachayevo-Cherkesskaya Respublika','KC',1177),(4225,'Kareliya, Respublika','KR',1177),(4226,'Khakasiya, Respublika','KK',1177),(4227,'Komi, Respublika','KO',1177),(4228,'Mariy El, Respublika','ME',1177),(4229,'Mordoviya, Respublika','MO',1177),(4230,'Sakha, Respublika [Yakutiya]','SA',1177),(4231,'Severnaya Osetiya, Respublika','SE',1177),(4232,'Tatarstan, Respublika','TA',1177),(4233,'Tyva, Respublika [Tuva]','TY',1177),(4234,'Udmurtskaya Respublika','UD',1177),(4235,'Altayskiy kray','ALT',1177),(4236,'Khabarovskiy kray','KHA',1177),(4237,'Krasnodarskiy kray','KDA',1177),(4238,'Krasnoyarskiy kray','KYA',1177),(4239,'Primorskiy kray','PRI',1177),(4240,'Stavropol\'skiy kray','STA',1177),(4241,'Amurskaya oblast\'','AMU',1177),(4242,'Arkhangel\'skaya oblast\'','ARK',1177),(4243,'Astrakhanskaya oblast\'','AST',1177),(4244,'Belgorodskaya oblast\'','BEL',1177),(4245,'Bryanskaya oblast\'','BRY',1177),(4246,'Chelyabinskaya oblast\'','CHE',1177),(4247,'Zabaykalsky Krai\'','ZSK',1177),(4248,'Irkutskaya oblast\'','IRK',1177),(4249,'Ivanovskaya oblast\'','IVA',1177),(4250,'Kaliningradskaya oblast\'','KGD',1177),(4251,'Kaluzhskaya oblast\'','KLU',1177),(4252,'Kamchatka Krai\'','KAM',1177),(4253,'Kemerovskaya oblast\'','KEM',1177),(4254,'Kirovskaya oblast\'','KIR',1177),(4255,'Kostromskaya oblast\'','KOS',1177),(4256,'Kurganskaya oblast\'','KGN',1177),(4257,'Kurskaya oblast\'','KRS',1177),(4258,'Leningradskaya oblast\'','LEN',1177),(4259,'Lipetskaya oblast\'','LIP',1177),(4260,'Magadanskaya oblast\'','MAG',1177),(4261,'Moskovskaya oblast\'','MOS',1177),(4262,'Murmanskaya oblast\'','MUR',1177),(4263,'Nizhegorodskaya oblast\'','NIZ',1177),(4264,'Novgorodskaya oblast\'','NGR',1177),(4265,'Novosibirskaya oblast\'','NVS',1177),(4266,'Omskaya oblast\'','OMS',1177),(4267,'Orenburgskaya oblast\'','ORE',1177),(4268,'Orlovskaya oblast\'','ORL',1177),(4269,'Penzenskaya oblast\'','PNZ',1177),(4270,'Perm krai\'','PEK',1177),(4271,'Pskovskaya oblast\'','PSK',1177),(4272,'Rostovskaya oblast\'','ROS',1177),(4273,'Ryazanskaya oblast\'','RYA',1177),(4274,'Sakhalinskaya oblast\'','SAK',1177),(4275,'Samarskaya oblast\'','SAM',1177),(4276,'Saratovskaya oblast\'','SAR',1177),(4277,'Smolenskaya oblast\'','SMO',1177),(4278,'Sverdlovskaya oblast\'','SVE',1177),(4279,'Tambovskaya oblast\'','TAM',1177),(4280,'Tomskaya oblast\'','TOM',1177),(4281,'Tul\'skaya oblast\'','TUL',1177),(4282,'Tverskaya oblast\'','TVE',1177),(4283,'Tyumenskaya oblast\'','TYU',1177),(4284,'Ul\'yanovskaya oblast\'','ULY',1177),(4285,'Vladimirskaya oblast\'','VLA',1177),(4286,'Volgogradskaya oblast\'','VGG',1177),(4287,'Vologodskaya oblast\'','VLG',1177),(4288,'Voronezhskaya oblast\'','VOR',1177),(4289,'Yaroslavskaya oblast\'','YAR',1177),(4290,'Moskva','MOW',1177),(4291,'Sankt-Peterburg','SPE',1177),(4292,'Yevreyskaya avtonomnaya oblast\'','YEV',1177),(4294,'Chukotskiy avtonomnyy okrug','CHU',1177),(4296,'Khanty-Mansiyskiy avtonomnyy okrug','KHM',1177),(4299,'Nenetskiy avtonomnyy okrug','NEN',1177),(4302,'Yamalo-Nenetskiy avtonomnyy okrug','YAN',1177),(4303,'Butare','C',1178),(4304,'Byumba','I',1178),(4305,'Cyangugu','E',1178),(4306,'Gikongoro','D',1178),(4307,'Gisenyi','G',1178),(4308,'Gitarama','B',1178),(4309,'Kibungo','J',1178),(4310,'Kibuye','F',1178),(4311,'Kigali-Rural Kigali y\' Icyaro','K',1178),(4312,'Kigali-Ville Kigali Ngari','L',1178),(4313,'Mutara','M',1178),(4314,'Ruhengeri','H',1178),(4315,'Al Bahah','11',1187),(4316,'Al Hudud Ash Shamaliyah','08',1187),(4317,'Al Jawf','12',1187),(4318,'Al Madinah','03',1187),(4319,'Al Qasim','05',1187),(4320,'Ar Riyad','01',1187),(4321,'Asir','14',1187),(4322,'Ha\'il','06',1187),(4323,'Jlzan','09',1187),(4324,'Makkah','02',1187),(4325,'Najran','10',1187),(4326,'Tabuk','07',1187),(4327,'Capital Territory (Honiara)','CT',1194),(4328,'Guadalcanal','GU',1194),(4329,'Isabel','IS',1194),(4330,'Makira','MK',1194),(4331,'Malaita','ML',1194),(4332,'Temotu','TE',1194),(4333,'A\'ali an Nil','23',1200),(4334,'Al Bah al Ahmar','26',1200),(4335,'Al Buhayrat','18',1200),(4336,'Al Jazirah','07',1200),(4337,'Al Khartum','03',1200),(4338,'Al Qadarif','06',1200),(4339,'Al Wahdah','22',1200),(4340,'An Nil','04',1200),(4341,'An Nil al Abyaq','08',1200),(4342,'An Nil al Azraq','24',1200),(4343,'Ash Shamallyah','01',1200),(4344,'Bahr al Jabal','17',1200),(4345,'Gharb al Istiwa\'iyah','16',1200),(4346,'Gharb Ba~r al Ghazal','14',1200),(4347,'Gharb Darfur','12',1200),(4348,'Gharb Kurdufan','10',1200),(4349,'Janub Darfur','11',1200),(4350,'Janub Rurdufan','13',1200),(4351,'Jnqall','20',1200),(4352,'Kassala','05',1200),(4353,'Shamal Batr al Ghazal','15',1200),(4354,'Shamal Darfur','02',1200),(4355,'Shamal Kurdufan','09',1200),(4356,'Sharq al Istiwa\'iyah','19',1200),(4357,'Sinnar','25',1200),(4358,'Warab','21',1200),(4359,'Blekinge län','K',1204),(4360,'Dalarnas län','W',1204),(4361,'Gotlands län','I',1204),(4362,'Gävleborgs län','X',1204),(4363,'Hallands län','N',1204),(4364,'Jämtlands län','Z',1204),(4365,'Jönkopings län','F',1204),(4366,'Kalmar län','H',1204),(4367,'Kronobergs län','G',1204),(4368,'Norrbottens län','BD',1204),(4369,'Skåne län','M',1204),(4370,'Stockholms län','AB',1204),(4371,'Södermanlands län','D',1204),(4372,'Uppsala län','C',1204),(4373,'Värmlands län','S',1204),(4374,'Västerbottens län','AC',1204),(4375,'Västernorrlands län','Y',1204),(4376,'Västmanlands län','U',1204),(4377,'Västra Götalands län','Q',1204),(4378,'Örebro län','T',1204),(4379,'Östergötlands län','E',1204),(4380,'Saint Helena','SH',1180),(4381,'Ascension','AC',1180),(4382,'Tristan da Cunha','TA',1180),(4383,'Ajdovščina','001',1193),(4384,'Beltinci','002',1193),(4385,'Benedikt','148',1193),(4386,'Bistrica ob Sotli','149',1193),(4387,'Bled','003',1193),(4388,'Bloke','150',1193),(4389,'Bohinj','004',1193),(4390,'Borovnica','005',1193),(4391,'Bovec','006',1193),(4392,'Braslovče','151',1193),(4393,'Brda','007',1193),(4394,'Brezovica','008',1193),(4395,'Brežice','009',1193),(4396,'Cankova','152',1193),(4397,'Celje','011',1193),(4398,'Cerklje na Gorenjskem','012',1193),(4399,'Cerknica','013',1193),(4400,'Cerkno','014',1193),(4401,'Cerkvenjak','153',1193),(4402,'Črenšovci','015',1193),(4403,'Črna na Koroškem','016',1193),(4404,'Črnomelj','017',1193),(4405,'Destrnik','018',1193),(4406,'Divača','019',1193),(4407,'Dobje','154',1193),(4408,'Dobrepolje','020',1193),(4409,'Dobrna','155',1193),(4410,'Dobrova-Polhov Gradec','021',1193),(4411,'Dobrovnik','156',1193),(4412,'Dol pri Ljubljani','022',1193),(4413,'Dolenjske Toplice','157',1193),(4414,'Domžale','023',1193),(4415,'Dornava','024',1193),(4416,'Dravograd','025',1193),(4417,'Duplek','026',1193),(4418,'Gorenja vas-Poljane','027',1193),(4419,'Gorišnica','028',1193),(4420,'Gornja Radgona','029',1193),(4421,'Gornji Grad','030',1193),(4422,'Gornji Petrovci','031',1193),(4423,'Grad','158',1193),(4424,'Grosuplje','032',1193),(4425,'Hajdina','159',1193),(4426,'Hoče-Slivnica','160',1193),(4427,'Hodoš','161',1193),(4428,'Horjul','162',1193),(4429,'Hrastnik','034',1193),(4430,'Hrpelje-Kozina','035',1193),(4431,'Idrija','036',1193),(4432,'Ig','037',1193),(4433,'Ilirska Bistrica','038',1193),(4434,'Ivančna Gorica','039',1193),(4435,'Izola','040',1193),(4436,'Jesenice','041',1193),(4437,'Jezersko','163',1193),(4438,'Juršinci','042',1193),(4439,'Kamnik','043',1193),(4440,'Kanal','044',1193),(4441,'Kidričevo','045',1193),(4442,'Kobarid','046',1193),(4443,'Kobilje','047',1193),(4444,'Kočevje','048',1193),(4445,'Komen','049',1193),(4446,'Komenda','164',1193),(4447,'Koper','050',1193),(4448,'Kostel','165',1193),(4449,'Kozje','051',1193),(4450,'Kranj','052',1193),(4451,'Kranjska Gora','053',1193),(4452,'Križevci','166',1193),(4453,'Krško','054',1193),(4454,'Kungota','055',1193),(4455,'Kuzma','056',1193),(4456,'Laško','057',1193),(4457,'Lenart','058',1193),(4458,'Lendava','059',1193),(4459,'Litija','060',1193),(4460,'Ljubljana','061',1193),(4461,'Ljubno','062',1193),(4462,'Ljutomer','063',1193),(4463,'Logatec','064',1193),(4464,'Loška dolina','065',1193),(4465,'Loški Potok','066',1193),(4466,'Lovrenc na Pohorju','167',1193),(4467,'Luče','067',1193),(4468,'Lukovica','068',1193),(4469,'Majšperk','069',1193),(4470,'Maribor','070',1193),(4471,'Markovci','168',1193),(4472,'Medvode','071',1193),(4473,'Mengeš','072',1193),(4474,'Metlika','073',1193),(4475,'Mežica','074',1193),(4476,'Miklavž na Dravskem polju','169',1193),(4477,'Miren-Kostanjevica','075',1193),(4478,'Mirna Peč','170',1193),(4479,'Mislinja','076',1193),(4480,'Moravče','077',1193),(4481,'Moravske Toplice','078',1193),(4482,'Mozirje','079',1193),(4483,'Murska Sobota','080',1193),(4484,'Muta','081',1193),(4485,'Naklo','082',1193),(4486,'Nazarje','083',1193),(4487,'Nova Gorica','084',1193),(4488,'Novo mesto','085',1193),(4489,'Sveta Ana','181',1193),(4490,'Sveti Andraž v Slovenskih goricah','182',1193),(4491,'Sveti Jurij','116',1193),(4492,'Šalovci','033',1193),(4493,'Šempeter-Vrtojba','183',1193),(4494,'Šenčur','117',1193),(4495,'Šentilj','118',1193),(4496,'Šentjernej','119',1193),(4497,'Šentjur','120',1193),(4498,'Škocjan','121',1193),(4499,'Škofja Loka','122',1193),(4500,'Škofljica','123',1193),(4501,'Šmarje pri Jelšah','124',1193),(4502,'Šmartno ob Paki','125',1193),(4503,'Šmartno pri Litiji','194',1193),(4504,'Šoštanj','126',1193),(4505,'Štore','127',1193),(4506,'Tabor','184',1193),(4507,'Tišina','010',1193),(4508,'Tolmin','128',1193),(4509,'Trbovlje','129',1193),(4510,'Trebnje','130',1193),(4511,'Trnovska vas','185',1193),(4512,'Tržič','131',1193),(4513,'Trzin','186',1193),(4514,'Turnišče','132',1193),(4515,'Velenje','133',1193),(4516,'Velika Polana','187',1193),(4517,'Velike Lašče','134',1193),(4518,'Veržej','188',1193),(4519,'Videm','135',1193),(4520,'Vipava','136',1193),(4521,'Vitanje','137',1193),(4522,'Vojnik','138',1193),(4523,'Vransko','189',1193),(4524,'Vrhnika','140',1193),(4525,'Vuzenica','141',1193),(4526,'Zagorje ob Savi','142',1193),(4527,'Zavrč','143',1193),(4528,'Zreče','144',1193),(4529,'Žalec','190',1193),(4530,'Železniki','146',1193),(4531,'Žetale','191',1193),(4532,'Žiri','147',1193),(4533,'Žirovnica','192',1193),(4534,'Žužemberk','193',1193),(4535,'Banskobystrický kraj','BC',1192),(4536,'Bratislavský kraj','BL',1192),(4537,'Košický kraj','KI',1192),(4538,'Nitriansky kraj','NJ',1192),(4539,'Prešovský kraj','PV',1192),(4540,'Trenčiansky kraj','TC',1192),(4541,'Trnavský kraj','TA',1192),(4542,'Žilinský kraj','ZI',1192),(4543,'Western Area (Freetown)','W',1190),(4544,'Dakar','DK',1188),(4545,'Diourbel','DB',1188),(4546,'Fatick','FK',1188),(4547,'Kaolack','KL',1188),(4548,'Kolda','KD',1188),(4549,'Louga','LG',1188),(4550,'Matam','MT',1188),(4551,'Saint-Louis','SL',1188),(4552,'Tambacounda','TC',1188),(4553,'Thies','TH',1188),(4554,'Ziguinchor','ZG',1188),(4555,'Awdal','AW',1195),(4556,'Bakool','BK',1195),(4557,'Banaadir','BN',1195),(4558,'Bay','BY',1195),(4559,'Galguduud','GA',1195),(4560,'Gedo','GE',1195),(4561,'Hiirsan','HI',1195),(4562,'Jubbada Dhexe','JD',1195),(4563,'Jubbada Hoose','JH',1195),(4564,'Mudug','MU',1195),(4565,'Nugaal','NU',1195),(4566,'Saneag','SA',1195),(4567,'Shabeellaha Dhexe','SD',1195),(4568,'Shabeellaha Hoose','SH',1195),(4569,'Sool','SO',1195),(4570,'Togdheer','TO',1195),(4571,'Woqooyi Galbeed','WO',1195),(4572,'Brokopondo','BR',1201),(4573,'Commewijne','CM',1201),(4574,'Coronie','CR',1201),(4575,'Marowijne','MA',1201),(4576,'Nickerie','NI',1201),(4577,'Paramaribo','PM',1201),(4578,'Saramacca','SA',1201),(4579,'Sipaliwini','SI',1201),(4580,'Wanica','WA',1201),(4581,'Principe','P',1207),(4582,'Sao Tome','S',1207),(4583,'Ahuachapan','AH',1066),(4584,'Cabanas','CA',1066),(4585,'Cuscatlan','CU',1066),(4586,'Chalatenango','CH',1066),(4587,'Morazan','MO',1066),(4588,'San Miguel','SM',1066),(4589,'San Salvador','SS',1066),(4590,'Santa Ana','SA',1066),(4591,'San Vicente','SV',1066),(4592,'Sonsonate','SO',1066),(4593,'Usulutan','US',1066),(4594,'Al Hasakah','HA',1206),(4595,'Al Ladhiqiyah','LA',1206),(4596,'Al Qunaytirah','QU',1206),(4597,'Ar Raqqah','RA',1206),(4598,'As Suwayda\'','SU',1206),(4599,'Dar\'a','DR',1206),(4600,'Dayr az Zawr','DY',1206),(4601,'Dimashq','DI',1206),(4602,'Halab','HL',1206),(4603,'Hamah','HM',1206),(4604,'Jim\'','HI',1206),(4605,'Idlib','ID',1206),(4606,'Rif Dimashq','RD',1206),(4607,'Tarts','TA',1206),(4608,'Hhohho','HH',1203),(4609,'Lubombo','LU',1203),(4610,'Manzini','MA',1203),(4611,'Shiselweni','SH',1203),(4612,'Batha','BA',1043),(4613,'Biltine','BI',1043),(4614,'Borkou-Ennedi-Tibesti','BET',1043),(4615,'Chari-Baguirmi','CB',1043),(4616,'Guera','GR',1043),(4617,'Kanem','KA',1043),(4618,'Lac','LC',1043),(4619,'Logone-Occidental','LO',1043),(4620,'Logone-Oriental','LR',1043),(4621,'Mayo-Kebbi','MK',1043),(4622,'Moyen-Chari','MC',1043),(4623,'Ouaddai','OD',1043),(4624,'Salamat','SA',1043),(4625,'Tandjile','TA',1043),(4626,'Kara','K',1214),(4627,'Maritime (Region)','M',1214),(4628,'Savannes','S',1214),(4629,'Krung Thep Maha Nakhon Bangkok','10',1211),(4630,'Phatthaya','S',1211),(4631,'Amnat Charoen','37',1211),(4632,'Ang Thong','15',1211),(4633,'Buri Ram','31',1211),(4634,'Chachoengsao','24',1211),(4635,'Chai Nat','18',1211),(4636,'Chaiyaphum','36',1211),(4637,'Chanthaburi','22',1211),(4638,'Chiang Mai','50',1211),(4639,'Chiang Rai','57',1211),(4640,'Chon Buri','20',1211),(4641,'Chumphon','86',1211),(4642,'Kalasin','46',1211),(4643,'Kamphasng Phet','62',1211),(4644,'Kanchanaburi','71',1211),(4645,'Khon Kaen','40',1211),(4646,'Krabi','81',1211),(4647,'Lampang','52',1211),(4648,'Lamphun','51',1211),(4649,'Loei','42',1211),(4650,'Lop Buri','16',1211),(4651,'Mae Hong Son','58',1211),(4652,'Maha Sarakham','44',1211),(4653,'Mukdahan','49',1211),(4654,'Nakhon Nayok','26',1211),(4655,'Nakhon Pathom','73',1211),(4656,'Nakhon Phanom','48',1211),(4657,'Nakhon Ratchasima','30',1211),(4658,'Nakhon Sawan','60',1211),(4659,'Nakhon Si Thammarat','80',1211),(4660,'Nan','55',1211),(4661,'Narathiwat','96',1211),(4662,'Nong Bua Lam Phu','39',1211),(4663,'Nong Khai','43',1211),(4664,'Nonthaburi','12',1211),(4665,'Pathum Thani','13',1211),(4666,'Pattani','94',1211),(4667,'Phangnga','82',1211),(4668,'Phatthalung','93',1211),(4669,'Phayao','56',1211),(4670,'Phetchabun','67',1211),(4671,'Phetchaburi','76',1211),(4672,'Phichit','66',1211),(4673,'Phitsanulok','65',1211),(4674,'Phrae','54',1211),(4675,'Phra Nakhon Si Ayutthaya','14',1211),(4676,'Phuket','83',1211),(4677,'Prachin Buri','25',1211),(4678,'Prachuap Khiri Khan','77',1211),(4679,'Ranong','85',1211),(4680,'Ratchaburi','70',1211),(4681,'Rayong','21',1211),(4682,'Roi Et','45',1211),(4683,'Sa Kaeo','27',1211),(4684,'Sakon Nakhon','47',1211),(4685,'Samut Prakan','11',1211),(4686,'Samut Sakhon','74',1211),(4687,'Samut Songkhram','75',1211),(4688,'Saraburi','19',1211),(4689,'Satun','91',1211),(4690,'Sing Buri','17',1211),(4691,'Si Sa Ket','33',1211),(4692,'Songkhla','90',1211),(4693,'Sukhothai','64',1211),(4694,'Suphan Buri','72',1211),(4695,'Surat Thani','84',1211),(4696,'Surin','32',1211),(4697,'Tak','63',1211),(4698,'Trang','92',1211),(4699,'Trat','23',1211),(4700,'Ubon Ratchathani','34',1211),(4701,'Udon Thani','41',1211),(4702,'Uthai Thani','61',1211),(4703,'Uttaradit','53',1211),(4704,'Yala','95',1211),(4705,'Yasothon','35',1211),(4706,'Sughd','SU',1209),(4707,'Khatlon','KT',1209),(4708,'Gorno-Badakhshan','GB',1209),(4709,'Ahal','A',1220),(4710,'Balkan','B',1220),(4711,'Dasoguz','D',1220),(4712,'Lebap','L',1220),(4713,'Mary','M',1220),(4714,'Béja','31',1218),(4715,'Ben Arous','13',1218),(4716,'Bizerte','23',1218),(4717,'Gabès','81',1218),(4718,'Gafsa','71',1218),(4719,'Jendouba','32',1218),(4720,'Kairouan','41',1218),(4721,'Rasserine','42',1218),(4722,'Kebili','73',1218),(4723,'L\'Ariana','12',1218),(4724,'Le Ref','33',1218),(4725,'Mahdia','53',1218),(4726,'La Manouba','14',1218),(4727,'Medenine','82',1218),(4728,'Moneatir','52',1218),(4729,'Naboul','21',1218),(4730,'Sfax','61',1218),(4731,'Sidi Bouxid','43',1218),(4732,'Siliana','34',1218),(4733,'Sousse','51',1218),(4734,'Tataouine','83',1218),(4735,'Tozeur','72',1218),(4736,'Tunis','11',1218),(4737,'Zaghouan','22',1218),(4738,'Adana','01',1219),(4739,'Ad yaman','02',1219),(4740,'Afyon','03',1219),(4741,'Ag r','04',1219),(4742,'Aksaray','68',1219),(4743,'Amasya','05',1219),(4744,'Ankara','06',1219),(4745,'Antalya','07',1219),(4746,'Ardahan','75',1219),(4747,'Artvin','08',1219),(4748,'Aydin','09',1219),(4749,'Bal kesir','10',1219),(4750,'Bartin','74',1219),(4751,'Batman','72',1219),(4752,'Bayburt','69',1219),(4753,'Bilecik','11',1219),(4754,'Bingol','12',1219),(4755,'Bitlis','13',1219),(4756,'Bolu','14',1219),(4757,'Burdur','15',1219),(4758,'Bursa','16',1219),(4759,'Canakkale','17',1219),(4760,'Cankir','18',1219),(4761,'Corum','19',1219),(4762,'Denizli','20',1219),(4763,'Diyarbakir','21',1219),(4764,'Duzce','81',1219),(4765,'Edirne','22',1219),(4766,'Elazig','23',1219),(4767,'Erzincan','24',1219),(4768,'Erzurum','25',1219),(4769,'Eskis\'ehir','26',1219),(4770,'Gaziantep','27',1219),(4771,'Giresun','28',1219),(4772,'Gms\'hane','29',1219),(4773,'Hakkari','30',1219),(4774,'Hatay','31',1219),(4775,'Igidir','76',1219),(4776,'Isparta','32',1219),(4777,'Icel','33',1219),(4778,'Istanbul','34',1219),(4779,'Izmir','35',1219),(4780,'Kahramanmaras','46',1219),(4781,'Karabk','78',1219),(4782,'Karaman','70',1219),(4783,'Kars','36',1219),(4784,'Kastamonu','37',1219),(4785,'Kayseri','38',1219),(4786,'Kirikkale','71',1219),(4787,'Kirklareli','39',1219),(4788,'Kirs\'ehir','40',1219),(4789,'Kilis','79',1219),(4790,'Kocaeli','41',1219),(4791,'Konya','42',1219),(4792,'Ktahya','43',1219),(4793,'Malatya','44',1219),(4794,'Manisa','45',1219),(4795,'Mardin','47',1219),(4796,'Mugila','48',1219),(4797,'Mus','49',1219),(4798,'Nevs\'ehir','50',1219),(4799,'Nigide','51',1219),(4800,'Ordu','52',1219),(4801,'Osmaniye','80',1219),(4802,'Rize','53',1219),(4803,'Sakarya','54',1219),(4804,'Samsun','55',1219),(4805,'Siirt','56',1219),(4806,'Sinop','57',1219),(4807,'Sivas','58',1219),(4808,'S\'anliurfa','63',1219),(4809,'S\'rnak','73',1219),(4810,'Tekirdag','59',1219),(4811,'Tokat','60',1219),(4812,'Trabzon','61',1219),(4813,'Tunceli','62',1219),(4814,'Us\'ak','64',1219),(4815,'Van','65',1219),(4816,'Yalova','77',1219),(4817,'Yozgat','66',1219),(4818,'Zonguldak','67',1219),(4819,'Couva-Tabaquite-Talparo','CTT',1217),(4820,'Diego Martin','DMN',1217),(4821,'Eastern Tobago','ETO',1217),(4822,'Penal-Debe','PED',1217),(4823,'Princes Town','PRT',1217),(4824,'Rio Claro-Mayaro','RCM',1217),(4825,'Sangre Grande','SGE',1217),(4826,'San Juan-Laventille','SJL',1217),(4827,'Siparia','SIP',1217),(4828,'Tunapuna-Piarco','TUP',1217),(4829,'Western Tobago','WTO',1217),(4830,'Arima','ARI',1217),(4831,'Chaguanas','CHA',1217),(4832,'Point Fortin','PTF',1217),(4833,'Port of Spain','POS',1217),(4834,'San Fernando','SFO',1217),(4835,'Aileu','AL',1063),(4836,'Ainaro','AN',1063),(4837,'Bacucau','BA',1063),(4838,'Bobonaro','BO',1063),(4839,'Cova Lima','CO',1063),(4840,'Dili','DI',1063),(4841,'Ermera','ER',1063),(4842,'Laulem','LA',1063),(4843,'Liquica','LI',1063),(4844,'Manatuto','MT',1063),(4845,'Manafahi','MF',1063),(4846,'Oecussi','OE',1063),(4847,'Viqueque','VI',1063),(4848,'Changhua County','CHA',1208),(4849,'Chiayi County','CYQ',1208),(4850,'Hsinchu County','HSQ',1208),(4851,'Hualien County','HUA',1208),(4852,'Ilan County','ILA',1208),(4853,'Kaohsiung County','KHQ',1208),(4854,'Miaoli County','MIA',1208),(4855,'Nantou County','NAN',1208),(4856,'Penghu County','PEN',1208),(4857,'Pingtung County','PIF',1208),(4858,'Taichung County','TXQ',1208),(4859,'Tainan County','TNQ',1208),(4860,'Taipei County','TPQ',1208),(4861,'Taitung County','TTT',1208),(4862,'Taoyuan County','TAO',1208),(4863,'Yunlin County','YUN',1208),(4864,'Keelung City','KEE',1208),(4865,'Arusha','01',1210),(4866,'Dar-es-Salaam','02',1210),(4867,'Dodoma','03',1210),(4868,'Iringa','04',1210),(4869,'Kagera','05',1210),(4870,'Kaskazini Pemba','06',1210),(4871,'Kaskazini Unguja','07',1210),(4872,'Xigoma','08',1210),(4873,'Kilimanjaro','09',1210),(4874,'Rusini Pemba','10',1210),(4875,'Kusini Unguja','11',1210),(4876,'Lindi','12',1210),(4877,'Manyara','26',1210),(4878,'Mara','13',1210),(4879,'Mbeya','14',1210),(4880,'Mjini Magharibi','15',1210),(4881,'Morogoro','16',1210),(4882,'Mtwara','17',1210),(4883,'Pwani','19',1210),(4884,'Rukwa','20',1210),(4885,'Ruvuma','21',1210),(4886,'Shinyanga','22',1210),(4887,'Singida','23',1210),(4888,'Tabora','24',1210),(4889,'Tanga','25',1210),(4890,'Cherkas\'ka Oblast\'','71',1224),(4891,'Chernihivs\'ka Oblast\'','74',1224),(4892,'Chernivets\'ka Oblast\'','77',1224),(4893,'Dnipropetrovs\'ka Oblast\'','12',1224),(4894,'Donets\'ka Oblast\'','14',1224),(4895,'Ivano-Frankivs\'ka Oblast\'','26',1224),(4896,'Kharkivs\'ka Oblast\'','63',1224),(4897,'Khersons\'ka Oblast\'','65',1224),(4898,'Khmel\'nyts\'ka Oblast\'','68',1224),(4899,'Kirovohrads\'ka Oblast\'','35',1224),(4900,'Kyivs\'ka Oblast\'','32',1224),(4901,'Luhans\'ka Oblast\'','09',1224),(4902,'L\'vivs\'ka Oblast\'','46',1224),(4903,'Mykolaivs\'ka Oblast\'','48',1224),(4904,'Odes \'ka Oblast\'','51',1224),(4905,'Poltavs\'ka Oblast\'','53',1224),(4906,'Rivnens\'ka Oblast\'','56',1224),(4907,'Sums \'ka Oblast\'','59',1224),(4908,'Ternopil\'s\'ka Oblast\'','61',1224),(4909,'Vinnyts\'ka Oblast\'','05',1224),(4910,'Volyos\'ka Oblast\'','07',1224),(4911,'Zakarpats\'ka Oblast\'','21',1224),(4912,'Zaporiz\'ka Oblast\'','23',1224),(4913,'Zhytomyrs\'ka Oblast\'','18',1224),(4914,'Respublika Krym','43',1224),(4915,'Kyiv','30',1224),(4916,'Sevastopol','40',1224),(4917,'Adjumani','301',1223),(4918,'Apac','302',1223),(4919,'Arua','303',1223),(4920,'Bugiri','201',1223),(4921,'Bundibugyo','401',1223),(4922,'Bushenyi','402',1223),(4923,'Busia','202',1223),(4924,'Gulu','304',1223),(4925,'Hoima','403',1223),(4926,'Iganga','203',1223),(4927,'Jinja','204',1223),(4928,'Kabale','404',1223),(4929,'Kabarole','405',1223),(4930,'Kaberamaido','213',1223),(4931,'Kalangala','101',1223),(4932,'Kampala','102',1223),(4933,'Kamuli','205',1223),(4934,'Kamwenge','413',1223),(4935,'Kanungu','414',1223),(4936,'Kapchorwa','206',1223),(4937,'Kasese','406',1223),(4938,'Katakwi','207',1223),(4939,'Kayunga','112',1223),(4940,'Kibaale','407',1223),(4941,'Kiboga','103',1223),(4942,'Kisoro','408',1223),(4943,'Kitgum','305',1223),(4944,'Kotido','306',1223),(4945,'Kumi','208',1223),(4946,'Kyenjojo','415',1223),(4947,'Lira','307',1223),(4948,'Luwero','104',1223),(4949,'Masaka','105',1223),(4950,'Masindi','409',1223),(4951,'Mayuge','214',1223),(4952,'Mbale','209',1223),(4953,'Mbarara','410',1223),(4954,'Moroto','308',1223),(4955,'Moyo','309',1223),(4956,'Mpigi','106',1223),(4957,'Mubende','107',1223),(4958,'Mukono','108',1223),(4959,'Nakapiripirit','311',1223),(4960,'Nakasongola','109',1223),(4961,'Nebbi','310',1223),(4962,'Ntungamo','411',1223),(4963,'Pader','312',1223),(4964,'Pallisa','210',1223),(4965,'Rakai','110',1223),(4966,'Rukungiri','412',1223),(4967,'Sembabule','111',1223),(4968,'Sironko','215',1223),(4969,'Soroti','211',1223),(4970,'Tororo','212',1223),(4971,'Wakiso','113',1223),(4972,'Yumbe','313',1223),(4973,'Baker Island','81',1227),(4974,'Howland Island','84',1227),(4975,'Jarvis Island','86',1227),(4976,'Johnston Atoll','67',1227),(4977,'Kingman Reef','89',1227),(4978,'Midway Islands','71',1227),(4979,'Navassa Island','76',1227),(4980,'Palmyra Atoll','95',1227),(4981,'Wake Island','79',1227),(4982,'Artigsa','AR',1229),(4983,'Canelones','CA',1229),(4984,'Cerro Largo','CL',1229),(4985,'Colonia','CO',1229),(4986,'Durazno','DU',1229),(4987,'Flores','FS',1229),(4988,'Lavalleja','LA',1229),(4989,'Maldonado','MA',1229),(4990,'Montevideo','MO',1229),(4991,'Paysandu','PA',1229),(4992,'Rivera','RV',1229),(4993,'Rocha','RO',1229),(4994,'Salto','SA',1229),(4995,'Soriano','SO',1229),(4996,'Tacuarembo','TA',1229),(4997,'Treinta y Tres','TT',1229),(4998,'Toshkent (city)','TK',1230),(4999,'Qoraqalpogiston Respublikasi','QR',1230),(5000,'Andijon','AN',1230),(5001,'Buxoro','BU',1230),(5002,'Farg\'ona','FA',1230),(5003,'Jizzax','JI',1230),(5004,'Khorazm','KH',1230),(5005,'Namangan','NG',1230),(5006,'Navoiy','NW',1230),(5007,'Qashqadaryo','QA',1230),(5008,'Samarqand','SA',1230),(5009,'Sirdaryo','SI',1230),(5010,'Surxondaryo','SU',1230),(5011,'Toshkent','TO',1230),(5012,'Xorazm','XO',1230),(5013,'Distrito Federal','A',1232),(5014,'Anzoategui','B',1232),(5015,'Apure','C',1232),(5016,'Aragua','D',1232),(5017,'Barinas','E',1232),(5018,'Carabobo','G',1232),(5019,'Cojedes','H',1232),(5020,'Falcon','I',1232),(5021,'Guarico','J',1232),(5022,'Lara','K',1232),(5023,'Merida','L',1232),(5024,'Miranda','M',1232),(5025,'Monagas','N',1232),(5026,'Nueva Esparta','O',1232),(5027,'Portuguesa','P',1232),(5028,'Tachira','S',1232),(5029,'Trujillo','T',1232),(5030,'Vargas','X',1232),(5031,'Yaracuy','U',1232),(5032,'Zulia','V',1232),(5033,'Delta Amacuro','Y',1232),(5034,'Dependencias Federales','W',1232),(5035,'An Giang','44',1233),(5036,'Ba Ria - Vung Tau','43',1233),(5037,'Bac Can','53',1233),(5038,'Bac Giang','54',1233),(5039,'Bac Lieu','55',1233),(5040,'Bac Ninh','56',1233),(5041,'Ben Tre','50',1233),(5042,'Binh Dinh','31',1233),(5043,'Binh Duong','57',1233),(5044,'Binh Phuoc','58',1233),(5045,'Binh Thuan','40',1233),(5046,'Ca Mau','59',1233),(5047,'Can Tho','48',1233),(5048,'Cao Bang','04',1233),(5049,'Da Nang, thanh pho','60',1233),(5050,'Dong Nai','39',1233),(5051,'Dong Thap','45',1233),(5052,'Gia Lai','30',1233),(5053,'Ha Giang','03',1233),(5054,'Ha Nam','63',1233),(5055,'Ha Noi, thu do','64',1233),(5056,'Ha Tay','15',1233),(5057,'Ha Tinh','23',1233),(5058,'Hai Duong','61',1233),(5059,'Hai Phong, thanh pho','62',1233),(5060,'Hoa Binh','14',1233),(5061,'Ho Chi Minh, thanh pho [Sai Gon]','65',1233),(5062,'Hung Yen','66',1233),(5063,'Khanh Hoa','34',1233),(5064,'Kien Giang','47',1233),(5065,'Kon Tum','28',1233),(5066,'Lai Chau','01',1233),(5067,'Lam Dong','35',1233),(5068,'Lang Son','09',1233),(5069,'Lao Cai','02',1233),(5070,'Long An','41',1233),(5071,'Nam Dinh','67',1233),(5072,'Nghe An','22',1233),(5073,'Ninh Binh','18',1233),(5074,'Ninh Thuan','36',1233),(5075,'Phu Tho','68',1233),(5076,'Phu Yen','32',1233),(5077,'Quang Binh','24',1233),(5078,'Quang Nam','27',1233),(5079,'Quang Ngai','29',1233),(5080,'Quang Ninh','13',1233),(5081,'Quang Tri','25',1233),(5082,'Soc Trang','52',1233),(5083,'Son La','05',1233),(5084,'Tay Ninh','37',1233),(5085,'Thai Binh','20',1233),(5086,'Thai Nguyen','69',1233),(5087,'Thanh Hoa','21',1233),(5088,'Thua Thien-Hue','26',1233),(5089,'Tien Giang','46',1233),(5090,'Tra Vinh','51',1233),(5091,'Tuyen Quang','07',1233),(5092,'Vinh Long','49',1233),(5093,'Vinh Phuc','70',1233),(5094,'Yen Bai','06',1233),(5095,'Malampa','MAP',1231),(5096,'Penama','PAM',1231),(5097,'Sanma','SAM',1231),(5098,'Shefa','SEE',1231),(5099,'Tafea','TAE',1231),(5100,'Torba','TOB',1231),(5101,'A\'ana','AA',1185),(5102,'Aiga-i-le-Tai','AL',1185),(5103,'Atua','AT',1185),(5104,'Fa\'aaaleleaga','FA',1185),(5105,'Gaga\'emauga','GE',1185),(5106,'Gagaifomauga','GI',1185),(5107,'Palauli','PA',1185),(5108,'Satupa\'itea','SA',1185),(5109,'Tuamasaga','TU',1185),(5110,'Va\'a-o-Fonoti','VF',1185),(5111,'Vaisigano','VS',1185),(5112,'Crna Gora','CG',1243),(5113,'Srbija','SR',1242),(5114,'Kosovo-Metohija','KM',1242),(5115,'Vojvodina','VO',1242),(5116,'Abyan','AB',1237),(5117,'Adan','AD',1237),(5118,'Ad Dali','DA',1237),(5119,'Al Bayda\'','BA',1237),(5120,'Al Hudaydah','MU',1237),(5121,'Al Mahrah','MR',1237),(5122,'Al Mahwit','MW',1237),(5123,'Amran','AM',1237),(5124,'Dhamar','DH',1237),(5125,'Hadramawt','HD',1237),(5126,'Hajjah','HJ',1237),(5127,'Ibb','IB',1237),(5128,'Lahij','LA',1237),(5129,'Ma\'rib','MA',1237),(5130,'Sa\'dah','SD',1237),(5131,'San\'a\'','SN',1237),(5132,'Shabwah','SH',1237),(5133,'Ta\'izz','TA',1237),(5134,'Eastern Cape','EC',1196),(5135,'Free State','FS',1196),(5136,'Gauteng','GT',1196),(5137,'Kwazulu-Natal','NL',1196),(5138,'Mpumalanga','MP',1196),(5139,'Northern Cape','NC',1196),(5140,'Limpopo','NP',1196),(5141,'Western Cape','WC',1196),(5142,'Copperbelt','08',1239),(5143,'Luapula','04',1239),(5144,'Lusaka','09',1239),(5145,'North-Western','06',1239),(5146,'Bulawayo','BU',1240),(5147,'Harare','HA',1240),(5148,'Manicaland','MA',1240),(5149,'Mashonaland Central','MC',1240),(5150,'Mashonaland East','ME',1240),(5151,'Mashonaland West','MW',1240),(5152,'Masvingo','MV',1240),(5153,'Matabeleland North','MN',1240),(5154,'Matabeleland South','MS',1240),(5155,'Midlands','MI',1240),(5156,'South Karelia','SK',1075),(5157,'South Ostrobothnia','SO',1075),(5158,'Etelä-Savo','ES',1075),(5159,'Häme','HH',1075),(5160,'Itä-Uusimaa','IU',1075),(5161,'Kainuu','KA',1075),(5162,'Central Ostrobothnia','CO',1075),(5163,'Central Finland','CF',1075),(5164,'Kymenlaakso','KY',1075),(5165,'Lapland','LA',1075),(5166,'Tampere Region','TR',1075),(5167,'Ostrobothnia','OB',1075),(5168,'North Karelia','NK',1075),(5169,'Northern Ostrobothnia','NO',1075),(5170,'Northern Savo','NS',1075),(5171,'Päijät-Häme','PH',1075),(5172,'Satakunta','SK',1075),(5173,'Uusimaa','UM',1075),(5174,'South-West Finland','SW',1075),(5175,'Åland','AL',1075),(5176,'Limburg','LI',1152),(5177,'Central and Western','CW',1098),(5178,'Eastern','EA',1098),(5179,'Southern','SO',1098),(5180,'Wan Chai','WC',1098),(5181,'Kowloon City','KC',1098),(5182,'Kwun Tong','KU',1098),(5183,'Sham Shui Po','SS',1098),(5184,'Wong Tai Sin','WT',1098),(5185,'Yau Tsim Mong','YT',1098),(5186,'Islands','IS',1098),(5187,'Kwai Tsing','KI',1098),(5188,'North','NO',1098),(5189,'Sai Kung','SK',1098),(5190,'Sha Tin','ST',1098),(5191,'Tai Po','TP',1098),(5192,'Tsuen Wan','TW',1098),(5193,'Tuen Mun','TM',1098),(5194,'Yuen Long','YL',1098),(5195,'Manchester','MR',1108),(5196,'Al Manāmah (Al ‘Āşimah)','13',1016),(5197,'Al Janūbīyah','14',1016),(5199,'Al Wusţá','16',1016),(5200,'Ash Shamālīyah','17',1016),(5201,'Jenin','_A',1165),(5202,'Tubas','_B',1165),(5203,'Tulkarm','_C',1165),(5204,'Nablus','_D',1165),(5205,'Qalqilya','_E',1165),(5206,'Salfit','_F',1165),(5207,'Ramallah and Al-Bireh','_G',1165),(5208,'Jericho','_H',1165),(5209,'Jerusalem','_I',1165),(5210,'Bethlehem','_J',1165),(5211,'Hebron','_K',1165),(5212,'North Gaza','_L',1165),(5213,'Gaza','_M',1165),(5214,'Deir el-Balah','_N',1165),(5215,'Khan Yunis','_O',1165),(5216,'Rafah','_P',1165),(5217,'Brussels','BRU',1020),(5218,'Distrito Federal','DIF',1140),(5219,'Taichung City','TXG',1208),(5220,'Kaohsiung City','KHH',1208),(5221,'Taipei City','TPE',1208),(5222,'Chiayi City','CYI',1208),(5223,'Hsinchu City','HSZ',1208),(5224,'Tainan City','TNN',1208),(9000,'North West','NW',1196),(9986,'Tyne and Wear','TWR',1226),(9988,'Greater Manchester','GTM',1226),(9989,'Co Tyrone','TYR',1226),(9990,'West Yorkshire','WYK',1226),(9991,'South Yorkshire','SYK',1226),(9992,'Merseyside','MSY',1226),(9993,'Berkshire','BRK',1226),(9994,'West Midlands','WMD',1226),(9998,'West Glamorgan','WGM',1226),(9999,'London','LON',1226),(10000,'Carbonia-Iglesias','CI',1107),(10001,'Olbia-Tempio','OT',1107),(10002,'Medio Campidano','VS',1107),(10003,'Ogliastra','OG',1107),(10009,'Jura','39',1076),(10010,'Barletta-Andria-Trani','Bar',1107),(10011,'Fermo','Fer',1107),(10012,'Monza e Brianza','Mon',1107),(10013,'Clwyd','CWD',1226),(10015,'South Glamorgan','SGM',1226),(10016,'Artibonite','AR',1094),(10017,'Centre','CE',1094),(10018,'Nippes','NI',1094),(10019,'Nord','ND',1094),(10020,'La Rioja','F',1010),(10021,'Andorra la Vella','07',1005),(10022,'Canillo','02',1005),(10023,'Encamp','03',1005),(10024,'Escaldes-Engordany','08',1005),(10025,'La Massana','04',1005),(10026,'Ordino','05',1005),(10027,'Sant Julia de Loria','06',1005),(10028,'Abaco Islands','AB',1212),(10029,'Andros Island','AN',1212),(10030,'Berry Islands','BR',1212),(10031,'Eleuthera','EL',1212),(10032,'Grand Bahama','GB',1212),(10033,'Rum Cay','RC',1212),(10034,'San Salvador Island','SS',1212),(10035,'Kongo central','01',1050),(10036,'Kwango','02',1050),(10037,'Kwilu','03',1050),(10038,'Mai-Ndombe','04',1050),(10039,'Kasai','05',1050),(10040,'Lulua','06',1050),(10041,'Lomami','07',1050),(10042,'Sankuru','08',1050),(10043,'Ituri','09',1050),(10044,'Haut-Uele','10',1050),(10045,'Tshopo','11',1050),(10046,'Bas-Uele','12',1050),(10047,'Nord-Ubangi','13',1050),(10048,'Mongala','14',1050),(10049,'Sud-Ubangi','15',1050),(10050,'Tshuapa','16',1050),(10051,'Haut-Lomami','17',1050),(10052,'Lualaba','18',1050),(10053,'Haut-Katanga','19',1050),(10054,'Tanganyika','20',1050),(10055,'Toledo','TO',1198),(10056,'Córdoba','CO',1198),(10057,'Metropolitan Manila','MNL',1170),(10058,'La Paz','LP',1097),(10059,'Yinchuan','YN',1045),(10060,'Shizuishan','SZ',1045),(10061,'Wuzhong','WZ',1045),(10062,'Guyuan','GY',1045),(10063,'Zhongwei','ZW',1045),(10064,'Luxembourg','L',1126),(10065,'Aizkraukles novads','002',1119),(10066,'Jaunjelgavas novads','038',1119),(10067,'Pļaviņu novads','072',1119),(10068,'Kokneses novads','046',1119),(10069,'Neretas novads','065',1119),(10070,'Skrīveru novads','092',1119),(10071,'Alūksnes novads','007',1119),(10072,'Apes novads','009',1119),(10073,'Balvu novads','015',1119),(10074,'Viļakas novads','108',1119),(10075,'Baltinavas novads','014',1119),(10076,'Rugāju novads','082',1119),(10077,'Bauskas novads','016',1119),(10078,'Iecavas novads','034',1119),(10079,'Rundāles novads','083',1119),(10080,'Vecumnieku novads','105',1119),(10081,'Cēsu novads','022',1119),(10082,'Līgatnes novads','055',1119),(10083,'Amatas novads','008',1119),(10084,'Jaunpiebalgas novads','039',1119),(10085,'Priekuļu novads','075',1119),(10086,'Pārgaujas novads','070',1119),(10087,'Raunas novads','076',1119),(10088,'Vecpiebalgas novads','104',1119),(10089,'Daugavpils novads','025',1119),(10090,'Ilūkstes novads','036',1119),(10091,'Dobeles novads','026',1119),(10092,'Auces novads','010',1119),(10093,'Tērvetes novads','098',1119),(10094,'Gulbenes novads','033',1119),(10095,'Jelgavas novads','041',1119),(10096,'Ozolnieku novads','069',1119),(10097,'Jēkabpils novads','042',1119),(10098,'Aknīstes novads','004',1119),(10099,'Viesītes novads','107',1119),(10100,'Krustpils novads','049',1119),(10101,'Salas novads','085',1119),(10102,'Krāslavas novads','047',1119),(10103,'Dagdas novads','024',1119),(10104,'Aglonas novads','001',1119),(10105,'Kuldīgas novads','050',1119),(10106,'Skrundas novads','093',1119),(10107,'Alsungas novads','006',1119),(10108,'Aizputes novads','003',1119),(10109,'Durbes novads','028',1119),(10110,'Grobiņas novads','032',1119),(10111,'Pāvilostas novads','071',1119),(10112,'Priekules novads','074',1119),(10113,'Nīcas novads','066',1119),(10114,'Rucavas novads','081',1119),(10115,'Vaiņodes novads','100',1119),(10116,'Limbažu novads','054',1119),(10117,'Alojas novads','005',1119),(10118,'Salacgrīvas novads','086',1119),(10119,'Ludzas novads','058',1119),(10120,'Kārsavas novads','044',1119),(10121,'Zilupes novads','110',1119),(10122,'Ciblas novads','023',1119),(10123,'Madonas novads','059',1119),(10124,'Cesvaines novads','021',1119),(10125,'Lubānas novads','057',1119),(10126,'Varakļānu novads','102',1119),(10127,'Ērgļu novads','030',1119),(10128,'Ogres novads','067',1119),(10129,'Ikšķiles novads','035',1119),(10130,'Ķeguma novads','051',1119),(10131,'Lielvārdes novads','053',1119),(10132,'Preiļu novads','073',1119),(10133,'Līvānu novads','056',1119),(10134,'Riebiņu novads','078',1119),(10135,'Vārkavas novads','103',1119),(10136,'Rēzeknes novads','077',1119),(10137,'Viļānu novads','109',1119),(10138,'Baldones novads','013',1119),(10139,'Ķekavas novads','052',1119),(10140,'Olaines novads','068',1119),(10141,'Salaspils novads','087',1119),(10142,'Saulkrastu novads','089',1119),(10143,'Siguldas novads','091',1119),(10144,'Inčukalna novads','037',1119),(10145,'Ādažu novads','011',1119),(10146,'Babītes novads','012',1119),(10147,'Carnikavas novads','020',1119),(10148,'Garkalnes novads','031',1119),(10149,'Krimuldas novads','048',1119),(10150,'Mālpils novads','061',1119),(10151,'Mārupes novads','062',1119),(10152,'Ropažu novads','080',1119),(10153,'Sējas novads','090',1119),(10154,'Stopiņu novads','095',1119),(10155,'Saldus novads','088',1119),(10156,'Brocēnu novads','018',1119),(10157,'Talsu novads','097',1119),(10158,'Dundagas novads','027',1119),(10159,'Mērsraga novads','063',1119),(10160,'Rojas novads','079',1119),(10161,'Tukuma novads','099',1119),(10162,'Kandavas novads','043',1119),(10163,'Engures novads','029',1119),(10164,'Jaunpils novads','040',1119),(10165,'Valkas novads','101',1119),(10166,'Smiltenes novads','094',1119),(10167,'Strenču novads','096',1119),(10168,'Kocēnu novads','045',1119),(10169,'Mazsalacas novads','060',1119),(10170,'Rūjienas novads','084',1119),(10171,'Beverīnas novads','017',1119),(10172,'Burtnieku novads','019',1119),(10173,'Naukšēnu novads','064',1119),(10174,'Ventspils novads','106',1119),(10175,'Jēkabpils','JKB',1119),(10176,'Valmiera','VMR',1119),(10177,'Florida','FL',1229),(10178,'Rio Negro','RN',1229),(10179,'San Jose','SJ',1229),(10180,'Plateau','PL',1157),(10181,'Pieria','61',1085),(10182,'Los Rios','LR',1044),(10183,'Arica y Parinacota','AP',1044),(10184,'Amazonas','AMA',1169),(10185,'Kalimantan Tengah','KT',1102),(10186,'Sulawesi Barat','SR',1102),(10187,'Kalimantan Utara','KU',1102),(10188,'Ankaran','86',1193),(10189,'Apače','87',1193),(10190,'Cirkulane','88',1193),(10191,'Gorje','89',1193),(10192,'Kostanjevica na Krki','90',1193),(10193,'Log-Dragomer','91',1193),(10194,'Makole','92',1193),(10195,'Mirna','93',1193),(10196,'Mokronog-Trebelno','94',1193),(10197,'Odranci','95',1193),(10198,'Oplotnica','96',1193),(10199,'Ormož','97',1193),(10200,'Osilnica','98',1193),(10201,'Pesnica','99',1193),(10202,'Piran','100',1193),(10203,'Pivka','101',1193),(10204,'Podčetrtek','102',1193),(10205,'Podlehnik','103',1193),(10206,'Podvelka','104',1193),(10207,'Poljčane','105',1193),(10208,'Polzela','106',1193),(10209,'Postojna','107',1193),(10210,'Prebold','108',1193),(10211,'Preddvor','109',1193),(10212,'Prevalje','110',1193),(10213,'Ptuj','111',1193),(10214,'Puconci','112',1193),(10215,'Rače-Fram','113',1193),(10216,'Radeče','114',1193),(10217,'Radenci','115',1193),(10218,'Radlje ob Dravi','139',1193),(10219,'Radovljica','145',1193),(10220,'Ravne na Koroškem','171',1193),(10221,'Razkrižje','172',1193),(10222,'Rečica ob Savinji','173',1193),(10223,'Renče-Vogrsko','174',1193),(10224,'Ribnica','175',1193),(10225,'Ribnica na Pohorju','176',1193),(10226,'Rogaška Slatina','177',1193),(10227,'Rogašovci','178',1193),(10228,'Rogatec','179',1193),(10229,'Ruše','180',1193),(10230,'Selnica ob Dravi','195',1193),(10231,'Semič','196',1193),(10232,'Šentrupert','197',1193),(10233,'Sevnica','198',1193),(10234,'Sežana','199',1193),(10235,'Slovenj Gradec','200',1193),(10236,'Slovenska Bistrica','201',1193),(10237,'Slovenske Konjice','202',1193),(10238,'Šmarješke Toplice','203',1193),(10239,'Sodražica','204',1193),(10240,'Solčava','205',1193),(10241,'Središče ob Dravi','206',1193),(10242,'Starše','207',1193),(10243,'Straža','208',1193),(10244,'Sveta Trojica v Slovenskih goricah','209',1193),(10245,'Sveti Jurij v Slovenskih goricah','210',1193),(10246,'Sveti Tomaž','211',1193),(10247,'Vodice','212',1193),(10248,'Abkhazia','AB',1081),(10249,'Adjara','AJ',1081),(10250,'Tbilisi','TB',1081),(10251,'Guria','GU',1081),(10252,'Imereti','IM',1081),(10253,'Kakheti','KA',1081),(10254,'Kvemo Kartli','KK',1081),(10255,'Mtskheta-Mtianeti','MM',1081),(10256,'Racha-Lechkhumi and Kvemo Svaneti','RL',1081),(10257,'Samegrelo-Zemo Svaneti','SZ',1081),(10258,'Samtskhe-Javakheti','SJ',1081),(10259,'Shida Kartli','SK',1081),(10260,'Central','C',1074),(10261,'Punjab','PB',1163),(10262,'La Libertad','LI',1066),(10263,'La Paz','PA',1066),(10264,'La Union','UN',1066),(10265,'Littoral','LT',1038),(10266,'Nord-Ouest','NW',1038),(10267,'Telangana','TG',1101),(10268,'Ash Sharqiyah','04',1187),(10269,'Guadeloupe','GP',1076),(10270,'Martinique','MQ',1076),(10271,'Guyane','GF',1076),(10272,'La Réunion','RE',1076),(10273,'Mayotte','YT',1076),(10274,'Baringo','01',1112),(10275,'Bomet','02',1112),(10276,'Bungoma','03',1112),(10277,'Busia','04',1112),(10278,'Elgeyo/Marakwet','05',1112),(10279,'Embu','06',1112),(10280,'Garissa','07',1112),(10281,'Homa Bay','08',1112),(10282,'Isiolo','09',1112),(10283,'Kajiado','10',1112),(10284,'Kakamega','11',1112),(10285,'Kericho','12',1112),(10286,'Kiambu','13',1112),(10287,'Kilifi','14',1112),(10288,'Kirinyaga','15',1112),(10289,'Kisii','16',1112),(10290,'Kisumu','17',1112),(10291,'Kitui','18',1112),(10292,'Kwale','19',1112),(10293,'Laikipia','20',1112),(10294,'Lamu','21',1112),(10295,'Machakos','22',1112),(10296,'Makueni','23',1112),(10297,'Mandera','24',1112),(10298,'Marsabit','25',1112),(10299,'Meru','26',1112),(10300,'Migori','27',1112),(10301,'Mombasa','28',1112),(10302,'Murang\'a','29',1112),(10303,'Nairobi City','30',1112),(10304,'Nakuru','31',1112),(10305,'Nandi','32',1112),(10306,'Narok','33',1112),(10307,'Nyamira','34',1112),(10308,'Nyandarua','35',1112),(10309,'Nyeri','36',1112),(10310,'Samburu','37',1112),(10311,'Siaya','38',1112),(10312,'Taita/Taveta','39',1112),(10313,'Tana River','40',1112),(10314,'Tharaka-Nithi','41',1112),(10315,'Trans Nzoia','42',1112),(10316,'Turkana','43',1112),(10317,'Uasin Gishu','44',1112),(10318,'Vihiga','45',1112),(10319,'Wajir','46',1112),(10320,'West Pokot','47',1112),(10321,'Chandigarh','CH',1101),(10322,'Central','CP',1083),(10323,'Eastern','EP',1083),(10324,'Northern','NP',1083),(10325,'Western','WP',1083),(10326,'Saint Kitts','K',1181),(10327,'Nevis','N',1181),(10328,'Eastern','E',1190),(10329,'Northern','N',1190),(10330,'Southern','S',1190),(10331,'Dushanbe','DU',1209),(10332,'Nohiyahoi Tobei Jumhurí','RA',1209),(10333,'Wallis-et-Futuna','WF',1076),(10334,'Nouvelle-Calédonie','NC',1076),(10335,'Haute-Marne','52',1076),(10336,'Saint George','03',1009),(10337,'Saint John','04',1009),(10338,'Saint Mary','05',1009),(10339,'Saint Paul','06',1009),(10340,'Saint Peter','07',1009),(10341,'Saint Philip','08',1009),(10342,'Barbuda','10',1009),(10343,'Redonda','11',1009),(10344,'Christ Church','01',1018),(10345,'Saint Andrew','02',1018),(10346,'Saint George','03',1018),(10347,'Saint James','04',1018),(10348,'Saint John','05',1018),(10349,'Saint Joseph','06',1018),(10350,'Saint Lucy','07',1018),(10351,'Saint Michael','08',1018),(10352,'Saint Peter','09',1018),(10353,'Saint Philip','10',1018),(10354,'Saint Thomas','11',1018),(10355,'Estuaire','01',1080),(10356,'Haut-Ogooué','02',1080),(10357,'Moyen-Ogooué','03',1080),(10358,'Ngounié','04',1080),(10359,'Nyanga','05',1080),(10360,'Ogooué-Ivindo','06',1080),(10361,'Ogooué-Lolo','07',1080),(10362,'Ogooué-Maritime','08',1080),(10363,'Woleu-Ntem','09',1080),(10364,'Monmouthshire','MON',1226),(10365,'Antrim and Newtownabbey','ANN',1226),(10366,'Ards and North Down','AND',1226),(10367,'Armagh City, Banbridge and Craigavon','ABC',1226),(10368,'Belfast','BFS',1226),(10369,'Causeway Coast and Glens','CCG',1226),(10370,'Derry City and Strabane','DRS',1226),(10371,'Fermanagh and Omagh','FMO',1226),(10372,'Lisburn and Castlereagh','LBC',1226),(10373,'Mid and East Antrim','MEA',1226),(10374,'Mid Ulster','MUL',1226),(10375,'Newry, Mourne and Down','NMD',1226),(10376,'Bridgend','BGE',1226),(10377,'Caerphilly','CAY',1226),(10378,'Cardiff','CRF',1226),(10379,'Carmarthenshire','CRF',1226),(10380,'Ceredigion','CGN',1226),(10381,'Conwy','CWY',1226),(10382,'Denbighshire','DEN',1226),(10383,'Flintshire','FLN',1226),(10384,'Isle of Anglesey','AGY',1226),(10385,'Merthyr Tydfil','MTY',1226),(10386,'Neath Port Talbot','NTL',1226),(10387,'Newport','NWP',1226),(10388,'Pembrokeshire','PEM',1226),(10389,'Rhondda, Cynon, Taff','RCT',1226),(10390,'Swansea','SWA',1226),(10391,'Torfaen','TOF',1226),(10392,'Wrexham','WRX',1226);
+INSERT INTO `civicrm_state_province` (`id`, `name`, `abbreviation`, `country_id`) VALUES (1000,'Alabama','AL',1228),(1001,'Alaska','AK',1228),(1002,'Arizona','AZ',1228),(1003,'Arkansas','AR',1228),(1004,'California','CA',1228),(1005,'Colorado','CO',1228),(1006,'Connecticut','CT',1228),(1007,'Delaware','DE',1228),(1008,'Florida','FL',1228),(1009,'Georgia','GA',1228),(1010,'Hawaii','HI',1228),(1011,'Idaho','ID',1228),(1012,'Illinois','IL',1228),(1013,'Indiana','IN',1228),(1014,'Iowa','IA',1228),(1015,'Kansas','KS',1228),(1016,'Kentucky','KY',1228),(1017,'Louisiana','LA',1228),(1018,'Maine','ME',1228),(1019,'Maryland','MD',1228),(1020,'Massachusetts','MA',1228),(1021,'Michigan','MI',1228),(1022,'Minnesota','MN',1228),(1023,'Mississippi','MS',1228),(1024,'Missouri','MO',1228),(1025,'Montana','MT',1228),(1026,'Nebraska','NE',1228),(1027,'Nevada','NV',1228),(1028,'New Hampshire','NH',1228),(1029,'New Jersey','NJ',1228),(1030,'New Mexico','NM',1228),(1031,'New York','NY',1228),(1032,'North Carolina','NC',1228),(1033,'North Dakota','ND',1228),(1034,'Ohio','OH',1228),(1035,'Oklahoma','OK',1228),(1036,'Oregon','OR',1228),(1037,'Pennsylvania','PA',1228),(1038,'Rhode Island','RI',1228),(1039,'South Carolina','SC',1228),(1040,'South Dakota','SD',1228),(1041,'Tennessee','TN',1228),(1042,'Texas','TX',1228),(1043,'Utah','UT',1228),(1044,'Vermont','VT',1228),(1045,'Virginia','VA',1228),(1046,'Washington','WA',1228),(1047,'West Virginia','WV',1228),(1048,'Wisconsin','WI',1228),(1049,'Wyoming','WY',1228),(1050,'District of Columbia','DC',1228),(1052,'American Samoa','AS',1228),(1053,'Guam','GU',1228),(1055,'Northern Mariana Islands','MP',1228),(1056,'Puerto Rico','PR',1228),(1057,'Virgin Islands','VI',1228),(1058,'United States Minor Outlying Islands','UM',1228),(1059,'Armed Forces Europe','AE',1228),(1060,'Armed Forces Americas','AA',1228),(1061,'Armed Forces Pacific','AP',1228),(1100,'Alberta','AB',1039),(1101,'British Columbia','BC',1039),(1102,'Manitoba','MB',1039),(1103,'New Brunswick','NB',1039),(1104,'Newfoundland and Labrador','NL',1039),(1105,'Northwest Territories','NT',1039),(1106,'Nova Scotia','NS',1039),(1107,'Nunavut','NU',1039),(1108,'Ontario','ON',1039),(1109,'Prince Edward Island','PE',1039),(1110,'Quebec','QC',1039),(1111,'Saskatchewan','SK',1039),(1112,'Yukon Territory','YT',1039),(1200,'Maharashtra','MM',1101),(1201,'Karnataka','KA',1101),(1202,'Andhra Pradesh','AP',1101),(1203,'Arunachal Pradesh','AR',1101),(1204,'Assam','AS',1101),(1205,'Bihar','BR',1101),(1206,'Chhattisgarh','CH',1101),(1207,'Goa','GA',1101),(1208,'Gujarat','GJ',1101),(1209,'Haryana','HR',1101),(1210,'Himachal Pradesh','HP',1101),(1211,'Jammu and Kashmir','JK',1101),(1212,'Jharkhand','JH',1101),(1213,'Kerala','KL',1101),(1214,'Madhya Pradesh','MP',1101),(1215,'Manipur','MN',1101),(1216,'Meghalaya','ML',1101),(1217,'Mizoram','MZ',1101),(1218,'Nagaland','NL',1101),(1219,'Orissa','OR',1101),(1220,'Punjab','PB',1101),(1221,'Rajasthan','RJ',1101),(1222,'Sikkim','SK',1101),(1223,'Tamil Nadu','TN',1101),(1224,'Tripura','TR',1101),(1225,'Uttarakhand','UT',1101),(1226,'Uttar Pradesh','UP',1101),(1227,'West Bengal','WB',1101),(1228,'Andaman and Nicobar Islands','AN',1101),(1229,'Dadra and Nagar Haveli','DN',1101),(1230,'Daman and Diu','DD',1101),(1231,'Delhi','DL',1101),(1232,'Lakshadweep','LD',1101),(1233,'Pondicherry','PY',1101),(1300,'mazowieckie','MZ',1172),(1301,'pomorskie','PM',1172),(1302,'dolnośląskie','DS',1172),(1303,'kujawsko-pomorskie','KP',1172),(1304,'lubelskie','LU',1172),(1305,'lubuskie','LB',1172),(1306,'łódzkie','LD',1172),(1307,'małopolskie','MA',1172),(1308,'opolskie','OP',1172),(1309,'podkarpackie','PK',1172),(1310,'podlaskie','PD',1172),(1311,'śląskie','SL',1172),(1312,'świętokrzyskie','SK',1172),(1313,'warmińsko-mazurskie','WN',1172),(1314,'wielkopolskie','WP',1172),(1315,'zachodniopomorskie','ZP',1172),(1500,'Abu Zaby','AZ',1225),(1501,'\'Ajman','AJ',1225),(1502,'Al Fujayrah','FU',1225),(1503,'Ash Shariqah','SH',1225),(1504,'Dubayy','DU',1225),(1505,'Ra\'s al Khaymah','RK',1225),(1506,'Dac Lac','33',1233),(1507,'Umm al Qaywayn','UQ',1225),(1508,'Badakhshan','BDS',1001),(1509,'Badghis','BDG',1001),(1510,'Baghlan','BGL',1001),(1511,'Balkh','BAL',1001),(1512,'Bamian','BAM',1001),(1513,'Farah','FRA',1001),(1514,'Faryab','FYB',1001),(1515,'Ghazni','GHA',1001),(1516,'Ghowr','GHO',1001),(1517,'Helmand','HEL',1001),(1518,'Herat','HER',1001),(1519,'Jowzjan','JOW',1001),(1520,'Kabul','KAB',1001),(1521,'Kandahar','KAN',1001),(1522,'Kapisa','KAP',1001),(1523,'Khowst','KHO',1001),(1524,'Konar','KNR',1001),(1525,'Kondoz','KDZ',1001),(1526,'Laghman','LAG',1001),(1527,'Lowgar','LOW',1001),(1528,'Nangrahar','NAN',1001),(1529,'Nimruz','NIM',1001),(1530,'Nurestan','NUR',1001),(1531,'Oruzgan','ORU',1001),(1532,'Paktia','PIA',1001),(1533,'Paktika','PKA',1001),(1534,'Parwan','PAR',1001),(1535,'Samangan','SAM',1001),(1536,'Sar-e Pol','SAR',1001),(1537,'Takhar','TAK',1001),(1538,'Wardak','WAR',1001),(1539,'Zabol','ZAB',1001),(1540,'Berat','BR',1002),(1541,'Bulqizë','BU',1002),(1542,'Delvinë','DL',1002),(1543,'Devoll','DV',1002),(1544,'Dibër','DI',1002),(1545,'Durrës','DR',1002),(1546,'Elbasan','EL',1002),(1547,'Fier','FR',1002),(1548,'Gramsh','GR',1002),(1549,'Gjirokastër','GJ',1002),(1550,'Has','HA',1002),(1551,'Kavajë','KA',1002),(1552,'Kolonjë','ER',1002),(1553,'Korçë','KO',1002),(1554,'Krujë','KR',1002),(1555,'Kuçovë','KC',1002),(1556,'Kukës','KU',1002),(1557,'Kurbin','KB',1002),(1558,'Lezhë','LE',1002),(1559,'Librazhd','LB',1002),(1560,'Lushnjë','LU',1002),(1561,'Malësi e Madhe','MM',1002),(1562,'Mallakastër','MK',1002),(1563,'Mat','MT',1002),(1564,'Mirditë','MR',1002),(1565,'Peqin','PQ',1002),(1566,'Përmet','PR',1002),(1567,'Pogradec','PG',1002),(1568,'Pukë','PU',1002),(1569,'Sarandë','SR',1002),(1570,'Skrapar','SK',1002),(1571,'Shkodër','SH',1002),(1572,'Tepelenë','TE',1002),(1573,'Tiranë','TR',1002),(1574,'Tropojë','TP',1002),(1575,'Vlorë','VL',1002),(1576,'Erevan','ER',1011),(1577,'Aragacotn','AG',1011),(1578,'Ararat','AR',1011),(1579,'Armavir','AV',1011),(1580,'Gegarkunik\'','GR',1011),(1581,'Kotayk\'','KT',1011),(1582,'Lory','LO',1011),(1583,'Sirak','SH',1011),(1584,'Syunik\'','SU',1011),(1585,'Tavus','TV',1011),(1586,'Vayoc Jor','VD',1011),(1587,'Bengo','BGO',1006),(1588,'Benguela','BGU',1006),(1589,'Bie','BIE',1006),(1590,'Cabinda','CAB',1006),(1591,'Cuando-Cubango','CCU',1006),(1592,'Cuanza Norte','CNO',1006),(1593,'Cuanza Sul','CUS',1006),(1594,'Cunene','CNN',1006),(1595,'Huambo','HUA',1006),(1596,'Huila','HUI',1006),(1597,'Luanda','LUA',1006),(1598,'Lunda Norte','LNO',1006),(1599,'Lunda Sul','LSU',1006),(1600,'Malange','MAL',1006),(1601,'Moxico','MOX',1006),(1602,'Namibe','NAM',1006),(1603,'Uige','UIG',1006),(1604,'Zaire','ZAI',1006),(1605,'Capital federal','C',1010),(1606,'Buenos Aires','B',1010),(1607,'Catamarca','K',1010),(1608,'Cordoba','X',1010),(1609,'Corrientes','W',1010),(1610,'Chaco','H',1010),(1611,'Chubut','U',1010),(1612,'Entre Rios','E',1010),(1613,'Formosa','P',1010),(1614,'Jujuy','Y',1010),(1615,'La Pampa','L',1010),(1616,'Mendoza','M',1010),(1617,'Misiones','N',1010),(1618,'Neuquen','Q',1010),(1619,'Rio Negro','R',1010),(1620,'Salta','A',1010),(1621,'San Juan','J',1010),(1622,'San Luis','D',1010),(1623,'Santa Cruz','Z',1010),(1624,'Santa Fe','S',1010),(1625,'Santiago del Estero','G',1010),(1626,'Tierra del Fuego','V',1010),(1627,'Tucuman','T',1010),(1628,'Burgenland','1',1014),(1629,'Kärnten','2',1014),(1630,'Niederösterreich','3',1014),(1631,'Oberösterreich','4',1014),(1632,'Salzburg','5',1014),(1633,'Steiermark','6',1014),(1634,'Tirol','7',1014),(1635,'Vorarlberg','8',1014),(1636,'Wien','9',1014),(1637,'Australian Antarctic Territory','AAT',1008),(1638,'Australian Capital Territory','ACT',1013),(1639,'Northern Territory','NT',1013),(1640,'New South Wales','NSW',1013),(1641,'Queensland','QLD',1013),(1642,'South Australia','SA',1013),(1643,'Tasmania','TAS',1013),(1644,'Victoria','VIC',1013),(1645,'Western Australia','WA',1013),(1646,'Naxcivan','NX',1015),(1647,'Ali Bayramli','AB',1015),(1648,'Baki','BA',1015),(1649,'Ganca','GA',1015),(1650,'Lankaran','LA',1015),(1651,'Mingacevir','MI',1015),(1652,'Naftalan','NA',1015),(1653,'Saki','SA',1015),(1654,'Sumqayit','SM',1015),(1655,'Susa','SS',1015),(1656,'Xankandi','XA',1015),(1657,'Yevlax','YE',1015),(1658,'Abseron','ABS',1015),(1659,'Agcabadi','AGC',1015),(1660,'Agdam','AGM',1015),(1661,'Agdas','AGS',1015),(1662,'Agstafa','AGA',1015),(1663,'Agsu','AGU',1015),(1664,'Astara','AST',1015),(1665,'Babak','BAB',1015),(1666,'Balakan','BAL',1015),(1667,'Barda','BAR',1015),(1668,'Beylagan','BEY',1015),(1669,'Bilasuvar','BIL',1015),(1670,'Cabrayll','CAB',1015),(1671,'Calilabad','CAL',1015),(1672,'Culfa','CUL',1015),(1673,'Daskasan','DAS',1015),(1674,'Davaci','DAV',1015),(1675,'Fuzuli','FUZ',1015),(1676,'Gadabay','GAD',1015),(1677,'Goranboy','GOR',1015),(1678,'Goycay','GOY',1015),(1679,'Haciqabul','HAC',1015),(1680,'Imisli','IMI',1015),(1681,'Ismayilli','ISM',1015),(1682,'Kalbacar','KAL',1015),(1683,'Kurdamir','KUR',1015),(1684,'Lacin','LAC',1015),(1685,'Lerik','LER',1015),(1686,'Masalli','MAS',1015),(1687,'Neftcala','NEF',1015),(1688,'Oguz','OGU',1015),(1689,'Ordubad','ORD',1015),(1690,'Qabala','QAB',1015),(1691,'Qax','QAX',1015),(1692,'Qazax','QAZ',1015),(1693,'Qobustan','QOB',1015),(1694,'Quba','QBA',1015),(1695,'Qubadli','QBI',1015),(1696,'Qusar','QUS',1015),(1697,'Saatli','SAT',1015),(1698,'Sabirabad','SAB',1015),(1699,'Sadarak','SAD',1015),(1700,'Sahbuz','SAH',1015),(1701,'Salyan','SAL',1015),(1702,'Samaxi','SMI',1015),(1703,'Samkir','SKR',1015),(1704,'Samux','SMX',1015),(1705,'Sarur','SAR',1015),(1706,'Siyazan','SIY',1015),(1707,'Tartar','TAR',1015),(1708,'Tovuz','TOV',1015),(1709,'Ucar','UCA',1015),(1710,'Xacmaz','XAC',1015),(1711,'Xanlar','XAN',1015),(1712,'Xizi','XIZ',1015),(1713,'Xocali','XCI',1015),(1714,'Xocavand','XVD',1015),(1715,'Yardimli','YAR',1015),(1716,'Zangilan','ZAN',1015),(1717,'Zaqatala','ZAQ',1015),(1718,'Zardab','ZAR',1015),(1719,'Federacija Bosna i Hercegovina','BIH',1026),(1720,'Republika Srpska','SRP',1026),(1721,'Bagerhat zila','05',1017),(1722,'Bandarban zila','01',1017),(1723,'Barguna zila','02',1017),(1724,'Barisal zila','06',1017),(1725,'Bhola zila','07',1017),(1726,'Bogra zila','03',1017),(1727,'Brahmanbaria zila','04',1017),(1728,'Chandpur zila','09',1017),(1729,'Chittagong zila','10',1017),(1730,'Chuadanga zila','12',1017),(1731,'Comilla zila','08',1017),(1732,'Cox\'s Bazar zila','11',1017),(1733,'Dhaka zila','13',1017),(1734,'Dinajpur zila','14',1017),(1735,'Faridpur zila','15',1017),(1736,'Feni zila','16',1017),(1737,'Gaibandha zila','19',1017),(1738,'Gazipur zila','18',1017),(1739,'Gopalganj zila','17',1017),(1740,'Habiganj zila','20',1017),(1741,'Jaipurhat zila','24',1017),(1742,'Jamalpur zila','21',1017),(1743,'Jessore zila','22',1017),(1744,'Jhalakati zila','25',1017),(1745,'Jhenaidah zila','23',1017),(1746,'Khagrachari zila','29',1017),(1747,'Khulna zila','27',1017),(1748,'Kishorganj zila','26',1017),(1749,'Kurigram zila','28',1017),(1750,'Kushtia zila','30',1017),(1751,'Lakshmipur zila','31',1017),(1752,'Lalmonirhat zila','32',1017),(1753,'Madaripur zila','36',1017),(1754,'Magura zila','37',1017),(1755,'Manikganj zila','33',1017),(1756,'Meherpur zila','39',1017),(1757,'Moulvibazar zila','38',1017),(1758,'Munshiganj zila','35',1017),(1759,'Mymensingh zila','34',1017),(1760,'Naogaon zila','48',1017),(1761,'Narail zila','43',1017),(1762,'Narayanganj zila','40',1017),(1763,'Narsingdi zila','42',1017),(1764,'Natore zila','44',1017),(1765,'Nawabganj zila','45',1017),(1766,'Netrakona zila','41',1017),(1767,'Nilphamari zila','46',1017),(1768,'Noakhali zila','47',1017),(1769,'Pabna zila','49',1017),(1770,'Panchagarh zila','52',1017),(1771,'Patuakhali zila','51',1017),(1772,'Pirojpur zila','50',1017),(1773,'Rajbari zila','53',1017),(1774,'Rajshahi zila','54',1017),(1775,'Rangamati zila','56',1017),(1776,'Rangpur zila','55',1017),(1777,'Satkhira zila','58',1017),(1778,'Shariatpur zila','62',1017),(1779,'Sherpur zila','57',1017),(1780,'Sirajganj zila','59',1017),(1781,'Sunamganj zila','61',1017),(1782,'Sylhet zila','60',1017),(1783,'Tangail zila','63',1017),(1784,'Thakurgaon zila','64',1017),(1785,'Antwerpen','VAN',1020),(1786,'Brabant Wallon','WBR',1020),(1787,'Hainaut','WHT',1020),(1788,'Liege','WLG',1020),(1789,'Limburg','VLI',1020),(1790,'Luxembourg','WLX',1020),(1791,'Namur','WNA',1020),(1792,'Oost-Vlaanderen','VOV',1020),(1793,'Vlaams-Brabant','VBR',1020),(1794,'West-Vlaanderen','VWV',1020),(1795,'Bale','BAL',1034),(1796,'Bam','BAM',1034),(1797,'Banwa','BAN',1034),(1798,'Bazega','BAZ',1034),(1799,'Bougouriba','BGR',1034),(1800,'Boulgou','BLG',1034),(1801,'Boulkiemde','BLK',1034),(1802,'Comoe','COM',1034),(1803,'Ganzourgou','GAN',1034),(1804,'Gnagna','GNA',1034),(1805,'Gourma','GOU',1034),(1806,'Houet','HOU',1034),(1807,'Ioba','IOB',1034),(1808,'Kadiogo','KAD',1034),(1809,'Kenedougou','KEN',1034),(1810,'Komondjari','KMD',1034),(1811,'Kompienga','KMP',1034),(1812,'Kossi','KOS',1034),(1813,'Koulpulogo','KOP',1034),(1814,'Kouritenga','KOT',1034),(1815,'Kourweogo','KOW',1034),(1816,'Leraba','LER',1034),(1817,'Loroum','LOR',1034),(1818,'Mouhoun','MOU',1034),(1819,'Nahouri','NAO',1034),(1820,'Namentenga','NAM',1034),(1821,'Nayala','NAY',1034),(1822,'Noumbiel','NOU',1034),(1823,'Oubritenga','OUB',1034),(1824,'Oudalan','OUD',1034),(1825,'Passore','PAS',1034),(1826,'Poni','PON',1034),(1827,'Sanguie','SNG',1034),(1828,'Sanmatenga','SMT',1034),(1829,'Seno','SEN',1034),(1830,'Siasili','SIS',1034),(1831,'Soum','SOM',1034),(1832,'Sourou','SOR',1034),(1833,'Tapoa','TAP',1034),(1834,'Tui','TUI',1034),(1835,'Yagha','YAG',1034),(1836,'Yatenga','YAT',1034),(1837,'Ziro','ZIR',1034),(1838,'Zondoma','ZON',1034),(1839,'Zoundweogo','ZOU',1034),(1840,'Blagoevgrad','01',1033),(1841,'Burgas','02',1033),(1842,'Dobrich','08',1033),(1843,'Gabrovo','07',1033),(1844,'Haskovo','26',1033),(1845,'Yambol','28',1033),(1846,'Kardzhali','09',1033),(1847,'Kyustendil','10',1033),(1848,'Lovech','11',1033),(1849,'Montana','12',1033),(1850,'Pazardzhik','13',1033),(1851,'Pernik','14',1033),(1852,'Pleven','15',1033),(1853,'Plovdiv','16',1033),(1854,'Razgrad','17',1033),(1855,'Ruse','18',1033),(1856,'Silistra','19',1033),(1857,'Sliven','20',1033),(1858,'Smolyan','21',1033),(1859,'Sofia','23',1033),(1860,'Stara Zagora','24',1033),(1861,'Shumen','27',1033),(1862,'Targovishte','25',1033),(1863,'Varna','03',1033),(1864,'Veliko Tarnovo','04',1033),(1865,'Vidin','05',1033),(1866,'Vratsa','06',1033),(1867,'Al Hadd','01',1016),(1868,'Al Manamah','03',1016),(1869,'Al Mintaqah al Gharbiyah','10',1016),(1870,'Al Mintagah al Wusta','07',1016),(1871,'Al Mintaqah ash Shamaliyah','05',1016),(1872,'Al Muharraq','02',1016),(1873,'Ar Rifa','09',1016),(1874,'Jidd Hafs','04',1016),(1875,'Madluat Jamad','12',1016),(1876,'Madluat Isa','08',1016),(1877,'Mintaqat Juzur tawar','11',1016),(1878,'Sitrah','06',1016),(1879,'Bubanza','BB',1036),(1880,'Bujumbura','BJ',1036),(1881,'Bururi','BR',1036),(1882,'Cankuzo','CA',1036),(1883,'Cibitoke','CI',1036),(1884,'Gitega','GI',1036),(1885,'Karuzi','KR',1036),(1886,'Kayanza','KY',1036),(1887,'Makamba','MA',1036),(1888,'Muramvya','MU',1036),(1889,'Mwaro','MW',1036),(1890,'Ngozi','NG',1036),(1891,'Rutana','RT',1036),(1892,'Ruyigi','RY',1036),(1893,'Alibori','AL',1022),(1894,'Atakora','AK',1022),(1895,'Atlantique','AQ',1022),(1896,'Borgou','BO',1022),(1897,'Collines','CO',1022),(1898,'Donga','DO',1022),(1899,'Kouffo','KO',1022),(1900,'Littoral','LI',1022),(1901,'Mono','MO',1022),(1902,'Oueme','OU',1022),(1903,'Plateau','PL',1022),(1904,'Zou','ZO',1022),(1905,'Belait','BE',1032),(1906,'Brunei-Muara','BM',1032),(1907,'Temburong','TE',1032),(1908,'Tutong','TU',1032),(1909,'Cochabamba','C',1025),(1910,'Chuquisaca','H',1025),(1911,'El Beni','B',1025),(1912,'La Paz','L',1025),(1913,'Oruro','O',1025),(1914,'Pando','N',1025),(1915,'Potosi','P',1025),(1916,'Tarija','T',1025),(1917,'Acre','AC',1029),(1918,'Alagoas','AL',1029),(1919,'Amazonas','AM',1029),(1920,'Amapa','AP',1029),(1921,'Bahia','BA',1029),(1922,'Ceara','CE',1029),(1923,'Distrito Federal','DF',1029),(1924,'Espirito Santo','ES',1029),(1926,'Goias','GO',1029),(1927,'Maranhao','MA',1029),(1928,'Minas Gerais','MG',1029),(1929,'Mato Grosso do Sul','MS',1029),(1930,'Mato Grosso','MT',1029),(1931,'Para','PA',1029),(1932,'Paraiba','PB',1029),(1933,'Pernambuco','PE',1029),(1934,'Piaui','PI',1029),(1935,'Parana','PR',1029),(1936,'Rio de Janeiro','RJ',1029),(1937,'Rio Grande do Norte','RN',1029),(1938,'Rondonia','RO',1029),(1939,'Roraima','RR',1029),(1940,'Rio Grande do Sul','RS',1029),(1941,'Santa Catarina','SC',1029),(1942,'Sergipe','SE',1029),(1943,'Sao Paulo','SP',1029),(1944,'Tocantins','TO',1029),(1945,'Acklins and Crooked Islands','AC',1212),(1946,'Bimini','BI',1212),(1947,'Cat Island','CI',1212),(1948,'Exuma','EX',1212),(1955,'Inagua','IN',1212),(1957,'Long Island','LI',1212),(1959,'Mayaguana','MG',1212),(1960,'New Providence','NP',1212),(1962,'Ragged Island','RI',1212),(1966,'Bumthang','33',1024),(1967,'Chhukha','12',1024),(1968,'Dagana','22',1024),(1969,'Gasa','GA',1024),(1970,'Ha','13',1024),(1971,'Lhuentse','44',1024),(1972,'Monggar','42',1024),(1973,'Paro','11',1024),(1974,'Pemagatshel','43',1024),(1975,'Punakha','23',1024),(1976,'Samdrup Jongkha','45',1024),(1977,'Samtee','14',1024),(1978,'Sarpang','31',1024),(1979,'Thimphu','15',1024),(1980,'Trashigang','41',1024),(1981,'Trashi Yangtse','TY',1024),(1982,'Trongsa','32',1024),(1983,'Tsirang','21',1024),(1984,'Wangdue Phodrang','24',1024),(1985,'Zhemgang','34',1024),(1986,'Central','CE',1027),(1987,'Ghanzi','GH',1027),(1988,'Kgalagadi','KG',1027),(1989,'Kgatleng','KL',1027),(1990,'Kweneng','KW',1027),(1991,'Ngamiland','NG',1027),(1992,'North-East','NE',1027),(1993,'North-West','NW',1027),(1994,'South-East','SE',1027),(1995,'Southern','SO',1027),(1996,'Brèsckaja voblasc\'','BR',1019),(1997,'Homel\'skaja voblasc\'','HO',1019),(1998,'Hrodzenskaja voblasc\'','HR',1019),(1999,'Mahilëuskaja voblasc\'','MA',1019),(2000,'Minskaja voblasc\'','MI',1019),(2001,'Vicebskaja voblasc\'','VI',1019),(2002,'Belize','BZ',1021),(2003,'Cayo','CY',1021),(2004,'Corozal','CZL',1021),(2005,'Orange Walk','OW',1021),(2006,'Stann Creek','SC',1021),(2007,'Toledo','TOL',1021),(2008,'Kinshasa','KN',1050),(2011,'Equateur','EQ',1050),(2014,'Kasai-Oriental','KE',1050),(2016,'Maniema','MA',1050),(2017,'Nord-Kivu','NK',1050),(2019,'Sud-Kivu','SK',1050),(2020,'Bangui','BGF',1042),(2021,'Bamingui-Bangoran','BB',1042),(2022,'Basse-Kotto','BK',1042),(2023,'Haute-Kotto','HK',1042),(2024,'Haut-Mbomou','HM',1042),(2025,'Kemo','KG',1042),(2026,'Lobaye','LB',1042),(2027,'Mambere-Kadei','HS',1042),(2028,'Mbomou','MB',1042),(2029,'Nana-Grebizi','KB',1042),(2030,'Nana-Mambere','NM',1042),(2031,'Ombella-Mpoko','MP',1042),(2032,'Ouaka','UK',1042),(2033,'Ouham','AC',1042),(2034,'Ouham-Pende','OP',1042),(2035,'Sangha-Mbaere','SE',1042),(2036,'Vakaga','VR',1042),(2037,'Brazzaville','BZV',1051),(2038,'Bouenza','11',1051),(2039,'Cuvette','8',1051),(2040,'Cuvette-Ouest','15',1051),(2041,'Kouilou','5',1051),(2042,'Lekoumou','2',1051),(2043,'Likouala','7',1051),(2044,'Niari','9',1051),(2045,'Plateaux','14',1051),(2046,'Pool','12',1051),(2047,'Sangha','13',1051),(2048,'Aargau','AG',1205),(2049,'Appenzell Innerrhoden','AI',1205),(2050,'Appenzell Ausserrhoden','AR',1205),(2051,'Bern','BE',1205),(2052,'Basel-Landschaft','BL',1205),(2053,'Basel-Stadt','BS',1205),(2054,'Fribourg','FR',1205),(2055,'Geneva','GE',1205),(2056,'Glarus','GL',1205),(2057,'Graubunden','GR',1205),(2058,'Jura','JU',1205),(2059,'Luzern','LU',1205),(2060,'Neuchatel','NE',1205),(2061,'Nidwalden','NW',1205),(2062,'Obwalden','OW',1205),(2063,'Sankt Gallen','SG',1205),(2064,'Schaffhausen','SH',1205),(2065,'Solothurn','SO',1205),(2066,'Schwyz','SZ',1205),(2067,'Thurgau','TG',1205),(2068,'Ticino','TI',1205),(2069,'Uri','UR',1205),(2070,'Vaud','VD',1205),(2071,'Valais','VS',1205),(2072,'Zug','ZG',1205),(2073,'Zurich','ZH',1205),(2074,'18 Montagnes','06',1054),(2075,'Agnebi','16',1054),(2076,'Bas-Sassandra','09',1054),(2077,'Denguele','10',1054),(2078,'Haut-Sassandra','02',1054),(2079,'Lacs','07',1054),(2080,'Lagunes','01',1054),(2081,'Marahoue','12',1054),(2082,'Moyen-Comoe','05',1054),(2083,'Nzi-Comoe','11',1054),(2084,'Savanes','03',1054),(2085,'Sud-Bandama','15',1054),(2086,'Sud-Comoe','13',1054),(2087,'Vallee du Bandama','04',1054),(2088,'Worodouqou','14',1054),(2089,'Zanzan','08',1054),(2090,'Aisen del General Carlos Ibanez del Campo','AI',1044),(2091,'Antofagasta','AN',1044),(2092,'Araucania','AR',1044),(2093,'Atacama','AT',1044),(2094,'Bio-Bio','BI',1044),(2095,'Coquimbo','CO',1044),(2096,'Libertador General Bernardo O\'Higgins','LI',1044),(2097,'Los Lagos','LL',1044),(2098,'Magallanes','MA',1044),(2099,'Maule','ML',1044),(2100,'Santiago Metropolitan','SM',1044),(2101,'Tarapaca','TA',1044),(2102,'Valparaiso','VS',1044),(2103,'Adamaoua','AD',1038),(2104,'Centre','CE',1038),(2105,'East','ES',1038),(2106,'Far North','EN',1038),(2107,'North','NO',1038),(2108,'South','SW',1038),(2109,'South-West','SW',1038),(2110,'West','OU',1038),(2111,'Beijing','11',1045),(2112,'Chongqing','50',1045),(2113,'Shanghai','31',1045),(2114,'Tianjin','12',1045),(2115,'Anhui','34',1045),(2116,'Fujian','35',1045),(2117,'Gansu','62',1045),(2118,'Guangdong','44',1045),(2119,'Guizhou','52',1045),(2120,'Hainan','46',1045),(2121,'Hebei','13',1045),(2122,'Heilongjiang','23',1045),(2123,'Henan','41',1045),(2124,'Hubei','42',1045),(2125,'Hunan','43',1045),(2126,'Jiangsu','32',1045),(2127,'Jiangxi','36',1045),(2128,'Jilin','22',1045),(2129,'Liaoning','21',1045),(2130,'Qinghai','63',1045),(2131,'Shaanxi','61',1045),(2132,'Shandong','37',1045),(2133,'Shanxi','14',1045),(2134,'Sichuan','51',1045),(2135,'Taiwan','71',1045),(2136,'Yunnan','53',1045),(2137,'Zhejiang','33',1045),(2138,'Guangxi','45',1045),(2139,'Neia Mongol (mn)','15',1045),(2140,'Xinjiang','65',1045),(2141,'Xizang','54',1045),(2142,'Hong Kong','91',1045),(2143,'Macau','92',1045),(2144,'Distrito Capital de Bogotá','DC',1048),(2145,'Amazonea','AMA',1048),(2146,'Antioquia','ANT',1048),(2147,'Arauca','ARA',1048),(2148,'Atlántico','ATL',1048),(2149,'Bolívar','BOL',1048),(2150,'Boyacá','BOY',1048),(2151,'Caldea','CAL',1048),(2152,'Caquetá','CAQ',1048),(2153,'Casanare','CAS',1048),(2154,'Cauca','CAU',1048),(2155,'Cesar','CES',1048),(2156,'Córdoba','COR',1048),(2157,'Cundinamarca','CUN',1048),(2158,'Chocó','CHO',1048),(2159,'Guainía','GUA',1048),(2160,'Guaviare','GUV',1048),(2161,'La Guajira','LAG',1048),(2162,'Magdalena','MAG',1048),(2163,'Meta','MET',1048),(2164,'Nariño','NAR',1048),(2165,'Norte de Santander','NSA',1048),(2166,'Putumayo','PUT',1048),(2167,'Quindio','QUI',1048),(2168,'Risaralda','RIS',1048),(2169,'San Andrés, Providencia y Santa Catalina','SAP',1048),(2170,'Santander','SAN',1048),(2171,'Sucre','SUC',1048),(2172,'Tolima','TOL',1048),(2173,'Valle del Cauca','VAC',1048),(2174,'Vaupés','VAU',1048),(2175,'Vichada','VID',1048),(2176,'Alajuela','A',1053),(2177,'Cartago','C',1053),(2178,'Guanacaste','G',1053),(2179,'Heredia','H',1053),(2180,'Limon','L',1053),(2181,'Puntarenas','P',1053),(2182,'San Jose','SJ',1053),(2183,'Camagey','09',1056),(2184,'Ciego de `vila','08',1056),(2185,'Cienfuegos','06',1056),(2186,'Ciudad de La Habana','03',1056),(2187,'Granma','12',1056),(2188,'Guantanamo','14',1056),(2189,'Holquin','11',1056),(2190,'La Habana','02',1056),(2191,'Las Tunas','10',1056),(2192,'Matanzas','04',1056),(2193,'Pinar del Rio','01',1056),(2194,'Sancti Spiritus','07',1056),(2195,'Santiago de Cuba','13',1056),(2196,'Villa Clara','05',1056),(2197,'Isla de la Juventud','99',1056),(2198,'Pinar del Roo','PR',1056),(2199,'Ciego de Avila','CA',1056),(2200,'Camagoey','CG',1056),(2201,'Holgun','HO',1056),(2202,'Sancti Spritus','SS',1056),(2203,'Municipio Especial Isla de la Juventud','IJ',1056),(2204,'Boa Vista','BV',1040),(2205,'Brava','BR',1040),(2206,'Calheta de Sao Miguel','CS',1040),(2207,'Fogo','FO',1040),(2208,'Maio','MA',1040),(2209,'Mosteiros','MO',1040),(2210,'Paul','PA',1040),(2211,'Porto Novo','PN',1040),(2212,'Praia','PR',1040),(2213,'Ribeira Grande','RG',1040),(2214,'Sal','SL',1040),(2215,'Sao Domingos','SD',1040),(2216,'Sao Filipe','SF',1040),(2217,'Sao Nicolau','SN',1040),(2218,'Sao Vicente','SV',1040),(2219,'Tarrafal','TA',1040),(2220,'Ammochostos Magusa','04',1057),(2221,'Keryneia','06',1057),(2222,'Larnaka','03',1057),(2223,'Lefkosia','01',1057),(2224,'Lemesos','02',1057),(2225,'Pafos','05',1057),(2226,'Jihočeský kraj','JC',1058),(2227,'Jihomoravský kraj','JM',1058),(2228,'Karlovarský kraj','KA',1058),(2229,'Královéhradecký kraj','KR',1058),(2230,'Liberecký kraj','LI',1058),(2231,'Moravskoslezský kraj','MO',1058),(2232,'Olomoucký kraj','OL',1058),(2233,'Pardubický kraj','PA',1058),(2234,'Plzeňský kraj','PL',1058),(2235,'Praha, hlavní město','PR',1058),(2236,'Středočeský kraj','ST',1058),(2237,'Ústecký kraj','US',1058),(2238,'Vysočina','VY',1058),(2239,'Zlínský kraj','ZL',1058),(2240,'Baden-Württemberg','BW',1082),(2241,'Bayern','BY',1082),(2242,'Bremen','HB',1082),(2243,'Hamburg','HH',1082),(2244,'Hessen','HE',1082),(2245,'Niedersachsen','NI',1082),(2246,'Nordrhein-Westfalen','NW',1082),(2247,'Rheinland-Pfalz','RP',1082),(2248,'Saarland','SL',1082),(2249,'Schleswig-Holstein','SH',1082),(2250,'Berlin','BE',1082),(2251,'Brandenburg','BB',1082),(2252,'Mecklenburg-Vorpommern','MV',1082),(2253,'Sachsen','SN',1082),(2254,'Sachsen-Anhalt','ST',1082),(2255,'Thüringen','TH',1082),(2256,'Ali Sabiah','AS',1060),(2257,'Dikhil','DI',1060),(2258,'Djibouti','DJ',1060),(2259,'Obock','OB',1060),(2260,'Tadjoura','TA',1060),(2261,'Frederiksberg','147',1059),(2262,'Copenhagen City','101',1059),(2263,'Copenhagen','015',1059),(2264,'Frederiksborg','020',1059),(2265,'Roskilde','025',1059),(2266,'Vestsjælland','030',1059),(2267,'Storstrøm','035',1059),(2268,'Bornholm','040',1059),(2269,'Fyn','042',1059),(2270,'South Jutland','050',1059),(2271,'Ribe','055',1059),(2272,'Vejle','060',1059),(2273,'Ringkjøbing','065',1059),(2274,'Århus','070',1059),(2275,'Viborg','076',1059),(2276,'North Jutland','080',1059),(2277,'Distrito Nacional (Santo Domingo)','01',1062),(2278,'Azua','02',1062),(2279,'Bahoruco','03',1062),(2280,'Barahona','04',1062),(2281,'Dajabón','05',1062),(2282,'Duarte','06',1062),(2283,'El Seybo [El Seibo]','08',1062),(2284,'Espaillat','09',1062),(2285,'Hato Mayor','30',1062),(2286,'Independencia','10',1062),(2287,'La Altagracia','11',1062),(2288,'La Estrelleta [Elias Pina]','07',1062),(2289,'La Romana','12',1062),(2290,'La Vega','13',1062),(2291,'Maroia Trinidad Sánchez','14',1062),(2292,'Monseñor Nouel','28',1062),(2293,'Monte Cristi','15',1062),(2294,'Monte Plata','29',1062),(2295,'Pedernales','16',1062),(2296,'Peravia','17',1062),(2297,'Puerto Plata','18',1062),(2298,'Salcedo','19',1062),(2299,'Samaná','20',1062),(2300,'San Cristóbal','21',1062),(2301,'San Pedro de Macorís','23',1062),(2302,'Sánchez Ramírez','24',1062),(2303,'Santiago','25',1062),(2304,'Santiago Rodríguez','26',1062),(2305,'Valverde','27',1062),(2306,'Adrar','01',1003),(2307,'Ain Defla','44',1003),(2308,'Ain Tmouchent','46',1003),(2309,'Alger','16',1003),(2310,'Annaba','23',1003),(2311,'Batna','05',1003),(2312,'Bechar','08',1003),(2313,'Bejaia','06',1003),(2314,'Biskra','07',1003),(2315,'Blida','09',1003),(2316,'Bordj Bou Arreridj','34',1003),(2317,'Bouira','10',1003),(2318,'Boumerdes','35',1003),(2319,'Chlef','02',1003),(2320,'Constantine','25',1003),(2321,'Djelfa','17',1003),(2322,'El Bayadh','32',1003),(2323,'El Oued','39',1003),(2324,'El Tarf','36',1003),(2325,'Ghardaia','47',1003),(2326,'Guelma','24',1003),(2327,'Illizi','33',1003),(2328,'Jijel','18',1003),(2329,'Khenchela','40',1003),(2330,'Laghouat','03',1003),(2331,'Mascara','29',1003),(2332,'Medea','26',1003),(2333,'Mila','43',1003),(2334,'Mostaganem','27',1003),(2335,'Msila','28',1003),(2336,'Naama','45',1003),(2337,'Oran','31',1003),(2338,'Ouargla','30',1003),(2339,'Oum el Bouaghi','04',1003),(2340,'Relizane','48',1003),(2341,'Saida','20',1003),(2342,'Setif','19',1003),(2343,'Sidi Bel Abbes','22',1003),(2344,'Skikda','21',1003),(2345,'Souk Ahras','41',1003),(2346,'Tamanghasset','11',1003),(2347,'Tebessa','12',1003),(2348,'Tiaret','14',1003),(2349,'Tindouf','37',1003),(2350,'Tipaza','42',1003),(2351,'Tissemsilt','38',1003),(2352,'Tizi Ouzou','15',1003),(2353,'Tlemcen','13',1003),(2354,'Azuay','A',1064),(2355,'Bolivar','B',1064),(2356,'Canar','F',1064),(2357,'Carchi','C',1064),(2358,'Cotopaxi','X',1064),(2359,'Chimborazo','H',1064),(2360,'El Oro','O',1064),(2361,'Esmeraldas','E',1064),(2362,'Galapagos','W',1064),(2363,'Guayas','G',1064),(2364,'Imbabura','I',1064),(2365,'Loja','L',1064),(2366,'Los Rios','R',1064),(2367,'Manabi','M',1064),(2368,'Morona-Santiago','S',1064),(2369,'Napo','N',1064),(2370,'Orellana','D',1064),(2371,'Pastaza','Y',1064),(2372,'Pichincha','P',1064),(2373,'Sucumbios','U',1064),(2374,'Tungurahua','T',1064),(2375,'Zamora-Chinchipe','Z',1064),(2376,'Harjumaa','37',1069),(2377,'Hiiumaa','39',1069),(2378,'Ida-Virumaa','44',1069),(2379,'Jõgevamaa','49',1069),(2380,'Järvamaa','51',1069),(2381,'Läänemaa','57',1069),(2382,'Lääne-Virumaa','59',1069),(2383,'Põlvamaa','65',1069),(2384,'Pärnumaa','67',1069),(2385,'Raplamaa','70',1069),(2386,'Saaremaa','74',1069),(2387,'Tartumaa','7B',1069),(2388,'Valgamaa','82',1069),(2389,'Viljandimaa','84',1069),(2390,'Võrumaa','86',1069),(2391,'Ad Daqahllyah','DK',1065),(2392,'Al Bahr al Ahmar','BA',1065),(2393,'Al Buhayrah','BH',1065),(2394,'Al Fayym','FYM',1065),(2395,'Al Gharbiyah','GH',1065),(2396,'Al Iskandarlyah','ALX',1065),(2397,'Al Isma illyah','IS',1065),(2398,'Al Jizah','GZ',1065),(2399,'Al Minuflyah','MNF',1065),(2400,'Al Minya','MN',1065),(2401,'Al Qahirah','C',1065),(2402,'Al Qalyublyah','KB',1065),(2403,'Al Wadi al Jadid','WAD',1065),(2404,'Ash Sharqiyah','SHR',1065),(2405,'As Suways','SUZ',1065),(2406,'Aswan','ASN',1065),(2407,'Asyut','AST',1065),(2408,'Bani Suwayf','BNS',1065),(2409,'Bur Sa\'id','PTS',1065),(2410,'Dumyat','DT',1065),(2411,'Janub Sina\'','JS',1065),(2412,'Kafr ash Shaykh','KFS',1065),(2413,'Matruh','MT',1065),(2414,'Qina','KN',1065),(2415,'Shamal Sina\'','SIN',1065),(2416,'Suhaj','SHG',1065),(2417,'Anseba','AN',1068),(2418,'Debub','DU',1068),(2419,'Debubawi Keyih Bahri [Debub-Keih-Bahri]','DK',1068),(2420,'Gash-Barka','GB',1068),(2421,'Maakel [Maekel]','MA',1068),(2422,'Semenawi Keyih Bahri [Semien-Keih-Bahri]','SK',1068),(2423,'Álava','VI',1198),(2424,'Albacete','AB',1198),(2425,'Alicante','A',1198),(2426,'Almería','AL',1198),(2427,'Asturias','O',1198),(2428,'Ávila','AV',1198),(2429,'Badajoz','BA',1198),(2430,'Baleares','PM',1198),(2431,'Barcelona','B',1198),(2432,'Burgos','BU',1198),(2433,'Cáceres','CC',1198),(2434,'Cádiz','CA',1198),(2435,'Cantabria','S',1198),(2436,'Castellón','CS',1198),(2437,'Ciudad Real','CR',1198),(2438,'Cuenca','CU',1198),(2439,'Girona [Gerona]','GE',1198),(2440,'Granada','GR',1198),(2441,'Guadalajara','GU',1198),(2442,'Guipúzcoa','SS',1198),(2443,'Huelva','H',1198),(2444,'Huesca','HU',1198),(2445,'Jaén','J',1198),(2446,'La Coruña','C',1198),(2447,'La Rioja','LO',1198),(2448,'Las Palmas','GC',1198),(2449,'León','LE',1198),(2450,'Lleida [Lérida]','L',1198),(2451,'Lugo','LU',1198),(2452,'Madrid','M',1198),(2453,'Málaga','MA',1198),(2454,'Murcia','MU',1198),(2455,'Navarra','NA',1198),(2456,'Ourense','OR',1198),(2457,'Palencia','P',1198),(2458,'Pontevedra','PO',1198),(2459,'Salamanca','SA',1198),(2460,'Santa Cruz de Tenerife','TF',1198),(2461,'Segovia','SG',1198),(2462,'Sevilla','SE',1198),(2463,'Soria','SO',1198),(2464,'Tarragona','T',1198),(2465,'Teruel','TE',1198),(2466,'Valencia','V',1198),(2467,'Valladolid','VA',1198),(2468,'Vizcaya','BI',1198),(2469,'Zamora','ZA',1198),(2470,'Zaragoza','Z',1198),(2471,'Ceuta','CE',1198),(2472,'Melilla','ML',1198),(2473,'Addis Ababa','AA',1070),(2474,'Dire Dawa','DD',1070),(2475,'Afar','AF',1070),(2476,'Amara','AM',1070),(2477,'Benshangul-Gumaz','BE',1070),(2478,'Gambela Peoples','GA',1070),(2479,'Harari People','HA',1070),(2480,'Oromia','OR',1070),(2481,'Somali','SO',1070),(2482,'Southern Nations, Nationalities and Peoples','SN',1070),(2483,'Tigrai','TI',1070),(2490,'Eastern','E',1074),(2491,'Northern','N',1074),(2492,'Western','W',1074),(2493,'Rotuma','R',1074),(2494,'Chuuk','TRK',1141),(2495,'Kosrae','KSA',1141),(2496,'Pohnpei','PNI',1141),(2497,'Yap','YAP',1141),(2498,'Ain','01',1076),(2499,'Aisne','02',1076),(2500,'Allier','03',1076),(2501,'Alpes-de-Haute-Provence','04',1076),(2502,'Alpes-Maritimes','06',1076),(2503,'Ardèche','07',1076),(2504,'Ardennes','08',1076),(2505,'Ariège','09',1076),(2506,'Aube','10',1076),(2507,'Aude','11',1076),(2508,'Aveyron','12',1076),(2509,'Bas-Rhin','67',1076),(2510,'Bouches-du-Rhône','13',1076),(2511,'Calvados','14',1076),(2512,'Cantal','15',1076),(2513,'Charente','16',1076),(2514,'Charente-Maritime','17',1076),(2515,'Cher','18',1076),(2516,'Corrèze','19',1076),(2517,'Corse-du-Sud','20A',1076),(2518,'Côte-d\'Or','21',1076),(2519,'Côtes-d\'Armor','22',1076),(2520,'Creuse','23',1076),(2521,'Deux-Sèvres','79',1076),(2522,'Dordogne','24',1076),(2523,'Doubs','25',1076),(2524,'Drôme','26',1076),(2525,'Essonne','91',1076),(2526,'Eure','27',1076),(2527,'Eure-et-Loir','28',1076),(2528,'Finistère','29',1076),(2529,'Gard','30',1076),(2530,'Gers','32',1076),(2531,'Gironde','33',1076),(2532,'Haut-Rhin','68',1076),(2533,'Haute-Corse','20B',1076),(2534,'Haute-Garonne','31',1076),(2535,'Haute-Loire','43',1076),(2536,'Haute-Saône','70',1076),(2537,'Haute-Savoie','74',1076),(2538,'Haute-Vienne','87',1076),(2539,'Hautes-Alpes','05',1076),(2540,'Hautes-Pyrénées','65',1076),(2541,'Hauts-de-Seine','92',1076),(2542,'Hérault','34',1076),(2543,'Indre','36',1076),(2544,'Ille-et-Vilaine','35',1076),(2545,'Indre-et-Loire','37',1076),(2546,'Isère','38',1076),(2547,'Landes','40',1076),(2548,'Loir-et-Cher','41',1076),(2549,'Loire','42',1076),(2550,'Loire-Atlantique','44',1076),(2551,'Loiret','45',1076),(2552,'Lot','46',1076),(2553,'Lot-et-Garonne','47',1076),(2554,'Lozère','48',1076),(2555,'Maine-et-Loire','49',1076),(2556,'Manche','50',1076),(2557,'Marne','51',1076),(2558,'Mayenne','53',1076),(2559,'Meurthe-et-Moselle','54',1076),(2560,'Meuse','55',1076),(2561,'Morbihan','56',1076),(2562,'Moselle','57',1076),(2563,'Nièvre','58',1076),(2564,'Nord','59',1076),(2565,'Oise','60',1076),(2566,'Orne','61',1076),(2567,'Paris','75',1076),(2568,'Pas-de-Calais','62',1076),(2569,'Puy-de-Dôme','63',1076),(2570,'Pyrénées-Atlantiques','64',1076),(2571,'Pyrénées-Orientales','66',1076),(2572,'Rhône','69',1076),(2573,'Saône-et-Loire','71',1076),(2574,'Sarthe','72',1076),(2575,'Savoie','73',1076),(2576,'Seine-et-Marne','77',1076),(2577,'Seine-Maritime','76',1076),(2578,'Seine-Saint-Denis','93',1076),(2579,'Somme','80',1076),(2580,'Tarn','81',1076),(2581,'Tarn-et-Garonne','82',1076),(2582,'Val d\'Oise','95',1076),(2583,'Territoire de Belfort','90',1076),(2584,'Val-de-Marne','94',1076),(2585,'Var','83',1076),(2586,'Vaucluse','84',1076),(2587,'Vendée','85',1076),(2588,'Vienne','86',1076),(2589,'Vosges','88',1076),(2590,'Yonne','89',1076),(2591,'Yvelines','78',1076),(2592,'Aberdeen City','ABE',1226),(2593,'Aberdeenshire','ABD',1226),(2594,'Angus','ANS',1226),(2595,'Co Antrim','ANT',1226),(2597,'Argyll and Bute','AGB',1226),(2598,'Co Armagh','ARM',1226),(2606,'Bedfordshire','BDF',1226),(2612,'Blaenau Gwent','BGW',1226),(2620,'Bristol, City of','BST',1226),(2622,'Buckinghamshire','BKM',1226),(2626,'Cambridgeshire','CAM',1226),(2634,'Cheshire','CHS',1226),(2635,'Clackmannanshire','CLK',1226),(2639,'Cornwall','CON',1226),(2643,'Cumbria','CMA',1226),(2647,'Derbyshire','DBY',1226),(2648,'Co Londonderry','DRY',1226),(2649,'Devon','DEV',1226),(2651,'Dorset','DOR',1226),(2652,'Co Down','DOW',1226),(2654,'Dumfries and Galloway','DGY',1226),(2655,'Dundee City','DND',1226),(2657,'County Durham','DUR',1226),(2659,'East Ayrshire','EAY',1226),(2660,'East Dunbartonshire','EDU',1226),(2661,'East Lothian','ELN',1226),(2662,'East Renfrewshire','ERW',1226),(2663,'East Riding of Yorkshire','ERY',1226),(2664,'East Sussex','ESX',1226),(2665,'Edinburgh, City of','EDH',1226),(2666,'Na h-Eileanan Siar','ELS',1226),(2668,'Essex','ESS',1226),(2669,'Falkirk','FAL',1226),(2670,'Co Fermanagh','FER',1226),(2671,'Fife','FIF',1226),(2674,'Glasgow City','GLG',1226),(2675,'Gloucestershire','GLS',1226),(2678,'Gwynedd','GWN',1226),(2682,'Hampshire','HAM',1226),(2687,'Herefordshire','HEF',1226),(2688,'Hertfordshire','HRT',1226),(2689,'Highland','HED',1226),(2692,'Inverclyde','IVC',1226),(2694,'Isle of Wight','IOW',1226),(2699,'Kent','KEN',1226),(2705,'Lancashire','LAN',1226),(2709,'Leicestershire','LEC',1226),(2712,'Lincolnshire','LIN',1226),(2723,'Midlothian','MLN',1226),(2726,'Moray','MRY',1226),(2734,'Norfolk','NFK',1226),(2735,'North Ayrshire','NAY',1226),(2738,'North Lanarkshire','NLK',1226),(2742,'North Yorkshire','NYK',1226),(2743,'Northamptonshire','NTH',1226),(2744,'Northumberland','NBL',1226),(2746,'Nottinghamshire','NTT',1226),(2747,'Oldham','OLD',1226),(2748,'Omagh','OMH',1226),(2749,'Orkney Islands','ORR',1226),(2750,'Oxfordshire','OXF',1226),(2752,'Perth and Kinross','PKN',1226),(2757,'Powys','POW',1226),(2761,'Renfrewshire','RFW',1226),(2766,'Rutland','RUT',1226),(2770,'Scottish Borders','SCB',1226),(2773,'Shetland Islands','ZET',1226),(2774,'Shropshire','SHR',1226),(2777,'Somerset','SOM',1226),(2778,'South Ayrshire','SAY',1226),(2779,'South Gloucestershire','SGC',1226),(2780,'South Lanarkshire','SLK',1226),(2785,'Staffordshire','STS',1226),(2786,'Stirling','STG',1226),(2791,'Suffolk','SFK',1226),(2793,'Surrey','SRY',1226),(2804,'Vale of Glamorgan, The','VGL',1226),(2811,'Warwickshire','WAR',1226),(2813,'West Dunbartonshire','WDU',1226),(2814,'West Lothian','WLN',1226),(2815,'West Sussex','WSX',1226),(2818,'Wiltshire','WIL',1226),(2823,'Worcestershire','WOR',1226),(2826,'Ashanti','AH',1083),(2827,'Brong-Ahafo','BA',1083),(2828,'Greater Accra','AA',1083),(2829,'Upper East','UE',1083),(2830,'Upper West','UW',1083),(2831,'Volta','TV',1083),(2832,'Banjul','B',1213),(2833,'Lower River','L',1213),(2834,'MacCarthy Island','M',1213),(2835,'North Bank','N',1213),(2836,'Upper River','U',1213),(2837,'Beyla','BE',1091),(2838,'Boffa','BF',1091),(2839,'Boke','BK',1091),(2840,'Coyah','CO',1091),(2841,'Dabola','DB',1091),(2842,'Dalaba','DL',1091),(2843,'Dinguiraye','DI',1091),(2844,'Dubreka','DU',1091),(2845,'Faranah','FA',1091),(2846,'Forecariah','FO',1091),(2847,'Fria','FR',1091),(2848,'Gaoual','GA',1091),(2849,'Guekedou','GU',1091),(2850,'Kankan','KA',1091),(2851,'Kerouane','KE',1091),(2852,'Kindia','KD',1091),(2853,'Kissidougou','KS',1091),(2854,'Koubia','KB',1091),(2855,'Koundara','KN',1091),(2856,'Kouroussa','KO',1091),(2857,'Labe','LA',1091),(2858,'Lelouma','LE',1091),(2859,'Lola','LO',1091),(2860,'Macenta','MC',1091),(2861,'Mali','ML',1091),(2862,'Mamou','MM',1091),(2863,'Mandiana','MD',1091),(2864,'Nzerekore','NZ',1091),(2865,'Pita','PI',1091),(2866,'Siguiri','SI',1091),(2867,'Telimele','TE',1091),(2868,'Tougue','TO',1091),(2869,'Yomou','YO',1091),(2870,'Region Continental','C',1067),(2871,'Region Insular','I',1067),(2872,'Annobon','AN',1067),(2873,'Bioko Norte','BN',1067),(2874,'Bioko Sur','BS',1067),(2875,'Centro Sur','CS',1067),(2876,'Kie-Ntem','KN',1067),(2877,'Litoral','LI',1067),(2878,'Wele-Nzas','WN',1067),(2879,'Achaïa','13',1085),(2880,'Aitolia-Akarnania','01',1085),(2881,'Argolis','11',1085),(2882,'Arkadia','12',1085),(2883,'Arta','31',1085),(2884,'Attiki','A1',1085),(2885,'Chalkidiki','64',1085),(2886,'Chania','94',1085),(2887,'Chios','85',1085),(2888,'Dodekanisos','81',1085),(2889,'Drama','52',1085),(2890,'Evros','71',1085),(2891,'Evrytania','05',1085),(2892,'Evvoia','04',1085),(2893,'Florina','63',1085),(2894,'Fokis','07',1085),(2895,'Fthiotis','06',1085),(2896,'Grevena','51',1085),(2897,'Ileia','14',1085),(2898,'Imathia','53',1085),(2899,'Ioannina','33',1085),(2900,'Irakleion','91',1085),(2901,'Karditsa','41',1085),(2902,'Kastoria','56',1085),(2903,'Kavalla','55',1085),(2904,'Kefallinia','23',1085),(2905,'Kerkyra','22',1085),(2906,'Kilkis','57',1085),(2907,'Korinthia','15',1085),(2908,'Kozani','58',1085),(2909,'Kyklades','82',1085),(2910,'Lakonia','16',1085),(2911,'Larisa','42',1085),(2912,'Lasithion','92',1085),(2913,'Lefkas','24',1085),(2914,'Lesvos','83',1085),(2915,'Magnisia','43',1085),(2916,'Messinia','17',1085),(2917,'Pella','59',1085),(2918,'Preveza','34',1085),(2919,'Rethymnon','93',1085),(2920,'Rodopi','73',1085),(2921,'Samos','84',1085),(2922,'Serrai','62',1085),(2923,'Thesprotia','32',1085),(2924,'Thessaloniki','54',1085),(2925,'Trikala','44',1085),(2926,'Voiotia','03',1085),(2927,'Xanthi','72',1085),(2928,'Zakynthos','21',1085),(2929,'Agio Oros','69',1085),(2930,'Alta Verapaz','AV',1090),(2931,'Baja Verapaz','BV',1090),(2932,'Chimaltenango','CM',1090),(2933,'Chiquimula','CQ',1090),(2934,'El Progreso','PR',1090),(2935,'Escuintla','ES',1090),(2936,'Guatemala','GU',1090),(2937,'Huehuetenango','HU',1090),(2938,'Izabal','IZ',1090),(2939,'Jalapa','JA',1090),(2940,'Jutiapa','JU',1090),(2941,'Peten','PE',1090),(2942,'Quetzaltenango','QZ',1090),(2943,'Quiche','QC',1090),(2944,'Retalhuleu','RE',1090),(2945,'Sacatepequez','SA',1090),(2946,'San Marcos','SM',1090),(2947,'Santa Rosa','SR',1090),(2948,'Sololá','SO',1090),(2949,'Suchitepequez','SU',1090),(2950,'Totonicapan','TO',1090),(2951,'Zacapa','ZA',1090),(2952,'Bissau','BS',1092),(2953,'Bafata','BA',1092),(2954,'Biombo','BM',1092),(2955,'Bolama','BL',1092),(2956,'Cacheu','CA',1092),(2957,'Gabu','GA',1092),(2958,'Oio','OI',1092),(2959,'Quloara','QU',1092),(2960,'Tombali S','TO',1092),(2961,'Barima-Waini','BA',1093),(2962,'Cuyuni-Mazaruni','CU',1093),(2963,'Demerara-Mahaica','DE',1093),(2964,'East Berbice-Corentyne','EB',1093),(2965,'Essequibo Islands-West Demerara','ES',1093),(2966,'Mahaica-Berbice','MA',1093),(2967,'Pomeroon-Supenaam','PM',1093),(2968,'Potaro-Siparuni','PT',1093),(2969,'Upper Demerara-Berbice','UD',1093),(2970,'Upper Takutu-Upper Essequibo','UT',1093),(2971,'Atlantida','AT',1097),(2972,'Colon','CL',1097),(2973,'Comayagua','CM',1097),(2974,'Copan','CP',1097),(2975,'Cortes','CR',1097),(2976,'Choluteca','CH',1097),(2977,'El Paraiso','EP',1097),(2978,'Francisco Morazan','FM',1097),(2979,'Gracias a Dios','GD',1097),(2980,'Intibuca','IN',1097),(2981,'Islas de la Bahia','IB',1097),(2982,'Lempira','LE',1097),(2983,'Ocotepeque','OC',1097),(2984,'Olancho','OL',1097),(2985,'Santa Barbara','SB',1097),(2986,'Valle','VA',1097),(2987,'Yoro','YO',1097),(2988,'Bjelovarsko-bilogorska zupanija','07',1055),(2989,'Brodsko-posavska zupanija','12',1055),(2990,'Dubrovacko-neretvanska zupanija','19',1055),(2991,'Istarska zupanija','18',1055),(2992,'Karlovacka zupanija','04',1055),(2993,'Koprivnickco-krizevacka zupanija','06',1055),(2994,'Krapinako-zagorska zupanija','02',1055),(2995,'Licko-senjska zupanija','09',1055),(2996,'Medimurska zupanija','20',1055),(2997,'Osjecko-baranjska zupanija','14',1055),(2998,'Pozesko-slavonska zupanija','11',1055),(2999,'Primorsko-goranska zupanija','08',1055),(3000,'Sisacko-moelavacka Iupanija','03',1055),(3001,'Splitako-dalmatinska zupanija','17',1055),(3002,'Sibenako-kninska zupanija','15',1055),(3003,'Varaidinska zupanija','05',1055),(3004,'VirovitiEko-podravska zupanija','10',1055),(3005,'VuRovarako-srijemska zupanija','16',1055),(3006,'Zadaraka','13',1055),(3007,'Zagrebacka zupanija','01',1055),(3008,'Grande-Anse','GA',1094),(3009,'Nord-Est','NE',1094),(3010,'Nord-Ouest','NO',1094),(3011,'Ouest','OU',1094),(3012,'Sud','SD',1094),(3013,'Sud-Est','SE',1094),(3014,'Budapest','BU',1099),(3015,'Bács-Kiskun','BK',1099),(3016,'Baranya','BA',1099),(3017,'Békés','BE',1099),(3018,'Borsod-Abaúj-Zemplén','BZ',1099),(3019,'Csongrád','CS',1099),(3020,'Fejér','FE',1099),(3021,'Győr-Moson-Sopron','GS',1099),(3022,'Hajdu-Bihar','HB',1099),(3023,'Heves','HE',1099),(3024,'Jász-Nagykun-Szolnok','JN',1099),(3025,'Komárom-Esztergom','KE',1099),(3026,'Nográd','NO',1099),(3027,'Pest','PE',1099),(3028,'Somogy','SO',1099),(3029,'Szabolcs-Szatmár-Bereg','SZ',1099),(3030,'Tolna','TO',1099),(3031,'Vas','VA',1099),(3032,'Veszprém','VE',1099),(3033,'Zala','ZA',1099),(3034,'Békéscsaba','BC',1099),(3035,'Debrecen','DE',1099),(3036,'Dunaújváros','DU',1099),(3037,'Eger','EG',1099),(3038,'Győr','GY',1099),(3039,'Hódmezővásárhely','HV',1099),(3040,'Kaposvár','KV',1099),(3041,'Kecskemét','KM',1099),(3042,'Miskolc','MI',1099),(3043,'Nagykanizsa','NK',1099),(3044,'Nyiregyháza','NY',1099),(3045,'Pécs','PS',1099),(3046,'Salgótarján','ST',1099),(3047,'Sopron','SN',1099),(3048,'Szeged','SD',1099),(3049,'Székesfehérvár','SF',1099),(3050,'Szekszárd','SS',1099),(3051,'Szolnok','SK',1099),(3052,'Szombathely','SH',1099),(3053,'Tatabánya','TB',1099),(3054,'Zalaegerszeg','ZE',1099),(3055,'Bali','BA',1102),(3056,'Kepulauan Bangka Belitung','BB',1102),(3057,'Banten','BT',1102),(3058,'Bengkulu','BE',1102),(3059,'Gorontalo','GO',1102),(3060,'Papua Barat','PB',1102),(3061,'Jambi','JA',1102),(3062,'Jawa Barat','JB',1102),(3063,'Jawa Tengah','JT',1102),(3064,'Jawa Timur','JI',1102),(3065,'Kalimantan Barat','KB',1102),(3066,'Kalimantan Timur','KI',1102),(3067,'Kalimantan Selatan','KS',1102),(3068,'Kepulauan Riau','KR',1102),(3069,'Lampung','LA',1102),(3070,'Maluku','MA',1102),(3071,'Maluku Utara','MU',1102),(3072,'Nusa Tenggara Barat','NB',1102),(3073,'Nusa Tenggara Timur','NT',1102),(3074,'Papua','PA',1102),(3075,'Riau','RI',1102),(3076,'Sulawesi Selatan','SN',1102),(3077,'Sulawesi Tengah','ST',1102),(3078,'Sulawesi Tenggara','SG',1102),(3079,'Sulawesi Utara','SA',1102),(3080,'Sumatra Barat','SB',1102),(3081,'Sumatra Selatan','SS',1102),(3082,'Sumatera Utara','SU',1102),(3083,'DKI Jakarta','JK',1102),(3084,'Aceh','AC',1102),(3085,'DI Yogyakarta','YO',1102),(3086,'Cork','C',1105),(3087,'Clare','CE',1105),(3088,'Cavan','CN',1105),(3089,'Carlow','CW',1105),(3090,'Dublin','D',1105),(3091,'Donegal','DL',1105),(3092,'Galway','G',1105),(3093,'Kildare','KE',1105),(3094,'Kilkenny','KK',1105),(3095,'Kerry','KY',1105),(3096,'Longford','LD',1105),(3097,'Louth','LH',1105),(3098,'Limerick','LK',1105),(3099,'Leitrim','LM',1105),(3100,'Laois','LS',1105),(3101,'Meath','MH',1105),(3102,'Monaghan','MN',1105),(3103,'Mayo','MO',1105),(3104,'Offaly','OY',1105),(3105,'Roscommon','RN',1105),(3106,'Sligo','SO',1105),(3107,'Tipperary','TA',1105),(3108,'Waterford','WD',1105),(3109,'Westmeath','WH',1105),(3110,'Wicklow','WW',1105),(3111,'Wexford','WX',1105),(3112,'HaDarom','D',1106),(3113,'HaMerkaz','M',1106),(3114,'HaZafon','Z',1106),(3115,'Haifa','HA',1106),(3116,'Tel-Aviv','TA',1106),(3117,'Jerusalem','JM',1106),(3118,'Al Anbar','AN',1104),(3119,'Al Ba,rah','BA',1104),(3120,'Al Muthanna','MU',1104),(3121,'Al Qadisiyah','QA',1104),(3122,'An Najef','NA',1104),(3123,'Arbil','AR',1104),(3124,'As Sulaymaniyah','SW',1104),(3125,'At Ta\'mim','TS',1104),(3126,'Babil','BB',1104),(3127,'Baghdad','BG',1104),(3128,'Dahuk','DA',1104),(3129,'Dhi Qar','DQ',1104),(3130,'Diyala','DI',1104),(3131,'Karbala\'','KA',1104),(3132,'Maysan','MA',1104),(3133,'Ninawa','NI',1104),(3134,'Salah ad Din','SD',1104),(3135,'Wasit','WA',1104),(3136,'Ardabil','03',1103),(3137,'Azarbayjan-e Gharbi','02',1103),(3138,'Azarbayjan-e Sharqi','01',1103),(3139,'Bushehr','06',1103),(3140,'Chahar Mahall va Bakhtiari','08',1103),(3141,'Esfahan','04',1103),(3142,'Fars','14',1103),(3143,'Gilan','19',1103),(3144,'Golestan','27',1103),(3145,'Hamadan','24',1103),(3146,'Hormozgan','23',1103),(3147,'Iiam','05',1103),(3148,'Kerman','15',1103),(3149,'Kermanshah','17',1103),(3150,'Khorasan','09',1103),(3151,'Khuzestan','10',1103),(3152,'Kohjiluyeh va Buyer Ahmad','18',1103),(3153,'Kordestan','16',1103),(3154,'Lorestan','20',1103),(3155,'Markazi','22',1103),(3156,'Mazandaran','21',1103),(3157,'Qazvin','28',1103),(3158,'Qom','26',1103),(3159,'Semnan','12',1103),(3160,'Sistan va Baluchestan','13',1103),(3161,'Tehran','07',1103),(3162,'Yazd','25',1103),(3163,'Zanjan','11',1103),(3164,'Austurland','7',1100),(3165,'Hofuoborgarsvaeoi utan Reykjavikur','1',1100),(3166,'Norourland eystra','6',1100),(3167,'Norourland vestra','5',1100),(3168,'Reykjavik','0',1100),(3169,'Suourland','8',1100),(3170,'Suournes','2',1100),(3171,'Vestfirolr','4',1100),(3172,'Vesturland','3',1100),(3173,'Agrigento','AG',1107),(3174,'Alessandria','AL',1107),(3175,'Ancona','AN',1107),(3176,'Aosta','AO',1107),(3177,'Arezzo','AR',1107),(3178,'Ascoli Piceno','AP',1107),(3179,'Asti','AT',1107),(3180,'Avellino','AV',1107),(3181,'Bari','BA',1107),(3182,'Belluno','BL',1107),(3183,'Benevento','BN',1107),(3184,'Bergamo','BG',1107),(3185,'Biella','BI',1107),(3186,'Bologna','BO',1107),(3187,'Bolzano','BZ',1107),(3188,'Brescia','BS',1107),(3189,'Brindisi','BR',1107),(3190,'Cagliari','CA',1107),(3191,'Caltanissetta','CL',1107),(3192,'Campobasso','CB',1107),(3193,'Caserta','CE',1107),(3194,'Catania','CT',1107),(3195,'Catanzaro','CZ',1107),(3196,'Chieti','CH',1107),(3197,'Como','CO',1107),(3198,'Cosenza','CS',1107),(3199,'Cremona','CR',1107),(3200,'Crotone','KR',1107),(3201,'Cuneo','CN',1107),(3202,'Enna','EN',1107),(3203,'Ferrara','FE',1107),(3204,'Firenze','FI',1107),(3205,'Foggia','FG',1107),(3206,'Forlì-Cesena','FC',1107),(3207,'Frosinone','FR',1107),(3208,'Genova','GE',1107),(3209,'Gorizia','GO',1107),(3210,'Grosseto','GR',1107),(3211,'Imperia','IM',1107),(3212,'Isernia','IS',1107),(3213,'L\'Aquila','AQ',1107),(3214,'La Spezia','SP',1107),(3215,'Latina','LT',1107),(3216,'Lecce','LE',1107),(3217,'Lecco','LC',1107),(3218,'Livorno','LI',1107),(3219,'Lodi','LO',1107),(3220,'Lucca','LU',1107),(3221,'Macerata','MC',1107),(3222,'Mantova','MN',1107),(3223,'Massa-Carrara','MS',1107),(3224,'Matera','MT',1107),(3225,'Messina','ME',1107),(3226,'Milano','MI',1107),(3227,'Modena','MO',1107),(3228,'Napoli','NA',1107),(3229,'Novara','NO',1107),(3230,'Nuoro','NU',1107),(3231,'Oristano','OR',1107),(3232,'Padova','PD',1107),(3233,'Palermo','PA',1107),(3234,'Parma','PR',1107),(3235,'Pavia','PV',1107),(3236,'Perugia','PG',1107),(3237,'Pesaro e Urbino','PU',1107),(3238,'Pescara','PE',1107),(3239,'Piacenza','PC',1107),(3240,'Pisa','PI',1107),(3241,'Pistoia','PT',1107),(3242,'Pordenone','PN',1107),(3243,'Potenza','PZ',1107),(3244,'Prato','PO',1107),(3245,'Ragusa','RG',1107),(3246,'Ravenna','RA',1107),(3247,'Reggio Calabria','RC',1107),(3248,'Reggio Emilia','RE',1107),(3249,'Rieti','RI',1107),(3250,'Rimini','RN',1107),(3251,'Roma','RM',1107),(3252,'Rovigo','RO',1107),(3253,'Salerno','SA',1107),(3254,'Sassari','SS',1107),(3255,'Savona','SV',1107),(3256,'Siena','SI',1107),(3257,'Siracusa','SR',1107),(3258,'Sondrio','SO',1107),(3259,'Taranto','TA',1107),(3260,'Teramo','TE',1107),(3261,'Terni','TR',1107),(3262,'Torino','TO',1107),(3263,'Trapani','TP',1107),(3264,'Trento','TN',1107),(3265,'Treviso','TV',1107),(3266,'Trieste','TS',1107),(3267,'Udine','UD',1107),(3268,'Varese','VA',1107),(3269,'Venezia','VE',1107),(3270,'Verbano-Cusio-Ossola','VB',1107),(3271,'Vercelli','VC',1107),(3272,'Verona','VR',1107),(3273,'Vibo Valentia','VV',1107),(3274,'Vicenza','VI',1107),(3275,'Viterbo','VT',1107),(3276,'Aichi','23',1109),(3277,'Akita','05',1109),(3278,'Aomori','02',1109),(3279,'Chiba','12',1109),(3280,'Ehime','38',1109),(3281,'Fukui','18',1109),(3282,'Fukuoka','40',1109),(3283,'Fukusima','07',1109),(3284,'Gifu','21',1109),(3285,'Gunma','10',1109),(3286,'Hiroshima','34',1109),(3287,'Hokkaido','01',1109),(3288,'Hyogo','28',1109),(3289,'Ibaraki','08',1109),(3290,'Ishikawa','17',1109),(3291,'Iwate','03',1109),(3292,'Kagawa','37',1109),(3293,'Kagoshima','46',1109),(3294,'Kanagawa','14',1109),(3295,'Kochi','39',1109),(3296,'Kumamoto','43',1109),(3297,'Kyoto','26',1109),(3298,'Mie','24',1109),(3299,'Miyagi','04',1109),(3300,'Miyazaki','45',1109),(3301,'Nagano','20',1109),(3302,'Nagasaki','42',1109),(3303,'Nara','29',1109),(3304,'Niigata','15',1109),(3305,'Oita','44',1109),(3306,'Okayama','33',1109),(3307,'Okinawa','47',1109),(3308,'Osaka','27',1109),(3309,'Saga','41',1109),(3310,'Saitama','11',1109),(3311,'Shiga','25',1109),(3312,'Shimane','32',1109),(3313,'Shizuoka','22',1109),(3314,'Tochigi','09',1109),(3315,'Tokushima','36',1109),(3316,'Tokyo','13',1109),(3317,'Tottori','31',1109),(3318,'Toyama','16',1109),(3319,'Wakayama','30',1109),(3320,'Yamagata','06',1109),(3321,'Yamaguchi','35',1109),(3322,'Yamanashi','19',1109),(3323,'Clarendon','CN',1108),(3324,'Hanover','HR',1108),(3325,'Kingston','KN',1108),(3326,'Portland','PD',1108),(3327,'Saint Andrew','AW',1108),(3328,'Saint Ann','AN',1108),(3329,'Saint Catherine','CE',1108),(3330,'Saint Elizabeth','EH',1108),(3331,'Saint James','JS',1108),(3332,'Saint Mary','MY',1108),(3333,'Saint Thomas','TS',1108),(3334,'Trelawny','TY',1108),(3335,'Westmoreland','WD',1108),(3336,'Ajln','AJ',1110),(3337,'Al \'Aqaba','AQ',1110),(3338,'Al Balqa\'','BA',1110),(3339,'Al Karak','KA',1110),(3340,'Al Mafraq','MA',1110),(3341,'Amman','AM',1110),(3342,'At Tafilah','AT',1110),(3343,'Az Zarga','AZ',1110),(3344,'Irbid','JR',1110),(3345,'Jarash','JA',1110),(3346,'Ma\'an','MN',1110),(3347,'Madaba','MD',1110),(3353,'Bishkek','GB',1117),(3354,'Batken','B',1117),(3355,'Chu','C',1117),(3356,'Jalal-Abad','J',1117),(3357,'Naryn','N',1117),(3358,'Osh','O',1117),(3359,'Talas','T',1117),(3360,'Ysyk-Kol','Y',1117),(3361,'Krong Kaeb','23',1037),(3362,'Krong Pailin','24',1037),(3363,'Xrong Preah Sihanouk','18',1037),(3364,'Phnom Penh','12',1037),(3365,'Baat Dambang','2',1037),(3366,'Banteay Mean Chey','1',1037),(3367,'Rampong Chaam','3',1037),(3368,'Kampong Chhnang','4',1037),(3369,'Kampong Spueu','5',1037),(3370,'Kampong Thum','6',1037),(3371,'Kampot','7',1037),(3372,'Kandaal','8',1037),(3373,'Kach Kong','9',1037),(3374,'Krachoh','10',1037),(3375,'Mondol Kiri','11',1037),(3376,'Otdar Mean Chey','22',1037),(3377,'Pousaat','15',1037),(3378,'Preah Vihear','13',1037),(3379,'Prey Veaeng','14',1037),(3380,'Rotanak Kiri','16',1037),(3381,'Siem Reab','17',1037),(3382,'Stueng Traeng','19',1037),(3383,'Svaay Rieng','20',1037),(3384,'Taakaev','21',1037),(3385,'Gilbert Islands','G',1113),(3386,'Line Islands','L',1113),(3387,'Phoenix Islands','P',1113),(3388,'Anjouan Ndzouani','A',1049),(3389,'Grande Comore Ngazidja','G',1049),(3390,'Moheli Moili','M',1049),(3391,'Kaesong-si','KAE',1114),(3392,'Nampo-si','NAM',1114),(3393,'Pyongyang-ai','PYO',1114),(3394,'Chagang-do','CHA',1114),(3395,'Hamgyongbuk-do','HAB',1114),(3396,'Hamgyongnam-do','HAN',1114),(3397,'Hwanghaebuk-do','HWB',1114),(3398,'Hwanghaenam-do','HWN',1114),(3399,'Kangwon-do','KAN',1114),(3400,'Pyonganbuk-do','PYB',1114),(3401,'Pyongannam-do','PYN',1114),(3402,'Yanggang-do','YAN',1114),(3403,'Najin Sonbong-si','NAJ',1114),(3404,'Seoul Teugbyeolsi','11',1115),(3405,'Busan Gwang\'yeogsi','26',1115),(3406,'Daegu Gwang\'yeogsi','27',1115),(3407,'Daejeon Gwang\'yeogsi','30',1115),(3408,'Gwangju Gwang\'yeogsi','29',1115),(3409,'Incheon Gwang\'yeogsi','28',1115),(3410,'Ulsan Gwang\'yeogsi','31',1115),(3411,'Chungcheongbugdo','43',1115),(3412,'Chungcheongnamdo','44',1115),(3413,'Gang\'weondo','42',1115),(3414,'Gyeonggido','41',1115),(3415,'Gyeongsangbugdo','47',1115),(3416,'Gyeongsangnamdo','48',1115),(3417,'Jejudo','49',1115),(3418,'Jeonrabugdo','45',1115),(3419,'Jeonranamdo','46',1115),(3420,'Al Ahmadi','AH',1116),(3421,'Al Farwanlyah','FA',1116),(3422,'Al Jahrah','JA',1116),(3423,'Al Kuwayt','KU',1116),(3424,'Hawalli','HA',1116),(3425,'Almaty','ALA',1111),(3426,'Astana','AST',1111),(3427,'Almaty oblysy','ALM',1111),(3428,'Aqmola oblysy','AKM',1111),(3429,'Aqtobe oblysy','AKT',1111),(3430,'Atyrau oblyfiy','ATY',1111),(3431,'Batys Quzaqstan oblysy','ZAP',1111),(3432,'Mangghystau oblysy','MAN',1111),(3433,'Ongtustik Quzaqstan oblysy','YUZ',1111),(3434,'Pavlodar oblysy','PAV',1111),(3435,'Qaraghandy oblysy','KAR',1111),(3436,'Qostanay oblysy','KUS',1111),(3437,'Qyzylorda oblysy','KZY',1111),(3438,'Shyghys Quzaqstan oblysy','VOS',1111),(3439,'Soltustik Quzaqstan oblysy','SEV',1111),(3440,'Zhambyl oblysy Zhambylskaya oblast\'','ZHA',1111),(3441,'Vientiane','VT',1118),(3442,'Attapu','AT',1118),(3443,'Bokeo','BK',1118),(3444,'Bolikhamxai','BL',1118),(3445,'Champasak','CH',1118),(3446,'Houaphan','HO',1118),(3447,'Khammouan','KH',1118),(3448,'Louang Namtha','LM',1118),(3449,'Louangphabang','LP',1118),(3450,'Oudomxai','OU',1118),(3451,'Phongsali','PH',1118),(3452,'Salavan','SL',1118),(3453,'Savannakhet','SV',1118),(3454,'Xaignabouli','XA',1118),(3455,'Xiasomboun','XN',1118),(3456,'Xekong','XE',1118),(3457,'Xiangkhoang','XI',1118),(3458,'Beirut','BA',1120),(3459,'Beqaa','BI',1120),(3460,'Mount Lebanon','JL',1120),(3461,'North Lebanon','AS',1120),(3462,'South Lebanon','JA',1120),(3463,'Nabatieh','NA',1120),(3464,'Ampara','52',1199),(3465,'Anuradhapura','71',1199),(3466,'Badulla','81',1199),(3467,'Batticaloa','51',1199),(3468,'Colombo','11',1199),(3469,'Galle','31',1199),(3470,'Gampaha','12',1199),(3471,'Hambantota','33',1199),(3472,'Jaffna','41',1199),(3473,'Kalutara','13',1199),(3474,'Kandy','21',1199),(3475,'Kegalla','92',1199),(3476,'Kilinochchi','42',1199),(3477,'Kurunegala','61',1199),(3478,'Mannar','43',1199),(3479,'Matale','22',1199),(3480,'Matara','32',1199),(3481,'Monaragala','82',1199),(3482,'Mullaittivu','45',1199),(3483,'Nuwara Eliya','23',1199),(3484,'Polonnaruwa','72',1199),(3485,'Puttalum','62',1199),(3486,'Ratnapura','91',1199),(3487,'Trincomalee','53',1199),(3488,'VavunLya','44',1199),(3489,'Bomi','BM',1122),(3490,'Bong','BG',1122),(3491,'Grand Basaa','GB',1122),(3492,'Grand Cape Mount','CM',1122),(3493,'Grand Gedeh','GG',1122),(3494,'Grand Kru','GK',1122),(3495,'Lofa','LO',1122),(3496,'Margibi','MG',1122),(3497,'Maryland','MY',1122),(3498,'Montserrado','MO',1122),(3499,'Nimba','NI',1122),(3500,'Rivercess','RI',1122),(3501,'Sinoe','SI',1122),(3502,'Berea','D',1121),(3503,'Butha-Buthe','B',1121),(3504,'Leribe','C',1121),(3505,'Mafeteng','E',1121),(3506,'Maseru','A',1121),(3507,'Mohale\'s Hoek','F',1121),(3508,'Mokhotlong','J',1121),(3509,'Qacha\'s Nek','H',1121),(3510,'Quthing','G',1121),(3511,'Thaba-Tseka','K',1121),(3512,'Alytaus Apskritis','AL',1125),(3513,'Kauno Apskritis','KU',1125),(3514,'Klaipėdos Apskritis','KL',1125),(3515,'Marijampolės Apskritis','MR',1125),(3516,'Panevėžio Apskritis','PN',1125),(3517,'Šiaulių Apskritis','SA',1125),(3518,'Tauragės Apskritis','TA',1125),(3519,'Telšių Apskritis','TE',1125),(3520,'Utenos Apskritis','UT',1125),(3521,'Vilniaus Apskritis','VL',1125),(3522,'Diekirch','D',1126),(3523,'GreveNmacher','G',1126),(3550,'Daugavpils','DGV',1119),(3551,'Jelgava','JEL',1119),(3552,'Jūrmala','JUR',1119),(3553,'Liepāja','LPX',1119),(3554,'Rēzekne','REZ',1119),(3555,'Rīga','RIX',1119),(3556,'Ventspils','VEN',1119),(3557,'Ajdābiyā','AJ',1123),(3558,'Al Buţnān','BU',1123),(3559,'Al Hizām al Akhdar','HZ',1123),(3560,'Al Jabal al Akhdar','JA',1123),(3561,'Al Jifārah','JI',1123),(3562,'Al Jufrah','JU',1123),(3563,'Al Kufrah','KF',1123),(3564,'Al Marj','MJ',1123),(3565,'Al Marqab','MB',1123),(3566,'Al Qaţrūn','QT',1123),(3567,'Al Qubbah','QB',1123),(3568,'Al Wāhah','WA',1123),(3569,'An Nuqaţ al Khams','NQ',1123),(3570,'Ash Shāţi\'','SH',1123),(3571,'Az Zāwiyah','ZA',1123),(3572,'Banghāzī','BA',1123),(3573,'Banī Walīd','BW',1123),(3574,'Darnah','DR',1123),(3575,'Ghadāmis','GD',1123),(3576,'Gharyān','GR',1123),(3577,'Ghāt','GT',1123),(3578,'Jaghbūb','JB',1123),(3579,'Mişrātah','MI',1123),(3580,'Mizdah','MZ',1123),(3581,'Murzuq','MQ',1123),(3582,'Nālūt','NL',1123),(3583,'Sabhā','SB',1123),(3584,'Şabrātah Şurmān','SS',1123),(3585,'Surt','SR',1123),(3586,'Tājūrā\' wa an Nawāhī al Arbāh','TN',1123),(3587,'Ţarābulus','TB',1123),(3588,'Tarhūnah-Masallātah','TM',1123),(3589,'Wādī al hayāt','WD',1123),(3590,'Yafran-Jādū','YJ',1123),(3591,'Agadir','AGD',1146),(3592,'Aït Baha','BAH',1146),(3593,'Aït Melloul','MEL',1146),(3594,'Al Haouz','HAO',1146),(3595,'Al Hoceïma','HOC',1146),(3596,'Assa-Zag','ASZ',1146),(3597,'Azilal','AZI',1146),(3598,'Beni Mellal','BEM',1146),(3599,'Ben Sllmane','BES',1146),(3600,'Berkane','BER',1146),(3601,'Boujdour','BOD',1146),(3602,'Boulemane','BOM',1146),(3603,'Casablanca [Dar el Beïda]','CAS',1146),(3604,'Chefchaouene','CHE',1146),(3605,'Chichaoua','CHI',1146),(3606,'El Hajeb','HAJ',1146),(3607,'El Jadida','JDI',1146),(3608,'Errachidia','ERR',1146),(3609,'Essaouira','ESI',1146),(3610,'Es Smara','ESM',1146),(3611,'Fès','FES',1146),(3612,'Figuig','FIG',1146),(3613,'Guelmim','GUE',1146),(3614,'Ifrane','IFR',1146),(3615,'Jerada','JRA',1146),(3616,'Kelaat Sraghna','KES',1146),(3617,'Kénitra','KEN',1146),(3618,'Khemisaet','KHE',1146),(3619,'Khenifra','KHN',1146),(3620,'Khouribga','KHO',1146),(3621,'Laâyoune (EH)','LAA',1146),(3622,'Larache','LAP',1146),(3623,'Marrakech','MAR',1146),(3624,'Meknsès','MEK',1146),(3625,'Nador','NAD',1146),(3626,'Ouarzazate','OUA',1146),(3627,'Oued ed Dahab (EH)','OUD',1146),(3628,'Oujda','OUJ',1146),(3629,'Rabat-Salé','RBA',1146),(3630,'Safi','SAF',1146),(3631,'Sefrou','SEF',1146),(3632,'Settat','SET',1146),(3633,'Sidl Kacem','SIK',1146),(3634,'Tanger','TNG',1146),(3635,'Tan-Tan','TNT',1146),(3636,'Taounate','TAO',1146),(3637,'Taroudannt','TAR',1146),(3638,'Tata','TAT',1146),(3639,'Taza','TAZ',1146),(3640,'Tétouan','TET',1146),(3641,'Tiznit','TIZ',1146),(3642,'Gagauzia, Unitate Teritoriala Autonoma','GA',1142),(3643,'Chisinau','CU',1142),(3644,'Stinga Nistrului, unitatea teritoriala din','SN',1142),(3645,'Balti','BA',1142),(3646,'Cahul','CA',1142),(3647,'Edinet','ED',1142),(3648,'Lapusna','LA',1142),(3649,'Orhei','OR',1142),(3650,'Soroca','SO',1142),(3651,'Taraclia','TA',1142),(3652,'Tighina [Bender]','TI',1142),(3653,'Ungheni','UN',1142),(3654,'Antananarivo','T',1129),(3655,'Antsiranana','D',1129),(3656,'Fianarantsoa','F',1129),(3657,'Mahajanga','M',1129),(3658,'Toamasina','A',1129),(3659,'Toliara','U',1129),(3660,'Ailinglapalap','ALL',1135),(3661,'Ailuk','ALK',1135),(3662,'Arno','ARN',1135),(3663,'Aur','AUR',1135),(3664,'Ebon','EBO',1135),(3665,'Eniwetok','ENI',1135),(3666,'Jaluit','JAL',1135),(3667,'Kili','KIL',1135),(3668,'Kwajalein','KWA',1135),(3669,'Lae','LAE',1135),(3670,'Lib','LIB',1135),(3671,'Likiep','LIK',1135),(3672,'Majuro','MAJ',1135),(3673,'Maloelap','MAL',1135),(3674,'Mejit','MEJ',1135),(3675,'Mili','MIL',1135),(3676,'Namorik','NMK',1135),(3677,'Namu','NMU',1135),(3678,'Rongelap','RON',1135),(3679,'Ujae','UJA',1135),(3680,'Ujelang','UJL',1135),(3681,'Utirik','UTI',1135),(3682,'Wotho','WTN',1135),(3683,'Wotje','WTJ',1135),(3684,'Bamako','BK0',1133),(3685,'Gao','7',1133),(3686,'Kayes','1',1133),(3687,'Kidal','8',1133),(3688,'Xoulikoro','2',1133),(3689,'Mopti','5',1133),(3690,'S69ou','4',1133),(3691,'Sikasso','3',1133),(3692,'Tombouctou','6',1133),(3693,'Ayeyarwady','07',1035),(3694,'Bago','02',1035),(3695,'Magway','03',1035),(3696,'Mandalay','04',1035),(3697,'Sagaing','01',1035),(3698,'Tanintharyi','05',1035),(3699,'Yangon','06',1035),(3700,'Chin','14',1035),(3701,'Kachin','11',1035),(3702,'Kayah','12',1035),(3703,'Kayin','13',1035),(3704,'Mon','15',1035),(3705,'Rakhine','16',1035),(3706,'Shan','17',1035),(3707,'Ulaanbaatar','1',1144),(3708,'Arhangay','073',1144),(3709,'Bayanhongor','069',1144),(3710,'Bayan-Olgiy','071',1144),(3711,'Bulgan','067',1144),(3712,'Darhan uul','037',1144),(3713,'Dornod','061',1144),(3714,'Dornogov,','063',1144),(3715,'DundgovL','059',1144),(3716,'Dzavhan','057',1144),(3717,'Govi-Altay','065',1144),(3718,'Govi-Smber','064',1144),(3719,'Hentiy','039',1144),(3720,'Hovd','043',1144),(3721,'Hovsgol','041',1144),(3722,'Omnogovi','053',1144),(3723,'Orhon','035',1144),(3724,'Ovorhangay','055',1144),(3725,'Selenge','049',1144),(3726,'Shbaatar','051',1144),(3727,'Tov','047',1144),(3728,'Uvs','046',1144),(3729,'Nouakchott','NKC',1137),(3730,'Assaba','03',1137),(3731,'Brakna','05',1137),(3732,'Dakhlet Nouadhibou','08',1137),(3733,'Gorgol','04',1137),(3734,'Guidimaka','10',1137),(3735,'Hodh ech Chargui','01',1137),(3736,'Hodh el Charbi','02',1137),(3737,'Inchiri','12',1137),(3738,'Tagant','09',1137),(3739,'Tiris Zemmour','11',1137),(3740,'Trarza','06',1137),(3741,'Beau Bassin-Rose Hill','BR',1138),(3742,'Curepipe','CU',1138),(3743,'Port Louis','PU',1138),(3744,'Quatre Bornes','QB',1138),(3745,'Vacosa-Phoenix','VP',1138),(3746,'Black River','BL',1138),(3747,'Flacq','FL',1138),(3748,'Grand Port','GP',1138),(3749,'Moka','MO',1138),(3750,'Pamplemousses','PA',1138),(3751,'Plaines Wilhems','PW',1138),(3752,'Riviere du Rempart','RP',1138),(3753,'Savanne','SA',1138),(3754,'Agalega Islands','AG',1138),(3755,'Cargados Carajos Shoals','CC',1138),(3756,'Rodrigues Island','RO',1138),(3757,'Male','MLE',1132),(3758,'Alif','02',1132),(3759,'Baa','20',1132),(3760,'Dhaalu','17',1132),(3761,'Faafu','14',1132),(3762,'Gaaf Alif','27',1132),(3763,'Gaefu Dhaalu','28',1132),(3764,'Gnaviyani','29',1132),(3765,'Haa Alif','07',1132),(3766,'Haa Dhaalu','23',1132),(3767,'Kaafu','26',1132),(3768,'Laamu','05',1132),(3769,'Lhaviyani','03',1132),(3770,'Meemu','12',1132),(3771,'Noonu','25',1132),(3772,'Raa','13',1132),(3773,'Seenu','01',1132),(3774,'Shaviyani','24',1132),(3775,'Thaa','08',1132),(3776,'Vaavu','04',1132),(3777,'Balaka','BA',1130),(3778,'Blantyre','BL',1130),(3779,'Chikwawa','CK',1130),(3780,'Chiradzulu','CR',1130),(3781,'Chitipa','CT',1130),(3782,'Dedza','DE',1130),(3783,'Dowa','DO',1130),(3784,'Karonga','KR',1130),(3785,'Kasungu','KS',1130),(3786,'Likoma Island','LK',1130),(3787,'Lilongwe','LI',1130),(3788,'Machinga','MH',1130),(3789,'Mangochi','MG',1130),(3790,'Mchinji','MC',1130),(3791,'Mulanje','MU',1130),(3792,'Mwanza','MW',1130),(3793,'Mzimba','MZ',1130),(3794,'Nkhata Bay','NB',1130),(3795,'Nkhotakota','NK',1130),(3796,'Nsanje','NS',1130),(3797,'Ntcheu','NU',1130),(3798,'Ntchisi','NI',1130),(3799,'Phalomba','PH',1130),(3800,'Rumphi','RU',1130),(3801,'Salima','SA',1130),(3802,'Thyolo','TH',1130),(3803,'Zomba','ZO',1130),(3804,'Aguascalientes','AGU',1140),(3805,'Baja California','BCN',1140),(3806,'Baja California Sur','BCS',1140),(3807,'Campeche','CAM',1140),(3808,'Coahuila','COA',1140),(3809,'Colima','COL',1140),(3810,'Chiapas','CHP',1140),(3811,'Chihuahua','CHH',1140),(3812,'Durango','DUR',1140),(3813,'Guanajuato','GUA',1140),(3814,'Guerrero','GRO',1140),(3815,'Hidalgo','HID',1140),(3816,'Jalisco','JAL',1140),(3817,'Mexico','MEX',1140),(3818,'Michoacin','MIC',1140),(3819,'Morelos','MOR',1140),(3820,'Nayarit','NAY',1140),(3821,'Nuevo Leon','NLE',1140),(3822,'Oaxaca','OAX',1140),(3823,'Puebla','PUE',1140),(3824,'Queretaro','QUE',1140),(3825,'Quintana Roo','ROO',1140),(3826,'San Luis Potosi','SLP',1140),(3827,'Sinaloa','SIN',1140),(3828,'Sonora','SON',1140),(3829,'Tabasco','TAB',1140),(3830,'Tamaulipas','TAM',1140),(3831,'Tlaxcala','TLA',1140),(3832,'Veracruz','VER',1140),(3833,'Yucatan','YUC',1140),(3834,'Zacatecas','ZAC',1140),(3835,'Wilayah Persekutuan Kuala Lumpur','14',1131),(3836,'Wilayah Persekutuan Labuan','15',1131),(3837,'Wilayah Persekutuan Putrajaya','16',1131),(3838,'Johor','01',1131),(3839,'Kedah','02',1131),(3840,'Kelantan','03',1131),(3841,'Melaka','04',1131),(3842,'Negeri Sembilan','05',1131),(3843,'Pahang','06',1131),(3844,'Perak','08',1131),(3845,'Perlis','09',1131),(3846,'Pulau Pinang','07',1131),(3847,'Sabah','12',1131),(3848,'Sarawak','13',1131),(3849,'Selangor','10',1131),(3850,'Terengganu','11',1131),(3851,'Maputo','MPM',1147),(3852,'Cabo Delgado','P',1147),(3853,'Gaza','G',1147),(3854,'Inhambane','I',1147),(3855,'Manica','B',1147),(3856,'Numpula','N',1147),(3857,'Niaaea','A',1147),(3858,'Sofala','S',1147),(3859,'Tete','T',1147),(3860,'Zambezia','Q',1147),(3861,'Caprivi','CA',1148),(3862,'Erongo','ER',1148),(3863,'Hardap','HA',1148),(3864,'Karas','KA',1148),(3865,'Khomas','KH',1148),(3866,'Kunene','KU',1148),(3867,'Ohangwena','OW',1148),(3868,'Okavango','OK',1148),(3869,'Omaheke','OH',1148),(3870,'Omusati','OS',1148),(3871,'Oshana','ON',1148),(3872,'Oshikoto','OT',1148),(3873,'Otjozondjupa','OD',1148),(3874,'Niamey','8',1156),(3875,'Agadez','1',1156),(3876,'Diffa','2',1156),(3877,'Dosso','3',1156),(3878,'Maradi','4',1156),(3879,'Tahoua','S',1156),(3880,'Tillaberi','6',1156),(3881,'Zinder','7',1156),(3882,'Abuja Federal Capital Territory','FC',1157),(3883,'Abia','AB',1157),(3884,'Adamawa','AD',1157),(3885,'Akwa Ibom','AK',1157),(3886,'Anambra','AN',1157),(3887,'Bauchi','BA',1157),(3888,'Bayelsa','BY',1157),(3889,'Benue','BE',1157),(3890,'Borno','BO',1157),(3891,'Cross River','CR',1157),(3892,'Delta','DE',1157),(3893,'Ebonyi','EB',1157),(3894,'Edo','ED',1157),(3895,'Ekiti','EK',1157),(3896,'Enugu','EN',1157),(3897,'Gombe','GO',1157),(3898,'Imo','IM',1157),(3899,'Jigawa','JI',1157),(3900,'Kaduna','KD',1157),(3901,'Kano','KN',1157),(3902,'Katsina','KT',1157),(3903,'Kebbi','KE',1157),(3904,'Kogi','KO',1157),(3905,'Kwara','KW',1157),(3906,'Lagos','LA',1157),(3907,'Nassarawa','NA',1157),(3908,'Niger','NI',1157),(3909,'Ogun','OG',1157),(3910,'Ondo','ON',1157),(3911,'Osun','OS',1157),(3912,'Oyo','OY',1157),(3913,'Rivers','RI',1157),(3914,'Sokoto','SO',1157),(3915,'Taraba','TA',1157),(3916,'Yobe','YO',1157),(3917,'Zamfara','ZA',1157),(3918,'Boaco','BO',1155),(3919,'Carazo','CA',1155),(3920,'Chinandega','CI',1155),(3921,'Chontales','CO',1155),(3922,'Esteli','ES',1155),(3923,'Jinotega','JI',1155),(3924,'Leon','LE',1155),(3925,'Madriz','MD',1155),(3926,'Managua','MN',1155),(3927,'Masaya','MS',1155),(3928,'Matagalpa','MT',1155),(3929,'Nueva Segovia','NS',1155),(3930,'Rio San Juan','SJ',1155),(3931,'Rivas','RI',1155),(3932,'Atlantico Norte','AN',1155),(3933,'Atlantico Sur','AS',1155),(3934,'Drente','DR',1152),(3935,'Flevoland','FL',1152),(3936,'Friesland','FR',1152),(3937,'Gelderland','GL',1152),(3938,'Groningen','GR',1152),(3939,'Noord-Brabant','NB',1152),(3940,'Noord-Holland','NH',1152),(3941,'Overijssel','OV',1152),(3942,'Utrecht','UT',1152),(3943,'Zuid-Holland','ZH',1152),(3944,'Zeeland','ZL',1152),(3945,'Akershus','02',1161),(3946,'Aust-Agder','09',1161),(3947,'Buskerud','06',1161),(3948,'Finnmark','20',1161),(3949,'Hedmark','04',1161),(3950,'Hordaland','12',1161),(3951,'Møre og Romsdal','15',1161),(3952,'Nordland','18',1161),(3953,'Nord-Trøndelag','17',1161),(3954,'Oppland','05',1161),(3955,'Oslo','03',1161),(3956,'Rogaland','11',1161),(3957,'Sogn og Fjordane','14',1161),(3958,'Sør-Trøndelag','16',1161),(3959,'Telemark','06',1161),(3960,'Troms','19',1161),(3961,'Vest-Agder','10',1161),(3962,'Vestfold','07',1161),(3963,'Østfold','01',1161),(3964,'Jan Mayen','22',1161),(3965,'Svalbard','21',1161),(3966,'Auckland','AUK',1154),(3967,'Bay of Plenty','BOP',1154),(3968,'Canterbury','CAN',1154),(3969,'Gisborne','GIS',1154),(3970,'Hawkes Bay','HKB',1154),(3971,'Manawatu-Wanganui','MWT',1154),(3972,'Marlborough','MBH',1154),(3973,'Nelson','NSN',1154),(3974,'Northland','NTL',1154),(3975,'Otago','OTA',1154),(3976,'Southland','STL',1154),(3977,'Taranaki','TKI',1154),(3978,'Tasman','TAS',1154),(3979,'Waikato','WKO',1154),(3980,'Wellington','WGN',1154),(3981,'West Coast','WTC',1154),(3982,'Ad Dakhillyah','DA',1162),(3983,'Al Batinah','BA',1162),(3984,'Al Janblyah','JA',1162),(3985,'Al Wusta','WU',1162),(3986,'Ash Sharqlyah','SH',1162),(3987,'Az Zahirah','ZA',1162),(3988,'Masqat','MA',1162),(3989,'Musandam','MU',1162),(3990,'Bocas del Toro','1',1166),(3991,'Cocle','2',1166),(3992,'Chiriqui','4',1166),(3993,'Darien','5',1166),(3994,'Herrera','6',1166),(3995,'Loa Santoa','7',1166),(3996,'Panama','8',1166),(3997,'Veraguas','9',1166),(3998,'Comarca de San Blas','Q',1166),(3999,'El Callao','CAL',1169),(4000,'Ancash','ANC',1169),(4001,'Apurimac','APU',1169),(4002,'Arequipa','ARE',1169),(4003,'Ayacucho','AYA',1169),(4004,'Cajamarca','CAJ',1169),(4005,'Cuzco','CUS',1169),(4006,'Huancavelica','HUV',1169),(4007,'Huanuco','HUC',1169),(4008,'Ica','ICA',1169),(4009,'Junin','JUN',1169),(4010,'La Libertad','LAL',1169),(4011,'Lambayeque','LAM',1169),(4012,'Lima','LIM',1169),(4013,'Loreto','LOR',1169),(4014,'Madre de Dios','MDD',1169),(4015,'Moquegua','MOQ',1169),(4016,'Pasco','PAS',1169),(4017,'Piura','PIU',1169),(4018,'Puno','PUN',1169),(4019,'San Martin','SAM',1169),(4020,'Tacna','TAC',1169),(4021,'Tumbes','TUM',1169),(4022,'Ucayali','UCA',1169),(4023,'National Capital District (Port Moresby)','NCD',1167),(4024,'Chimbu','CPK',1167),(4025,'Eastern Highlands','EHG',1167),(4026,'East New Britain','EBR',1167),(4027,'East Sepik','ESW',1167),(4028,'Enga','EPW',1167),(4029,'Gulf','GPK',1167),(4030,'Madang','MPM',1167),(4031,'Manus','MRL',1167),(4032,'Milne Bay','MBA',1167),(4033,'Morobe','MPL',1167),(4034,'New Ireland','NIK',1167),(4035,'North Solomons','NSA',1167),(4036,'Santaun','SAN',1167),(4037,'Southern Highlands','SHM',1167),(4038,'Western Highlands','WHM',1167),(4039,'West New Britain','WBK',1167),(4040,'Abra','ABR',1170),(4041,'Agusan del Norte','AGN',1170),(4042,'Agusan del Sur','AGS',1170),(4043,'Aklan','AKL',1170),(4044,'Albay','ALB',1170),(4045,'Antique','ANT',1170),(4046,'Apayao','APA',1170),(4047,'Aurora','AUR',1170),(4048,'Basilan','BAS',1170),(4049,'Bataan','BAN',1170),(4050,'Batanes','BTN',1170),(4051,'Batangas','BTG',1170),(4052,'Benguet','BEN',1170),(4053,'Biliran','BIL',1170),(4054,'Bohol','BOH',1170),(4055,'Bukidnon','BUK',1170),(4056,'Bulacan','BUL',1170),(4057,'Cagayan','CAG',1170),(4058,'Camarines Norte','CAN',1170),(4059,'Camarines Sur','CAS',1170),(4060,'Camiguin','CAM',1170),(4061,'Capiz','CAP',1170),(4062,'Catanduanes','CAT',1170),(4063,'Cavite','CAV',1170),(4064,'Cebu','CEB',1170),(4065,'Compostela Valley','COM',1170),(4066,'Davao','DAV',1170),(4067,'Davao del Sur','DAS',1170),(4068,'Davao Oriental','DAO',1170),(4069,'Eastern Samar','EAS',1170),(4070,'Guimaras','GUI',1170),(4071,'Ifugao','IFU',1170),(4072,'Ilocos Norte','ILN',1170),(4073,'Ilocos Sur','ILS',1170),(4074,'Iloilo','ILI',1170),(4075,'Isabela','ISA',1170),(4076,'Kalinga-Apayso','KAL',1170),(4077,'Laguna','LAG',1170),(4078,'Lanao del Norte','LAN',1170),(4079,'Lanao del Sur','LAS',1170),(4080,'La Union','LUN',1170),(4081,'Leyte','LEY',1170),(4082,'Maguindanao','MAG',1170),(4083,'Marinduque','MAD',1170),(4084,'Masbate','MAS',1170),(4085,'Mindoro Occidental','MDC',1170),(4086,'Mindoro Oriental','MDR',1170),(4087,'Misamis Occidental','MSC',1170),(4088,'Misamis Oriental','MSR',1170),(4089,'Mountain Province','MOU',1170),(4090,'Negroe Occidental','NEC',1170),(4091,'Negros Oriental','NER',1170),(4092,'North Cotabato','NCO',1170),(4093,'Northern Samar','NSA',1170),(4094,'Nueva Ecija','NUE',1170),(4095,'Nueva Vizcaya','NUV',1170),(4096,'Palawan','PLW',1170),(4097,'Pampanga','PAM',1170),(4098,'Pangasinan','PAN',1170),(4099,'Quezon','QUE',1170),(4100,'Quirino','QUI',1170),(4101,'Rizal','RIZ',1170),(4102,'Romblon','ROM',1170),(4103,'Sarangani','SAR',1170),(4104,'Siquijor','SIG',1170),(4105,'Sorsogon','SOR',1170),(4106,'South Cotabato','SCO',1170),(4107,'Southern Leyte','SLE',1170),(4108,'Sultan Kudarat','SUK',1170),(4109,'Sulu','SLU',1170),(4110,'Surigao del Norte','SUN',1170),(4111,'Surigao del Sur','SUR',1170),(4112,'Tarlac','TAR',1170),(4113,'Tawi-Tawi','TAW',1170),(4114,'Western Samar','WSA',1170),(4115,'Zambales','ZMB',1170),(4116,'Zamboanga del Norte','ZAN',1170),(4117,'Zamboanga del Sur','ZAS',1170),(4118,'Zamboanga Sibiguey','ZSI',1170),(4119,'Islamabad Federal Capital Area','IS',1163),(4120,'Baluchistan','BA',1163),(4121,'Khyber Pakhtun Khawa','NW',1163),(4122,'Sindh','SD',1163),(4123,'Federally Administered Tribal Areas','TA',1163),(4124,'Azad Kashmir','JK',1163),(4125,'Gilgit-Baltistan','NA',1163),(4126,'Aveiro','01',1173),(4127,'Beja','02',1173),(4128,'Braga','03',1173),(4129,'Bragança','04',1173),(4130,'Castelo Branco','05',1173),(4131,'Coimbra','06',1173),(4132,'Évora','07',1173),(4133,'Faro','08',1173),(4134,'Guarda','09',1173),(4135,'Leiria','10',1173),(4136,'Lisboa','11',1173),(4137,'Portalegre','12',1173),(4138,'Porto','13',1173),(4139,'Santarém','14',1173),(4140,'Setúbal','15',1173),(4141,'Viana do Castelo','16',1173),(4142,'Vila Real','17',1173),(4143,'Viseu','18',1173),(4144,'Região Autónoma dos Açores','20',1173),(4145,'Região Autónoma da Madeira','30',1173),(4146,'Asuncion','ASU',1168),(4147,'Alto Paraguay','16',1168),(4148,'Alto Parana','10',1168),(4149,'Amambay','13',1168),(4150,'Boqueron','19',1168),(4151,'Caeguazu','5',1168),(4152,'Caazapl','6',1168),(4153,'Canindeyu','14',1168),(4154,'Concepcion','1',1168),(4155,'Cordillera','3',1168),(4156,'Guaira','4',1168),(4157,'Itapua','7',1168),(4158,'Miaiones','8',1168),(4159,'Neembucu','12',1168),(4160,'Paraguari','9',1168),(4161,'Presidente Hayes','15',1168),(4162,'San Pedro','2',1168),(4163,'Ad Dawhah','DA',1175),(4164,'Al Ghuwayriyah','GH',1175),(4165,'Al Jumayliyah','JU',1175),(4166,'Al Khawr','KH',1175),(4167,'Al Wakrah','WA',1175),(4168,'Ar Rayyan','RA',1175),(4169,'Jariyan al Batnah','JB',1175),(4170,'Madinat ash Shamal','MS',1175),(4171,'Umm Salal','US',1175),(4172,'Bucuresti','B',1176),(4173,'Alba','AB',1176),(4174,'Arad','AR',1176),(4175,'Argeș','AG',1176),(4176,'Bacău','BC',1176),(4177,'Bihor','BH',1176),(4178,'Bistrița-Năsăud','BN',1176),(4179,'Botoșani','BT',1176),(4180,'Brașov','BV',1176),(4181,'Brăila','BR',1176),(4182,'Buzău','BZ',1176),(4183,'Caraș-Severin','CS',1176),(4184,'Călărași','CL',1176),(4185,'Cluj','CJ',1176),(4186,'Constanța','CT',1176),(4187,'Covasna','CV',1176),(4188,'Dâmbovița','DB',1176),(4189,'Dolj','DJ',1176),(4190,'Galați','GL',1176),(4191,'Giurgiu','GR',1176),(4192,'Gorj','GJ',1176),(4193,'Harghita','HR',1176),(4194,'Hunedoara','HD',1176),(4195,'Ialomița','IL',1176),(4196,'Iași','IS',1176),(4197,'Ilfov','IF',1176),(4198,'Maramureș','MM',1176),(4199,'Mehedinți','MH',1176),(4200,'Mureș','MS',1176),(4201,'Neamț','NT',1176),(4202,'Olt','OT',1176),(4203,'Prahova','PH',1176),(4204,'Satu Mare','SM',1176),(4205,'Sălaj','SJ',1176),(4206,'Sibiu','SB',1176),(4207,'Suceava','SV',1176),(4208,'Teleorman','TR',1176),(4209,'Timiș','TM',1176),(4210,'Tulcea','TL',1176),(4211,'Vaslui','VS',1176),(4212,'Vâlcea','VL',1176),(4213,'Vrancea','VN',1176),(4214,'Adygeya, Respublika','AD',1177),(4215,'Altay, Respublika','AL',1177),(4216,'Bashkortostan, Respublika','BA',1177),(4217,'Buryatiya, Respublika','BU',1177),(4218,'Chechenskaya Respublika','CE',1177),(4219,'Chuvashskaya Respublika','CU',1177),(4220,'Dagestan, Respublika','DA',1177),(4221,'Ingushskaya Respublika','IN',1177),(4222,'Kabardino-Balkarskaya','KB',1177),(4223,'Kalmykiya, Respublika','KL',1177),(4224,'Karachayevo-Cherkesskaya Respublika','KC',1177),(4225,'Kareliya, Respublika','KR',1177),(4226,'Khakasiya, Respublika','KK',1177),(4227,'Komi, Respublika','KO',1177),(4228,'Mariy El, Respublika','ME',1177),(4229,'Mordoviya, Respublika','MO',1177),(4230,'Sakha, Respublika [Yakutiya]','SA',1177),(4231,'Severnaya Osetiya, Respublika','SE',1177),(4232,'Tatarstan, Respublika','TA',1177),(4233,'Tyva, Respublika [Tuva]','TY',1177),(4234,'Udmurtskaya Respublika','UD',1177),(4235,'Altayskiy kray','ALT',1177),(4236,'Khabarovskiy kray','KHA',1177),(4237,'Krasnodarskiy kray','KDA',1177),(4238,'Krasnoyarskiy kray','KYA',1177),(4239,'Primorskiy kray','PRI',1177),(4240,'Stavropol\'skiy kray','STA',1177),(4241,'Amurskaya oblast\'','AMU',1177),(4242,'Arkhangel\'skaya oblast\'','ARK',1177),(4243,'Astrakhanskaya oblast\'','AST',1177),(4244,'Belgorodskaya oblast\'','BEL',1177),(4245,'Bryanskaya oblast\'','BRY',1177),(4246,'Chelyabinskaya oblast\'','CHE',1177),(4247,'Zabaykalsky Krai\'','ZSK',1177),(4248,'Irkutskaya oblast\'','IRK',1177),(4249,'Ivanovskaya oblast\'','IVA',1177),(4250,'Kaliningradskaya oblast\'','KGD',1177),(4251,'Kaluzhskaya oblast\'','KLU',1177),(4252,'Kamchatka Krai\'','KAM',1177),(4253,'Kemerovskaya oblast\'','KEM',1177),(4254,'Kirovskaya oblast\'','KIR',1177),(4255,'Kostromskaya oblast\'','KOS',1177),(4256,'Kurganskaya oblast\'','KGN',1177),(4257,'Kurskaya oblast\'','KRS',1177),(4258,'Leningradskaya oblast\'','LEN',1177),(4259,'Lipetskaya oblast\'','LIP',1177),(4260,'Magadanskaya oblast\'','MAG',1177),(4261,'Moskovskaya oblast\'','MOS',1177),(4262,'Murmanskaya oblast\'','MUR',1177),(4263,'Nizhegorodskaya oblast\'','NIZ',1177),(4264,'Novgorodskaya oblast\'','NGR',1177),(4265,'Novosibirskaya oblast\'','NVS',1177),(4266,'Omskaya oblast\'','OMS',1177),(4267,'Orenburgskaya oblast\'','ORE',1177),(4268,'Orlovskaya oblast\'','ORL',1177),(4269,'Penzenskaya oblast\'','PNZ',1177),(4270,'Perm krai\'','PEK',1177),(4271,'Pskovskaya oblast\'','PSK',1177),(4272,'Rostovskaya oblast\'','ROS',1177),(4273,'Ryazanskaya oblast\'','RYA',1177),(4274,'Sakhalinskaya oblast\'','SAK',1177),(4275,'Samarskaya oblast\'','SAM',1177),(4276,'Saratovskaya oblast\'','SAR',1177),(4277,'Smolenskaya oblast\'','SMO',1177),(4278,'Sverdlovskaya oblast\'','SVE',1177),(4279,'Tambovskaya oblast\'','TAM',1177),(4280,'Tomskaya oblast\'','TOM',1177),(4281,'Tul\'skaya oblast\'','TUL',1177),(4282,'Tverskaya oblast\'','TVE',1177),(4283,'Tyumenskaya oblast\'','TYU',1177),(4284,'Ul\'yanovskaya oblast\'','ULY',1177),(4285,'Vladimirskaya oblast\'','VLA',1177),(4286,'Volgogradskaya oblast\'','VGG',1177),(4287,'Vologodskaya oblast\'','VLG',1177),(4288,'Voronezhskaya oblast\'','VOR',1177),(4289,'Yaroslavskaya oblast\'','YAR',1177),(4290,'Moskva','MOW',1177),(4291,'Sankt-Peterburg','SPE',1177),(4292,'Yevreyskaya avtonomnaya oblast\'','YEV',1177),(4294,'Chukotskiy avtonomnyy okrug','CHU',1177),(4296,'Khanty-Mansiyskiy avtonomnyy okrug','KHM',1177),(4299,'Nenetskiy avtonomnyy okrug','NEN',1177),(4302,'Yamalo-Nenetskiy avtonomnyy okrug','YAN',1177),(4303,'Butare','C',1178),(4304,'Byumba','I',1178),(4305,'Cyangugu','E',1178),(4306,'Gikongoro','D',1178),(4307,'Gisenyi','G',1178),(4308,'Gitarama','B',1178),(4309,'Kibungo','J',1178),(4310,'Kibuye','F',1178),(4311,'Kigali-Rural Kigali y\' Icyaro','K',1178),(4312,'Kigali-Ville Kigali Ngari','L',1178),(4313,'Mutara','M',1178),(4314,'Ruhengeri','H',1178),(4315,'Al Bahah','11',1187),(4316,'Al Hudud Ash Shamaliyah','08',1187),(4317,'Al Jawf','12',1187),(4318,'Al Madinah','03',1187),(4319,'Al Qasim','05',1187),(4320,'Ar Riyad','01',1187),(4321,'Asir','14',1187),(4322,'Ha\'il','06',1187),(4323,'Jlzan','09',1187),(4324,'Makkah','02',1187),(4325,'Najran','10',1187),(4326,'Tabuk','07',1187),(4327,'Capital Territory (Honiara)','CT',1194),(4328,'Guadalcanal','GU',1194),(4329,'Isabel','IS',1194),(4330,'Makira','MK',1194),(4331,'Malaita','ML',1194),(4332,'Temotu','TE',1194),(4333,'A\'ali an Nil','23',1200),(4334,'Al Bah al Ahmar','26',1200),(4335,'Al Buhayrat','18',1200),(4336,'Al Jazirah','07',1200),(4337,'Al Khartum','03',1200),(4338,'Al Qadarif','06',1200),(4339,'Al Wahdah','22',1200),(4340,'An Nil','04',1200),(4341,'An Nil al Abyaq','08',1200),(4342,'An Nil al Azraq','24',1200),(4343,'Ash Shamallyah','01',1200),(4344,'Bahr al Jabal','17',1200),(4345,'Gharb al Istiwa\'iyah','16',1200),(4346,'Gharb Ba~r al Ghazal','14',1200),(4347,'Gharb Darfur','12',1200),(4348,'Gharb Kurdufan','10',1200),(4349,'Janub Darfur','11',1200),(4350,'Janub Rurdufan','13',1200),(4351,'Jnqall','20',1200),(4352,'Kassala','05',1200),(4353,'Shamal Batr al Ghazal','15',1200),(4354,'Shamal Darfur','02',1200),(4355,'Shamal Kurdufan','09',1200),(4356,'Sharq al Istiwa\'iyah','19',1200),(4357,'Sinnar','25',1200),(4358,'Warab','21',1200),(4359,'Blekinge län','K',1204),(4360,'Dalarnas län','W',1204),(4361,'Gotlands län','I',1204),(4362,'Gävleborgs län','X',1204),(4363,'Hallands län','N',1204),(4364,'Jämtlands län','Z',1204),(4365,'Jönkopings län','F',1204),(4366,'Kalmar län','H',1204),(4367,'Kronobergs län','G',1204),(4368,'Norrbottens län','BD',1204),(4369,'Skåne län','M',1204),(4370,'Stockholms län','AB',1204),(4371,'Södermanlands län','D',1204),(4372,'Uppsala län','C',1204),(4373,'Värmlands län','S',1204),(4374,'Västerbottens län','AC',1204),(4375,'Västernorrlands län','Y',1204),(4376,'Västmanlands län','U',1204),(4377,'Västra Götalands län','Q',1204),(4378,'Örebro län','T',1204),(4379,'Östergötlands län','E',1204),(4380,'Saint Helena','SH',1180),(4381,'Ascension','AC',1180),(4382,'Tristan da Cunha','TA',1180),(4383,'Ajdovščina','001',1193),(4384,'Beltinci','002',1193),(4385,'Benedikt','148',1193),(4386,'Bistrica ob Sotli','149',1193),(4387,'Bled','003',1193),(4388,'Bloke','150',1193),(4389,'Bohinj','004',1193),(4390,'Borovnica','005',1193),(4391,'Bovec','006',1193),(4392,'Braslovče','151',1193),(4393,'Brda','007',1193),(4394,'Brezovica','008',1193),(4395,'Brežice','009',1193),(4396,'Cankova','152',1193),(4397,'Celje','011',1193),(4398,'Cerklje na Gorenjskem','012',1193),(4399,'Cerknica','013',1193),(4400,'Cerkno','014',1193),(4401,'Cerkvenjak','153',1193),(4402,'Črenšovci','015',1193),(4403,'Črna na Koroškem','016',1193),(4404,'Črnomelj','017',1193),(4405,'Destrnik','018',1193),(4406,'Divača','019',1193),(4407,'Dobje','154',1193),(4408,'Dobrepolje','020',1193),(4409,'Dobrna','155',1193),(4410,'Dobrova-Polhov Gradec','021',1193),(4411,'Dobrovnik','156',1193),(4412,'Dol pri Ljubljani','022',1193),(4413,'Dolenjske Toplice','157',1193),(4414,'Domžale','023',1193),(4415,'Dornava','024',1193),(4416,'Dravograd','025',1193),(4417,'Duplek','026',1193),(4418,'Gorenja vas-Poljane','027',1193),(4419,'Gorišnica','028',1193),(4420,'Gornja Radgona','029',1193),(4421,'Gornji Grad','030',1193),(4422,'Gornji Petrovci','031',1193),(4423,'Grad','158',1193),(4424,'Grosuplje','032',1193),(4425,'Hajdina','159',1193),(4426,'Hoče-Slivnica','160',1193),(4427,'Hodoš','161',1193),(4428,'Horjul','162',1193),(4429,'Hrastnik','034',1193),(4430,'Hrpelje-Kozina','035',1193),(4431,'Idrija','036',1193),(4432,'Ig','037',1193),(4433,'Ilirska Bistrica','038',1193),(4434,'Ivančna Gorica','039',1193),(4435,'Izola','040',1193),(4436,'Jesenice','041',1193),(4437,'Jezersko','163',1193),(4438,'Juršinci','042',1193),(4439,'Kamnik','043',1193),(4440,'Kanal','044',1193),(4441,'Kidričevo','045',1193),(4442,'Kobarid','046',1193),(4443,'Kobilje','047',1193),(4444,'Kočevje','048',1193),(4445,'Komen','049',1193),(4446,'Komenda','164',1193),(4447,'Koper','050',1193),(4448,'Kostel','165',1193),(4449,'Kozje','051',1193),(4450,'Kranj','052',1193),(4451,'Kranjska Gora','053',1193),(4452,'Križevci','166',1193),(4453,'Krško','054',1193),(4454,'Kungota','055',1193),(4455,'Kuzma','056',1193),(4456,'Laško','057',1193),(4457,'Lenart','058',1193),(4458,'Lendava','059',1193),(4459,'Litija','060',1193),(4460,'Ljubljana','061',1193),(4461,'Ljubno','062',1193),(4462,'Ljutomer','063',1193),(4463,'Logatec','064',1193),(4464,'Loška dolina','065',1193),(4465,'Loški Potok','066',1193),(4466,'Lovrenc na Pohorju','167',1193),(4467,'Luče','067',1193),(4468,'Lukovica','068',1193),(4469,'Majšperk','069',1193),(4470,'Maribor','070',1193),(4471,'Markovci','168',1193),(4472,'Medvode','071',1193),(4473,'Mengeš','072',1193),(4474,'Metlika','073',1193),(4475,'Mežica','074',1193),(4476,'Miklavž na Dravskem polju','169',1193),(4477,'Miren-Kostanjevica','075',1193),(4478,'Mirna Peč','170',1193),(4479,'Mislinja','076',1193),(4480,'Moravče','077',1193),(4481,'Moravske Toplice','078',1193),(4482,'Mozirje','079',1193),(4483,'Murska Sobota','080',1193),(4484,'Muta','081',1193),(4485,'Naklo','082',1193),(4486,'Nazarje','083',1193),(4487,'Nova Gorica','084',1193),(4488,'Novo mesto','085',1193),(4489,'Sveta Ana','181',1193),(4490,'Sveti Andraž v Slovenskih goricah','182',1193),(4491,'Sveti Jurij','116',1193),(4492,'Šalovci','033',1193),(4493,'Šempeter-Vrtojba','183',1193),(4494,'Šenčur','117',1193),(4495,'Šentilj','118',1193),(4496,'Šentjernej','119',1193),(4497,'Šentjur','120',1193),(4498,'Škocjan','121',1193),(4499,'Škofja Loka','122',1193),(4500,'Škofljica','123',1193),(4501,'Šmarje pri Jelšah','124',1193),(4502,'Šmartno ob Paki','125',1193),(4503,'Šmartno pri Litiji','194',1193),(4504,'Šoštanj','126',1193),(4505,'Štore','127',1193),(4506,'Tabor','184',1193),(4507,'Tišina','010',1193),(4508,'Tolmin','128',1193),(4509,'Trbovlje','129',1193),(4510,'Trebnje','130',1193),(4511,'Trnovska vas','185',1193),(4512,'Tržič','131',1193),(4513,'Trzin','186',1193),(4514,'Turnišče','132',1193),(4515,'Velenje','133',1193),(4516,'Velika Polana','187',1193),(4517,'Velike Lašče','134',1193),(4518,'Veržej','188',1193),(4519,'Videm','135',1193),(4520,'Vipava','136',1193),(4521,'Vitanje','137',1193),(4522,'Vojnik','138',1193),(4523,'Vransko','189',1193),(4524,'Vrhnika','140',1193),(4525,'Vuzenica','141',1193),(4526,'Zagorje ob Savi','142',1193),(4527,'Zavrč','143',1193),(4528,'Zreče','144',1193),(4529,'Žalec','190',1193),(4530,'Železniki','146',1193),(4531,'Žetale','191',1193),(4532,'Žiri','147',1193),(4533,'Žirovnica','192',1193),(4534,'Žužemberk','193',1193),(4535,'Banskobystrický kraj','BC',1192),(4536,'Bratislavský kraj','BL',1192),(4537,'Košický kraj','KI',1192),(4538,'Nitriansky kraj','NJ',1192),(4539,'Prešovský kraj','PV',1192),(4540,'Trenčiansky kraj','TC',1192),(4541,'Trnavský kraj','TA',1192),(4542,'Žilinský kraj','ZI',1192),(4543,'Western Area (Freetown)','W',1190),(4544,'Dakar','DK',1188),(4545,'Diourbel','DB',1188),(4546,'Fatick','FK',1188),(4547,'Kaolack','KL',1188),(4548,'Kolda','KD',1188),(4549,'Louga','LG',1188),(4550,'Matam','MT',1188),(4551,'Saint-Louis','SL',1188),(4552,'Tambacounda','TC',1188),(4553,'Thies','TH',1188),(4554,'Ziguinchor','ZG',1188),(4555,'Awdal','AW',1195),(4556,'Bakool','BK',1195),(4557,'Banaadir','BN',1195),(4558,'Bay','BY',1195),(4559,'Galguduud','GA',1195),(4560,'Gedo','GE',1195),(4561,'Hiirsan','HI',1195),(4562,'Jubbada Dhexe','JD',1195),(4563,'Jubbada Hoose','JH',1195),(4564,'Mudug','MU',1195),(4565,'Nugaal','NU',1195),(4566,'Saneag','SA',1195),(4567,'Shabeellaha Dhexe','SD',1195),(4568,'Shabeellaha Hoose','SH',1195),(4569,'Sool','SO',1195),(4570,'Togdheer','TO',1195),(4571,'Woqooyi Galbeed','WO',1195),(4572,'Brokopondo','BR',1201),(4573,'Commewijne','CM',1201),(4574,'Coronie','CR',1201),(4575,'Marowijne','MA',1201),(4576,'Nickerie','NI',1201),(4577,'Paramaribo','PM',1201),(4578,'Saramacca','SA',1201),(4579,'Sipaliwini','SI',1201),(4580,'Wanica','WA',1201),(4581,'Principe','P',1207),(4582,'Sao Tome','S',1207),(4583,'Ahuachapan','AH',1066),(4584,'Cabanas','CA',1066),(4585,'Cuscatlan','CU',1066),(4586,'Chalatenango','CH',1066),(4587,'Morazan','MO',1066),(4588,'San Miguel','SM',1066),(4589,'San Salvador','SS',1066),(4590,'Santa Ana','SA',1066),(4591,'San Vicente','SV',1066),(4592,'Sonsonate','SO',1066),(4593,'Usulutan','US',1066),(4594,'Al Hasakah','HA',1206),(4595,'Al Ladhiqiyah','LA',1206),(4596,'Al Qunaytirah','QU',1206),(4597,'Ar Raqqah','RA',1206),(4598,'As Suwayda\'','SU',1206),(4599,'Dar\'a','DR',1206),(4600,'Dayr az Zawr','DY',1206),(4601,'Dimashq','DI',1206),(4602,'Halab','HL',1206),(4603,'Hamah','HM',1206),(4604,'Jim\'','HI',1206),(4605,'Idlib','ID',1206),(4606,'Rif Dimashq','RD',1206),(4607,'Tarts','TA',1206),(4608,'Hhohho','HH',1203),(4609,'Lubombo','LU',1203),(4610,'Manzini','MA',1203),(4611,'Shiselweni','SH',1203),(4612,'Batha','BA',1043),(4613,'Biltine','BI',1043),(4614,'Borkou-Ennedi-Tibesti','BET',1043),(4615,'Chari-Baguirmi','CB',1043),(4616,'Guera','GR',1043),(4617,'Kanem','KA',1043),(4618,'Lac','LC',1043),(4619,'Logone-Occidental','LO',1043),(4620,'Logone-Oriental','LR',1043),(4621,'Mayo-Kebbi','MK',1043),(4622,'Moyen-Chari','MC',1043),(4623,'Ouaddai','OD',1043),(4624,'Salamat','SA',1043),(4625,'Tandjile','TA',1043),(4626,'Kara','K',1214),(4627,'Maritime (Region)','M',1214),(4628,'Savannes','S',1214),(4629,'Krung Thep Maha Nakhon Bangkok','10',1211),(4630,'Phatthaya','S',1211),(4631,'Amnat Charoen','37',1211),(4632,'Ang Thong','15',1211),(4633,'Buri Ram','31',1211),(4634,'Chachoengsao','24',1211),(4635,'Chai Nat','18',1211),(4636,'Chaiyaphum','36',1211),(4637,'Chanthaburi','22',1211),(4638,'Chiang Mai','50',1211),(4639,'Chiang Rai','57',1211),(4640,'Chon Buri','20',1211),(4641,'Chumphon','86',1211),(4642,'Kalasin','46',1211),(4643,'Kamphasng Phet','62',1211),(4644,'Kanchanaburi','71',1211),(4645,'Khon Kaen','40',1211),(4646,'Krabi','81',1211),(4647,'Lampang','52',1211),(4648,'Lamphun','51',1211),(4649,'Loei','42',1211),(4650,'Lop Buri','16',1211),(4651,'Mae Hong Son','58',1211),(4652,'Maha Sarakham','44',1211),(4653,'Mukdahan','49',1211),(4654,'Nakhon Nayok','26',1211),(4655,'Nakhon Pathom','73',1211),(4656,'Nakhon Phanom','48',1211),(4657,'Nakhon Ratchasima','30',1211),(4658,'Nakhon Sawan','60',1211),(4659,'Nakhon Si Thammarat','80',1211),(4660,'Nan','55',1211),(4661,'Narathiwat','96',1211),(4662,'Nong Bua Lam Phu','39',1211),(4663,'Nong Khai','43',1211),(4664,'Nonthaburi','12',1211),(4665,'Pathum Thani','13',1211),(4666,'Pattani','94',1211),(4667,'Phangnga','82',1211),(4668,'Phatthalung','93',1211),(4669,'Phayao','56',1211),(4670,'Phetchabun','67',1211),(4671,'Phetchaburi','76',1211),(4672,'Phichit','66',1211),(4673,'Phitsanulok','65',1211),(4674,'Phrae','54',1211),(4675,'Phra Nakhon Si Ayutthaya','14',1211),(4676,'Phuket','83',1211),(4677,'Prachin Buri','25',1211),(4678,'Prachuap Khiri Khan','77',1211),(4679,'Ranong','85',1211),(4680,'Ratchaburi','70',1211),(4681,'Rayong','21',1211),(4682,'Roi Et','45',1211),(4683,'Sa Kaeo','27',1211),(4684,'Sakon Nakhon','47',1211),(4685,'Samut Prakan','11',1211),(4686,'Samut Sakhon','74',1211),(4687,'Samut Songkhram','75',1211),(4688,'Saraburi','19',1211),(4689,'Satun','91',1211),(4690,'Sing Buri','17',1211),(4691,'Si Sa Ket','33',1211),(4692,'Songkhla','90',1211),(4693,'Sukhothai','64',1211),(4694,'Suphan Buri','72',1211),(4695,'Surat Thani','84',1211),(4696,'Surin','32',1211),(4697,'Tak','63',1211),(4698,'Trang','92',1211),(4699,'Trat','23',1211),(4700,'Ubon Ratchathani','34',1211),(4701,'Udon Thani','41',1211),(4702,'Uthai Thani','61',1211),(4703,'Uttaradit','53',1211),(4704,'Yala','95',1211),(4705,'Yasothon','35',1211),(4706,'Sughd','SU',1209),(4707,'Khatlon','KT',1209),(4708,'Gorno-Badakhshan','GB',1209),(4709,'Ahal','A',1220),(4710,'Balkan','B',1220),(4711,'Dasoguz','D',1220),(4712,'Lebap','L',1220),(4713,'Mary','M',1220),(4714,'Béja','31',1218),(4715,'Ben Arous','13',1218),(4716,'Bizerte','23',1218),(4717,'Gabès','81',1218),(4718,'Gafsa','71',1218),(4719,'Jendouba','32',1218),(4720,'Kairouan','41',1218),(4721,'Rasserine','42',1218),(4722,'Kebili','73',1218),(4723,'L\'Ariana','12',1218),(4724,'Le Ref','33',1218),(4725,'Mahdia','53',1218),(4726,'La Manouba','14',1218),(4727,'Medenine','82',1218),(4728,'Moneatir','52',1218),(4729,'Naboul','21',1218),(4730,'Sfax','61',1218),(4731,'Sidi Bouxid','43',1218),(4732,'Siliana','34',1218),(4733,'Sousse','51',1218),(4734,'Tataouine','83',1218),(4735,'Tozeur','72',1218),(4736,'Tunis','11',1218),(4737,'Zaghouan','22',1218),(4738,'Adana','01',1219),(4739,'Ad yaman','02',1219),(4740,'Afyon','03',1219),(4741,'Ag r','04',1219),(4742,'Aksaray','68',1219),(4743,'Amasya','05',1219),(4744,'Ankara','06',1219),(4745,'Antalya','07',1219),(4746,'Ardahan','75',1219),(4747,'Artvin','08',1219),(4748,'Aydin','09',1219),(4749,'Bal kesir','10',1219),(4750,'Bartin','74',1219),(4751,'Batman','72',1219),(4752,'Bayburt','69',1219),(4753,'Bilecik','11',1219),(4754,'Bingol','12',1219),(4755,'Bitlis','13',1219),(4756,'Bolu','14',1219),(4757,'Burdur','15',1219),(4758,'Bursa','16',1219),(4759,'Canakkale','17',1219),(4760,'Cankir','18',1219),(4761,'Corum','19',1219),(4762,'Denizli','20',1219),(4763,'Diyarbakir','21',1219),(4764,'Duzce','81',1219),(4765,'Edirne','22',1219),(4766,'Elazig','23',1219),(4767,'Erzincan','24',1219),(4768,'Erzurum','25',1219),(4769,'Eskis\'ehir','26',1219),(4770,'Gaziantep','27',1219),(4771,'Giresun','28',1219),(4772,'Gms\'hane','29',1219),(4773,'Hakkari','30',1219),(4774,'Hatay','31',1219),(4775,'Igidir','76',1219),(4776,'Isparta','32',1219),(4777,'Icel','33',1219),(4778,'Istanbul','34',1219),(4779,'Izmir','35',1219),(4780,'Kahramanmaras','46',1219),(4781,'Karabk','78',1219),(4782,'Karaman','70',1219),(4783,'Kars','36',1219),(4784,'Kastamonu','37',1219),(4785,'Kayseri','38',1219),(4786,'Kirikkale','71',1219),(4787,'Kirklareli','39',1219),(4788,'Kirs\'ehir','40',1219),(4789,'Kilis','79',1219),(4790,'Kocaeli','41',1219),(4791,'Konya','42',1219),(4792,'Ktahya','43',1219),(4793,'Malatya','44',1219),(4794,'Manisa','45',1219),(4795,'Mardin','47',1219),(4796,'Mugila','48',1219),(4797,'Mus','49',1219),(4798,'Nevs\'ehir','50',1219),(4799,'Nigide','51',1219),(4800,'Ordu','52',1219),(4801,'Osmaniye','80',1219),(4802,'Rize','53',1219),(4803,'Sakarya','54',1219),(4804,'Samsun','55',1219),(4805,'Siirt','56',1219),(4806,'Sinop','57',1219),(4807,'Sivas','58',1219),(4808,'S\'anliurfa','63',1219),(4809,'S\'rnak','73',1219),(4810,'Tekirdag','59',1219),(4811,'Tokat','60',1219),(4812,'Trabzon','61',1219),(4813,'Tunceli','62',1219),(4814,'Us\'ak','64',1219),(4815,'Van','65',1219),(4816,'Yalova','77',1219),(4817,'Yozgat','66',1219),(4818,'Zonguldak','67',1219),(4819,'Couva-Tabaquite-Talparo','CTT',1217),(4820,'Diego Martin','DMN',1217),(4821,'Eastern Tobago','ETO',1217),(4822,'Penal-Debe','PED',1217),(4823,'Princes Town','PRT',1217),(4824,'Rio Claro-Mayaro','RCM',1217),(4825,'Sangre Grande','SGE',1217),(4826,'San Juan-Laventille','SJL',1217),(4827,'Siparia','SIP',1217),(4828,'Tunapuna-Piarco','TUP',1217),(4829,'Western Tobago','WTO',1217),(4830,'Arima','ARI',1217),(4831,'Chaguanas','CHA',1217),(4832,'Point Fortin','PTF',1217),(4833,'Port of Spain','POS',1217),(4834,'San Fernando','SFO',1217),(4835,'Aileu','AL',1063),(4836,'Ainaro','AN',1063),(4837,'Bacucau','BA',1063),(4838,'Bobonaro','BO',1063),(4839,'Cova Lima','CO',1063),(4840,'Dili','DI',1063),(4841,'Ermera','ER',1063),(4842,'Laulem','LA',1063),(4843,'Liquica','LI',1063),(4844,'Manatuto','MT',1063),(4845,'Manafahi','MF',1063),(4846,'Oecussi','OE',1063),(4847,'Viqueque','VI',1063),(4848,'Changhua County','CHA',1208),(4849,'Chiayi County','CYQ',1208),(4850,'Hsinchu County','HSQ',1208),(4851,'Hualien County','HUA',1208),(4852,'Ilan County','ILA',1208),(4853,'Kaohsiung County','KHQ',1208),(4854,'Miaoli County','MIA',1208),(4855,'Nantou County','NAN',1208),(4856,'Penghu County','PEN',1208),(4857,'Pingtung County','PIF',1208),(4858,'Taichung County','TXQ',1208),(4859,'Tainan County','TNQ',1208),(4860,'Taipei County','TPQ',1208),(4861,'Taitung County','TTT',1208),(4862,'Taoyuan County','TAO',1208),(4863,'Yunlin County','YUN',1208),(4864,'Keelung City','KEE',1208),(4865,'Arusha','01',1210),(4866,'Dar-es-Salaam','02',1210),(4867,'Dodoma','03',1210),(4868,'Iringa','04',1210),(4869,'Kagera','05',1210),(4870,'Kaskazini Pemba','06',1210),(4871,'Kaskazini Unguja','07',1210),(4872,'Xigoma','08',1210),(4873,'Kilimanjaro','09',1210),(4874,'Rusini Pemba','10',1210),(4875,'Kusini Unguja','11',1210),(4876,'Lindi','12',1210),(4877,'Manyara','26',1210),(4878,'Mara','13',1210),(4879,'Mbeya','14',1210),(4880,'Mjini Magharibi','15',1210),(4881,'Morogoro','16',1210),(4882,'Mtwara','17',1210),(4883,'Pwani','19',1210),(4884,'Rukwa','20',1210),(4885,'Ruvuma','21',1210),(4886,'Shinyanga','22',1210),(4887,'Singida','23',1210),(4888,'Tabora','24',1210),(4889,'Tanga','25',1210),(4890,'Cherkas\'ka Oblast\'','71',1224),(4891,'Chernihivs\'ka Oblast\'','74',1224),(4892,'Chernivets\'ka Oblast\'','77',1224),(4893,'Dnipropetrovs\'ka Oblast\'','12',1224),(4894,'Donets\'ka Oblast\'','14',1224),(4895,'Ivano-Frankivs\'ka Oblast\'','26',1224),(4896,'Kharkivs\'ka Oblast\'','63',1224),(4897,'Khersons\'ka Oblast\'','65',1224),(4898,'Khmel\'nyts\'ka Oblast\'','68',1224),(4899,'Kirovohrads\'ka Oblast\'','35',1224),(4900,'Kyivs\'ka Oblast\'','32',1224),(4901,'Luhans\'ka Oblast\'','09',1224),(4902,'L\'vivs\'ka Oblast\'','46',1224),(4903,'Mykolaivs\'ka Oblast\'','48',1224),(4904,'Odes \'ka Oblast\'','51',1224),(4905,'Poltavs\'ka Oblast\'','53',1224),(4906,'Rivnens\'ka Oblast\'','56',1224),(4907,'Sums \'ka Oblast\'','59',1224),(4908,'Ternopil\'s\'ka Oblast\'','61',1224),(4909,'Vinnyts\'ka Oblast\'','05',1224),(4910,'Volyos\'ka Oblast\'','07',1224),(4911,'Zakarpats\'ka Oblast\'','21',1224),(4912,'Zaporiz\'ka Oblast\'','23',1224),(4913,'Zhytomyrs\'ka Oblast\'','18',1224),(4914,'Respublika Krym','43',1224),(4915,'Kyiv','30',1224),(4916,'Sevastopol','40',1224),(4917,'Adjumani','301',1223),(4918,'Apac','302',1223),(4919,'Arua','303',1223),(4920,'Bugiri','201',1223),(4921,'Bundibugyo','401',1223),(4922,'Bushenyi','402',1223),(4923,'Busia','202',1223),(4924,'Gulu','304',1223),(4925,'Hoima','403',1223),(4926,'Iganga','203',1223),(4927,'Jinja','204',1223),(4928,'Kabale','404',1223),(4929,'Kabarole','405',1223),(4930,'Kaberamaido','213',1223),(4931,'Kalangala','101',1223),(4932,'Kampala','102',1223),(4933,'Kamuli','205',1223),(4934,'Kamwenge','413',1223),(4935,'Kanungu','414',1223),(4936,'Kapchorwa','206',1223),(4937,'Kasese','406',1223),(4938,'Katakwi','207',1223),(4939,'Kayunga','112',1223),(4940,'Kibaale','407',1223),(4941,'Kiboga','103',1223),(4942,'Kisoro','408',1223),(4943,'Kitgum','305',1223),(4944,'Kotido','306',1223),(4945,'Kumi','208',1223),(4946,'Kyenjojo','415',1223),(4947,'Lira','307',1223),(4948,'Luwero','104',1223),(4949,'Masaka','105',1223),(4950,'Masindi','409',1223),(4951,'Mayuge','214',1223),(4952,'Mbale','209',1223),(4953,'Mbarara','410',1223),(4954,'Moroto','308',1223),(4955,'Moyo','309',1223),(4956,'Mpigi','106',1223),(4957,'Mubende','107',1223),(4958,'Mukono','108',1223),(4959,'Nakapiripirit','311',1223),(4960,'Nakasongola','109',1223),(4961,'Nebbi','310',1223),(4962,'Ntungamo','411',1223),(4963,'Pader','312',1223),(4964,'Pallisa','210',1223),(4965,'Rakai','110',1223),(4966,'Rukungiri','412',1223),(4967,'Sembabule','111',1223),(4968,'Sironko','215',1223),(4969,'Soroti','211',1223),(4970,'Tororo','212',1223),(4971,'Wakiso','113',1223),(4972,'Yumbe','313',1223),(4973,'Baker Island','81',1227),(4974,'Howland Island','84',1227),(4975,'Jarvis Island','86',1227),(4976,'Johnston Atoll','67',1227),(4977,'Kingman Reef','89',1227),(4978,'Midway Islands','71',1227),(4979,'Navassa Island','76',1227),(4980,'Palmyra Atoll','95',1227),(4981,'Wake Island','79',1227),(4982,'Artigsa','AR',1229),(4983,'Canelones','CA',1229),(4984,'Cerro Largo','CL',1229),(4985,'Colonia','CO',1229),(4986,'Durazno','DU',1229),(4987,'Flores','FS',1229),(4988,'Lavalleja','LA',1229),(4989,'Maldonado','MA',1229),(4990,'Montevideo','MO',1229),(4991,'Paysandu','PA',1229),(4992,'Rivera','RV',1229),(4993,'Rocha','RO',1229),(4994,'Salto','SA',1229),(4995,'Soriano','SO',1229),(4996,'Tacuarembo','TA',1229),(4997,'Treinta y Tres','TT',1229),(4998,'Toshkent (city)','TK',1230),(4999,'Qoraqalpogiston Respublikasi','QR',1230),(5000,'Andijon','AN',1230),(5001,'Buxoro','BU',1230),(5002,'Farg\'ona','FA',1230),(5003,'Jizzax','JI',1230),(5004,'Khorazm','KH',1230),(5005,'Namangan','NG',1230),(5006,'Navoiy','NW',1230),(5007,'Qashqadaryo','QA',1230),(5008,'Samarqand','SA',1230),(5009,'Sirdaryo','SI',1230),(5010,'Surxondaryo','SU',1230),(5011,'Toshkent','TO',1230),(5012,'Xorazm','XO',1230),(5013,'Distrito Federal','A',1232),(5014,'Anzoategui','B',1232),(5015,'Apure','C',1232),(5016,'Aragua','D',1232),(5017,'Barinas','E',1232),(5018,'Carabobo','G',1232),(5019,'Cojedes','H',1232),(5020,'Falcon','I',1232),(5021,'Guarico','J',1232),(5022,'Lara','K',1232),(5023,'Merida','L',1232),(5024,'Miranda','M',1232),(5025,'Monagas','N',1232),(5026,'Nueva Esparta','O',1232),(5027,'Portuguesa','P',1232),(5028,'Tachira','S',1232),(5029,'Trujillo','T',1232),(5030,'Vargas','X',1232),(5031,'Yaracuy','U',1232),(5032,'Zulia','V',1232),(5033,'Delta Amacuro','Y',1232),(5034,'Dependencias Federales','W',1232),(5035,'An Giang','44',1233),(5036,'Ba Ria - Vung Tau','43',1233),(5037,'Bac Can','53',1233),(5038,'Bac Giang','54',1233),(5039,'Bac Lieu','55',1233),(5040,'Bac Ninh','56',1233),(5041,'Ben Tre','50',1233),(5042,'Binh Dinh','31',1233),(5043,'Binh Duong','57',1233),(5044,'Binh Phuoc','58',1233),(5045,'Binh Thuan','40',1233),(5046,'Ca Mau','59',1233),(5047,'Can Tho','48',1233),(5048,'Cao Bang','04',1233),(5049,'Da Nang, thanh pho','60',1233),(5050,'Dong Nai','39',1233),(5051,'Dong Thap','45',1233),(5052,'Gia Lai','30',1233),(5053,'Ha Giang','03',1233),(5054,'Ha Nam','63',1233),(5055,'Ha Noi, thu do','64',1233),(5056,'Ha Tay','15',1233),(5057,'Ha Tinh','23',1233),(5058,'Hai Duong','61',1233),(5059,'Hai Phong, thanh pho','62',1233),(5060,'Hoa Binh','14',1233),(5061,'Ho Chi Minh, thanh pho [Sai Gon]','65',1233),(5062,'Hung Yen','66',1233),(5063,'Khanh Hoa','34',1233),(5064,'Kien Giang','47',1233),(5065,'Kon Tum','28',1233),(5066,'Lai Chau','01',1233),(5067,'Lam Dong','35',1233),(5068,'Lang Son','09',1233),(5069,'Lao Cai','02',1233),(5070,'Long An','41',1233),(5071,'Nam Dinh','67',1233),(5072,'Nghe An','22',1233),(5073,'Ninh Binh','18',1233),(5074,'Ninh Thuan','36',1233),(5075,'Phu Tho','68',1233),(5076,'Phu Yen','32',1233),(5077,'Quang Binh','24',1233),(5078,'Quang Nam','27',1233),(5079,'Quang Ngai','29',1233),(5080,'Quang Ninh','13',1233),(5081,'Quang Tri','25',1233),(5082,'Soc Trang','52',1233),(5083,'Son La','05',1233),(5084,'Tay Ninh','37',1233),(5085,'Thai Binh','20',1233),(5086,'Thai Nguyen','69',1233),(5087,'Thanh Hoa','21',1233),(5088,'Thua Thien-Hue','26',1233),(5089,'Tien Giang','46',1233),(5090,'Tra Vinh','51',1233),(5091,'Tuyen Quang','07',1233),(5092,'Vinh Long','49',1233),(5093,'Vinh Phuc','70',1233),(5094,'Yen Bai','06',1233),(5095,'Malampa','MAP',1231),(5096,'Penama','PAM',1231),(5097,'Sanma','SAM',1231),(5098,'Shefa','SEE',1231),(5099,'Tafea','TAE',1231),(5100,'Torba','TOB',1231),(5101,'A\'ana','AA',1185),(5102,'Aiga-i-le-Tai','AL',1185),(5103,'Atua','AT',1185),(5104,'Fa\'aaaleleaga','FA',1185),(5105,'Gaga\'emauga','GE',1185),(5106,'Gagaifomauga','GI',1185),(5107,'Palauli','PA',1185),(5108,'Satupa\'itea','SA',1185),(5109,'Tuamasaga','TU',1185),(5110,'Va\'a-o-Fonoti','VF',1185),(5111,'Vaisigano','VS',1185),(5112,'Crna Gora','CG',1243),(5113,'Srbija','SR',1242),(5114,'Kosovo-Metohija','KM',1242),(5115,'Vojvodina','VO',1242),(5116,'Abyan','AB',1237),(5117,'Adan','AD',1237),(5118,'Ad Dali','DA',1237),(5119,'Al Bayda\'','BA',1237),(5120,'Al Hudaydah','MU',1237),(5121,'Al Mahrah','MR',1237),(5122,'Al Mahwit','MW',1237),(5123,'Amran','AM',1237),(5124,'Dhamar','DH',1237),(5125,'Hadramawt','HD',1237),(5126,'Hajjah','HJ',1237),(5127,'Ibb','IB',1237),(5128,'Lahij','LA',1237),(5129,'Ma\'rib','MA',1237),(5130,'Sa\'dah','SD',1237),(5131,'San\'a\'','SN',1237),(5132,'Shabwah','SH',1237),(5133,'Ta\'izz','TA',1237),(5134,'Eastern Cape','EC',1196),(5135,'Free State','FS',1196),(5136,'Gauteng','GT',1196),(5137,'Kwazulu-Natal','NL',1196),(5138,'Mpumalanga','MP',1196),(5139,'Northern Cape','NC',1196),(5140,'Limpopo','NP',1196),(5141,'Western Cape','WC',1196),(5142,'Copperbelt','08',1239),(5143,'Luapula','04',1239),(5144,'Lusaka','09',1239),(5145,'North-Western','06',1239),(5146,'Bulawayo','BU',1240),(5147,'Harare','HA',1240),(5148,'Manicaland','MA',1240),(5149,'Mashonaland Central','MC',1240),(5150,'Mashonaland East','ME',1240),(5151,'Mashonaland West','MW',1240),(5152,'Masvingo','MV',1240),(5153,'Matabeleland North','MN',1240),(5154,'Matabeleland South','MS',1240),(5155,'Midlands','MI',1240),(5156,'South Karelia','SK',1075),(5157,'South Ostrobothnia','SO',1075),(5158,'Etelä-Savo','ES',1075),(5159,'Häme','HH',1075),(5160,'Itä-Uusimaa','IU',1075),(5161,'Kainuu','KA',1075),(5162,'Central Ostrobothnia','CO',1075),(5163,'Central Finland','CF',1075),(5164,'Kymenlaakso','KY',1075),(5165,'Lapland','LA',1075),(5166,'Tampere Region','TR',1075),(5167,'Ostrobothnia','OB',1075),(5168,'North Karelia','NK',1075),(5169,'Northern Ostrobothnia','NO',1075),(5170,'Northern Savo','NS',1075),(5171,'Päijät-Häme','PH',1075),(5172,'Satakunta','SK',1075),(5173,'Uusimaa','UM',1075),(5174,'South-West Finland','SW',1075),(5175,'Åland','AL',1075),(5176,'Limburg','LI',1152),(5177,'Central and Western','CW',1098),(5178,'Eastern','EA',1098),(5179,'Southern','SO',1098),(5180,'Wan Chai','WC',1098),(5181,'Kowloon City','KC',1098),(5182,'Kwun Tong','KU',1098),(5183,'Sham Shui Po','SS',1098),(5184,'Wong Tai Sin','WT',1098),(5185,'Yau Tsim Mong','YT',1098),(5186,'Islands','IS',1098),(5187,'Kwai Tsing','KI',1098),(5188,'North','NO',1098),(5189,'Sai Kung','SK',1098),(5190,'Sha Tin','ST',1098),(5191,'Tai Po','TP',1098),(5192,'Tsuen Wan','TW',1098),(5193,'Tuen Mun','TM',1098),(5194,'Yuen Long','YL',1098),(5195,'Manchester','MR',1108),(5196,'Al Manāmah (Al ‘Āşimah)','13',1016),(5197,'Al Janūbīyah','14',1016),(5199,'Al Wusţá','16',1016),(5200,'Ash Shamālīyah','17',1016),(5201,'Jenin','_A',1165),(5202,'Tubas','_B',1165),(5203,'Tulkarm','_C',1165),(5204,'Nablus','_D',1165),(5205,'Qalqilya','_E',1165),(5206,'Salfit','_F',1165),(5207,'Ramallah and Al-Bireh','_G',1165),(5208,'Jericho','_H',1165),(5209,'Jerusalem','_I',1165),(5210,'Bethlehem','_J',1165),(5211,'Hebron','_K',1165),(5212,'North Gaza','_L',1165),(5213,'Gaza','_M',1165),(5214,'Deir el-Balah','_N',1165),(5215,'Khan Yunis','_O',1165),(5216,'Rafah','_P',1165),(5217,'Brussels','BRU',1020),(5218,'Distrito Federal','DIF',1140),(5219,'Taichung City','TXG',1208),(5220,'Kaohsiung City','KHH',1208),(5221,'Taipei City','TPE',1208),(5222,'Chiayi City','CYI',1208),(5223,'Hsinchu City','HSZ',1208),(5224,'Tainan City','TNN',1208),(9000,'North West','NW',1196),(9986,'Tyne and Wear','TWR',1226),(9988,'Greater Manchester','GTM',1226),(9989,'Co Tyrone','TYR',1226),(9990,'West Yorkshire','WYK',1226),(9991,'South Yorkshire','SYK',1226),(9992,'Merseyside','MSY',1226),(9993,'Berkshire','BRK',1226),(9994,'West Midlands','WMD',1226),(9998,'West Glamorgan','WGM',1226),(9999,'London','LON',1226),(10000,'Carbonia-Iglesias','CI',1107),(10001,'Olbia-Tempio','OT',1107),(10002,'Medio Campidano','VS',1107),(10003,'Ogliastra','OG',1107),(10009,'Jura','39',1076),(10010,'Barletta-Andria-Trani','BT',1107),(10011,'Fermo','FM',1107),(10012,'Monza e Brianza','MB',1107),(10013,'Clwyd','CWD',1226),(10015,'South Glamorgan','SGM',1226),(10016,'Artibonite','AR',1094),(10017,'Centre','CE',1094),(10018,'Nippes','NI',1094),(10019,'Nord','ND',1094),(10020,'La Rioja','F',1010),(10021,'Andorra la Vella','07',1005),(10022,'Canillo','02',1005),(10023,'Encamp','03',1005),(10024,'Escaldes-Engordany','08',1005),(10025,'La Massana','04',1005),(10026,'Ordino','05',1005),(10027,'Sant Julia de Loria','06',1005),(10028,'Abaco Islands','AB',1212),(10029,'Andros Island','AN',1212),(10030,'Berry Islands','BR',1212),(10031,'Eleuthera','EL',1212),(10032,'Grand Bahama','GB',1212),(10033,'Rum Cay','RC',1212),(10034,'San Salvador Island','SS',1212),(10035,'Kongo central','01',1050),(10036,'Kwango','02',1050),(10037,'Kwilu','03',1050),(10038,'Mai-Ndombe','04',1050),(10039,'Kasai','05',1050),(10040,'Lulua','06',1050),(10041,'Lomami','07',1050),(10042,'Sankuru','08',1050),(10043,'Ituri','09',1050),(10044,'Haut-Uele','10',1050),(10045,'Tshopo','11',1050),(10046,'Bas-Uele','12',1050),(10047,'Nord-Ubangi','13',1050),(10048,'Mongala','14',1050),(10049,'Sud-Ubangi','15',1050),(10050,'Tshuapa','16',1050),(10051,'Haut-Lomami','17',1050),(10052,'Lualaba','18',1050),(10053,'Haut-Katanga','19',1050),(10054,'Tanganyika','20',1050),(10055,'Toledo','TO',1198),(10056,'Córdoba','CO',1198),(10057,'Metropolitan Manila','MNL',1170),(10058,'La Paz','LP',1097),(10059,'Yinchuan','YN',1045),(10060,'Shizuishan','SZ',1045),(10061,'Wuzhong','WZ',1045),(10062,'Guyuan','GY',1045),(10063,'Zhongwei','ZW',1045),(10064,'Luxembourg','L',1126),(10065,'Aizkraukles novads','002',1119),(10066,'Jaunjelgavas novads','038',1119),(10067,'Pļaviņu novads','072',1119),(10068,'Kokneses novads','046',1119),(10069,'Neretas novads','065',1119),(10070,'Skrīveru novads','092',1119),(10071,'Alūksnes novads','007',1119),(10072,'Apes novads','009',1119),(10073,'Balvu novads','015',1119),(10074,'Viļakas novads','108',1119),(10075,'Baltinavas novads','014',1119),(10076,'Rugāju novads','082',1119),(10077,'Bauskas novads','016',1119),(10078,'Iecavas novads','034',1119),(10079,'Rundāles novads','083',1119),(10080,'Vecumnieku novads','105',1119),(10081,'Cēsu novads','022',1119),(10082,'Līgatnes novads','055',1119),(10083,'Amatas novads','008',1119),(10084,'Jaunpiebalgas novads','039',1119),(10085,'Priekuļu novads','075',1119),(10086,'Pārgaujas novads','070',1119),(10087,'Raunas novads','076',1119),(10088,'Vecpiebalgas novads','104',1119),(10089,'Daugavpils novads','025',1119),(10090,'Ilūkstes novads','036',1119),(10091,'Dobeles novads','026',1119),(10092,'Auces novads','010',1119),(10093,'Tērvetes novads','098',1119),(10094,'Gulbenes novads','033',1119),(10095,'Jelgavas novads','041',1119),(10096,'Ozolnieku novads','069',1119),(10097,'Jēkabpils novads','042',1119),(10098,'Aknīstes novads','004',1119),(10099,'Viesītes novads','107',1119),(10100,'Krustpils novads','049',1119),(10101,'Salas novads','085',1119),(10102,'Krāslavas novads','047',1119),(10103,'Dagdas novads','024',1119),(10104,'Aglonas novads','001',1119),(10105,'Kuldīgas novads','050',1119),(10106,'Skrundas novads','093',1119),(10107,'Alsungas novads','006',1119),(10108,'Aizputes novads','003',1119),(10109,'Durbes novads','028',1119),(10110,'Grobiņas novads','032',1119),(10111,'Pāvilostas novads','071',1119),(10112,'Priekules novads','074',1119),(10113,'Nīcas novads','066',1119),(10114,'Rucavas novads','081',1119),(10115,'Vaiņodes novads','100',1119),(10116,'Limbažu novads','054',1119),(10117,'Alojas novads','005',1119),(10118,'Salacgrīvas novads','086',1119),(10119,'Ludzas novads','058',1119),(10120,'Kārsavas novads','044',1119),(10121,'Zilupes novads','110',1119),(10122,'Ciblas novads','023',1119),(10123,'Madonas novads','059',1119),(10124,'Cesvaines novads','021',1119),(10125,'Lubānas novads','057',1119),(10126,'Varakļānu novads','102',1119),(10127,'Ērgļu novads','030',1119),(10128,'Ogres novads','067',1119),(10129,'Ikšķiles novads','035',1119),(10130,'Ķeguma novads','051',1119),(10131,'Lielvārdes novads','053',1119),(10132,'Preiļu novads','073',1119),(10133,'Līvānu novads','056',1119),(10134,'Riebiņu novads','078',1119),(10135,'Vārkavas novads','103',1119),(10136,'Rēzeknes novads','077',1119),(10137,'Viļānu novads','109',1119),(10138,'Baldones novads','013',1119),(10139,'Ķekavas novads','052',1119),(10140,'Olaines novads','068',1119),(10141,'Salaspils novads','087',1119),(10142,'Saulkrastu novads','089',1119),(10143,'Siguldas novads','091',1119),(10144,'Inčukalna novads','037',1119),(10145,'Ādažu novads','011',1119),(10146,'Babītes novads','012',1119),(10147,'Carnikavas novads','020',1119),(10148,'Garkalnes novads','031',1119),(10149,'Krimuldas novads','048',1119),(10150,'Mālpils novads','061',1119),(10151,'Mārupes novads','062',1119),(10152,'Ropažu novads','080',1119),(10153,'Sējas novads','090',1119),(10154,'Stopiņu novads','095',1119),(10155,'Saldus novads','088',1119),(10156,'Brocēnu novads','018',1119),(10157,'Talsu novads','097',1119),(10158,'Dundagas novads','027',1119),(10159,'Mērsraga novads','063',1119),(10160,'Rojas novads','079',1119),(10161,'Tukuma novads','099',1119),(10162,'Kandavas novads','043',1119),(10163,'Engures novads','029',1119),(10164,'Jaunpils novads','040',1119),(10165,'Valkas novads','101',1119),(10166,'Smiltenes novads','094',1119),(10167,'Strenču novads','096',1119),(10168,'Kocēnu novads','045',1119),(10169,'Mazsalacas novads','060',1119),(10170,'Rūjienas novads','084',1119),(10171,'Beverīnas novads','017',1119),(10172,'Burtnieku novads','019',1119),(10173,'Naukšēnu novads','064',1119),(10174,'Ventspils novads','106',1119),(10175,'Jēkabpils','JKB',1119),(10176,'Valmiera','VMR',1119),(10177,'Florida','FL',1229),(10178,'Rio Negro','RN',1229),(10179,'San Jose','SJ',1229),(10180,'Plateau','PL',1157),(10181,'Pieria','61',1085),(10182,'Los Rios','LR',1044),(10183,'Arica y Parinacota','AP',1044),(10184,'Amazonas','AMA',1169),(10185,'Kalimantan Tengah','KT',1102),(10186,'Sulawesi Barat','SR',1102),(10187,'Kalimantan Utara','KU',1102),(10188,'Ankaran','86',1193),(10189,'Apače','87',1193),(10190,'Cirkulane','88',1193),(10191,'Gorje','89',1193),(10192,'Kostanjevica na Krki','90',1193),(10193,'Log-Dragomer','91',1193),(10194,'Makole','92',1193),(10195,'Mirna','93',1193),(10196,'Mokronog-Trebelno','94',1193),(10197,'Odranci','95',1193),(10198,'Oplotnica','96',1193),(10199,'Ormož','97',1193),(10200,'Osilnica','98',1193),(10201,'Pesnica','99',1193),(10202,'Piran','100',1193),(10203,'Pivka','101',1193),(10204,'Podčetrtek','102',1193),(10205,'Podlehnik','103',1193),(10206,'Podvelka','104',1193),(10207,'Poljčane','105',1193),(10208,'Polzela','106',1193),(10209,'Postojna','107',1193),(10210,'Prebold','108',1193),(10211,'Preddvor','109',1193),(10212,'Prevalje','110',1193),(10213,'Ptuj','111',1193),(10214,'Puconci','112',1193),(10215,'Rače-Fram','113',1193),(10216,'Radeče','114',1193),(10217,'Radenci','115',1193),(10218,'Radlje ob Dravi','139',1193),(10219,'Radovljica','145',1193),(10220,'Ravne na Koroškem','171',1193),(10221,'Razkrižje','172',1193),(10222,'Rečica ob Savinji','173',1193),(10223,'Renče-Vogrsko','174',1193),(10224,'Ribnica','175',1193),(10225,'Ribnica na Pohorju','176',1193),(10226,'Rogaška Slatina','177',1193),(10227,'Rogašovci','178',1193),(10228,'Rogatec','179',1193),(10229,'Ruše','180',1193),(10230,'Selnica ob Dravi','195',1193),(10231,'Semič','196',1193),(10232,'Šentrupert','197',1193),(10233,'Sevnica','198',1193),(10234,'Sežana','199',1193),(10235,'Slovenj Gradec','200',1193),(10236,'Slovenska Bistrica','201',1193),(10237,'Slovenske Konjice','202',1193),(10238,'Šmarješke Toplice','203',1193),(10239,'Sodražica','204',1193),(10240,'Solčava','205',1193),(10241,'Središče ob Dravi','206',1193),(10242,'Starše','207',1193),(10243,'Straža','208',1193),(10244,'Sveta Trojica v Slovenskih goricah','209',1193),(10245,'Sveti Jurij v Slovenskih goricah','210',1193),(10246,'Sveti Tomaž','211',1193),(10247,'Vodice','212',1193),(10248,'Abkhazia','AB',1081),(10249,'Adjara','AJ',1081),(10250,'Tbilisi','TB',1081),(10251,'Guria','GU',1081),(10252,'Imereti','IM',1081),(10253,'Kakheti','KA',1081),(10254,'Kvemo Kartli','KK',1081),(10255,'Mtskheta-Mtianeti','MM',1081),(10256,'Racha-Lechkhumi and Kvemo Svaneti','RL',1081),(10257,'Samegrelo-Zemo Svaneti','SZ',1081),(10258,'Samtskhe-Javakheti','SJ',1081),(10259,'Shida Kartli','SK',1081),(10260,'Central','C',1074),(10261,'Punjab','PB',1163),(10262,'La Libertad','LI',1066),(10263,'La Paz','PA',1066),(10264,'La Union','UN',1066),(10265,'Littoral','LT',1038),(10266,'Nord-Ouest','NW',1038),(10267,'Telangana','TG',1101),(10268,'Ash Sharqiyah','04',1187),(10269,'Guadeloupe','GP',1076),(10270,'Martinique','MQ',1076),(10271,'Guyane','GF',1076),(10272,'La Réunion','RE',1076),(10273,'Mayotte','YT',1076),(10274,'Baringo','01',1112),(10275,'Bomet','02',1112),(10276,'Bungoma','03',1112),(10277,'Busia','04',1112),(10278,'Elgeyo/Marakwet','05',1112),(10279,'Embu','06',1112),(10280,'Garissa','07',1112),(10281,'Homa Bay','08',1112),(10282,'Isiolo','09',1112),(10283,'Kajiado','10',1112),(10284,'Kakamega','11',1112),(10285,'Kericho','12',1112),(10286,'Kiambu','13',1112),(10287,'Kilifi','14',1112),(10288,'Kirinyaga','15',1112),(10289,'Kisii','16',1112),(10290,'Kisumu','17',1112),(10291,'Kitui','18',1112),(10292,'Kwale','19',1112),(10293,'Laikipia','20',1112),(10294,'Lamu','21',1112),(10295,'Machakos','22',1112),(10296,'Makueni','23',1112),(10297,'Mandera','24',1112),(10298,'Marsabit','25',1112),(10299,'Meru','26',1112),(10300,'Migori','27',1112),(10301,'Mombasa','28',1112),(10302,'Murang\'a','29',1112),(10303,'Nairobi City','30',1112),(10304,'Nakuru','31',1112),(10305,'Nandi','32',1112),(10306,'Narok','33',1112),(10307,'Nyamira','34',1112),(10308,'Nyandarua','35',1112),(10309,'Nyeri','36',1112),(10310,'Samburu','37',1112),(10311,'Siaya','38',1112),(10312,'Taita/Taveta','39',1112),(10313,'Tana River','40',1112),(10314,'Tharaka-Nithi','41',1112),(10315,'Trans Nzoia','42',1112),(10316,'Turkana','43',1112),(10317,'Uasin Gishu','44',1112),(10318,'Vihiga','45',1112),(10319,'Wajir','46',1112),(10320,'West Pokot','47',1112),(10321,'Chandigarh','CH',1101),(10322,'Central','CP',1083),(10323,'Eastern','EP',1083),(10324,'Northern','NP',1083),(10325,'Western','WP',1083),(10326,'Saint Kitts','K',1181),(10327,'Nevis','N',1181),(10328,'Eastern','E',1190),(10329,'Northern','N',1190),(10330,'Southern','S',1190),(10331,'Dushanbe','DU',1209),(10332,'Nohiyahoi Tobei Jumhurí','RA',1209),(10333,'Wallis-et-Futuna','WF',1076),(10334,'Nouvelle-Calédonie','NC',1076),(10335,'Haute-Marne','52',1076),(10336,'Saint George','03',1009),(10337,'Saint John','04',1009),(10338,'Saint Mary','05',1009),(10339,'Saint Paul','06',1009),(10340,'Saint Peter','07',1009),(10341,'Saint Philip','08',1009),(10342,'Barbuda','10',1009),(10343,'Redonda','11',1009),(10344,'Christ Church','01',1018),(10345,'Saint Andrew','02',1018),(10346,'Saint George','03',1018),(10347,'Saint James','04',1018),(10348,'Saint John','05',1018),(10349,'Saint Joseph','06',1018),(10350,'Saint Lucy','07',1018),(10351,'Saint Michael','08',1018),(10352,'Saint Peter','09',1018),(10353,'Saint Philip','10',1018),(10354,'Saint Thomas','11',1018),(10355,'Estuaire','01',1080),(10356,'Haut-Ogooué','02',1080),(10357,'Moyen-Ogooué','03',1080),(10358,'Ngounié','04',1080),(10359,'Nyanga','05',1080),(10360,'Ogooué-Ivindo','06',1080),(10361,'Ogooué-Lolo','07',1080),(10362,'Ogooué-Maritime','08',1080),(10363,'Woleu-Ntem','09',1080),(10364,'Monmouthshire','MON',1226),(10365,'Antrim and Newtownabbey','ANN',1226),(10366,'Ards and North Down','AND',1226),(10367,'Armagh City, Banbridge and Craigavon','ABC',1226),(10368,'Belfast','BFS',1226),(10369,'Causeway Coast and Glens','CCG',1226),(10370,'Derry City and Strabane','DRS',1226),(10371,'Fermanagh and Omagh','FMO',1226),(10372,'Lisburn and Castlereagh','LBC',1226),(10373,'Mid and East Antrim','MEA',1226),(10374,'Mid Ulster','MUL',1226),(10375,'Newry, Mourne and Down','NMD',1226),(10376,'Bridgend','BGE',1226),(10377,'Caerphilly','CAY',1226),(10378,'Cardiff','CRF',1226),(10379,'Carmarthenshire','CRF',1226),(10380,'Ceredigion','CGN',1226),(10381,'Conwy','CWY',1226),(10382,'Denbighshire','DEN',1226),(10383,'Flintshire','FLN',1226),(10384,'Isle of Anglesey','AGY',1226),(10385,'Merthyr Tydfil','MTY',1226),(10386,'Neath Port Talbot','NTL',1226),(10387,'Newport','NWP',1226),(10388,'Pembrokeshire','PEM',1226),(10389,'Rhondda, Cynon, Taff','RCT',1226),(10390,'Swansea','SWA',1226),(10391,'Torfaen','TOF',1226),(10392,'Wrexham','WRX',1226);
/*!40000 ALTER TABLE `civicrm_state_province` ENABLE KEYS */;
UNLOCK TABLES;
VALUES
( @domainID, CONCAT('civicrm/report/instance/', @instanceID,'&reset=1'), 'Mailing Detail Report', 'Mailing Detail Report', 'administer CiviMail', 'OR', @reportlastID, '1', NULL, @instanceID+2 );
UPDATE civicrm_report_instance SET navigation_id = LAST_INSERT_ID() WHERE id = @instanceID;
-UPDATE civicrm_domain SET version = '5.32.alpha1';
+UPDATE civicrm_domain SET version = '5.33.alpha1';
{ts}None found.{/ts}
</div>
{/if}
- <div class="action-link">
- {crmButton q="action=add&reset=1" id="newMailSettings" icon="plus-circle"}{ts}Add Mail Account{/ts}{/crmButton}
- {crmButton p="civicrm/admin" q="reset=1" class="cancel" icon="times"}{ts}Done{/ts}{/crmButton}
- </div>
+ {if $setupActions}
+ <form>
+ <select id="crm-mail-setup" name="crm-mail-setup" class="crm-select2 crm-form-select" aria-label="{ts}Add Mail Account{/ts}">
+ <option value="" aria-hidden="true">{ts}Add Mail Account{/ts}</option>
+ {foreach from=$setupActions key=setupActionsName item=setupAction}
+ <option value="{$setupActionsName|escape}">{$setupAction.title|escape}</option>
+ {/foreach}
+ </select>
+ </form>
+ {else}
+ <div class="action-link">
+ {crmButton q="action=add&reset=1" id="newMailSettings" icon="plus-circle"}{ts}Add Mail Account{/ts}{/crmButton}
+ {crmButton p="civicrm/admin" q="reset=1" class="cancel" icon="times"}{ts}Done{/ts}{/crmButton}
+ </div>
+ {/if}
+
{/if}
</div>
+{literal}
+ <script type="text/javascript">
+ cj('#crm-mail-setup').val('');
+ cj('#crm-mail-setup').on('select2-selecting', function(event) {
+ if (!event.val) {
+ return;
+ }
+ event.stopPropagation();
+ var url = CRM.url('civicrm/ajax/setupMailAccount', {type: event.val});
+ window.location = url;
+ });
+ </script>
+{/literal}
<div class="block-crm crm-container">
<form method="post" id="id_fulltext_search">
<div style="margin-bottom: 8px;">
- <input type="text" name="text" id='text' value="" class="crm-form-text" style="width: 10em;" /> <button type="submit" name="submit" id="fulltext_submit" class="crm-button crm-form-submit" onclick='submitForm();'>{ts}Go{/ts}</button>
+ <input type="text" name="text" id='text' value="" class="crm-form-text" />
<input type="hidden" name="qfKey" value="{crmKey name='CRM_Contact_Controller_Search' addSequence=1}" />
</div>
<select class="form-select" id="fulltext_table" name="fulltext_table">
<option value="Membership">{ts}Memberships{/ts}</option>
{/if}
</select> {help id="id-fullText" file="CRM/Contact/Form/Search/Custom/FullText.hlp"}
+ <div class="crm-submit-buttons"><button type="submit" name="submit" id="fulltext_submit" class="crm-button crm-form-submit" onclick='submitForm();'>{ts}Search{/ts}</button></div>
</form>
</div>
<div class="form-item">
<table class="form-layout-compressed">
<tr>
- <td class="label">{$form.text.label}</td>
- <td>{$form.text.html}</td>
- <td class="label">{ts}in...{/ts}</td>
- <td>{$form.table.html}</td>
+ <td>
+ <label>{$form.text.label}</label>
+ {$form.text.html}
+ </td>
+ <td>
+ <label>{ts}in...{/ts}</label>
+ {$form.table.html}
+ </td>
<td>{$form.buttons.html} {help id="id-fullText"}</td>
</tr>
</table>
<div class="premium-full-title">{$row.name}</div>
<div class="premium-full-disabled">
{ts 1=$row.min_contribution|crmMoney}You must contribute at least %1 to get this item{/ts}<br/>
- <button type="button" value="{ts 1=$row.min_contribution|crmMoney}Contribute %1 Instead{/ts}" amount="{$row.min_contribution}" />
+ <button type="button" amount="{$row.min_contribution}">
+ {ts 1=$row.min_contribution|crmMoney}Contribute %1 Instead{/ts}
+ </button>
</div>
<div class="premium-full-description">
{$row.description}
</div>
{/if}
<table class="form-layout-compressed">
- <tr class="crm-contribution-contributionpage-amount-form-block-is_monetary"><th scope="row" class="label" width="20%">{$form.is_monetary.label}</th>
+ <tr class="crm-contribution-contributionpage-amount-form-block-is_monetary"><td scope="row" class="label" width="20%">{$form.is_monetary.label}</td>
<td>{$form.is_monetary.html}<br />
<span class="description">{ts}Uncheck this box if you are using this contribution page for free membership signup ONLY, or to solicit in-kind / non-monetary donations such as furniture, equipment.. etc.{/ts}</span></td>
</tr>
- <tr class="crm-contribution-contributionpage-amount-form-block-currency"><th scope="row" class="label" width="20%">{$form.currency.label}</th>
+ <tr class="crm-contribution-contributionpage-amount-form-block-currency"><td scope="row" class="label" width="20%">{$form.currency.label}</td>
<td>{$form.currency.html}<br />
<span class="description">{ts}Select the currency to be used for contributions submitted from this contribution page.{/ts}</span></td>
</tr>
{if $paymentProcessor}
- <tr class="crm-contribution-contributionpage-amount-form-block-payment_processor"><th scope="row" class="label" width="20%">{$form.payment_processor.label}</th>
+ <tr class="crm-contribution-contributionpage-amount-form-block-payment_processor"><td scope="row" class="label" width="20%">{$form.payment_processor.label}</td>
<td>{$form.payment_processor.html}<br />
<span class="description">{ts}Select the payment processor to be used for contributions submitted from this contribution page (unless you are soliciting non-monetary / in-kind contributions only).{/ts} {docURL page="user/contributions/payment-processors"}</span></td>
</tr>
{/if}
- <tr class="crm-contribution-contributionpage-amount-form-block-is_pay_later"><th scope="row" class="label">{$form.is_pay_later.label}</th>
+ <tr class="crm-contribution-contributionpage-amount-form-block-is_pay_later"><td scope="row" class="label">{$form.is_pay_later.label}</td>
<td>{$form.is_pay_later.html}<br />
<span class="description">{ts}Check this box if you want to give users the option to submit payment offline (e.g. mail in a check, call in a credit card, etc.).{/ts}</span></td>
</tr>
<tr id="payLaterFields" class="crm-contribution-form-block-payLaterFields"><td> </td>
<td>
<table class="form-layout">
- <tr class="crm-contribution-contributionpage-amount-form-block-pay_later_text"><th scope="row" class="label">{$form.pay_later_text.label} <span class="crm-marker" title="This field is required.">*</span> {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_contribution_page' field='pay_later_text' id=$contributionPageID}{/if}</th>
+ <tr class="crm-contribution-contributionpage-amount-form-block-pay_later_text"><td scope="row" class="label">{$form.pay_later_text.label} <span class="crm-marker" title="This field is required.">*</span> {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_contribution_page' field='pay_later_text' id=$contributionPageID}{/if}</td>
<td>{$form.pay_later_text.html|crmAddClass:big}<br />
<span class="description">{ts}Text displayed next to the checkbox for the 'pay later' option on the contribution form. You may include HTML formatting tags.{/ts}</span></td></tr>
- <tr class="crm-contribution-contributionpage-amount-form-block-pay_later_receipt"><th scope="row" class="label">{$form.pay_later_receipt.label} <span class="crm-marker" title="This field is required.">*</span> {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_contribution_page' field='pay_later_receipt' id=$contributionPageID}{/if}</th>
+ <tr class="crm-contribution-contributionpage-amount-form-block-pay_later_receipt"><td scope="row" class="label">{$form.pay_later_receipt.label} <span class="crm-marker" title="This field is required.">*</span> {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_contribution_page' field='pay_later_receipt' id=$contributionPageID}{/if}</td>
<td>{$form.pay_later_receipt.html|crmAddClass:big}<br />
<span class="description">{ts}Instructions added to Confirmation and Thank-you pages, as well as the confirmation email, when the user selects the 'pay later' option (e.g. 'Mail your check to ... within 3 business days.').{/ts}</span></td></tr>
- <tr><th scope="row" class="label">{$form.is_billing_required.label}</th>
+ <tr><td scope="row" class="label">{$form.is_billing_required.label}</td>
<td>{$form.is_billing_required.html}<br />
<span class="description">{ts}Check this box to require users who select the pay later option to provide billing name and address.{/ts}</span>
</td></tr>
</td>
</tr>
<tr class="crm-contribution-contributionpage-amount-form-block-amount_block_is_active">
- <th scope="row" class="label">{$form.amount_block_is_active.label}</th>
+ <td scope="row" class="label">{$form.amount_block_is_active.label}</td>
<td>{$form.amount_block_is_active.html}<br />
<span class="description">{ts}Uncheck this box if you are using this contribution page for membership signup and renewal only – and you do NOT want users to select or enter any additional contribution amounts.{/ts}</span></td>
</tr>
<tr id="priceSet" class="crm-contribution-contributionpage-amount-form-block-priceSet">
- <th scope="row" class="label">{$form.price_set_id.label}</th>
+ <td scope="row" class="label">{$form.price_set_id.label}</td>
{if $price eq true}
<td>{$form.price_set_id.html}<br /><span class="description">{ts 1=$adminPriceSets}Select a pre-configured Price Set to offer multiple individually priced options for contributions. Otherwise, select "-none-" and enter one or more fixed contribution options in the table below. Create or edit Price Sets <a href='%1'>here</a>.{/ts}</span></td>
{else}
{if $recurringPaymentProcessor}
- <tr id="recurringContribution" class="crm-contribution-form-block-is_recur"><th scope="row" class="label" width="20%">{$form.is_recur.label}</th>
+ <tr id="recurringContribution" class="crm-contribution-form-block-is_recur"><td scope="row" class="label" width="20%">{$form.is_recur.label}</td>
<td>{$form.is_recur.html}<br />
<span class="description">{ts}Check this box if you want to give users the option to make recurring contributions. This feature requires that you use a payment processor which supports recurring billing / subscriptions functionality.{/ts} {docURL page="user/contributions/payment-processors"}</span>
</td>
<tr id="recurFields" class="crm-contribution-form-block-recurFields"><td> </td>
<td>
<table class="form-layout-compressed">
- <tr class="crm-contribution-form-block-recur_frequency_unit"><th scope="row" class="label">{$form.recur_frequency_unit.label}<span class="crm-marker" title="This field is required.">*</span></th>
+ <tr class="crm-contribution-form-block-recur_frequency_unit"><td scope="row" class="label">{$form.recur_frequency_unit.label}<span class="crm-marker" title="This field is required.">*</span></td>
<td>{$form.recur_frequency_unit.html}<br />
<span class="description">{ts}Select recurring units supported for recurring payments.{/ts}</span></td>
</tr>
- <tr class="crm-contribution-form-block-is_recur_interval"><th scope="row" class="label">{$form.is_recur_interval.label}</th>
+ <tr class="crm-contribution-form-block-is_recur_interval"><td scope="row" class="label">{$form.is_recur_interval.label}</td>
<td>{$form.is_recur_interval.html}<br />
<span class="description">{ts}Can users also set an interval (e.g. every '3' months)?{/ts}</span></td>
</tr>
- <tr class="crm-contribution-form-block-is_recur_installments"><th scope="row" class="label">{$form.is_recur_installments.label}</th>
+ <tr class="crm-contribution-form-block-is_recur_installments"><td scope="row" class="label">{$form.is_recur_installments.label}</td>
<td>{$form.is_recur_installments.html}<br />
<span class="description">{ts}Give the user a choice of installments (e.g. donate every month for 6 months)? If not, recurring donations will continue indefinitely.{/ts}</span></td>
</tr>
<table class="form-layout-compressed">
{* handle CiviPledge fields *}
{if $civiPledge}
- <tr class="crm-contribution-form-block-is_pledge_active"><th scope="row" class="label" width="20%">{$form.is_pledge_active.label}</th>
+ <tr class="crm-contribution-form-block-is_pledge_active"><td scope="row" class="label" width="20%">{$form.is_pledge_active.label}</td>
<td>{$form.is_pledge_active.html}<br />
<span class="description">{ts}Check this box if you want to give users the option to make a Pledge (a commitment to contribute a fixed amount on a recurring basis).{/ts}</span>
</td>
</tr>
<tr id="pledgeFields" class="crm-contribution-form-block-pledgeFields"><td></td><td>
<table class="form-layout-compressed">
- <tr class="crm-contribution-form-block-pledge_frequency_unit"><th scope="row" class="label">{$form.pledge_frequency_unit.label}<span class="crm-marker"> *</span></th>
+ <tr class="crm-contribution-form-block-pledge_frequency_unit"><td scope="row" class="label">{$form.pledge_frequency_unit.label}<span class="crm-marker"> *</span></td>
<td>{$form.pledge_frequency_unit.html}<br />
<span class="description">{ts}Which frequencies can the user pick from (e.g. every 'week', every 'month', every 'year')?{/ts}</span></td>
</tr>
- <tr class="crm-contribution-form-block-is_pledge_interval"><th scope="row" class="label">{$form.is_pledge_interval.label}</th>
+ <tr class="crm-contribution-form-block-is_pledge_interval"><td scope="row" class="label">{$form.is_pledge_interval.label}</td>
<td>{$form.is_pledge_interval.html}<br />
<span class="description">{ts}Can they also set an interval (e.g. every '3' months)?{/ts}</span></td>
</tr>
- <tr class="crm-contribution-form-block-initial_reminder_day"><th scope="row" class="label">{$form.initial_reminder_day.label}</th>
+ <tr class="crm-contribution-form-block-initial_reminder_day"><td scope="row" class="label">{$form.initial_reminder_day.label}</td>
<td>{$form.initial_reminder_day.html}
<span class="label">{ts}Days prior to each scheduled payment due date.{/ts}</span></td>
</tr>
- <tr class="crm-contribution-form-block-max_reminders"><th scope="row" class="label">{$form.max_reminders.label}</th>
+ <tr class="crm-contribution-form-block-max_reminders"><td scope="row" class="label">{$form.max_reminders.label}</td>
<td>{$form.max_reminders.html}
<span class="label">{ts}Reminders for each scheduled payment.{/ts}</span></td>
</tr>
- <tr class="crm-contribution-form-block-additional_reminder_day"><th scope="row" class="label">{$form.additional_reminder_day.label}</th>
+ <tr class="crm-contribution-form-block-additional_reminder_day"><td scope="row" class="label">{$form.additional_reminder_day.label}</td>
<td>{$form.additional_reminder_day.html}
<span class="label">{ts}Days after the last one sent, up to the maximum number of reminders.{/ts}</span></td>
</tr>
{if $futurePaymentProcessor}
- <tr id="adjustRecurringFields" class="crm-contribution-form-block-adjust_recur_start_date"><th scope="row" class="label">{$form.adjust_recur_start_date.label}</th>
+ <tr id="adjustRecurringFields" class="crm-contribution-form-block-adjust_recur_start_date"><td scope="row" class="label">{$form.adjust_recur_start_date.label}</td>
<td>{$form.adjust_recur_start_date.html}<br/>
<div id="recurDefaults">
<span class="description">{$form.pledge_default_toggle.label}</span>
{/if}
<tr class="crm-contribution-form-block-amount_label">
- <th scope="row" class="label" width="20%">{$form.amount_label.label}<span class="crm-marker"> *</span></th>
+ <td scope="row" class="label" width="20%">{$form.amount_label.label}<span class="crm-marker"> *</span></td>
<td>{$form.amount_label.html}</td>
</tr>
- <tr class="crm-contribution-form-block-is_allow_other_amount"><th scope="row" class="label" width="20%">{$form.is_allow_other_amount.label}</th>
+ <tr class="crm-contribution-form-block-is_allow_other_amount"><td scope="row" class="label" width="20%">{$form.is_allow_other_amount.label}</td>
<td>{$form.is_allow_other_amount.html}<br />
<span class="description">{ts}Check this box if you want to give users the option to enter their own contribution amount. Your page will then include a text field labeled <strong>Other Amount</strong>.{/ts}</span></td></tr>
<tr id="minMaxFields" class="crm-contribution-form-block-minMaxFields"><td> </td><td>
<table class="form-layout-compressed">
- <tr class="crm-contribution-form-block-min_amount"><th scope="row" class="label">{$form.min_amount.label}</th>
+ <tr class="crm-contribution-form-block-min_amount"><td scope="row" class="label">{$form.min_amount.label}</td>
<td>{$form.min_amount.html}</td></tr>
- <tr class="crm-contribution-form-block-max_amount"><th scope="row" class="label">{$form.max_amount.label}</th>
+ <tr class="crm-contribution-form-block-max_amount"><td scope="row" class="label">{$form.max_amount.label}</td>
<td>{$form.max_amount.html}<br />
<span class="description">{ts 1=5|crmMoney}If you have chosen to <strong>Allow Other Amounts</strong>, you can use the fields above to control minimum and/or maximum acceptable values (e.g. don't allow contribution amounts less than %1).{/ts}</span></td></tr>
</table>
</div>
<br />
<table id="map-field-table">
- <tr class="columnheader" ><th scope="column">{ts}Contribution Label{/ts}</th><th scope="column">{ts}Amount{/ts}</th><th scope="column">{ts}Default?{/ts}<br />{$form.default.0.html}</th></tr>
+ <tr class="columnheader" ><td scope="column">{ts}Contribution Label{/ts}</td><td scope="column">{ts}Amount{/ts}</td><td scope="column">{ts}Default?{/ts}<br />{$form.default.0.html}</td></tr>
{section name=loop start=1 loop=11}
{assign var=idx value=$smarty.section.loop.index}
<tr><td class="even-row">{$form.label.$idx.html}</td><td>{$form.value.$idx.html}</td><td class="even-row">{$form.default.$idx.html}</td></tr>
$("#noteColumns, #noteRows, #noteLength", $form).toggle(dataType === 'Memo');
- $(".crm-custom-field-form-block-serialize", $form).toggle(htmlType === 'Select');
+ $(".crm-custom-field-form-block-serialize", $form).toggle((htmlType === 'Select' || htmlType === 'Autocomplete-Select') && dataType !== 'ContactReference');
}
function makeDefaultValueField(dataType) {
$form.submit(function() {
var htmlType = $('#html_type', $form).val(),
serialize = $("#serialize", $form).is(':checked'),
- htmlTypeLabel = (serialize && htmlType === 'Select') ? ts('Multi-Select') : _.find(htmlTypes, {key: htmlType}).value;
+ htmlTypeLabel = (serialize && _.includes(['Select', 'Autocomplete-Select'], htmlType)) ? ts('Multi-Select') : _.find(htmlTypes, {key: htmlType}).value;
if (originalHtmlType && (originalHtmlType !== htmlType || originalSerialize !== serialize)) {
var origHtmlTypeLabel = (originalSerialize && originalHtmlType === 'Select') ? ts('Multi-Select') : _.find(htmlTypes, {key: originalHtmlType}).value;
if (originalSerialize && !serialize && existingMultiValueCount) {
</td>
</tr>
- {if $group.created_by}
- <tr class="crm-group-form-block-created">
- <td class="label">{ts}Created By{/ts}</td>
- <td>{$group.created_by}</td>
- </tr>
- {/if}
-
- {if $group.modified_by}
- <tr class="crm-group-form-block-modified">
- <td class="label">{ts}Modified By{/ts}</td>
- <td>{$group.modified_by}</td>
- </tr>
- {/if}
-
<tr class="crm-group-form-block-description">
<td class="label">{$form.description.label}</td>
- <td>{$form.description.html}<br />
- <span class="description">{ts}Group description is displayed when groups are listed in Profiles and Mailing List Subscribe forms.{/ts}</span>
+ <td>{$form.description.html}</td>
+ </tr>
+
+ <tr><td colspan="2">If either of the following fields are filled out they will be used instead of the title or description field in profiles and Mailing List Subscription/unsubscribe forms</td></tr>
+
+ <tr class="crm-group-form-block-frontend-title">
+ <td class="label">{$form.frontend_title.label} {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_group' field='frontend_title' id=$group.id}{/if}</td>
+ <td>{$form.frontend_title.html|crmAddClass:huge}
+ {if $group.saved_search_id} ({ts}Smart Group{/ts}){/if}
</td>
</tr>
+ <tr class="crm-group-form-block-frontend-description">
+ <td class="label">{$form.frontend_description.label} {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_group' field='frontend_description' id=$group.id}{/if}</td>
+ <td>{$form.frontend_description.html}</td>
+ </tr>
+
{if $form.group_type}
<tr class="crm-group-form-block-group_type">
<td class="label">{$form.group_type.label}</td>
<td>{$form.is_active.html}</td>
</tr>
+ {if $group.created_by}
+ <tr class="crm-group-form-block-created">
+ <td class="label">{ts}Created By{/ts}</td>
+ <td>{$group.created_by}</td>
+ </tr>
+ {/if}
+
+ {if $group.modified_by}
+ <tr class="crm-group-form-block-modified">
+ <td class="label">{ts}Modified By{/ts}</td>
+ <td>{$group.modified_by}</td>
+ </tr>
+ {/if}
+
+
<tr>
<td colspan=2>{include file="CRM/Custom/Form/CustomData.tpl"}</td>
</tr>
{if ($extends eq 'Contribution') || ($extends eq 'Membership')}
<span id='amount_sum_label'>{ts}Total Amount{/ts}</span>
{else}
- <span id='amount_sum_label'>{ts}Total Fee(s){/ts}{if $isAdditionalParticipants} {ts}for this participant{/ts}{/if}</span>
+ {if $isAdditionalParticipants}
+ <span id='amount_sum_label'>{ts}Total for this participant{/ts}</span>
+ {else}
+ <span id='amount_sum_label'>{ts}Total{/ts}</span>
+ {/if}
{/if}
</div>
<div class="content calc-value" {if $hideTotal}style="display:none;"{/if} id="pricevalue"></div>
<a title="{$row.$fieldHover|escape}" href="{$row.$fieldLink}" {$row.$fieldClass}>
{/if}
- {if $row.$field eq 'Subtotal'}
+ {if is_array($row.$field)}
+ {foreach from=$row.$field item=fieldrow key=fieldid}
+ <div class="crm-report-{$field}-row-{$fieldid}">{$fieldrow}</div>
+ {/foreach}
+ {elseif $row.$field eq 'Subtotal'}
{$row.$field}
{elseif $header.type & 4 OR $header.type & 256}
{if $header.group_by eq 'MONTH' or $header.group_by eq 'QUARTER'}
<a title="{$row.$fieldHover|escape}" href="{$row.$fieldLink}" {if $row.$fieldClass} class="{$row.$fieldClass}"{/if}>
{/if}
- {if $row.$field eq 'Subtotal'}
+ {if is_array($row.$field)}
+ {foreach from=$row.$field item=fieldrow key=fieldid}
+ <div class="crm-report-{$field}-row-{$fieldid}">{$fieldrow}</div>
+ {/foreach}
+ {elseif $row.$field eq 'Subtotal'}
{$row.$field}
{elseif $header.type & 4 OR $header.type & 256}
{if $header.group_by eq 'MONTH' or $header.group_by eq 'QUARTER'}
// define('CIVICRM_LANGUAGE_MAPPING_ES', 'es_MX');
// define('CIVICRM_LANGUAGE_MAPPING_PT', 'pt_BR');
// define('CIVICRM_LANGUAGE_MAPPING_ZH', 'zh_TW');
+// define('CIVICRM_LANGUAGE_MAPPING_NL', 'nl_BE');
/**
* Native gettext improves performance of localized CiviCRM installations
* <http://www.gnu.org/licenses/>.
*/
+use Civi\Api4\Campaign;
+
/**
* Test CRM/Member/BAO Membership Log add , delete functions
*
$this->setCurrencySeparators($thousandSeparator);
$form = new CRM_Batch_Form_Entry();
- $profileID = $this->callAPISuccessGetValue('UFGroup', ['return' => 'id', 'name' => 'membership_batch_entry']);
+ $profileID = (int) $this->callAPISuccessGetValue('UFGroup', ['return' => 'id', 'name' => 'membership_batch_entry']);
$form->_fields = CRM_Core_BAO_UFGroup::getFields($profileID, FALSE, CRM_Core_Action::VIEW);
$params = $this->getMembershipData();
- $this->assertTrue($form->testProcessMembership($params));
- $result = $this->callAPISuccess('membership', 'get', []);
+ $this->assertEquals(4500.0, $form->testProcessMembership($params));
+ $result = $this->callAPISuccess('membership', 'get');
$this->assertEquals(3, $result['count']);
//check start dates #1 should default to 1 Jan this year, #2 should be as entered
$this->assertEquals(date('Y-m-d', strtotime('first day of January 2013')), $result['values'][1]['start_date']);
//check start dates #1 should default to 1 Jan this year, #2 should be as entered
$this->assertEquals(date('Y-m-d', strtotime('07/22/2013')), $result['values'][1]['join_date']);
$this->assertEquals(date('Y-m-d', strtotime('07/03/2013')), $result['values'][2]['join_date']);
- $this->assertEquals(date('Y-m-d', strtotime('now')), $result['values'][3]['join_date']);
+ $this->assertEquals(date('Y-m-d'), $result['values'][3]['join_date']);
$result = $this->callAPISuccess('contribution', 'get', ['return' => ['total_amount', 'trxn_id']]);
$this->assertEquals(3, $result['count']);
foreach ($result['values'] as $key => $contribution) {
*/
public function testMembershipRenewalDates() {
$form = new CRM_Batch_Form_Entry();
+ $campaignID = Campaign::create()->setValues(['name' => 'blah', 'title' => 'blah'])->execute()->first()['id'];
foreach ([$this->_contactID, $this->_contactID2] as $contactID) {
$membershipParams = [
'membership_type_id' => $this->_membershipTypeID2,
];
$params['field'][1]['membership_type'] = [0 => $this->_orgContactID2, 1 => $this->_membershipTypeID2];
$params['field'][1]['receive_date'] = date('Y-m-d');
+ $params['field'][1]['member_campaign_id'] = $campaignID;
// explicitly specify start and end dates
$params['field'][2]['membership_type'] = [0 => $this->_orgContactID2, 1 => $this->_membershipTypeID2];
$params['field'][2]['membership_end_date'] = "2017-03-31";
$params['field'][2]['receive_date'] = "2016-04-01";
- $this->assertTrue($form->testProcessMembership($params));
- $result = $this->callAPISuccess('membership', 'get', []);
+ $this->assertEquals(3.0, $form->testProcessMembership($params));
+ $result = $this->callAPISuccess('membership', 'get')['values'];
// renewal dates should be from current if start_date and end_date is passed as NULL
- $this->assertEquals(date('Y-m-d'), $result['values'][1]['start_date']);
+ $this->assertEquals(date('Y-m-d'), $result[1]['start_date']);
$endDate = date("Y-m-d", strtotime(date("Y-m-d") . " +1 year -1 day"));
- $this->assertEquals($endDate, $result['values'][1]['end_date']);
+ $this->assertEquals($endDate, $result[1]['end_date']);
+ $this->assertEquals(1, $result[1]['campaign_id']);
// verify if the modified dates asserts with the dates passed above
- $this->assertEquals('2016-04-01', $result['values'][2]['start_date']);
- $this->assertEquals('2017-03-31', $result['values'][2]['end_date']);
+ $this->assertEquals('2016-04-01', $result[2]['start_date']);
+ $this->assertEquals('2017-03-31', $result[2]['end_date']);
+ $this->assertTrue(empty($result[2]['campaign_id']));
}
/**
}
}
+ /**
+ * Test that long unicode individual names are truncated properly when
+ * creating sort/display name.
+ *
+ * @dataProvider longUnicodeIndividualNames
+ *
+ * @param array $input
+ * @param array $expected
+ */
+ public function testLongUnicodeIndividualName(array $input, array $expected) {
+ // needs to be passed by reference
+ $params = [
+ 'contact_type' => 'Individual',
+ 'first_name' => $input['first_name'],
+ 'last_name' => $input['last_name'],
+ ];
+ $contact = CRM_Contact_BAO_Contact::add($params);
+
+ $this->assertEquals($expected['sort_name'], $contact->sort_name);
+ $this->assertEquals($expected['display_name'], $contact->display_name);
+
+ $this->contactDelete($contact->id);
+ }
+
+ /**
+ * Data provider for testLongUnicodeIndividualName
+ * @return array
+ */
+ public function longUnicodeIndividualNames():array {
+ return [
+ 'much less than 128' => [
+ [
+ 'first_name' => 'асдадасда',
+ 'last_name' => 'лнплнплнп',
+ ],
+ [
+ 'sort_name' => 'лнплнплнп, асдадасда',
+ 'display_name' => 'асдадасда лнплнплнп',
+ ],
+ ],
+ 'less than 128 but still too big' => [
+ [
+ 'first_name' => 'асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдаш',
+ 'last_name' => 'лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпш',
+ ],
+ [
+ 'sort_name' => 'лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпш, асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдаш',
+ 'display_name' => 'асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдаш лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпш',
+ ],
+ ],
+ // note we have to account for the comma and space
+ 'equal 128 sort_name' => [
+ [
+ 'first_name' => 'асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасд',
+ 'last_name' => 'лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнп',
+ ],
+ [
+ 'sort_name' => 'лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнп, асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасд',
+ 'display_name' => 'асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасд лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнп',
+ ],
+ ],
+ // note we have to account for the space
+ 'equal 128 display_name' => [
+ [
+ 'first_name' => 'асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдa',
+ 'last_name' => 'лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнп',
+ ],
+ [
+ 'sort_name' => 'лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнп, асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасд',
+ 'display_name' => 'асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдa лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнп',
+ ],
+ ],
+ 'longer than 128' => [
+ [
+ 'first_name' => 'асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдаш',
+ 'last_name' => 'лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпш',
+ ],
+ [
+ 'sort_name' => 'лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпш, асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдада',
+ 'display_name' => 'асдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдашасдадасдаш лнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнплнпшлнплнпл',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Test that long unicode org names are truncated properly when creating
+ * sort/display name.
+ *
+ * @dataProvider longUnicodeOrgNames
+ *
+ * @param string $input
+ * @param string $expected
+ */
+ public function testLongUnicodeOrgName(string $input, string $expected) {
+ // needs to be passed by reference
+ $params = [
+ 'contact_type' => 'Organization',
+ 'organization_name' => $input,
+ ];
+ $contact = CRM_Contact_BAO_Contact::add($params);
+
+ $this->assertEquals($expected, $contact->sort_name);
+ $this->assertEquals($expected, $contact->display_name);
+
+ $this->contactDelete($contact->id);
+ }
+
+ /**
+ * Data provider for testLongUnicodeOrgName
+ * @return array
+ */
+ public function longUnicodeOrgNames():array {
+ return [
+ 'much less than 128' => [
+ 'асдадасда шшшшшшшшшш',
+ 'асдадасда шшшшшшшшшш',
+ ],
+ 'less than 128 but still too big' => [
+ 'асдадасда шшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшасд',
+ 'асдадасда шшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшасд',
+ ],
+ 'equal 128' => [
+ 'асдадасда шшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшасд',
+ 'асдадасда шшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшасд',
+ ],
+ 'longer than 128' => [
+ 'асдадасда шшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшасдасд',
+ 'асдадасда шшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшш...',
+ ],
+ ];
+ }
+
}
$_REQUEST = [];
}
+ /**
+ * Tests the event dashboard as a minimally permissioned user.
+ */
+ public function testEventDashboard() {
+ CRM_Core_Config::singleton()->userPermissionClass->permissions = [
+ 'register for events',
+ 'access Contact Dashboard',
+ ];
+ $event1id = $this->eventCreate()['id'];
+ $event2id = $this->eventCreate(['title' => 'Social Distancing Meetup Group'])['id'];
+ $params['contact_id'] = $this->contactID;
+ $params['event_id'] = $event1id;
+ $this->participantCreate($params);
+ $params['event_id'] = $event2id;
+ $this->participantCreate($params);
+ $this->runUserDashboard();
+ $expectedStrings = [
+ '<div class="header-dark">Your Event(s)</div>',
+ '<td class="crm-participant-event-id_1"><a href="/index.php?q=civicrm/event/info&reset=1&id=1&context=dashboard">Annual CiviCRM meet</a></td>',
+ '<td class="crm-participant-event-id_2"><a href="/index.php?q=civicrm/event/info&reset=1&id=2&context=dashboard">Social Distancing Meetup Group</a></td>',
+ ];
+ $this->assertPageContains($expectedStrings);
+ $this->individualCreate();
+ }
+
}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | Use of this source code is governed by the AGPL license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+use Civi\Api4\Contribution;
+use Civi\Api4\ContributionRecur;
+
+/**
+ * Class CRM_Contribute_Page_AjaxTest
+ * @group headless
+ */
+class CRM_Contribute_Page_TabTest extends CiviUnitTestCase {
+
+ /**
+ * Test links render correctly for manual processor.
+ *
+ * @throws \API_Exception
+ * @throws \CiviCRM_API3_Exception
+ */
+ public function testLinks() {
+ $contactID = $this->individualCreate();
+ $recurID = ContributionRecur::create()->setValues([
+ 'contact_id' => $contactID,
+ 'amount' => 10,
+ 'frequency_interval' => 'week',
+ 'start_date' => 'now',
+ 'is_active' => TRUE,
+ 'contribution_status_id:name' => 'Pending',
+ ])
+ ->addChain(
+ 'contribution',
+ Contribution::create()->setValues([
+ 'contribution_id' => '$id',
+ 'financial_type_id:name' => 'Donation',
+ 'total_amount' => 60,
+ 'receive_date' => 'now',
+ 'contact_id' => $contactID,
+ ])
+ )->execute()->first()['id'];
+ $page = new CRM_Contribute_Page_Tab();
+ $page->_contactId = $contactID;
+ $page->_action = CRM_Core_Action::VIEW;
+ $page->browse();
+
+ $templateVariable = CRM_Core_Smarty::singleton()->get_template_vars();
+ $this->assertEquals('Mr. Anthony Anderson II', $templateVariable['displayName']);
+ $this->assertEquals("<span><a href=\"/index.php?q=civicrm/contact/view/contributionrecur&reset=1&id=" . $recurID . "&cid=" . $contactID . "&context=contribution\" class=\"action-item crm-hover-button\" title='View Recurring Payment' >View</a><a href=\"/index.php?q=civicrm/contribute/updaterecur&reset=1&action=update&crid=1&cid=3&context=contribution\" class=\"action-item crm-hover-button\" title='Edit Recurring Payment' >Edit</a><a href=\"/index.php?q=civicrm/contribute/unsubscribe&reset=1&crid=" . $recurID . "&cid=" . $contactID . "&context=contribution\" class=\"action-item crm-hover-button\" title='Cancel' >Cancel</a></span>",
+ $templateVariable['activeRecurRows'][1]['action']
+ );
+ }
+
+}
}
}
+ /**
+ * Test for single select Autocomplete custom field.
+ *
+ */
+ public function testSingleSelectAutoComplete() {
+ $customGroupId = $this->customGroupCreate([
+ 'extends' => 'Individual',
+ ])['id'];
+ $colors = ['Y' => 'Yellow', 'G' => 'Green'];
+ $fieldId = $this->createAutoCompleteCustomField([
+ 'custom_group_id' => $customGroupId,
+ 'option_values' => $colors,
+ ])['id'];
+ $contactId = $this->individualCreate(['custom_' . $fieldId => 'Y']);
+ $value = $this->callAPISuccessGetValue('Contact', [
+ 'id' => $contactId,
+ 'return' => 'custom_' . $fieldId,
+ ]);
+ $this->assertEquals('Y', $value);
+ }
+
+ /**
+ * Test for multi select Autocomplete custom field.
+ *
+ */
+ public function testMultiSelectAutoComplete() {
+ $customGroupId = $this->customGroupCreate([
+ 'extends' => 'Individual',
+ ])['id'];
+ $colors = ['Y' => 'Yellow', 'G' => 'Green'];
+ $fieldId = $this->createAutoCompleteCustomField([
+ 'custom_group_id' => $customGroupId,
+ 'serialize' => '1',
+ 'option_values' => $colors,
+ ])['id'];
+ $contactId = $this->individualCreate(['custom_' . $fieldId => ['Y', 'G']]);
+ $value = $this->callAPISuccessGetValue('Contact', [
+ 'id' => $contactId,
+ 'return' => 'custom_' . $fieldId,
+ ]);
+ $this->assertEquals(array_keys($colors), $value);
+ }
+
}
$this->assertRegexp('/CRM_Core_ErrorTest->testFormatBacktrace_exception/', $msg);
}
+ public function testExceptionLogging() {
+ $e = new \Exception("the exception");
+ Civi::log()->notice('There was an exception!', [
+ 'exception' => $e,
+ ]);
+
+ $e = new Error('the error');
+ Civi::log()->notice('There was an error!', [
+ 'exception' => $e,
+ ]);
+ }
+
/**
* We have two coding conventions for writing to log. Make sure that they work together.
*
$form->_action = CRM_Core_Action::ADD;
},
],
+ // Also a bit flawed, but catches simple stuff.
+ 'Fulltext search' => [
+ 'CRM_Contact_Form_Search_Custom',
+ function(CRM_Core_Form $form) {
+ $form->_action = CRM_Core_Action::BASIC;
+ $form->set('csid', 15);
+ },
+ ],
];
}
$contribution = $this->callAPISuccess('contribution', 'get', [
'contribution_recur_id' => $this->_contributionRecurID,
'sequential' => 1,
- ]);
- $this->assertEquals(2, $contribution['count']);
- $this->assertEquals('second_one', $contribution['values'][1]['trxn_id']);
- $this->assertEquals(date('Y-m-d'), date('Y-m-d', strtotime($contribution['values'][1]['receive_date'])));
+ ])['values'];
+ $this->assertCount(2, $contribution);
+ $secondContribution = $contribution[1];
+ $this->assertEquals('second_one', $secondContribution['trxn_id']);
+ $this->assertEquals(date('Y-m-d'), date('Y-m-d', strtotime($secondContribution['receive_date'])));
+ $this->assertEquals('expensive', $secondContribution['amount_level']);
+ $this->assertEquals($this->ids['campaign'][0], $secondContribution['campaign_id']);
}
/**
+--------------------------------------------------------------------+
*/
+use Civi\Api4\Contribution;
+
/**
* Class CRM_Core_Payment_BaseIPNTest
* @group headless
public function testThatCancellingEventPaymentWillCancelAllAdditionalPendingParticipantsAndCreateCancellationActivities() {
$this->_setUpParticipantObjects('Pending from incomplete transaction');
- $this->IPN->loadObjects($this->input, $this->ids, $this->objects, FALSE, $this->_processorId);
$additionalParticipantId = $this->participantCreate([
'event_id' => $this->_eventId,
'registered_by_id' => $this->_participantId,
'status_id' => 'Pending from incomplete transaction',
]);
- $transaction = new CRM_Core_Transaction();
- $this->IPN->cancelled($this->objects);
+ Contribution::update(FALSE)->setValues([
+ 'cancel_date' => 'now',
+ 'contribution_status_id:name' => 'Cancelled',
+ ])->addWhere('id', '=', $this->_contributionId)->execute();
$cancelledParticipantsCount = civicrm_api3('Participant', 'get', [
'sequential' => 1,
*/
public function testIPNPaymentRecurSuccess() {
$this->setupRecurringPaymentProcessorTransaction([], ['total_amount' => '15.00']);
+ $mut = new CiviMailUtils($this, TRUE);
$paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurTransaction());
$paypalIPN->main();
+ $mut->checkMailLog(['https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_subscr-find'], ['civicrm/contribute/unsubscribe', 'civicrm/contribute/updatebilling']);
+ $mut->stop();
$contribution1 = $this->callAPISuccess('Contribution', 'getsingle', ['id' => $this->_contributionID]);
$this->assertEquals(1, $contribution1['contribution_status_id']);
$this->assertEquals('8XA571746W2698126', $contribution1['trxn_id']);
$this->assertEquals(1, $count, 'Expect one registered snippet');
}
+ /**
+ * Create a few resources with aliases. Use a mix of reads+writes on both the
+ * canonical names and aliased names.
+ */
+ public function testAliases() {
+ $b = $this->createEmptyCollection();
+ $b->add([
+ 'styleUrl' => 'https://example.com/foo.css',
+ 'name' => 'foo',
+ 'aliases' => ['bar', 'borg'],
+ ]);
+ $b->add([
+ 'scriptUrl' => 'https://example.com/whiz.js',
+ 'name' => 'whiz',
+ 'aliases' => 'bang',
+ ]);
+
+ $this->assertEquals('foo', $b->get('foo')['name']);
+ $this->assertEquals('foo', $b->get('bar')['name']);
+ $this->assertEquals('foo', $b->get('borg')['name']);
+ $this->assertEquals('whiz', $b->get('whiz')['name']);
+ $this->assertEquals('whiz', $b->get('bang')['name']);
+ $this->assertEquals(NULL, $b->get('snafu'));
+
+ // Go back+forth, updating with one name then reading with the other.
+
+ $b->get('borg')['borgify'] = TRUE;
+ $this->assertEquals(TRUE, $b->get('foo')['borgify']);
+
+ $b->get('foo')['d'] = 'ie';
+ $this->assertEquals('ie', $b->get('borg')['d']);
+
+ $b->update('bang', ['b52' => 'love shack']);
+ $this->assertEquals('love shack', $b->get('whiz')['b52']);
+
+ $b->update('whiz', ['golly' => 'gee']);
+ $this->assertEquals('gee', $b->get('bang')['golly']);
+ }
+
/**
* Add some items to a bundle - then clear() all of them.
*/
}
public function testViewOwnEvent() {
- self::setViewOwnEventPermissions();
+ $this->setViewOwnEventPermissions();
unset(\Civi::$statics['CRM_Event_BAO_Event']['permissions']);
$permissions = CRM_Event_BAO_Event::checkPermission($this->_ownEventId, CRM_Core_Permission::VIEW);
$this->assertTrue($permissions);
}
public function testEditOwnEvent() {
- self::setViewOwnEventPermissions();
+ $this->setViewOwnEventPermissions();
unset(\Civi::$statics['CRM_Event_BAO_Event']['permissions']);
$this->_loggedInUser = CRM_Core_Session::singleton()->get('userID');
$permissions = CRM_Event_BAO_Event::checkPermission($this->_ownEventId, CRM_Core_Permission::EDIT);
*/
public function testDeleteOwnEvent() {
// Check that you can't delete your own event without "Delete in CiviEvent" permission
- self::setViewOwnEventPermissions();
+ $this->setViewOwnEventPermissions();
unset(\Civi::$statics['CRM_Event_BAO_Event']['permissions']);
$permissions = CRM_Event_BAO_Event::checkPermission($this->_ownEventId, CRM_Core_Permission::DELETE);
$this->assertFalse($permissions);
public function testDeleteOtherEventDenied() {
// FIXME: This test could be improved, but for now it checks that we can't delete if we don't have "Delete in CiviEvent"
- self::setEditAllEventPermissions();
+ $this->setEditAllEventPermissions();
unset(\Civi::$statics['CRM_Event_BAO_Event']['permissions']);
$permissions = CRM_Event_BAO_Event::checkPermission($this->_otherEventId, CRM_Core_Permission::DELETE);
$this->assertFalse($permissions);
}
+ /**
+ * Test get complete info function returns all info for contacts with view all info.
+ */
+ public function testGetCompleteInfo() {
+ $this->setupScenarioCoreACLEveryonePermittedToEvent();
+ $info = CRM_Event_BAO_Event::getCompleteInfo('20000101');
+ $this->assertEquals('Annual CiviCRM meet', $info[0]['title']);
+ $this->assertEquals('Annual CiviCRM meet', $info[1]['title']);
+ }
+
}
$this->callAPISuccess('address', 'create', $params);
}
+ /**
+ * Test for single select Autocomplete custom field.
+ *
+ */
+ public function testSingleAndMultiSelectAutoComplete() {
+ $customGroupId = $this->customGroupCreate([
+ 'extends' => 'Individual',
+ ])['id'];
+ $colors = ['Y' => 'Yellow', 'G' => 'Green', 'R' => 'Red'];
+ $fieldId1 = $this->createAutoCompleteCustomField([
+ 'custom_group_id' => $customGroupId,
+ 'option_values' => $colors,
+ 'label' => 'Autocomplete Color',
+ ])['id'];
+ $fieldId2 = $this->createAutoCompleteCustomField([
+ 'custom_group_id' => $customGroupId,
+ 'option_values' => $colors,
+ 'label' => 'Autocomplete Colors',
+ 'serialize' => 1,
+ ])['id'];
+ $contactId = $this->individualCreate([
+ "custom_$fieldId1" => 'Y',
+ "custom_$fieldId2" => ['Y', 'G'],
+ ]);
+ $selectedFields = [
+ ['name' => 'contact_id'],
+ ['name' => "custom_{$fieldId1}"],
+ ['name' => "custom_{$fieldId2}"],
+ ];
+
+ $this->doExportTest([
+ 'ids' => [$contactId],
+ 'fields' => $selectedFields,
+ 'exportMode' => CRM_Export_Form_Select::CONTACT_EXPORT,
+ ]);
+ $row = $this->csv->fetchOne();
+ $this->assertEquals('Yellow', $row['Autocomplete Color']);
+ $this->assertEquals('Yellow, Green', $row['Autocomplete Colors']);
+ }
+
}
--- /dev/null
+<?php
+
+/**
+ * @group headless
+ */
+class CRM_Mailing_MailStoreTest extends \CiviUnitTestCase {
+
+ protected $workDir;
+
+ public function setUp() {
+ $this->useTransaction(TRUE);
+ parent::setUp();
+ $this->workDir = tempnam(sys_get_temp_dir(), 'mailstoretest');
+ @unlink($this->workDir);
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ if (is_dir($this->workDir)) {
+ CRM_Utils_File::cleanDir($this->workDir);
+ }
+ }
+
+ /**
+ * Create an example store (maildir) using default behaviors (no hooks).
+ */
+ public function testMaildirBasic() {
+ $this->createMaildirSettings([
+ 'name' => __FUNCTION__,
+ ]);
+ $store = CRM_Mailing_MailStore::getStore(__FUNCTION__);
+ $this->assertTrue($store instanceof CRM_Mailing_MailStore_Maildir);
+ }
+
+ /**
+ * Create an example store (maildir) and change the driver via hook.
+ */
+ public function testMaildirHook() {
+ // This hook swaps out the implementation used for 'Maildir' stores.
+ Civi::dispatcher()
+ ->addListener('hook_civicrm_alterMailStore', function ($e) {
+ if ($e->params['protocol'] === 'Maildir') {
+ $e->params['factory'] = function ($mailSettings) {
+ $this->assertEquals('testMaildirHook', $mailSettings['name']);
+ // Make a fake object that technically meets the contract of 'MailStore'
+ return new class extends CRM_Mailing_MailStore {
+
+ public function frobnicate() {
+ return 'totally';
+ }
+
+ };
+ };
+ }
+ });
+
+ $this->createMaildirSettings([
+ 'name' => __FUNCTION__,
+ ]);
+ $store = CRM_Mailing_MailStore::getStore(__FUNCTION__);
+
+ // The hook gave us an unusual instance of MailStore.
+ $this->assertTrue($store instanceof CRM_Mailing_MailStore);
+ $this->assertFalse($store instanceof CRM_Mailing_MailStore_Maildir);
+ $this->assertEquals('totally', $store->frobnicate());
+ }
+
+ /**
+ * Create a "MailSettings" record for maildir store.
+ * @param array $values
+ * Some values to set
+ * @return array
+ */
+ private function createMaildirSettings($values = []):array {
+ mkdir($this->workDir);
+ $defaults = [
+ 'protocol:name' => 'Maildir',
+ 'name' => NULL,
+ 'source' => $this->workDir,
+ 'domain' => 'maildir.example.com',
+ 'username' => 'pass-my-name',
+ 'password' => 'pass-my-pass',
+ ];
+ $mailSettings = \Civi\Api4\MailSettings::create(0)
+ ->setValues(array_merge($defaults, $values))
+ ->execute()
+ ->single();
+ return $mailSettings;
+ }
+
+}
'is_override' => 1,
'status_id' => $this->_membershipStatusID,
];
- CRM_Member_BAO_Membership::create($params);
+ $this->callAPISuccess('Membership', 'create', $params);
$membershipTypeId = $this->assertDBNotNull('CRM_Member_BAO_Membership', $contactId,
'membership_type_id', 'contact_id',
'status_id' => $this->_membershipStatusID,
];
- CRM_Member_BAO_Membership::create($params);
+ $this->callAPISuccess('Membership', 'create', $params);
$membershipId1 = $this->assertDBNotNull('CRM_Member_BAO_Membership', $contactId, 'id',
'contact_id', 'Database check for created membership.'
'status_id' => $this->_membershipStatusID,
];
- CRM_Member_BAO_Membership::create($params);
+ $this->callAPISuccess('Membership', 'create', $params);
$membershipId2 = $this->assertDBNotNull('CRM_Member_BAO_Membership', 'source123', 'id',
'source', 'Database check for created membership.'
'status_id' => $this->_membershipStatusID,
];
- CRM_Member_BAO_Membership::create($params);
+ $this->callAPISuccess('Membership', 'create', $params);
$membershipId1 = $this->assertDBNotNull('CRM_Member_BAO_Membership', $contactId, 'id',
'contact_id', 'Database check for created membership.'
'status_id' => $this->_membershipStatusID,
];
- CRM_Member_BAO_Membership::create($params);
+ $this->callAPISuccess('Membership', 'create', $params);
$membershipId2 = $this->assertDBNotNull('CRM_Member_BAO_Membership', 'PaySource', 'id',
'source', 'Database check for created membership.'
'status_id' => $this->_membershipStatusID,
];
- $membership = CRM_Member_BAO_Membership::create($params);
- $membershipId = $this->assertDBNotNull('CRM_Member_BAO_Membership', $contactId, 'id',
- 'contact_id', 'Database check for created membership.'
- );
+ $this->callAPISuccess('Membership', 'create', $params);
+
+ $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $contactId]);
$this->assertDBNotNull('CRM_Member_BAO_MembershipLog',
- $membership->id,
+ $membership['id'],
'id',
'membership_id',
'Database checked on membershiplog record.'
NULL,
NULL,
FALSE,
- NULL,
NULL
);
- $endDate = date("Y-m-d", strtotime($membership->end_date . " +1 year"));
+ $endDate = date("Y-m-d", strtotime($membership['end_date'] . " +1 year"));
$this->assertDBNotNull('CRM_Member_BAO_MembershipLog',
$MembershipRenew->id,
$this->assertEquals($this->_membershipTypeID, $MembershipRenew->membership_type_id, 'Verify membership type is changed during renewal.');
$this->assertEquals($endDate, $MembershipRenew->end_date, 'Verify correct end date is calculated after membership renewal');
- $this->membershipDelete($membershipId);
+ $this->membershipDelete($membership['id']);
$this->contactDelete($contactId);
}
'status_id' => $statusId,
];
- $membership = CRM_Member_BAO_Membership::create($params);
-
- $membershipId = $this->assertDBNotNull('CRM_Member_BAO_Membership', $contactId, 'id',
- 'contact_id', 'Database check for created membership.'
- );
-
- $this->assertEquals($membership->status_id, $statusId, 'Verify correct status id is calculated.');
- $this->assertEquals($membership->membership_type_id, $this->_membershipTypeID,
- 'Verify correct membership type id.'
- );
+ $this->callAPISuccess('Membership', 'create', $params);
- //verify all dates.
- $dates = [
- 'startDate' => 'start_date',
- 'joinDate' => 'join_date',
- 'endDate' => 'end_date',
- ];
+ $membership = $this->callAPISuccessGetSingle('Membership', [
+ 'contact_id' => $contactId,
+ 'start_date' => $startDate,
+ 'join_date' => $joinDate,
+ 'end_date' => $endDate,
+ ]);
- foreach ($dates as $date => $dbDate) {
- $this->assertEquals($membership->$dbDate, $$date,
- "Verify correct {$date} is present."
- );
- }
+ $this->assertEquals($membership['status_id'], $statusId, 'Verify correct status id is calculated.');
+ $this->assertEquals($membership['membership_type_id'], $this->_membershipTypeID);
$this->assertDBNotNull('CRM_Member_BAO_MembershipLog',
- $membership->id,
+ $membership['id'],
'id',
'membership_id',
'Database checked on membership log record.'
NULL,
NULL,
NULL,
- FALSE,
- NULL
+ FALSE
);
$this->assertDBNotNull('CRM_Member_BAO_MembershipLog',
'Database checked on membership log record.'
);
- $this->membershipDelete($membershipId);
+ $this->membershipDelete($membership['id']);
$this->contactDelete($contactId);
}
'status_id' => $this->_membershipStatusID,
];
- $createdMembership = CRM_Member_BAO_Membership::create($params);
+ $createdMembershipID = $this->callAPISuccess('Membership', 'create', $params)['id'];
civicrm_api3('Job', 'process_membership');
$membershipAfterProcess = civicrm_api3('Membership', 'get', [
'sequential' => 1,
- 'id' => $createdMembership->id,
+ 'id' => $createdMembershipID,
'return' => ['id', 'is_override', 'status_override_end_date'],
])['values'][0];
- $this->assertEquals($createdMembership->id, $membershipAfterProcess['id']);
+ $this->assertEquals($createdMembershipID, $membershipAfterProcess['id']);
$this->assertArrayNotHasKey('is_override', $membershipAfterProcess);
$this->assertArrayNotHasKey('status_override_end_date', $membershipAfterProcess);
}
'status_id' => $this->_membershipStatusID,
];
- $createdMembership = CRM_Member_BAO_Membership::create($params);
+ $createdMembershipID = $this->callAPISuccess('Membership', 'create', $params)['id'];
civicrm_api3('Job', 'process_membership');
$membershipAfterProcess = civicrm_api3('Membership', 'get', [
'sequential' => 1,
- 'id' => $createdMembership->id,
+ 'id' => $createdMembershipID,
'return' => ['id', 'is_override', 'status_override_end_date'],
])['values'][0];
- $this->assertEquals($createdMembership->id, $membershipAfterProcess['id']);
+ $this->assertEquals($createdMembershipID, $membershipAfterProcess['id']);
$this->assertArrayNotHasKey('is_override', $membershipAfterProcess);
$this->assertArrayNotHasKey('status_override_end_date', $membershipAfterProcess);
}
'status_id' => $this->_membershipStatusID,
];
- $createdMembership = CRM_Member_BAO_Membership::create($params);
+ $createdMembershipID = $this->callAPISuccess('Membership', 'create', $params)['id'];
civicrm_api3('Job', 'process_membership');
$membershipAfterProcess = civicrm_api3('Membership', 'get', [
'sequential' => 1,
- 'id' => $createdMembership->id,
+ 'id' => $createdMembershipID,
'return' => ['id', 'is_override', 'status_override_end_date'],
])['values'][0];
- $this->assertEquals($createdMembership->id, $membershipAfterProcess['id']);
+ $this->assertEquals($createdMembershipID, $membershipAfterProcess['id']);
$this->assertEquals(1, $membershipAfterProcess['is_override']);
}
'status_id' => $this->_membershipStatusID,
];
- CRM_Member_BAO_Membership::create($params);
+ $this->callAPISuccess('Membership', 'create', $params);
$membershipId = $this->assertDBNotNull('CRM_Member_BAO_Membership', $contactId, 'id',
'contact_id', 'Database check for created membership.'
+--------------------------------------------------------------------+
*/
+use Civi\Api4\MembershipType;
+
/**
* Class CRM_Member_BAO_MembershipTypeTest
* @group headless
*
*/
public function testGetRenewalDatesForMembershipType() {
- $ids = [];
$params = [
'name' => 'General',
'domain_id' => 1,
'visibility' => 'Public',
'is_active' => 1,
];
- $membershipType = CRM_Member_BAO_MembershipType::add($params, $ids);
+ $membershipTypeID = MembershipType::create()->setValues($params)->execute()->first()['id'];
$params = [
'contact_id' => $this->_indiviContactID,
- 'membership_type_id' => $membershipType->id,
+ 'membership_type_id' => $membershipTypeID,
'join_date' => '20060121000000',
'start_date' => '20060121000000',
'end_date' => '20070120000000',
'status_id' => $this->_membershipStatusID,
];
- $membership = CRM_Member_BAO_Membership::create($params);
+ $membership = $this->callAPISuccess('Membership', 'create', $params);
- $membershipRenewDates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membership->id);
+ $membershipRenewDates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membership['id']);
- $this->assertEquals($membershipRenewDates['start_date'], '20060121', 'Verify membership renewal start date.');
- $this->assertEquals($membershipRenewDates['end_date'], '20080120', 'Verify membership renewal end date.');
+ $this->assertEquals('20060121', $membershipRenewDates['start_date'], 'Verify membership renewal start date.');
+ $this->assertEquals('20080120', $membershipRenewDates['end_date'], 'Verify membership renewal end date.');
- $this->membershipDelete($membership->id);
- $this->membershipTypeDelete(['id' => $membershipType->id]);
+ $this->membershipDelete($membership['id']);
+ $this->membershipTypeDelete(['id' => $membershipTypeID]);
}
/**
*
* @param string $thousandSeparator
*
- * @dataProvider getThousandSeparators
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
+ *
+ * @dataProvider getThousandSeparators
*/
- public function testSubmit($thousandSeparator) {
+ public function testSubmit(string $thousandSeparator) {
CRM_Core_Session::singleton()->getStatus(TRUE);
$this->setCurrencySeparators($thousandSeparator);
$form = $this->getForm();
$this->createLoggedInUser();
$params = [
'cid' => $this->_individualId,
- 'join_date' => date('2/d/Y'),
+ 'join_date' => date('Y-m-d'),
'start_date' => '',
'end_date' => '',
// This format reflects the organisation & then the type.
'cvv2' => '123',
'credit_card_exp_date' => [
'M' => '9',
- // TODO: Future proof
- 'Y' => '2024',
+ 'Y' => date('Y', strtotime('+ 2 years')),
],
'credit_card_type' => 'Visa',
'billing_first_name' => 'Test',
], 1);
$this->assertEquals([
[
- 'text' => 'AnnualFixed membership for Mr. Anthony Anderson II has been added. The new membership End Date is ' . date('F jS, Y', strtotime('last day of this month')) . ' 12:00 AM.',
+ 'text' => 'AnnualFixed membership for Mr. Anthony Anderson II has been added. The new membership End Date is ' . date('F jS, Y', strtotime('last day of this month')) . '.',
'title' => 'Complete',
'type' => 'success',
'options' => NULL,
*/
public function testCreatePendingWithMultipleTerms() {
CRM_Core_Session::singleton()->getStatus(TRUE);
- $form = $this->getForm();
- $form->preProcess();
$this->mut = new CiviMailUtils($this, TRUE);
$this->createLoggedInUser();
$membershipTypeAnnualRolling = $this->callAPISuccess('membership_type', 'create', [
'from_email_address' => '"Demonstrators Anonymous" <info@example.org>',
'receipt_text' => '',
];
+ $form = $this->getForm();
+ $form->preProcess();
$form->_contactID = $this->_individualId;
$form->testSubmit($params);
$membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
return $this->callAPISuccess('custom_field', 'create', $params)['values'][0];
}
+ /**
+ * Create custom select field.
+ *
+ * @param array $params
+ * Parameter overrides, must include custom_group_id.
+ *
+ * @return array
+ */
+ protected function createAutoCompleteCustomField(array $params): array {
+ $params = array_merge($this->getFieldsValuesByType('String', 'Autocomplete-Select'), $params);
+ return $this->callAPISuccess('custom_field', 'create', $params)['values'][0];
+ }
+
/**
* Create a custom field of type date.
*
'contribution_recur_id' => $contributionRecur['id'],
'source' => 'Payment',
],
+ 'line_item' => $this->getMembershipLineItem(),
+ ],
+ ],
+ ])['id'];
+
+ $this->ids['ContributionRecur'][0] = $contributionRecur['id'];
+ $this->ids['Contribution'][0] = $orderID;
+ }
+
+ /**
+ * Create an order with a contribution AND a membership line item.
+ *
+ * @throws \CRM_Core_Exception
+ */
+ protected function createContributionAndMembershipOrder() {
+ $this->ids['membership_type'][0] = $this->membershipTypeCreate();
+ $orderID = $this->callAPISuccess('Order', 'create', [
+ 'financial_type_id' => 'Donation',
+ 'contact_id' => $this->_contactID,
+ 'is_test' => 0,
+ 'payment_instrument_id' => 'Check',
+ 'receive_date' => date('Y-m-d'),
+ 'line_items' => [
+ [
+ 'params' => [
+ 'contact_id' => $this->_contactID,
+ 'source' => 'Payment',
+ ],
'line_item' => [
[
- 'label' => 'General',
+ 'label' => 'Contribution Amount',
'qty' => 1,
- 'unit_price' => 200,
- 'line_total' => 200,
- 'financial_type_id' => 1,
- 'entity_table' => 'civicrm_membership',
- 'price_field_id' => $this->callAPISuccess('price_field', 'getvalue', [
+ 'unit_price' => 100,
+ 'line_total' => 100,
+ 'financial_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'financial_type_id', 'Donation'),
+ 'entity_table' => 'civicrm_contribution',
+ 'price_field_id' => $this->callAPISuccessGetValue('price_field', [
'return' => 'id',
- 'label' => 'Membership Amount',
- 'options' => ['limit' => 1, 'sort' => 'id DESC'],
- ]),
- 'price_field_value_id' => $this->callAPISuccess('price_field_value', 'getvalue', [
- 'return' => 'id',
- 'label' => 'General',
+ 'label' => 'Contribution Amount',
'options' => ['limit' => 1, 'sort' => 'id DESC'],
]),
+ 'price_field_value_id' => NULL,
],
],
],
+ [
+ 'params' => [
+ 'contact_id' => $this->_contactID,
+ 'membership_type_id' => 'General',
+ 'source' => 'Payment',
+ ],
+ 'line_item' => $this->getMembershipLineItem(),
+ ],
],
])['id'];
- $this->ids['ContributionRecur'][0] = $contributionRecur['id'];
$this->ids['Contribution'][0] = $orderID;
}
]);
}
+ /**
+ * @return array[]
+ * @throws \CRM_Core_Exception
+ */
+ protected function getMembershipLineItem(): array {
+ return [
+ [
+ 'label' => 'General',
+ 'qty' => 1,
+ 'unit_price' => 200,
+ 'line_total' => 200,
+ 'financial_type_id' => 1,
+ 'entity_table' => 'civicrm_membership',
+ 'price_field_id' => $this->callAPISuccess('price_field', 'getvalue', [
+ 'return' => 'id',
+ 'label' => 'Membership Amount',
+ 'options' => ['limit' => 1, 'sort' => 'id DESC'],
+ ]),
+ 'price_field_value_id' => $this->callAPISuccess('price_field_value', 'getvalue', [
+ 'return' => 'id',
+ 'label' => 'General',
+ 'options' => ['limit' => 1, 'sort' => 'id DESC'],
+ ]),
+ ],
+ ];
+ }
+
}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Angular;
+
+/**
+ * Test the Angular loader.
+ */
+class LoaderTest extends \CiviUnitTestCase {
+
+ public static $dummy_setting_count = 0;
+ public static $dummy_callback_count = 0;
+
+ public function setUp() {
+ parent::setUp();
+ $this->hookClass->setHook('civicrm_angularModules', [$this, 'hook_angularModules']);
+ self::$dummy_setting_count = 0;
+ self::$dummy_callback_count = 0;
+ $this->createLoggedInUser();
+ }
+
+ public function factoryScenarios() {
+ return [
+ ['dummy1', 2, 1, ['access CiviCRM', 'administer CiviCRM']],
+ ['dummy2', 2, 0, []],
+ ['dummy3', 2, 2, ['access CiviCRM', 'administer CiviCRM', 'view debug output']],
+ ];
+ }
+
+ /**
+ * Tests that AngularLoader only conditionally loads settings via factory functions for in-use modules.
+ * Our dummy settings callback functions keep a count of the number of times they have been called.
+ *
+ * @dataProvider factoryScenarios
+ * @param $module
+ * @param $expectedSettingCount
+ * @param $expectedCallbackCount
+ * @param $expectedPermissions
+ */
+ public function testSettingFactory($module, $expectedSettingCount, $expectedCallbackCount, $expectedPermissions) {
+ (new \Civi\Angular\AngularLoader())
+ ->setModules([$module])
+ ->useApp()
+ ->load();
+
+ // Run factory callbacks
+ $actual = \Civi::resources()->getSettings();
+
+ // Dummy1 module's factory setting should be set if it is loaded directly or required by dummy3
+ $this->assertTrue(($expectedCallbackCount > 0) === isset($actual['dummy1']['dummy_setting_factory']));
+ // Dummy3 module's factory setting should be set if it is loaded directly
+ $this->assertTrue(($expectedCallbackCount > 1) === isset($actual['dummy3']['dummy_setting_factory']));
+
+ // Dummy1 module's regular setting should be set if it is loaded directly or required by dummy3
+ $this->assertTrue(($module !== 'dummy2') === isset($actual['dummy1']['dummy_setting']));
+ // Dummy2 module's regular setting should be set if loaded
+ $this->assertTrue(($module === 'dummy2') === isset($actual['dummy2']['dummy_setting']));
+
+ // Assert appropriate permissions have been added
+ $this->assertEquals($expectedPermissions, array_keys($actual['permissions']));
+
+ // Assert the callback functions ran the expected number of times
+ $this->assertEquals($expectedSettingCount, self::$dummy_setting_count);
+ $this->assertEquals($expectedCallbackCount, self::$dummy_callback_count);
+ }
+
+ public function hook_angularModules(&$modules) {
+ $modules['dummy1'] = [
+ 'ext' => 'civicrm',
+ 'settings' => $this->getDummySetting(),
+ 'permissions' => ['access CiviCRM', 'administer CiviCRM'],
+ 'settingsFactory' => [self::class, 'getDummySettingFactory'],
+ ];
+ $modules['dummy2'] = [
+ 'ext' => 'civicrm',
+ 'settings' => $this->getDummySetting(),
+ ];
+ $modules['dummy3'] = [
+ 'ext' => 'civicrm',
+ // The string self::class is preferred but passing object $this should also work
+ 'settingsFactory' => [$this, 'getDummySettingFactory'],
+ // This should get merged with dummy1's permissions
+ 'permissions' => ['view debug output', 'administer CiviCRM'],
+ 'requires' => ['dummy1'],
+ ];
+ }
+
+ public function getDummySetting() {
+ return ['dummy_setting' => self::$dummy_setting_count++];
+ }
+
+ public static function getDummySettingFactory() {
+ return ['dummy_setting_factory' => self::$dummy_callback_count++];
+ }
+
+}
* @throws \CRM_Core_Exception
*/
public function setupRecurringPaymentProcessorTransaction($recurParams = [], $contributionParams = []) {
+ $this->ids['campaign'][0] = $this->callAPISuccess('Campaign', 'create', ['title' => 'get the money'])['id'];
$contributionParams = array_merge([
'total_amount' => '200',
'invoice_id' => $this->_invoiceID,
'is_test' => 0,
'receive_date' => '2019-07-25 07:34:23',
'skipCleanMoney' => TRUE,
+ 'amount_level' => 'expensive',
+ 'campaign_id' => $this->ids['campaign'][0],
], $contributionParams);
$contributionRecur = $this->callAPISuccess('contribution_recur', 'create', array_merge([
'contact_id' => $this->_contactID,
* Check that the active prev-next service behaves as expected.
*
* @package E2E\Core
- * @group e2e
+ * @group e2e ornery
*/
class PrevNextTest extends \CiviEndToEndTestCase {
$this->assertEquals(4, $this->prevNext->getCount($this->cacheKey));
$this->assertEquals(0, $this->prevNext->getCount('not-a-key-' . $this->cacheKey));
- $all = $this->prevNext->getSelection($this->cacheKey, 'getall')[$this->cacheKey];
- $this->assertEquals([100, 400, 200, 300], array_keys($all));
+ $all = $this->assertSelections([100, 400, 200, 300], 'getall', $this->cacheKey);
$this->assertEquals([1], array_unique(array_values($all)));
- $this->assertSelections([]);
+ $this->assertSelections([], 'get', $this->cacheKey);
}
public function testFetch() {
// Add some data that we're actually working with.
$this->testFillArray();
- $all = $this->prevNext->getSelection($this->cacheKey, 'getall')[$this->cacheKey];
- $this->assertEquals([100, 400, 200, 300], array_keys($all), 'selected cache not correct for ' . $this->cacheKey
- . ' defined keys are ' . $this->cacheKey . 'and ' . $this->cacheKeyB
- . ' the prevNext cache is ' . print_r($this->prevNext, TRUE)
- );
+ $all = $this->assertSelections([100, 400, 200, 300], 'getall', $this->cacheKey);
list ($id1, $id2, $id3) = array_keys($all);
$this->prevNext->markSelection($this->cacheKey, 'select', [$id1, $id3]);
- $this->assertSelections([$id1, $id3]);
+ $this->assertSelections([$id1, $id3], 'get', $this->cacheKey);
$this->prevNext->deleteItem(NULL, $this->cacheKey);
- $all = $this->prevNext->getSelection($this->cacheKey, 'getall')[$this->cacheKey];
- $this->assertEquals([], array_keys($all));
- $this->assertSelections([]);
+ $this->assertSelections([], 'getall', $this->cacheKey);
+ $this->assertSelections([], 'get', $this->cacheKey);
// Ensure background data was untouched.
$this->assertSelections([100], 'get', $this->cacheKeyB);
* Contact IDs that should be selected.
* @param string $action
* @param string|NULL $cacheKey
+ * @return array
+ * Contact IDs that were returned by getSelection($cacheKey, $action)
*/
protected function assertSelections($ids, $action = 'get', $cacheKey = NULL) {
if ($cacheKey === NULL) {
);
$this->assertCount(count($ids), $selected);
+ return $selected;
}
}
use CRMTraits_Profile_ProfileTrait;
use CRMTraits_Custom_CustomDataTrait;
+ use CRMTraits_Financial_OrderTrait;
protected $_individualId;
protected $_contribution;
$this->assertEquals($expectedLineItem, $lineItem2['values'][0]);
}
+ /**
+ * Test Contribution with Order api.
+ *
+ * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
+ */
+ public function testContributionOrder() {
+ $this->_contactID = $this->individualCreate();
+ $this->createContributionAndMembershipOrder();
+ $contribution = $this->callAPISuccess('contribution', 'get')['values'][$this->ids['Contribution'][0]];
+ $this->assertEquals('Pending Label**', $contribution['contribution_status']);
+ $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_contactID]);
+
+ $this->callAPISuccess('Payment', 'create', [
+ 'contribution_id' => $this->ids['Contribution'][0],
+ 'payment_instrument_id' => 'Check',
+ 'total_amount' => 300,
+ ]);
+ $contribution = $this->callAPISuccess('contribution', 'get')['values'][$this->ids['Contribution'][0]];
+ $this->assertEquals('Completed', $contribution['contribution_status']);
+
+ $lineItem = $this->callAPISuccess('LineItem', 'get', [
+ 'sequential' => 1,
+ 'contribution_id' => $this->ids['Contribution'][0],
+ ])['values'];
+ $this->assertCount(2, $lineItem);
+ $this->assertEquals($this->ids['Contribution'][0], $lineItem[0]['entity_id']);
+ $this->assertEquals('civicrm_contribution', $lineItem[0]['entity_table']);
+ $this->assertEquals($this->ids['Contribution'][0], $lineItem[0]['contribution_id']);
+ $this->assertEquals($this->ids['Contribution'][0], $lineItem[1]['contribution_id']);
+ $this->assertEquals('100.00', $lineItem[0]['line_total']);
+ $this->assertEquals('200.00', $lineItem[1]['line_total']);
+ $this->assertEquals($membership['id'], $lineItem[1]['entity_id']);
+ $this->assertEquals('civicrm_membership', $lineItem[1]['entity_table']);
+ }
+
/**
* Test financial_type_id override behaviour with a single line item.
*
], [
'Event',
]);
+ $this->checkReceiptDetails($mut, $contributionPage['id'], $contribution['id']);
$mut->stop();
}
]);
}
+ /**
+ * Check receipt details in sent mail via API
+ *
+ * @param CiviMailUtils $mut
+ * @param int $pageID Page ID
+ * @param int $contributionID Contribution ID
+ *
+ * @throws \CRM_Core_Exception
+ */
+ public function checkReceiptDetails($mut, $pageID, $contributionID) {
+ $pageReceipt = [
+ 'receipt_from_name' => "Page FromName",
+ 'receipt_from_email' => "page_from@email.com",
+ 'cc_receipt' => "page_cc@email.com",
+ 'receipt_text' => "Page Receipt Text",
+ ];
+ $customReceipt = [
+ 'receipt_from_name' => "Custom FromName",
+ 'receipt_from_email' => "custom_from@email.com",
+ 'cc_receipt' => "custom_cc@email.com",
+ 'receipt_text' => "Test Custom Receipt Text",
+ ];
+ $this->callAPISuccess('ContributionPage', 'create', array_merge([
+ 'id' => $pageID,
+ 'is_email_receipt' => 1,
+ ], $pageReceipt));
+
+ $this->callAPISuccess('contribution', 'sendconfirmation', array_merge([
+ 'id' => $contributionID,
+ 'payment_processor_id' => $this->paymentProcessorID,
+ ], $customReceipt));
+
+ //Verify if custom receipt details are present in email.
+ $mut->checkMailLog(array_values($customReceipt), array_values($pageReceipt));
+ }
+
/**
* Test sending a mail via the API.
*/
* @subpackage API_Job
*
* @copyright CiviCRM LLC https://civicrm.org/licensing
- * @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $
*
*/
$this->_mut->assertRecipients($this->getRecipients(1, 2));
}
+ /**
+ * Test that a contact deleted after the mailing is queued is not emailed.
+ *
+ * @throws \CRM_Core_Exception
+ */
+ public function testDeletedRecipient() {
+ $this->createContactsInGroup(2, $this->_groupID);
+ $this->callAPISuccess('Mailing', 'create', $this->_params);
+ $this->callAPISuccess('Contact', 'delete', ['id' => $this->callAPISuccessGetValue('GroupContact', ['return' => 'contact_id', 'options' => ['limit' => 1, 'sort' => 'id DESC']])]);
+ $this->callAPISuccess('job', 'process_mailing');
+ $this->_mut->assertRecipients($this->getRecipients(1, 1));
+ }
+
/**
* Test what happens when a contact is set to decesaed
*/
- public function testDecesasedRecepient() {
+ public function testDeceasedRecipient() {
$contactID = $this->individualCreate(['first_name' => 'test dead recipeint', 'email' => 'mailtestdead@civicrm.org']);
$this->callAPISuccess('group_contact', 'create', [
'contact_id' => $contactID,
'civicrm_activity_contact',
'civicrm_activity',
]);
+ Civi::settings()->set('mailerBatchLimit', 0);
}
/**
/**
* CRM-18503 - Test membership join date is correctly set for fixed memberships.
*
- * @throws \CRM_Core_Exception
+ * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
*/
public function testMembershipJoinDateFixed() {
$memStatus = CRM_Member_PseudoConstant::membershipStatus();
'membership_type_id' => $this->_membershipTypeID2,
'source' => 'test membership',
'is_pay_later' => 0,
- 'status_id' => array_search('Pending', $memStatus),
+ 'status_id' => 'Pending',
'skipStatusCal' => 1,
'is_for_organization' => 1,
];
- $membership = CRM_Member_BAO_Membership::create($params);
+ $membership = $this->callAPISuccess('Membership', 'create', $params);
// Update membership to 'Completed' and check the dates.
$memParams = [
- 'id' => $membership->id,
+ 'id' => $membership['id'],
'contact_id' => $contactId,
'is_test' => 0,
'membership_type_id' => $this->_membershipTypeID2,
'num_terms' => 1,
- 'status_id' => array_search('New', $memStatus),
+ 'status_id' => 'New',
];
$result = $this->callAPISuccess('Membership', 'create', $memParams);
$rollOver = FALSE;
$startDate = date('Y-m-d', strtotime(date('Y-03-01') . '- 1 year'));
}
- $membershipTypeDetails = CRM_Member_BAO_MembershipType::getMembershipTypeDetails($this->_membershipTypeID2);
+ $membershipTypeDetails = CRM_Member_BAO_MembershipType::getMembershipType($this->_membershipTypeID2);
$fixedPeriodRollover = CRM_Member_BAO_MembershipType::isDuringFixedAnnualRolloverPeriod($joinDate, $membershipTypeDetails, $year, $startDate);
$y = 1;
if ($fixedPeriodRollover && $rollOver) {
- $y += 1;
+ ++$y;
}
$expectedDates = [
]);
}
- /**
- * Test cancel order api
- */
- public function testCancelWithParticipant() {
- $event = $this->eventCreate();
- $this->_eventId = $event['id'];
- $eventParams = [
- 'id' => $this->_eventId,
- 'financial_type_id' => 4,
- 'is_monetary' => 1,
- ];
- $this->callAPISuccess('event', 'create', $eventParams);
- $participantParams = [
- 'financial_type_id' => 4,
- 'event_id' => $this->_eventId,
- 'role_id' => 1,
- 'status_id' => 1,
- 'fee_currency' => 'USD',
- 'contact_id' => $this->_individualId,
- ];
- $participant = $this->callAPISuccess('Participant', 'create', $participantParams);
- $extraParams = [
- 'contribution_mode' => 'participant',
- 'participant_id' => $participant['id'],
- ];
- $contribution = $this->addOrder(TRUE, 100, $extraParams);
- $paymentParticipant = [
- 'participant_id' => $participant['id'],
- 'contribution_id' => $contribution['id'],
- ];
- $this->callAPISuccess('ParticipantPayment', 'create', $paymentParticipant);
- $params = [
- 'contribution_id' => $contribution['id'],
- ];
- $this->callAPISuccess('order', 'cancel', $params);
- $order = $this->callAPISuccess('Order', 'get', $params);
- $expectedResult = [
- $contribution['id'] => [
- 'total_amount' => 100,
- 'contribution_id' => $contribution['id'],
- 'contribution_status' => 'Cancelled',
- 'net_amount' => 100,
- ],
- ];
- $this->checkPaymentResult($order, $expectedResult);
- $participantPayment = $this->callAPISuccess('ParticipantPayment', 'getsingle', $params);
- $participant = $this->callAPISuccess('participant', 'get', ['id' => $participantPayment['participant_id']]);
- $this->assertEquals($participant['values'][$participant['id']]['participant_status'], 'Cancelled');
- $this->callAPISuccess('Contribution', 'Delete', [
- 'id' => $contribution['id'],
- ]);
- }
-
/**
* Test an exception is thrown if line items do not add up to total_amount, no tax.
*/
$this->assertEquals($contribution['contribution_status'], 'Refunded Label**');
}
+ /**
+ * Test negative payment using create API when the "cancelled_payment_id" param is set.
+ *
+ * @throws \CRM_Core_Exception
+ */
+ public function testRefundPaymentWithCancelledPaymentId() {
+ $result = $this->callAPISuccess('Contribution', 'create', [
+ 'financial_type_id' => "Donation",
+ 'total_amount' => 100,
+ 'contact_id' => $this->_individualId,
+ ]);
+ $contributionID = $result['id'];
+
+ //Refund the complete amount.
+ $this->callAPISuccess('Payment', 'create', [
+ 'contribution_id' => $contributionID,
+ 'total_amount' => -100,
+ 'cancelled_payment_id' => 12345,
+ ]);
+ $contribution = $this->callAPISuccessGetSingle('Contribution', [
+ 'return' => ['contribution_status_id'],
+ 'id' => $contributionID,
+ ]);
+ //Assert if main contribution status is updated to "Refunded".
+ $this->assertEquals($contribution['contribution_status'], 'Refunded Label**');
+ }
+
/**
* Test cancel payment api
*
$limit2 = Contact::get(FALSE)->setLimit(2)->addSelect('sort_name', 'row_count')->execute();
$this->assertCount(2, (array) $limit2);
$this->assertCount($num, $limit2);
+ try {
+ $limit2->single();
+ }
+ catch (\API_Exception $e) {
+ $this->assertRegExp(';Expected to find one Contact record;', $e->getMessage());
+ }
+ $limit1 = Contact::get(FALSE)->setLimit(1)->execute();
+ $this->assertCount(1, (array) $limit1);
+ $this->assertCount(1, $limit1);
+ $this->assertTrue(!empty($limit1->single()['sort_name']));
}
}
$info = $entityClass::getInfo();
$this->assertNotEmpty($info['name']);
$this->assertNotEmpty($info['title']);
- $this->assertNotEmpty($info['titlePlural']);
+ $this->assertNotEmpty($info['title_plural']);
$this->assertNotEmpty($info['type']);
$this->assertNotEmpty($info['description']);
}
$requiredParams = [];
foreach ($requiredFields as $requiredField) {
$value = $this->getRequiredValue($requiredField);
+ if ($entity === 'UFField' && $requiredField->getName() === 'field_name') {
+ // This is a ruthless hack to avoid a unique constraint - but
+ // it's also a test class & hard to care enough to do something
+ // better
+ $value = 'activity_campaign_id';
+ }
$requiredParams[$requiredField->getName()] = $value;
}
<add>1.1</add>
<log>true</log>
<icon>fa-tasks</icon>
+ <paths>
+ <add>civicrm/activity?reset=1&action=add&context=standalone</add>
+ <view>civicrm/activity?reset=1&action=view&id=[id]</view>
+ <update>civicrm/activity/add?reset=1&action=update&id=[id]</update>
+ <delete>civicrm/activity?reset=1&action=delete&id=[id]</delete>
+ </paths>
<field>
<name>id</name>
<uniqueName>activity_id</uniqueName>
<comment>Campaign Details.</comment>
<add>3.3</add>
<icon>fa-bullhorn</icon>
+ <paths>
+ <add>civicrm/campaign/add?reset=1</add>
+ <update>civicrm/campaign/add?reset=1&action=update&id=[id]</update>
+ <delete>civicrm/campaign/add?reset=1&action=delete&id=[id]</delete>
+ </paths>
<field>
<name>id</name>
<title>Campaign ID</title>
<add>1.1</add>
<log>true</log>
<icon>fa-address-book-o</icon>
+ <paths>
+ <add>civicrm/contact/add?reset=1&ct=[contact_type]</add>
+ <view>civicrm/contact/view?reset=1&cid=[id]</view>
+ <update>civicrm/contact/add?reset=1&action=update&cid=[id]</update>
+ <delete>civicrm/contact/view/delete?reset=1&delete=1&cid=[id]</delete>
+ </paths>
<field>
<name>id</name>
<type>int unsigned</type>
<add>1.1</add>
<log>true</log>
<icon>fa-users</icon>
+ <paths>
+ <add>civicrm/group/add?reset=1</add>
+ </paths>
<field>
<name>id</name>
<type>int unsigned</type>
<title>Group Title</title>
<length>255</length>
<localizable>true</localizable>
- <required>true</required>
<comment>Name of Group.</comment>
<add>1.1</add>
<html>
<name>id</name>
<autoincrement>false</autoincrement>
</primaryKey>
+
+ <field>
+ <name>name</name>
+ <title>Saved Search Name</title>
+ <type>varchar</type>
+ <length>255</length>
+ <default>NULL</default>
+ <comment>Unique name of saved search</comment>
+ <html>
+ <type>Text</type>
+ </html>
+ <add>1.0</add>
+ </field>
+ <index>
+ <name>UI_name</name>
+ <fieldName>name</fieldName>
+ <unique>true</unique>
+ <add>5.32</add>
+ </index>
+
+ <field>
+ <name>label</name>
+ <title>Saved Search Label</title>
+ <type>varchar</type>
+ <length>255</length>
+ <default>NULL</default>
+ <comment>Administrative label for search</comment>
+ <html>
+ <type>Text</type>
+ </html>
+ <add>5.32</add>
+ </field>
+
<field>
<name>form_values</name>
<title>Submitted Form Values</title>
<serialize>PHP</serialize>
<add>1.1</add>
</field>
+
<field>
<name>mapping_id</name>
<type>int unsigned</type>
<onDelete>SET NULL</onDelete>
<add>1.5</add>
</foreignKey>
+
<field>
<name>search_custom_id</name>
<type>int unsigned</type>
<comment>Foreign key to civicrm_option value table used for saved custom searches.</comment>
<add>2.0</add>
</field>
+
<field>
<name>where_clause</name>
<type>text</type>
<add>1.6</add>
<drop>5.24</drop>
</field>
+
<field>
<name>select_tables</name>
<type>text</type>
<add>1.6</add>
<drop>5.24</drop>
</field>
+
<field>
<name>where_tables</name>
<type>text</type>
<add>1.6</add>
<drop>5.24</drop>
</field>
+
<field>
<name>api_entity</name>
<type>varchar</type>
<comment>Entity name for API based search</comment>
<add>5.24</add>
</field>
+
<field>
<name>api_params</name>
<type>text</type>
<add>1.3</add>
<log>true</log>
<icon>fa-credit-card</icon>
+ <paths>
+ <add>civicrm/contribute/add?reset=1&action=add&context=standalone</add>
+ <view>civicrm/contact/view/contribution?reset=1&action=view&id=[id]</view>
+ <update>civicrm/contact/view/contribution?reset=1&action=update&id=[id]</update>
+ <delete>civicrm/contact/view/contribution?reset=1&action=delete&id=[id]</delete>
+ </paths>
<field>
<name>id</name>
<uniqueName>contribution_id</uniqueName>
<add>1.7</add>
<log>true</log>
<icon>fa-calendar</icon>
+ <paths>
+ <add>civicrm/event/add?reset=1</add>
+ <view>civicrm/event/info?reset=1&id=[id]</view>
+ </paths>
<field>
<name>id</name>
<type>int unsigned</type>
<title>Payment Processor</title>
<type>int unsigned</type>
<comment>Payment Processor for this financial transaction</comment>
+ <pseudoconstant>
+ <table>civicrm_payment_processor</table>
+ <keyColumn>id</keyColumn>
+ <labelColumn>name</labelColumn>
+ </pseudoconstant>
+ <html>
+ <type>Select</type>
+ </html>
<add>4.3</add>
</field>
<foreignKey>
<add>1.8</add>
<log>true</log>
<icon>fa-money</icon>
+ <paths>
+ <add>civicrm/grant/add?reset=1&action=add&context=standalone</add>
+ <view>contact/view/grant?reset=1&action=view&id=[id]&cid=[contact_id]</view>
+ <update>civicrm/contact/view/grant?reset=1&action=update&id=[id]&cid=[contact_id]</update>
+ <delete>civicrm/contact/view/grant?reset=1&action=delete&id=[id]&cid=[contact_id]</delete>
+ </paths>
<field>
<name>id</name>
<type>int unsigned</type>
<comment>Stores information about a mailing.</comment>
<archive>true</archive>
<icon>fa-envelope-o</icon>
+ <paths>
+ <add>civicrm/a/#/mailing/new</add>
+ <update>civicrm/a/#/mailing/[id]</update>
+ </paths>
<field>
<name>id</name>
<title>Mailing ID</title>
<add>1.5</add>
<log>true</log>
<icon>fa-id-badge</icon>
+ <paths>
+ <add>civicrm/member/add?reset=1&action=add&context=standalone</add>
+ <view>civicrm/contact/view/membership?reset=1&action=view&id=[id]&cid=[contact_id]</view>
+ <update>civicrm/contact/view/membership?reset=1&action=update&id=[id]&cid=[contact_id]</update>
+ <delete>civicrm/contact/view/membership?reset=1&action=delete&id=[id]&cid=[contact_id]</delete>
+ </paths>
<field>
<name>id</name>
<uniqueName>membership_id</uniqueName>
INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'greenwich', 'Theme: Greenwich', 'Theme: Greenwich', 'greenwich', 1);
INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'eventcart', 'Event cart', 'Event cart', 'eventcart', 1);
INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'financialacls', 'Financial ACLs', 'Financial ACLs', 'financialacls', 1);
+INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'contributioncancelactions', 'Contribution cancel actions', 'Contribution cancel actions', 'contributioncancelactions', 1);
-- department of France (CRM-4769)
(10009, 1076, "39", "Jura"),
--- new Italian provinces, as yet without codes (CRM-5048)
-(10010, 1107, "Bar", "Barletta-Andria-Trani"),
-(10011, 1107, "Fer", "Fermo"),
-(10012, 1107, "Mon", "Monza e Brianza"),
+-- new Italian provinces (CRM-5048)
+(10010, 1107, "BT", "Barletta-Andria-Trani"),
+(10011, 1107, "FM", "Fermo"),
+(10012, 1107, "MB", "Monza e Brianza"),
-- new UK provinces (CRM-5224)
(10013, 1226, "CWD", "Clwyd"),
* @var bool
*/
public static $_log = {$table.log|strtoupper};
+ {if $table.paths}
+ /**
+ * Paths for accessing this entity in the UI.
+ *
+ * @var string[]
+ */
+ protected static $_paths = {$table.paths|@print_array};
+ {/if}
{foreach from=$table.fields item=field}
/**
<?xml version="1.0" encoding="iso-8859-1" ?>
<version>
- <version_no>5.32.alpha1</version_no>
+ <version_no>5.33.alpha1</version_no>
</version>