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