Merge pull request #18748 from eileenmcnaughton/log
authorSeamus Lee <seamuslee001@gmail.com>
Fri, 13 Nov 2020 04:48:12 +0000 (15:48 +1100)
committerGitHub <noreply@github.com>
Fri, 13 Nov 2020 04:48:12 +0000 (15:48 +1100)
dev/core#2120 Do not attempt to obsolete primary key on log tables

392 files changed:
.gitignore
CRM/ACL/BAO/ACL.php
CRM/Activity/Controller/Search.php
CRM/Activity/DAO/Activity.php
CRM/Activity/Export/Form/Select.php
CRM/Activity/Form/Task.php
CRM/Admin/Form/MailSettings.php
CRM/Admin/Form/MessageTemplates.php
CRM/Admin/Page/MailSettings.php
CRM/Batch/Form/Entry.php
CRM/Campaign/Controller/Search.php
CRM/Campaign/DAO/Campaign.php
CRM/Campaign/Form/Task.php
CRM/Campaign/Selector/Search.php
CRM/Case/Controller/Search.php
CRM/Contact/BAO/Contact.php
CRM/Contact/BAO/DashboardContact.php
CRM/Contact/BAO/Group.php
CRM/Contact/BAO/GroupContact.php
CRM/Contact/BAO/Relationship.php
CRM/Contact/BAO/RelationshipCache.php
CRM/Contact/BAO/SavedSearch.php
CRM/Contact/Controller/Search.php
CRM/Contact/DAO/Contact.php
CRM/Contact/DAO/Group.php
CRM/Contact/DAO/SavedSearch.php
CRM/Contact/Export/Form/Select.php
CRM/Contact/Form/Edit/Address.php
CRM/Contact/Form/Edit/TagsAndGroups.php
CRM/Contact/Form/Search/Basic.php
CRM/Contact/Form/Search/Custom/FullText.php
CRM/Contact/Page/View/UserDashBoard/GroupContact.php
CRM/Contact/Selector.php
CRM/Contribute/BAO/Contribution.php
CRM/Contribute/BAO/ContributionRecur.php
CRM/Contribute/BAO/Query.php
CRM/Contribute/Controller/Search.php
CRM/Contribute/DAO/Contribution.php
CRM/Contribute/Export/Form/Select.php
CRM/Contribute/Form/Contribution.php
CRM/Contribute/Form/Contribution/Confirm.php
CRM/Contribute/Form/Task.php
CRM/Contribute/Form/Task/Batch.php
CRM/Contribute/Page/Tab.php
CRM/Contribute/Page/UserDashboard.php
CRM/Core/BAO/CustomField.php
CRM/Core/BAO/CustomOption.php
CRM/Core/BAO/MailSettings.php
CRM/Core/BAO/Phone.php
CRM/Core/BAO/SchemaHandler.php
CRM/Core/BAO/UFGroup.php
CRM/Core/ClassLoader.php
CRM/Core/CodeGen/Specification.php
CRM/Core/CodeGen/Util/Template.php
CRM/Core/DAO.php
CRM/Core/Error.php
CRM/Core/Form.php
CRM/Core/Form/Task.php
CRM/Core/I18n/PseudoConstant.php
CRM/Core/I18n/SchemaStructure.php
CRM/Core/Payment.php
CRM/Core/Payment/AuthorizeNetIPN.php
CRM/Core/Payment/BaseIPN.php
CRM/Core/Payment/Manual.php
CRM/Core/Payment/PayPalIPN.php
CRM/Core/Payment/PayPalImpl.php
CRM/Core/Payment/PayPalProIPN.php
CRM/Core/Permission.php
CRM/Core/Resources/CollectionInterface.php
CRM/Core/Resources/CollectionTrait.php
CRM/Custom/Form/Field.php
CRM/Custom/Form/Option.php
CRM/Event/BAO/Event.php
CRM/Event/Controller/Search.php
CRM/Event/DAO/Event.php
CRM/Event/Export/Form/Select.php
CRM/Event/Form/Task.php
CRM/Event/Page/UserDashboard.php
CRM/Export/Form/Select.php
CRM/Export/Form/Select/Case.php
CRM/Financial/BAO/FinancialType.php
CRM/Financial/BAO/Payment.php
CRM/Financial/DAO/FinancialTrxn.php
CRM/Grant/Controller/Search.php
CRM/Grant/DAO/Grant.php
CRM/Grant/Export/Form/Select.php
CRM/Grant/Form/Task.php
CRM/Group/Form/Edit.php
CRM/Import/DataSource/CSV.php
CRM/Import/Form/DataSource.php
CRM/Logging/Differ.php
CRM/Logging/ReportDetail.php
CRM/Logging/Schema.php
CRM/Mailing/BAO/Recipients.php
CRM/Mailing/DAO/Mailing.php
CRM/Mailing/Event/BAO/Unsubscribe.php
CRM/Mailing/Form/Component.php
CRM/Mailing/Form/Subscribe.php
CRM/Mailing/Info.php
CRM/Mailing/MailStore.php
CRM/Mailing/Page/AJAX.php
CRM/Mailing/Page/Url.php
CRM/Mailing/xml/Menu/Mailing.xml
CRM/Member/BAO/Membership.php
CRM/Member/BAO/MembershipType.php
CRM/Member/Controller/Search.php
CRM/Member/DAO/Membership.php
CRM/Member/Export/Form/Select.php
CRM/Member/Form/Membership.php
CRM/Member/Form/MembershipRenewal.php
CRM/Member/Form/Task.php
CRM/Member/Import/Parser/Membership.php
CRM/Member/Page/RecurringContributions.php
CRM/PCP/Form/Campaign.php
CRM/Pledge/Controller/Search.php
CRM/Pledge/Export/Form/Select.php
CRM/Pledge/Form/Task.php
CRM/Price/BAO/PriceSet.php
CRM/Report/Form/Activity.php
CRM/Report/Form/Contribute/Bookkeeping.php
CRM/Report/Form/Contribute/Detail.php
CRM/Report/Form/Contribute/Lybunt.php
CRM/Report/Form/Contribute/Recur.php
CRM/Report/Form/Contribute/Repeat.php
CRM/Report/Form/Contribute/SoftCredit.php
CRM/Report/Form/Contribute/Summary.php
CRM/Report/Form/Contribute/Sybunt.php
CRM/Report/Form/Contribute/TopDonor.php
CRM/SMS/Form/Group.php
CRM/Upgrade/Incremental/Base.php
CRM/Upgrade/Incremental/php/FiveThirtyOne.php
CRM/Upgrade/Incremental/php/FiveThirtyThree.php [new file with mode: 0644]
CRM/Upgrade/Incremental/php/FiveThirtyTwo.php
CRM/Upgrade/Incremental/sql/5.31.beta2.mysql.tpl [new file with mode: 0644]
CRM/Upgrade/Incremental/sql/5.32.alpha1.mysql.tpl
CRM/Upgrade/Incremental/sql/5.32.beta1.mysql.tpl [new file with mode: 0644]
CRM/Upgrade/Incremental/sql/5.33.alpha1.mysql.tpl [new file with mode: 0644]
CRM/Utils/API/HTMLInputCoder.php
CRM/Utils/Constant.php
CRM/Utils/Date.php
CRM/Utils/Hook.php
CRM/Utils/Money.php
CRM/Utils/ReCAPTCHA.php
CRM/Utils/String.php
CRM/Utils/System.php
CRM/Utils/System/Backdrop.php
Civi/Angular/AngularLoader.php
Civi/Angular/Manager.php
Civi/Api4/Action/Entity/Get.php
Civi/Api4/Action/MailSettings/TestConnection.php [new file with mode: 0644]
Civi/Api4/Entity.php
Civi/Api4/Generic/AbstractEntity.php
Civi/Api4/Generic/DAOEntity.php
Civi/Api4/Generic/Result.php
Civi/Api4/MailSettings.php
Civi/Api4/Utils/FormattingUtil.php
Civi/Install/Requirements.php
Civi/Payment/System.php
ang/api4Explorer/Clause.html
ang/api4Explorer/Explorer.js
ang/crmMailing.ang.php
api/class.api.php
api/v3/CustomField.php
api/v3/Order.php
bin/regen.sh
composer.json
composer.lock
contributor-key.yml
distmaker/dists/common.sh
ext/afform/core/ang/afformStandalone.js
ext/afform/gui/ang/afGuiEditor/main.html
ext/contributioncancelactions/LICENSE.txt [new file with mode: 0644]
ext/contributioncancelactions/README.md [new file with mode: 0644]
ext/contributioncancelactions/contributioncancelactions.civix.php [new file with mode: 0644]
ext/contributioncancelactions/contributioncancelactions.php [new file with mode: 0644]
ext/contributioncancelactions/info.xml [new file with mode: 0644]
ext/contributioncancelactions/phpunit.xml.dist [new file with mode: 0644]
ext/contributioncancelactions/tests/phpunit/CancelTest.php [new file with mode: 0644]
ext/contributioncancelactions/tests/phpunit/bootstrap.php [new file with mode: 0644]
ext/financialacls/financialacls.php
ext/financialacls/tests/phpunit/Civi/Financialacls/BaseTestClass.php [new file with mode: 0644]
ext/financialacls/tests/phpunit/Civi/Financialacls/BuildAmountHookTest.php [new file with mode: 0644]
ext/financialacls/tests/phpunit/Civi/Financialacls/LineItemTest.php [moved from ext/financialacls/tests/phpunit/LineItemTest.php with 62% similarity]
ext/financialacls/tests/phpunit/Civi/Financialacls/MembershipTypesTest.php [new file with mode: 0644]
ext/financialacls/tests/phpunit/Civi/Financialacls/OptionsTest.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/Angular.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/BAO/OAuthClient.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/BAO/OAuthSysToken.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/DAO/OAuthClient.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/DAO/OAuthSysToken.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/MailSetup.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/Page/Return.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/Upgrader.php [new file with mode: 0644]
ext/oauth-client/CRM/OAuth/Upgrader/Base.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/Action/OAuthClient/AbstractGrantAction.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/Action/OAuthClient/AuthorizationCode.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/Action/OAuthClient/ClientCredential.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/Action/OAuthClient/Create.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/Action/OAuthClient/Update.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/Action/OAuthClient/UserPassword.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/Action/OAuthSysToken/Refresh.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/OAuthClient.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/OAuthProvider.php [new file with mode: 0644]
ext/oauth-client/Civi/Api4/OAuthSysToken.php [new file with mode: 0644]
ext/oauth-client/Civi/OAuth/CiviGenericProvider.php [new file with mode: 0644]
ext/oauth-client/Civi/OAuth/OAuthException.php [new file with mode: 0644]
ext/oauth-client/Civi/OAuth/OAuthLeagueFacade.php [new file with mode: 0644]
ext/oauth-client/Civi/OAuth/OAuthTokenFacade.php [new file with mode: 0644]
ext/oauth-client/LICENSE.txt [new file with mode: 0644]
ext/oauth-client/README.md [new file with mode: 0644]
ext/oauth-client/ang/oauthClientAdmin.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientAdmin.aff.json [new file with mode: 0644]
ext/oauth-client/ang/oauthClientCreateHelp.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientCreator.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientEditor.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientList.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientTokens.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthJwtDebug.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthJwtDebug.aff.json [new file with mode: 0644]
ext/oauth-client/ang/oauthProviderDetail.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthProviderList.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthUtil.ang.php [new file with mode: 0644]
ext/oauth-client/ang/oauthUtil.js [new file with mode: 0644]
ext/oauth-client/ang/unvalidatedJwtDecode.ang.php [new file with mode: 0644]
ext/oauth-client/ang/unvalidatedJwtDecode.js [new file with mode: 0644]
ext/oauth-client/bin/local-redir.php [new file with mode: 0644]
ext/oauth-client/bin/local-redir.sh [new file with mode: 0755]
ext/oauth-client/images/screenshot.png [new file with mode: 0644]
ext/oauth-client/info.xml [new file with mode: 0644]
ext/oauth-client/oauth_client.civix.php [new file with mode: 0644]
ext/oauth-client/oauth_client.php [new file with mode: 0644]
ext/oauth-client/phpunit.xml.dist [new file with mode: 0644]
ext/oauth-client/providers/gmail.dist.json [new file with mode: 0644]
ext/oauth-client/providers/ms-exchange.dist.json [new file with mode: 0644]
ext/oauth-client/providers/test_example_1.test.json [new file with mode: 0644]
ext/oauth-client/providers/test_example_2.test.json [new file with mode: 0644]
ext/oauth-client/settings/OAuthClient.setting.php [new file with mode: 0644]
ext/oauth-client/sql/auto_install.sql [new file with mode: 0644]
ext/oauth-client/sql/auto_uninstall.sql [new file with mode: 0644]
ext/oauth-client/templates/CRM/OAuth/Page/Return.tpl [new file with mode: 0644]
ext/oauth-client/tests/phpunit/CRM/OAuth/MailSetupTest.php [new file with mode: 0644]
ext/oauth-client/tests/phpunit/api/v4/OAuthClientGrantTest.php [new file with mode: 0644]
ext/oauth-client/tests/phpunit/api/v4/OAuthClientTest.php [new file with mode: 0644]
ext/oauth-client/tests/phpunit/api/v4/OAuthProviderTest.php [new file with mode: 0644]
ext/oauth-client/tests/phpunit/api/v4/OAuthSysTokenTest.php [new file with mode: 0644]
ext/oauth-client/tests/phpunit/bootstrap.php [new file with mode: 0644]
ext/oauth-client/xml/Menu/oauth_client.xml [new file with mode: 0644]
ext/oauth-client/xml/schema/CRM/OAuth/OAuthClient.entityType.php [new file with mode: 0644]
ext/oauth-client/xml/schema/CRM/OAuth/OAuthClient.xml [new file with mode: 0644]
ext/oauth-client/xml/schema/CRM/OAuth/OAuthSysToken.entityType.php [new file with mode: 0644]
ext/oauth-client/xml/schema/CRM/OAuth/OAuthSysToken.xml [new file with mode: 0644]
ext/search/CRM/Search/BAO/SearchDisplay.php [new file with mode: 0644]
ext/search/CRM/Search/DAO/SearchDisplay.php [new file with mode: 0644]
ext/search/CRM/Search/Page/Admin.php [new file with mode: 0644]
ext/search/CRM/Search/Page/Ang.php [deleted file]
ext/search/CRM/Search/Page/Search.php [new file with mode: 0644]
ext/search/CRM/Search/Upgrader.php
ext/search/Civi/Api4/SearchDisplay.php [new file with mode: 0644]
ext/search/Civi/Api4/Service/Spec/Provider/SearchDisplayCreationSpecProvider.php [new file with mode: 0644]
ext/search/Civi/Search/Actions.php [new file with mode: 0644]
ext/search/Civi/Search/Admin.php [new file with mode: 0644]
ext/search/Civi/Search/Display.php [new file with mode: 0644]
ext/search/ang/crmSearchActions.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchActions.module.js [new file with mode: 0644]
ext/search/ang/crmSearchActions/SaveSmartGroup.ctrl.js [moved from ext/search/ang/search/SaveSmartGroup.ctrl.js with 67% similarity]
ext/search/ang/crmSearchActions/crmSearchActionDelete.ctrl.js [moved from ext/search/ang/search/crmSearchActions/crmSearchActionDelete.ctrl.js with 68% similarity]
ext/search/ang/crmSearchActions/crmSearchActionDelete.html [moved from ext/search/ang/search/crmSearchActions/crmSearchActionDelete.html with 62% similarity]
ext/search/ang/crmSearchActions/crmSearchActionUpdate.ctrl.js [moved from ext/search/ang/search/crmSearchActions/crmSearchActionUpdate.ctrl.js with 69% similarity]
ext/search/ang/crmSearchActions/crmSearchActionUpdate.html [moved from ext/search/ang/search/crmSearchActions/crmSearchActionUpdate.html with 66% similarity]
ext/search/ang/crmSearchActions/crmSearchActions.component.js [moved from ext/search/ang/search/crmSearchActions.component.js with 52% similarity]
ext/search/ang/crmSearchActions/crmSearchActions.html [moved from ext/search/ang/search/crmSearchActions.html with 67% similarity]
ext/search/ang/crmSearchActions/saveSmartGroup.directive.js [new file with mode: 0644]
ext/search/ang/crmSearchActions/saveSmartGroup.html [moved from ext/search/ang/search/saveSmartGroup.html with 86% similarity]
ext/search/ang/crmSearchAdmin.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchAdmin.module.js [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/compose/controls.html [moved from ext/search/ang/search/crmSearch/controls.html with 51% similarity]
ext/search/ang/crmSearchAdmin/compose/criteria.html [moved from ext/search/ang/search/crmSearch/criteria.html with 54% similarity]
ext/search/ang/crmSearchAdmin/compose/debug.html [moved from ext/search/ang/search/crmSearch/debug.html with 100% similarity]
ext/search/ang/crmSearchAdmin/compose/pager.html [moved from ext/search/ang/search/crmSearch/pager.html with 100% similarity]
ext/search/ang/crmSearchAdmin/compose/results.html [moved from ext/search/ang/search/crmSearch/results.html with 53% similarity]
ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js [moved from ext/search/ang/search/crmSearch.component.js with 52% similarity]
ext/search/ang/crmSearchAdmin/crmSearchAdmin.html [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.html [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.directive.js [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/crmSearchClause.component.js [moved from ext/search/ang/search/crmSearchClause.component.js with 75% similarity]
ext/search/ang/crmSearchAdmin/crmSearchClause.html [moved from ext/search/ang/search/crmSearchClause.html with 95% similarity]
ext/search/ang/crmSearchAdmin/crmSearchFunction.component.js [moved from ext/search/ang/search/crmSearchFunction.component.js with 77% similarity]
ext/search/ang/crmSearchAdmin/crmSearchFunction.html [moved from ext/search/ang/search/crmSearchFunction.html with 100% similarity]
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/group.html [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/searchList.controller.js [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/searchList.html [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/tabs.html [new file with mode: 0644]
ext/search/ang/crmSearchDisplay.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchDisplay.module.js [new file with mode: 0644]
ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js [new file with mode: 0644]
ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html [new file with mode: 0644]
ext/search/ang/crmSearchKit.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchKit.module.js [new file with mode: 0644]
ext/search/ang/crmSearchKit/crmSearchValue.directive.js [moved from ext/search/ang/search/crmSearchValue.directive.js with 93% similarity]
ext/search/ang/crmSearchPage.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchPage.module.js [new file with mode: 0644]
ext/search/ang/search.ang.php [deleted file]
ext/search/ang/search.module.js [deleted file]
ext/search/ang/search/crmSearch.html [deleted file]
ext/search/css/search.css
ext/search/info.xml
ext/search/managed/SearchDisplayType.mgd.php [new file with mode: 0644]
ext/search/search.civix.php
ext/search/search.php
ext/search/sql/auto_install.sql [new file with mode: 0644]
ext/search/sql/auto_uninstall.sql [new file with mode: 0644]
ext/search/templates/CRM/Search/Page/Admin.tpl [moved from ext/search/templates/CRM/Search/Page/Ang.tpl with 100% similarity]
ext/search/templates/CRM/Search/Page/Search.tpl [new file with mode: 0644]
ext/search/xml/Menu/search.xml
ext/search/xml/schema/CRM/Search/SearchDisplay.entityType.php [new file with mode: 0644]
ext/search/xml/schema/CRM/Search/SearchDisplay.xml [new file with mode: 0644]
js/Common.js
release-notes.md
release-notes/5.30.1.md [new file with mode: 0644]
release-notes/5.31.0.md [new file with mode: 0644]
release-notes/5.32.0.md [new file with mode: 0644]
setup/plugins/blocks/advanced.tpl.php
setup/plugins/checkRequirements/CoreRequirementsAdapter.civi-setup.php
setup/plugins/init/Backdrop.civi-setup.php
setup/plugins/init/Drupal.civi-setup.php
setup/plugins/init/Drupal8.civi-setup.php
setup/plugins/init/WordPress.civi-setup.php
setup/plugins/installFiles/InstallSettingsFile.civi-setup.php
setup/src/Setup/DbUtil.php
setup/src/Setup/DrupalUtil.php
sql/civicrm_generated.mysql
sql/test_data_second_domain.mysql
templates/CRM/Admin/Page/MailSettings.tpl
templates/CRM/Block/FullTextSearch.tpl
templates/CRM/Contact/Form/Search/Custom/FullText.tpl
templates/CRM/Contribute/Form/Contribution/PremiumBlock.tpl
templates/CRM/Contribute/Form/ContributionPage/Amount.tpl
templates/CRM/Custom/Form/Field.tpl
templates/CRM/Group/Form/Edit.tpl
templates/CRM/Price/Form/Calculate.tpl
templates/CRM/Report/Form/Layout/Overlay.tpl
templates/CRM/Report/Form/Layout/Table.tpl
templates/CRM/common/civicrm.settings.php.template
tests/phpunit/CRM/Batch/Form/EntryTest.php
tests/phpunit/CRM/Contact/BAO/ContactTest.php
tests/phpunit/CRM/Contact/Page/View/UserDashBoardTest.php
tests/phpunit/CRM/Contribute/Page/TabTest.php [new file with mode: 0644]
tests/phpunit/CRM/Core/BAO/CustomFieldTest.php
tests/phpunit/CRM/Core/ErrorTest.php
tests/phpunit/CRM/Core/FormTest.php
tests/phpunit/CRM/Core/Payment/AuthorizeNetIPNTest.php
tests/phpunit/CRM/Core/Payment/BaseIPNTest.php
tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php
tests/phpunit/CRM/Core/Resources/CollectionTestTrait.php
tests/phpunit/CRM/Event/BAO/EventPermissionsTest.php
tests/phpunit/CRM/Export/BAO/ExportTest.php
tests/phpunit/CRM/Mailing/MailStoreTest.php [new file with mode: 0644]
tests/phpunit/CRM/Member/BAO/MembershipTest.php
tests/phpunit/CRM/Member/BAO/MembershipTypeTest.php
tests/phpunit/CRM/Member/Form/MembershipTest.php
tests/phpunit/CRMTraits/Custom/CustomDataTrait.php
tests/phpunit/CRMTraits/Financial/OrderTrait.php
tests/phpunit/Civi/Angular/LoaderTest.php [new file with mode: 0644]
tests/phpunit/CiviTest/CiviUnitTestCase.php
tests/phpunit/E2E/Core/PrevNextTest.php
tests/phpunit/api/v3/ContributionTest.php
tests/phpunit/api/v3/JobProcessMailingTest.php
tests/phpunit/api/v3/MembershipTest.php
tests/phpunit/api/v3/OrderTest.php
tests/phpunit/api/v3/PaymentTest.php
tests/phpunit/api/v4/Action/ContactGetTest.php
tests/phpunit/api/v4/Entity/ConformanceTest.php
tests/phpunit/api/v4/Service/TestCreationParameterProvider.php
xml/schema/Activity/Activity.xml
xml/schema/Campaign/Campaign.xml
xml/schema/Contact/Contact.xml
xml/schema/Contact/Group.xml
xml/schema/Contact/SavedSearch.xml
xml/schema/Contribute/Contribution.xml
xml/schema/Event/Event.xml
xml/schema/Financial/FinancialTrxn.xml
xml/schema/Grant/Grant.xml
xml/schema/Mailing/Mailing.xml
xml/schema/Member/Membership.xml
xml/templates/civicrm_data.tpl
xml/templates/civicrm_state_province.tpl
xml/templates/dao.tpl
xml/version.xml

index 4e197028908e5c863a3109384fdd3255c8fda977..26477c1b8360a05c547f46086f5460d68c9fdc68 100644 (file)
@@ -9,8 +9,10 @@
 !/ext/greenwich
 /ext/greenwich/dist
 /ext/greenwich/extern
+!/ext/oauth-client
 !/ext/search
 !/ext/financialacls
+!/ext/contributioncancelactions
 backdrop/
 bower_components
 CRM/Case/xml/configuration
index 2fa703199fa0deb142fe90a7fef9b8ef6f5d15d2..f1f8cb0df8a1c67f51fb2bc348cde8620f1d7dad 100644 (file)
@@ -518,6 +518,8 @@ ORDER BY a.object_id
     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) {
index 21ad61932c161c3d39980ca00ddabbbc8691ead2..226305d1692e6e0e7f14893c1a08a478c7679fd7 100644 (file)
@@ -28,6 +28,8 @@
  */
 class CRM_Activity_Controller_Search extends CRM_Core_Controller {
 
+  protected $entity = 'Activity';
+
   /**
    * Class constructor.
    *
@@ -46,6 +48,7 @@ class CRM_Activity_Controller_Search extends CRM_Core_Controller {
 
     // Add all the actions.
     $this->addActions();
+    $this->set('entity', $this->entity);
   }
 
   /**
index 26f50e0b5f652bb5d1e2df9557dfbf7b49e10ca8..39655685fd8c5b0c65f0cfb7d068eb29bd23dffd 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Activity/Activity.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:c1b4cc908c0220abf69f57d281eeda95)
+ * (GenCodeChecksum:af42b47e95e8c7f47eb7a3ef53833383)
  */
 
 /**
@@ -37,6 +37,18 @@ class CRM_Activity_DAO_Activity extends CRM_Core_DAO {
    */
   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
    *
index ef856053fc3f9d72d09cdeacd1630d3cac4625be..d50e2a83297bdfcdca0f8df288d94279a1344915 100644 (file)
@@ -36,4 +36,22 @@ class CRM_Activity_Export_Form_Select extends CRM_Export_Form_Select {
     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';
+  }
+
 }
index 3be85d5bc3025648c3dd67e80dce747db8142542..bee1f03e482f9d3f965c7a82e9bc3700265afb03 100644 (file)
@@ -50,12 +50,8 @@ class CRM_Activity_Form_Task extends CRM_Core_Form_Task {
     $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');
@@ -83,24 +79,7 @@ class CRM_Activity_Form_Task extends CRM_Core_Form_Task {
     }
 
     $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');
   }
 
   /**
index 70c227b1ede4f976e260ce4a8e65808b81b17487..c41cb9b509d6483f1ce1821ee53b0b69a2501bfb 100644 (file)
@@ -21,6 +21,8 @@
  */
 class CRM_Admin_Form_MailSettings extends CRM_Admin_Form {
 
+  protected $_testButtonName;
+
   /**
    * Build the form object.
    */
@@ -32,6 +34,16 @@ class CRM_Admin_Form_MailSettings extends CRM_Admin_Form {
       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.
@@ -79,7 +91,7 @@ class CRM_Admin_Form_MailSettings extends CRM_Admin_Form {
    * 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() {
@@ -107,15 +119,21 @@ class CRM_Admin_Form_MailSettings extends CRM_Admin_Form {
    *
    * @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;
@@ -185,6 +203,14 @@ class CRM_Admin_Form_MailSettings extends CRM_Admin_Form {
     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');
+    }
   }
 
 }
index 47567b4742741093626ed2d3a51fe3ed52da2c4b..bc3f3e5074810e0dcfb96a7e23815d6cfa59e9b2 100644 (file)
@@ -270,6 +270,8 @@ class CRM_Admin_Form_MessageTemplates extends CRM_Core_Form {
   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
@@ -309,6 +311,11 @@ class CRM_Admin_Form_MessageTemplates extends CRM_Core_Form {
       }
 
       $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'])) {
index 6ede83649efaeff2a72b1afbf1314281e3902a70..c3dca8c85342c505c791ecfcda0eff51039243dc 100644 (file)
@@ -107,6 +107,11 @@ class CRM_Admin_Page_MailSettings extends CRM_Core_Page_Basic {
     }
 
     $this->assign('rows', $allMailSettings);
+
+    $setupActions = CRM_Core_BAO_MailSettings::getSetupActions();
+    if (count($setupActions) > 1 || !isset($setupActions['standard'])) {
+      $this->assign('setupActions', $setupActions);
+    }
   }
 
   /**
index 35c407a79f267102716643a7d1e62d989d065c35..fe349644421c62960f73b9858280fc9988353820 100644 (file)
@@ -433,7 +433,7 @@ class CRM_Batch_Form_Entry extends CRM_Core_Form {
       $this->processContribution($params);
     }
     elseif ($this->_batchInfo['type_id'] == $batchTypes['Membership']) {
-      $this->processMembership($params);
+      $params['actualBatchTotal'] = $this->processMembership($params);
     }
 
     // update batch to close status
@@ -623,7 +623,7 @@ class CRM_Batch_Form_Entry extends CRM_Core_Form {
         //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(
@@ -663,13 +663,16 @@ class CRM_Batch_Form_Entry extends CRM_Core_Form {
    * 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));
@@ -755,7 +758,7 @@ class CRM_Batch_Form_Entry extends CRM_Core_Form {
           $value['total_amount'] = (float) $value['total_amount'];
         }
 
-        $params['actualBatchTotal'] += $value['total_amount'];
+        $batchTotal += $value['total_amount'];
 
         unset($value['financial_type']);
         unset($value['payment_instrument']);
@@ -821,13 +824,6 @@ class CRM_Batch_Form_Entry extends CRM_Core_Form {
           $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,
@@ -838,7 +834,7 @@ class CRM_Batch_Form_Entry extends CRM_Core_Form {
             $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
@@ -849,45 +845,16 @@ class CRM_Batch_Form_Entry extends CRM_Core_Form {
           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
@@ -926,7 +893,7 @@ class CRM_Batch_Form_Entry extends CRM_Core_Form {
         }
       }
     }
-    return TRUE;
+    return $batchTotal;
   }
 
   /**
index 8089ddd23fc371c3d73d628a30d1e320bea89836..520c3f9442b73eb44f0b79c06136eae532e422fb 100644 (file)
@@ -28,6 +28,8 @@
  */
 class CRM_Campaign_Controller_Search extends CRM_Core_Controller {
 
+  protected $entity = 'Campaign';
+
   /**
    * Class constructor.
    *
@@ -45,8 +47,8 @@ class CRM_Campaign_Controller_Search extends CRM_Core_Controller {
     $this->addPages($this->_stateMachine, $action);
 
     // add all the actions
-    $config = CRM_Core_Config::singleton();
     $this->addActions();
+    $this->set('entity', $this->entity);
   }
 
 }
index 8dc0245910ea1a95712955f831fe5589ae63c872..89de98a9bb48c552089f7a006747a8ee92e99b3a 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Campaign/Campaign.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:a5a49e13e66a5d32b690835a49baf535)
+ * (GenCodeChecksum:1356a7073a6caa15e0da58422c6763b9)
  */
 
 /**
@@ -37,6 +37,17 @@ class CRM_Campaign_DAO_Campaign extends CRM_Core_DAO {
    */
   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.
    *
index 61411931d0ad3017097b620e3bd4e50440334711..f5193017a287e7e83a90081e02ba91ccd04d90ff 100644 (file)
@@ -58,15 +58,7 @@ class CRM_Campaign_Form_Task extends CRM_Core_Form_Task {
     $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');
   }
 
   /**
index 73ae1cb41d7bb556ef2e21f9d48ae5cf843d410d..f767a4602bc602d60cde21fa9e2417edfe385dc2 100644 (file)
@@ -267,7 +267,7 @@ class CRM_Campaign_Selector_Search extends CRM_Core_Selector_Base implements CRM
 
       $selectSQL = "
       SELECT %1, contact_a.id, contact_a.display_name
-FROM {$sql['from']}
+{$sql['from']}
 ";
 
       try {
index 4a4b47cc3f46e09cae41c25230be17f9d9e386a7..bfc16d3c89b8815bd95ddbc32b87a34f79063b88 100644 (file)
@@ -28,6 +28,8 @@
  */
 class CRM_Case_Controller_Search extends CRM_Core_Controller {
 
+  protected $entity = 'Case';
+
   /**
    * Class constructor.
    *
@@ -45,8 +47,8 @@ class CRM_Case_Controller_Search extends CRM_Core_Controller {
     $this->addPages($this->_stateMachine, $action);
 
     // add all the actions
-    $config = CRM_Core_Config::singleton();
     $this->addActions();
+    $this->set('entity', $this->entity);
   }
 
 }
index d258641cb70ae7fe95619484b4f3ca979f12402d..988cab9a5f527e969886de1bf583749514652a1a 100644 (file)
@@ -157,11 +157,18 @@ class CRM_Contact_BAO_Contact extends CRM_Contact_DAO_Contact {
       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;
index b9b728fa942341193eaf9f49ffeace1f4a405564..c6ff99f6891a9cfd33421c01eab9a20843b64da6 100644 (file)
  * @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.');
+      }
+    }
+  }
+
 }
index 96d94f478d84cd0e8b29ef2d27a0b7f4fa944bdf..5861b057571d81d39075582ba5361543015b1c79 100644 (file)
@@ -503,23 +503,23 @@ class CRM_Contact_BAO_Group extends CRM_Contact_DAO_Group {
   }
 
   /**
-   * 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;
     }
@@ -1030,6 +1030,7 @@ class CRM_Contact_BAO_Group extends CRM_Contact_DAO_Group {
    * @param string $parents
    * @param string $spacer
    * @param bool $titleOnly
+   * @param bool $public
    *
    * @return array
    */
@@ -1037,7 +1038,8 @@ class CRM_Contact_BAO_Group extends CRM_Contact_DAO_Group {
     $groupIDs,
     $parents = NULL,
     $spacer = '<span class="child-indent"></span>',
-    $titleOnly = FALSE
+    $titleOnly = FALSE,
+    $public = FALSE
   ) {
     if (empty($groupIDs)) {
       return [];
@@ -1055,7 +1057,7 @@ class CRM_Contact_BAO_Group extends CRM_Contact_DAO_Group {
     $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
 ";
@@ -1076,22 +1078,32 @@ 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,
         ];
       }
     }
index e2ae71125ee19ef44d498ea96921361b387564e2..a763bfdbae5cefce4595034efc751de3ed1c89bd 100644 (file)
@@ -304,6 +304,8 @@ class CRM_Contact_BAO_GroupContact extends CRM_Contact_DAO_GroupContact {
    *
    * @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
@@ -317,7 +319,8 @@ class CRM_Contact_BAO_GroupContact extends CRM_Contact_DAO_GroupContact {
     $onlyPublicGroups = FALSE,
     $excludeHidden = TRUE,
     $groupId = NULL,
-    $includeSmartGroups = FALSE
+    $includeSmartGroups = FALSE,
+    $public = FALSE
   ) {
     if ($count) {
       $select = 'SELECT count(DISTINCT civicrm_group_contact.id)';
@@ -326,6 +329,7 @@ class CRM_Contact_BAO_GroupContact extends CRM_Contact_DAO_GroupContact {
       $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,
@@ -393,7 +397,7 @@ class CRM_Contact_BAO_GroupContact extends CRM_Contact_DAO_GroupContact {
         $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) {
index 7e6c9bfef293bd386f263d994e746188ced54500..85a8f53822f191de1eb30b97dae3f8eaf995e5b1 100644 (file)
@@ -1896,14 +1896,15 @@ AND cc.sort_name LIKE '%$name%'";
   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
@@ -2431,7 +2432,7 @@ SELECT count(*)
     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;
   }
index edb76cd1dba402000a508a3582b26162c474a32d..c1483973e8120dfed4339228f97e1d4777a486aa 100644 (file)
@@ -71,6 +71,9 @@ class CRM_Contact_BAO_RelationshipCache extends CRM_Contact_DAO_RelationshipCach
    */
   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.
@@ -97,8 +100,8 @@ class CRM_Contact_BAO_RelationshipCache extends CRM_Contact_DAO_RelationshipCach
         '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
index d2ed700e7dcd4b6aaf05b72ac1108469622f17cd..d5e4456720c3d1e230e943ddc5c3d03f8b7917a7 100644 (file)
@@ -335,18 +335,28 @@ LEFT JOIN civicrm_email ON (contact_a.id = civicrm_email.contact_id AND civicrm_
   }
 
   /**
-   * 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);
   }
 
   /**
@@ -433,8 +443,7 @@ LEFT JOIN civicrm_email ON (contact_a.id = civicrm_email.contact_id AND civicrm_
     $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)) {
index 6d2d1c4f83e2cc42c5391f0e1f077af6c0ae65e2..af1ca61955c8a9134fe4ae1bb6ebe31cf6aa314f 100644 (file)
@@ -27,6 +27,8 @@
  */
 class CRM_Contact_Controller_Search extends CRM_Core_Controller {
 
+  protected $entity = 'Contact';
+
   /**
    * Class constructor.
    *
@@ -44,6 +46,7 @@ class CRM_Contact_Controller_Search extends CRM_Core_Controller {
 
     // add all the actions
     $this->addActions();
+    $this->set('entity', $this->entity);
   }
 
   /**
index c585b127039b72171f6e390c44967ce5a1900020..30a35c82d0c01597c635cd9a0685b8fdb8ead932 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Contact/Contact.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:f118596cceae71668861504b7316afa7)
+ * (GenCodeChecksum:25d4fcd3814380d746574c9a17b51134)
  */
 
 /**
@@ -37,6 +37,18 @@ class CRM_Contact_DAO_Contact extends CRM_Core_DAO {
    */
   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
    *
index c6428e5b0ef4b03ad27157625bfeae6de6476e00..b126c0e9fdd09e20655366d0f9562b98b7bf69a5 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Contact/Group.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:25bcea958ed3b88317a2bbb89169b0a6)
+ * (GenCodeChecksum:e40e779ac8e9bec5ea4d6c55f6f3b863)
  */
 
 /**
@@ -37,6 +37,15 @@ class CRM_Contact_DAO_Group extends CRM_Core_DAO {
    */
   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
    *
@@ -264,7 +273,6 @@ class CRM_Contact_DAO_Group extends CRM_Core_DAO {
           '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',
index c532997a2a3db7381442cdf6be1468f12e9e57c2..86d7d917d95b934b9fc93bac8a10d31ace8befb3 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Contact/SavedSearch.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:d863f8b0b8659633bc84578e1d6cbf10)
+ * (GenCodeChecksum:2a23a737d07cbfc49ce1d60a642fee3e)
  */
 
 /**
@@ -37,6 +37,20 @@ class CRM_Contact_DAO_SavedSearch extends CRM_Core_DAO {
    */
   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
    *
@@ -126,6 +140,42 @@ class CRM_Contact_DAO_SavedSearch extends CRM_Core_DAO {
           '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,
@@ -262,7 +312,17 @@ class CRM_Contact_DAO_SavedSearch extends CRM_Core_DAO {
    * @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;
   }
 
index d15ff7afd0bbc0183610703fbc752beeacecdaae..41fe6c5e3aa9c21da9a75f203a2d8ae90819d796 100644 (file)
@@ -36,4 +36,22 @@ class CRM_Contact_Export_Form_Select extends CRM_Export_Form_Select {
     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';
+  }
+
 }
index 7c46252f4a03e58865bfa5d30d3d25a89e705397..105ef33ce3ccd1939294d3fdb1a05e3ed3444c16 100644 (file)
@@ -121,7 +121,7 @@ class CRM_Contact_Form_Edit_Address {
         $name = 'name';
       }
 
-      $params = ['entity' => 'address'];
+      $params = ['entity' => 'Address'];
 
       if ($name === 'postal_code_suffix') {
         $params['label'] = ts('Suffix');
index de2084c0a2a2432c40ffa80156d560cb6192cb72..11bb704fca2518cfd87a927409ce572924f8cddd 100644 (file)
@@ -41,9 +41,10 @@ class CRM_Contact_Form_Edit_TagsAndGroups {
    *   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,
@@ -54,7 +55,8 @@ class CRM_Contact_Form_Edit_TagsAndGroups {
     $groupName = 'Group(s)',
     $tagName = 'Tag(s)',
     $fieldName = NULL,
-    $groupElementType = 'checkbox'
+    $groupElementType = 'checkbox',
+    $public = FALSE
   ) {
     if (!isset($form->_tagGroup)) {
       $form->_tagGroup = [];
@@ -88,12 +90,13 @@ class CRM_Contact_Form_Edit_TagsAndGroups {
       }
 
       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'
@@ -102,7 +105,7 @@ class CRM_Contact_Form_Edit_TagsAndGroups {
           }
 
           if ($groupElementType == 'select') {
-            $groupsOptions[$id] = $group;
+            $groupsOptions[$key] = $group;
           }
           else {
             $form->_tagGroup[$fName][$id]['description'] = $group['description'];
index 537e19e0bdef1db6b52fcb814470e9d06bbc5772..f7fa2e648560bd6c9d23574efb688ae80e6ab4c5 100644 (file)
@@ -49,14 +49,13 @@ class CRM_Contact_Form_Search_Basic extends CRM_Contact_Form_Search {
 
     // 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',
       ]);
     }
 
index c8ea5961aa3235e76509a6e105610cd995b3c61c..09faca084a55d2e3a89037302cb159991043f034 100644 (file)
@@ -297,11 +297,7 @@ WHERE      t.table_name = 'Activity' AND
     $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')];
index 857cd0d4fe0d3812b6544d21317f3c0334c0b849..41a9fdac32c012f1dc9ae8499e4485d52752adf8 100644 (file)
@@ -25,7 +25,7 @@ class CRM_Contact_Page_View_UserDashBoard_GroupContact extends CRM_Contact_Page_
       NULL,
       NULL, TRUE, TRUE,
       $this->_onlyPublicGroups,
-      NULL, NULL, TRUE
+      NULL, NULL, TRUE, TRUE
     );
 
     $in = CRM_Contact_BAO_GroupContact::getContactGroup(
@@ -33,7 +33,7 @@ class CRM_Contact_Page_View_UserDashBoard_GroupContact extends CRM_Contact_Page_
       'Added',
       NULL, FALSE, TRUE,
       $this->_onlyPublicGroups,
-      NULL, NULL, TRUE
+      NULL, NULL, TRUE, TRUE
     );
 
     $pending = CRM_Contact_BAO_GroupContact::getContactGroup(
@@ -41,7 +41,7 @@ class CRM_Contact_Page_View_UserDashBoard_GroupContact extends CRM_Contact_Page_
       'Pending',
       NULL, FALSE, TRUE,
       $this->_onlyPublicGroups,
-      NULL, NULL, TRUE
+      NULL, NULL, TRUE, TRUE
     );
 
     $out = CRM_Contact_BAO_GroupContact::getContactGroup(
@@ -49,7 +49,7 @@ class CRM_Contact_Page_View_UserDashBoard_GroupContact extends CRM_Contact_Page_
       'Removed',
       NULL, FALSE, TRUE,
       $this->_onlyPublicGroups,
-      NULL, NULL, TRUE
+      NULL, NULL, TRUE, TRUE
     );
 
     $this->assign('groupCount', $count);
index cbad362a83d61f5fe41e662fe147d180b2d0c79d..72cf3039dbf8a86f63eb9d01e1d9c4ebaad1ad06 100644 (file)
@@ -1016,6 +1016,8 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
    */
   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);
@@ -1044,7 +1046,7 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
     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
index 5423b6efd348d88817436d31df9d251c44647acc..3d095bbd94439fc5541ab2e886db27450c36791b 100644 (file)
@@ -44,9 +44,19 @@ class CRM_Contribute_BAO_Contribution extends CRM_Contribute_DAO_Contribution {
   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 = [];
 
@@ -2613,18 +2623,12 @@ LEFT JOIN  civicrm_contribution contribution ON ( componentPayment.contribution_
         $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'],
@@ -2644,10 +2648,20 @@ LEFT JOIN  civicrm_contribution contribution ON ( componentPayment.contribution_
       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
@@ -2665,6 +2679,8 @@ LEFT JOIN  civicrm_contribution contribution ON ( componentPayment.contribution_
       $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;
     }
   }
@@ -2955,8 +2971,10 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac
     //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);
@@ -4451,11 +4469,6 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac
       $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');
@@ -4849,101 +4862,6 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac
     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.
    *
index c44cd2f3bcb7a1bfc80e48dd247c6da7439dd8f1..7128e8f17d2f78ae68efa66ab8e5d52809c88276 100644 (file)
@@ -163,6 +163,7 @@ class CRM_Contribute_BAO_ContributionRecur extends CRM_Contribute_DAO_Contributi
    *   (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;
   }
@@ -556,6 +557,7 @@ INNER JOIN civicrm_contribution       con ON ( con.id = mp.contribution_id )
    * @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');
index f91512a0eaa3131945c6f6ec63456238e380f4fe..5255b6d36ebf783df925a43dda4c2fa5e6b36f82 100644 (file)
@@ -931,9 +931,8 @@ class CRM_Contribute_BAO_Query extends CRM_Core_BAO_Query {
     );
 
     // 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
index 0fb964ed776c4bc98f229ec77a90b732dec44ce3..88b41e4e6d8da8a2ae7e2a35f6ee327676bd80ed 100644 (file)
  */
 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) {
 
@@ -40,6 +44,7 @@ class CRM_Contribute_Controller_Search extends CRM_Core_Controller {
     $this->_stateMachine = new CRM_Contribute_StateMachine_Search($this, $action);
     $this->addPages($this->_stateMachine, $action);
     $this->addActions();
+    $this->set('entity', $this->entity);
   }
 
 }
index 174e9bf69591ed243117ec5f61e0137dac46330c..76c7adc1335d098547b23fe27d06efffd5449d36 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Contribute/Contribution.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:d937ea0497be1a1aeb1bac09986dd802)
+ * (GenCodeChecksum:600b0cd019cba16cd62aae7463beab77)
  */
 
 /**
@@ -37,6 +37,18 @@ class CRM_Contribute_DAO_Contribution extends CRM_Core_DAO {
    */
   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
    *
index 921c0595644d55ef4680d6a0be6e5f2682085943..9ad30badd631521ec3e5ad89264893618ce7b849 100644 (file)
@@ -36,4 +36,22 @@ class CRM_Contribute_Export_Form_Select extends CRM_Export_Form_Select {
     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';
+  }
+
 }
index d453df3a7c9e482fbf5ab3310130541979402172..c28a695d92ef3c6d9114f623c9a106029c70a1f1 100644 (file)
@@ -1602,13 +1602,12 @@ class CRM_Contribute_Form_Contribution extends CRM_Contribute_Form_AbstractEditP
 
       // 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.'));
index d9642ae9bcf249965ce51cd9a71478e6fa758c5c..1d17d82837d6dd909f29ee2f9809add7d0ff564e 100644 (file)
@@ -1527,13 +1527,9 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr
         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)) {
@@ -1557,7 +1553,7 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr
           date('YmdHis'), $membershipParams['cms_contactID'] ?? NULL,
           $customFieldsFormatted,
           $numTerms, $membershipID, $pending,
-          $contributionRecurID, $membershipSource, $isPayLater, $campaignId, [], $membershipContribution,
+          $contributionRecurID, $membershipSource, $isPayLater, $memParams, [], $membershipContribution,
           $membershipLineItems
         );
 
@@ -1590,8 +1586,8 @@ class CRM_Contribute_Form_Contribution_Confirm extends CRM_Contribute_Form_Contr
         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';
index e6cc4ea750453eb9c129213d27aee29c97ce33af..e8756cc48465d16cf9f25f25274336724ef028a4 100644 (file)
@@ -62,19 +62,15 @@ class CRM_Contribute_Form_Task extends CRM_Core_Form_Task {
     $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;
           }
@@ -136,25 +132,7 @@ class CRM_Contribute_Form_Task extends CRM_Core_Form_Task {
 
     $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');
   }
 
   /**
index a22383623e0a5d6611ff30a34345f79086d1d8a3..5cf089e51a51bf89efa9f90cc98d89ea39789732 100644 (file)
@@ -211,11 +211,12 @@ class CRM_Contribute_Form_Task_Batch extends CRM_Contribute_Form_Task {
         // @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');
index fac5a250bc7bf7907cd5913985a540ca8754268a..a3fb460d00396dd2bcb2a70c31091fcd9d282035 100644 (file)
@@ -42,12 +42,12 @@ class CRM_Contribute_Page_Tab extends CRM_Core_Page {
    * - 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'),
@@ -68,36 +68,28 @@ class CRM_Contribute_Page_Tab extends CRM_Core_Page {
       ],
     ];
 
-    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;
@@ -240,7 +232,7 @@ class CRM_Contribute_Page_Tab extends CRM_Core_Page {
       // 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]);
@@ -259,7 +251,7 @@ class CRM_Contribute_Page_Tab extends CRM_Core_Page {
         $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,
index dc8707155ce578ede8fdead8121352b88d30f4ca..cad2bc4aa4680b27f30aaeb5eb21eb970a12ada9 100644 (file)
@@ -98,7 +98,7 @@ class CRM_Contribute_Page_UserDashboard extends CRM_Contact_Page_View_UserDashBo
       $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;
@@ -107,7 +107,7 @@ class CRM_Contribute_Page_UserDashboard extends CRM_Contact_Page_View_UserDashBo
         $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'],
index 43b0588bac55a7ba1f3593eb72b31b641ab6d236..4a0ae0fba333a56b001bbafabf77631b65f20092 100644 (file)
@@ -114,7 +114,7 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
    * @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);
@@ -122,17 +122,9 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
       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;
     }
 
@@ -145,7 +137,7 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_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) {
@@ -893,7 +885,7 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_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],
             ],
@@ -1452,6 +1444,11 @@ SELECT id
       }
     }
     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) : '';
     }
 
index e876436f75ff03a34da1d192348282ea46563921..cb20ec185b66d3a6589810aa68fe690310a9e8ee 100644 (file)
@@ -137,7 +137,7 @@ class CRM_Core_BAO_CustomOption {
         $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 {
index e7ea7b196b93d97afa7386f3d5781450c092445d..26086ea2e8e4cac27f890c484a358c8e475da1c0 100644 (file)
@@ -23,6 +23,30 @@ class CRM_Core_BAO_MailSettings extends CRM_Core_DAO_MailSettings {
     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
index 7e5a7eede55faf7e314f0e2fef87acaf6d35a221..dd73b9f9b739d3990ad93805c69216e793985576 100644 (file)
@@ -92,7 +92,7 @@ class CRM_Core_BAO_Phone extends CRM_Core_DAO_Phone {
 
     $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";
       }
index 5a8d4fa93fa94ca0a023d9c4b505ee408de02476..cb56200c4b740e846df19fd8325b9e13347e7d91 100644 (file)
@@ -271,43 +271,6 @@ ALTER TABLE {$tableName}
     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.
    *
@@ -432,6 +395,8 @@ ADD UNIQUE INDEX `unique_entity_id` ( `entity_id` )";
           }
         }
 
+        $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.
@@ -439,11 +404,11 @@ ADD UNIQUE INDEX `unique_entity_id` ( `entity_id` )";
         // 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})";
         }
       }
     }
@@ -889,6 +854,9 @@ MODIFY      {$columnName} varchar( $length )
       // 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;
   }
 
index 412f753ce7cf424d20effc9e7cdbc0401b795169..988be26421492d7f13083d3b133839e9c2fce78c 100644 (file)
@@ -246,7 +246,7 @@ class CRM_Core_BAO_UFGroup extends CRM_Core_DAO_UFGroup {
    * 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
@@ -2026,7 +2026,7 @@ AND    ( entity_id IS NULL OR entity_id <= 0 )
       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') {
index 18e608aca10ff819f6f2c98c7f988d2fcf37476b..688470b9d9b70a2f2758acf75f6691b93aa5354e 100644 (file)
@@ -198,7 +198,7 @@ class CRM_Core_ClassLoader {
    * @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
index 6a96e0d7c2ab1b2596341dfaf101607ebaa71bd2..ed9e2f768ecd7142c271666d158bef5088b232f0 100644 (file)
@@ -215,6 +215,7 @@ class CRM_Core_CodeGen_Specification {
       '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]),
index 5febab786d75beff796a82725c290586d3913dd4..67073ab01d559c49b3c3a689ab0b3c2757ab6997 100644 (file)
@@ -78,7 +78,7 @@ class CRM_Core_CodeGen_Util_Template {
         '=> 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);
index f29b13cca9015e07875c35c2310f1d53b8c0e706..13c0c8c65a4654998c4b79ca4bca3e006d3d8709 100644 (file)
@@ -758,8 +758,9 @@ class CRM_Core_DAO extends DB_DataObject {
         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;
@@ -3151,4 +3152,13 @@ SELECT contact_id
     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 ?? [];
+  }
+
 }
index ec287600277dac29c2f4e07671972240b329c832..4011b3e0afa3f0b6088d8d90fb6bf3be86d70d33 100644 (file)
@@ -603,15 +603,12 @@ class CRM_Core_Error extends PEAR_ErrorStack {
    * @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);
     }
   }
 
@@ -820,11 +817,11 @@ class CRM_Core_Error extends PEAR_ErrorStack {
   /**
    * 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
@@ -859,11 +856,11 @@ class CRM_Core_Error extends PEAR_ErrorStack {
   /**
    * 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;
index 9795790fa31a7d892d710160b158170af69663ac..28417580e3507f686f23f685551c5c31e9c4a246 100644 (file)
@@ -1541,25 +1541,29 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    *   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]);
   }
 
   /**
@@ -1660,6 +1664,11 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
       }
     }
     $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.
@@ -1718,9 +1727,6 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
       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);
 
@@ -2428,6 +2434,7 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    * @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'], [
@@ -2441,11 +2448,11 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
       '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;
 
@@ -2455,7 +2462,7 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     // 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);
   }
 
   /**
index 2bc0033da6707fe65734b9516ccb6b6349148583..d23f253c73375f64c0cdda76b847cc28d16995ab 100644 (file)
@@ -73,6 +73,50 @@ abstract class CRM_Core_Form_Task extends CRM_Core_Form {
    */
   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.
    *
@@ -112,17 +156,17 @@ abstract class CRM_Core_Form_Task extends CRM_Core_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));
     }
 
@@ -133,24 +177,8 @@ abstract class CRM_Core_Form_Task extends CRM_Core_Form {
     // 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
-      ));
-    }
   }
 
   /**
@@ -159,7 +187,7 @@ abstract class CRM_Core_Form_Task extends CRM_Core_Form {
    */
   public function setContactIDs() {
     $this->_contactIds = CRM_Core_DAO::getContactIDsFromComponent($this->_entityIds,
-      $this::$tableName
+      $this->getTableName()
     );
   }
 
@@ -269,4 +297,42 @@ SELECT contact_id
     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';
+  }
+
 }
index 5d1c8793d7d5e4b158415f253c4e1d3c04cd206c..6c4576b3ba3ea294964b7b08a568262ebfeb3451 100644 (file)
@@ -47,6 +47,7 @@ class CRM_Core_I18n_PseudoConstant {
       $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;
   }
index 641d565d23d64bbf27f9d2f33413b3acbd881ad4..03fda988b5ff60b694dc55d571dbc63f302bd177 100644 (file)
@@ -92,7 +92,7 @@ class CRM_Core_I18n_SchemaStructure {
           '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.'",
         ],
@@ -414,7 +414,6 @@ class CRM_Core_I18n_SchemaStructure {
         'civicrm_group' => [
           'title' => [
             'type' => "Text",
-            'required' => "true",
           ],
           'frontend_title' => [
             'type' => "Text",
index 94bdfd88ec35af1d0633547d9b6bc16c31ea47be..d5bd619d7a9563b4965fcd4d60027184cfafad2c 100644 (file)
@@ -600,16 +600,7 @@ abstract class CRM_Core_Payment {
         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') {
@@ -1688,7 +1679,8 @@ abstract class CRM_Core_Payment {
    * @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
index e0f92f1ec1eefd75cef9d54cae7ad0d5d0e21c9d..607fcc1fec21d1c43cafe3559e5a9880a43469f7 100644 (file)
@@ -30,120 +30,93 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
   }
 
   /**
-   * @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) {
@@ -212,27 +185,18 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
       // 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;
   }
 
   /**
@@ -287,32 +251,12 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
    *
    * @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;
   }
 
   /**
@@ -359,4 +303,64 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr
     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',
+    ]);
+  }
+
 }
index 614568b51889bbecade7c5dd2a84e7677f68ac86..7bf95e4710fbc7e4e0e5af20f0905333747e8c9e 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\Contribution;
+
 /**
  * Class CRM_Core_Payment_BaseIPN.
  */
@@ -186,13 +188,6 @@ 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()
@@ -229,58 +224,64 @@ class CRM_Core_Payment_BaseIPN {
   /**
    * 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");
@@ -309,6 +310,8 @@ class CRM_Core_Payment_BaseIPN {
    * 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
@@ -328,9 +331,10 @@ class CRM_Core_Payment_BaseIPN {
    * @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');
index 4c254691cc83d8964223a95717ff3a561423a14c..7889ccc2ed211fda7fc312fe63ec4bda2cef676c 100644 (file)
@@ -270,7 +270,18 @@ class CRM_Core_Payment_Manual extends CRM_Core_Payment {
         }
         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;
+  }
+
 }
index 9e1a6050e36e2a5941abd6002ebce085f94d43bf..18586e471cdffa88724633f464ade4cfcb14e993 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\Contribution;
+
 /**
  *
  * @package CRM
@@ -206,8 +208,8 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN {
 
     $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);
   }
 
@@ -320,24 +322,61 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN {
           );
         }
       }
-      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);
@@ -348,7 +387,11 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN {
         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') {
@@ -357,8 +400,8 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN {
       }
       $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) {
index 428d77317ebd03b0f0b60c0f17623aec2cf93928..e2f8cb7596321bb05c778339884a509e88a805fe 100644 (file)
@@ -644,16 +644,23 @@ class CRM_Core_Payment_PayPalImpl extends CRM_Core_Payment {
   }
 
   /**
-   * @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);
   }
 
   /**
index 9901c3ea8f7bd679dd2064e6515ae8b2fe325950..d6f44109d6465a076c35b57ccf61c4745c31d49e 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\Contribution;
+
 /**
  *
  * @package CRM
@@ -278,8 +280,8 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN {
 
     $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);
   }
 
@@ -327,8 +329,12 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN {
       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') {
@@ -440,25 +446,21 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr
 
       $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) {
index 899440812e6bfa95417859e81d131578beceeb06..1de3147c8ab342386dc553a8e50a4ab1afa9da1b 100644 (file)
@@ -1491,6 +1491,18 @@ class CRM_Core_Permission {
       ],
     ];
 
+    // Dashboard permissions
+    $permissions['dashboard'] = [
+      'get' => [
+        'access CiviCRM',
+      ],
+    ];
+    $permissions['dashboard_contact'] = [
+      'default' => [
+        'access CiviCRM',
+      ],
+    ];
+
     // Profile permissions
     $permissions['profile'] = [
       // the profile will take care of this
index e3d1433b710530111921061d50d7e8fb5271626e..a81db1cfacb32e45f4eed2ee9c9ba7a265e3cac1 100644 (file)
@@ -50,6 +50,7 @@
  *
  *   - 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
index ec7ef1845356c3c4f95245ae17cfbfe9a65cf0f3..71fe17ece0ccfb8c93df4ab5950e843e27e743ac 100644 (file)
@@ -114,6 +114,9 @@ trait CRM_Core_Resources_CollectionTrait {
       }
       $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 */
@@ -121,6 +124,13 @@ trait CRM_Core_Resources_CollectionTrait {
       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;
@@ -149,8 +159,15 @@ trait CRM_Core_Resources_CollectionTrait {
    * @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;
   }
 
@@ -174,7 +191,12 @@ trait CRM_Core_Resources_CollectionTrait {
    * @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;
   }
 
   /**
@@ -290,6 +312,24 @@ trait CRM_Core_Resources_CollectionTrait {
     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
index ba68cc706a72f04505de7246441f9f99a7c5a3e2..89dfdc93a3bfc16ad080820db80ce9434cf9be87 100644 (file)
@@ -844,7 +844,7 @@ AND    option_group_id = %2";
       $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 {
index 05f6dcd81e72784dffd4ce024dbd224208efef91..60c941c3dbdb1ad38f63daeec2ff7c3cbb02ba54 100644 (file)
@@ -95,9 +95,9 @@ class CRM_Custom_Form_Option extends CRM_Core_Form {
 
       $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,
@@ -420,7 +420,8 @@ SELECT count(*)
       $customField->find(TRUE) &&
       (
         $customField->html_type == 'CheckBox' ||
-        $customField->html_type == 'Multi-Select'
+        // Multi Value Select
+        ($customField->html_type == 'Select' && $customField->serialize == 1)
       )
     ) {
       $defVal = explode(
@@ -483,6 +484,7 @@ SELECT count(*)
       }
     }
 
+    CRM_Core_BAO_CustomOption::updateValue($customOption->id, $customOption->value);
     $customOption->save();
 
     $msg = ts('Your multiple choice option \'%1\' has been saved', [1 => $customOption->label]);
index b583fdd09926938c387655857f44b8053d664546..26bb7f276595b880ba16fdb4fea060f71be3b8a8 100644 (file)
@@ -730,7 +730,7 @@ WHERE civicrm_address.geo_code_1 IS NOT NULL
    * @return array
    *   array of all the events that are searched
    */
-  public static function &getCompleteInfo(
+  public static function getCompleteInfo(
     $start = NULL,
     $type = NULL,
     $eventId = NULL,
@@ -839,15 +839,15 @@ WHERE civicrm_event.is_active = 1
 
     // 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;
 
@@ -1075,7 +1075,7 @@ WHERE civicrm_event.is_active = 1
             $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,
index 927c9422fe53ed306733c892647849f89131a7f1..1206ecee06e3945f4a0cf5321d4d105115089220 100644 (file)
@@ -22,6 +22,8 @@
  */
 class CRM_Event_Controller_Search extends CRM_Core_Controller {
 
+  protected $entity = 'Participant';
+
   /**
    * Class constructor.
    *
@@ -56,6 +58,7 @@ class CRM_Event_Controller_Search extends CRM_Core_Controller {
 
     // add all the actions
     $this->addActions($uploadDir, $uploadNames);
+    $this->set('entity', $this->entity);
   }
 
 }
index 553ddd7e1f39bc3fd4a9bcab1128a370b249b802..a1e23959c08c7089a3a2ea51a085bacdbfb5c676 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Event/Event.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:3514f838a27ddbf9bdf6e63ea20aabec)
+ * (GenCodeChecksum:a7abbcbe6a0e5e49e55d61afd013c791)
  */
 
 /**
@@ -37,6 +37,16 @@ class CRM_Event_DAO_Event extends CRM_Core_DAO {
    */
   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
    *
index f281decc2312f739d585b64e1b6e148a4ee04bf9..835200669ddf6f25a34bc1138ce7f9a2828e874e 100644 (file)
@@ -36,4 +36,22 @@ class CRM_Event_Export_Form_Select extends CRM_Export_Form_Select {
     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';
+  }
+
 }
index 4da684f51308070f10a006c0da301fd01bf75629..745fe3c06fa74cb3bf09a21b8f159f89bec6c86f 100644 (file)
@@ -86,24 +86,7 @@ class CRM_Event_Form_Task extends CRM_Core_Form_Task {
 
     $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');
   }
 
   /**
index 39097caa463dcb73d461a60027fa6cb21c6b52d1..e1c0aa73c2c089217de4a78efd2b0d020e3a11aa 100644 (file)
@@ -25,19 +25,29 @@ class CRM_Event_Page_UserDashboard extends CRM_Contact_Page_View_UserDashBoard {
    *
    */
   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);
   }
 
   /**
index 95a9e3a83e3d29b7c778ad3c873dff7adb9cf320..82ddb1b7b43c70ebb8585e3eccaa9593d564cd1b 100644 (file)
@@ -95,19 +95,8 @@ class CRM_Export_Form_Select extends CRM_Core_Form_Task {
       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;
@@ -159,7 +148,14 @@ FROM   {$this->_componentTable}
     $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');
   }
 
   /**
index cb7ac3e1c759c7db391637354e5acedb1576342e..fe2356e85f08e1fbb771d2a23bf6dee7b9a7722d 100644 (file)
@@ -41,4 +41,22 @@ class CRM_Export_Form_Select_Case extends CRM_Export_Form_Select {
     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';
+  }
+
 }
index dd4d85dbe627bb167bef09b5ce73974b63a2741b..841952fba751fa8ce72ae95541677adf053dc195 100644 (file)
@@ -267,6 +267,12 @@ class CRM_Financial_BAO_FinancialType extends CRM_Financial_DAO_FinancialType {
   /**
    * 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
index 2b93c545e1ff604f83ca344eebc6b108481eaaa9..44840e408c3e3d9ce8c92d736d4ae0a95f3d22ae 100644 (file)
@@ -98,44 +98,45 @@ class CRM_Financial_BAO_Payment {
 
     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);
+        }
       }
     }
 
index e94ee652f48390634f733429fb4058d768c87735..d0f2b857380057c950edae24d73a67305bc0408e 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Financial/FinancialTrxn.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:857c64b471d1872d98141aefa56aecb6)
+ * (GenCodeChecksum:834a9dbff8acecd70dbb68d640cb1d46)
  */
 
 /**
@@ -422,6 +422,14 @@ class CRM_Financial_DAO_FinancialTrxn extends CRM_Core_DAO {
           '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' => [
index 166cb93e9fef96619e9babdb80c07d0eeef204f6..e68d6e91f87fa0cd4e0d5026324646403de460d3 100644 (file)
@@ -28,6 +28,8 @@
  */
 class CRM_Grant_Controller_Search extends CRM_Core_Controller {
 
+  protected $entity = 'Grant';
+
   /**
    * Class constructor.
    *
@@ -45,8 +47,8 @@ class CRM_Grant_Controller_Search extends CRM_Core_Controller {
     $this->addPages($this->_stateMachine, $action);
 
     // add all the actions
-    $config = CRM_Core_Config::singleton();
     $this->addActions();
+    $this->set('entity', $this->entity);
   }
 
 }
index 05d6701011e6506707332bf75a0bb8fc7cfa60eb..4a3700260696886e3bdbe83380e281d7a95af7b8 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Grant/Grant.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:91aecd5b45ba8c5cd6636bb95ddbbfee)
+ * (GenCodeChecksum:febf55259ea92f4dd6a7d1ee4b5ea93a)
  */
 
 /**
@@ -37,6 +37,18 @@ class CRM_Grant_DAO_Grant extends CRM_Core_DAO {
    */
   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
    *
index a8c8d0df80e6bffb19eeae0d6d6ad5733e93bccd..6bade5bccdc681eae4ef3b391c354de3ad1d5cf0 100644 (file)
@@ -36,4 +36,22 @@ class CRM_Grant_Export_Form_Select extends CRM_Export_Form_Select {
     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';
+  }
+
 }
index 893077cfb76462b69fec4d1f6f3b37cc6767b345..a5096f91603ad4aa5586394e7c115ddc5a780d6f 100644 (file)
@@ -81,15 +81,7 @@ class CRM_Grant_Form_Task extends CRM_Core_Form_Task {
 
     $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');
   }
 
   /**
index 2fb7d2199d88eb6c672f455aa4cb49336b6b946d..801342f9e05cb811bf8f3b5f1e1e07ef3689a163 100644 (file)
@@ -67,6 +67,8 @@ class CRM_Group_Form_Edit extends CRM_Core_Form {
         'required' => TRUE,
       ],
       'description' => ['name' => 'description'],
+      'frontend_title' => ['name' => 'frontend_title'],
+      'frontend_description' => ['name' => 'frontend_description'],
     ];
   }
 
index 6aa4e7be150064dbaa8936ed0132418528958290..48687237cb9e955386dc7cd511cf86e33ee5b5e7 100644 (file)
@@ -58,7 +58,7 @@ class CRM_Import_DataSource_CSV extends CRM_Import_DataSource {
     }
     $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,
index 90e5cd71c73be5f701872de901335f5069472266..d06d6c56e998eb754e5ae68cc3a719ec2d714493 100644 (file)
@@ -50,7 +50,7 @@ abstract class CRM_Import_Form_DataSource extends CRM_Core_Form {
 
     $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,
index 863322bb2aae8f04b3499783396023da5b6fe5a0..b6040c7bbac6aa531bcb12ae1efd75adec8c0492 100644 (file)
@@ -403,11 +403,20 @@ ORDER BY log_date
    *
    * @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";
@@ -416,17 +425,48 @@ ORDER BY log_date
         $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.
    *
index fdbd92216a895882e7e64861f067e6119585fb40..ad16340ec68b273fee25f9e5fb0b502c19077ddc 100644 (file)
@@ -15,6 +15,8 @@
  * @copyright CiviCRM LLC https://civicrm.org/licensing
  */
 class CRM_Logging_ReportDetail extends CRM_Report_Form {
+
+  const ROW_COUNT_LIMIT = 50;
   protected $cid;
 
   /**
@@ -172,6 +174,7 @@ class CRM_Logging_ReportDetail extends CRM_Report_Form {
 
     // 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])) {
@@ -228,10 +231,31 @@ class CRM_Logging_ReportDetail extends CRM_Report_Form {
           $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;
   }
 
@@ -266,6 +290,9 @@ class CRM_Logging_ReportDetail extends CRM_Report_Form {
    * 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();
   }
 
@@ -280,10 +307,28 @@ class CRM_Logging_ReportDetail extends CRM_Report_Form {
     }
     $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());
     }
   }
 
@@ -347,6 +392,61 @@ class CRM_Logging_ReportDetail extends CRM_Report_Form {
     $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;
+    }
   }
 
 }
index e323c82114a1f79ba246efc7a485b722b386269c..39786f033237fc30f2021b868be16b98987ea75a 100644 (file)
@@ -288,7 +288,7 @@ AND    (TABLE_NAME LIKE 'log_civicrm_%' $nonStandardTableNameString )
       $config->logging = TRUE;
     }
     if ($config->logging) {
-      $this->fixSchemaDifferencesForALL();
+      $this->fixSchemaDifferencesForAll();
     }
     // invoke the meta trigger creation call
     CRM_Core_DAO::triggerRebuild(NULL, TRUE);
index 14820ed84cf19e35450e17770867f781be48d62c..53cf1802078d84d3bac14729e816bbbc4160b98c 100644 (file)
@@ -57,28 +57,23 @@ WHERE  mailing_id = %1
       $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);
index 95b4563035c617c21ad96b89bfe2d2db55e34c9d..392012f8453f59ddab5ef5e61b9a075aa589945b 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Mailing/Mailing.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:9cd784dc86cf4f54983f14440be05239)
+ * (GenCodeChecksum:f1ac9d0a02ed59171d4efdc4209a376c)
  */
 
 /**
@@ -37,6 +37,16 @@ class CRM_Mailing_DAO_Mailing extends CRM_Core_DAO {
    */
   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
    */
index ce92b388e2604c1d55c32b00ba693ea32682ce3e..a29fb7612aed7c12c021baa3a22010f4fe0507f7 100644 (file)
@@ -234,6 +234,8 @@ WHERE  email = %2
     $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
@@ -250,15 +252,15 @@ WHERE  email = %2
       $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();
index dacb27d55c6ec672ae0eb174bda46cd3389132f0..6044fd565888a6011a84405daa3808ab4bdcfc87 100644 (file)
@@ -116,6 +116,10 @@ class CRM_Mailing_Form_Component extends CRM_Core_Form {
     }
 
     $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');
index 97405dce40716e93f8d11136826a457e92aff25c..4afec4edc65fd9f83031b2f166b57f8149274dab 100644 (file)
@@ -34,7 +34,7 @@ class CRM_Mailing_Form_Subscribe extends CRM_Core_Form {
 
       // 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'
@@ -42,8 +42,8 @@ SELECT   title, description
 
       $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.");
@@ -77,7 +77,7 @@ SELECT   title, description
       $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 )
@@ -89,8 +89,8 @@ ORDER BY title";
       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'],
index cfba5ccde0d9ed2f16061276facf097b932b515b..fa647e4679cde83681de3ffca161bc3aff698774 100644 (file)
@@ -26,52 +26,20 @@ class CRM_Mailing_Info extends CRM_Core_Component_Info {
   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();
@@ -89,7 +57,13 @@ class CRM_Mailing_Info extends CRM_Core_Component_Info {
     ]);
     $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', [
@@ -115,46 +89,75 @@ class CRM_Mailing_Info extends CRM_Core_Component_Info {
     $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;
   }
index 90db173cdc97764a7a304c67af399ac10df7caca..afc7d312edca1dcb561cc174f1f30af3488ae787 100644 (file)
@@ -28,7 +28,7 @@ class CRM_Mailing_MailStore {
    *   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) {
@@ -40,34 +40,79 @@ class CRM_Mailing_MailStore {
     }
 
     $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 [];
     }
   }
 
index e5b4fb9e8fdcd232cdf509e1fb3f6dc6ea640a98..5af3033f931f8c1aefcf20800d3c85d408887339 100644 (file)
  */
 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
    */
index cef3c8d5e1233a2409c455c9eb16dc615dde47ca..7540a45dddb073c3b2a6a5c508e45d802a51ed09 100644 (file)
@@ -32,7 +32,7 @@ class CRM_Mailing_Page_Url extends CRM_Core_Page {
   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) {
index 78ac7ebe0654962a97646554a690d577b0e21a21..3c4446704fb317d7eda7d11b609c0dfaf7cc2777 100644 (file)
     <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>
index 41fa61e50f2569aff31bf3f7fcc3be234dff142b..8c4f7e9b2f0b290681d1cf846ce542e7d17e4f69 100644 (file)
@@ -1311,7 +1311,7 @@ WHERE  civicrm_membership.contact_id = civicrm_contact.id
    * @throws \CiviCRM_API3_Exception
    */
   public static function createRelatedMemberships($params, $dao) {
-
+    unset($params['membership_id']);
     $membership = new CRM_Member_DAO_Membership();
     $membership->id = $dao->id;
 
@@ -1448,11 +1448,10 @@ WHERE  civicrm_membership.contact_id = civicrm_contact.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 {
@@ -1513,7 +1512,10 @@ WHERE  civicrm_membership.contact_id = civicrm_contact.id
   }
 
   /**
-   * 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
@@ -1528,7 +1530,7 @@ WHERE  civicrm_membership.contact_id = civicrm_contact.id
    */
   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) {
@@ -1540,13 +1542,6 @@ WHERE  civicrm_membership.contact_id = civicrm_contact.id
     }
 
     CRM_Utils_Hook::membershipTypeValues($form, $membershipTypeValues);
-
-    if (is_numeric($membershipTypeID) &&
-      $membershipTypeID > 0
-    ) {
-      CRM_Core_Error::deprecatedFunctionWarning('Non arrays deprecated');
-      return $membershipTypeValues[$membershipTypeID];
-    }
     return $membershipTypeValues;
   }
 
@@ -1751,7 +1746,7 @@ INNER JOIN  civicrm_contact contact ON ( contact.id = membership.contact_id AND
    * @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
@@ -1760,7 +1755,7 @@ INNER JOIN  civicrm_contact contact ON ( contact.id = membership.contact_id AND
    * @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';
@@ -1786,7 +1781,7 @@ INNER JOIN  civicrm_contact contact ON ( contact.id = membership.contact_id AND
         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'],
@@ -1797,7 +1792,7 @@ INNER JOIN  civicrm_contact contact ON ( contact.id = membership.contact_id AND
           '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;
         }
@@ -1836,7 +1831,7 @@ INNER JOIN  civicrm_contact contact ON ( contact.id = membership.contact_id AND
         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.
@@ -1856,7 +1851,6 @@ INNER JOIN  civicrm_contact contact ON ( contact.id = membership.contact_id AND
         );
 
         // 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;
@@ -1887,10 +1881,10 @@ INNER JOIN  civicrm_contact contact ON ( contact.id = membership.contact_id AND
     }
     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);
@@ -1953,11 +1947,6 @@ INNER JOIN  civicrm_contact contact ON ( contact.id = membership.contact_id AND
     }
     $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.
index 581f11d686a177c33dbe59af36cdb75e73f82433..6ddf5172e66d88e7d2f06273e7c73796c21d86bd 100644 (file)
@@ -860,21 +860,4 @@ class CRM_Member_BAO_MembershipType extends CRM_Member_DAO_MembershipType {
     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;
-  }
-
 }
index 02ca8b7ff692bf13c036725a4e4fe59c29669567..b4cfb3301d43f9c7f84e6542212b86d6663a94c4 100644 (file)
@@ -28,6 +28,8 @@
  */
 class CRM_Member_Controller_Search extends CRM_Core_Controller {
 
+  protected $entity = 'Membership';
+
   /**
    * Class constructor.
    *
@@ -45,8 +47,8 @@ class CRM_Member_Controller_Search extends CRM_Core_Controller {
     $this->addPages($this->_stateMachine, $action);
 
     // add all the actions
-    $config = CRM_Core_Config::singleton();
     $this->addActions();
+    $this->set('entity', $this->entity);
   }
 
 }
index 86173b83afcc6b600e17bc5b54f71bb343291e0b..58610aef48a8e890d94bac567a24c1d7de7a27d4 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Member/Membership.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:d80be256fb175b763047883b8694559c)
+ * (GenCodeChecksum:2f2fd321dc15b2e2f453fc600d994c9a)
  */
 
 /**
@@ -37,6 +37,18 @@ class CRM_Member_DAO_Membership extends CRM_Core_DAO {
    */
   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
    *
index 61896fe7663e5d7cd25447ab716306fb645c2a39..764784f535aac9d5edcf70535003647e6e6e3be2 100644 (file)
@@ -36,4 +36,22 @@ class CRM_Member_Export_Form_Select extends CRM_Export_Form_Select {
     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';
+  }
+
 }
index b249c0818001b6e1b22156ec44410de50d6914d5..0086b47d7ef09688188f63149a376d90dccb3be0 100644 (file)
@@ -994,9 +994,9 @@ class CRM_Member_Form_Membership extends CRM_Member_Form {
     $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));
     }
@@ -1127,7 +1127,6 @@ class CRM_Member_Form_Membership extends CRM_Member_Form {
 
     // 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
 
@@ -1503,6 +1502,7 @@ class CRM_Member_Form_Membership extends CRM_Member_Form {
         $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 = [];
@@ -1569,15 +1569,6 @@ class CRM_Member_Form_Membership extends CRM_Member_Form {
             $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;
@@ -1600,7 +1591,7 @@ class CRM_Member_Form_Membership extends CRM_Member_Form {
       $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));
     }
 
@@ -1610,8 +1601,8 @@ class CRM_Member_Form_Membership extends CRM_Member_Form {
       $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';
@@ -1818,35 +1809,34 @@ class CRM_Member_Form_Membership extends CRM_Member_Form {
    * 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);
index 33cdd982559274485e5bf948891234a81e524b65..87be03190375731a94b518845bf0c13753225a1d 100644 (file)
@@ -682,8 +682,8 @@ class CRM_Member_Form_MembershipRenewal extends CRM_Member_Form {
       $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,
index 6718d6eaebf1562e55e69fca363a4ea8b58cf965..b2a6225c752d9be09ec57bfb248612ba709cc08b 100644 (file)
@@ -88,25 +88,7 @@ class CRM_Member_Form_Task extends CRM_Core_Form_Task {
     }
 
     $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');
   }
 
   /**
index 5acd0d716691a8fbc0aedf0fe2872df95fe62e0a..237e4c3d3a859780739d5d06d1e0471b3cbe74a1 100644 (file)
@@ -308,9 +308,6 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
               $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') {
@@ -327,7 +324,8 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
 
       $formatValues = [];
       foreach ($params as $key => $field) {
-        if ($field == NULL || $field === '') {
+        // ignore empty values or empty arrays etc
+        if (CRM_Utils_System::isNull($field)) {
           continue;
         }
 
@@ -371,20 +369,9 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
               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.');
@@ -621,10 +608,6 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
     $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)) {
index aac582c3892ec1aadfe8868bcdfa4ba77c641c2b..3a497b748ab9d3505576a27c690de7c37125084c 100644 (file)
@@ -79,7 +79,7 @@ class CRM_Member_Page_RecurringContributions extends CRM_Core_Page {
     $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) {
@@ -110,7 +110,7 @@ class CRM_Member_Page_RecurringContributions extends CRM_Core_Page {
    * @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
index bea78c69e22e13f797e69dfb521352436daa32de..4c244a29dd9eacdc11cd6781fa7de7d84e49ae5b 100644 (file)
@@ -109,7 +109,7 @@ class CRM_PCP_Form_Campaign extends CRM_Core_Form {
     }
 
     $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);
index 527375d07d0ba5da25f6985662969509460ad5eb..4b532f2053f88dc9cde85ab7ee2d3892aa34936c 100644 (file)
@@ -27,6 +27,8 @@
  */
 class CRM_Pledge_Controller_Search extends CRM_Core_Controller {
 
+  protected $entity = 'Pledge';
+
   /**
    * Class constructor.
    *
@@ -44,8 +46,8 @@ class CRM_Pledge_Controller_Search extends CRM_Core_Controller {
     $this->addPages($this->_stateMachine, $action);
 
     // add all the actions
-    $config = CRM_Core_Config::singleton();
     $this->addActions();
+    $this->set('entity', $this->entity);
   }
 
 }
index 55e77b1ed435f64d9921a44bb70ee1c5a9db7baf..d7495ed2103cc74c2273aeecf25cb32232b6b013 100644 (file)
@@ -36,4 +36,22 @@ class CRM_Pledge_Export_Form_Select extends CRM_Export_Form_Select {
     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';
+  }
+
 }
index 12df902a9169ea1fbb03de84d507f3577b20a359..9de09f66667dc2b63fd691764d2231582aad0236 100644 (file)
@@ -38,7 +38,7 @@ class CRM_Pledge_Form_Task extends CRM_Core_Form_Task {
   /**
    * Common pre-processing.
    *
-   * @param CRM_Core_Form $form
+   * @param CRM_Pledge_Form_Task $form
    */
   public static function preProcessCommon(&$form) {
     $form->_pledgeIds = [];
@@ -79,16 +79,7 @@ class CRM_Pledge_Form_Task extends CRM_Core_Form_Task {
     }
 
     $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');
   }
 
   /**
index e12716861383e02f28b418e3eeda14fb9d29c76d..4795d8dbd2ea0ff3d2d1a4e539a1a62fc3c2b283 100644 (file)
@@ -673,7 +673,7 @@ WHERE  id = %1";
         continue;
       }
 
-      list($params, $lineItem) = self::getLine($params, $lineItem, $priceSetID, $field, $id);
+      [$params, $lineItem] = self::getLine($params, $lineItem, $priceSetID, $field, $id);
     }
 
     $amount_level = [];
@@ -880,59 +880,10 @@ WHERE  id = %1";
       $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);
   }
 
   /**
@@ -941,9 +892,12 @@ WHERE  id = %1";
    * @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) {
@@ -1771,4 +1725,57 @@ WHERE     ct.id = cp.financial_type_id AND
     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
+          );
+        }
+      }
+    }
+  }
+
 }
index 6d430a5a2cdf9e6e2d7f35e27499b74f8b1753fb..d85edac156e175b1f4d3ecb49ec39ec930453fd7 100644 (file)
@@ -94,6 +94,19 @@ class CRM_Report_Form_Activity extends CRM_Report_Form {
             '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',
@@ -852,6 +865,7 @@ GROUP BY civicrm_activity_id $having {$this->_orderBy}";
     $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??
@@ -1030,6 +1044,13 @@ GROUP BY civicrm_activity_id $having {$this->_orderBy}";
         }
       }
 
+      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) {
index 06f89ab8c959ab10f3b892059bb996e391281fd2..c0bc36eacb0a6ff69dd1739fc000f1cd9b348f6f 100644 (file)
@@ -245,7 +245,7 @@ class CRM_Report_Form_Contribute_Bookkeeping extends CRM_Report_Form {
             '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' => [
index 14b5facbd9cbc964a44b6de8ae196972034162ce..4576423e11211e3af0d2a70665637e3c2ca32d3b 100644 (file)
@@ -205,7 +205,7 @@ class CRM_Report_Form_Contribute_Detail extends CRM_Report_Form {
             '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' => [
index 101717609d06fdfb51fea784f4f5df79d3162ad4..97c0d8f9d32120a153a236997b7f484622e4c060 100644 (file)
@@ -205,7 +205,7 @@ class CRM_Report_Form_Contribute_Lybunt extends CRM_Report_Form {
             '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'),
index 6f0109d2845e0b1d43b4f5470b5b1ed69b4a87ce..c61fb4b76d7d8aa43b94c146e29ca8abc9a88cdc 100644 (file)
@@ -185,7 +185,7 @@ class CRM_Report_Form_Contribute_Recur extends CRM_Report_Form {
           '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' => [
index d5044a08e7224be21d81fbe12d8a178cc712fb59..18bfd109bd0eb29a7c85581fb2fbbaf5f7ba5365 100644 (file)
@@ -220,7 +220,7 @@ class CRM_Report_Form_Contribute_Repeat extends CRM_Report_Form {
             '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'),
index 8370e7420319e0a7a4e0e014024f62a8447a0803..4d4277f6b6f7e29b16db0e22d55a1bbd902932d7 100644 (file)
@@ -206,7 +206,7 @@ class CRM_Report_Form_Contribute_SoftCredit extends CRM_Report_Form {
             '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',
index b7c40f5a199f7c4d43e885766dc2b516660016bd..c73da90e77d44734ea3c68d345b9dd1b0045b326 100644 (file)
@@ -171,7 +171,7 @@ class CRM_Report_Form_Contribute_Summary extends CRM_Report_Form {
           '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' => [
index 1b45af360eec4a46d86f50d1f4f72f64716f032a..dc6b31898af1f79233c50746c613254238880968 100644 (file)
@@ -209,7 +209,7 @@ class CRM_Report_Form_Contribute_Sybunt extends CRM_Report_Form {
             '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'),
index 83d903950e66f7f694567f1cfa0a26a9c5c9dd18..7b6c14f51fb520903d3681c7357f874fabd775a2 100644 (file)
@@ -135,7 +135,7 @@ class CRM_Report_Form_Contribute_TopDonor extends CRM_Report_Form {
             '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'),
index 9755626958142e612333687f3f1075af7d85289e..5dad9206a10edee7733683730140304603cc3a2f 100644 (file)
@@ -97,7 +97,7 @@ class CRM_SMS_Form_Group extends CRM_Contact_Form_Task {
 
     $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
     );
 
index a7c5ee6e4e7f4ca7af818a7a5222d082975adaa8..1a2e3b008c67ef454b8af4801daa5f03cd6e623a 100644 (file)
@@ -244,12 +244,13 @@ class CRM_Upgrade_Incremental_Base {
    *
    * @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;
   }
index 105161699a2f06f75f171e0c5ac3c88414318c89..42b905011c0dc34ab97c21dfdce1ae4d22da57d4 100644 (file)
@@ -71,6 +71,11 @@ class CRM_Upgrade_Incremental_php_FiveThirtyOne extends CRM_Upgrade_Incremental_
       '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) {
@@ -164,4 +169,28 @@ class CRM_Upgrade_Incremental_php_FiveThirtyOne extends CRM_Upgrade_Incremental_
     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;
+  }
+
 }
diff --git a/CRM/Upgrade/Incremental/php/FiveThirtyThree.php b/CRM/Upgrade/Incremental/php/FiveThirtyThree.php
new file mode 100644 (file)
index 0000000..f7bc1ab
--- /dev/null
@@ -0,0 +1,72 @@
+<?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;
+  // }
+
+}
index a68bb9c20ee4f953306af0b6a37cfe7bbdd90330..14e1412333166149c7f3968db134a2309f6926d2 100644 (file)
@@ -10,7 +10,8 @@
  */
 
 /**
- * Upgrade logic for FiveThirtyTwo */
+ * Upgrade logic for FiveThirtyTwo
+ */
 class CRM_Upgrade_Incremental_php_FiveThirtyTwo extends CRM_Upgrade_Incremental_Base {
 
   /**
@@ -46,27 +47,49 @@ class CRM_Upgrade_Incremental_php_FiveThirtyTwo extends CRM_Upgrade_Incremental_
     // }
   }
 
-  /*
-   * 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');
+  }
 
 }
diff --git a/CRM/Upgrade/Incremental/sql/5.31.beta2.mysql.tpl b/CRM/Upgrade/Incremental/sql/5.31.beta2.mysql.tpl
new file mode 100644 (file)
index 0000000..33384db
--- /dev/null
@@ -0,0 +1 @@
+{* file to handle db changes in 5.31.beta2 during upgrade *}
index 29d6826d8c3363ad696d22a2c2e3ec49a485b513..974d76e4cb4243a87e93ff7515233d2639e2b55f 100644 (file)
@@ -1 +1,20 @@
 {* 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';
diff --git a/CRM/Upgrade/Incremental/sql/5.32.beta1.mysql.tpl b/CRM/Upgrade/Incremental/sql/5.32.beta1.mysql.tpl
new file mode 100644 (file)
index 0000000..e2257e2
--- /dev/null
@@ -0,0 +1 @@
+{* file to handle db changes in 5.32.beta1 during upgrade *}
diff --git a/CRM/Upgrade/Incremental/sql/5.33.alpha1.mysql.tpl b/CRM/Upgrade/Incremental/sql/5.33.alpha1.mysql.tpl
new file mode 100644 (file)
index 0000000..483b660
--- /dev/null
@@ -0,0 +1 @@
+{* file to handle db changes in 5.33.alpha1 during upgrade *}
index 50634d22bc8d3de08eb44da2143df2551c2bc82e..ef0feed7242c2d0ffe0b1fa73a60b5ca04d05b96 100644 (file)
@@ -111,6 +111,8 @@ class CRM_Utils_API_HTMLInputCoder extends CRM_Utils_API_AbstractFieldCoder {
         '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()) {
index 5d941207e4acd50421e157fab187385fd643370d..253c743a8ba1f01c087719102ea4fcbef77909d3 100644 (file)
@@ -23,20 +23,23 @@ class CRM_Utils_Constant {
   /**
    * 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;
   }
 
 }
index 11008a84439bc91cb17dc033b0beeab5c8ec834d..af0fd42584f995dae429f69b398f3e2afff2d3f9 100644 (file)
@@ -422,6 +422,19 @@ class CRM_Utils_Date {
     }
   }
 
+  /**
+   * 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
    *
index a987971774b41dc02e2bf2a00254168dff9d95a5..bc905d21baa3f7d259071d8e8bcf72140d037dba 100644 (file)
@@ -1042,6 +1042,24 @@ abstract class CRM_Utils_Hook {
     );
   }
 
+  /**
+   * 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.
    *
@@ -1201,6 +1219,33 @@ abstract class CRM_Utils_Hook {
     );
   }
 
+  /**
+   * 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.
    *
@@ -2381,6 +2426,7 @@ abstract class CRM_Utils_Hook {
    *   - 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,
index c99a792167c766901f6d58c1cefdf53dfdf40f90..274b96626a034fdddbdafd43733ea8d84a71384f 100644 (file)
@@ -67,7 +67,7 @@ class CRM_Utils_Money {
     }
 
     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) {
index 14c4272f9ddc43b9a00ae8751e351585a7675132..2764e242641c8d061804f234c04f72b7016465c3 100644 (file)
@@ -96,6 +96,7 @@ class CRM_Utils_ReCAPTCHA {
       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',
@@ -117,4 +118,18 @@ class CRM_Utils_ReCAPTCHA {
     }
   }
 
+  /**
+   * @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;
+  }
+
 }
index 8ffc390096fd850f21de324560b2655eba96797b..7c0897436508acb9f83c494f343981706c3b235c 100644 (file)
@@ -944,7 +944,7 @@ class CRM_Utils_String {
     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';
index bb8fbf488fdceeb7b4cc4f5e60c8bebfb405a28c..2ca112f31a01914c6fe4a2308b4ae0386987c4a0 100644 (file)
@@ -1876,6 +1876,7 @@ class CRM_Utils_System {
    *   - query: array
    *   - title: string
    *   - url: string
+   * @deprecated
    */
   public static function createDefaultCrudLink($crudLinkSpec) {
     $crudLinkSpec['action'] = CRM_Utils_Array::value('action', $crudLinkSpec, CRM_Core_Action::VIEW);
index 93342e256f36d9035463f88d0d289a46108360ed..9e7f5640132f5ed86fa8e93ab0350968d2665440 100644 (file)
@@ -1033,4 +1033,20 @@ AND    u.status = 1
     $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();
+    }
+  }
+
 }
index 1dca6bcd9e1d9c61bb30e4620ee8381675620d1e..11ef57276c5956ea8622b148c2885752dd95df83 100644 (file)
@@ -115,8 +115,23 @@ class AngularLoader {
     }
 
     $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,
@@ -125,7 +140,6 @@ class AngularLoader {
           '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);
index a763cf9e716dfa048731474dcbea2b1dd8bba8b0..ab9600175a885e120fe515f22d32e0439102b057 100644 (file)
@@ -29,6 +29,12 @@ class Manager {
    *     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;
 
@@ -120,9 +126,19 @@ class Manager {
         $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);
@@ -397,7 +413,9 @@ class Manager {
               break;
 
             case 'settings':
+            case 'settingsFactory':
             case 'requires':
+            case 'permissions':
               if (!empty($module[$resType])) {
                 $result[$moduleName] = $module[$resType];
               }
index 2d14f325be91559b8b8504a986cad93c7e9454ff..3b8484e16f1a7b0978577ddd4ce718a9092d27f9 100644 (file)
@@ -96,8 +96,8 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
       $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',
diff --git a/Civi/Api4/Action/MailSettings/TestConnection.php b/Civi/Api4/Action/MailSettings/TestConnection.php
new file mode 100644 (file)
index 0000000..5107623
--- /dev/null
@@ -0,0 +1,83 @@
+<?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,
+      ];
+    }
+  }
+
+}
index 49f39859ecc6b090f4326dc14a5f784aa9b5e9bd..883e80dc5c5c47f5f5a2f5ebcff7538b355a5b9f 100644 (file)
@@ -50,7 +50,11 @@ class Entity extends Generic\AbstractEntity {
         ],
         [
           'name' => 'title',
-          'description' => 'Localized title',
+          'description' => 'Localized title (singular)',
+        ],
+        [
+          'name' => 'title_plural',
+          'description' => 'Localized title (plural)',
         ],
         [
           'name' => 'type',
@@ -73,6 +77,11 @@ class Entity extends Generic\AbstractEntity {
           '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',
index 8084614b14ef73f6b1f5d6af306f5b0f891cce48..443a7f186ca01825e1725c3107fd793cce298720 100644 (file)
@@ -89,6 +89,15 @@ abstract class AbstractEntity {
     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.
    *
@@ -123,8 +132,9 @@ abstract class AbstractEntity {
     $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']]);
index c27aac4ce0c33562f687784058465ecb264712e2..fa035259386816c624e111d4fb52999dabc23e1d 100644 (file)
@@ -108,6 +108,7 @@ abstract class DAOEntity extends AbstractEntity {
     $info = parent::getInfo();
     $dao = \CRM_Core_DAO_AllCoreTables::getFullName($info['name']);
     if ($dao) {
+      $info['paths'] = $dao::getEntityPaths();
       $info['icon'] = $dao::$_icon;
       $info['dao'] = $dao;
     }
index afc30c217ade1d5b7dde54284881ac457609ae4b..77720c9685737983e66abb378c5497d828701716 100644 (file)
@@ -67,6 +67,32 @@ class Result extends \ArrayObject implements \JsonSerializable {
     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
index f94667ec99c74dd3919661d8b878107817f9f2df..012a7869f222856f04555db2cb08a3ad70b5bbad 100644 (file)
@@ -26,4 +26,15 @@ namespace Civi\Api4;
  */
 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);
+  }
+
 }
index 82e1f770059e2d468cf367d56b1ce53afcc2712f..6b4ff620a0c3305126a07fcf390a90de5961f2be 100644 (file)
@@ -124,7 +124,7 @@ class FormattingUtil {
     }
 
     $hic = \CRM_Utils_API_HTMLInputCoder::singleton();
-    if (!$hic->isSkippedField($fieldSpec['name'])) {
+    if (!$hic->isSkippedField($fieldSpec['name']) && is_string($value)) {
       $value = $hic->encodeValue($value);
     }
   }
index 7d30855076b79c0380a1d6746f7deece5ce0e2b0..813b2f6f19391bc9296f0c79276bccba02877f24 100644 (file)
@@ -133,7 +133,7 @@ class Requirements {
       $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;
@@ -146,7 +146,7 @@ class Requirements {
         $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;
       }
     }
index fbbfc7d146bcb754a322ab428c3276a0b721d657..358ce7f023eba1360af77e94e9b197924c013074 100644 (file)
@@ -104,16 +104,19 @@ class System {
    *
    * @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);
   }
 
   /**
index 66d4022af2de93dea206f403a21c37f610a2e935..f87748fcb4dbae903edb213e3c658f51ae2eb3d7 100644 (file)
@@ -18,8 +18,8 @@
         </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]]">
index a9741dadc45ab8be22048e0a017510508dd5643f..4b8c0257d92155506fd2ca26723363dc15f6670a 100644 (file)
       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';
index a3e140c6058a753d02f98223ee8cd777e8315235..a91d495221145168616754f1f209b7d2b1e6ea67 100644 (file)
@@ -14,5 +14,16 @@ return [
   ],
   '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',
+  ],
 ];
index df6ef1283f4c0c4149987fcc73cec06f868743b3..77c1e986c826a8555bfe5e71a10d2b9f2b8fb1e7 100644 (file)
  *   // 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 {
 
@@ -109,6 +120,8 @@ 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'])) {
@@ -180,6 +193,10 @@ class civicrm_api3 {
       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)) {
index ecfe665fddaeed5624b8a80065ced93ead91b4ca..346a1ec2be98be0ed25717e7e9a7b196036c68fc 100644 (file)
@@ -179,7 +179,7 @@ function civicrm_api3_custom_field_get($params) {
         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']);
         }
       }
index 5e9c07b81bac23ab7d71272d703db6a4b96a1aa6..e99b38542c40afd1e00409f01c51e8a74bf80050 100644 (file)
@@ -88,10 +88,25 @@ function civicrm_api3_order_create($params) {
         $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'];
@@ -99,10 +114,8 @@ function civicrm_api3_order_create($params) {
             $items['entity_id'] = $entityResult['id'];
           }
         }
-        else {
-          // pledge payment
-        }
       }
+
       if (empty($priceSetID)) {
         $item = reset($lineItems['line_item']);
         $priceSetID = (int) civicrm_api3('PriceField', 'getvalue', [
@@ -183,7 +196,6 @@ function civicrm_api3_order_cancel($params) {
   $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');
 }
 
index 42cb64cdc35d9d99e74cd1259fdb96b67197aff5..84f49fd0344fbd3f5d54c827f3383022e9f140d4 100755 (executable)
@@ -47,7 +47,7 @@ php GenerateData.php
 
 ## 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
index 37e4daed1884157807aa5866749bb52ebaa35181..8f4edd6c055693abe14b9750f73a326105fef0ed 100644 (file)
     "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",
index d299663350c6f9ac406913d776d36db7b8ff42ba..8a7b77d823597dfe3b658ceacbf0b97396d17885 100644 (file)
@@ -4,7 +4,7 @@
         "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"
         },
         {
index adf56d17aafc433a438173877be344ed37fc8161..980c98f3ee87c999b321b113517c11fc38e0fd0a 100644 (file)
   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
index e882e0a16d11f2d9a9013308c745d361fa88f1b0..c54cec4215646639bd9cff2bb0612d5e228161b2 100644 (file)
@@ -126,6 +126,7 @@ function dm_core_exts() {
   echo ext/financialacls
   echo ext/afform
   echo ext/greenwich
+  echo ext/contributioncancelactions
 }
 
 ## Copy all packages
index 4d65f38090659d2461aef0a3b3b6591c72c74453..4cdf39a0d33aa5fd0f226a908db744c93376745a 100644 (file)
@@ -7,7 +7,7 @@
       $routeProvider.when('/', {
         controller: 'AfformStandalonePageCtrl',
         template: function() {
-          return '<div ' + CRM.afform.open + '="{}"></div>';
+          return '<div id="bootstrap-theme" ' + CRM.afform.open + '="{}"></div>';
         }
       });
     })
index f89bbe1283f9f0a74daa19f90afb39bc7b84eaa1..5bee0da02a8a168d5255a517889db32fc5baf51a 100644 (file)
@@ -2,7 +2,7 @@
 <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>
diff --git a/ext/contributioncancelactions/LICENSE.txt b/ext/contributioncancelactions/LICENSE.txt
new file mode 100644 (file)
index 0000000..2ca31bf
--- /dev/null
@@ -0,0 +1,667 @@
+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/>.
diff --git a/ext/contributioncancelactions/README.md b/ext/contributioncancelactions/README.md
new file mode 100644 (file)
index 0000000..e2e17c3
--- /dev/null
@@ -0,0 +1,24 @@
+# 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.
+
diff --git a/ext/contributioncancelactions/contributioncancelactions.civix.php b/ext/contributioncancelactions/contributioncancelactions.civix.php
new file mode 100644 (file)
index 0000000..1475430
--- /dev/null
@@ -0,0 +1,477 @@
+<?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, []);
+}
diff --git a/ext/contributioncancelactions/contributioncancelactions.php b/ext/contributioncancelactions/contributioncancelactions.php
new file mode 100644 (file)
index 0000000..61966d7
--- /dev/null
@@ -0,0 +1,102 @@
+<?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]);
+  }
+}
diff --git a/ext/contributioncancelactions/info.xml b/ext/contributioncancelactions/info.xml
new file mode 100644 (file)
index 0000000..37cf838
--- /dev/null
@@ -0,0 +1,33 @@
+<?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>
diff --git a/ext/contributioncancelactions/phpunit.xml.dist b/ext/contributioncancelactions/phpunit.xml.dist
new file mode 100644 (file)
index 0000000..fc8f870
--- /dev/null
@@ -0,0 +1,18 @@
+<?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>
diff --git a/ext/contributioncancelactions/tests/phpunit/CancelTest.php b/ext/contributioncancelactions/tests/phpunit/CancelTest.php
new file mode 100644 (file)
index 0000000..3e850a5
--- /dev/null
@@ -0,0 +1,320 @@
+<?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'];
+  }
+
+}
diff --git a/ext/contributioncancelactions/tests/phpunit/bootstrap.php b/ext/contributioncancelactions/tests/phpunit/bootstrap.php
new file mode 100644 (file)
index 0000000..a5b4925
--- /dev/null
@@ -0,0 +1,63 @@
+<?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)");
+  }
+}
index c0edae5e58c12b4ff95002b722496cf4742fff0b..c039ef1998f005ea1017a53063f79f69c24af753 100644 (file)
@@ -190,6 +190,86 @@ function financialacls_civicrm_selectWhereClause($entity, &$clauses) {
 
 }
 
+/**
+ * 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. ---
 
 /**
diff --git a/ext/financialacls/tests/phpunit/Civi/Financialacls/BaseTestClass.php b/ext/financialacls/tests/phpunit/Civi/Financialacls/BaseTestClass.php
new file mode 100644 (file)
index 0000000..6eb159c
--- /dev/null
@@ -0,0 +1,59 @@
+<?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();
+  }
+
+}
diff --git a/ext/financialacls/tests/phpunit/Civi/Financialacls/BuildAmountHookTest.php b/ext/financialacls/tests/phpunit/Civi/Financialacls/BuildAmountHookTest.php
new file mode 100644 (file)
index 0000000..1ef9435
--- /dev/null
@@ -0,0 +1,44 @@
+<?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']);
+  }
+
+}
similarity index 62%
rename from ext/financialacls/tests/phpunit/LineItemTest.php
rename to ext/financialacls/tests/phpunit/Civi/Financialacls/LineItemTest.php
index 0c4c4b9c720fa8bd45b215f999d683207b582da6..33db7de3424cf4b5e70fa0f504d3310991052b9c 100644 (file)
@@ -1,11 +1,12 @@
 <?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.
  *
@@ -20,25 +21,11 @@ use Civi\Api4\PriceField;
  *
  * @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) {
@@ -51,13 +38,13 @@ class LineItemTest extends \PHPUnit\Framework\TestCase implements HeadlessInterf
         [
           '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,
@@ -68,18 +55,7 @@ class LineItemTest extends \PHPUnit\Framework\TestCase implements HeadlessInterf
     ]);
     $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);
@@ -105,19 +81,6 @@ class LineItemTest extends \PHPUnit\Framework\TestCase implements HeadlessInterf
     $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
diff --git a/ext/financialacls/tests/phpunit/Civi/Financialacls/MembershipTypesTest.php b/ext/financialacls/tests/phpunit/Civi/Financialacls/MembershipTypesTest.php
new file mode 100644 (file)
index 0000000..8e63eee
--- /dev/null
@@ -0,0 +1,28 @@
+<?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));
+  }
+
+}
diff --git a/ext/financialacls/tests/phpunit/Civi/Financialacls/OptionsTest.php b/ext/financialacls/tests/phpunit/Civi/Financialacls/OptionsTest.php
new file mode 100644 (file)
index 0000000..1f9ab60
--- /dev/null
@@ -0,0 +1,24 @@
+<?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));
+  }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/Angular.php b/ext/oauth-client/CRM/OAuth/Angular.php
new file mode 100644 (file)
index 0000000..4f0bc33
--- /dev/null
@@ -0,0 +1,14 @@
+<?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;
+  }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/BAO/OAuthClient.php b/ext/oauth-client/CRM/OAuth/BAO/OAuthClient.php
new file mode 100644 (file)
index 0000000..e7a0f53
--- /dev/null
@@ -0,0 +1,73 @@
+<?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);
+  }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/BAO/OAuthSysToken.php b/ext/oauth-client/CRM/OAuth/BAO/OAuthSysToken.php
new file mode 100644 (file)
index 0000000..21201f3
--- /dev/null
@@ -0,0 +1,59 @@
+<?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;
+    }
+  }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/DAO/OAuthClient.php b/ext/oauth-client/CRM/OAuth/DAO/OAuthClient.php
new file mode 100644 (file)
index 0000000..0307da9
--- /dev/null
@@ -0,0 +1,318 @@
+<?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;
+  }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/DAO/OAuthSysToken.php b/ext/oauth-client/CRM/OAuth/DAO/OAuthSysToken.php
new file mode 100644 (file)
index 0000000..3fbbcc9
--- /dev/null
@@ -0,0 +1,471 @@
+<?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;
+  }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/MailSetup.php b/ext/oauth-client/CRM/OAuth/MailSetup.php
new file mode 100644 (file)
index 0000000..2ed9c01
--- /dev/null
@@ -0,0 +1,169 @@
+<?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'];
+  }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/Page/Return.php b/ext/oauth-client/CRM/OAuth/Page/Return.php
new file mode 100644 (file)
index 0000000..b3df413
--- /dev/null
@@ -0,0 +1,123 @@
+<?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;
+  }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/Upgrader.php b/ext/oauth-client/CRM/OAuth/Upgrader.php
new file mode 100644 (file)
index 0000000..acc937d
--- /dev/null
@@ -0,0 +1,153 @@
+<?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;
+  // }
+
+}
diff --git a/ext/oauth-client/CRM/OAuth/Upgrader/Base.php b/ext/oauth-client/CRM/OAuth/Upgrader/Base.php
new file mode 100644 (file)
index 0000000..824fc1c
--- /dev/null
@@ -0,0 +1,396 @@
+<?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:
+    }
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/Action/OAuthClient/AbstractGrantAction.php b/ext/oauth-client/Civi/Api4/Action/OAuthClient/AbstractGrantAction.php
new file mode 100644 (file)
index 0000000..31352dc
--- /dev/null
@@ -0,0 +1,110 @@
+<?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;
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/Action/OAuthClient/AuthorizationCode.php b/ext/oauth-client/Civi/Api4/Action/OAuthClient/AuthorizationCode.php
new file mode 100644 (file)
index 0000000..f9c896a
--- /dev/null
@@ -0,0 +1,125 @@
+<?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);
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/Action/OAuthClient/ClientCredential.php b/ext/oauth-client/Civi/Api4/Action/OAuthClient/ClientCredential.php
new file mode 100644 (file)
index 0000000..1369e68
--- /dev/null
@@ -0,0 +1,41 @@
+<?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);
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/Action/OAuthClient/Create.php b/ext/oauth-client/Civi/Api4/Action/OAuthClient/Create.php
new file mode 100644 (file)
index 0000000..89175a7
--- /dev/null
@@ -0,0 +1,20 @@
+<?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();
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/Action/OAuthClient/Update.php b/ext/oauth-client/Civi/Api4/Action/OAuthClient/Update.php
new file mode 100644 (file)
index 0000000..8131f6f
--- /dev/null
@@ -0,0 +1,23 @@
+<?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;
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/Action/OAuthClient/UserPassword.php b/ext/oauth-client/Civi/Api4/Action/OAuthClient/UserPassword.php
new file mode 100644 (file)
index 0000000..d961a2e
--- /dev/null
@@ -0,0 +1,62 @@
+<?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);
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/Action/OAuthSysToken/Refresh.php b/ext/oauth-client/Civi/Api4/Action/OAuthSysToken/Refresh.php
new file mode 100644 (file)
index 0000000..043a83b
--- /dev/null
@@ -0,0 +1,87 @@
+<?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;
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/OAuthClient.php b/ext/oauth-client/Civi/Api4/OAuthClient.php
new file mode 100644 (file)
index 0000000..decfad9
--- /dev/null
@@ -0,0 +1,66 @@
+<?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'],
+    ];
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/OAuthProvider.php b/ext/oauth-client/Civi/Api4/OAuthProvider.php
new file mode 100644 (file)
index 0000000..fc556d5
--- /dev/null
@@ -0,0 +1,78 @@
+<?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"],
+    ];
+  }
+
+}
diff --git a/ext/oauth-client/Civi/Api4/OAuthSysToken.php b/ext/oauth-client/Civi/Api4/OAuthSysToken.php
new file mode 100644 (file)
index 0000000..8cba88e
--- /dev/null
@@ -0,0 +1,39 @@
+<?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'.
+    ];
+  }
+
+}
diff --git a/ext/oauth-client/Civi/OAuth/CiviGenericProvider.php b/ext/oauth-client/Civi/OAuth/CiviGenericProvider.php
new file mode 100644 (file)
index 0000000..8bff0ff
--- /dev/null
@@ -0,0 +1,70 @@
+<?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),
+    ];
+  }
+
+}
diff --git a/ext/oauth-client/Civi/OAuth/OAuthException.php b/ext/oauth-client/Civi/OAuth/OAuthException.php
new file mode 100644 (file)
index 0000000..70108a9
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+namespace Civi\OAuth;
+
+class OAuthException extends \CRM_Core_Exception {
+
+}
diff --git a/ext/oauth-client/Civi/OAuth/OAuthLeagueFacade.php b/ext/oauth-client/Civi/OAuth/OAuthLeagueFacade.php
new file mode 100644 (file)
index 0000000..49f50b9
--- /dev/null
@@ -0,0 +1,114 @@
+<?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();
+  }
+
+}
diff --git a/ext/oauth-client/Civi/OAuth/OAuthTokenFacade.php b/ext/oauth-client/Civi/OAuth/OAuthTokenFacade.php
new file mode 100644 (file)
index 0000000..dcad477
--- /dev/null
@@ -0,0 +1,138 @@
+<?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();
+  }
+
+}
diff --git a/ext/oauth-client/LICENSE.txt b/ext/oauth-client/LICENSE.txt
new file mode 100644 (file)
index 0000000..762bf87
--- /dev/null
@@ -0,0 +1,667 @@
+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/>.
diff --git a/ext/oauth-client/README.md b/ext/oauth-client/README.md
new file mode 100644 (file)
index 0000000..de75935
--- /dev/null
@@ -0,0 +1,44 @@
+# 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 *)
diff --git a/ext/oauth-client/ang/oauthClientAdmin.aff.html b/ext/oauth-client/ang/oauthClientAdmin.aff.html
new file mode 100644 (file)
index 0000000..234fa9b
--- /dev/null
@@ -0,0 +1,11 @@
+<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
diff --git a/ext/oauth-client/ang/oauthClientAdmin.aff.json b/ext/oauth-client/ang/oauthClientAdmin.aff.json
new file mode 100644 (file)
index 0000000..dbc4750
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "title": "OAuth2 Client Administration",
+  "server_route": "civicrm/admin/oauth",
+  "permission": "manage OAuth client"
+}
diff --git a/ext/oauth-client/ang/oauthClientCreateHelp.aff.html b/ext/oauth-client/ang/oauthClientCreateHelp.aff.html
new file mode 100644 (file)
index 0000000..4af0fa8
--- /dev/null
@@ -0,0 +1,32 @@
+<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
diff --git a/ext/oauth-client/ang/oauthClientCreator.aff.html b/ext/oauth-client/ang/oauthClientCreator.aff.html
new file mode 100644 (file)
index 0000000..15e7443
--- /dev/null
@@ -0,0 +1,14 @@
+<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>
diff --git a/ext/oauth-client/ang/oauthClientEditor.aff.html b/ext/oauth-client/ang/oauthClientEditor.aff.html
new file mode 100644 (file)
index 0000000..7588460
--- /dev/null
@@ -0,0 +1,32 @@
+<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>
diff --git a/ext/oauth-client/ang/oauthClientList.aff.html b/ext/oauth-client/ang/oauthClientList.aff.html
new file mode 100644 (file)
index 0000000..923a439
--- /dev/null
@@ -0,0 +1,46 @@
+<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>
diff --git a/ext/oauth-client/ang/oauthClientTokens.aff.html b/ext/oauth-client/ang/oauthClientTokens.aff.html
new file mode 100644 (file)
index 0000000..7dc9f03
--- /dev/null
@@ -0,0 +1,39 @@
+<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>
diff --git a/ext/oauth-client/ang/oauthJwtDebug.aff.html b/ext/oauth-client/ang/oauthJwtDebug.aff.html
new file mode 100644 (file)
index 0000000..5096a24
--- /dev/null
@@ -0,0 +1,32 @@
+<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
diff --git a/ext/oauth-client/ang/oauthJwtDebug.aff.json b/ext/oauth-client/ang/oauthJwtDebug.aff.json
new file mode 100644 (file)
index 0000000..a0ab1a2
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "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
diff --git a/ext/oauth-client/ang/oauthProviderDetail.aff.html b/ext/oauth-client/ang/oauthProviderDetail.aff.html
new file mode 100644 (file)
index 0000000..1b19df6
--- /dev/null
@@ -0,0 +1,55 @@
+<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>
diff --git a/ext/oauth-client/ang/oauthProviderList.aff.html b/ext/oauth-client/ang/oauthProviderList.aff.html
new file mode 100644 (file)
index 0000000..3b797a6
--- /dev/null
@@ -0,0 +1,30 @@
+<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>
diff --git a/ext/oauth-client/ang/oauthUtil.ang.php b/ext/oauth-client/ang/oauthUtil.ang.php
new file mode 100644 (file)
index 0000000..759f585
--- /dev/null
@@ -0,0 +1,20 @@
+<?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',
+  ],
+];
diff --git a/ext/oauth-client/ang/oauthUtil.js b/ext/oauth-client/ang/oauthUtil.js
new file mode 100644 (file)
index 0000000..f436510
--- /dev/null
@@ -0,0 +1,48 @@
+(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._);
diff --git a/ext/oauth-client/ang/unvalidatedJwtDecode.ang.php b/ext/oauth-client/ang/unvalidatedJwtDecode.ang.php
new file mode 100644 (file)
index 0000000..3db419d
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+return [
+  'js' => [
+    'ang/unvalidatedJwtDecode.js',
+  ],
+  'requires' => [
+    'crmUi',
+    'crmUtil',
+  ],
+];
diff --git a/ext/oauth-client/ang/unvalidatedJwtDecode.js b/ext/oauth-client/ang/unvalidatedJwtDecode.js
new file mode 100644 (file)
index 0000000..dfb7d53
--- /dev/null
@@ -0,0 +1,32 @@
+(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._);
diff --git a/ext/oauth-client/bin/local-redir.php b/ext/oauth-client/bin/local-redir.php
new file mode 100644 (file)
index 0000000..ab1bca9
--- /dev/null
@@ -0,0 +1,28 @@
+<?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);
diff --git a/ext/oauth-client/bin/local-redir.sh b/ext/oauth-client/bin/local-redir.sh
new file mode 100755 (executable)
index 0000000..9ed55fb
--- /dev/null
@@ -0,0 +1,62 @@
+#!/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");'
diff --git a/ext/oauth-client/images/screenshot.png b/ext/oauth-client/images/screenshot.png
new file mode 100644 (file)
index 0000000..6765b69
Binary files /dev/null and b/ext/oauth-client/images/screenshot.png differ
diff --git a/ext/oauth-client/info.xml b/ext/oauth-client/info.xml
new file mode 100644 (file)
index 0000000..f0b5fa5
--- /dev/null
@@ -0,0 +1,36 @@
+<?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>
diff --git a/ext/oauth-client/oauth_client.civix.php b/ext/oauth-client/oauth_client.civix.php
new file mode 100644 (file)
index 0000000..64892d3
--- /dev/null
@@ -0,0 +1,488 @@
+<?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',
+    ],
+  ]);
+}
diff --git a/ext/oauth-client/oauth_client.php b/ext/oauth-client/oauth_client.php
new file mode 100644 (file)
index 0000000..7623094
--- /dev/null
@@ -0,0 +1,250 @@
+<?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);
+}
diff --git a/ext/oauth-client/phpunit.xml.dist b/ext/oauth-client/phpunit.xml.dist
new file mode 100644 (file)
index 0000000..fc8f870
--- /dev/null
@@ -0,0 +1,18 @@
+<?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>
diff --git a/ext/oauth-client/providers/gmail.dist.json b/ext/oauth-client/providers/gmail.dist.json
new file mode 100644 (file)
index 0000000..c8b9426
--- /dev/null
@@ -0,0 +1,26 @@
+{
+  "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
diff --git a/ext/oauth-client/providers/ms-exchange.dist.json b/ext/oauth-client/providers/ms-exchange.dist.json
new file mode 100644 (file)
index 0000000..b496c5c
--- /dev/null
@@ -0,0 +1,28 @@
+{
+  "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
+  }
+}
diff --git a/ext/oauth-client/providers/test_example_1.test.json b/ext/oauth-client/providers/test_example_1.test.json
new file mode 100644 (file)
index 0000000..99a59c6
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "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"]
+  }
+}
diff --git a/ext/oauth-client/providers/test_example_2.test.json b/ext/oauth-client/providers/test_example_2.test.json
new file mode 100644 (file)
index 0000000..e5e5748
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "name": "test_example_2",
+  "title": "Second Test Example",
+  "class": "My\\Example2",
+  "options": {
+    "urlAuthorize": "https://example.com/two",
+    "scopes": ["scope-2-foo", "scope-2-bar"]
+  }
+}
diff --git a/ext/oauth-client/settings/OAuthClient.setting.php b/ext/oauth-client/settings/OAuthClient.setting.php
new file mode 100644 (file)
index 0000000..9bcffb2
--- /dev/null
@@ -0,0 +1,17 @@
+<?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".'),
+  ],
+];
diff --git a/ext/oauth-client/sql/auto_install.sql b/ext/oauth-client/sql/auto_install.sql
new file mode 100644 (file)
index 0000000..fd7f643
--- /dev/null
@@ -0,0 +1,105 @@
+-- +--------------------------------------------------------------------+
+-- | 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
diff --git a/ext/oauth-client/sql/auto_uninstall.sql b/ext/oauth-client/sql/auto_uninstall.sql
new file mode 100644 (file)
index 0000000..db6fecf
--- /dev/null
@@ -0,0 +1,23 @@
+-- +--------------------------------------------------------------------+
+-- | 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
diff --git a/ext/oauth-client/templates/CRM/OAuth/Page/Return.tpl b/ext/oauth-client/templates/CRM/OAuth/Page/Return.tpl
new file mode 100644 (file)
index 0000000..0939508
--- /dev/null
@@ -0,0 +1,41 @@
+{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}
diff --git a/ext/oauth-client/tests/phpunit/CRM/OAuth/MailSetupTest.php b/ext/oauth-client/tests/phpunit/CRM/OAuth/MailSetupTest.php
new file mode 100644 (file)
index 0000000..06ee4b8
--- /dev/null
@@ -0,0 +1,85 @@
+<?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);
+  }
+
+}
diff --git a/ext/oauth-client/tests/phpunit/api/v4/OAuthClientGrantTest.php b/ext/oauth-client/tests/phpunit/api/v4/OAuthClientGrantTest.php
new file mode 100644 (file)
index 0000000..9cc4a1f
--- /dev/null
@@ -0,0 +1,71 @@
+<?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;
+  }
+
+}
diff --git a/ext/oauth-client/tests/phpunit/api/v4/OAuthClientTest.php b/ext/oauth-client/tests/phpunit/api/v4/OAuthClientTest.php
new file mode 100644 (file)
index 0000000..d4107ea
--- /dev/null
@@ -0,0 +1,117 @@
+<?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();
+  }
+
+}
diff --git a/ext/oauth-client/tests/phpunit/api/v4/OAuthProviderTest.php b/ext/oauth-client/tests/phpunit/api/v4/OAuthProviderTest.php
new file mode 100644 (file)
index 0000000..a7f54e8
--- /dev/null
@@ -0,0 +1,46 @@
+<?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']);
+  }
+
+}
diff --git a/ext/oauth-client/tests/phpunit/api/v4/OAuthSysTokenTest.php b/ext/oauth-client/tests/phpunit/api/v4/OAuthSysTokenTest.php
new file mode 100644 (file)
index 0000000..43b2079
--- /dev/null
@@ -0,0 +1,215 @@
+<?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));
+  }
+
+}
diff --git a/ext/oauth-client/tests/phpunit/bootstrap.php b/ext/oauth-client/tests/phpunit/bootstrap.php
new file mode 100644 (file)
index 0000000..41c0800
--- /dev/null
@@ -0,0 +1,64 @@
+<?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)");
+  }
+}
diff --git a/ext/oauth-client/xml/Menu/oauth_client.xml b/ext/oauth-client/xml/Menu/oauth_client.xml
new file mode 100644 (file)
index 0000000..1039478
--- /dev/null
@@ -0,0 +1,9 @@
+<?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>
diff --git a/ext/oauth-client/xml/schema/CRM/OAuth/OAuthClient.entityType.php b/ext/oauth-client/xml/schema/CRM/OAuth/OAuthClient.entityType.php
new file mode 100644 (file)
index 0000000..21e774a
--- /dev/null
@@ -0,0 +1,10 @@
+<?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',
+  ],
+];
diff --git a/ext/oauth-client/xml/schema/CRM/OAuth/OAuthClient.xml b/ext/oauth-client/xml/schema/CRM/OAuth/OAuthClient.xml
new file mode 100644 (file)
index 0000000..9b3649a
--- /dev/null
@@ -0,0 +1,100 @@
+<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>
diff --git a/ext/oauth-client/xml/schema/CRM/OAuth/OAuthSysToken.entityType.php b/ext/oauth-client/xml/schema/CRM/OAuth/OAuthSysToken.entityType.php
new file mode 100644 (file)
index 0000000..5c53dd6
--- /dev/null
@@ -0,0 +1,10 @@
+<?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',
+  ],
+];
diff --git a/ext/oauth-client/xml/schema/CRM/OAuth/OAuthSysToken.xml b/ext/oauth-client/xml/schema/CRM/OAuth/OAuthSysToken.xml
new file mode 100644 (file)
index 0000000..45fc80c
--- /dev/null
@@ -0,0 +1,166 @@
+<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>
diff --git a/ext/search/CRM/Search/BAO/SearchDisplay.php b/ext/search/CRM/Search/BAO/SearchDisplay.php
new file mode 100644 (file)
index 0000000..0d6f665
--- /dev/null
@@ -0,0 +1,17 @@
+<?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 {
+
+}
diff --git a/ext/search/CRM/Search/DAO/SearchDisplay.php b/ext/search/CRM/Search/DAO/SearchDisplay.php
new file mode 100644 (file)
index 0000000..271b36f
--- /dev/null
@@ -0,0 +1,300 @@
+<?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;
+  }
+
+}
diff --git a/ext/search/CRM/Search/Page/Admin.php b/ext/search/CRM/Search/Page/Admin.php
new file mode 100644 (file)
index 0000000..ccd4cf6
--- /dev/null
@@ -0,0 +1,51 @@
+<?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();
+  }
+
+}
diff --git a/ext/search/CRM/Search/Page/Ang.php b/ext/search/CRM/Search/Page/Ang.php
deleted file mode 100644 (file)
index 22cab1d..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-<?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;
-  }
-
-}
diff --git a/ext/search/CRM/Search/Page/Search.php b/ext/search/CRM/Search/Page/Search.php
new file mode 100644 (file)
index 0000000..053a8ee
--- /dev/null
@@ -0,0 +1,30 @@
+<?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();
+  }
+
+}
index 667460ecba06408145f47d036d1bf696c8112e26..9d314e49104420394738d90e38a50617aafd6027 100644 (file)
@@ -14,7 +14,7 @@ class CRM_Search_Upgrader extends CRM_Search_Upgrader_Base {
       ->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)
@@ -31,4 +31,14 @@ class CRM_Search_Upgrader extends CRM_Search_Upgrader_Base {
       ->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;
+  }
+
 }
diff --git a/ext/search/Civi/Api4/SearchDisplay.php b/ext/search/Civi/Api4/SearchDisplay.php
new file mode 100644 (file)
index 0000000..33f4497
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+namespace Civi\Api4;
+
+/**
+ * SearchDisplay entity.
+ *
+ * Provided by the Search Kit extension.
+ *
+ * @package Civi\Api4
+ */
+class SearchDisplay extends Generic\DAOEntity {
+
+}
diff --git a/ext/search/Civi/Api4/Service/Spec/Provider/SearchDisplayCreationSpecProvider.php b/ext/search/Civi/Api4/Service/Spec/Provider/SearchDisplayCreationSpecProvider.php
new file mode 100644 (file)
index 0000000..bc83237
--- /dev/null
@@ -0,0 +1,34 @@
+<?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';
+  }
+
+}
diff --git a/ext/search/Civi/Search/Actions.php b/ext/search/Civi/Search/Actions.php
new file mode 100644 (file)
index 0000000..5918d75
--- /dev/null
@@ -0,0 +1,90 @@
+<?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;
+  }
+
+}
diff --git a/ext/search/Civi/Search/Admin.php b/ext/search/Civi/Search/Admin.php
new file mode 100644 (file)
index 0000000..bf392c5
--- /dev/null
@@ -0,0 +1,133 @@
+<?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);
+  }
+
+}
diff --git a/ext/search/Civi/Search/Display.php b/ext/search/Civi/Search/Display.php
new file mode 100644 (file)
index 0000000..aa23ac4
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 [];
+    }
+  }
+
+}
diff --git a/ext/search/ang/crmSearchActions.ang.php b/ext/search/ang/crmSearchActions.ang.php
new file mode 100644 (file)
index 0000000..478cd09
--- /dev/null
@@ -0,0 +1,16 @@
+<?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'],
+];
diff --git a/ext/search/ang/crmSearchActions.module.js b/ext/search/ang/crmSearchActions.module.js
new file mode 100644 (file)
index 0000000..912d2e5
--- /dev/null
@@ -0,0 +1,7 @@
+(function(angular, $, _) {
+  "use strict";
+
+  // Declare module
+  angular.module('crmSearchActions', CRM.angRequires('crmSearchActions'));
+
+})(angular, CRM.$, CRM._);
similarity index 67%
rename from ext/search/ang/search/SaveSmartGroup.ctrl.js
rename to ext/search/ang/crmSearchActions/SaveSmartGroup.ctrl.js
index 47837b2c8c10bfee694d3ebc62537bb9a8564a3d..7b89a9dc2852f62db175b422a09066336caabca2 100644 (file)
@@ -1,11 +1,9 @@
 (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'));
@@ -53,7 +35,7 @@
     $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() {
similarity index 68%
rename from ext/search/ang/search/crmSearchActions/crmSearchActionDelete.ctrl.js
rename to ext/search/ang/crmSearchActions/crmSearchActionDelete.ctrl.js
index 28a401e65258d488bf47dabeadfac36db9d509ab..d7c3f4828213c190a8c6ef399b393f25688f2087 100644 (file)
@@ -1,12 +1,12 @@
 (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');
similarity index 62%
rename from ext/search/ang/search/crmSearchActions/crmSearchActionDelete.html
rename to ext/search/ang/crmSearchActions/crmSearchActionDelete.html
index a8c95de8a207b9ed3d441c532e552589f52d7f69..855f3570d4f8ea4bfc98fec3d0a1cae3082be18c 100644 (file)
@@ -1,10 +1,10 @@
 <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>
similarity index 69%
rename from ext/search/ang/search/crmSearchActions/crmSearchActionUpdate.ctrl.js
rename to ext/search/ang/crmSearchActions/crmSearchActionUpdate.ctrl.js
index a4fb759887a08965a7144565e86f9247cc62e0e8..01bd9e67ee98f1fd978fb9cee94af8ec36751320 100644 (file)
@@ -1,18 +1,20 @@
 (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;
similarity index 66%
rename from ext/search/ang/search/crmSearchActions/crmSearchActionUpdate.html
rename to ext/search/ang/crmSearchActions/crmSearchActionUpdate.html
index fac9c10af20efc54027d5e1c7ccc7339480e31ff..bb1a38033799304d9cbb63a393e467c7df23e6d4 100644 (file)
@@ -1,16 +1,17 @@
 <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>
similarity index 52%
rename from ext/search/ang/search/crmSearchActions.component.js
rename to ext/search/ang/crmSearchActions/crmSearchActions.component.js
index 212b8d914f85b8acf26638da53fe8501cc756f49..1c300db7b5a335f3ce9712035d0d5944a7d49d47 100644 (file)
@@ -1,29 +1,45 @@
 (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);
@@ -35,7 +51,8 @@
         }
         var data = {
           ids: ctrl.ids,
-          entity: ctrl.entity
+          entity: ctrl.entity,
+          entityInfo: ctrl.entityInfo
         };
         // If action uses a crmPopup form
         if (action.crmPopup) {
similarity index 67%
rename from ext/search/ang/search/crmSearchActions.html
rename to ext/search/ang/crmSearchActions/crmSearchActions.html
index 7442efbe09e46dfb30ee55e35d9e61790f0b23ea..41696415e0d6438eace4c4bb5f2caa297bc45d4a 100644 (file)
@@ -1,5 +1,5 @@
 <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">
diff --git a/ext/search/ang/crmSearchActions/saveSmartGroup.directive.js b/ext/search/ang/crmSearchActions/saveSmartGroup.directive.js
new file mode 100644 (file)
index 0000000..bd68699
--- /dev/null
@@ -0,0 +1,40 @@
+(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._);
similarity index 86%
rename from ext/search/ang/search/saveSmartGroup.html
rename to ext/search/ang/crmSearchActions/saveSmartGroup.html
index 3589ff2234015433e14eeb6f704f63a070467c4e..43686da3384ad6faf6e55204d8521e517789ee4f 100644 (file)
@@ -13,7 +13,7 @@
     <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">&nbsp;
+      <div class="checkbox" ng-repeat="option in groupOptions.group_type track by option.id">&nbsp;
         <label>
           <input type="checkbox" checklist-model="model.group_type" checklist-value="option.id">
           {{ option.label }}
@@ -22,7 +22,7 @@
     </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">
diff --git a/ext/search/ang/crmSearchAdmin.ang.php b/ext/search/ang/crmSearchAdmin.ang.php
new file mode 100644 (file)
index 0000000..d582bd0
--- /dev/null
@@ -0,0 +1,18 @@
+<?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'],
+];
diff --git a/ext/search/ang/crmSearchAdmin.module.js b/ext/search/ang/crmSearchAdmin.module.js
new file mode 100644 (file)
index 0000000..a497f7b
--- /dev/null
@@ -0,0 +1,162 @@
+(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._);
similarity index 51%
rename from ext/search/ang/search/crmSearch/controls.html
rename to ext/search/ang/crmSearchAdmin/compose/controls.html
index ce1f843e0286aabcb0862131c6f206b7f60e1051..77a8b7774029a06ce72171ec258d0c134e6ee12f 100644 (file)
       {{:: 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>
similarity index 54%
rename from ext/search/ang/search/crmSearch/criteria.html
rename to ext/search/ang/crmSearchAdmin/compose/criteria.html
index a52979c8be66513dbef08c853e9e31c6df876d80..6b5eb4b3af88db2ae3450de3fed941767c8343af 100644 (file)
@@ -1,11 +1,7 @@
 <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>
similarity index 53%
rename from ext/search/ang/search/crmSearch/results.html
rename to ext/search/ang/crmSearchAdmin/compose/results.html
index 01b200207ccbcbad97ec25bbd6e4291d8dc56005..3cda603bc89f4cae520144a663412d3f7bcfa187 100644 (file)
@@ -1,13 +1,16 @@
 <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')">
@@ -19,9 +22,7 @@
       <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>
similarity index 52%
rename from ext/search/ang/search/crmSearch.component.js
rename to ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js
index 49c99081722035bf0d45069800d3fc71fa00ad74..2e7d617cf08b055fe0ac1e626d2fc30b1328f25c 100644 (file)
@@ -1,12 +1,11 @@
 (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;
-            }
-          });
-      };
     }
   });
 
diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdmin.html b/ext/search/ang/crmSearchAdmin/crmSearchAdmin.html
new file mode 100644 (file)
index 0000000..d1c0267
--- /dev/null
@@ -0,0 +1,53 @@
+<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>
diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js b/ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js
new file mode 100644 (file)
index 0000000..4bdf10f
--- /dev/null
@@ -0,0 +1,54 @@
+(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._);
diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.html b/ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.html
new file mode 100644 (file)
index 0000000..e97e753
--- /dev/null
@@ -0,0 +1,7 @@
+<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>
diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.directive.js b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.directive.js
new file mode 100644 (file)
index 0000000..6676055
--- /dev/null
@@ -0,0 +1,46 @@
+(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._);
diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html
new file mode 100644 (file)
index 0000000..47648e7
--- /dev/null
@@ -0,0 +1,10 @@
+<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)" />
similarity index 75%
rename from ext/search/ang/search/crmSearchClause.component.js
rename to ext/search/ang/crmSearchAdmin/crmSearchClause.component.js
index 3fb1a01d6f4f2f57708068b16d6c213a13533e75..3c196a440b10307565874a4d2bf0cb01a1027ca7 100644 (file)
@@ -1,7 +1,7 @@
 (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, []]);
       };
