diff --git a/CRM/Admin/Page/Job.php b/CRM/Admin/Page/Job.php index a521989ae254..6423b85848ab 100644 --- a/CRM/Admin/Page/Job.php +++ b/CRM/Admin/Page/Job.php @@ -83,7 +83,7 @@ public function &links() { CRM_Core_Action::COPY => array( 'name' => ts('Copy'), 'url' => 'civicrm/admin/job', - 'qs' => 'action=copy&id=%%id%%', + 'qs' => 'action=copy&id=%%id%%&qfKey=%%key%%', 'title' => ts('Copy Scheduled Job'), ), ); @@ -122,6 +122,11 @@ public function run() { ); if (($this->_action & CRM_Core_Action::COPY) && (!empty($this->_id))) { + $key = $_POST['qfKey'] ?? $_GET['qfKey'] ?? $_REQUEST['qfKey'] ?? NULL; + $k = CRM_Core_Key::validate($key, CRM_Utils_System::getClassName($this)); + if (!$k) { + $this->invalidKey(); + } try { $jobResult = civicrm_api3('Job', 'clone', array('id' => $this->_id)); if ($jobResult['count'] > 0) { @@ -176,7 +181,7 @@ public function browse() { } $job->action = CRM_Core_Action::formLink($this->links(), $action, - ['id' => $job->id], + ['id' => $job->id, 'key' => CRM_Core_Key::get(CRM_Utils_System::getClassName($this))], ts('more'), FALSE, 'job.manage.action', diff --git a/CRM/Contribute/Page/ContributionPage.php b/CRM/Contribute/Page/ContributionPage.php index 9f96923ec30f..b6318faefa43 100644 --- a/CRM/Contribute/Page/ContributionPage.php +++ b/CRM/Contribute/Page/ContributionPage.php @@ -64,7 +64,7 @@ public static function &actionLinks() { CRM_Core_Action::COPY => array( 'name' => ts('Make a Copy'), 'url' => CRM_Utils_System::currentPath(), - 'qs' => 'action=copy&gid=%%id%%', + 'qs' => 'action=copy&gid=%%id%%&qfKey=%%key%%', 'title' => ts('Make a Copy of CiviCRM Contribution Page'), 'extra' => 'onclick = "return confirm(\'' . $copyExtra . '\');"', ), @@ -306,12 +306,14 @@ public function run() { $this->assign('CiviMember', CRM_Core_Component::isEnabled('CiviMember')); } elseif ($action & CRM_Core_Action::COPY) { - // @todo Unused local variable can be safely removed. - // But are there any side effects of CRM_Core_Session::singleton() that we - // need to preserve? - $session = CRM_Core_Session::singleton(); - CRM_Core_Session::setStatus(ts('A copy of the contribution page has been created'), ts('Successfully Copied'), 'success'); + $key = $_POST['qfKey'] ?? $_GET['qfKey'] ?? $_REQUEST['qfKey'] ?? NULL; + $k = CRM_Core_Key::validate($key, CRM_Utils_System::getClassName($this)); + if (!$k) { + $this->invalidKey(); + } + $this->copy(); + CRM_Core_Session::setStatus(ts('A copy of the contribution page has been created'), ts('Successfully Copied'), 'success'); } elseif ($action & CRM_Core_Action::DELETE) { CRM_Utils_System::appendBreadCrumb($breadCrumb); @@ -518,7 +520,7 @@ public function browse($action = NULL) { //build the normal action links. $contributions[$dao->id]['action'] = CRM_Core_Action::formLink(self::actionLinks(), $action, - array('id' => $dao->id), + ['id' => $dao->id, 'key' => CRM_Core_Key::get(CRM_Utils_System::getClassName($this))], ts('more'), TRUE, 'contributionpage.action.links', diff --git a/CRM/Core/Page.php b/CRM/Core/Page.php index 76a3e7e26e25..a0099ccf8abd 100644 --- a/CRM/Core/Page.php +++ b/CRM/Core/Page.php @@ -540,4 +540,9 @@ public function addExpectedSmartyVariables(array $elementNames): void { } } + public function invalidKey() { + $msg = ts("We can't load the requested web page. This page requires cookies to be enabled in your browser settings. Please check this setting and enable cookies (if they are not enabled). Then try again. If this error persists, contact the site administrator for assistance.") . '

' . ts('Site Administrators: This error may indicate that users are accessing this page using a domain or URL other than the configured Base URL. EXAMPLE: Base URL is http://example.org, but some users are accessing the page via http://www.example.org or a domain alias like http://myotherexample.org.') . '

' . ts('Error type: Could not find a valid session key.'); + throw new CRM_Core_Exception($msg); + } + } diff --git a/CRM/Event/Page/ManageEvent.php b/CRM/Event/Page/ManageEvent.php index a39eb17a6a32..7518052d6d1d 100644 --- a/CRM/Event/Page/ManageEvent.php +++ b/CRM/Event/Page/ManageEvent.php @@ -74,7 +74,7 @@ public function &links() { CRM_Core_Action::COPY => [ 'name' => ts('Copy'), 'url' => CRM_Utils_System::currentPath(), - 'qs' => 'reset=1&action=copy&id=%%id%%', + 'qs' => 'reset=1&action=copy&id=%%id%%&qfKey=%%key%%', 'extra' => 'onclick = "return confirm(\'' . $copyExtra . '\');"', 'title' => ts('Copy Event'), ], @@ -259,6 +259,11 @@ public function run() { return $controller->run(); } elseif ($action & CRM_Core_Action::COPY) { + $key = $_POST['qfKey'] ?? $_GET['qfKey'] ?? $_REQUEST['qfKey'] ?? NULL; + $k = CRM_Core_Key::validate($key, CRM_Utils_System::getClassName($this)); + if (!$k) { + $this->invalidKey(); + } $this->copy(); } @@ -394,7 +399,7 @@ public function browse() { ); $manageEvent[$dao->id]['action'] = CRM_Core_Action::formLink(self::links(), $action, - ['id' => $dao->id], + ['id' => $dao->id, 'key' => CRM_Core_Key::get(CRM_Utils_System::getClassName($this))], ts('more'), TRUE, 'event.manage.list', diff --git a/CRM/Financial/Form/BatchTransaction.php b/CRM/Financial/Form/BatchTransaction.php index e93585cb5669..bb7d8bfb63a0 100644 --- a/CRM/Financial/Form/BatchTransaction.php +++ b/CRM/Financial/Form/BatchTransaction.php @@ -68,6 +68,7 @@ public function preProcess() { $this->assign('columnHeaders', $columnHeaders); } $this->assign('batchStatus', $this->_batchStatus); + $this->assign('financialAJAXQFKey', CRM_Core_key::get('CRM_Financial_Page_AJAX')); } /** diff --git a/CRM/Financial/Page/AJAX.php b/CRM/Financial/Page/AJAX.php index c76c45332ba7..6caaa2d687cb 100644 --- a/CRM/Financial/Page/AJAX.php +++ b/CRM/Financial/Page/AJAX.php @@ -142,6 +142,11 @@ public static function jqFinancialType($config) { public static function assignRemove() { $op = CRM_Utils_Type::escape($_POST['op'], 'String'); $recordBAO = CRM_Utils_Type::escape($_POST['recordBAO'], 'String'); + $key = CRM_Utils_Request::retrieveValue('qfKey', 'String'); + $k = CRM_Core_Key::validate($key, 'CRM_Financial_Page_AJAX'); + if (!$k) { + CRM_Utils_JSON::output(['status' => 'invalid key']); + } foreach ($_POST['records'] as $record) { $recordID = CRM_Utils_Type::escape($record, 'Positive', FALSE); if ($recordID) { diff --git a/CRM/Financial/Page/Batch.php b/CRM/Financial/Page/Batch.php index 30bbdad3665b..077783082ab9 100644 --- a/CRM/Financial/Page/Batch.php +++ b/CRM/Financial/Page/Batch.php @@ -81,6 +81,7 @@ public function userContext($mode = NULL) { public function browse() { $status = CRM_Utils_Request::retrieve('status', 'Positive', CRM_Core_DAO::$_nullObject, FALSE, 1); $this->assign('status', $status); + $this->assign('financialAJAXQFKey', CRM_Core_Key::get('CRM_Financial_Page_AJAX')); $this->search(); } diff --git a/CRM/Financial/Page/FinancialBatch.php b/CRM/Financial/Page/FinancialBatch.php index 3a6e08ca5592..76bb06a33fe8 100644 --- a/CRM/Financial/Page/FinancialBatch.php +++ b/CRM/Financial/Page/FinancialBatch.php @@ -72,6 +72,7 @@ public function run() { ) { $this->edit($this->_action, $id); } + $this->assign('financialAJAXQFKey', CRM_Core_Key::get('CRM_Financial_Page_AJAX')) // parent run return CRM_Core_Page::run(); } diff --git a/CRM/Price/Page/Set.php b/CRM/Price/Page/Set.php index c88e4a8d40fa..3263943e8a5d 100644 --- a/CRM/Price/Page/Set.php +++ b/CRM/Price/Page/Set.php @@ -83,7 +83,7 @@ public function &actionLinks() { CRM_Core_Action::COPY => [ 'name' => ts('Copy Price Set'), 'url' => CRM_Utils_System::currentPath(), - 'qs' => 'action=copy&sid=%%sid%%', + 'qs' => 'action=copy&sid=%%sid%%&qfKey=%%key%%', 'title' => ts('Make a Copy of Price Set'), 'extra' => 'onclick = "return confirm(\'' . $copyExtra . '\');"', ], @@ -126,6 +126,11 @@ public function run() { $this->preview($sid); } elseif ($action & CRM_Core_Action::COPY) { + $key = $_POST['qfKey'] ?? $_GET['qfKey'] ?? $_REQUEST['qfKey'] ?? NULL; + $k = CRM_Core_Key::validate($key, CRM_Utils_System::getClassName($this)); + if (!$k) { + $this->invalidKey(); + } CRM_Core_Session::setStatus(ts('A copy of the price set has been created'), ts('Saved'), 'success'); $this->copy(); } @@ -280,7 +285,7 @@ public function browse($action = NULL) { $actionLinks[CRM_Core_Action::BROWSE]['name'] = ts('View Price Fields'); } $priceSet[$dao->id]['action'] = CRM_Core_Action::formLink($actionLinks, $action, - ['sid' => $dao->id], + ['sid' => $dao->id, 'key' => CRM_Core_Key::get(CRM_Utils_System::getClassName($this))], ts('more'), FALSE, 'priceSet.row.actions', diff --git a/CRM/UF/Page/Group.php b/CRM/UF/Page/Group.php index c3b35bc0bb5e..30ab00871546 100644 --- a/CRM/UF/Page/Group.php +++ b/CRM/UF/Page/Group.php @@ -103,7 +103,7 @@ public static function &actionLinks() { CRM_Core_Action::COPY => [ 'name' => ts('Copy'), 'url' => 'civicrm/admin/uf/group', - 'qs' => 'action=copy&gid=%%id%%', + 'qs' => 'action=copy&gid=%%id%%&qfKey=%%key%%', 'title' => ts('Make a Copy of CiviCRM Profile Group'), 'extra' => 'onclick = "return confirm(\'' . $copyExtra . '\');"', ], @@ -166,6 +166,11 @@ public function run() { $this->preview($id, $action); } elseif ($action & CRM_Core_Action::COPY) { + $key = $_POST['qfKey'] ?? $_GET['qfKey'] ?? $_REQUEST['qfKey'] ?? NULL; + $k = CRM_Core_Key::validate($key, CRM_Utils_System::getClassName($this)); + if (!$k) { + $this->invalidKey(); + } $this->copy(); } // finally browse the uf groups @@ -349,7 +354,7 @@ public function browse($action = NULL) { $ufGroup[$id]['group_type'] = self::formatGroupTypes($groupTypes); $ufGroup[$id]['action'] = CRM_Core_Action::formLink(self::actionLinks(), $action, - ['id' => $id], + ['id' => $id, 'key' => CRM_Core_Key::get(CRM_Utils_System::getClassName($this))], ts('more'), FALSE, 'ufGroup.row.actions', diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index ade9286e2609..2e27de4436fa 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -76,6 +76,12 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { */ private $_afform; + /** + * @var array + * Ex: ['civicrm/foo/bar?id=[id]&widget=gizmo' => 'CRMFooBar1234abcd1234abcd'] + */ + private $_qfKeys = []; + /** * Override execute method to change the result object type * @return \Civi\Api4\Result\SearchDisplayRunResult @@ -405,9 +411,13 @@ private function formatFieldLinks($column, $data, $value): array { $path = $this->getLinkPath($column['link'], $data, $index); $path = $this->replaceTokens($path, $data, 'url', $index); if ($path) { + $query = []; + if (($column['link']['csrf'] ?? NULL) === 'qfKey' && $column['link']['path']) { + $query['qfKey'] = $this->getQfKey($column['link']['path']); + } $link = [ 'text' => $val, - 'url' => $this->getUrl($path), + 'url' => $this->getUrl($path, $query), ]; if (!empty($column['link']['target'])) { $link['target'] = $column['link']['target']; @@ -435,9 +445,13 @@ private function formatLinksColumn($column, $data): array { } $path = $this->replaceTokens($this->getLinkPath($item, $data), $data, 'url'); if ($path) { + $query = []; + if (($item['csrf'] ?? NULL) === 'qfKey' && $item['path']) { + $query['qfKey'] = $this->getQfKey($item['path']); + } $link = [ 'text' => $this->replaceTokens($item['text'] ?? '', $data, 'view'), - 'url' => $this->getUrl($path), + 'url' => $this->getUrl($path, $query), ]; foreach (['target', 'style', 'icon'] as $prop) { if (!empty($item[$prop])) { @@ -1260,4 +1274,27 @@ private function loadSearchDisplay(): void { } } + /** + * @param string $pathExpr + * Path formula. Should specify an explicit path. + * Ex: 'civicrm/foo/bar?id=[id]&widget=gizmo` + * @return string|null + */ + private function getQfKey(string $pathExpr): ?string { + if (isset($this->_qfKeys[$pathExpr])) { + // No point re-computing this for 100x links per page-view - same value works. + return $this->_qfKeys[$pathExpr]; + } + $result = NULL; + if ($routeName = parse_url($pathExpr, PHP_URL_PATH)) { + if ($routeItem = \CRM_Core_Menu::get($routeName)) { + if (!empty($routeItem['page_callback'])) { + $result = \CRM_Core_Key::get($routeItem['page_callback']); + } + } + } + $this->_qfKeys[$pathExpr] = $result; + return $result; + } + } diff --git a/templates/CRM/Financial/Form/Search.tpl b/templates/CRM/Financial/Form/Search.tpl index 53301668cf95..52d9a0798807 100644 --- a/templates/CRM/Financial/Form/Search.tpl +++ b/templates/CRM/Financial/Form/Search.tpl @@ -216,7 +216,7 @@ CRM.$(function($) { } function saveRecords(records, op) { - var postUrl = CRM.url('civicrm/ajax/rest', 'className=CRM_Financial_Page_AJAX&fnName=assignRemove'); + var postUrl = CRM.url('civicrm/ajax/rest', 'className=CRM_Financial_Page_AJAX&fnName=assignRemove&qfKey={/literal}{$financialAJAXQFKey}{literal}'); //post request and get response $.post(postUrl, {records: records, recordBAO: 'CRM_Batch_BAO_Batch', op: op, key: {/literal}"{crmKey name='civicrm/ajax/ar'}"{literal}}, function(response) { diff --git a/templates/CRM/Financial/Page/BatchTransaction.tpl b/templates/CRM/Financial/Page/BatchTransaction.tpl index 1b1519723ece..4077b7f91f37 100644 --- a/templates/CRM/Financial/Page/BatchTransaction.tpl +++ b/templates/CRM/Financial/Page/BatchTransaction.tpl @@ -123,7 +123,7 @@ function saveRecord(recordID, op, recordBAO, entityID) { window.location.href = CRM.url('civicrm/financial/batch/export', {reset: 1, id: recordID, status: 1}); return; } - var postUrl = {/literal}"{crmURL p='civicrm/ajax/rest' h=0 q='className=CRM_Financial_Page_AJAX&fnName=assignRemove'}"{literal}; + var postUrl = {/literal}"{crmURL p='civicrm/ajax/rest' h=0 q="className=CRM_Financial_Page_AJAX&fnName=assignRemove&qfKey={$financialAJAXQFKey}"}"{literal}; //post request and get response CRM.$.post( postUrl, { records: [recordID], recordBAO: recordBAO, op:op, entityID:entityID, key: {/literal}"{crmKey name='civicrm/ajax/ar'}"{literal} }, function( html ){ //this is custom status set when record update success.