Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
controller.php
1<?php
2
3namespace Bitrix\Main\Engine;
4
30
32{
33 public const SCOPE_REST = 'rest';
34 public const SCOPE_AJAX = 'ajax';
35 public const SCOPE_CLI = 'cli';
36
37 public const EVENT_ON_BEFORE_ACTION = 'onBeforeAction';
38 public const EVENT_ON_AFTER_ACTION = 'onAfterAction';
39
40 public const ERROR_REQUIRED_PARAMETER = 'MAIN_CONTROLLER_22001';
41 public const ERROR_UNKNOWN_ACTION = 'MAIN_CONTROLLER_22002';
42
43 public const EXCEPTION_UNKNOWN_ACTION = 22002;
44
48 protected $request;
51 private Action $currentAction;
52 private array $eventHandlersIds = [
53 'prefilters' => [],
54 'postfilters' => [],
55 ];
57 private $configurationOfActions = null;
59 private string $scope;
61 private $currentUser;
63 private Converter $converter;
65 private $filePath;
67 private $sourceParametersList;
68 private $unsignedParameters;
69
70
75 final public static function className(): string
76 {
77 return static::class;
78 }
79
84 public function __construct(Request $request = null)
85 {
86 $this->scope = self::SCOPE_AJAX;
87 $this->errorCollection = new ErrorCollection;
88 $this->request = $request?: Context::getCurrent()->getRequest();
89 $this->configurator = new Configurator();
90 $this->converter = Converter::toJson();
91
92 $this->init();
93 }
94
103 public function forward($controller, string $actionName, array $parameters = null)
104 {
105 if (is_string($controller))
106 {
107 $controller = new $controller;
108 }
109
111 //propbably should refactor with ControllerBuilder::build
112
113 // override parameters
114 $controller->request = $this->getRequest();
115 $controller->setScope($this->getScope());
116 $controller->setCurrentUser($this->getCurrentUser() ?? CurrentUser::get());
117
118 $currentAction = $this->getCurrentAction();
119 $this->detachFilters($currentAction);
120
121 // run action
122 $result = $controller->run(
123 $actionName,
124 $parameters === null ? $this->getSourceParametersList() : [$parameters]
125 );
126
127 $this->attachFilters($currentAction);
128 $this->addErrors($controller->getErrors());
129
130 return $result;
131 }
132
138 protected function init()
139 {
140 $this->buildConfigurationOfActions();
141 }
142
146 final public function getConfigurationOfActions()
147 {
148 return $this->configurationOfActions;
149 }
150
157 final public function getModuleId()
158 {
159 return getModuleId($this->getFilePath());
160 }
161
162 private function getCurrentAction(): Action
163 {
164 return $this->currentAction;
165 }
166
167 private function setCurrentAction(Action $currentAction): self
168 {
169 $this->currentAction = $currentAction;
170
171 return $this;
172 }
173
174 final public function isLocatedUnderPsr4(): bool
175 {
176 // do not lower if probably psr4
177 $firstLetter = mb_substr(basename($this->getFilePath()), 0, 1);
178
179 return $firstLetter !== mb_strtolower($firstLetter);
180 }
181
182 final protected function getFilePath()
183 {
184 if (!$this->filePath)
185 {
186 $reflector = new \ReflectionClass($this);
187 $this->filePath = preg_replace('#[\\\/]+#', '/', $reflector->getFileName());
188 }
189
190 return $this->filePath;
191 }
192
203 final public function getActionUri(string $actionName, array $params = [], bool $absolute = false): Uri
204 {
205 if (strpos($this->getFilePath(), '/components/') === false)
206 {
207 return UrlManager::getInstance()->createByController($this, $actionName, $params, $absolute);
208 }
209
210 return UrlManager::getInstance()->createByComponentController($this, $actionName, $params, $absolute);
211 }
212
216 final public function getUnsignedParameters()
217 {
218 return $this->unsignedParameters;
219 }
220
221 final protected function processUnsignedParameters(): void
222 {
223 foreach ($this->getSourceParametersList() as $source)
224 {
225 $signedParameters = $source['signedParameters'] ?? null;
226 if (is_string($signedParameters))
227 {
228 try
229 {
230 $this->unsignedParameters = ParameterSigner::unsignParameters(
231 $this->getSaltToUnsign(),
232 $signedParameters
233 );
234 }
235 catch (BadSignatureException $exception)
236 {}
237
238 return;
239 }
240 }
241 }
242
248 protected function getSaltToUnsign()
249 {
250 foreach ($this->getSourceParametersList() as $source)
251 {
252 if (isset($source['c']) && is_string($source['c']))
253 {
254 return $source['c'];
255 }
256 }
257
258 return null;
259 }
260
264 final public function getCurrentUser(): ?CurrentUser
265 {
266 return $this->currentUser;
267 }
268
272 final public function setCurrentUser(CurrentUser $currentUser): void
273 {
274 $this->currentUser = $currentUser;
275 }
276
284 public function convertKeysToCamelCase($data)
285 {
286 return $this->converter->process($data);
287 }
288
293 final public function listNameActions(): array
294 {
295 $actions = array_keys($this->getConfigurationOfActions());
296 $lengthSuffix = mb_strlen(self::METHOD_ACTION_SUFFIX);
297
298 $class = new \ReflectionClass($this);
299 foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method)
300 {
301 $probablySuffix = mb_substr($method->getName(), -$lengthSuffix);
302 if ($probablySuffix === self::METHOD_ACTION_SUFFIX)
303 {
304 $actions[] = mb_strtolower(mb_substr($method->getName(), 0, -$lengthSuffix));
305 }
306 }
307
308 return array_unique($actions);
309 }
310
314 public function configureActions()
315 {
316 return [];
317 }
318
322 public function getAutoWiredParameters()
323 {
324 return [];
325 }
326
331 {
332 return null;
333 }
334
338 final public function getDefaultAutoWiredParameters()
339 {
340 return [];
341 }
342
343 private function buildConfigurationOfActions(): void
344 {
345 $this->configurationOfActions = $this->configurator->getConfigurationByController($this);
346 }
347
351 final public function getRequest()
352 {
353 return $this->request;
354 }
355
359 final public function getScope()
360 {
361 return $this->scope;
362 }
363
369 final public function setScope($scope)
370 {
371 $this->scope = $scope;
372
373 return $this;
374 }
375
379 final public function getSourceParametersList()
380 {
381 return $this->sourceParametersList;
382 }
383
389 final public function setSourceParametersList($sourceParametersList)
390 {
391 $this->sourceParametersList = $sourceParametersList;
392
393 return $this;
394 }
395
403 final public function run($actionName, array $sourceParametersList)
404 {
405 $this->collectDebugInfo();
406
407 $result = null;
408
409 try
410 {
411 $this->setSourceParametersList($sourceParametersList);
413
414 $action = $this->create($actionName);
415 if (!$action)
416 {
417 throw new SystemException("Could not create action by name {$actionName}");
418 }
419 $this->setCurrentAction($action);
420
421 $this->attachFilters($action);
422 if ($this->shouldDecodePostData($action))
423 {
424 $this->decodePostData();
425 }
426
427 if ($this->prepareParams() &&
428 $this->processBeforeAction($action) === true &&
429 $this->triggerOnBeforeAction($action) === true)
430 {
431 $result = $action->runWithSourceParametersList();
432
433 if ($action instanceof Errorable)
434 {
435 $this->errorCollection->add($action->getErrors());
436 }
437 }
438
439 $result = $this->triggerOnAfterAction($action, $result);
440 $probablyResult = $this->processAfterAction($action, $result);
441 if ($probablyResult !== null)
442 {
443 $result = $probablyResult;
444 }
445 }
446 catch (\Throwable $e)
447 {
448 $this->runProcessingThrowable($e);
449 $this->processExceptionInDebug($e);
450 }
451 finally
452 {
453 if (isset($action))
454 {
455 $this->detachFilters($action);
456 }
457 }
458
459 $this->logDebugInfo();
460
461 return $result;
462 }
463
464 protected function writeToLogException(\Throwable $e)
465 {
466 $exceptionHandler = Application::getInstance()->getExceptionHandler();
467 $exceptionHandler->writeToLog($e);
468 }
469
470 private function processExceptionInDebug(\Throwable $e)
471 {
472 if ($this->shouldWriteToLogException($e))
473 {
474 $this->writeToLogException($e);
475 }
476
477 $exceptionHandling = Configuration::getValue('exception_handling');
478 if (!empty($exceptionHandling['debug']))
479 {
480 $this->addError(new Error(ExceptionHandlerFormatter::format($e)));
481 if ($e->getPrevious())
482 {
483 $this->addError(new Error(ExceptionHandlerFormatter::format($e->getPrevious())));
484 }
485 }
486 }
487
488 private function shouldWriteToLogException(\Throwable $e): bool
489 {
490 if ($e instanceof BinderArgumentException)
491 {
492 return false;
493 }
494
495 if ($e instanceof SystemException && ($e->getCode() === self::EXCEPTION_UNKNOWN_ACTION))
496 {
497 return false;
498 }
499
500 return true;
501 }
502
503 final public static function getFullEventName($eventName): string
504 {
505 return static::class . '::' . $eventName;
506 }
507
512 final protected function collectDebugInfo(): void
513 {
514 }
515
521 final protected function logDebugInfo(): void
522 {
523 }
524
529 protected function prepareParams()
530 {
531 return true;
532 }
533
539 protected function processBeforeAction(Action $action)
540 {
541 return true;
542 }
543
544 protected function shouldDecodePostData(Action $action): bool
545 {
546 return $this->request->isPost();
547 }
548
549 final protected function decodePostData(): void
550 {
551 \CUtil::jSPostUnescape();
552 $this->request->addFilter(new PostDecodeFilter);
553 $this->request->addFilter(new FileDecodeFilter);
554 }
555
565 final protected function triggerOnBeforeAction(Action $action): bool
566 {
567 $event = new Event(
568 'main',
569 static::getFullEventName(static::EVENT_ON_BEFORE_ACTION),
570 [
571 'action' => $action,
572 'controller' => $this,
573 ]
574 );
575 $event->send($this);
576
577 $allow = true;
578 foreach ($event->getResults() as $eventResult)
579 {
580 if ($eventResult->getType() != EventResult::SUCCESS)
581 {
582 $handler = $eventResult->getHandler();
583 if ($handler instanceof Errorable)
584 {
585 $this->errorCollection->add($handler->getErrors());
586 }
587
588 $allow = false;
589 }
590 }
591
592 $this->detachPreFilters($action);
593
594 return $allow;
595 }
596
606 protected function processAfterAction(Action $action, $result)
607 {}
608
617 public function finalizeResponse(Response $response)
618 {}
619
620 final protected function triggerOnAfterAction(Action $action, $result)
621 {
622 $event = new Event(
623 'main',
624 static::getFullEventName(static::EVENT_ON_AFTER_ACTION),
625 [
626 'result' => $result,
627 'action' => $action,
628 'controller' => $this,
629 ]
630 );
631 $event->send($this);
632
633 $this->detachPostFilters($action);
634
635 return $event->getParameter('result');
636 }
637
638 final public function generateActionMethodName($action): string
639 {
640 return $action . self::METHOD_ACTION_SUFFIX;
641 }
642
643 protected function create($actionName)
644 {
645 $config = $this->getActionConfig($actionName);
646 $methodName = $this->generateActionMethodName($actionName);
647
648 if (method_exists($this, $methodName))
649 {
650 $method = new \ReflectionMethod($this, $methodName);
651 if ($method->isPublic() && mb_strtolower($method->getName()) === mb_strtolower($methodName))
652 {
653 return new InlineAction($actionName, $this, $config);
654 }
655 }
656 else
657 {
658 if (!$config && ($this instanceof Contract\FallbackActionInterface))
659 {
660 return new FallbackAction($actionName, $this, []);
661 }
662 if (!$config)
663 {
664 throw new SystemException(
665 "Could not find description of {$actionName} in {$this::className()}",
666 self::EXCEPTION_UNKNOWN_ACTION
667 );
668 }
669
670 return $this->buildActionInstance($actionName, $config);
671 }
672
673 return null;
674 }
675
676 final protected function buildActionInstance($actionName, array $config): Action
677 {
678 if (isset($config['callable']))
679 {
680 $callable = $config['callable'];
681 if (!is_callable($callable))
682 {
683 throw new ArgumentTypeException('callable', 'callable');
684 }
685
686 return new ClosureAction($actionName, $this, $callable);
687 }
688
689 if (empty($config['class']))
690 {
691 throw new SystemException(
692 "Could not find class in description of {$actionName} in {$this::className()} to create instance",
693 self::EXCEPTION_UNKNOWN_ACTION
694 );
695 }
696
698 return new $config['class']($actionName, $this, $config);
699 }
700
701 final protected function existsAction($actionName): bool
702 {
703 try
704 {
705 $action = $this->create($actionName);
706 }
707 catch (SystemException $e)
708 {
709 if ($e->getCode() !== Controller::EXCEPTION_UNKNOWN_ACTION)
710 {
711 throw $e;
712 }
713 }
714
715 return isset($action);
716 }
717
722 protected function getDefaultPreFilters()
723 {
724 return [
727 [ActionFilter\HttpMethod::METHOD_GET, ActionFilter\HttpMethod::METHOD_POST]
728 ),
729 new ActionFilter\Csrf(),
730 ];
731 }
732
737 protected function getDefaultPostFilters()
738 {
739 return [];
740 }
741
752 final protected function buildFilters(array $config = null): array
753 {
754 if ($config === null)
755 {
756 $config = [];
757 }
758
759 if (!isset($config['prefilters']))
760 {
761 $config['prefilters'] = $this->configurator->wrapFiltersClosure(
762 $this->getDefaultPreFilters()
763 );
764 }
765 if (!isset($config['postfilters']))
766 {
767 $config['postfilters'] = $this->configurator->wrapFiltersClosure(
768 $this->getDefaultPostFilters()
769 );
770 }
771
772 $hasPostMethod = $hasCsrfCheck = false;
773 foreach ($config['prefilters'] as $filter)
774 {
775 if ($filter instanceof ActionFilter\HttpMethod && $filter->containsPostMethod())
776 {
777 $hasPostMethod = true;
778 }
779 if ($filter instanceof ActionFilter\Csrf)
780 {
781 $hasCsrfCheck = true;
782 }
783 }
784
785 if ($hasPostMethod && !$hasCsrfCheck && $this->request->isPost())
786 {
787 $config['prefilters'][] = new ActionFilter\Csrf;
788 }
789
790 if (!empty($config['-prefilters']))
791 {
792 $config['prefilters'] = $this->removeFilters($config['prefilters'], $config['-prefilters']);
793 }
794
795 if (!empty($config['-postfilters']))
796 {
797 $config['postfilters'] = $this->removeFilters($config['postfilters'], $config['-postfilters']);
798 }
799
800 if (!empty($config['+prefilters']))
801 {
802 $config['prefilters'] = $this->appendFilters($config['prefilters'], $config['+prefilters']);
803 }
804
805 if (!empty($config['+postfilters']))
806 {
807 $config['postfilters'] = $this->appendFilters($config['postfilters'], $config['+postfilters']);
808 }
809
810 return $config;
811 }
812
813 final protected function appendFilters(array $filters, array $filtersToAppend): array
814 {
815 return array_merge($filters, $filtersToAppend);
816 }
817
818 final protected function removeFilters(array $filters, array $filtersToRemove): array
819 {
820 $cleanedFilters = [];
821 foreach ($filters as $filter)
822 {
823 $found = false;
824 foreach ($filtersToRemove as $filterToRemove)
825 {
826 if (is_a($filter, $filterToRemove))
827 {
828 $found = true;
829 break;
830 }
831 }
832
833 if (!$found)
834 {
835 $cleanedFilters[] = $filter;
836 }
837 }
838
839 return $cleanedFilters;
840 }
841
842 final protected function attachFilters(Action $action): void
843 {
844 $modifiedConfig = $this->buildFilters(
845 $this->getActionConfig($action->getName())
846 );
847
848 $eventManager = EventManager::getInstance();
849 foreach ($modifiedConfig['prefilters'] as $filter)
850 {
852 if (!in_array($this->getScope(), $filter->listAllowedScopes(), true))
853 {
854 continue;
855 }
856
857 $filter->bindAction($action);
858
859 $this->eventHandlersIds['prefilters'][] = $eventManager->addEventHandler(
860 'main',
861 static::getFullEventName(static::EVENT_ON_BEFORE_ACTION),
862 [$filter, 'onBeforeAction']
863 );
864 }
865
866 foreach ($modifiedConfig['postfilters'] as $filter)
867 {
869 if (!in_array($this->getScope(), $filter->listAllowedScopes(), true))
870 {
871 continue;
872 }
873
875 $filter->bindAction($action);
876
877 $this->eventHandlersIds['postfilters'][] = $eventManager->addEventHandler(
878 'main',
879 static::getFullEventName(static::EVENT_ON_AFTER_ACTION),
880 [$filter, 'onAfterAction']
881 );
882 }
883 }
884
885 final protected function detachFilters(Action $action): void
886 {
887 $this->detachPreFilters($action);
888 $this->detachPostFilters($action);
889 }
890
891 final protected function detachPreFilters(Action $action): void
892 {
893 $eventManager = EventManager::getInstance();
894 foreach ($this->eventHandlersIds['prefilters'] as $handlerId)
895 {
896 $eventManager->removeEventHandler(
897 'main',
898 static::getFullEventName(static::EVENT_ON_BEFORE_ACTION),
899 $handlerId
900 );
901 }
902
903 $this->eventHandlersIds['prefilters'] = [];
904 }
905
906 final protected function detachPostFilters(Action $action): void
907 {
908 $eventManager = EventManager::getInstance();
909 foreach ($this->eventHandlersIds['postfilters'] as $handlerId)
910 {
911 $eventManager->removeEventHandler(
912 'main',
913 static::getFullEventName(static::EVENT_ON_AFTER_ACTION),
914 $handlerId
915 );
916 }
917
918 $this->eventHandlersIds['postfilters'] = [];
919 }
920
921 final protected function getActionConfig($actionName): ?array
922 {
923 $listOfActions = array_change_key_case($this->configurationOfActions, CASE_LOWER);
924 $actionName = mb_strtolower($actionName);
925
926 if (!isset($listOfActions[$actionName]))
927 {
928 return null;
929 }
930
931 return $listOfActions[$actionName];
932 }
933
934 final protected function setActionConfig($actionName, array $config = null): self
935 {
936 $this->configurationOfActions[$actionName] = $config;
937
938 return $this;
939 }
940
941 protected function runProcessingThrowable(\Throwable $throwable)
942 {
943 if ($throwable instanceof BinderArgumentException)
944 {
945 $this->runProcessingBinderThrowable($throwable);
946 }
947 elseif ($throwable instanceof \Exception)
948 {
949 $this->runProcessingException($throwable);
950 }
951 elseif ($throwable instanceof \Error)
952 {
953 $this->runProcessingError($throwable);
954 }
955 }
956
962 protected function runProcessingException(\Exception $e)
963 {
964 // throw $e;
965 $this->errorCollection[] = $this->buildErrorFromException($e);
966 }
967
968 protected function runProcessingError(\Error $error)
969 {
970 // throw $error;
971 $this->errorCollection[] = $this->buildErrorFromPhpError($error);
972 }
973
975 {
976 $currentControllerErrors = $this->getErrors();
977 $errors = $e->getErrors();
978 if ($errors)
979 {
980 foreach ($errors as $error)
981 {
982 if (in_array($error, $currentControllerErrors, true))
983 {
984 continue;
985 }
986
987 $this->addError($error);
988 }
989 }
990 else
991 {
992 $this->runProcessingException($e);
993 }
994 }
995
996 protected function buildErrorFromException(\Exception $e)
997 {
998 if ($e instanceof ArgumentNullException)
999 {
1000 return new Error($e->getMessage(), self::ERROR_REQUIRED_PARAMETER);
1001 }
1002
1003 return new Error($e->getMessage(), $e->getCode());
1004 }
1005
1006 protected function buildErrorFromPhpError(\Error $error)
1007 {
1008 return new Error($error->getMessage(), $error->getCode());
1009 }
1010
1016 {
1017 $this->addError(new Error('User is not authorized'));
1018
1019 throw new SystemException('User is not authorized');
1020 }
1021
1027 {
1028 $this->addError(new Error('Invalid csrf token'));
1029
1030 throw new SystemException('Invalid csrf token');
1031 }
1032
1040 public function redirectTo($url): HttpResponse
1041 {
1042 return Context::getCurrent()->getResponse()->redirectTo($url);
1043 }
1044
1051 protected function addError(Error $error)
1052 {
1053 $this->errorCollection[] = $error;
1054
1055 return $this;
1056 }
1057
1064 protected function addErrors(array $errors)
1065 {
1066 $this->errorCollection->add($errors);
1067
1068 return $this;
1069 }
1070
1075 final public function getErrors()
1076 {
1077 return $this->errorCollection->toArray();
1078 }
1079
1085 final public function getErrorByCode($code)
1086 {
1087 return $this->errorCollection->getErrorByCode($code);
1088 }
1089}
static unsignParameters($componentName, $signedParameters)
static getCurrent()
Definition context.php:241
static format($exception, $htmlMode=false, $level=0)
buildFilters(array $config=null)
runProcessingThrowable(\Throwable $throwable)
shouldDecodePostData(Action $action)
processAfterAction(Action $action, $result)
buildActionInstance($actionName, array $config)
setSourceParametersList($sourceParametersList)
runProcessingException(\Exception $e)
appendFilters(array $filters, array $filtersToAppend)
__construct(Request $request=null)
processBeforeAction(Action $action)
finalizeResponse(Response $response)
triggerOnBeforeAction(Action $action)
runProcessingBinderThrowable(BinderArgumentException $e)
buildErrorFromPhpError(\Error $error)
detachPreFilters(Action $action)
forward($controller, string $actionName, array $parameters=null)
runProcessingError(\Error $error)
static getFullEventName($eventName)
getActionUri(string $actionName, array $params=[], bool $absolute=false)
run($actionName, array $sourceParametersList)
setActionConfig($actionName, array $config=null)
buildErrorFromException(\Exception $e)
triggerOnAfterAction(Action $action, $result)
setCurrentUser(CurrentUser $currentUser)
detachPostFilters(Action $action)
removeFilters(array $filters, array $filtersToRemove)
writeToLogException(\Throwable $e)