similarity index 95%
rename from ext/search/ang/search/crmSearchClause.html
rename to ext/search/ang/crmSearchAdmin/crmSearchClause.html
index a50df23725056022d9b7194aef96857b1b3615f3..bcc23f7038f0c36eaab9e044cc4caa458e76792a 100644 (file)
@@ -17,7 +17,7 @@
       <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>
similarity index 77%
rename from ext/search/ang/search/crmSearchFunction.component.js
rename to ext/search/ang/crmSearchAdmin/crmSearchFunction.component.js
index 1b2c45832d85c5b2ee7f56b0ff1510f46feb0281..ff009085f8e7df86cd92001ad5c8220a18955982 100644 (file)
@@ -1,18 +1,18 @@
 (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;
@@ -22,7 +22,7 @@
       };
 
       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;
         }
diff --git a/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js b/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js
new file mode 100644 (file)
index 0000000..78fb645
--- /dev/null
@@ -0,0 +1,83 @@
+(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._);
diff --git a/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html b/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html
new file mode 100644 (file)
index 0000000..66a15a4
--- /dev/null
@@ -0,0 +1,42 @@
+<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>
diff --git a/ext/search/ang/crmSearchAdmin/group.html b/ext/search/ang/crmSearchAdmin/group.html
new file mode 100644 (file)
index 0000000..10a1f4d
--- /dev/null
@@ -0,0 +1,27 @@
+<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">&nbsp;
+      <label>
+        <input type="checkbox" checklist-model="$ctrl.savedSearch.groups[0].group_type" checklist-value="option.id">
+        {{ option.label }}
+      </label>&nbsp;
+    </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>
diff --git a/ext/search/ang/crmSearchAdmin/searchList.controller.js b/ext/search/ang/crmSearchAdmin/searchList.controller.js
new file mode 100644 (file)
index 0000000..aebfc76
--- /dev/null
@@ -0,0 +1,26 @@
+(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._);
diff --git a/ext/search/ang/crmSearchAdmin/searchList.html b/ext/search/ang/crmSearchAdmin/searchList.html
new file mode 100644 (file)
index 0000000..3622112
--- /dev/null
@@ -0,0 +1,55 @@
+<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>
diff --git a/ext/search/ang/crmSearchAdmin/tabs.html b/ext/search/ang/crmSearchAdmin/tabs.html
new file mode 100644 (file)
index 0000000..7fb2382
--- /dev/null
@@ -0,0 +1,44 @@
+<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>
diff --git a/ext/search/ang/crmSearchDisplay.ang.php b/ext/search/ang/crmSearchDisplay.ang.php
new file mode 100644 (file)
index 0000000..84d0b76
--- /dev/null
@@ -0,0 +1,17 @@
+<?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',
+  ],
+];
diff --git a/ext/search/ang/crmSearchDisplay.module.js b/ext/search/ang/crmSearchDisplay.module.js
new file mode 100644 (file)
index 0000000..beae3e5
--- /dev/null
@@ -0,0 +1,7 @@
+(function(angular, $, _) {
+  "use strict";
+
+  // Declare module
+  angular.module('crmSearchDisplay', CRM.angRequires('crmSearchDisplay'));
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js b/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js
new file mode 100644 (file)
index 0000000..c3d4e5e
--- /dev/null
@@ -0,0 +1,188 @@
+(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._);
diff --git a/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html b/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html
new file mode 100644 (file)
index 0000000..6b08633
--- /dev/null
@@ -0,0 +1,42 @@
+<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="&lsaquo;"
+      next-text="&rsaquo;"
+      first-text="&laquo;"
+      last-text="&raquo;"
+  ></ul>
+</div>
diff --git a/ext/search/ang/crmSearchKit.ang.php b/ext/search/ang/crmSearchKit.ang.php
new file mode 100644 (file)
index 0000000..e48bd14
--- /dev/null
@@ -0,0 +1,14 @@
+<?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'],
+];
diff --git a/ext/search/ang/crmSearchKit.module.js b/ext/search/ang/crmSearchKit.module.js
new file mode 100644 (file)
index 0000000..76caa98
--- /dev/null
@@ -0,0 +1,21 @@
+(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._);
similarity index 93%
rename from ext/search/ang/search/crmSearchValue.directive.js
rename to ext/search/ang/crmSearchKit/crmSearchValue.directive.js
index 9e39cf1585b50059c563229efc931ae6253892a5..2d1bbd09e5ded1362ea23110e48c56b8e77c45e0 100644 (file)
@@ -1,7 +1,7 @@
 (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');
           }
         });
       }
