Example #1
0
 public function modelToJsClasses($parent, string $foreignKey = '')
 {
     /** @var ModelEx $model */
     $self = $parent['self'];
     $template = '';
     if ($class = $this->resolver->getModel($self['name'])) {
         $model = new $class();
         $localKey = $model->getKeyName();
         $template .= $this->modelJs->createItem($self['alias'], $parent['children'], $this->database->getColumns($model->getTable()));
         $template .= $this->modelJs->createItemArray($self['alias'], $class, $localKey, $foreignKey);
         foreach ($parent['children'] ?? [] as $child) {
             $template .= $this->modelToJsClasses($child, $localKey);
         }
     }
     return $template;
 }
Example #2
0
 public function update(UserUpdateDataEvent $event)
 {
     if ($user = $event->getUser()) {
         $fields = array_diff($this->database->getColumns($user->getTable()), ['user_id', 'ident', 'created_at', 'updated_at']);
         $userDataModel = $this->resolver->getModel('UserData', true);
         foreach ($event->getNewData() as $key => $value) {
             if (in_array($key, $fields)) {
                 if ($event->isOverwrite() || empty($user->{$key}) || $key === 'verified' && $value === 'true') {
                     $dataChanged = $dataChanged ?? $user->{$key} !== $value;
                     $user->{$key} = $key === 'password' ? password_hash($value, PASSWORD_DEFAULT) : $value;
                 }
             } elseif (!empty($userDataModel)) {
                 /** @var MUserDatum $extra */
                 $extra = $userDataModel::firstOrCreate(['user_id' => $user->user_id, 'key' => $key]);
                 if ($event->isOverwrite() || empty($extra->data)) {
                     if (!empty($value)) {
                         $extra->data = $value;
                         $extra->save();
                     } else {
                         $extra->delete();
                     }
                 }
             } else {
                 $event->setError('INVALID_FIELD');
                 throw new UserUpdateDataError("Field {$key} not found in users table");
             }
         }
         if (!empty($dataChanged)) {
             if ($user->save()) {
                 $event->setHandled(true);
             }
         } else {
             $event->setHandled(true);
         }
     } else {
         $event->setError('INVALID_USER');
     }
 }
Example #3
0
 /**
  * Automatically fills fields like created_at, updated_at, *_slug, *_safe
  * and sets unset fields to null
  *
  * @param ModelEx $model
  *
  * @return ModelEx
  */
 public function fillMissing($model)
 {
     if ($columns = $this->database->getColumns($model->getTable())) {
         foreach ($columns as $column) {
             if (!$model->{$column} && $model->{$column} !== 0) {
                 if (preg_match('/^(created_at|updated_at)$/', $column)) {
                     $model->{$column} = Carbon::now();
                 } elseif (preg_match('/(\\w+)\\_slug$/', $column, $matches)) {
                     if ($root = $model->{$matches[1]} ?? null) {
                         $pk = $model->getKeyName();
                         $slug = $copy = (new Slugify())->slugify($root);
                         $count = 1;
                         while ($record = $model::where($column, '=', $slug)->first()) {
                             if ($record->{$pk} == $model->{$pk}) {
                                 $slug = false;
                                 break;
                             }
                             $slug = sprintf('%s-%d', $copy, $count++);
                         }
                         if (!empty($slug)) {
                             $model->{$column} = $slug;
                         }
                     }
                 } elseif ($model->isNullable($column)) {
                     $model->{$column} = null;
                 }
             }
             if (preg_match('/\\_safe$/', $column)) {
                 $model->{$column} = Htmlawed::filter($model->{$column});
             } elseif (preg_match('/\\_json$/', $column) && is_array($model->{$column})) {
                 $model->{$column} = json_encode($model->{$column});
             }
         }
     }
     return $model;
 }
