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

Last change on this file since 30214 was 28587, checked in by mistic100, 10 years ago

feature 3010 : replace trigger_action/event by trigger_notify/change

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