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

Last change on this file since 5415 was 5196, checked in by plg, 15 years ago

increase copyright year to 2010

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