Example #4
0
 /**
  * Find the matching route and execute the controller
  *
  * Algorithm - http://www.minutephp.com/ADD
  *      # Get the URL and Method from the event
  *      Find the matching route by URL and method
  *          Get the controller, auth and list of models from route
  *          Throw error if the user is not authorized to access the route
  *
  *          If Method is GET
  *              Parse the models to create parent child relations
  *              Throw error if the user isn't allowed to Read the model
  *              Create a list of constraints using \
  *                  \ conditions in the route model definitions
  *                  \ conditions added using `addConstraint` method
  *              Perform additional checks if permission is SAME_USER
  *              If the controller is null guess it from the route URL
  *              If this is an ajax request with metadata then override the controller with our own GET handler
  *              Call the controller with the constraints using an event
  *
  *          If Method is POST
  *              If this is an ajax request and we have model definitions
  *                  Parse the models in the route
  *                  Get the model, and items and type of operation from the request
  *                  Throw error if the user cannot the operation on selected model
  *                  Create models instances for each item
  *                  Call the GenericPostHandler with the instances using an event
  * end.
  *
  * @param RequestEvent $requestEvent
  *
  * @throws AuthError
  * @throws ModelError
  * @throws RouteError
  * @throws ValidationError
  */
 public function handle(RequestEvent $requestEvent)
 {
     $request = $requestEvent->getRequest();
     //try {
     $parents = null;
     $method = $request->getMethod();
     $this->compile();
     //Since we can have multiple POST requests for a single Url
     if ($method === 'POST' && ($alias = $request->getParameter('alias')) && ($class = $request->getParameter('model'))) {
         $newCollection = new RouteCollection();
         foreach (['alias' => $alias, 'name' => $class] as $key => $value) {
             /** @var RouteEx $routeEx */
             foreach ($this->routeCollection as $name => $routeEx) {
                 if (in_array('POST', $routeEx->getMethods())) {
                     $parents = $routeEx->parsePostModels();
                     if ($filtered = array_filter($parents, function ($m) use($key, $value) {
                         return $m[$key] === $value;
                     })) {
                         $newCollection->add($name, $routeEx);
                     }
                 }
             }
         }
         if ($newCollection->count()) {
             $this->routeCollection = $newCollection;
         } else {
             throw new ResourceNotFoundException("No Post handler configured for alias '{$alias}' or class '{$class}'");
         }
     }
     try {
         $route = $this->match($method, $request->getPath());
     } catch (ResourceNotFoundException $e) {
         $event = new RouterEvent($method, $request->getPath());
         $this->dispatcher->fire(RouterEvent::ROUTER_GET_FALLBACK_RESOURCE, $event);
         if (!($route = $event->getRoute())) {
             throw $e;
         }
     }
     $event = new AuthEvent($route->getDefault('auth'));
     $this->lastMatchingRoute = $route;
     $this->dispatcher->fire(AuthEvent::AUTH_CHECK_ACCESS, $event);
     if ($event->isAuthorized()) {
         $user_id = $event->getActiveUserId();
         $controller = $controllerArgs = null;
         $contentType = 'html';
         if ($method === 'POST') {
             $parents = $route->parsePostModels();
             $controller = $route->getDefault('controller');
             $matches = [];
             if (empty($controller)) {
                 if (!empty($parents)) {
                     $controller = 'Generic/DefaultPostHandler.php';
                 } else {
                     $controller = trim(Str::camel(str_replace(' ', '', ucwords(preg_replace('/(\\W+)/', '\\1 ', $route->getPath())))), '/');
                 }
             }
             if (!empty($parents)) {
                 if ($alias = $request->getParameter('alias')) {
                     $matches = array_filter($parents, function ($f) use($alias) {
                         return $f['alias'] === $alias;
                     });
                 }
                 if (count($matches) === 1) {
                     $model = array_shift($matches);
                 } else {
                     if ($class = $request->getParameter('model')) {
                         $matches = array_filter($parents, function ($f) use($class) {
                             return $f['name'] === $class;
                         });
                     }
                     if (count($matches) === 1) {
                         $model = array_shift($matches);
                     } else {
                         throw new ModelError("Post route does not have a matching model for alias:'{$alias}' or class:'{$class}'");
                     }
                 }
                 /** @var ModelEx $inst */
                 if ($modelClass = $this->resolver->getModel($model['name'])) {
                     $inst = new $modelClass();
                     $cmd = $request->getParameter('cmd', 'save');
                     $pri = $inst->getKeyName();
                     $items = $request->getParameter('items', []);
                     $cols = $this->database->getColumns($inst->getTable());
                     if (!empty($items)) {
                         foreach ((array) $items as $item) {
                             $accessMode = $cmd == 'save' ? !empty($item[$pri]) ? 'update' : 'create' : 'delete';
                             $permission = call_user_func([$route, sprintf('get%sPermission', ucwords($accessMode))], $model['alias']);
                             if ($this->canAccessModel($permission, $event->isLoggedInUser())) {
                                 $fields = $model['fields'] ?? $cols;
                                 $immutable = [$pri, 'user_id'];
                                 $fields = array_diff($fields, $immutable);
                                 $has_uid = in_array('user_id', $cols);
                                 if (count(array_diff(array_keys($item), array_merge($fields, $immutable))) !== 0) {
                                     throw new ValidationError(sprintf("Field restriction on '%s' failed on fields: '%s'", $model['alias'], join(', ', array_keys($item))));
                                 }
                                 if ($accessMode === 'create') {
                                     /** @var ModelEx $m */
                                     $modelClass::unguard();
                                     $instance = new $modelClass($item);
                                 } else {
                                     if ($instance = $modelClass::where($pri, '=', $item[$pri])->first()) {
                                         if ($accessMode !== 'delete') {
                                             foreach ($fields as $field) {
                                                 $instance->{$field} = $item[$field] ?? $instance->{$field} ?? null;
                                             }
                                         }
                                     } else {
                                         throw new ModelError(sprintf("No record found in %s for '%s' = '%s'", $model['name'], $pri, $item[$pri]));
                                     }
                                 }
                                 if ($permission == Permission::SAME_USER) {
                                     if (!$instance->user_id) {
                                         $instance->user_id = $user_id;
                                     } else {
                                         if ($instance->user_id !== $user_id) {
                                             throw new ModelError(sprintf("%s user_id does not match to current user.", ucfirst($model['name'])));
                                         }
                                     }
                                 }
                                 if ($has_uid && $accessMode !== 'delete') {
                                     $instance->user_id = $instance->user_id ?: ($user_id ?: ($item['user_id'] ?: 0));
                                 }
                                 $models[] = $instance;
                             } else {
                                 throw new ModelError(sprintf("%s access denied to '%s' in %s. Enable auth or change read permission", ucwords($accessMode), $model['alias'], $route->getPath()));
                             }
                         }
                     }
                 } else {
                     throw new RouteError(sprintf("Cannot create model for class: %s in %s", $model['name'], $route->getPath()));
                 }
             }
             $controllerArgs = ['_parents' => $parents, '_models' => $models ?? null, '_mode' => $accessMode ?? $cmd ?? 'update'];
         } elseif ($method === 'GET') {
             $parents = $route->parseGetModels();
             $metadata = $request->getParameter('metadata');
             $addConstraints = function ($alias, $permission, $self) use($route, $event, $user_id) {
                 if ($canIgnore = $permission === SpecialPermission::SAME_USER_OR_IGNORE) {
                     $permission = Permission::SAME_USER;
                 }
                 $hasAccess = $this->canAccessModel($permission, $event->isLoggedInUser());
                 if (!$hasAccess && $canIgnore) {
                     $route->addConstraint($alias, function (Builder $builder) {
                         return $builder->whereRaw('1 = 0');
                     });
                 } elseif ($hasAccess) {
                     $colValue = null;
                     if ($permission === Permission::SAME_USER) {
                         $route->addConstraint($alias, ['user_id', '=', $user_id]);
                     }
                     if ($matchInfo = $self['matchInfo'] ?? null) {
                         if (!empty($matchInfo['name'])) {
                             list($name, $col, $type) = [$matchInfo['name'], $matchInfo['col'], $matchInfo['type']];
                             $colValue = $type === 'url_param' ? $route->getDefault($name) : ($type === 'var' ? ${$name} : ($type === 'string' ? $matchInfo['value'] : null));
                         }
                     }
                     if (!empty($col) && isset($colValue)) {
                         $route->addConstraint($alias, [$col, '=', $colValue]);
                     } elseif ($route->getDefault($alias) !== '*' && $permission !== Permission::SAME_USER) {
                         //we block all queries when there is no matchInfo (unless alias explicitly defaults to *)
                         $route->addConstraint($alias, function (Builder $builder) {
                             return $builder->whereRaw('1 = 0');
                         });
                     }
                 } else {
                     throw new ModelError(sprintf("Read access denied to %s in %s. Enable auth or change read permission", $alias, $route->getPath()));
                 }
             };
             $joiner = function ($parents) use(&$joiner, $route, $addConstraints) {
                 foreach ($parents['children'] as $alias => $value) {
                     $permission = $route->getJoinPermission($alias);
                     if ($permission !== Permission::EVERYONE) {
                         $addConstraints($alias, $permission, $value['self']);
                     }
                     if (!empty($value['children'])) {
                         $joiner($value);
                     }
                 }
             };
             foreach ($parents as $key => $value) {
                 $self = $value['self'];
                 if ($parentModelClass = $this->resolver->getModel($self['name'])) {
                     $parentAlias = $self['alias'];
                     $permission = $route->getReadPermission($parentAlias);
                     $addConstraints($parentAlias, $permission, $self);
                 } else {
                     throw new RouteError(sprintf("Cannot create model for class: %s in %s", $self['name'], $route->getPath()));
                 }
                 $joiner($value);
             }
             $controller = $route->getDefault('controller');
             $modifyNodeByAlias = function (&$root, $alias, $modifiers, $append) use(&$modifyNodeByAlias) {
                 foreach ($root as $parent => $child) {
                     if ($parent === $alias) {
                         foreach ($modifiers as $key => $value) {
                             if ($append) {
                                 //conditions
                                 $root[$parent]['self'][$key] = array_merge($root[$parent]['self'][$key] ?? [], $value);
                             } else {
                                 //offset, limit, etc
                                 $root[$parent]['self'][$key] = $value;
                             }
                         }
                         return true;
                     } elseif (!empty($child['children']) && ($found = $modifyNodeByAlias($root[$parent]['children'], $alias, $modifiers, $append))) {
                         return $found;
                     }
                 }
                 return false;
             };
             foreach ($route->getAllConstraints() as $alias => $constraint) {
                 if (!$modifyNodeByAlias($parents, $alias, ['conditions' => $constraint], true)) {
                     throw new ModelError("Could not find model {$alias} to add condition");
                 }
             }
             if ($controller === null) {
                 $controller = 'Generic/Page.php';
             }
             if ($request->isAjaxRequest() && !empty($metadata)) {
                 //we override controller for ajax request which have $_GET['metadata'] set
                 $metadata = json_decode($metadata, true);
                 $contentType = 'ajax';
                 foreach ($metadata ?? [] as $alias => $values) {
                     if (!$modifyNodeByAlias($parents, $alias, $values, false)) {
                         throw new ModelError("Could not find model {$alias} to apply metadata");
                     }
                 }
             }
             $controllerArgs = ['_parents' => $parents];
         }
         $defaults = array_diff_key($route->getDefaults(), array_flip(['url', 'controller', 'auth', 'models', '_route']));
         $params = array_merge($defaults, $request->getParameters());
         $event = new ControllerEvent($controller, array_merge(['_route' => $route, '_method' => $method, '_params' => $params, '_contentType' => $contentType], $params, $controllerArgs ?? []));
         $this->dispatcher->fire(ControllerEvent::CONTROLLER_EXECUTE, $event);
         $response = $event->getResponse();
     } else {
         throw new AuthError('Forbidden');
     }
     $requestEvent->setResponse($response);
 }