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

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

two new options for API methods : 'admin_only' and 'post_only'

  • Property svn:eol-style set to LF
File size: 19.4 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 (optional) - if true, this method won't be visible by reflection.getMethodList
328   *    @option bool admin_only (optional)
329   *    @option bool post_only (optional)
330   */
331  function addMethod($methodName, $callback, $params=array(), $description='', $include_file='', $options=array())
332  {
333    if (!is_array($params))
334    {
335      $params = array();
336    }
337
338    if ( range(0, count($params) - 1) === array_keys($params) )
339    {
340      $params = array_flip($params);
341    }
342
343    foreach( $params as $param=>$data)
344    {
345      if ( !is_array($data) )
346      {
347        $params[$param] = array('flags'=>0,'type'=>0);
348      }
349      else
350      {
351        if ( !isset($data['flags']) )
352        {
353          $data['flags'] = 0;
354        }
355        if ( array_key_exists('default', $data) )
356        {
357          $data['flags'] |= WS_PARAM_OPTIONAL;
358        }
359        if ( !isset($data['type']) )
360        {
361          $data['type'] = 0;
362        }
363        $params[$param] = $data;
364      }
365    }
366
367    $this->_methods[$methodName] = array(
368      'callback'    => $callback,
369      'description' => $description,
370      'signature'   => $params,
371      'include'     => $include_file,
372      'options'     => $options,
373      );
374  }
375
376  function hasMethod($methodName)
377  {
378    return isset($this->_methods[$methodName]);
379  }
380
381  function getMethodDescription($methodName)
382  {
383    $desc = @$this->_methods[$methodName]['description'];
384    return isset($desc) ? $desc : '';
385  }
386
387  function getMethodSignature($methodName)
388  {
389    $signature = @$this->_methods[$methodName]['signature'];
390    return isset($signature) ? $signature : array();
391  }
392
393  static function isPost()
394  {
395    return isset($HTTP_RAW_POST_DATA) or !empty($_POST);
396  }
397
398  static function makeArrayParam(&$param)
399  {
400    if ( $param==null )
401    {
402      $param = array();
403    }
404    else
405    {
406      if ( !is_array($param) )
407      {
408        $param = array($param);
409      }
410    }
411  }
412 
413  static function checkType(&$param, $type, $name)
414  {
415    $opts = array();
416    $msg = '';
417    if ( self::hasFlag($type, WS_TYPE_POSITIVE | WS_TYPE_NOTNULL) )
418    {
419      $opts['options']['min_range'] = 1;
420      $msg = ' positive and not null';
421    }
422    else if ( self::hasFlag($type, WS_TYPE_POSITIVE) )
423    {
424      $opts['options']['min_range'] = 0;
425      $msg = ' positive';
426    }
427   
428    if ( is_array($param) )
429    {
430      if ( self::hasFlag($type, WS_TYPE_BOOL) )
431      {
432        foreach ($param as &$value)
433        {
434          if ( ($value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) === null )
435          {
436            return new PwgError(WS_ERR_INVALID_PARAM, $name.' must only contain booleans' );
437          }
438        }
439        unset($value);
440      }
441      else if ( self::hasFlag($type, WS_TYPE_INT) )
442      {
443        foreach ($param as &$value)
444        {
445          if ( ($value = filter_var($value, FILTER_VALIDATE_INT, $opts)) === false )
446          {
447            return new PwgError(WS_ERR_INVALID_PARAM, $name.' must only contain'.$msg.' integers' );
448          }
449        }
450        unset($value);
451      }
452      else if ( self::hasFlag($type, WS_TYPE_FLOAT) )
453      {
454        foreach ($param as &$value)
455        {
456          if (
457            ($value = filter_var($value, FILTER_VALIDATE_FLOAT)) === false
458            or ( isset($opts['options']['min_range']) and $value < $opts['options']['min_range'] )
459          ) {
460            return new PwgError(WS_ERR_INVALID_PARAM, $name.' must only contain'.$msg.' floats' );
461          }
462        }
463        unset($value);
464      }
465    }
466    else if ( $param !== '' )
467    {
468      if ( self::hasFlag($type, WS_TYPE_BOOL) )
469      {
470        if ( ($param = filter_var($param, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) === null )
471        {
472          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be a boolean' );
473        }
474      }
475      else if ( self::hasFlag($type, WS_TYPE_INT) )
476      {
477        if ( ($param = filter_var($param, FILTER_VALIDATE_INT, $opts)) === false )
478        {
479          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be an'.$msg.' integer' );
480        }
481      }
482      else if ( self::hasFlag($type, WS_TYPE_FLOAT) )
483      {
484        if (
485          ($param = filter_var($param, FILTER_VALIDATE_FLOAT)) === false
486          or ( isset($opts['options']['min_range']) and $param < $opts['options']['min_range'] )
487        ) {
488          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be a'.$msg.' float' );
489        }
490      }
491    }
492   
493    return null;
494  }
495 
496  static function hasFlag($val, $flag)
497  {
498    return ($val & $flag) == $flag;
499  }
500
501  /**
502   *  Invokes a registered method. Returns the return of the method (or
503   *  a PwgError object if the method is not found)
504   *  @param methodName string the name of the method to invoke
505   *  @param params array array of parameters to pass to the invoked method
506   */
507  function invoke($methodName, $params)
508  {
509    $method = @$this->_methods[$methodName];
510
511    if ( $method == null )
512    {
513      return new PwgError(WS_ERR_INVALID_METHOD, 'Method name is not valid');
514    }
515   
516    if ( isset($method['options']['post_only']) and $method['options']['post_only'] and !self::isPost() )
517    {
518      return new PwgError(405, 'This method requires HTTP POST');
519    }
520   
521    if ( isset($method['options']['admin_only']) and $method['options']['admin_only'] and !is_admin() )
522    {
523      return new PwgError(401, 'Access denied');
524    }
525
526    // parameter check and data correction
527    $signature = $method['signature'];
528    $missing_params = array();
529   
530    foreach ($signature as $name => $options)
531    {
532      $flags = $options['flags'];
533     
534      // parameter not provided in the request
535      if ( !array_key_exists($name, $params) )
536      {
537        if ( !self::hasFlag($flags, WS_PARAM_OPTIONAL) )
538        {
539          $missing_params[] = $name;
540        }
541        else if ( array_key_exists('default', $options) )
542        {
543          $params[$name] = $options['default'];
544          if ( self::hasFlag($flags, WS_PARAM_FORCE_ARRAY) )
545          {
546            self::makeArrayParam($params[$name]);
547          }
548        }
549      }
550      // parameter provided but empty
551      else if ( $params[$name]==='' and !self::hasFlag($flags, WS_PARAM_OPTIONAL) )
552      {
553        $missing_params[] = $name;
554      }
555      // parameter provided - do some basic checks
556      else
557      {
558        $the_param = $params[$name];
559       
560        if ( is_array($the_param) and !self::hasFlag($flags, WS_PARAM_ACCEPT_ARRAY) )
561        {
562          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be scalar' );
563        }
564       
565        if ( self::hasFlag($flags, WS_PARAM_FORCE_ARRAY) )
566        {
567          self::makeArrayParam($the_param);
568        }
569       
570        if ( $options['type'] > 0 )
571        {
572          if ( ($ret = self::checkType($the_param, $options['type'], $name)) !== null )
573          {
574            return $ret;
575          }
576        }
577       
578        if ( isset($options['maxValue']) and $the_param>$options['maxValue'])
579        {
580          $the_param = $options['maxValue'];
581        }
582       
583        $params[$name] = $the_param;
584      }
585    }
586   
587    if (count($missing_params))
588    {
589      return new PwgError(WS_ERR_MISSING_PARAM, 'Missing parameters: '.implode(',',$missing_params));
590    }
591   
592    $result = trigger_event('ws_invoke_allowed', true, $methodName, $params);
593    if ( strtolower( @get_class($result) )!='pwgerror')
594    {
595      if ( !empty($method['include']) )
596      {
597        include_once( $method['include'] );
598      }
599      $result = call_user_func_array($method['callback'], array($params, &$this) );
600    }
601   
602    return $result;
603  }
604
605  /**
606   * WS reflection method implementation: lists all available methods
607   */
608  static function ws_getMethodList($params, &$service)
609  {
610    $methods = array_filter($service->_methods,
611            create_function('$m', 'return empty($m["options"]["hidden"]) || !$m["options"]["hidden"];'));
612    return array('methods' => new PwgNamedArray( array_keys($methods),'method' ) );
613  }
614
615  /**
616   * WS reflection method implementation: gets information about a given method
617   */
618  static function ws_getMethodDetails($params, &$service)
619  {
620    $methodName = $params['methodName'];
621   
622    if (!$service->hasMethod($methodName))
623    {
624      return new PwgError(WS_ERR_INVALID_PARAM, 'Requested method does not exist');
625    }
626   
627    $res = array(
628      'name' => $methodName,
629      'description' => $service->getMethodDescription($methodName),
630      'params' => array(),
631    );
632   
633    foreach ($service->getMethodSignature($methodName) as $name => $options)
634    {
635      $param_data = array(
636        'name' => $name,
637        'optional' => self::hasFlag($options['flags'], WS_PARAM_OPTIONAL),
638        'acceptArray' => self::hasFlag($options['flags'], WS_PARAM_ACCEPT_ARRAY),
639        'type' => 'mixed',
640        );
641     
642      if (isset($options['default']))
643      {
644        $param_data['defaultValue'] = $options['default'];
645      }
646      if (isset($options['info']))
647      {
648        $param_data['info'] = $options['info'];
649      }
650     
651      if ( self::hasFlag($options['type'], WS_TYPE_BOOL) )
652      {
653        $param_data['type'] = 'bool';
654      }
655      else if ( self::hasFlag($options['type'], WS_TYPE_INT) )
656      {
657        $param_data['type'] = 'int';
658      }
659      else if ( self::hasFlag($options['type'], WS_TYPE_FLOAT) )
660      {
661        $param_data['type'] = 'float';
662      }
663      if ( self::hasFlag($options['type'], WS_TYPE_POSITIVE) )
664      {
665        $param_data['type'].= ' positive';
666      }
667      if ( self::hasFlag($options['type'], WS_TYPE_NOTNULL) )
668      {
669        $param_data['type'].= ' notnull';
670      }
671     
672      $res['params'][] = $param_data;
673    }
674    return $res;
675  }
676}
677?>
Note: See TracBrowser for help on using the repository browser.