diff --git a/ext/search/ang/crmSearchPage.ang.php b/ext/search/ang/crmSearchPage.ang.php
new file mode 100644 (file)
index 0000000..b806777
--- /dev/null
@@ -0,0 +1,15 @@
+<?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'],
+];
diff --git a/ext/search/ang/crmSearchPage.module.js b/ext/search/ang/crmSearchPage.module.js
new file mode 100644 (file)
index 0000000..8eafa0e
--- /dev/null
@@ -0,0 +1,46 @@
+(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._);
diff --git a/ext/search/ang/search.ang.php b/ext/search/ang/search.ang.php
deleted file mode 100644 (file)
index 6727508..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-<?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'],
-];
diff --git a/ext/search/ang/search.module.js b/ext/search/ang/search.module.js
deleted file mode 100644 (file)
index 291552c..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-(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._);
diff --git a/ext/search/ang/search/crmSearch.html b/ext/search/ang/search/crmSearch.html
deleted file mode 100644 (file)
index 5c6a010..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<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>
index 276755632f688215c021a57bd6e4dc3d60550129..2353aa2bd666bb6b777ac27f2e974358fadd3982 100644 (file)
 .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;
+}
index 587477bff06c8bace64b001fcc3e9ef7e9e9f60c..048de3f491d88cdc4a8988e71bde35f13d2a97e1 100644 (file)
@@ -13,8 +13,8 @@
     <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>
