source: trunk/include/ws_core.inc.php @ 25077

Last change on this file since 25077 was 25077, checked in by mistic100, 11 years ago

feature:2982 API: add high-level type check
introduces some constants fro bool, int, float, positive and notnull parameters
types are tested in PwgServer::invoke and no in each method
+ some optimizations + update methods descriptions

  • Property svn:eol-style set to LF
File size: 18.9 KB
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based photo gallery                                    |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008-2013 Piwigo Team                  http://piwigo.org |
6// | Copyright(C) 2003-2008 PhpWebGallery Team    http://phpwebgallery.net |
7// | Copyright(C) 2002-2003 Pierrick LE GALL   http://le-gall.net/pierrick |
8// +-----------------------------------------------------------------------+
9// | This program is free software; you can redistribute it and/or modify  |
10// | it under the terms of the GNU General Public License as published by  |
11// | the Free Software Foundation                                          |
12// |                                                                       |
13// | This program is distributed in the hope that it will be useful, but   |
14// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
15// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
16// | General Public License for more details.                              |
17// |                                                                       |
18// | You should have received a copy of the GNU General Public License     |
19// | along with this program; if not, write to the Free Software           |
20// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
21// | USA.                                                                  |
22// +-----------------------------------------------------------------------+
23
24/**** WEB SERVICE CORE CLASSES************************************************
25 * PwgServer - main object - the link between web service methods, request
26 *  handler and response encoder
27 * PwgRequestHandler - base class for handlers
28 * PwgResponseEncoder - base class for response encoders
29 * PwgError, PwgNamedArray, PwgNamedStruct - can be used by web service functions
30 * as return values
31 */
32
33
34define( 'WS_PARAM_ACCEPT_ARRAY',  0x010000 );
35define( 'WS_PARAM_FORCE_ARRAY',   0x030000 );
36define( 'WS_PARAM_OPTIONAL',      0x040000 );
37
38define( 'WS_TYPE_BOOL',           0x01 );
39define( 'WS_TYPE_INT',            0x02 );
40define( 'WS_TYPE_FLOAT',          0x04 );
41define( 'WS_TYPE_POSITIVE',       0x10 );
42define( 'WS_TYPE_NOTNULL',        0x20 );
43define( 'WS_TYPE_ID', WS_TYPE_INT | WS_TYPE_POSITIVE | WS_TYPE_NOTNULL);
44
45define( 'WS_ERR_INVALID_METHOD',  501 );
46define( 'WS_ERR_MISSING_PARAM',   1002 );
47define( 'WS_ERR_INVALID_PARAM',   1003 );
48
49define( 'WS_XML_ATTRIBUTES', 'attributes_xml_');
50
51/**
52 * PwgError object can be returned from any web service function implementation.
53 */
54class PwgError
55{
56  private $_code;
57  private $_codeText;
58
59  function PwgError($code, $codeText)
60  {
61    if ($code>=400 and $code<600)
62    {
63      set_status_header($code, $codeText);
64    }
65
66    $this->_code = $code;
67    $this->_codeText = $codeText;
68  }
69
70  function code() { return $this->_code; }
71  function message() { return $this->_codeText; }
72}
73
74/**
75 * Simple wrapper around an array (keys are consecutive integers starting at 0).
76 * Provides naming clues for xml output (xml attributes vs. xml child elements?)
77 * Usually returned by web service function implementation.
78 */
79class PwgNamedArray
80{
81  /*private*/ var $_content;
82  /*private*/ var $_itemName;
83  /*private*/ var $_xmlAttributes;
84
85  /**
86   * Constructs a named array
87   * @param arr array (keys must be consecutive integers starting at 0)
88   * @param itemName string xml element name for values of arr (e.g. image)
89   * @param xmlAttributes array of sub-item attributes that will be encoded as
90   *      xml attributes instead of xml child elements
91   */
92  function PwgNamedArray($arr, $itemName, $xmlAttributes=array() )
93  {
94    $this->_content = $arr;
95    $this->_itemName = $itemName;
96    $this->_xmlAttributes = array_flip($xmlAttributes);
97  }
98}
99/**
100 * Simple wrapper around a "struct" (php array whose keys are not consecutive
101 * integers starting at 0). Provides naming clues for xml output (what is xml
102 * attributes and what is element)
103 */
104class PwgNamedStruct
105{
106  /*private*/ var $_content;
107  /*private*/ var $_xmlAttributes;
108
109  /**
110   * Constructs a named struct (usually returned by web service function
111   * implementation)
112   * @param name string - containing xml element name
113   * @param content array - the actual content (php array)
114   * @param xmlAttributes array - name of the keys in $content that will be
115   *    encoded as xml attributes (if null - automatically prefer xml attributes
116   *    whenever possible)
117   */
118  function PwgNamedStruct($content, $xmlAttributes=null, $xmlElements=null )
119  {
120    $this->_content = $content;
121    if ( isset($xmlAttributes) )
122    {
123      $this->_xmlAttributes = array_flip($xmlAttributes);
124    }
125    else
126    {
127      $this->_xmlAttributes = array();
128      foreach ($this->_content as $key=>$value)
129      {
130        if (!empty($key) and (is_scalar($value) or is_null($value)) )
131        {
132          if ( empty($xmlElements) or !in_array($key,$xmlElements) )
133          {
134            $this->_xmlAttributes[$key]=1;
135          }
136        }
137      }
138    }
139  }
140}
141
142
143/**
144 * Abstract base class for request handlers.
145 */
146abstract class PwgRequestHandler
147{
148  /** Virtual abstract method. Decodes the request (GET or POST) handles the
149   * method invocation as well as response sending.
150   */
151  abstract function handleRequest(&$service);
152}
153
154/**
155 *
156 * Base class for web service response encoder.
157 */
158abstract class PwgResponseEncoder
159{
160  /** encodes the web service response to the appropriate output format
161   * @param response mixed the unencoded result of a service method call
162   */
163  abstract function encodeResponse($response);
164
165  /** default "Content-Type" http header for this kind of response format
166   */
167  abstract function getContentType();
168
169  /**
170   * returns true if the parameter is a 'struct' (php array type whose keys are
171   * NOT consecutive integers starting with 0)
172   */
173  static function is_struct(&$data)
174  {
175    if (is_array($data) )
176    {
177      if (range(0, count($data) - 1) !== array_keys($data) )
178      { # string keys, unordered, non-incremental keys, .. - whatever, make object
179        return true;
180      }
181    }
182    return false;
183  }
184
185  /**
186   * removes all XML formatting from $response (named array, named structs, etc)
187   * usually called by every response encoder, except rest xml.
188   */
189  static function flattenResponse(&$value)
190  {
191    self::flatten($value);
192  }
193
194  private static function flatten(&$value)
195  {
196    if (is_object($value))
197    {
198      $class = strtolower( @get_class($value) );
199      if ($class == 'pwgnamedarray')
200      {
201        $value = $value->_content;
202      }
203      if ($class == 'pwgnamedstruct')
204      {
205        $value = $value->_content;
206      }
207    }
208
209    if (!is_array($value))
210      return;
211
212    if (self::is_struct($value))
213    {
214      if ( isset($value[WS_XML_ATTRIBUTES]) )
215      {
216        $value = array_merge( $value, $value[WS_XML_ATTRIBUTES] );
217        unset( $value[WS_XML_ATTRIBUTES] );
218      }
219    }
220
221    foreach ($value as $key=>&$v)
222    {
223      self::flatten($v);
224    }
225  }
226}
227
228
229
230class PwgServer
231{
232  var $_requestHandler;
233  var $_requestFormat;
234  var $_responseEncoder;
235  var $_responseFormat;
236
237  var $_methods = array();
238
239  function PwgServer()
240  {
241  }
242
243  /**
244   *  Initializes the request handler.
245   */
246  function setHandler($requestFormat, &$requestHandler)
247  {
248    $this->_requestHandler = &$requestHandler;
249    $this->_requestFormat = $requestFormat;
250  }
251
252  /**
253   *  Initializes the request handler.
254   */
255  function setEncoder($responseFormat, &$encoder)
256  {
257    $this->_responseEncoder = &$encoder;
258    $this->_responseFormat = $responseFormat;
259  }
260
261  /**
262   * Runs the web service call (handler and response encoder should have been
263   * created)
264   */
265  function run()
266  {
267    if ( is_null($this->_responseEncoder) )
268    {
269      set_status_header(400);
270      @header("Content-Type: text/plain");
271      echo ("Cannot process your request. Unknown response format.
272Request format: ".@$this->_requestFormat." Response format: ".@$this->_responseFormat."\n");
273      var_export($this);
274      die(0);
275    }
276
277    if ( is_null($this->_requestHandler) )
278    {
279      $this->sendResponse( new PwgError(400, 'Unknown request format') );
280      return;
281    }
282
283    // add reflection methods
284    $this->addMethod(
285        'reflection.getMethodList',
286        array('PwgServer', 'ws_getMethodList')
287        );
288    $this->addMethod(
289        'reflection.getMethodDetails',
290        array('PwgServer', 'ws_getMethodDetails'),
291        array('methodName')
292        );
293
294    trigger_action('ws_add_methods', array(&$this) );
295    uksort( $this->_methods, 'strnatcmp' );
296    $this->_requestHandler->handleRequest($this);
297  }
298
299  /**
300   * Encodes a response and sends it back to the browser.
301   */
302  function sendResponse($response)
303  {
304    $encodedResponse = $this->_responseEncoder->encodeResponse($response);
305    $contentType = $this->_responseEncoder->getContentType();
306
307    @header('Content-Type: '.$contentType.'; charset='.get_pwg_charset());
308    print_r($encodedResponse);
309    trigger_action('sendResponse', $encodedResponse );
310  }
311
312  /**
313   * Registers a web service method.
314   * @param methodName string - the name of the method as seen externally
315   * @param callback mixed - php method to be invoked internally
316   * @param params array - map of allowed parameter names with options
317   *    @option mixed default (optional)
318   *    @option int flags (optional)
319   *      possible values: WS_PARAM_ALLOW_ARRAY, WS_PARAM_FORCE_ARRAY, WS_PARAM_OPTIONAL
320   *    @option int type (optional)
321   *      possible values: WS_TYPE_BOOL, WS_TYPE_INT, WS_TYPE_FLOAT, WS_TYPE_ID
322   *                       WS_TYPE_POSITIVE, WS_TYPE_NOTNULL
323   *    @option int|float maxValue (optional)
324   * @param description string - a description of the method.
325   * @param include_file string - a file to be included befaore the callback is executed
326   * @param options array
327   *    @option bool hidden (hidden) - if true, this method won't be visible by reflection.getMethodList
328   */
329  function addMethod($methodName, $callback, $params=array(), $description='', $include_file='', $options=array())
330  {
331    if (!is_array($params))
332    {
333      $params = array();
334    }
335
336    if ( range(0, count($params) - 1) === array_keys($params) )
337    {
338      $params = array_flip($params);
339    }
340
341    foreach( $params as $param=>$data)
342    {
343      if ( !is_array($data) )
344      {
345        $params[$param] = array('flags'=>0,'type'=>0);
346      }
347      else
348      {
349        if ( !isset($data['flags']) )
350        {
351          $data['flags'] = 0;
352        }
353        if ( array_key_exists('default', $data) )
354        {
355          $data['flags'] |= WS_PARAM_OPTIONAL;
356        }
357        if ( !isset($data['type']) )
358        {
359          $data['type'] = 0;
360        }
361        $params[$param] = $data;
362      }
363    }
364
365    $this->_methods[$methodName] = array(
366      'callback'    => $callback,
367      'description' => $description,
368      'signature'   => $params,
369      'include'     => $include_file,
370      'options'     => $options,
371      );
372  }
373
374  function hasMethod($methodName)
375  {
376    return isset($this->_methods[$methodName]);
377  }
378
379  function getMethodDescription($methodName)
380  {
381    $desc = @$this->_methods[$methodName]['description'];
382    return isset($desc) ? $desc : '';
383  }
384
385  function getMethodSignature($methodName)
386  {
387    $signature = @$this->_methods[$methodName]['signature'];
388    return isset($signature) ? $signature : array();
389  }
390
391  /*static*/ function isPost()
392  {
393    return isset($HTTP_RAW_POST_DATA) or !empty($_POST);
394  }
395
396  static function makeArrayParam(&$param)
397  {
398    if ( $param==null )
399    {
400      $param = array();
401    }
402    else
403    {
404      if ( !is_array($param) )
405      {
406        $param = array($param);
407      }
408    }
409  }
410 
411  static function checkType(&$param, $type, $name)
412  {
413    $opts = array();
414    $msg = '';
415    if ( self::hasFlag($type, WS_TYPE_POSITIVE | WS_TYPE_NOTNULL) )
416    {
417      $opts['options']['min_range'] = 1;
418      $msg = ' positive and not null';
419    }
420    else if ( self::hasFlag($type, WS_TYPE_POSITIVE) )
421    {
422      $opts['options']['min_range'] = 0;
423      $msg = ' positive';
424    }
425   
426    if ( is_array($param) )
427    {
428      if ( self::hasFlag($type, WS_TYPE_BOOL) )
429      {
430        foreach ($param as &$value)
431        {
432          if ( ($value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) === null )
433          {
434            return new PwgError(WS_ERR_INVALID_PARAM, $name.' must only contain booleans' );
435          }
436        }
437        unset($value);
438      }
439      else if ( self::hasFlag($type, WS_TYPE_INT) )
440      {
441        foreach ($param as &$value)
442        {
443          if ( ($value = filter_var($value, FILTER_VALIDATE_INT, $opts)) === false )
444          {
445            return new PwgError(WS_ERR_INVALID_PARAM, $name.' must only contain'.$msg.' integers' );
446          }
447        }
448        unset($value);
449      }
450      else if ( self::hasFlag($type, WS_TYPE_FLOAT) )
451      {
452        foreach ($param as &$value)
453        {
454          if (
455            ($value = filter_var($value, FILTER_VALIDATE_FLOAT)) === false
456            or ( isset($opts['options']['min_range']) and $value < $opts['options']['min_range'] )
457          ) {
458            return new PwgError(WS_ERR_INVALID_PARAM, $name.' must only contain'.$msg.' floats' );
459          }
460        }
461        unset($value);
462      }
463    }
464    else if ( $param !== '' )
465    {
466      if ( self::hasFlag($type, WS_TYPE_BOOL) )
467      {
468        if ( ($param = filter_var($param, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) === null )
469        {
470          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be a boolean' );
471        }
472      }
473      else if ( self::hasFlag($type, WS_TYPE_INT) )
474      {
475        if ( ($param = filter_var($param, FILTER_VALIDATE_INT, $opts)) === false )
476        {
477          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be an'.$msg.' integer' );
478        }
479      }
480      else if ( self::hasFlag($type, WS_TYPE_FLOAT) )
481      {
482        if (
483          ($param = filter_var($param, FILTER_VALIDATE_FLOAT)) === false
484          or ( isset($opts['options']['min_range']) and $param < $opts['options']['min_range'] )
485        ) {
486          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be a'.$msg.' float' );
487        }
488      }
489    }
490   
491    return null;
492  }
493 
494  static function hasFlag($val, $flag)
495  {
496    return ($val & $flag) == $flag;
497  }
498
499  /**
500   *  Invokes a registered method. Returns the return of the method (or
501   *  a PwgError object if the method is not found)
502   *  @param methodName string the name of the method to invoke
503   *  @param params array array of parameters to pass to the invoked method
504   */
505  function invoke($methodName, $params)
506  {
507    $method = @$this->_methods[$methodName];
508
509    if ( $method == null )
510    {
511      return new PwgError(WS_ERR_INVALID_METHOD, 'Method name is not valid');
512    }
513
514    // parameter check and data correction
515    $signature = $method['signature'];
516    $missing_params = array();
517   
518    foreach ($signature as $name => $options)
519    {
520      $flags = $options['flags'];
521     
522      // parameter not provided in the request
523      if ( !array_key_exists($name, $params) )
524      {
525        if ( !self::hasFlag($flags, WS_PARAM_OPTIONAL) )
526        {
527          $missing_params[] = $name;
528        }
529        else if ( array_key_exists('default', $options) )
530        {
531          $params[$name] = $options['default'];
532          if ( self::hasFlag($flags, WS_PARAM_FORCE_ARRAY) )
533          {
534            self::makeArrayParam($params[$name]);
535          }
536        }
537      }
538      // parameter provided but empty
539      else if ( $params[$name]==='' and !self::hasFlag($flags, WS_PARAM_OPTIONAL) )
540      {
541        $missing_params[] = $name;
542      }
543      // parameter provided - do some basic checks
544      else
545      {
546        $the_param = $params[$name];
547       
548        if ( is_array($the_param) and !self::hasFlag($flags, WS_PARAM_ACCEPT_ARRAY) )
549        {
550          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be scalar' );
551        }
552       
553        if ( self::hasFlag($flags, WS_PARAM_FORCE_ARRAY) )
554        {
555          self::makeArrayParam($the_param);
556        }
557       
558        if ( $options['type'] > 0 )
559        {
560          if ( ($ret = self::checkType($the_param, $options['type'], $name)) !== null )
561          {
562            return $ret;
563          }
564        }
565       
566        if ( isset($options['maxValue']) and $the_param>$options['maxValue'])
567        {
568          $the_param = $options['maxValue'];
569        }
570       
571        $params[$name] = $the_param;
572      }
573    }
574   
575    if (count($missing_params))
576    {
577      return new PwgError(WS_ERR_MISSING_PARAM, 'Missing parameters: '.implode(',',$missing_params));
578    }
579   
580    $result = trigger_event('ws_invoke_allowed', true, $methodName, $params);
581    if ( strtolower( @get_class($result) )!='pwgerror')
582    {
583      if ( !empty($method['include']) )
584      {
585        include_once( $method['include'] );
586      }
587      $result = call_user_func_array($method['callback'], array($params, &$this) );
588    }
589   
590    return $result;
591  }
592
593  /**
594   * WS reflection method implementation: lists all available methods
595   */
596  static function ws_getMethodList($params, &$service)
597  {
598    $methods = array_filter($service->_methods,
599            create_function('$m', 'return empty($m["options"]["hidden"]) || !$m["options"]["hidden"];'));
600    return array('methods' => new PwgNamedArray( array_keys($methods),'method' ) );
601  }
602
603  /**
604   * WS reflection method implementation: gets information about a given method
605   */
606  static function ws_getMethodDetails($params, &$service)
607  {
608    $methodName = $params['methodName'];
609   
610    if (!$service->hasMethod($methodName))
611    {
612      return new PwgError(WS_ERR_INVALID_PARAM, 'Requested method does not exist');
613    }
614   
615    $res = array(
616      'name' => $methodName,
617      'description' => $service->getMethodDescription($methodName),
618      'params' => array(),
619    );
620   
621    foreach ($service->getMethodSignature($methodName) as $name => $options)
622    {
623      $param_data = array(
624        'name' => $name,
625        'optional' => self::hasFlag($options['flags'], WS_PARAM_OPTIONAL),
626        'acceptArray' => self::hasFlag($options['flags'], WS_PARAM_ACCEPT_ARRAY),
627        'type' => 'mixed',
628        );
629     
630      if (isset($options['default']))
631      {
632        $param_data['defaultValue'] = $options['default'];
633      }
634      if (isset($options['info']))
635      {
636        $param_data['info'] = $options['info'];
637      }
638     
639      if ( self::hasFlag($options['type'], WS_TYPE_BOOL) )
640      {
641        $param_data['type'] = 'bool';
642      }
643      else if ( self::hasFlag($options['type'], WS_TYPE_INT) )
644      {
645        $param_data['type'] = 'int';
646      }
647      else if ( self::hasFlag($options['type'], WS_TYPE_FLOAT) )
648      {
649        $param_data['type'] = 'float';
650      }
651      if ( self::hasFlag($options['type'], WS_TYPE_POSITIVE) )
652      {
653        $param_data['type'].= ' positive';
654      }
655      if ( self::hasFlag($options['type'], WS_TYPE_NOTNULL) )
656      {
657        $param_data['type'].= ' notnull';
658      }
659     
660      $res['params'][] = $param_data;
661    }
662    return $res;
663  }
664}
665?>
Note: See TracBrowser for help on using the repository browser.