@@ -96,6 +96,26 @@ public static function useSession($sessionProvider = null) {
9696 self ::$ config ['sessionProvider ' ] = (isset ($ sessionProvider ) ? $ sessionProvider : true );
9797 }
9898
99+ /**
100+ * Allows to manage HTTP request validators.
101+ * By default, all validators are enabled. With this method, you can disable certain validators.
102+ *
103+ * WARNING: Disabling HTTP requests validation can jeopardize your site and users of your site.
104+ * For security reasons, it is not recommended to disable HTTP request validation.
105+ *
106+ * @param array|bool $validators List of validators to use or disable.
107+ * For example:
108+ * array(
109+ * 'crossSiteScripting' => false, // disable CrossSiteScriptingValidation
110+ * 'actionName' => false // disable action name validation
111+ * )
112+ *
113+ * @return void
114+ */
115+ public static function useValidation ($ validators ) {
116+ self ::$ config ['validators ' ] = $ validators ;
117+ }
118+
99119 /**
100120 * Sets custom handlers.
101121 *
@@ -164,19 +184,29 @@ public static function routes($routes) {
164184 * @return void
165185 */
166186 public static function build () {
167- self ::init ();
168- self ::include ();
169-
170- $ route = self ::canRoute ();
171-
172- if ($ route !== false ) {
173- $ actionContext = self ::actionContext ($ route );
187+ try {
188+ self ::init ();
189+ self ::include ();
190+
191+ $ route = self ::canRoute ();
192+
193+ if ($ route !== false ) {
194+ $ actionContext = self ::actionContext ($ route );
195+
196+ if ($ actionContext !== null && !self ::cached ($ actionContext )) {
197+ self ::filters ($ actionContext );
198+ self ::headers ();
199+ self ::validation ();
200+ self ::render ($ actionContext );
201+ }
202+ }
203+ }
204+ catch (\Exception $ ex ) {
205+ $ errorHandlerEventArgs = new ErrorHandlerEventArgs ($ ex );
206+ self ::invokeAll (self ::$ appContext ->getErrorHandler (), array ($ errorHandlerEventArgs ));
174207
175- if ($ actionContext !== null && !self ::cached ($ actionContext )) {
176- self ::filters ($ actionContext );
177- self ::headers ();
178- self ::validation ();
179- self ::render ($ actionContext );
208+ if ($ errorHandlerEventArgs ->getHandled () !== true ) {
209+ throw $ ex ;
180210 }
181211 }
182212 }
@@ -423,6 +453,12 @@ private static function actionContext($route) {
423453
424454 InternalHelper::setPropertyValue ($ actionContext , 'controller ' , $ controllerInstance );
425455
456+ if (!method_exists ($ controllerInstance , PHPMVC_ACTION )) {
457+ $ response ->setStatusCode (404 );
458+ $ response ->end ();
459+ return null ;
460+ }
461+
426462 // get and set model annotations
427463 $ annotations = InternalHelper::getStaticPropertyValue ('\\PhpMvc \\Model ' , 'annotations ' );
428464 $ modelState = $ actionContext ->getModelState ();
@@ -448,6 +484,9 @@ private static function actionContext($route) {
448484 // arguments and model state
449485 self ::makeActionState ($ actionContext );
450486
487+ // invoke custom handlers
488+ self ::invokeAll (self ::$ appContext ->getActionContextInit (), array ($ actionContext ));
489+
451490 return $ actionContext ;
452491 }
453492
@@ -803,8 +842,62 @@ private static function filters($actionContext) {
803842 * @return void
804843 */
805844 private static function validation () {
806- if (substr (PHPMVC_ACTION , 0 , 2 ) == '__ ' ) {
807- throw new \Exception ('Action names can not begin with a "_". ' );
845+ if (isset (self ::$ config ['validators ' ])) {
846+ $ validators = self ::$ config ['validators ' ];
847+ }
848+ else {
849+ $ validators = true ;
850+ }
851+
852+ // action name validation
853+ if ($ validators === true || (!isset ($ validators ['actionName ' ]) || $ validators ['actionName ' ] === true )) {
854+ if (substr (PHPMVC_ACTION , 0 , 2 ) == '__ ' ) {
855+ throw new ActionNameValidationException ();
856+ }
857+ }
858+
859+ // request verification token
860+ if ($ validators === true || (!isset ($ validators ['antiForgeryToken ' ]) || $ validators ['antiForgeryToken ' ] === true )) {
861+ if (($ request = self ::$ config ['httpContext ' ]->getRequest ())->isPost ()) {
862+ $ post = $ request ->post ();
863+ $ expected = $ request ->cookies ('__requestVerificationToken ' );
864+
865+ if (!isset ($ post )) { $ post = array (); }
866+
867+ if ((isset ($ expected ) && (!isset ($ post ['__requestVerificationToken ' ]) || $ post ['__requestVerificationToken ' ] != $ expected )) || (isset ($ post ['__requestVerificationToken ' ]) && empty ($ expected ))) {
868+ throw new HttpAntiForgeryException ();
869+ }
870+ }
871+ }
872+
873+ // cross site scripting validation
874+ if ($ validators === true || (!isset ($ validators ['crossSiteScripting ' ]) || $ validators ['crossSiteScripting ' ] === true )) {
875+ $ request = self ::$ config ['httpContext ' ]->getRequest ();
876+
877+ $ get = $ request ->get ();
878+ $ post = $ request ->post ();
879+
880+ if (!isset ($ get )) { $ get = array (); }
881+ if (!isset ($ post )) { $ post = array (); }
882+
883+ $ items = array_merge ($ get , $ post );
884+
885+ foreach ($ items as $ key => $ value ) {
886+ $ isDangerousString = CrossSiteScriptingValidation::IsDangerousString ($ key ) || CrossSiteScriptingValidation::IsDangerousString ($ value );
887+
888+ if ($ isDangerousString ) {
889+ $ source = null ;
890+
891+ if (!empty ($ get [$ key ])) {
892+ $ source = '$_GET ' ;
893+ }
894+ elseif (!empty ($ post [$ key ])) {
895+ $ source = '$_POST ' ;
896+ }
897+
898+ throw new HttpRequestValidationException ($ source , $ key , $ value );
899+ }
900+ }
808901 }
809902 }
810903
@@ -927,8 +1020,9 @@ private static function makeActionState($actionContext) {
9271020 $ hasModel = false ;
9281021 $ request = $ actionContext ->getHttpContext ()->getRequest ();
9291022
930- // route values
1023+ // route
9311024 $ route = $ actionContext ->getRoute ();
1025+ $ segments = $ route ->getSegments (true );
9321026
9331027 // params of get http
9341028 // TODO: подумать о возможности управлять этим
@@ -937,27 +1031,8 @@ private static function makeActionState($actionContext) {
9371031 // change case of keys
9381032 $ get = array_change_key_case ($ get , CASE_LOWER );
9391033
940- // params of post
1034+ // post
9411035 $ isPost = $ request ->isPost ();
942- $ postData = $ isPost ? $ request ->post () : null ;
943- $ post = null ;
944-
945- if (!empty ($ postData )) {
946- $ post = InternalHelper::arrayToObject ($ postData );
947- }
948-
949- if (empty ($ post ) && $ isPost ) {
950- $ contentType = $ request ->contentType ();
951-
952- if (strrpos ($ contentType , '/json ' ) !== false ) {
953- $ requestBody = file_get_contents ('php://input ' );
954-
955- $ post = json_decode ($ requestBody );
956- }
957- else {
958- // TODO...
959- }
960- }
9611036
9621037 // get params of action method
9631038 $ r = new \ReflectionMethod (get_class ($ actionContext ->getController ()), $ actionContext ->getActionName ());
@@ -976,14 +1051,49 @@ private static function makeActionState($actionContext) {
9761051 }
9771052 else {
9781053 // parameter not found
979- if ($ isPost && !$ hasModel && !isset ($ get [$ name ])) {
1054+ if ($ isPost && !$ hasModel && !isset ($ get [$ name ]) && !isset ($ segments [$ name ])) {
1055+ $ postData = $ request ->post ();
1056+ $ paramTypeName = '\stdClass ' ;
1057+ $ post = null ;
1058+
1059+ if (isset ($ postData ) && isset ($ postData ['__requestVerificationToken ' ])) {
1060+ unset($ postData ['__requestVerificationToken ' ]);
1061+ }
1062+
1063+ if ($ param ->hasType ()) {
1064+ if ($ param ->getType ()->isBuiltin ()) {
1065+ $ arguments [$ name ] = ($ param ->isOptional () ? $ param ->getDefaultValue () : null );
1066+ continue ;
1067+ }
1068+
1069+ if (($ paramTypeName = $ param ->getClass ()) !== null ) {
1070+ $ paramTypeName = $ paramTypeName ->getName ();
1071+ }
1072+ }
1073+
1074+ if (!empty ($ postData )) {
1075+ InternalHelper::arrayToObject ($ postData , $ post , $ paramTypeName );
1076+ }
1077+
1078+ if (empty ($ post )) {
1079+ $ contentType = $ request ->contentType ();
1080+
1081+ if (strrpos ($ contentType , '/json ' ) !== false ) {
1082+ $ requestBody = file_get_contents ('php://input ' );
1083+ InternalHelper::arrayToObject (json_decode ($ requestBody , true ), $ post , $ paramTypeName );
1084+ }
1085+ else {
1086+ // TODO...
1087+ }
1088+ }
1089+
9801090 // post method and model not yet received
9811091 if ($ post == null ) {
9821092 throw new \Exception ('" ' . $ name . ' is required. ' );
9831093 }
9841094
9851095 $ arguments [$ name ] = $ post ;
986-
1096+
9871097 // parse post data
9881098 foreach (get_object_vars ($ post ) as $ key => $ value ) {
9891099 $ modelState [$ key ] = new ModelStateEntry ($ key , $ value );
0 commit comments