diff --git a/ext/search/managed/SearchDisplayType.mgd.php b/ext/search/managed/SearchDisplayType.mgd.php
new file mode 100644 (file)
index 0000000..b8e3fa9
--- /dev/null
@@ -0,0 +1,24 @@
+<?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',
+    ],
+  ],
+];
index 2a696e2cffa0848049994a544c3bcb8069308097..7ae23cc7de4f1d0330c86f1819f0db0bb4cd7cde 100644 (file)
@@ -7,9 +7,9 @@
  * 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.
@@ -473,5 +473,11 @@ function _search_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
  * @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',
+    ],
+  ]);
 }
index 7c7f9e1a83eac1135efa45bcabc5d0656253956c..ce37a888686bc3e0e4020414f31aabbd56399bd5 100644 (file)
@@ -121,8 +121,58 @@ function search_civicrm_entityTypes(&$entityTypes) {
 }
 
 /**
- * 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);
+
+}
diff --git a/ext/search/sql/auto_install.sql b/ext/search/sql/auto_install.sql
new file mode 100644 (file)
index 0000000..4bdafa8
--- /dev/null
@@ -0,0 +1,69 @@
+-- +--------------------------------------------------------------------+
+-- | 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
diff --git a/ext/search/sql/auto_uninstall.sql b/ext/search/sql/auto_uninstall.sql
new file mode 100644 (file)
index 0000000..7fc5f6b
--- /dev/null
@@ -0,0 +1,22 @@
+-- +--------------------------------------------------------------------+
+-- | 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
diff --git a/ext/search/templates/CRM/Search/Page/Search.tpl b/ext/search/templates/CRM/Search/Page/Search.tpl
new file mode 100644 (file)
index 0000000..e69de29
index d57a0c52ce6ec9ca04023c6cc3efd4e2ee8b2659..168defc411e816ece36869c80062e17f00c2051a 100644 (file)
@@ -2,7 +2,12 @@
 <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>
diff --git a/ext/search/xml/schema/CRM/Search/SearchDisplay.entityType.php b/ext/search/xml/schema/CRM/Search/SearchDisplay.entityType.php
new file mode 100644 (file)
index 0000000..6629b57
--- /dev/null
@@ -0,0 +1,10 @@
+<?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',
+  ],
+];
diff --git a/ext/search/xml/schema/CRM/Search/SearchDisplay.xml b/ext/search/xml/schema/CRM/Search/SearchDisplay.xml
new file mode 100644 (file)
index 0000000..750977d
--- /dev/null
@@ -0,0 +1,100 @@
+<?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>
index 8a1ee54a31b0949a4e616d1d4a58ed526c447bbb..22757a2bb41bff54eb845d9f60caf50019d76555 100644 (file)
@@ -249,6 +249,11 @@ if (!CRM.vars) CRM.vars = {};
         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);
@@ -1300,10 +1305,10 @@ if (!CRM.vars) CRM.vars = {};
 
     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();
@@ -1323,7 +1328,9 @@ if (!CRM.vars) CRM.vars = {};
         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);
     }
index 970335ce2d46916a877c4c1ce7ae3bfc3e86e99e..1d4f602400aea8ae0a8e15e0a1b1394dfe01c943 100644 (file)
@@ -15,6 +15,37 @@ Other resources for identifying changes are:
     * 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
diff --git a/release-notes/5.30.1.md b/release-notes/5.30.1.md
new file mode 100644 (file)
index 0000000..28137c0
--- /dev/null
@@ -0,0 +1,41 @@
+# 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`.
diff --git a/release-notes/5.31.0.md b/release-notes/5.31.0.md
new file mode 100644 (file)
index 0000000..0a48c51
--- /dev/null
@@ -0,0 +1,1235 @@
+# 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`.
diff --git a/release-notes/5.32.0.md b/release-notes/5.32.0.md
new file mode 100644 (file)
index 0000000..c78fa24
--- /dev/null
@@ -0,0 +1,594 @@
+# 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`.
index abfc0392604922e9ef9663e0eed42870d09358e0..52665827de2d3e4af241825552df2a4a2009705b 100644 (file)
@@ -39,6 +39,7 @@ endif; ?>
           <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>
index 4e4e051f31a759dafe229fea743fba5dedf557de..a4fc784f90634cffed522431ebb16e8def9f64c7 100644 (file)
@@ -22,10 +22,11 @@ if (!defined('CIVI_SETUP')) {
     _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'],
index 2320b88dace623244d904118b1d3147093f953bc..4805d998f46b6c8e3e411a3989207a4217b668cb 100644 (file)
@@ -40,6 +40,7 @@ if (!defined('CIVI_SETUP')) {
     // 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'],
index d57ff432c21cfa4f80be5129073b00d7e4a9dceb..c65d12f220ff445e04d0dce18b27240fdf843a73 100644 (file)
@@ -38,6 +38,7 @@ if (!defined('CIVI_SETUP')) {
     // 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'],
index 22034e90895400b5d3cb81cb4617ee90b35ede6c..98586c695e0f89d5ca0eab75fbe6cc42201f30a9 100644 (file)
@@ -32,7 +32,7 @@ if (!defined('CIVI_SETUP')) {
 
     // 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;
@@ -41,6 +41,7 @@ if (!defined('CIVI_SETUP')) {
     // 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'],
index 21294a6e2a0d83eaf6177d7bf6589166a0f120e7..f273231d0c8e32f0eb60106b27d432a34c8e90c9 100644 (file)
@@ -50,8 +50,9 @@ if (!defined('CIVI_SETUP')) {
     $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,
index 5422292f32a5f5e41bfd8d5e33929cd139025bff..ca107325d151e434f86abbe62278dfe6495846d7 100644 (file)
@@ -28,11 +28,30 @@ if (!defined('CIVI_SETUP')) {
       $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));
+      }
     }
   });
 
index 87849057c6d896932e818dc87d7a01a38f104f90..6c23c251658c7ade05f8ae4cbbc96a8cf2ea2cf6 100644 (file)
@@ -11,11 +11,27 @@ class DbUtil {
    */
   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),
     );
   }
