Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(dev/core#4433) - Implement Civi::url() with prefixes and OOP enhancements #26861

Merged
merged 19 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CRM/Core/Invoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ public static function runItem($item) {
self::registerPharHandler();

$config = CRM_Core_Config::singleton();

// WISHLIST: if $item is a web-service route, swap prepend to $civicrm_url_defaults

if ($config->userFramework == 'Joomla' && $item) {
$config->userFrameworkURLVar = 'task';

Expand Down
39 changes: 39 additions & 0 deletions CRM/Core/Menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,44 @@ public static function build(&$menu) {
self::buildAdminLinks($menu);
}

/**
* Determine whether a route should canonically use a frontend or backend UI.
*
* @param string $path
* Ex: 'civicrm/contribute/transact'
* @return bool
* TRUE if the route is marked with 'is_public=1'.
* @internal
* We may wish to revise the metadata to allow more distinctions. In that case, `isPublicRoute()`
* would probably get replaced by something else.
*/
public static function isPublicRoute(string $path): bool {
// A page-view may include hundreds of links - so don't hit DB for every link. Use cache.
// In default+demo builds, the list of public routes is much smaller than the list of
// private routes (roughly 1:10; ~50 entries vs ~450 entries). Cache the smaller list.
$cache = Civi::cache('long');
$index = $cache->get('PublicRouteIndex');
if ($index === NULL) {
$routes = CRM_Core_DAO::executeQuery('SELECT id, path FROM civicrm_menu WHERE is_public = 1')
->fetchMap('id', 'path');
if (empty($routes)) {
Civi::log()->warning('isPublicRoute() should not be called before the menu has been built.');
return FALSE;
}
$index = array_fill_keys(array_values($routes), TRUE);
$cache->set('PublicRouteIndex', $index);
}

$parts = explode('/', $path);
while (count($parts) > 1) {
if (isset($index[implode('/', $parts)])) {
return TRUE;
}
array_pop($parts);
}
return FALSE;
}

/**
* This function recomputes menu from xml and populates civicrm_menu.
*
Expand All @@ -291,6 +329,7 @@ public static function store($truncate = TRUE) {
$query = 'TRUNCATE civicrm_menu';
CRM_Core_DAO::executeQuery($query);
}
Civi::cache('long')->delete('PublicRouteIndex');
$menuArray = self::items($truncate);

self::build($menuArray);
Expand Down
60 changes: 60 additions & 0 deletions CRM/Core/Smarty/plugins/block.url.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* Generate a URL. This is thin wrapper for the Civi::url() helper.
*
* @see Civi::url()
*
* Ex: Generate a backend URL.
* {url}backend://civicrm/admin?reset=1{/url}
*
* Ex: Generate a backend URL. Assign it to a Smarty variable.
* {url assign=tmpVar}backend://civicrm/admin?reset=1{/url}
*
* Ex: Generate a backend URL. Set optional flags: (t)ext, (s)sl, (a)bsolute.
* {url flags=tsa}backend://civicrm/admin?reset=1{/url}
*
* Ex: Generate a URL in the current (active) routing scheme. Add named variables. (Values are escaped).
* {url verb="Eat" target="Apples and bananas"}//civicrm/fruit?method=[verb]&data=[target]{/url}
*
* Ex: As above, but use numerical variables.
* {url 1="Eat" 2="Apples and bananas"}//civicrm/fruit?method=[1]&data=[2]{/url}
*
* Ex: Generate a URL. Add some pre-escaped variables using Smarty {$foo}.
* {assign var=myEscapedAction value="Eat"}
* {assign var=myEscapedData value="Apples+and+bananas"}
* {url}//civicrm/fruit?method={$myEscapedAction}&data={$myEscapedData}{/url}
*
* @param array $params
* The following parameters have specific meanings:
* - "assign" (string) - Assign output to a Smarty variable
* - "flags" (string) - List of options, as per `Civi::url(...$flags)`
* All other parameters will be passed-through as variables for the URL.
* @param string $text
* Contents of block.
* @param CRM_Core_Smarty $smarty
* The Smarty object.
* @return string
*/
function smarty_block_url($params, $text, &$smarty) {
if ($text === NULL) {
return NULL;
}

$flags = 'h' . ($params['flags'] ?? '');
$assign = $params['assign'] ?? NULL;
unset($params['flags'], $params['assign']);

$url = (string) Civi::url($text, $flags)->addVars($params);

// This could be neat, but see discussion in CRM_Core_Smarty_plugins_UrlTest for why it's currently off.
// $url->setVarsCallback([$smarty, 'get_template_vars']);

if ($assign !== NULL) {
$smarty->assign([$assign => $url]);
return '';
}
else {
return $url;
}
}
1 change: 1 addition & 0 deletions CRM/Core/xml/Menu/Misc.xml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
<path>civicrm/asset/builder</path>
<page_callback>\Civi\Core\AssetBuilder::pageRun</page_callback>
<access_arguments>*always allow*</access_arguments>
<is_public>1</is_public>
</item>
<item>
<path>civicrm/contribute/ajax/tableview</path>
Expand Down
36 changes: 36 additions & 0 deletions CRM/Utils/System/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,42 @@ abstract public function url(
$forceBackend = FALSE
);

/**
* Compose the URL for a page/route.
*
* @internal
* @see \Civi\Core\Url::__toString
* @param string $scheme
* Ex: 'frontend', 'backend', 'service'
* @param string $path
* Ex: 'civicrm/event/info'
* @param string|null $query
* Ex: 'id=100&msg=Hello+world'
* @return string|null
* Absolute URL, or NULL if scheme is unsupported.
* Ex: 'https://subdomain.example.com/index.php?q=civicrm/event/info&id=100&msg=Hello+world'
*/
public function getRouteUrl(string $scheme, string $path, ?string $query): ?string {
switch ($scheme) {
case 'frontend':
return $this->url($path, $query, TRUE, NULL, TRUE, FALSE, FALSE);

case 'service':
// The original `url()` didn't have an analog for "service://". But "frontend" is probably the closer bet?
// Or maybe getNotifyUrl() makes sense?
return $this->url($path, $query, TRUE, NULL, TRUE, FALSE, FALSE);

case 'backend':
return $this->url($path, $query, TRUE, NULL, FALSE, TRUE, FALSE);

// If the UF defines other major UI/URL conventions, then you might hypothetically handle
// additional schemes.

default:
return NULL;
}
}

/**
* Return the Notification URL for Payments.
*
Expand Down
85 changes: 85 additions & 0 deletions Civi.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,89 @@ public static function settings($domainID = NULL) {
return \Civi\Core\Container::getBootService('settings_manager')->getBagByDomain($domainID);
}

/**
* Construct a URL based on a logical service address. For example:
*
* Civi::url('frontend://civicrm/user?reset=1');
*
* Civi::url()
* ->setScheme('frontend')
* ->setPath(['civicrm', 'user'])
* ->setQuery(['reset' => 1])
*
* URL building follows a few rules:
*
* 1. You may initialize with a baseline URL.
* 2. The scheme indicates the general type of URL ('frontend://', 'backend://', 'asset://', 'assetBuilder://').
* 3. The result object provides getters, setters, and adders (e.g. `getScheme()`, `setPath(...)`, `addQuery(...)`)
* 4. Strings are raw. Arrays are auto-encoded. (`addQuery('name=John+Doughnut')` or `addQuery(['name' => 'John Doughnut'])`)
* 5. You may use variable expressions (`id=[contact]&gid=[profile]`).
* 6. The URL can be cast to string (aka `__toString()`).
*
* If you are converting from `CRM_Utils_System::url()` to `Civi::url()`, then be sure to:
*
* - Pay attention to the scheme (eg 'current://' vs 'frontend://')
* - Pay attention to HTML escaping, as the behavior changed:
* - Civi::url() returns plain URLs (eg "id=100&gid=200") by default
* - CRM_Utils_System::url() returns HTML-escaped URLs (eg "id=100&amp;gid=200") by default
*
* Here are several examples:
*
* Ex: Link to constituent's dashboard (on frontend UI or backend UI -- based on the active scheme of current page-view)
* $url = Civi::url('current://civicrm/user?reset=1');
* $url = Civi::url('//civicrm/user?reset=1');
*
* Ex: Link to constituent's dashboard (with method calls - good for dynamic options)
* $url = Civi::url('frontend:')
* ->setPath('civicrm/user')
* ->addQuery(['reset' => 1]);
*
* Ex: Link to constituent's dashboard (with quick flags: absolute URL, SSL required, HTML escaping)
* $url = Civi::url('frontend://civicrm/user?reset=1', 'ash');
*
* Ex: Link to constituent's dashboard (with method flags - good for dynamic options)
* $url = Civi::url('frontend://civicrm/user?reset=1')
* ->setPreferFormat('absolute')
* ->setSsl(TRUE)
* ->setHtmlEscape(TRUE);
*
* Ex: Link to a dynamically generated asset-file.
* $url = Civi::url('assetBuilder://crm-l10n.js?locale=en_US');
*
* Ex: Link to a static asset (resource-file) in one of core's configurable paths.
* $url = Civi::url('[civicrm.root]/js/Common.js');
*
* Ex: Link to a static asset (resource-file) in an extension.
* $url = Civi::url('ext://org.civicrm.search_kit/css/crmSearchTasks.css');
*
* Ex: Link with variable substitution
* $url = Civi::url('frontend://civicrm/ajax/api4/[entity]/[action]')
* ->addVars(['entity' => 'Foo', 'action' => 'bar']);
*
* @param string|null $logicalUri
* Logical URI. The scheme of the URI may be one of:
* - 'frontend://' (Front-end page-route for constituents)
* - 'backend://' (Back-end page-route for staff)
* - 'service://' (Web-service page-route for automated integrations; aka webhooks and IPNs)
* - 'current://' (Whichever UI is currently active)
* - 'default://' (Whichever UI is recorded in the metadata)
* - 'asset://' (Static asset-file; see \Civi::paths())
* - 'assetBuilder://' (Dynamically-generated asset-file; see \Civi\Core\AssetBuilder)
* - 'ext://' (Static asset-file provided by an extension)
* An empty scheme (`//hello.txt`) is equivalent to `current://hello.txt`.
* @param string|null $flags
* List of flags. Some combination of the following:
* - 'a': absolute (aka `setPreferFormat('absolute')`)
* - 'r': relative (aka `setPreferFormat('relative')`)
* - 'h': html (aka `setHtmlEscape(TRUE)`)
* - 't': text (aka `setHtmlEscape(FALSE)`)
* - 's': ssl (aka `setSsl(TRUE)`)
* - 'c': cache code for resources (aka Civi::resources()->addCacheCode())
* @return \Civi\Core\Url
* URL object which may be modified or rendered as text.
*/
public static function url(?string $logicalUri = NULL, ?string $flags = NULL): \Civi\Core\Url {
return new \Civi\Core\Url($logicalUri, $flags);
}

}
Loading