Autodoc
  • Namespace
  • Class
  • Tree

Namespaces

  • BlueTihi
    • Context
  • Brickrouge
    • Element
      • Nodes
    • Renderer
    • Widget
  • ICanBoogie
    • ActiveRecord
    • AutoConfig
    • CLDR
    • Composer
    • Core
    • Event
    • Exception
    • HTTP
      • Dispatcher
      • Request
    • I18n
      • Translator
    • Mailer
    • Modules
      • Taxonomy
        • Support
      • Thumbnailer
        • Versions
    • Object
    • Operation
      • Dispatcher
    • Prototype
    • Routes
    • Routing
      • Dispatcher
    • Session
  • Icybee
    • ActiveRecord
      • Model
    • ConfigOperation
    • Document
    • EditBlock
    • Element
      • ActionbarContextual
      • ActionbarSearch
      • ActionbarToolbar
    • FormBlock
    • Installer
    • ManageBlock
    • Modules
      • Articles
      • Cache
        • Collection
        • ManageBlock
      • Comments
        • ManageBlock
      • Contents
        • ManageBlock
      • Dashboard
      • Editor
        • Collection
      • Files
        • File
        • ManageBlock
      • Forms
        • Form
        • ManageBlock
      • I18n
      • Images
        • ManageBlock
      • Members
      • Modules
        • ManageBlock
      • Nodes
        • ManageBlock
        • Module
      • Pages
        • BreadcrumbElement
        • LanguagesElement
        • ManageBlock
        • NavigationBranchElement
        • NavigationElement
        • Page
        • PageController
      • Registry
      • Search
      • Seo
      • Sites
        • ManageBlock
      • Taxonomy
        • Terms
          • ManageBlock
        • Vocabulary
          • ManageBlock
      • Users
        • ManageBlock
        • NonceLogin
        • Roles
      • Views
        • ActiveRecordProvider
        • Collection
        • View
    • Operation
      • ActiveRecord
      • Constructor
      • Module
      • Widget
    • Rendering
  • None
  • Patron
  • PHP

Classes

  • BeforeControlEvent
  • BeforeProcessEvent
  • BeforeValidateEvent
  • ControlEvent
  • ControlEventBase
  • Dispatcher
  • FailureEvent
  • GetFormEvent
  • ProcessEvent
  • RescueEvent
  • Response
  • ValidateEvent
  • ValidateEventBase

Exceptions

  • Failure
  • FormHasExpired

