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

Last change on this file since 23210 was 22729, checked in by rvelices, 11 years ago

bug 2900: Improve web service output

  • Property svn:eol-style set to LF
File size: 14.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_ERR_INVALID_METHOD',  501 );
39define( 'WS_ERR_MISSING_PARAM',   1002 );
40define( 'WS_ERR_INVALID_PARAM',   1003 );
41
42define( 'WS_XML_ATTRIBUTES', 'attributes_xml_');
43
44/**
45 * PwgError object can be returned from any web service function implementation.
46 */
47class PwgError
48{
49  private $_code;
50  private $_codeText;
51
52  function PwgError($code, $codeText)
53  {
54    if ($code>=400 and $code<600)
55    {
56      set_status_header($code, $codeText);
57    }
58
59    $this->_code = $code;
60    $this->_codeText = $codeText;
61  }
62
63  function code() { return $this->_code; }
64  function message() { return $this->_codeText; }
65}
66
67/**
68 * Simple wrapper around an array (keys are consecutive integers starting at 0).
69 * Provides naming clues for xml output (xml attributes vs. xml child elements?)
70 * Usually returned by web service function implementation.
71 */
72class PwgNamedArray
73{
74  /*private*/ var $_content;
75  /*private*/ var $_itemName;
76  /*private*/ var $_xmlAttributes;
77
78  /**
79   * Constructs a named array
80   * @param arr array (keys must be consecutive integers starting at 0)
81   * @param itemName string xml element name for values of arr (e.g. image)
82   * @param xmlAttributes array of sub-item attributes that will be encoded as
83   *      xml attributes instead of xml child elements
84   */
85  function PwgNamedArray($arr, $itemName, $xmlAttributes=array() )
86  {
87    $this->_content = $arr;
88    $this->_itemName = $itemName;
89    $this->_xmlAttributes = array_flip($xmlAttributes);
90  }
91}
92/**
93 * Simple wrapper around a "struct" (php array whose keys are not consecutive
94 * integers starting at 0). Provides naming clues for xml output (what is xml
95 * attributes and what is element)
96 */
97class PwgNamedStruct
98{
99  /*private*/ var $_content;
100  /*private*/ var $_xmlAttributes;
101
102  /**
103   * Constructs a named struct (usually returned by web service function
104   * implementation)
105   * @param name string - containing xml element name
106   * @param content array - the actual content (php array)
107   * @param xmlAttributes array - name of the keys in $content that will be
108   *    encoded as xml attributes (if null - automatically prefer xml attributes
109   *    whenever possible)
110   */
111  function PwgNamedStruct($content, $xmlAttributes=null, $xmlElements=null )
112  {
113    $this->_content = $content;
114    if ( isset($xmlAttributes) )
115    {
116      $this->_xmlAttributes = array_flip($xmlAttributes);
117    }
118    else
119    {
120      $this->_xmlAttributes = array();
121      foreach ($this->_content as $key=>$value)
122      {
123        if (!empty($key) and (is_scalar($value) or is_null($value)) )
124        {
125          if ( empty($xmlElements) or !in_array($key,$xmlElements) )
126          {
127            $this->_xmlAttributes[$key]=1;
128          }
129        }
130      }
131    }
132  }
133}
134
135
136/**
137 * Abstract base class for request handlers.
138 */
139class PwgRequestHandler
140{
141  /** Virtual abstract method. Decodes the request (GET or POST) handles the
142   * method invocation as well as response sending.
143   */
144  function handleRequest(&$server) { assert(false); }
145}
146
147/**
148 *
149 * Base class for web service response encoder.
150 */
151class PwgResponseEncoder
152{
153  /** encodes the web service response to the appropriate output format
154   * @param response mixed the unencoded result of a service method call
155   */
156  function encodeResponse($response) { assert(false); }
157
158  /** default "Content-Type" http header for this kind of response format
159   */
160  function getContentType() { assert(false); }
161
162  /**
163   * returns true if the parameter is a 'struct' (php array type whose keys are
164   * NOT consecutive integers starting with 0)
165   */
166  static function is_struct(&$data)
167  {
168    if (is_array($data) )
169    {
170      if (range(0, count($data) - 1) !== array_keys($data) )
171      { # string keys, unordered, non-incremental keys, .. - whatever, make object
172        return true;
173      }
174    }
175    return false;
176  }
177
178  /**
179   * removes all XML formatting from $response (named array, named structs, etc)
180   * usually called by every response encoder, except rest xml.
181   */
182  static function flattenResponse(&$value)
183  {
184    self::flatten($value);
185  }
186
187  private static function flatten(&$value)
188  {
189    if (is_object($value))
190    {
191      $class = strtolower( @get_class($value) );
192      if ($class == 'pwgnamedarray')
193      {
194        $value = $value->_content;
195      }
196      if ($class == 'pwgnamedstruct')
197      {
198        $value = $value->_content;
199      }
200    }
201
202    if (!is_array($value))
203      return;
204
205    if (self::is_struct($value))
206    {
207      if ( isset($value[WS_XML_ATTRIBUTES]) )
208      {
209        $value = array_merge( $value, $value[WS_XML_ATTRIBUTES] );
210        unset( $value[WS_XML_ATTRIBUTES] );
211      }
212    }
213
214    foreach ($value as $key=>&$v)
215    {
216      self::flatten($v);
217    }
218  }
219}
220
221
222
223class PwgServer
224{
225  var $_requestHandler;
226  var $_requestFormat;
227  var $_responseEncoder;
228  var $_responseFormat;
229
230  var $_methods = array();
231
232  function PwgServer()
233  {
234  }
235
236  /**
237   *  Initializes the request handler.
238   */
239  function setHandler($requestFormat, &$requestHandler)
240  {
241    $this->_requestHandler = &$requestHandler;
242    $this->_requestFormat = $requestFormat;
243  }
244
245  /**
246   *  Initializes the request handler.
247   */
248  function setEncoder($responseFormat, &$encoder)
249  {
250    $this->_responseEncoder = &$encoder;
251    $this->_responseFormat = $responseFormat;
252  }
253
254  /**
255   * Runs the web service call (handler and response encoder should have been
256   * created)
257   */
258  function run()
259  {
260    if ( is_null($this->_responseEncoder) )
261    {
262      set_status_header(400);
263      @header("Content-Type: text/plain");
264      echo ("Cannot process your request. Unknown response format.
265Request format: ".@$this->_requestFormat." Response format: ".@$this->_responseFormat."\n");
266      var_export($this);
267      die(0);
268    }
269
270    if ( is_null($this->_requestHandler) )
271    {
272      $this->sendResponse( new PwgError(400, 'Unknown request format') );
273      return;
274    }
275
276    $this->addMethod('reflection.getMethodList',
277        array('PwgServer', 'ws_getMethodList'),
278        null, '' );
279    $this->addMethod('reflection.getMethodDetails',
280        array('PwgServer', 'ws_getMethodDetails'),
281        array('methodName'),'');
282
283    trigger_action('ws_add_methods', array(&$this) );
284    uksort( $this->_methods, 'strnatcmp' );
285    $this->_requestHandler->handleRequest($this);
286  }
287
288  /**
289   * Encodes a response and sends it back to the browser.
290   */
291  function sendResponse($response)
292  {
293    $encodedResponse = $this->_responseEncoder->encodeResponse($response);
294    $contentType = $this->_responseEncoder->getContentType();
295
296    @header('Content-Type: '.$contentType.'; charset='.get_pwg_charset());
297    print_r($encodedResponse);
298    trigger_action('sendResponse', $encodedResponse );
299  }
300
301  /**
302   * Registers a web service method.
303   * @param methodName string - the name of the method as seen externally
304   * @param callback mixed - php method to be invoked internally
305   * @param params array - map of allowed parameter names with optional default
306   * values and parameter flags. Example of $params:
307   * array( 'param1' => array('default'=>523, 'flags'=>WS_PARAM_FORCE_ARRAY) ) .
308   * Possible parameter flags are:
309   * WS_PARAM_ALLOW_ARRAY - this parameter can be an array
310   * WS_PARAM_FORCE_ARRAY - if this parameter is scalar, force it to an array
311   *  before invoking the method
312   * @param description string - a description of the method.
313   */
314  function addMethod($methodName, $callback, $params=array(), $description, $include_file='')
315  {
316    if (!is_array($params))
317    {
318      $params = array();
319    }
320
321    if ( range(0, count($params) - 1) === array_keys($params) )
322    {
323      $params = array_flip($params);
324    }
325
326    foreach( $params as $param=>$options)
327    {
328      if ( !is_array($options) )
329      {
330        $params[$param] = array('flags'=>0);
331      }
332      else
333      {
334        $flags = isset($options['flags']) ? $options['flags'] : 0;
335        if ( array_key_exists('default', $options) )
336        {
337          $flags |= WS_PARAM_OPTIONAL;
338        }
339        $options['flags'] = $flags;
340        $params[$param] = $options;
341      }
342    }
343
344    $this->_methods[$methodName] = array(
345      'callback'    => $callback,
346      'description' => $description,
347      'signature'   => $params,
348      'include'     => $include_file,
349      );
350  }
351
352  function hasMethod($methodName)
353  {
354    return isset($this->_methods[$methodName]);
355  }
356
357  function getMethodDescription($methodName)
358  {
359    $desc = @$this->_methods[$methodName]['description'];
360    return isset($desc) ? $desc : '';
361  }
362
363  function getMethodSignature($methodName)
364  {
365    $signature = @$this->_methods[$methodName]['signature'];
366    return isset($signature) ? $signature : array();
367  }
368
369  /*static*/ function isPost()
370  {
371    return isset($HTTP_RAW_POST_DATA) or !empty($_POST);
372  }
373
374  /*static*/ function makeArrayParam(&$param)
375  {
376    if ( $param==null )
377    {
378      $param = array();
379    }
380    else
381    {
382      if (! is_array($param) )
383      {
384        $param = array($param);
385      }
386    }
387  }
388
389  /**
390   *  Invokes a registered method. Returns the return of the method (or
391   *  a PwgError object if the method is not found)
392   *  @param methodName string the name of the method to invoke
393   *  @param params array array of parameters to pass to the invoked method
394   */
395  function invoke($methodName, $params)
396  {
397    $method = @$this->_methods[$methodName];
398
399    if ( $method==null )
400    {
401      return new PwgError(WS_ERR_INVALID_METHOD, 'Method name is not valid');
402    }
403
404    // parameter check and data coercion !
405    $signature = $method['signature'];
406    $missing_params = array();
407    foreach($signature as $name=>$options)
408    {
409      $flags = $options['flags'];
410      if ( !array_key_exists($name, $params) )
411      {// parameter not provided in the request
412        if ( !($flags&WS_PARAM_OPTIONAL) )
413        {
414          $missing_params[] = $name;
415        }
416        else if ( array_key_exists('default',$options) )
417        {
418          $params[$name] = $options['default'];
419          if ( ($flags&WS_PARAM_FORCE_ARRAY)==WS_PARAM_FORCE_ARRAY )
420          {
421            $this->makeArrayParam( $params[$name] );
422          }
423        }
424      }
425      else
426      {// parameter provided - do some basic checks
427        $the_param = $params[$name];
428        if ( is_array($the_param) and ($flags&WS_PARAM_ACCEPT_ARRAY)==0 )
429        {
430          return new PwgError(WS_ERR_INVALID_PARAM, $name.' must be scalar' );
431        }
432        if ( ($flags&WS_PARAM_FORCE_ARRAY)==WS_PARAM_FORCE_ARRAY )
433        {
434          $this->makeArrayParam( $the_param );
435        }
436        if ( isset($options['maxValue']) and $the_param>$options['maxValue'])
437        {
438          $the_param = $options['maxValue'];
439        }
440        $params[$name] = $the_param;
441      }
442    }
443    if (count($missing_params))
444    {
445      return new PwgError(WS_ERR_MISSING_PARAM, 'Missing parameters: '.implode(',',$missing_params));
446    }
447    $result = trigger_event('ws_invoke_allowed', true, $methodName, $params);
448    if ( strtolower( @get_class($result) )!='pwgerror')
449    {
450      if ( !empty($method['include']) )
451      {
452        include_once( $method['include'] );
453      }
454      $result = call_user_func_array($method['callback'], array($params, &$this) );
455    }
456    return $result;
457  }
458
459  /**
460   * WS reflection method implementation: lists all available methods
461   */
462  static function ws_getMethodList($params, &$service)
463  {
464    return array('methods' => new PwgNamedArray( array_keys($service->_methods),'method' ) );
465  }
466
467  /**
468   * WS reflection method implementation: gets information about a given method
469   */
470  static function ws_getMethodDetails($params, &$service)
471  {
472    $methodName = $params['methodName'];
473    if (!$service->hasMethod($methodName))
474    {
475      return new PwgError(WS_ERR_INVALID_PARAM,
476            'Requested method does not exist');
477    }
478    $res = array(
479      'name' => $methodName,
480      'description' => $service->getMethodDescription($methodName),
481      'params' => array(),
482    );
483    $signature = $service->getMethodSignature($methodName);
484    foreach ($signature as $name => $options)
485    {
486      $param_data = array(
487        'name' => $name,
488        'optional' => ($options['flags']&WS_PARAM_OPTIONAL)?true:false,
489        'acceptArray' => ($options['flags']&WS_PARAM_ACCEPT_ARRAY)?true:false,
490        );
491      if (isset($options['default']))
492      {
493        $param_data['defaultValue'] = $options['default'];
494      }
495      if (isset($options['info']))
496      {
497        $param_data['info'] = $options['info'];
498      }
499      $res['params'][] = $param_data;
500    }
501    return $res;
502  }
503}
504?>
Note: See TracBrowser for help on using the repository browser.