--- /dev/null
+<?php
+
+namespace Civi\Core;
+
+use Civi;
+
+/**
+ * Test generation of URLs via `Civi::url()` (`Civi\Core\Url`).
+ *
+ * This class is focused on portable aspects of the functionality.
+ * There is also some coverage of the UF-specific parts in the E2E suite.
+ *
+ * @see \E2E\Core\PathUrlTest
+ * @group headless
+ */
+class UrlTest extends \CiviUnitTestCase {
+
+ public function setUp(): void {
+ $parts = explode('/', CIVICRM_UF_BASEURL);
+ $this->assertRegexp(';^[a-z0-9\.\-]+(:\d+)?$;', $parts[2], 'CIVICRM_UF_BASEURL should have domain name and/or port');
+ $tmpVars['_SERVER']['HTTP_HOST'] = $parts[2];
+ \CRM_Utils_GlobalStack::singleton()->push($tmpVars);
+
+ parent::setUp();
+ $this->useTransaction();
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+ \CRM_Utils_GlobalStack::singleton()->pop();
+ }
+
+ public function testAbsoluteRelative() {
+ $absolutes = [];
+ $absolutes['flag'] = Civi::url('backend://civicrm/admin', 'a');
+ $absolutes['method'] = Civi::url('backend://civicrm/admin')->setPreferFormat('absolute');
+
+ $relatives = [];
+ $relatives['default'] = Civi::url('backend://civicrm/admin');
+ $relatives['flag'] = Civi::url('backend://civicrm/admin', 'r');
+ $relatives['method'] = Civi::url('backend://civicrm/admin')->setPreferFormat('relative');
+
+ foreach ($absolutes as $key => $url) {
+ $this->assertRegExp(';^https?://;', (string) $url, "absolutes[$key] should be absolute URL");
+ }
+ foreach ($relatives as $key => $url) {
+ $this->assertNotRegExp(';^https?://;', (string) $url, "relatives[$key] should be relative URL");
+ }
+ }
+
+ public function testPath() {
+ $examples = [];
+ $examples[] = ['civicrm/ajax/api4', Civi::url('service://civicrm/ajax/api4')];
+ $examples[] = ['civicrm/ajax/api4/Contact/get', Civi::url('service://civicrm/ajax/api4')->addPath('Contact/get')];
+ $examples[] = ['civicrm/ajax/api4/Contact/get', Civi::url('service://civicrm/ajax/api4')->addPath('Contact')->addPath('get')];
+ $examples[] = ['civicrm/new-path', Civi::url('service://civicrm/old-path')->setPath('civicrm/new-path')];
+
+ foreach ($examples as $key => $example) {
+ /** @var \Civi\Core\Url $url */
+ [$expected, $url] = $example;
+ $this->assertEquals($expected, $url->getPath());
+ $this->assertStringContainsString($expected, (string) $url);
+ }
+ }
+
+ public function testQuery() {
+ $examples = [];
+ $examples[] = ['reset=1&id=9', Civi::url('frontend://civicrm/profile/view?reset=1&id=9')];
+ $examples[] = ['reset=1&id=9', Civi::url('frontend://civicrm/profile/view')->addQuery('reset=1&id=9')];
+ $examples[] = ['reset=1&id=9', Civi::url('frontend://civicrm/profile/view')->addQuery(['reset' => 1, 'id' => 9])];
+ $examples[] = ['noise=Hello+world%3F', Civi::url('frontend://civicrm/profile/view?noise=Hello+world%3F')];
+ $examples[] = ['noise=Hello+world%3F', Civi::url('frontend://civicrm/profile/view')->addQuery(['noise' => 'Hello world?'])];
+ $examples[] = ['reset=1&id=9', Civi::url('frontend://civicrm/profile/view?forget=this')->setQuery('reset=1&id=9')];
+ $examples[] = ['reset=1&id=9', Civi::url('frontend://civicrm/profile/view?forget=this')->setQuery('reset=1')->addQuery('id=9')];
+
+ foreach ($examples as $key => $example) {
+ /** @var \Civi\Core\Url $url */
+ [$expected, $url] = $example;
+ $this->assertEquals($expected, $url->getQuery());
+ $this->assertStringContainsString($expected, (string) $url);
+ }
+ }
+
+}
}
}
+ /**
+ * Get URLs through Civi::url().
+ *
+ * @see \Civi\Core\UrlTest
+ */
+ public function testUrl(): void {
+ // Make some requests for actual URLs
+ $this->assertUrlContentRegex(';MIT-LICENSE.txt;', \Civi::url('[civicrm.packages]/jquery/plugins/jquery.timeentry.js', 'a'));
+ $this->assertUrlContentRegex(';MIT-LICENSE.txt;', \Civi::url('asset://[civicrm.packages]/jquery/plugins/jquery.timeentry.js', 'a'));
+ $this->assertUrlContentRegex(';Please enter a valid email address;', \Civi::url('assetBuilder://crm-l10n.js?locale=en_US', 'a'));
+ $this->assertUrlContentRegex(';.module..crmSearchAdmin;', \Civi::url('ext://org.civicrm.search_kit/ang/crmSearchAdmin.module.js', 'a'));
+ $this->assertUrlContentRegex(';crm-section event_date_time-section;', \Civi::url('frontend://civicrm/event/info?id=1', 'a'));
+
+ // Check for well-formedness of some URLs
+ $urlPats = [];
+ switch (CIVICRM_UF) {
+ case 'Drupal':
+ case 'Drupal8':
+ case 'Backdrop':
+ $urlPats[] = [';/civicrm/event/info\?reset=1&id=9;', \Civi::url('frontend://civicrm/event/info?reset=1')->addQuery('id=9')];
+ $urlPats[] = [';/civicrm/admin\?reset=1;', \Civi::url('backend://civicrm/admin')->addQuery(['reset' => 1])];
+ break;
+
+ case 'WordPress':
+ $urlPats[] = [';civiwp=CiviCRM.*civicrm.*event.*info.*reset=1&id=9;', \Civi::url('frontend://civicrm/event/info?reset=1')->addQuery('id=9')];
+ $urlPats[] = [';/wp-admin.*civicrm.*admin.*reset=1;', \Civi::url('backend://civicrm/admin?reset=1')];
+ break;
+
+ case 'Joomla':
+ $urlPats[] = [';/index.php\?.*task=civicrm/event/info&reset=1&id=9;', \Civi::url('frontend://civicrm/event/inof?reset=1')->addQuery('id=9')];
+ $urlPats[] = [';/administrator/.*task=civicrm/admin/reset=1;', \Civi::url('backend://civicrm/admin')->addQuery('reset=1')];
+ break;
+
+ default:
+ $this->fail('Unrecognized UF: ' . CIVICRM_UF);
+ }
+
+ $urlPats[] = [';^https?://.*civicrm;', \Civi::url('frontend://civicrm/event/info?reset=1', 'a')];
+ $urlPats[] = [';^https://.*civicrm;', \Civi::url('frontend://civicrm/event/info?reset=1', 'as')];
+
+ // Some test-harnesses have HTTP_HOST. Some don't. It's pre-req for truly relative URLs.
+ if (!empty($_SERVER['HTTP_HOST'])) {
+ $urlPats[] = [';^/.*civicrm.*ajax.*api4.*Contact.*get;', \Civi::url('backend://civicrm/ajax/api4/Contact/get', 'r')];
+ }
+
+ $this->assertNotEmpty($urlPats);
+ foreach ($urlPats as $urlPat) {
+ $this->assertRegExp($urlPat[0], $urlPat[1]);
+ }
+ }
+
+ /**
+ * Check that 'frontend://', 'backend://', and 'current://' have the expected relations.
+ */
+ public function testUrl_FrontBackCurrent(): void {
+ $front = (string) \Civi::url('frontend://civicrm/profile/view');
+ $back = (string) \Civi::url('backend://civicrm/profile/view');
+ $current = (string) \Civi::url('current://civicrm/profile/view');
+ $this->assertStringContainsString('profile', $front);
+ $this->assertStringContainsString('profile', $back);
+ $this->assertStringContainsString('profile', $current);
+ if (CIVICRM_UF === 'WordPress' || CIVICRM_UF === 'Joomla') {
+ $this->assertNotEquals($front, $back, "On WordPress/Joomla, some URLs should support frontend+backend flavors.");
+ }
+ else {
+ $this->assertEquals($front, $back, "On Drupal/Backdrop/Standalone, frontend and backend URLs should look the same.");
+ }
+ $this->assertEquals($back, $current, "Within E2E tests, current routing style is backend.");
+ // For purposes of this test, it doesn't matter if "current" is frontend or backend - as long as it's consistent.
+ }
+
/**
* @param string $expectContentRegex
* @param string $url