forked from civicrm/civicrm-core
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathInvoke.php
423 lines (379 loc) · 14.3 KB
/
Invoke.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
<?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 |
+--------------------------------------------------------------------+
*/
/**
*
* Given an argument list, invoke the appropriate CRM function
* Serves as a wrapper between the UserFrameWork and Core CRM
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Core_Invoke {
/**
* This is the main front-controller that integrates with the CMS. Any
* page-request that is sent to the CMS and intended for CiviCRM should
* be processed by invoke().
*
* @param array $args
* The parts of the URL which identify the intended CiviCRM page
* (e.g. array('civicrm', 'event', 'register')).
* @return string
* HTML. For non-HTML content, invoke() may call print() and exit().
*
*/
public static function invoke($args) {
try {
return self::_invoke($args);
}
catch (Exception $e) {
CRM_Core_Error::handleUnhandledException($e);
}
}
/**
* This is the same as invoke(), but it does *not* include exception
* handling.
*
* @param array $args
* The parts of the URL which identify the intended CiviCRM page
* (e.g. array('civicrm', 'event', 'register')).
* @return string
* HTML. For non-HTML content, invoke() may call print() and exit().
*/
public static function _invoke($args) {
if ($args[0] !== 'civicrm') {
return NULL;
}
// CRM-15901: Turn off PHP errors display for all ajax calls
if (($args[1] ?? NULL) == 'ajax' || !empty($_REQUEST['snippet'])) {
ini_set('display_errors', 0);
}
if (!defined('CIVICRM_SYMFONY_PATH')) {
// Traditional Civi invocation path
// may exit
self::hackMenuRebuild($args);
self::init($args);
Civi::dispatcher()->dispatch('civi.invoke.auth', \Civi\Core\Event\GenericHookEvent::create(['args' => $args]));
$item = self::getItem($args);
return self::runItem($item);
}
else {
// Symfony-based invocation path
require_once CIVICRM_SYMFONY_PATH . '/app/bootstrap.php.cache';
require_once CIVICRM_SYMFONY_PATH . '/app/AppKernel.php';
$kernel = new AppKernel('dev', TRUE);
$kernel->loadClassCache();
$response = $kernel->handle(Symfony\Component\HttpFoundation\Request::createFromGlobals());
if (preg_match(':^text/html:', $response->headers->get('Content-Type'))) {
// let the CMS handle the trappings
return $response->getContent();
}
else {
$response->send();
exit();
}
}
}
/**
* Hackish support /civicrm/menu/rebuild
*
* @param array $args
* List of path parts.
* @void
*/
public static function hackMenuRebuild($args) {
if (['civicrm', 'menu', 'rebuild'] == $args || ['civicrm', 'clearcache'] == $args) {
// ensure that the user has a good privilege level
if (CRM_Core_Permission::check('administer CiviCRM')) {
self::rebuildMenuAndCaches();
CRM_Core_Session::setStatus(ts('Cleared all CiviCRM caches (database, menu, templates)'), ts('Complete'), 'success');
// exits
return CRM_Utils_System::redirect();
}
else {
CRM_Core_Error::statusBounce(ts('You do not have permission to execute this url'));
}
}
}
/**
* Perform general setup.
*
* @param array $args
* List of path parts.
* @void
*/
public static function init($args) {
// first fire up IDS and check for bad stuff
$config = CRM_Core_Config::singleton();
// also initialize the i18n framework
require_once 'CRM/Core/I18n.php';
$i18n = CRM_Core_I18n::singleton();
}
/**
* Determine which menu $item corresponds to $args
*
* @param array $args
* List of path parts.
* @return array; see CRM_Core_Menu
*/
public static function getItem($args) {
if (is_array($args)) {
// get the menu items
$path = implode('/', $args);
}
else {
$path = $args;
}
$item = CRM_Core_Menu::get($path);
// we should try to compute menus, if item is empty and stay on the same page,
// rather than compute and redirect to dashboard.
if (!$item) {
CRM_Core_Menu::store(FALSE);
$item = CRM_Core_Menu::get($path);
}
return $item;
}
/**
* Register an alternative phar:// stream wrapper to filter out insecure Phars
*
* PHP makes it possible to trigger Object Injection vulnerabilities by using
* a side-effect of the phar:// stream wrapper that unserializes Phar
* metadata. To mitigate this vulnerability, projects such as TYPO3 and Drupal
* have implemented an alternative Phar stream wrapper that disallows
* inclusion of phar files based on certain parameters.
*
* This code attempts to register the TYPO3 Phar stream wrapper using the
* interceptor defined in \Civi\Core\Security\PharExtensionInterceptor. In an
* environment where the stream wrapper was already registered via
* \TYPO3\PharStreamWrapper\Manager (i.e. Drupal), this code does not do
* anything. In other environments (e.g. WordPress, at the time of this
* writing), the TYPO3 library is used to register the interceptor to mitigate
* the vulnerability.
*/
private static function registerPharHandler() {
try {
// try to get the existing stream wrapper, registered e.g. by Drupal
\TYPO3\PharStreamWrapper\Manager::instance();
}
catch (\LogicException $e) {
if ($e->getCode() === 1535189872) {
// no phar stream wrapper was registered by \TYPO3\PharStreamWrapper\Manager.
// This means we're probably not on Drupal and need to register our own.
\TYPO3\PharStreamWrapper\Manager::initialize(
(new \TYPO3\PharStreamWrapper\Behavior())
->withAssertion(new \Civi\Core\Security\PharExtensionInterceptor())
);
if (in_array('phar', stream_get_wrappers())) {
stream_wrapper_unregister('phar');
stream_wrapper_register('phar', \TYPO3\PharStreamWrapper\PharStreamWrapper::class);
}
}
else {
// this is not an exception we can handle
throw $e;
}
}
}
/**
* Given a menu item, call the appropriate controller and return the response
*
* @param array $item
* See CRM_Core_Menu.
* @return string, HTML
*/
public static function runItem($item) {
$ids = new CRM_Core_IDS();
$ids->check($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';
// joomla 1.5RC1 seems to push this in the POST variable, which messes
// QF and checkboxes
unset($_POST['option']);
CRM_Core_Joomla::sidebarLeft();
}
// set active Component
$template = CRM_Core_Smarty::singleton();
$template->assign('activeComponent', 'CiviCRM');
$template->assign('formTpl', 'default');
// Ensure template variables have 'something' assigned for e-notice
// prevention. These are ones that are included very often
// and not tied to a specific form.
// jsortable.tpl (datatables)
$template->assign('sourceUrl');
$template->assign('useAjax', 0);
if ($item) {
if (!array_key_exists('page_callback', $item)) {
CRM_Core_Error::debug('Bad item', $item);
CRM_Core_Error::statusBounce(ts('Bad menu record in database'));
}
// check that we are permissioned to access this page
if (!CRM_Core_Permission::checkMenuItem($item)) {
CRM_Utils_System::permissionDenied();
return NULL;
}
// check if ssl is set
if (!empty($item['is_ssl'])) {
CRM_Utils_System::redirectToSSL();
}
if (isset($item['title'])) {
CRM_Utils_System::setTitle($item['title']);
}
if (isset($item['breadcrumb']) && !isset($item['is_public'])) {
CRM_Utils_System::appendBreadCrumb($item['breadcrumb']);
}
$pageArgs = NULL;
if (!empty($item['page_arguments'])) {
$pageArgs = CRM_Core_Menu::getArrayForPathArgs($item['page_arguments']);
}
$template = CRM_Core_Smarty::singleton();
if (!empty($item['is_public'])) {
$template->assign('urlIsPublic', TRUE);
}
else {
$template->assign('urlIsPublic', FALSE);
self::statusCheck($template);
}
if (isset($item['return_url'])) {
$session = CRM_Core_Session::singleton();
$args = CRM_Utils_Array::value(
'return_url_args',
$item,
'reset=1'
);
$session->pushUserContext(CRM_Utils_System::url($item['return_url'], $args));
}
$result = NULL;
// WISHLIST: Refactor this. Instead of pattern-matching on page_callback, lookup
// page_callback via Civi\Core\Resolver and check the implemented interfaces. This
// would require rethinking the default constructor.
if (is_array($item['page_callback']) || strpos($item['page_callback'], ':')) {
$result = call_user_func(Civi\Core\Resolver::singleton()->get($item['page_callback']));
}
elseif (strpos($item['page_callback'], '_Form') !== FALSE) {
$wrapper = new CRM_Utils_Wrapper();
$result = $wrapper->run(
$item['page_callback'] ?? NULL,
$item['title'] ?? NULL,
$pageArgs ?? NULL
);
}
else {
$newArgs = explode('/', $_GET[$config->userFrameworkURLVar]);
$mode = 'null';
if (isset($pageArgs['mode'])) {
$mode = $pageArgs['mode'];
unset($pageArgs['mode']);
}
$title = $item['title'] ?? NULL;
if (strstr($item['page_callback'], '_Page') || strstr($item['page_callback'], '\\Page\\')) {
$object = new $item['page_callback']($title, $mode);
$object->urlPath = explode('/', $_GET[$config->userFrameworkURLVar]);
}
elseif (strstr($item['page_callback'], '_Controller') || strstr($item['page_callback'], '\\Controller\\')) {
$addSequence = 'false';
if (isset($pageArgs['addSequence'])) {
$addSequence = $pageArgs['addSequence'];
$addSequence = $addSequence ? 'true' : 'false';
unset($pageArgs['addSequence']);
}
$object = new $item['page_callback']($title, TRUE, $mode, NULL, $addSequence);
}
else {
throw new CRM_Core_Exception('Execute supplied menu action');
}
$result = $object->run($newArgs, $pageArgs);
}
CRM_Core_Session::storeSessionObjects();
return $result;
}
CRM_Core_Menu::store();
CRM_Core_Session::setStatus(ts('Menu has been rebuilt'), ts('Complete'), 'success');
return CRM_Utils_System::redirect();
}
/**
* This function contains the default action.
*
* Unused function.
*
* @param $action
*
* @param $contact_type
* @param $contact_sub_type
*
* @Deprecated
*/
public static function form($action, $contact_type, $contact_sub_type) {
CRM_Core_Error::deprecatedWarning('unused');
CRM_Utils_System::setUserContext(['civicrm/contact/search/basic', 'civicrm/contact/view']);
$wrapper = new CRM_Utils_Wrapper();
$properties = CRM_Core_Component::contactSubTypeProperties($contact_sub_type, 'Edit');
if ($properties) {
$wrapper->run($properties['class'], ts('New %1', [1 => $contact_sub_type]), $action, TRUE);
}
else {
$wrapper->run('CRM_Contact_Form_Contact', ts('New Contact'), $action, TRUE);
}
}
/**
* Show status in the footer (admin only)
*
* @param CRM_Core_Smarty $template
*/
public static function statusCheck($template) {
if (CRM_Core_Config::isUpgradeMode() || !CRM_Core_Permission::check('administer CiviCRM')) {
return;
}
// always use cached results - they will be refreshed by the session timer
$status = Civi::cache('checks')->get('systemStatusCheckResult');
$template->assign('footer_status_severity', $status);
$template->assign('footer_status_message', CRM_Utils_Check::toStatusLabel($status));
}
/**
* @param bool $triggerRebuild
* @param bool $sessionReset
*
* @throws Exception
*/
public static function rebuildMenuAndCaches(bool $triggerRebuild = FALSE, bool $sessionReset = FALSE): void {
$config = CRM_Core_Config::singleton();
$config->clearModuleList();
// dev/core#3660 - Activate any new classloaders/mixins/etc before re-hydrating any data-structures.
CRM_Extension_System::singleton()->getClassLoader()->refresh();
CRM_Extension_System::singleton()->getMixinLoader()->run(TRUE);
// also cleanup all caches
$config->cleanupCaches($sessionReset || CRM_Utils_Request::retrieve('sessionReset', 'Boolean', CRM_Core_DAO::$_nullObject, FALSE, 0, 'GET'));
CRM_Core_Menu::store();
// also reset navigation
CRM_Core_BAO_Navigation::resetNavigation();
// also cleanup module permissions
$config->cleanupPermissions();
// rebuild word replacement cache - pass false to prevent operations redundant with this fn
CRM_Core_BAO_WordReplacement::rebuild(FALSE);
Civi::service('settings_manager')->flush();
// Clear js caches
CRM_Core_Resources::singleton()->flushStrings()->resetCacheCode();
CRM_Case_XMLRepository::singleton(TRUE);
// also rebuild triggers if requested explicitly
if (
$triggerRebuild ||
CRM_Utils_Request::retrieve('triggerRebuild', 'Boolean', CRM_Core_DAO::$_nullObject, FALSE, 0, 'GET')
) {
Civi::service('sql_triggers')->rebuild();
// Rebuild Drupal 8/9/10 route cache only if "triggerRebuild" is set to TRUE as it's
// computationally very expensive and only needs to be done when routes change on the Civi-side.
// For example - when uninstalling an extension. We already set "triggerRebuild" to true for these operations.
$config->userSystem->invalidateRouteCache();
}
CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();
}
}