@@ -41,9 +57,9 @@ class DbUtil {
    * @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;
@@ -56,8 +72,7 @@ class DbUtil {
         $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;
       }
     }
@@ -84,21 +99,34 @@ class DbUtil {
    *   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);
   }
 
   /**
index 8d1e95bc2e7208156cb950b6fa26c975454d670d..d16717dc2eae7ebc0a01367ce75f1f6107564773 100644 (file)
@@ -20,7 +20,7 @@ class DrupalUtil {
       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/".');
index 59e1f9d52b44691816a5d195feda9bdbfcfbf89f..ade11c28c4c785f2b87ffb174dabffe2dc06a87c 100644 (file)
@@ -399,7 +399,7 @@ UNLOCK TABLES;
 
 LOCK TABLES `civicrm_domain` WRITE;
 /*!40000 ALTER TABLE `civicrm_domain` DISABLE KEYS */;
-INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `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;
 
@@ -495,7 +495,7 @@ 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;
 
@@ -1328,7 +1328,7 @@ 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;
 
index 83dc830de32254d50a977976f3095c85b4005cb4..c8e46441a40b0cf28a15e8aed8aa045807a20ed5 100644 (file)
@@ -963,4 +963,4 @@ INSERT INTO civicrm_navigation
 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';
index f7e2bde5c4b9c875d0741af52b06d9bf6e4c9d30..18f1d67148d371cec73ab297df18e794bbea7088 100644 (file)
       {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}
index b076bc2f3a1f2d1c6d63a980f9e7c9474194b988..9a7299a39b98a8735d06c18cb571bca178374470 100644 (file)
@@ -24,7 +24,7 @@
 <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;" />&nbsp;<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">
@@ -46,5 +46,6 @@
         <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>
index 51a3c91956d8b96f243496c06062b4865605cd98..f2bcfdebb169d2202bd1ae94ce1d3bd9b8d8e21a 100644 (file)
     <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>
index 5a3bd098639cbdae1bbf529889be7cfb22b0570c..286cee5a6763848495a810a32ea35fbe9daf4cf4 100644 (file)
@@ -62,7 +62,9 @@
               <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}
index 8be7d88e7b2d33784fa4ab9692fb3a77a72c5d11..497e11799b82b6e9d6bce2fd43451f7adaa1732b 100644 (file)
         </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>&nbsp;</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 &ndash; 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 &quot;-none-&quot; 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}
@@ -77,7 +77,7 @@
 
 
   {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>&nbsp;</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>&nbsp;</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>
index aeb56d695bc6e5c3844c278eb5e69842a258806d..75c9fed24713236f8fd21fdb170c2dc3ed211cc8 100644 (file)
 
       $("#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) {
index fb46c69e9bc98c08822158b71047fae71d1a86e8..8c9a85bb1c2dad7a8299ba0be692ae5d9f139421 100644 (file)
       </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}&nbsp;({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>
index 2c6c7c039555d306c683f928badac402d6e5b7a3..dab6017c0150993adaa98bace6736fe31c759211 100644 (file)
     {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>
index cb582857e391c21577cd691e419cd3f7f9c004fd..a75e03b04fd934de33175be0043e52253911633b 100644 (file)
                             <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'}
index 4e0c9c2401d0edfd4bd72143a86f6e8a114354f9..3e28f420ac2ca511b3deb030f7680bce42320b9d 100644 (file)
                             <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'}
index d66a63d632ecb2b5f2c7605c5ca314e0e4ab5b89..d4018fcc0ef743dab2153ff1e0307bb5b8e413f4 100644 (file)
@@ -443,6 +443,7 @@ if (!defined('CIVICRM_PSR16_STRICT')) {
 // 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
index 6b730b171bc1d613aecf8018cf57ebe0d0e8530e..3d9dbbcb3f4cbd3e52d5b6f1bbdec18611b2fb9e 100644 (file)
@@ -24,6 +24,8 @@
  *   <http://www.gnu.org/licenses/>.
  */
 
+use Civi\Api4\Campaign;
+
 /**
  *  Test CRM/Member/BAO Membership Log add , delete functions
  *
@@ -171,12 +173,12 @@ class CRM_Batch_Form_EntryTest extends CiviUnitTestCase {
     $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']);
@@ -190,7 +192,7 @@ class CRM_Batch_Form_EntryTest extends CiviUnitTestCase {
     //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) {
@@ -238,6 +240,7 @@ class CRM_Batch_Form_EntryTest extends CiviUnitTestCase {
    */
   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,
@@ -257,6 +260,7 @@ class CRM_Batch_Form_EntryTest extends CiviUnitTestCase {
     ];
     $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];
@@ -264,17 +268,19 @@ class CRM_Batch_Form_EntryTest extends CiviUnitTestCase {
     $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']));
   }
 
   /**
index b4fc988a67003eff1185ee5152cb2b214e694a9b..075655206d1d07a26fff0e57aff91bd9bee10e2e 100644 (file)
@@ -1656,4 +1656,137 @@ class CRM_Contact_BAO_ContactTest extends CiviUnitTestCase {
     }
   }
 
+  /**
+   * 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' => [
+        'асдадасда шшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшасдасд',
+        'асдадасда шшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшш...',
+      ],
+    ];
+  }
+
 }
index 54a4f572db9fd442cea5c54cfe3f96453fcdfb3f..71d5481acb17b205de605406c4f2a6c02128c75f 100644 (file)
@@ -167,4 +167,29 @@ class CRM_Contact_Page_View_UserDashBoardTest extends CiviUnitTestCase {
     $_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&amp;reset=1&amp;id=1&amp;context=dashboard">Annual CiviCRM meet</a></td>',
+      '<td class="crm-participant-event-id_2"><a href="/index.php?q=civicrm/event/info&amp;reset=1&amp;id=2&amp;context=dashboard">Social Distancing Meetup Group</a></td>',
+    ];
+    $this->assertPageContains($expectedStrings);
+    $this->individualCreate();
+  }
+
 }
diff --git a/tests/phpunit/CRM/Contribute/Page/TabTest.php b/tests/phpunit/CRM/Contribute/Page/TabTest.php
new file mode 100644 (file)
index 0000000..722e6fd
--- /dev/null
@@ -0,0 +1,59 @@
+<?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&amp;reset=1&amp;id=" . $recurID . "&amp;cid=" . $contactID . "&amp;context=contribution\" class=\"action-item crm-hover-button\" title='View Recurring Payment' >View</a><a href=\"/index.php?q=civicrm/contribute/updaterecur&amp;reset=1&amp;action=update&amp;crid=1&amp;cid=3&amp;context=contribution\" class=\"action-item crm-hover-button\" title='Edit Recurring Payment' >Edit</a><a href=\"/index.php?q=civicrm/contribute/unsubscribe&amp;reset=1&amp;crid=" . $recurID . "&amp;cid=" . $contactID . "&amp;context=contribution\" class=\"action-item crm-hover-button\" title='Cancel' >Cancel</a></span>",
+      $templateVariable['activeRecurRows'][1]['action']
+    );
+  }
+
+}
index 6642d75c778218fca7255163ca0d18a4d6b83ad1..082f24ed687f5bd8fab5e3bf5d710c2b9f11efb1 100644 (file)
@@ -983,4 +983,47 @@ class CRM_Core_BAO_CustomFieldTest extends CiviUnitTestCase {
     }
   }
 
+  /**
+   * 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);
+  }
+
 }
index c8f21a3171c6988add5b4c48ec965dfccab078bb..94ecc5f96f4e52fc1bea932e9872dc839939304d 100644 (file)
@@ -46,6 +46,18 @@ class CRM_Core_ErrorTest extends CiviUnitTestCase {
     $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.
    *
index 57bca3e47b272206c08aadaddabef5cc2f4a0e6d..34abdec5bbaa1b43b52de741ad236814ca4de942 100644 (file)
@@ -72,6 +72,14 @@ class CRM_Core_FormTest extends CiviUnitTestCase {
           $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);
+        },
+      ],
     ];
   }
 
index 2b04f7a484f721a24408e081e482d5216cb89731..5953eb796cbfa97775a3d3b970cd0104340c9532 100644 (file)
@@ -164,10 +164,13 @@ class CRM_Core_Payment_AuthorizeNetIPNTest extends CiviUnitTestCase {
     $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']);
   }
 
   /**
index 52d9426fbfc038038d5e99bf85aba28134b855f3..dac135b09919ccddfcd9128e5ce08f76a40186dc 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\Contribution;
+
 /**
  * Class CRM_Core_Payment_BaseIPNTest
  * @group headless
@@ -405,15 +407,16 @@ class CRM_Core_Payment_BaseIPNTest extends CiviUnitTestCase {
 
   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,
index a073033f90974b05d17ab51c318930d7de89b3a5..80a030be6ab99585cd62ea2940c379b450101375 100644 (file)
@@ -104,8 +104,11 @@ class CRM_Core_Payment_PayPalIPNTest extends CiviUnitTestCase {
    */
   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']);
index 3b1a069cbb420135037fea3ff800877c0b9732c3..63fad9d1986e3ca40a99ad1d3a5cc9f785f93ddf 100644 (file)
@@ -222,6 +222,45 @@ trait CRM_Core_Resources_CollectionTestTrait {
     $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.
    */
index 120253be360fe09ab72ea8fbb4bb31ee662bbfef..3fc3e5f2554d216a9714a707e019bff3c3d1d684 100644 (file)
@@ -56,7 +56,7 @@ class CRM_Event_BAO_EventPermissionsTest extends CiviUnitTestCase {
   }
 
   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);
@@ -67,7 +67,7 @@ class CRM_Event_BAO_EventPermissionsTest extends CiviUnitTestCase {
   }
 
   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);
@@ -79,7 +79,7 @@ class CRM_Event_BAO_EventPermissionsTest extends CiviUnitTestCase {
    */
   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);
@@ -135,10 +135,20 @@ class CRM_Event_BAO_EventPermissionsTest extends CiviUnitTestCase {
 
   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']);
+  }
+
 }
index 7d21e3cfc09d0206fee66aa2302f7f6b8d37ff6c..3bdeb7971c2ce3b6c5a8afe0f0a3638c602f899b 100644 (file)
@@ -2986,4 +2986,44 @@ class CRM_Export_BAO_ExportTest extends CiviUnitTestCase {
     $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']);
+  }
+
 }
diff --git a/tests/phpunit/CRM/Mailing/MailStoreTest.php b/tests/phpunit/CRM/Mailing/MailStoreTest.php
new file mode 100644 (file)
index 0000000..ba982b9
--- /dev/null
@@ -0,0 +1,91 @@
+<?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;
+  }
+
+}
index aa4dee5e8326a4b65d613be4946769f286b327cc..83c6bbbc3d95fe31ea75cc3cd23b83547b0ab0bd 100644 (file)
@@ -148,7 +148,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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',
@@ -181,7 +181,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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.'
@@ -198,7 +198,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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.'
@@ -242,7 +242,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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.'
@@ -264,7 +264,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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.'
@@ -415,13 +415,12 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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.'
@@ -445,10 +444,9 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       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,
@@ -459,7 +457,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
     $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);
   }
 
@@ -484,32 +482,20 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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.'
@@ -527,8 +513,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       NULL,
       NULL,
       NULL,
-      FALSE,
-      NULL
+      FALSE
     );
 
     $this->assertDBNotNull('CRM_Member_BAO_MembershipLog',
@@ -538,7 +523,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       'Database checked on membership log record.'
     );
 
-    $this->membershipDelete($membershipId);
+    $this->membershipDelete($membership['id']);
     $this->contactDelete($contactId);
   }
 
@@ -555,17 +540,17 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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);
   }
@@ -583,17 +568,17 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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);
   }
@@ -610,17 +595,17 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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']);
   }
 
@@ -827,7 +812,7 @@ class CRM_Member_BAO_MembershipTest extends CiviUnitTestCase {
       '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.'
index 45f9178dbf1a157d520275fe4004aab5fd6eeda5..c7d855562d4ecddef2452f6621bdeeb957e10b17 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\MembershipType;
+
 /**
  * Class CRM_Member_BAO_MembershipTypeTest
  * @group headless
@@ -281,7 +283,6 @@ class CRM_Member_BAO_MembershipTypeTest extends CiviUnitTestCase {
    *
    */
   public function testGetRenewalDatesForMembershipType() {
-    $ids = [];
     $params = [
       'name' => 'General',
       'domain_id' => 1,
@@ -296,11 +297,11 @@ class CRM_Member_BAO_MembershipTypeTest extends CiviUnitTestCase {
       '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',
@@ -309,15 +310,15 @@ class CRM_Member_BAO_MembershipTypeTest extends CiviUnitTestCase {
       '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]);
   }
 
   /**
index 91bb01be62f5eb3403c76df09d983c655c937565..9f61916847e013f77690f234c7b77b656bd6a661 100644 (file)
@@ -474,11 +474,12 @@ class CRM_Member_Form_MembershipTest extends CiviUnitTestCase {
    *
    * @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();
@@ -488,7 +489,7 @@ class CRM_Member_Form_MembershipTest extends CiviUnitTestCase {
     $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.
@@ -508,8 +509,7 @@ class CRM_Member_Form_MembershipTest extends CiviUnitTestCase {
       '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',
@@ -802,7 +802,7 @@ class CRM_Member_Form_MembershipTest extends CiviUnitTestCase {
     ], 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,
@@ -1488,8 +1488,6 @@ Expires: ',
    */
   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', [
@@ -1522,6 +1520,8 @@ Expires: ',
       '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]);
index 1d041f4f479b8b36068912472db9c57238400ec7..1e9ef5e4bfd33b14bd9bcd07e8aa98810eb94532 100644 (file)
@@ -349,6 +349,19 @@ trait CRMTraits_Custom_CustomDataTrait {
     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.
    *
index c927e71857473add0769b48ae536df80e7bf8533..fbbd2347eb9447e30ae1a72948693d4bece0b62c 100644 (file)
@@ -60,31 +60,62 @@ trait CRMTraits_Financial_OrderTrait {
             '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;
   }
 
@@ -191,4 +222,31 @@ trait CRMTraits_Financial_OrderTrait {
     ]);
   }
 
+  /**
+   * @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'],
+        ]),
+      ],
+    ];
+  }
+
 }
diff --git a/tests/phpunit/Civi/Angular/LoaderTest.php b/tests/phpunit/Civi/Angular/LoaderTest.php
new file mode 100644 (file)
index 0000000..c6c144b
--- /dev/null
@@ -0,0 +1,104 @@
+<?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++];
+  }
+
+}
index 429249e2bde6284fe6523863db54285e737801c6..8d540ed0554aeb5ab05fab91fd5c35ed23c5f689 100644 (file)
@@ -2504,6 +2504,7 @@ VALUES
    * @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,
@@ -2515,6 +2516,8 @@ VALUES
       '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,
index f05ebb113d792f6fa6b0bba3250d46f6633025db..7d0d944429e6ef7e0cd91cfd519ff6f9a051b06d 100644 (file)
@@ -8,7 +8,7 @@ namespace E2E\Core;
  * Check that the active prev-next service behaves as expected.
  *
  * @package E2E\Core
- * @group e2e
+ * @group e2e ornery
  */
 class PrevNextTest extends \CiviEndToEndTestCase {
 
@@ -92,11 +92,10 @@ 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() {
@@ -250,20 +249,15 @@ class PrevNextTest extends \CiviEndToEndTestCase {
     // 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);
@@ -329,6 +323,8 @@ class PrevNextTest extends \CiviEndToEndTestCase {
    *   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) {
@@ -342,6 +338,7 @@ class PrevNextTest extends \CiviEndToEndTestCase {
     );
 
     $this->assertCount(count($ids), $selected);
+    return $selected;
   }
 
 }
index deb222613378c4cb3afc68d7ecfd63ea627ec9a8..6efc949f33fbf7aa5c7761e9ad23cd3a5707dde7 100644 (file)
@@ -22,6 +22,7 @@ class api_v3_ContributionTest extends CiviUnitTestCase {
 
   use CRMTraits_Profile_ProfileTrait;
   use CRMTraits_Custom_CustomDataTrait;
+  use CRMTraits_Financial_OrderTrait;
 
   protected $_individualId;
   protected $_contribution;
@@ -2767,6 +2768,41 @@ class api_v3_ContributionTest extends CiviUnitTestCase {
     $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.
    *
@@ -3715,6 +3751,7 @@ class api_v3_ContributionTest extends CiviUnitTestCase {
     ], [
       'Event',
     ]);
+    $this->checkReceiptDetails($mut, $contributionPage['id'], $contribution['id']);
     $mut->stop();
   }
 
@@ -3743,6 +3780,42 @@ class api_v3_ContributionTest extends CiviUnitTestCase {
     ]);
   }
 
+  /**
+   * 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.
    */
index 71d18540772e086214903b309041eaf58bb387a2..163ef35b212b2ce6ecc6dbe61bffe4a0b37c5eb2 100644 (file)
@@ -16,7 +16,6 @@
  * @subpackage API_Job
  *
  * @copyright CiviCRM LLC https://civicrm.org/licensing
- * @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $
  *
  */
 
@@ -100,10 +99,23 @@ class api_v3_JobProcessMailingTest extends CiviUnitTestCase {
     $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,
@@ -571,6 +583,7 @@ class api_v3_JobProcessMailingTest extends CiviUnitTestCase {
       'civicrm_activity_contact',
       'civicrm_activity',
     ]);
+    Civi::settings()->set('mailerBatchLimit', 0);
   }
 
   /**
index 0d3b51eda1a35b13a2871baae033bd4750e12151..bec416700c6f26cfc8d4c2e51b460deefa1b085d 100644 (file)
@@ -1341,7 +1341,7 @@ class api_v3_MembershipTest extends CiviUnitTestCase {
   /**
    * 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();
@@ -1354,20 +1354,20 @@ class api_v3_MembershipTest extends CiviUnitTestCase {
       '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);
 
@@ -1380,11 +1380,11 @@ class api_v3_MembershipTest extends CiviUnitTestCase {
       $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 = [
index 337f2c54e31432cd52abfac430ce0cb8e96618fc..bf81f4fb6888e01a58e45077e969ebaf4197d62b 100644 (file)
@@ -468,59 +468,6 @@ class api_v3_OrderTest extends CiviUnitTestCase {
     ]);
   }
 
-  /**
-   * 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.
    */
index 5c36bde5d2c9498251f4c7ff715ca455acb067f4..ad9f744763dcdda2736c4a29ad554e20b17cc499 100644 (file)
@@ -621,6 +621,33 @@ class api_v3_PaymentTest extends CiviUnitTestCase {
     $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
    *
index 3fc8f6928f0df43dd9e8f7dd15ed1e0bb11647db..7524a2db83998560f15772efb6b28eb2591700b2 100644 (file)
@@ -88,6 +88,16 @@ class ContactGetTest extends \api\v4\UnitTestCase {
     $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']));
   }
 
 }
index 9a8806ed02d88ea5b68f997555018141dfd0b1f7..957ed485d7568c601482938abbe6909e3db3ef63 100644 (file)
@@ -141,7 +141,7 @@ class ConformanceTest extends UnitTestCase {
     $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']);
   }
index da96cec0c402868625d3928967cc60b2b2785ee9..ee0138a946d7967ca7afbd1ae6adf79d2a7d62c3 100644 (file)
@@ -53,6 +53,12 @@ class TestCreationParameterProvider {
     $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;
     }
 
index 1fee89be2f4d91bb0bcd49bd35a3a46728bb6eb6..2a1dde72ea4c78143e90b5f9a8c1e29328ca8978 100644 (file)
@@ -7,6 +7,12 @@
   <add>1.1</add>
   <log>true</log>
   <icon>fa-tasks</icon>
+  <paths>
+    <add>civicrm/activity?reset=1&amp;action=add&amp;context=standalone</add>
+    <view>civicrm/activity?reset=1&amp;action=view&amp;id=[id]</view>
+    <update>civicrm/activity/add?reset=1&amp;action=update&amp;id=[id]</update>
+    <delete>civicrm/activity?reset=1&amp;action=delete&amp;id=[id]</delete>
+  </paths>
   <field>
     <name>id</name>
     <uniqueName>activity_id</uniqueName>
index c87c03730d137ee4a5864854214f6caf1a23718e..3625aca74aa2dde5f42287679ff39153a7a48d4a 100644 (file)
@@ -6,6 +6,11 @@
   <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&amp;action=update&amp;id=[id]</update>
+    <delete>civicrm/campaign/add?reset=1&amp;action=delete&amp;id=[id]</delete>
+  </paths>
   <field>
     <name>id</name>
     <title>Campaign ID</title>
index 8200affa648c656720ea1dbadae17d22a5fcbd1f..6002cc6f4643330e7597c726c06232d2dfe6897f 100644 (file)
@@ -8,6 +8,12 @@
   <add>1.1</add>
   <log>true</log>
   <icon>fa-address-book-o</icon>
+  <paths>
+    <add>civicrm/contact/add?reset=1&amp;ct=[contact_type]</add>
+    <view>civicrm/contact/view?reset=1&amp;cid=[id]</view>
+    <update>civicrm/contact/add?reset=1&amp;action=update&amp;cid=[id]</update>
+    <delete>civicrm/contact/view/delete?reset=1&amp;delete=1&amp;cid=[id]</delete>
+  </paths>
   <field>
     <name>id</name>
     <type>int unsigned</type>
index 043beab52d43ab1fe408c788aa2e79f20a0ae53b..41948bbb39bd88ee3bcf1f749ffd505672c41b38 100644 (file)
@@ -8,6 +8,9 @@
   <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>
@@ -34,7 +37,6 @@
     <title>Group Title</title>
     <length>255</length>
     <localizable>true</localizable>
-    <required>true</required>
     <comment>Name of Group.</comment>
     <add>1.1</add>
     <html>
index 2d73182ac5c55948c9c0d3d5ec1d2bc671208b5a..1edf5f9ac937cc1c04c55bd7f8de4c40421f4d1b 100644 (file)
     <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>
@@ -27,6 +60,7 @@
     <serialize>PHP</serialize>
     <add>1.1</add>
   </field>
+
   <field>
     <name>mapping_id</name>
     <type>int unsigned</type>
@@ -41,6 +75,7 @@
     <onDelete>SET NULL</onDelete>
     <add>1.5</add>
   </foreignKey>
+
   <field>
     <name>search_custom_id</name>
     <type>int unsigned</type>
@@ -48,6 +83,7 @@
     <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>
@@ -56,6 +92,7 @@
     <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>
index fed11d358c87d55b59ae90f106a72f000d9de42c..8d7635d51cfd45e6d982056dd2f82e4dcb3d8812 100644 (file)
@@ -7,6 +7,12 @@
   <add>1.3</add>
   <log>true</log>
   <icon>fa-credit-card</icon>
+  <paths>
+    <add>civicrm/contribute/add?reset=1&amp;action=add&amp;context=standalone</add>
+    <view>civicrm/contact/view/contribution?reset=1&amp;action=view&amp;id=[id]</view>
+    <update>civicrm/contact/view/contribution?reset=1&amp;action=update&amp;id=[id]</update>
+    <delete>civicrm/contact/view/contribution?reset=1&amp;action=delete&amp;id=[id]</delete>
+  </paths>
   <field>
     <name>id</name>
     <uniqueName>contribution_id</uniqueName>
index 979a84df4de453565b0df4caeee064e77a763763..7471a61e8a87d49977b7f5b7fe7c21c92c0a67f2 100644 (file)
@@ -7,6 +7,10 @@
   <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&amp;id=[id]</view>
+  </paths>
   <field>
     <name>id</name>
     <type>int unsigned</type>
index e34d9b91123c79838ab7caa7294672e5010a95a6..f66395144b2a8b02478de371876a561a8cd0db25 100644 (file)
     <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>
index b6c2c8923519bbf48baf1c875e677d7d2bdf957a..da03aea06f62f16ca80b4a22d372dbc83aaeeb44 100644 (file)
@@ -7,6 +7,12 @@
   <add>1.8</add>
   <log>true</log>
   <icon>fa-money</icon>
+  <paths>
+    <add>civicrm/grant/add?reset=1&amp;action=add&amp;context=standalone</add>
+    <view>contact/view/grant?reset=1&amp;action=view&amp;id=[id]&amp;cid=[contact_id]</view>
+    <update>civicrm/contact/view/grant?reset=1&amp;action=update&amp;id=[id]&amp;cid=[contact_id]</update>
+    <delete>civicrm/contact/view/grant?reset=1&amp;action=delete&amp;id=[id]&amp;cid=[contact_id]</delete>
+  </paths>
   <field>
     <name>id</name>
     <type>int unsigned</type>
index 389b30be964d2d99ef448e7616bbfdbd28f83e5f..5fc2a62b2069036bc6af39046a18142ba828e288 100644 (file)
@@ -7,6 +7,10 @@
   <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>
index e0f35ffb3afde61e2498249b03409837a0246cbd..b2063cb4add4e02db18d9e5a68032aa369b3ecc6 100644 (file)
@@ -8,6 +8,12 @@
   <add>1.5</add>
   <log>true</log>
   <icon>fa-id-badge</icon>
+  <paths>
+    <add>civicrm/member/add?reset=1&amp;action=add&amp;context=standalone</add>
+    <view>civicrm/contact/view/membership?reset=1&amp;action=view&amp;id=[id]&amp;cid=[contact_id]</view>
+    <update>civicrm/contact/view/membership?reset=1&amp;action=update&amp;id=[id]&amp;cid=[contact_id]</update>
+    <delete>civicrm/contact/view/membership?reset=1&amp;action=delete&amp;id=[id]&amp;cid=[contact_id]</delete>
+  </paths>
   <field>
     <name>id</name>
     <uniqueName>membership_id</uniqueName>
index e86bd1e5847280612e4c9f0293eed2eb36f878ec..50a26abd2b2bd0c7434a6e01a1541124c74f1438 100644 (file)
@@ -1782,3 +1782,4 @@ INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_act
 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);
index a11b5472757c26eae2c22e56c2691ebcc8b42f18..91908340bbbdebeb9c48087d9ef77bdddf7116a5 100644 (file)
@@ -3682,10 +3682,10 @@ INSERT INTO civicrm_state_province (id, country_id, abbreviation, name) VALUES
 -- 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"),
index 034137433ed489ed46385eb3060fc5bfcbb3c550..b8c2e514d8281d3abedb9eb282453b841ef150e8 100644 (file)
@@ -37,6 +37,14 @@ class {$table.className} extends CRM_Core_DAO {ldelim}
        * @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}
     /**
index bece5ae308ee56eb0a04042f6556f81954b0f843..c60cbeb03d2dd17a756036708abab1bab3288aef 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="iso-8859-1" ?>
 <version>
-  <version_no>5.32.alpha1</version_no>
+  <version_no>5.33.alpha1</version_no>
 </version>