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