Functions

  • array_to_xml
   1 <?php
   2 
   3 /*
   4  * This file is part of the ICanBoogie package.
   5  *
   6  * (c) Olivier Laviale <olivier.laviale@gmail.com>
   7  *
   8  * For the full copyright and license information, please view the LICENSE
   9  * file that was distributed with this source code.
  10  */
  11 
  12 namespace ICanBoogie;
  13 
  14 use ICanBoogie\HTTP;
  15 use ICanBoogie\HTTP\HTTPError;
  16 use ICanBoogie\HTTP\NotFound;
  17 use ICanBoogie\HTTP\Request;
  18 use ICanBoogie\Operation\Failure;
  19 use ICanBoogie\Operation\FailureEvent;
  20 use ICanBoogie\Operation\GetFormEvent;
  21 use ICanBoogie\Operation\FormHasExpired;
  22 use ICanBoogie\Operation\BeforeControlEvent;
  23 use ICanBoogie\Operation\ControlEvent;
  24 use ICanBoogie\Operation\BeforeValidateEvent;
  25 use ICanBoogie\Operation\ValidateEvent;
  26 use ICanBoogie\Operation\BeforeProcessEvent;
  27 use ICanBoogie\Operation\ProcessEvent;
  28 
  29 /**
  30  * An operation.
  31  *
  32  * @property ActiveRecord $record The target active record object of the operation.
  33  * @property-read Request $request The request.
  34  * @property-read bool $is_forwarded `true` if the operation is forwarded.
  35  * See {@link get_is_forwarded()}.
  36  */
  37 abstract class Operation extends Object
  38 {
  39     /**
  40      * Defines the destination of a forwarded operation.
  41      *
  42      * @var string
  43      */
  44     const DESTINATION = '_operation_destination';
  45 
  46     /**
  47      * Defines the operation name of a forwarded operation.
  48      *
  49      * @var string
  50      */
  51     const NAME = '_operation_name';
  52 
  53     /**
  54      * Defines the key of the resource targeted by the operation.
  55      *
  56      * @var string
  57      */
  58     const KEY = '_operation_key';
  59 
  60     /**
  61      * Defines the session token to be matched.
  62      *
  63      * @var string
  64      */
  65     const SESSION_TOKEN = '_session_token';
  66 
  67     const RESTFUL_BASE = '/api/';
  68     const RESTFUL_BASE_LENGTH = 5;
  69 
  70     /**
  71      * Creates a {@link Operation} instance from the specified parameters.
  72      *
  73      * @return Operation
  74      */
  75     static public function from($properties=null, array $construct_args=array(), $class_name=null)
  76     {
  77         if ($properties instanceof Request)
  78         {
  79             return static::from_request($properties);
  80         }
  81 
  82         return parent::from($properties, $construct_args, $class_name);
  83     }
  84 
  85     /**
  86      * Creates an operation instance from a request.
  87      *
  88      * An operation can be defined as a route, in which case the path of the request starts with
  89      * "/api/". An operation can also be defined using the request parameters, in which case
  90      * the {@link DESTINATION}, {@link NAME} and optionaly {@link KEY} parameters are defined
  91      * within the request parameters.
  92      *
  93      * When the operation is defined as a route, the method searches for a matching route.
  94      *
  95      * If a matching route is found, the captured parameters of the matching route are merged
  96      * with the request parameters and the method tries to create an Operation instance using the
  97      * route.
  98      *
  99      * If no matching route could be found, the method tries to extract the {@link DESTINATION},
 100      * {@link NAME} and optional {@link KEY} parameters from the route using the
 101      * `/api/:destination(/:key)/:name` pattern. If the route matches this pattern, captured
 102      * parameters are merged with the request parameters and the operation decoding continues as
 103      * if the operation was defined using parameters instead of the REST API.
 104      *
 105      * Finally, the method searches for the {@link DESTINATION}, {@link NAME} and optional
 106      * {@link KEY} aparameters within the request parameters to create the Operation instance.
 107      *
 108      * If no operation was found in the request, the method returns null.
 109      *
 110      *
 111      * Instancing using the matching route
 112      * -----------------------------------
 113      *
 114      * The matching route must define either the class of the operation instance (by defining the
 115      * `class` key) or a callback that would create the operation instance (by defining the
 116      * `callback` key).
 117      *
 118      * If the route defines the instance class, it is used to create the instance. Otherwise, the
 119      * callback is used to create the instance.
 120      *
 121      *
 122      * Instancing using the request parameters
 123      * ---------------------------------------
 124      *
 125      * The operation destination (specified by the {@link DESTINATION} parameter) is the id of the
 126      * destination module. The class and the operation name (specified by the {@link NAME}
 127      * parameter) are used to search for the corresponding operation class to create the instance:
 128      *
 129      *     ICanBoogie\<normalized_module_id>\<normalized_operation_name>Operation
 130      *
 131      * The inheritance of the module class is used the find a suitable class. For example,
 132      * these are the classes tried for the "articles" module and the "save" operation:
 133      *
 134      *     ICanBoogie\Modules\Articles\SaveOperation
 135      *     ICanBoogie\Modules\Contents\SaveOperation
 136      *     ICanBoogie\Modules\Nodes\SaveOperation
 137      *
 138      * An instance of the found class is created with the request arguments and returned. If the
 139      * class could not be found to create the operation instance, an exception is raised.
 140      *
 141      * @param string $uri The request URI.
 142      * @param array $params The request parameters.
 143      *
 144      * @throws \BadMethodCallException when the destination module or the operation name is
 145      * not defined for a module operation.
 146      *
 147      * @return Operation|null The decoded operation or null if no operation was found.
 148      */
 149     static protected function from_request(HTTP\Request $request)
 150     {
 151         global $core;
 152 
 153         $path = \ICanBoogie\Routing\decontextualize($request->path);
 154         $extension = $request->extension;
 155 
 156         if ($extension == 'json')
 157         {
 158             $path = substr($path, 0, -5);
 159             $request->headers['Accept'] = 'application/json';
 160             $request->headers['X-Requested-With'] = 'XMLHttpRequest'; // FIXME-20110925: that's not very nice
 161         }
 162         else if ($extension == 'xml')
 163         {
 164             $path = substr($path, 0, -4);
 165             $request->headers['Accept'] = 'application/xml';
 166             $request->headers['X-Requested-With'] = 'XMLHttpRequest'; // FIXME-20110925: that's not very nice
 167         }
 168 
 169         $path = rtrim($path, '/');
 170 
 171         if (substr($path, 0, self::RESTFUL_BASE_LENGTH) == self::RESTFUL_BASE)
 172         {
 173             $operation = static::from_route($request, $path);
 174 
 175             if ($operation)
 176             {
 177                 return $operation;
 178             }
 179 
 180             if ($request->is_patch)
 181             {
 182                 preg_match('#^([^/]+)/(\d+)$#', substr($path, self::RESTFUL_BASE_LENGTH), $matches);
 183 
 184                 if (!$matches)
 185                 {
 186                     throw new NotFound(format('Unknown operation %operation.', array('operation' => $path)));
 187                 }
 188 
 189                 list(, $module_id, $operation_key) = $matches;
 190 
 191                 $operation_name = 'patch';
 192             }
 193             else
 194             {
 195                 #
 196                 # We could not find a matching route, we try to extract the DESTINATION, NAME and
 197                 # optional KEY from the URI.
 198                 #
 199 
 200                 preg_match('#^([a-z\.\-]+)/(([^/]+)/)?([a-zA-Z0-9_\-]+)$#', substr($path, self::RESTFUL_BASE_LENGTH), $matches);
 201 
 202                 if (!$matches)
 203                 {
 204                     throw new NotFound(format('Unknown operation %operation.', array('operation' => $path)));
 205                 }
 206 
 207                 list(, $module_id, , $operation_key, $operation_name) = $matches;
 208             }
 209 
 210             if (empty($core->modules->descriptors[$module_id]))
 211             {
 212                 throw new NotFound(format('Unknown operation %operation.', array('operation' => $path)));
 213             }
 214 
 215             if ($operation_key)
 216             {
 217                 $request[self::KEY] = $operation_key;
 218             }
 219 
 220             return static::from_module_request($request, $module_id, $operation_name);
 221         }
 222 
 223         $module_id = $request[self::DESTINATION];
 224         $operation_name = $request[self::NAME];
 225         $operation_key = $request[self::KEY];
 226 
 227         if (!$module_id && !$operation_name)
 228         {
 229             return;
 230         }
 231         else if (!$module_id)
 232         {
 233             throw new \BadMethodCallException("The operation's destination is required.");
 234         }
 235         else if (!$operation_name)
 236         {
 237             throw new \BadMethodCallException("The operation's name is required.");
 238         }
 239 
 240         return static::from_module_request($request, $module_id, $operation_name);
 241     }
 242 
 243     /**
 244      * Tries to create an {@link Operation} instance from a route.
 245      *
 246      * @param HTTP\Request $request
 247      * @param string $path An API path.
 248      *
 249      * @throws \Exception If the route controller fails to produce an {@link Operation} instance.
 250      * @throws \InvalidArgumentException If the route's controller cannot be determined from the
 251      * route definition.
 252      *
 253      * @return Operation|null
 254      */
 255     static protected function from_route(Request $request, $path)
 256     {
 257         global $core;
 258 
 259         $route = $core->routes->find($path, $captured, $request->method, 'api');
 260 
 261         if (!$route)
 262         {
 263             return;
 264         }
 265 
 266         #
 267         # We found a matching route. The arguments captured from the route are merged with
 268         # the request parameters. The route must define either a class for the operation
 269         # instance (defined using the `class` key) or a callback to create that instance
 270         # (defined using the `callback` key).
 271         #
 272 
 273         if ($captured)
 274         {
 275             if (isset($route->param_translation_list))
 276             {
 277                 foreach ($route->param_translation_list as $from => $to)
 278                 {
 279                     $captured[$to] = $captured[$from];
 280                 }
 281             }
 282 
 283             $request->path_params = $captured;
 284             $request->params = $captured + $request->params;
 285 
 286             if (isset($request->path_params[self::DESTINATION]))
 287             {
 288                 $route->module = $request->path_params[self::DESTINATION];
 289             }
 290         }
 291 
 292         if ($route->controller)
 293         {
 294             $controller = $route->controller;
 295 
 296             if (is_callable($controller))
 297             {
 298                 $operation = call_user_func($controller, $request);
 299             }
 300             else if (!class_exists($controller, true))
 301             {
 302                 throw new \Exception("Unable to instantiate operation, class not found: $controller.");
 303             }
 304             else
 305             {
 306                 $operation = new $controller($request);
 307             }
 308 
 309             if (!($operation instanceof self))
 310             {
 311                 throw new \Exception(format
 312                 (
 313                     'The controller for the route %route failed to produce an operation object, %rc returned.', array
 314                     (
 315                         'route' => $path,
 316                         'rc' => $operation
 317                     )
 318                 ));
 319             }
 320 
 321             if (isset($route->module))
 322             {
 323                 $operation->module = $core->modules[$route->module];
 324             }
 325 
 326             if (isset($request->path_params[self::KEY]))
 327             {
 328                 $operation->key = $request->path_params[self::KEY];
 329             }
 330         }
 331         else
 332         {
 333             if ($route->callback)
 334             {
 335                 throw new \InvalidArgumentException("'callback' is no longer supported, use 'controller'.");
 336             }
 337             else if ($route->class)
 338             {
 339                 throw new \InvalidArgumentException("'class' is no longer supported, use 'controller'.");
 340             }
 341 
 342             throw new \InvalidArgumentException("'controller' is required.");
 343         }
 344 
 345         return $operation;
 346     }
 347 
 348     /**
 349      * Creates an {@link Operation} instance from a module request.
 350      *
 351      * @param Request $request
 352      * @param unknown $module_id
 353      * @param unknown $operation_name
 354      *
 355      * @throws HTTPError if the operation is not supported by the module.
 356      *
 357      * @return Operation
 358      */
 359     static protected function from_module_request(Request $request, $module_id, $operation_name)
 360     {
 361         global $core;
 362 
 363         $module = $core->modules[$module_id];
 364         $class = self::resolve_operation_class($operation_name, $module);
 365 
 366         if (!$class)
 367         {
 368             throw new HTTPError(format
 369             (
 370                 'The operation %operation is not supported by the module %module.', array
 371                 (
 372                     '%module' => (string) $module,
 373                     '%operation' => $operation_name
 374                 )
 375             ), 404);
 376         }
 377 
 378         return new $class($module);
 379     }
 380 
 381     /**
 382      * Encodes a RESTful operation.
 383      *
 384      * @param string $pattern
 385      * @param array $params
 386      *
 387      * @return string The operation encoded as a RESTful relative URL.
 388      */
 389     static public function encode($pattern, array $params=array())
 390     {
 391         $destination = null;
 392         $name = null;
 393         $key = null;
 394 
 395         if (isset($params[self::DESTINATION]))
 396         {
 397             $destination = $params[self::DESTINATION];
 398 
 399             unset($params[self::DESTINATION]);
 400         }
 401 
 402         if (isset($params[self::NAME]))
 403         {
 404             $name = $params[self::NAME];
 405 
 406             unset($params[self::NAME]);
 407         }
 408 
 409         if (isset($params[self::KEY]))
 410         {
 411             $key = $params[self::KEY];
 412 
 413             unset($params[self::KEY]);
 414         }
 415 
 416         $qs = http_build_query($params, '', '&');
 417 
 418         $rc = self::RESTFUL_BASE . strtr
 419         (
 420             $pattern, array
 421             (
 422                 '{destination}' => $destination,
 423                 '{name}' => $name,
 424                 '{key}' => $key
 425             )
 426         )
 427 
 428         . ($qs ? '?' . $qs : '');
 429 
 430         return \ICanBoogie\Routing\contextualize($rc);
 431     }
 432 
 433     /**
 434      * Resolve operation class.
 435      *
 436      * The operation class name is resolved using the inherited classes for the target and the
 437      * operation name.
 438      *
 439      * @param string $name Name of the operation.
 440      * @param Module $target Target module.
 441      *
 442      * @return string|null The resolve class name, or null if none was found.
 443      */
 444     static private function resolve_operation_class($name, Module $target)
 445     {
 446         $module = $target;
 447 
 448         while ($module)
 449         {
 450             $class = self::format_class_name($module->descriptor[Module::T_NAMESPACE], $name);
 451 
 452             if (class_exists($class, true))
 453             {
 454                 return $class;
 455             }
 456 
 457             $module = $module->parent;
 458         }
 459     }
 460 
 461     /**
 462      * Formats the specified namespace and operation name into an operation class.
 463      *
 464      * @param string $namespace
 465      * @param string $operation_name
 466      *
 467      * @return string
 468      */
 469     static public function format_class_name($namespace, $operation_name)
 470     {
 471         return $namespace . '\\' . camelize(strtr($operation_name, '-', '_')) . 'Operation';
 472     }
 473 
 474     public $key;
 475     public $destination;
 476 
 477     /**
 478      * @var \ICanBoogie\HTTP\Request The request triggering the operation.
 479      */
 480     protected $request;
 481 
 482     protected function get_request()
 483     {
 484         return $this->request;
 485     }
 486 
 487     public $response;
 488     public $method;
 489 
 490     const CONTROL_METHOD = 101;
 491     const CONTROL_SESSION_TOKEN = 102;
 492     const CONTROL_AUTHENTICATION = 103;
 493     const CONTROL_PERMISSION = 104;
 494     const CONTROL_RECORD = 105;
 495     const CONTROL_OWNERSHIP = 106;
 496     const CONTROL_FORM = 107;
 497 
 498     /**
 499      * Returns the controls to pass.
 500      *
 501      * @return array All the controls set to false.
 502      */
 503     protected function get_controls()
 504     {
 505         return array
 506         (
 507             self::CONTROL_METHOD => false,
 508             self::CONTROL_SESSION_TOKEN => false,
 509             self::CONTROL_AUTHENTICATION => false,
 510             self::CONTROL_PERMISSION => false,
 511             self::CONTROL_RECORD => false,
 512             self::CONTROL_OWNERSHIP => false,
 513             self::CONTROL_FORM => false
 514         );
 515     }
 516 
 517     /**
 518      * Getter for the {@link $record} property.
 519      *
 520      * @return ActiveRecord
 521      */
 522     protected function lazy_get_record()
 523     {
 524         return $this->module->model[$this->key];
 525     }
 526 
 527     /**
 528      * Returns the operation response.
 529      *
 530      * @return Operation\Response
 531      */
 532     protected function get_response()
 533     {
 534         return $this->response;
 535     }
 536 
 537     /**
 538      * The form object of the operation.
 539      *
 540      * @var object
 541      */
 542     protected $form;
 543 
 544     /**
 545      * Getter for the {@link $form} property.
 546      *
 547      * The operation object fires a {@link GetFormEvent} event to retrieve the form. One can listen
 548      * to the event to provide the form associated with the operation.
 549      *
 550      * One can override this method to provide the form using another method. Or simply define the
 551      * {@link $form} property to circumvent the getter.
 552      *
 553      * @return object|null
 554      */
 555     protected function lazy_get_form()
 556     {
 557         new GetFormEvent($this, $this->request, $form);
 558 
 559         return $form;
 560     }
 561 
 562     /**
 563      * @var array The properties for the operation.
 564      */
 565     protected $properties;
 566 
 567     /**
 568      * Getter for the {@link $properties} property.
 569      *
 570      * The getter should only be called during the {@link process()} method.
 571      *
 572      * @return array
 573      */
 574     protected function lazy_get_properties()
 575     {
 576         return array();
 577     }
 578 
 579     /**
 580      * Output format of the operation response.
 581      *
 582      * @var string
 583      */
 584     protected $format;
 585 
 586     /**
 587      * Target module for the operation.
 588      *
 589      * The property is set by the constructor.
 590      *
 591      * @var Module
 592      */
 593     protected $module;
 594 
 595     protected function get_module()
 596     {
 597         return $this->module;
 598     }
 599 
 600     /**
 601      * Returns `true` if the operation is forwarded.
 602      *
 603      * An operation is considered forwarded if the destination module and the operation name are
 604      * defined in the request parameters. This is usually the case for forms which are posted
 605      * on their URI but forwarded to a specified destination module.
 606      *
 607      * @return boolean
 608      */
 609     protected function get_is_forwarded()
 610     {
 611         return !empty($this->request->request_params[Operation::NAME])
 612         && !empty($this->request->request_params[Operation::DESTINATION]);
 613     }
 614 
 615     /**
 616      * Constructor.
 617      *
 618      * The {@link $controls} property is unset in order for its getters to be called on the next
 619      * access, while keeping its scope.
 620      *
 621      * @param Request $request @todo: should be a Request, but is sometimes a module.
 622      */
 623     public function __construct($request=null)
 624     {
 625         global $core;
 626 
 627         unset($this->controls);
 628 
 629         if ($request instanceof Request)
 630         {
 631             if ($request[self::DESTINATION])
 632             {
 633                 $this->module = $core->modules[$request[self::DESTINATION]];
 634             }
 635         }
 636         else if ($request instanceof Module)
 637         {
 638             $this->module = $request;
 639         }
 640     }
 641 
 642     /**
 643      * Handles the operation and prints or returns its result.
 644      *
 645      * The {@link $record}, {@link $form} and {@link $properties} properties are unset in order
 646      * for their getters to be called on the next access, while keeping their scope.
 647      *
 648      * The response object
 649      * -------------------
 650      *
 651      * The operation result is saved in a _response_ object, which may contain meta data describing
 652      * or accompanying the result. For example, the {@link Operation} class returns success and
 653      * error messages in the {@link $message} and {@link $errors} properties.
 654      *
 655      * Depending on the `Accept` header of the request, the response object can be formatted as
 656      * JSON or XML. If the `Accept` header is "application/json" the response is formatted as JSON.
 657      * If the `Accept` header is "application/xml" the response is formatted as XML. If the
 658      * `Accept` header is not of a supported type, only the result is printed, as a string.
 659      *
 660      * For API requests, the output format can also be defined by appending the corresponding
 661      * extension to the request path:
 662      *
 663      *     /api/system.nodes/12/online.json
 664      *
 665      *
 666      * The response location
 667      * ---------------------
 668      *
 669      * The `Location` header is used to ask the browser to load a different web page. This is often
 670      * used to redirect the user when an operation has been performed e.g. creating/deleting a
 671      * resource. The `location` property of the response is used to set that header. This is not
 672      * a desirable behavior for XHR because although we might want to redirect the user, we still
 673      * need to get the result of our request first. That is why when the `location` property is
 674      * set, and the request is an XHR, the location is set to the `redirect_to` field and the
 675      * `location` property is set to `null` to disable browser redirection.
 676      *
 677      *
 678      *
 679      * Control, validation and processing
 680      * ----------------------------------
 681      *
 682      * Before the operation is actually processed with the {@link process()} method, it is
 683      * controlled and validated using the {@link control()} and {@link validate()} methods. If the
 684      * control or validation fail the operation is not processed.
 685      *
 686      * The controls passed to the {@link control()} method are obtained through the
 687      * {@link $controls} property or the {@link get_controls()} getter if the property is not
 688      * accessible.
 689      *
 690      *
 691      * Events
 692      * ------
 693      *
 694      * The `failure` event is fired when the control or validation of the operation failed. The
 695      * `type` property of the event is "control" or "validation" depending on which method failed.
 696      * Note that the event won't be fired if an exception is thrown.
 697      *
 698      * The `process:before` event is fired with the operation as sender before the operation is
 699      * processed using the {@link process()} method.
 700      *
 701      * The `process` event is fired with the operation as sender after the operation has been
 702      * processed if its result is not `null`.
 703      *
 704      *
 705      * Failed operation
 706      * ----------------
 707      *
 708      * If the result of the operation is `null`, the operation is considered as failed, in which
 709      * case the status code of the response is changed to 404 and the {@link ProcessEvent} is not
 710      * fired.
 711      *
 712      * Note that exceptions are not caught by the method.
 713      *
 714      * @param HTTP\Request $request The request triggering the operation.
 715      *
 716      * @return Operation\Response The response of the operation.
 717      *
 718      * @throws Failure when the response has a client or server error, or the
 719      * {@link FormHasExpired} exception was raised.
 720      */
 721     public function __invoke(HTTP\Request $request)
 722     {
 723         $this->request = $request;
 724         $this->reset();
 725 
 726         $rc = null;
 727         $response = $this->response;
 728 
 729         try
 730         {
 731             $controls = $this->controls;
 732             $control_success = true;
 733             $control_payload = array('success' => &$control_success, 'controls' => &$controls, 'request' => $request);
 734 
 735             new BeforeControlEvent($this, $control_payload);
 736 
 737             if ($control_success)
 738             {
 739                 $control_success = $this->control($controls);
 740             }
 741 
 742             new ControlEvent($this, $control_payload);
 743 
 744             if (!$control_success)
 745             {
 746                 new FailureEvent($this, FailureEvent::TYPE_CONTROL, $request);
 747 
 748                 if (!$response->errors->count())
 749                 {
 750                     $response->errors[] = 'Operation control failed.';
 751                 }
 752             }
 753             else
 754             {
 755                 $validate_success = true;
 756                 $validate_payload = array('success' => &$validate_success, 'errors' => &$response->errors, 'request' => $request);
 757 
 758                 new BeforeValidateEvent($this, $validate_payload);
 759 
 760                 if ($validate_success)
 761                 {
 762                     $validate_success = $this->validate($response->errors);
 763                 }
 764 
 765                 new ValidateEvent($this, $validate_payload);
 766 
 767                 if (!$validate_success || $response->errors->count())
 768                 {
 769                     new FailureEvent($this, FailureEvent::TYPE_VALIDATE, $request);
 770 
 771                     if (!$response->errors->count())
 772                     {
 773                         $response->errors[] = 'Operation validation failed.';
 774                     }
 775                 }
 776                 else
 777                 {
 778                     new BeforeProcessEvent($this, array('request' => $request, 'response' => $response, 'errors' => $response->errors));
 779 
 780                     if (!$response->errors->count())
 781                     {
 782                         $rc = $this->process();
 783 
 784                         if ($rc === null && !$response->errors->count())
 785                         {
 786                             $response->errors[] = 'Operation failed (result was null).';
 787                         }
 788                     }
 789                 }
 790             }
 791         }
 792         catch (Operation\FormHasExpired $e)
 793         {
 794             log_error($e->getMessage());
 795 
 796             throw new Failure($this, $e);
 797         }
 798         catch (\Exception $e)
 799         {
 800             throw $e;
 801         }
 802 
 803         $response->rc = $rc;
 804 
 805         #
 806         # errors
 807         #
 808 
 809         if ($response->errors->count() && !$request->is_xhr && !isset($this->form))
 810         {
 811             foreach ($response->errors as $error_message)
 812             {
 813                 log_error($error_message);
 814             }
 815         }
 816 
 817         #
 818         # If the operation succeed (its result is not null), the ProcessEvent event is fired.
 819         # Listeners might use the event for further processing. For example, a _comment_ module
 820         # might delete the comments related to an _article_ module from which an article was
 821         # deleted.
 822         #
 823 
 824         if ($rc === null)
 825         {
 826             $response->status = array(400, 'Operation failed');
 827         }
 828         else
 829         {
 830             new ProcessEvent($this, array('rc' => &$response->rc, 'response' => $response, 'request' => $request));
 831         }
 832 
 833         #
 834         # We log the `message` if the request is the main request and is not an XHR.
 835         #
 836 
 837         if ($response->message && !$request->previous && !$request->is_xhr)
 838         {
 839             log_success($response->message);
 840         }
 841 
 842         #
 843         # Operation\Request rewrites the response body if the body is null, but we only want that
 844         # for XHR request, so we need to set the response body to some value, which should be
 845         # the operation result, or an empty string of the request is redirected.
 846         #
 847 
 848         if ($request->is_xhr)
 849         {
 850             $response->content_type = $request->headers['Accept'];
 851 
 852             if ($response->location)
 853             {
 854                 $response['redirect_to'] = $response->location;
 855             }
 856 
 857             $response->location = null;
 858         }
 859         else if ($response->location)
 860         {
 861             $response->body = '';
 862             $response->headers['Referer'] = $request->uri;
 863         }
 864         else if ($response->status == 304)
 865         {
 866             $response->body = '';
 867         }
 868 
 869         #
 870         # If the operation failed, we throw a Failure exception.
 871         #
 872 
 873         if ($response->is_client_error || $response->is_server_error)
 874         {
 875             throw new Failure($this);
 876         }
 877 
 878         return $response;
 879     }
 880 
 881     /**
 882      * Resets the operation state.
 883      *
 884      * A same operation object can be used multiple time to perform an operation with different
 885      * parameters, this method is invoked to reset the operation state before it is controled,
 886      * validated and processed.
 887      */
 888     protected function reset()
 889     {
 890         $this->response = new Operation\Response;
 891 
 892         unset($this->form);
 893         unset($this->record);
 894         unset($this->properties);
 895 
 896         $key = $this->request[self::KEY];
 897 
 898         if ($key)
 899         {
 900             $this->key = $key;
 901         }
 902     }
 903 
 904     /**
 905      * Controls the operation.
 906      *
 907      * A number of controls may be passed before an operation is validated and processed. Controls
 908      * are defined as an array where the key is the control identifier, and the value defines
 909      * whether the control is enabled. Controls are enabled by setting their value to true:
 910      *
 911      *     array
 912      *     (
 913      *         self::CONTROL_AUTHENTICATION => true,
 914      *         self::CONTROL_RECORD => true,
 915      *         self::CONTROL_FORM => false
 916      *     );
 917      *
 918      * Instead of a boolean, the "permission" control is enabled by a permission string or a
 919      * permission level.
 920      *
 921      *     array
 922      *     (
 923      *         self::CONTROL_PERMISSION => Module::PERMISSION_MAINTAIN
 924      *     );
 925      *
 926      * The {@link $controls} property is used to get the controls or its magic getter
 927      * {@link get_controls()} if the property is not accessible.
 928      *
 929      * Controls are passed in the following order:
 930      *
 931      * 1. CONTROL_SESSION_TOKEN
 932      *
 933      * Controls that '_session_token' is defined in $_POST and matches the current session's
 934      * token. The {@link control_session_token()} method is invoked for this control. An exception
 935      * with code 401 is thrown when the control fails.
 936      *
 937      * 2. CONTROL_AUTHENTICATION
 938      *
 939      * Controls the authentication of the user. The {@link control_authentication()} method is
 940      * invoked for this control. An exception with the code 401 is thrown when the control fails.
 941      *
 942      * 3. CONTROL_PERMISSION
 943      *
 944      * Controls the permission of the guest or user. The {@link control_permission()} method is
 945      * invoked for this control. An exception with code 401 is thrown when the control fails.
 946      *
 947      * 4. CONTROL_RECORD
 948      *
 949      * Controls the existence of the record specified by the operation's key. The
 950      * {@link control_record()} method is invoked for this control. The value returned by the
 951      * method is set in the operation object under the {@link record} property. The callback method
 952      * must throw an exception if the record could not be loaded or the control of this record
 953      * failed.
 954      *
 955      * The {@link record} property, or the {@link lazy_get_record()} getter, is used to get the
 956      * record.
 957      *
 958      * 5. CONTROL_OWNERSHIP
 959      *
 960      * Controls the ownership of the user over the record loaded during the CONTROL_RECORD step.
 961      * The {@link control_ownership()} method is invoked for the control. An exception with code
 962      * 401 is thrown if the control fails.
 963      *
 964      * 6. CONTROL_FORM
 965      *
 966      * Controls the form associated with the operation by checking its existence and validity. The
 967      * {@link control_form()} method is invoked for this control. Failing the control does not
 968      * throw an exception, but a message is logged to the debug log.
 969      *
 970      * @param array $controls The controls to pass for the operation to be processed.
 971      *
 972      * @return boolean true if all the controls pass, false otherwise.
 973      *
 974      * @throws HTTPError Depends on the control.
 975      */
 976     protected function control(array $controls)
 977     {
 978         $controls += $this->controls;
 979 
 980         $method = $controls[self::CONTROL_METHOD];
 981 
 982         if ($method && !$this->control_method($method))
 983         {
 984             throw new HTTPError
 985             (
 986                 format("The %operation operation requires the %method method.", array
 987                 (
 988                     'operation' => get_class($this),
 989                     'method' => $method
 990                 ))
 991             );
 992         }
 993 
 994         if ($controls[self::CONTROL_SESSION_TOKEN] && !$this->control_session_token())
 995         {
 996             throw new HTTPError("Session token doesn't match", 401);
 997         }
 998 
 999         if ($controls[self::CONTROL_AUTHENTICATION] && !$this->control_authentication())
1000         {
1001             throw new HTTPError
1002             (
1003                 format('The %operation operation requires authentication.', array
1004                 (
1005                     '%operation' => get_class($this)
1006                 )),
1007 
1008                 401
1009             );
1010         }
1011 
1012         if ($controls[self::CONTROL_PERMISSION] && !$this->control_permission($controls[self::CONTROL_PERMISSION]))
1013         {
1014             throw new HTTPError
1015             (
1016                 format("You don't have permission to perform the %operation operation.", array
1017                 (
1018                     '%operation' => get_class($this)
1019                 )),
1020 
1021                 401
1022             );
1023         }
1024 
1025         if ($controls[self::CONTROL_RECORD] && !$this->control_record())
1026         {
1027             throw new HTTPError
1028             (
1029                 format('Unable to retrieve record required for the %operation operation.', array
1030                 (
1031                     '%operation' => get_class($this)
1032                 ))
1033             );
1034         }
1035 
1036         if ($controls[self::CONTROL_OWNERSHIP] && !$this->control_ownership())
1037         {
1038             throw new HTTPError("You don't have ownership of the record.", 401);
1039         }
1040 
1041         if ($controls[self::CONTROL_FORM] && !$this->control_form())
1042         {
1043             log('Control %control failed for operation %operation.', array('%control' => 'form', '%operation' => get_class($this)));
1044 
1045             return false;
1046         }
1047 
1048         return true;
1049     }
1050 
1051     /**
1052      * Controls the request method.
1053      *
1054      * If the method is {@link Request::METHOD_ANY} it always matches.
1055      *
1056      * @param string $method
1057      *
1058      * @return boolean `true` if the method matches, `false` otherwise.
1059      */
1060     protected function control_method($method)
1061     {
1062         return ($method === Request::METHOD_ANY) ? true : $method === $this->request->method;
1063     }
1064 
1065     /**
1066      * Controls the session token.
1067      *
1068      * @return boolean true if the token is defined and correspond to the session token, false
1069      * otherwise.
1070      */
1071     protected function control_session_token()
1072     {
1073         global $core;
1074 
1075         $request = $this->request;
1076 
1077         return isset($request->request_params['_session_token']) && $request->request_params['_session_token'] == $core->session->token;
1078     }
1079 
1080     /**
1081      * Controls the authentication of the user.
1082      */
1083     protected function control_authentication()
1084     {
1085         global $core;
1086 
1087         return ($core->user_id != 0);
1088     }
1089 
1090     /**
1091      * Controls the permission of the user for the operation.
1092      *
1093      * @param mixed $permission The required permission.
1094      *
1095      * @return bool true if the user has the specified permission, false otherwise.
1096      */
1097     protected function control_permission($permission)
1098     {
1099         global $core;
1100 
1101         return $core->user->has_permission($permission, $this->module);
1102     }
1103 
1104     /**
1105      * Controls the ownership of the user over the operation target record.
1106      *
1107      * @return bool true if the user as ownership of the record or there is no record, false
1108      * otherwise.
1109      */
1110     protected function control_ownership()
1111     {
1112         global $core;
1113 
1114         $record = $this->record;
1115 
1116         return (!$record || $core->user->has_ownership($this->module, $record));
1117     }
1118 
1119     /**
1120      * Checks if the operation target record exists.
1121      *
1122      * The method simply returns the {@link $record} property, which calls the
1123      * {@link lazy_get_record()} getter if the property is not accessible.
1124      *
1125      * @return ActiveRecord|null
1126      */
1127     protected function control_record()
1128     {
1129         return $this->record;
1130     }
1131 
1132     /**
1133      * Control the operation's form.
1134      *
1135      * The form is retrieved from the {@link $form} property, which invokes the
1136      * {@link lazy_get_form()} getter if the property is not accessible.
1137      *
1138      * @return bool true if the form exists and validates, false otherwise.
1139      */
1140     protected function control_form()
1141     {
1142         $form = $this->form;
1143 
1144         return ($form && $form->validate($this->request->params, $this->response->errors));
1145     }
1146 
1147     /**
1148      * Validates the operation before processing.
1149      *
1150      * The method is abstract and therefore must be implemented by subclasses.
1151      *
1152      * @throws \Exception If something horribly wrong happens.
1153      *
1154      * @return bool true if the operation is valid, false otherwise.
1155      */
1156     abstract protected function validate(Errors $errors);
1157 
1158     /**
1159      * Processes the operation.
1160      *
1161      * The method is abstract and therefore must be implemented by subclasses.
1162      *
1163      * @return mixed Depends on the implementation.
1164      */
1165     abstract protected function process();
1166 }
1167 
1168 /*
1169  * Operation events
1170  */
1171 
1172 namespace ICanBoogie\Operation;
1173 
1174 use ICanBoogie\HTTP\Request;
1175 
1176 abstract class ControlEventBase extends \ICanBoogie\Event
1177 {
1178     /**
1179      * Reference to the success result of the control.
1180      *
1181      * @var bool
1182      */
1183     public $success;
1184 
1185     /**
1186      * Reference to operation controls.
1187      *
1188      * @var array
1189      */
1190     public $controls;
1191 
1192     /**
1193      * The request that triggered the operation.
1194      *
1195      * @ var \ICanBoogie\HTTP\Request
1196      */
1197     public $request;
1198 }
1199 
1200 /**
1201  * Event class for the `ICanBoogie\Operation::control:before` event.
1202  *
1203  * Third parties may use this event to alter the controls to run or clear them altogether.
1204  */
1205 class BeforeControlEvent extends ControlEventBase
1206 {
1207     /**
1208      * The event is constructed with the type `control:before`.
1209      *
1210      * @param \ICanBoogie\Operation $target
1211      * @param array $payload
1212      */
1213     public function __construct(\ICanBoogie\Operation $target, array $payload)
1214     {
1215         parent::__construct($target, 'control:before', $payload);
1216     }
1217 }
1218 
1219 /**
1220  * Event class for the `ICanBoogie\Operation::control` event.
1221  *
1222  * Third parties may use this event to alter the outcome of the control.
1223  */
1224 class ControlEvent extends ControlEventBase
1225 {
1226     /**
1227      * The event is constructed with the type `control`.
1228      *
1229      * @param \ICanBoogie\Operation $target
1230      * @param array $payload
1231      */
1232     public function __construct(\ICanBoogie\Operation $target, array $payload)
1233     {
1234         parent::__construct($target, 'control', $payload);
1235     }
1236 }
1237 
1238 abstract class ValidateEventBase extends \ICanBoogie\Event
1239 {
1240     /**
1241      * Reference the success of the validation.
1242      *
1243      * @var bool
1244      */
1245     public $success;
1246 
1247     /**
1248      * Reference to the validation errors.
1249      *
1250      * @var \ICanBoogie\Errors
1251      */
1252     public $errors;
1253 
1254     /**
1255      * Request that triggered the operation.
1256      *
1257      * @var \ICanBoogie\HTTP\Request
1258      */
1259     public $request;
1260 }
1261 
1262 /**
1263  * Event class for the `ICanBoogie\Operation::validate:before` event.
1264  */
1265 class BeforeValidateEvent extends ValidateEventBase
1266 {
1267     /**
1268      * The event is constructed with the type `validate:before`.
1269      *
1270      * @param \ICanBoogie\Operation $target
1271      * @param array $payload
1272      */
1273     public function __construct(\ICanBoogie\Operation $target, array $payload)
1274     {
1275         parent::__construct($target, 'validate:before', $payload);
1276     }
1277 }
1278 
1279 /**
1280  * Event class for the `ICanBoogie\Operation::validate` event.
1281  */
1282 class ValidateEvent extends ValidateEventBase
1283 {
1284     /**
1285      * The event is constructed with the type `validate`.
1286      *
1287      * @param \ICanBoogie\Operation $target
1288      * @param array $payload
1289      */
1290     public function __construct(\ICanBoogie\Operation $target, array $payload)
1291     {
1292         parent::__construct($target, 'validate', $payload);
1293     }
1294 }
1295 
1296 /**
1297  * Event class for the `ICanBoogie\Operation::failure` event.
1298  */
1299 class FailureEvent extends \ICanBoogie\Event
1300 {
1301     /**
1302      * The failure occured during {@link \ICanBoogie\Operation::control()}.
1303      *
1304      * @var string
1305      */
1306     const TYPE_CONTROL = 'control';
1307 
1308     /**
1309      * The failure occured during {@link \ICanBoogie\Operation::validate()}.
1310      *
1311      * @var string
1312      */
1313     const TYPE_VALIDATE = 'validate';
1314 
1315     /**
1316      * Type of failure, either {@link TYPE_CONTROL} or {@link TYPE_VALIDATION}.
1317      *
1318      * @var string
1319      */
1320     public $type;
1321 
1322     /**
1323      * The request that triggered the operation.
1324      *
1325      * @var \ICanBoogie\HTTP\Request
1326      */
1327     public $request;
1328 
1329     /**
1330      * The event is constructed with the type `failure`.
1331      *
1332      * @param \ICanBoogie\Operation $target
1333      * @param array $payload
1334      */
1335     public function __construct(\ICanBoogie\Operation $target, $type, Request $request)
1336     {
1337         $this->type = $type;
1338         $this->request = $request;
1339 
1340         parent::__construct($target, 'failure');
1341     }
1342 
1343     public function __get($property)
1344     {
1345         switch ($property)
1346         {
1347             case 'is_control': return $this->type == self::TYPE_CONTROL;
1348             case 'is_validate': return $this->type == self::TYPE_VALIDATE;
1349         }
1350 
1351         return parent::__get($property);
1352     }
1353 }
1354 
1355 /**
1356  * Event class for the `ICanBoogie\Operation::process:before` event.
1357  *
1358  * Third parties may use this event to alter the request, response or errors.
1359  */
1360 class BeforeProcessEvent extends \ICanBoogie\Event
1361 {
1362     /**
1363      * The request that triggered the operation.
1364      *
1365      * @var \ICanBoogie\HTTP\Request
1366      */
1367     public $request;
1368 
1369     /**
1370      * The response of the operation.
1371      *
1372      * @var \ICanBoogie\HTTP\Response
1373      */
1374     public $response;
1375 
1376     /**
1377      * The errors collector.
1378      *
1379      * @var \ICanBoogie\Errors
1380      */
1381     public $errors;
1382 
1383     /**
1384      * The event is constructed with the type `process:before`.
1385      *
1386      * @param \ICanBoogie\Operation $target
1387      * @param array $payload
1388      */
1389     public function __construct(\ICanBoogie\Operation $target, array $payload)
1390     {
1391         parent::__construct($target, 'process:before', $payload);
1392     }
1393 }
1394 
1395 /**
1396  * Event class for the `ICanBoogie\Operation::process` event.
1397  */
1398 class ProcessEvent extends \ICanBoogie\Event
1399 {
1400     /**
1401      * Reference to the response result property.
1402      *
1403      * @var mixed
1404      */
1405     public $rc;
1406 
1407     /**
1408      * The response object of the operation.
1409      *
1410      * @var \ICanBoogie\HTTP\Response
1411      */
1412     public $response;
1413 
1414     /**
1415      * The request that triggered the operation.
1416      *
1417      * @var \ICanBoogie\HTTP\Request
1418      */
1419     public $request;
1420 
1421     /**
1422      * The event is constructed with the type `process`.
1423      *
1424      * @param \ICanBoogie\Operation $target
1425      * @param array $payload
1426      */
1427     public function __construct(\ICanBoogie\Operation $target, array $payload)
1428     {
1429         parent::__construct($target, 'process', $payload);
1430     }
1431 }
1432 
1433 /**
1434  * Event class for the `ICanBoogie\Operation::get_form` event.
1435  */
1436 class GetFormEvent extends \ICanBoogie\Event
1437 {
1438     /**
1439      * Reference to the result variable.
1440      *
1441      * @var mixed
1442      */
1443     public $form;
1444 
1445     /**
1446      * The request that triggered the operation.
1447      *
1448      * @var \ICanBoogie\HTTP\Request
1449      */
1450     public $request;
1451 
1452     /**
1453      * The event is constructed with the type `get_form`.
1454      *
1455      * @param \ICanBoogie\Operation $target
1456      * @param array $payload
1457      */
1458     public function __construct(\ICanBoogie\Operation $target, Request $request, &$form)
1459     {
1460         $this->request = $request;
1461         $this->form = &$form;
1462 
1463         parent::__construct($target, 'get_form');
1464     }
1465 }
1466 
1467 /**
1468  * Exception thrown when the form associated with an operation has expired.
1469  *
1470  * The exception is considered recoverable, if the request is not XHR.
1471  */
1472 class FormHasExpired extends \Exception
1473 {
1474     public function __construct($message="The form associated with the request has expired.", $code=500, \Exception $previous=null)
1475     {
1476         parent::__construct($message, $code, $previous);
1477     }
1478 }
Autodoc API documentation generated by ApiGen 2.8.0