This repository has been archived by the owner on Jun 24, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathRPCServer.class.php4
1811 lines (1652 loc) · 76 KB
/
RPCServer.class.php4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
# JSON/XML-RPC Server in PHP4 <http://code.google.com/p/json-xml-rpc/>
# Version: 0.8.1.1 (2008-01-06)
# Copyright: 2007, Weston Ruter <http://weston.ruter.net/>
# License: Dual licensed under MIT <http://creativecommons.org/licenses/MIT/>
# and GPL <http://creativecommons.org/licenses/GPL/2.0/> licenses.
#
# The comments contained in this code are largely quotations from the following specs:
# * XML-RPC: <http://www.xmlrpc.com/spec>
# * JSON-RPC 1.0: <http://json-rpc.org/wiki/specification>
# * JSON-RPC 1.1 (working draft): <http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html>
# Note that development on JSON-RPC continues at <http://groups.google.com/group/json-rpc>
#
# Usage:
# if(!class_exists('DateTime'))
# require_once('DateTime.class.php');
# require_once('RPCServer.class.php4');
# putenv('TZ=UTC');
# $server = RPCServer::getInstance(); //note that the RPCServer class is a singleton
# function getTemp($zipCode){
# //...
# return $row['temperature'];
# }
# $server->addMethod("getTemp");
# $server->processRequest();
class RPCServer {
var $JSON_RPC_VERSION = "1.1";
var $XML_RPC_VERSION = "1.0";
var $XML = 1;
var $JSON = 2;
var $JAVASCRIPT = 3;
//var $HTTP_GET = 4;
var $name;
var $id;
var $version;
var $summary;
var $help;
var $address;
var $requestID;
var $requestType; #XML | JSON | HTTP_GET
var $responseType; #XML | JSON | JAVASCRIPT
var $isJSONOmitResponseWrapper;
var $isMethodNotFound;
var $callbackFunction;
var $publicMethodName;
var $isDebugMode = false;
var $isCallingMethod = false;
var $isFinished = false;
var $requestData;
var $responseData;
var $publicToPrivateMap = array();
var $defaultResponseType;
var $JSONDateFormat = 'ISO8601'; #or "classHinting" "@ticks@" or "ASP.NET"
var $dbResultIndexType = 'ASSOC'; #or "NUM"
var $isUsingIncludedFunctions = false; #if not Reflection API
var $defaultParametersPreserved = true;
var $iso8601StringsConverted = true;
#function getInstance()
#function __construct()
#function __clone()
#function preserveDefaultParameters($defaultParametersPreserved)
#function convertISO8601Strings($iso8601StringsConverted)
#function useIncludedFunctions($isUsingIncludedFunctions) #if not Reflection API
#function setDefaultResponseType($type)
#function setJSONDateFormat($formatName)
#function setDBResultIndexType($indexType)
#function setDebugMode($state)
#function addMethod($privateProcName, $publicMethodName)
#function printResponseStart()
#function printResponseEnd()
#function handleError($errno, $errstr, $errfile, $errline, $errcontext)
#function handleException($e)
#function respondWithFault($errno, $errstr, $errfile = null, $errline = null, $errcontext = null)
#function processRequest()
#function encodeJson($value)
#function decodeJson($json)
#function decodeJson_processTokens(&$tokens, &$i)
#function encodeXmlRpc($value)
#function decodeXmlRpc($valueEl)
#function convertResource($resource)
#function getEscapeSequence_callback($regExMatches)
#function getEscapedChar_callback($regExMatches)
#function isVector(&$array)
#function stringToNumber($str)
#function stringToType($str)
#function ticksToDateTime($ticks)
#function unicodeToUtf8($src)
/*BEGIN PHP5**
private static $instance = null;
function getInstance(){
if(!RPCServer::$instance){
$c = __CLASS__;
RPCServer::$instance =& new $c;
}
return RPCServer::$instance;
}
function __construct(){
$this->init();
}
function __clone(){}
function __destruct(){
$this->processRequest();
}
**END PHP5*/
/*BEGIN PHP4*/
function RPCServer(){
$this->init();
}
function getInstance(){
static $instance = null;
if($instance == null){
#$c = __CLASS__;
#return new $c;
$instance =& new RPCServer();
}
return $instance;
}
#In PHP5, the processRequests method is called automatically by the destructor. PHP4 does
# not support destructors, so I tried using register_shutdown_function instead for PHP4's sake.
# However, when there is a fatal error in the user code, the error is not then caught when
# using... register_shutdown_function(array(&$this, 'processRequest'));
/*END PHP4*/
function init(){
ob_start();
$this->isMethodNotFound = false; #if true, then catch block will return HTTP status 404; otherwise 500 (JSON-RPC only)
$this->defaultResponseType = $this->JSON;
$this->requestType = null;
$this->responseType = null;
#"Don't assume a timezone. It should be specified by the server in its documentation what
# assumptions it makes about timezones."
/*BEGIN PHP5**
//date_default_timezone_set("UTC"); //This implementation was assuming universal time.
**END PHP5*/
/*BEGIN PHP4*/
//putenv('TZ=UTC'); #IS THIS VALID? //This implementation was assuming universal time.
/*END PHP4*/
#Set error handler
set_error_handler(array(&$this, "handleError"));
/*BEGIN PHP5**
set_exception_handler(array(&$this, "handleException"));
**END PHP5*/
#############################################################################################
# Get request data
#############################################################################################
$this->requestData = '';
if($_SERVER['REQUEST_METHOD'] == 'POST')
$this->requestData = trim(file_get_contents("php://input"));
#Sniff the request types. While the XML-RPC spec says that the Content-Type [must be] text/xml,
# and while JSON-RPC says the Content-Type MUST be specified and SHOULD read application/json, this
# implementation prefers to determine the content type of the request.
#This implementation does not address JSON-RPC section 6.1. HTTP Header Requirements
if(preg_match("/^\s*<\?xml\b/", $this->requestData)) #preg_match("/\bxml\b/", $headers['Content-Type']) ||
$this->requestType = $this->XML;
else if(preg_match("/^\s*{/", $this->requestData))
$this->requestType = $this->JSON;
#else if(isset($_SERVER['PATH_INFO']))
# $this->requestType = $this->HTTP_GET;
#Determine the appropriate response type. This implementation may respond with an XML-RPC methodResponse
# document, a JSON-RPC response object, bare JSON date (if query string parameter 'JSON-omit-response-wrapper'
# is passed), or JavaScript code with user-specified callback function
# with the JSON response as its parameter. This last response type allow JSON-RPC calls to be made
# to the server by inserting SCRIPT elements into the client document.
if(isset($_GET['JSON-response-callback'])){
$this->responseType = $this->JAVASCRIPT;
if(!preg_match('/^(?:\w|\$|_)(?:\d|\w|\$|_)*(\.(?:\w|\$|_)(?:\d|\w|\$|_)*)*$/', $_GET['JSON-response-callback']))
trigger_error("The provided JavaScript callback function \"" . $_GET['JSON-response-callback'] . "\" is not a valid JavaScript identifier.");
$this->callbackFunction = $_GET['JSON-response-callback'];
#Remove the 'JSON-response-callback' parameter from the query string; it is done this way instead
# of via unset($_GET['JSON-response-callback']) because it is necessary to manually parse the
# query string parameters manually below.
$_SERVER['QUERY_STRING'] = preg_replace('/(^|&)JSON-response-callback=[^&]+/', '', $_SERVER['QUERY_STRING']);
$_SERVER['QUERY_STRING'] = preg_replace('/^&/', '', $_SERVER['QUERY_STRING']);
}
#Determine if the user wants to receive JSON result only (omitting skipping the JSON-RPC response object)
#$this->isJSONOmitResponseWrapper = false;
if($this->isJSONOmitResponseWrapper = isset($_GET['JSON-omit-response-wrapper'])){ # && (bool)$_GET['JSON-omit-response-wrapper']
#$this->isJSONOmitResponseWrapper = true;
#Remove the 'JSON-omit-response-wrapper' parameter from the query string; it is done this way instead
# of via unset($_GET['JSON-omit-response-wrapper']) because it is necessary to manually parse the
# query string parameters manually below.
$_SERVER['QUERY_STRING'] = preg_replace('/(^|&)JSON-omit-response-wrapper=[^&]+/', '', $_SERVER['QUERY_STRING']);
$_SERVER['QUERY_STRING'] = preg_replace('/^&/', '', $_SERVER['QUERY_STRING']);
}
}
function preserveDefaultParameters($defaultParametersPreserved){
$this->defaultParametersPreserved = (bool) $defaultParametersPreserved;
}
function convertISO8601Strings($iso8601StringsConverted){
$this->iso8601StringsConverted = (bool) $iso8601StringsConverted;
}
#disregarded if Reflection API available
function useIncludedFunctions($isUsingIncludedFunctions){
$this->isUsingIncludedFunctions = (bool) $isUsingIncludedFunctions;
}
function setDefaultResponseType($responseType){
switch($responseType){
case $this->JSON:
case $this->XML:
#case $this->JAVASCRIPT:
$this->defaultResponseType = $responseType;
break;
default:
trigger_error('Invalid response type "' . $responseType . '"; it must be one of the constants defined for the RPCServer class: XML or JSON.');
}
}
function setJSONDateFormat($formatName){
switch($formatName){
case 'ISO8601':
case 'classHinting':
case '@ticks@':
case 'ASP.NET':
$this->JSONDateFormat = $formatName;
break;
case '@timestamp@':
$this->JSONDateFormat = '@ticks@';
break;
default:
trigger_error('Invalid format name "' . $formatName . '" provided for specifying the JSON date format. Format must be either "ISO8601", "classHinting", "@ticks@", or "ASP.NET".');
}
setcookie('JSONDateFormat', $formatName, time()+60*60*24*30, dirname($_SERVER['SCRIPT_NAME']));
$_COOKIE['JSONDateFormat'] = $formatName;
return true;
}
function setDBResultIndexType($indexType){
switch($indexType){
case 'ASSOC':
case 'NUM':
$this->dbResultIndexType = $indexType;
break;
default:
trigger_error('Invalid index type "' . $indexType . '" provided for specifying database result indices; it must be either "ASSOC" or "NUM".');
}
setcookie('dbResultIndexType', $indexType, time()+60*60*24*30, dirname($_SERVER['SCRIPT_NAME']));
$_COOKIE['dbResultIndexType'] = $indexType;
return true;
}
function setDebugMode($state){
$this->isDebugMode = (bool) $state;
}
#If a public method name is not provided, then the PHP function name will be used as the public name
function addMethod($privateProcName, $publicMethodName = null){
if($this->isFinished)
die("The server's request has already been processed. You may only invoke 'addMethod' before the 'processRequest' is executed.");
if(!$publicMethodName)
$publicMethodName = $privateProcName;
if(!function_exists($privateProcName))
trigger_error("\$RPCServerInstance->addMethod() failed because the function \"$privateProcName\" does not exist.");
if(isset($this->publicToPrivateMap[$publicMethodName]))
trigger_error("\$RPCServerInstance->addMethod() failed because the method name \"$publicMethodName\" has already been assigned to the function \"" . $this->publicToPrivateMap[$publicMethodName] . "\".");
$this->publicToPrivateMap[$publicMethodName] = $privateProcName;
}
function printResponseStart(){
if(!$this->responseType){
if($this->isJSONOmitResponseWrapper || preg_match("/\bapplication\/json\b/i", $_SERVER['HTTP_ACCEPT']))
$this->responseType = $this->JSON;
else if(!$this->isJSONOmitResponseWrapper && preg_match("/\bxml\b/i", $_SERVER['HTTP_ACCEPT']))
$this->responseType = $this->XML;
else if($this->defaultResponseType)
$this->responseType = $this->defaultResponseType;
else #$this->requestType == $this->XML || $this->requestType == $this->JSON
$this->responseType = $this->requestType;
}
##Start the output to the client in the appropriate format
if($this->responseType == $this->XML){
#The Content-Type is text/xml. Content-Length must be present and correct.
# The body of the response is a single XML structure, a <methodResponse>, which
# can contain a single <params> which contains a single <param> which contains a single <value>.
header('content-type: text/xml; charset=utf-8');
print '<?xml version="1.0"?><methodResponse>';
}
else { #JSON or JavaScript
if($this->responseType == $this->JAVASCRIPT) {
header('content-type: text/javascript; charset=utf-8');
print $this->callbackFunction . "(";
}
else { #$this->responseType == $this->JSON
header('content-type: application/json; charset=utf-8');
}
#Begin response object
if(!$this->isJSONOmitResponseWrapper){
print "{";
#version REQUIRED. A String specifying the version of the JSON-RPC protocol to which the
# client conforms. An implementation conforming to this specification MUST use the exact
# String value of "1.1" for this member. The absence of this member can effectively be
# taken to mean that the remote server implement version 1.0 of the JSON-RPC protocol.
print '"version":"' . $this->JSON_RPC_VERSION . '",';
if($this->requestID)
print '"id":' . $this->encodeJson($this->requestID) . ',';
}
}
}
function printResponseEnd(){
if($this->responseType == $this->XML)
print "</methodResponse>";
else if(!$this->isJSONOmitResponseWrapper)
print "}";
if($this->responseType == $this->JAVASCRIPT)
print ");";
}
function handleError($errno, $errstr, $errfile, $errline, $errcontext){
$this->respondWithFault($errno, $errstr, $errfile, $errline, $errcontext);
return true;
}
/*BEGIN PHP5**
function handleException($e){
$this->respondWithFault($e->getCode(), $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTrace());
return true;
}
**END PHP5*/
function respondWithFault($errno, $errstr, $errfile = null, $errline = null, $errdetails = null){
ob_clean();
$this->printResponseStart();
if($this->isCallingMethod)
$errstr = "Error raised when calling method \"" . $this->publicMethodName . "\": " . $errstr;
$faultDetails = array();
if($this->isDebugMode){
$faultDetails['file'] = $errfile;
$faultDetails['line'] = $errline;
if(is_array($errdetails) && isset($errdetails[0]))
$faultDetails['trace'] = $errdetails;
else
$faultDetails['context'] = $errdetails;
}
if($this->responseType == $this->XML){
#XML-RPC: Unless there's a lower-level error, always return 200 OK.
#The <methodResponse> could also contain a <fault> which contains a <value> which
# is a <struct> containing two elements, one named <faultCode>, an <int> and one
# named <faultString>, a <string>.
print '<fault><value><struct>';
print '<member>';
print '<name>faultCode</name>';
print '<value><int>' . htmlspecialchars($errno, ENT_NOQUOTES) . '</int></value>';
print '</member>';
print '<member>';
print '<name>faultString</name>';
print '<value>' . htmlspecialchars($errstr, ENT_NOQUOTES) . '</value>';
print '</member>';
if($this->isDebugMode){
print '<member>';
print '<name>faultDetails</name>';
print $this->encodeXmlRpc($faultDetails);
#print '<value>' . htmlspecialchars($errorMessageForXML, ENT_NOQUOTES) . '</value>';
print '</member>';
}
print '</struct></value></fault>';
}
else { #JSON
# Unless noted otherwise, a status code of 500 (Internal Server Error) MUST be
# used under the following conditions:
# * There was an error parsing the JSON text comprising the Procedure Call.
# * The target procedure does not exist on the server. For HTTP GET, a server
# SHOULD use 404 (Not Found) instead of 500.
# * The procedure could not be invoked due to an error resulting from call approximation.
# * The invocation took place but resulted in an error from inside the procedure.
if($this->responseType == $this->JAVASCRIPT) #Status code cannot be examined anyway
header("HTTP/1.1 200 OK");
else if($this->isMethodNotFound)
header("HTTP/1.1 404 Not Found");
else
header("HTTP/1.1 500 Internal Server Error");
#REQUIRED on error. An Object containing error information about the fault that
# occured before, during or after the call. This member MUST be entirely omitted
# if there was no error.
#When a remote procedure call fails, the Procedure Return object MUST contain the
# error member whose value is a JSON Object with the following properties members:
# * name REQUIRED. A String value that MUST read "JSONRPCError".
# * code REQUIRED. A Number value that indicates the actual error
# that occurred. This MUST be an integer between 100 and 999.
# * message REQUIRED. A String value that provides a short description
# of the error. The message SHOULD be limited to a single sentence.
# * error OPTIONAL. A JSON Null, Number, String or Object value that
# carries custom and application-specific error information. Error
# objects MAY be nested using this property.
if($this->isJSONOmitResponseWrapper)
print '{';
print '"error":{';
print '"name":"JSONRPCError",';
print '"code":' . $this->encodeJson($errno) . ',';
print '"message":' . $this->encodeJson($errstr);
if($this->isDebugMode){
print ',"error":';
print $this->encodeJson($faultDetails);
}
#print ',"error":' . ();
print '}';
if($this->isJSONOmitResponseWrapper)
print '}';
}
$this->printResponseEnd();
$this->isFinished = true;
exit;
}
function processRequest(){
if($this->isFinished)
return; //trigger_error("You may only call the 'processRequest' method once.");
if(isset($_COOKIE['JSONDateFormat']))
$this->JSONDateFormat = $_COOKIE['JSONDateFormat'];
if(isset($_COOKIE['dbResultIndexType']))
$this->dbResultIndexType = $_COOKIE['dbResultIndexType'];
$this->publicMethodName = '';
$requestParams = array();
$this->requestID = null;
#############################################################################################
# Parse request
#############################################################################################
#A remote procedure call is made by sending a request to a remote service using either HTTP
# POST or HTTP GET. How and where the call is encoded within the HTTP message depends on
# the HTTP method that is employed. In the case of HTTP POST, the procedure call is carried
# in the body of the HTTP message whereas in the case of HTTP GET, it is expressed along
# the path and query components of the HTTP Request-URI.
if($this->requestType == $this->XML){
/*BEGIN PHP5**
$doc = new DOMDocument();
$doc->loadXML($this->requestData);
**END PHP5*/
/*BEGIN PHP4*/
if(!($doc = domxml_open_mem($this->requestData, DOMXML_LOAD_PARSING, &$error)))
trigger_error("Parse error: " . join(" ... ", $error));
/*END PHP4*/
#The payload is in XML, a single <methodCall> structure.
if(($root = $doc->document_element()) && $root->node_name != 'methodCall')
trigger_error("The root of the document must be a 'methodCall' element.");
unset($root);
#The <methodCall> must contain a <methodName> sub-item, a string, containing the name of
# the method to be called. The string may only contain identifier characters, upper and
# lower-case A-Z, the numeric characters, 0-9, underscore, dot, colon and slash. It's
# entirely up to the server to decide how to interpret the characters in a methodName.
$methodNameElements = $doc->get_elements_by_tagname('methodName');
if(count($methodNameElements) &&
$methodNameElements[0]->first_child &&
$methodNameElements[0]->first_child->node_value)
{
$this->publicMethodName = $methodNameElements[0]->first_child->node_value;
if(preg_match("/[^A-Z0-9_\.:\/]/i", $this->publicMethodName))
trigger_error("The supplied method name \"" . $this->publicMethodName . "\" contains illegal characters. It may may only contain identifier characters, upper and lower-case A-Z, the numeric characters, 0-9, underscore, dot, colon and slash.");
}
#If the procedure call has parameters, the <methodCall> must contain a <params> sub-item.
# The <params> sub-item can contain any number of <param>s, each of which has a <value>.
#Iterate over all params
$paramElements = $doc->get_elements_by_tagname('param');
for($i = 0; $i < count($paramElements); $i++){
$paramEl = $paramElements[$i];
$valueEl = $paramEl->first_child;
while($valueEl && ($valueEl->node_type != 1 || $valueEl->node_name != 'value'))
$valueEl = $valueEl->next_sibling;
if(!$valueEl)
trigger_error("XML-RPC Parse Error: Expected a 'value' element child of the 'param' element.");
array_push($requestParams, $this->decodeXmlRpc($valueEl));
}
}
else if($this->requestType == $this->JSON){
#When using HTTP POST, the call is expressed in the HTTP request body as a JSON Object
# that carries the following members:
$request = $this->decodeJson($this->requestData);
if(!$request)
trigger_error("Parse error in JSON input.");
if(!is_array($request))
trigger_error("Invalid format for JSON-RPC request.");
#id OPTIONAL. This MUST be the same value as that of the id member of Procedure Call
# object to which the response corresponds. This member is maintained for backward
# compatibility with version 1.0 of the specification where it was used to correlate
# a response with its request. If the id member was present on the request, then the
# server MUST repeat it verbatim on the response.
if(!$this->isJSONOmitResponseWrapper && isset($request['id'])) # && ($this->responseType == $this->JSON || $this->responseType == $this->JAVASCRIPT)
$this->requestID = $request['id'];
#version REQUIRED. A String specifying the version of the JSON-RPC protocol to which
# the client conforms. An implementation conforming to this specification MUST use
# the exact String value of "1.1" for this member.
if(!isset($request['version']))
trigger_error("The JSON request object must provide the JSON-RPC version.");
if((float) $request['version'] > 1.1)
trigger_error("This JSON-RPC library supports version 1.1, but you are attempting to use version " . $request['version'] . ".");
#method REQUIRED. A String containing the name of the procedure to be invoked. Procedure
# names that begin with the word system followed by a period character (U+002E or ASCII 46)
# are reserved. In other words, a procedure named system.foobar is considered to have
# reserved semantics.
if(isset($request['method']) && $request['method'])
$this->publicMethodName = $request['method'];
#params OPTIONAL. An Array or Object that holds the actual parameter values for the
# invocation of the procedure.
if(isset($request['params']))
$requestParams = $request['params'];
}
#JSON Spec: 6.3. Call Encoding Using HTTP GET.
else if(isset($_SERVER['PATH_INFO'])) {
#First, an HTTP GET targeting a procedure on a JSON-RPC service is largely indistinguishable
# from a regular HTTP GET transaction. For this reason, there is no mention of of the
# JSON-RPC protocol version being used. Second, the entire call is encoded in the HTTP
# Request-URI. The procedure to invoke is appended to the location of the service, such
# that it appears as the last component of the URI path component. The call parameters
# appear as the query component and are named after the formal arguments of the target procedure.
#When using HTTP GET, the target procedure and parameters for the call are entirely expressed
# within the Request-URI of the HTTP message. The target procedure MUST appear as the last
# component of the Request-URI path component. The procedure's name MUST therefore be preceded
# by a forward-slash (U+002F or ASCII 47) but MUST NOT end in one.
$this->publicMethodName = substr($_SERVER['PATH_INFO'], 1);
if(strrpos($this->publicMethodName, '/') == strlen($this->publicMethodName)-1)
trigger_error("The procedure's name must not end in a forward-slash.");
#The parameters are placed in the query component (as defined in RFC 3986) of the Request-URI,
# which is then formatted using the same scheme as defined for HTML Forms with the get method.
# Each parameters consists of a name/position and value pair that is separated by the equal
# sign (U+003D or ASCII 61) and parameters themselves are separated by an ampersand (U+0026
# or ASCII 38):
$queryPostRequest = $_SERVER["QUERY_STRING"] . '&' . $this->requestData;
#if(preg_match("/=/", $queryPostRequest)){
$queryStringParams = split("&", $queryPostRequest);
foreach($queryStringParams as $param){ #foreach(array_keys($_GET) as $name){
if($param == '')
continue;
#After decoding, the server MUST treat all values as if they were sent as JSON String values.
# The server MAY then perform conversions at its discretion (on a best-attempt basis) if
# the formal arguments of the target procedure expects other non-String values. This
# specification does not define any conversion rules or methods.
#[NOTE: For this to work, $parametersForPrivateProcs must be populated earlier than this point.
# then we could get $parametersForPrivateProcs[$this->publicToPrivateMap[$this->publicMethodName]]]
# and determine the type cast for each parameter.
#Parameters named identically on the query string MUST be collapsed into an Array of String
# values using the same order in which they appear in the query string and identified by
# the repeating parameter name. For instance, the following query string specifies two
# parameters only, namely scale and city:
#It is specifically not possible to send parameters of type Object using HTTP GET.
$pair = split("=", $param, 2);
$key = urldecode($pair[0]);
#$value = isset($pair[1]) ? RPCServer::stringToType(urldecode($pair[1])) : null;
$value = isset($pair[1]) ? urldecode($pair[1]) : null;
if(isset($requestParams[$key])){
if(is_array($requestParams[$key]))
array_push($requestParams[$key], $value);
else $requestParams[$key] = array($requestParams[$key], $value);
}
else $requestParams[$key] = $value;
#if(isset($requestParams[$name])){
# if(is_array($requestParams[$name]))
# array_push($requestParams[$name], $_GET[$name]);
# else $requestParams[$name] = array($requestParams[$name], $_GET[$name]);
#}
#else $requestParams[$name] = $_GET[$name];
}
#}
}
else trigger_error("Unable to discern the requested method name. You must pass the request as " .
"XML-RPC document or JSON-RPC object in the POST data, or by passing the method " .
"name and parameters as path and query components of the HTTP Request-URI respectively.");
if(!$this->publicMethodName)
trigger_error("Method name not provided.");
###################################################################################
# Introspect the methods to determine their parameter lists
###################################################################################
$parametersForPrivateProcs = array();
/*BEGIN PHP5**
**END PHP5*/
/*BEGIN PHP4*/
/*END PHP4*/
#Using the Reflection API in PHP 5.1.0
if(class_exists('ReflectionFunction') && method_exists('ReflectionParameter', 'isArray')){
foreach(array_keys($this->publicToPrivateMap) as $publicProc){
$functionName = $this->publicToPrivateMap[$publicProc];
$rf = new ReflectionFunction($functionName);
$parametersForPrivateProcs[$functionName] = array();
foreach($rf->getParameters() as $param){ #$i =>
if($param->isPassedByReference())
trigger_error("User functions cannot be defined with parameters passed by reference. The function \"$functionName\" wants the parameter \"" . $param->getName() . "\" to be passed by reference.");
$paramDetails = array('name' => $param->getName(),
'type' => 'any');
if($param->isDefaultValueAvailable())
$paramDetails['default'] = $param->getDefaultValue();
if($param->isArray())
$paramDetails['type'] = 'arr';
else if($rc = $param->getClass()){
switch(strtolower($rc->getName())){
case 'datetime':
$paramDetails['type'] = 'obj';
break;
default:
trigger_error("The only class type that may be hinted is DateTime; you provided \"" . $rc->getName() . "\" in the function \"$functionName\".");
}
}
//$param->isOptional() and $param->allowsNull() are not needed since PHP will raise errors
// when call_user_func is invoked.
array_push($parametersForPrivateProcs[$functionName], $paramDetails);
}
}
unset($functionName);
unset($rf);
unset($rc);
unset($param);
unset($paramDetails);
}
#Using the PHP tokenizer before PHP 5.1.0
else {
$privateToPublicMap = array_flip($this->publicToPrivateMap);
if($this->isUsingIncludedFunctions)
$sourceFiles = get_included_files();
else
$sourceFiles = array();
array_push($sourceFiles, $_SERVER['SCRIPT_FILENAME']);
foreach($sourceFiles as $sourceFile){
if($sourceFile == __FILE__)
continue;
$tokens = token_get_all(file_get_contents($sourceFile));
#Remove all comments and whitespace tokens
for($i = 0; $i < count($tokens); ){
if(is_array($tokens[$i]) && ($tokens[$i][0] == T_WHITESPACE || $tokens[$i][0] == T_COMMENT))
array_splice($tokens, $i, 1);
else $i++;
}
$inClassDef = false;
$braceDepth = 0;
for($i = 0; $i < count($tokens); $i++){
#Skip all class definitions #################################
if(is_array($tokens[$i])){
if($tokens[$i][0] == T_CLASS){
$inClassDef = true;
continue;
}
}
else if($tokens[$i] == '{'){
$braceDepth++;
}
#If token is closing brace and the brace depth is now zero, then class definition complete
else if($tokens[$i] == '}' && (--$braceDepth == 0)){
$inClassDef = false;
++$i; #move to next token after class
}
if($inClassDef)
continue;
# Parse function declarations ################################################
if(is_array($tokens[$i]) && $tokens[$i][0] == T_FUNCTION){
#Get function name
++$i; #now $tokens[$i] == the function name
$functionName = $tokens[$i][1];
if(!isset($privateToPublicMap[$functionName]))
continue;
$parametersForPrivateProcs[$functionName] = array();
#Get parameter list
++$i; #now $tokens[$i] == '('
$parenDepth = 1;
#Iterate over every parameter for the function
for($i++; $i < count($tokens) && $parenDepth > 0; $i++){
if(is_array($tokens[$i])){
#Parameter name found: obtain all type information available
if($parenDepth == 1 && $tokens[$i][0] == T_VARIABLE){
$paramDetails = array('name' => substr($tokens[$i][1], 1),
'type' => 'any');
#Get the argument type
if(is_array($tokens[$i-1])){
#Object parameter (specifically DateTime)
if($tokens[$i-1][0] == T_STRING){
switch(strtolower($tokens[$i-1][1])){
case 'datetime':
#$paramType = "datetime";
#$paramType = "obj";
$paramDetails['type'] = 'obj';
break;
default:
trigger_error("The only class type that may be hinted is DateTime; you provided \"" . $tokens[$i-1][1] . "\" in the function \"$functionName\".");
}
}
#Array parameter
else if($tokens[$i-1][0] == T_ARRAY){
#$paramType = 'arr';
$paramDetails['type'] = 'arr';
}
}
#Of course data may not be passed from the client by reference
else if($tokens[$i-1] == '&')
trigger_error("User functions cannot be defined with parameters passed by reference. The function \"$functionName\" wants the parameter \"" . $paramDetails['name'] . "\" to be passed by reference.");
#Get the default value if it was was provided
if($this->defaultParametersPreserved && $tokens[$i+1] == '='){
#$paramDetails['default']
if(is_array($tokens[$i+2])){
switch($tokens[$i+2][0]){
case T_ARRAY:
#Iterate over the next tokens to compose a string of the entire
# literal array value used for the default; then eval this string
$evalVal = 'array(';
$arrayParenDepth = 1;
for($i += 3; $arrayParenDepth > 0; $i++){
if(is_array($tokens[$i+1])){
$evalVal .= $tokens[$i+1][1];
}
else {
$evalVal .= $tokens[$i+1];
if($tokens[$i+1] == ')')
$arrayParenDepth--;
else if($tokens[$i+1] == '(')
$arrayParenDepth++;
}
}
$paramDetails['default'] = eval("return $evalVal;"); #array(); #NOTE: MORE NEEDED HERE: recursive parsing of values
break;
case T_CONSTANT_ENCAPSED_STRING:
$i += 2;
$paramDetails['default'] = eval('return ' . $tokens[$i][1] . ';');
break;
case T_DNUMBER:
$i += 2;
$paramDetails['default'] = (double) $tokens[$i][1];
break;
case T_LNUMBER:
$i += 2;
$paramDetails['default'] = (int) $tokens[$i][1];
break;
case T_STRING:
$i += 2;
if(defined($tokens[$i][1])) #Bare string is a constant
$paramDetails['default'] = eval('return ' . $tokens[$i][1] . ';');
else
$paramDetails['default'] = $tokens[$i][1];
break;
}
}
}
array_push($parametersForPrivateProcs[$functionName], $paramDetails);
}
}
#Determine when the parameter list is entered and exited
else {
if($tokens[$i] == '(')
$parenDepth++;
else if($tokens[$i] == ')'){
$parenDepth--;
#This right paren marks the end of the parameter list
if($parenDepth == 0)
break;
}
}
}
}
}
}
unset($tokens);
unset($sourceFiles);
unset($sourceFile);
unset($paramDetails);
unset($parenDepth);
unset($functionName);
unset($braceDepth);
unset($inClassDef);
unset($privateToPublicMap);
#Iterate over all public methods and see if their parameter lists have been found
# by parsing the tokens of the PHP functions.
foreach($this->publicToPrivateMap as $publicProc => $privateProc){
if(!isset($parametersForPrivateProcs[$privateProc]))
trigger_error("Because this version of PHP does not support the Reflection API, unable to use the public method \"$publicProc\" because its associated private ".
"procedure is located in an included file; to get around this, you must explicitly ".
"allow externally defined functions by calling \$RPCServerInstance->useIncludedFunctions(true); this will decrease performance. ".
"Note you may not use non-user-defined functions as public methods (native PHP functions may not be used) when the Reflection API is not available.");
}
unset($publicProc);
unset($privateProc);
}
#############################################################################################
# Execute request
#############################################################################################
#A JSON-RPC service MUST, at a mimum, support a procedure called system.describe. The result
# of calling this procedure without any parameters MUST be a Service Description object
# as described in the next section.
if($this->publicMethodName == 'system.describe'){
#A service description is a JSON Object with the following members or properties:
$this->responseData = array();
#sdversion REQUIRED. A String value that represents the version number of this object
# and MUST read "1.0" for conforming implementations.
$this->responseData['sdversion'] = '1.0';
#name REQUIRED. A String value that provides a simple name for the method.
if(isset($this->name))
$this->responseData['name'] = $this->name;
#id REQUIRED. A String value that uniquely and globally identifies the service. The
# string MUST use the URI Generic Syntax (RFC 3986).
if(isset($this->id))
$this->responseData['id'] = $this->id;
#version OPTIONAL. A String value that indicates version number of the service and MAY
# be used by the applications for checking compatibility. The version number, when
# present, MUST include a major and minor component separated by a period (U+002E or
# ASCII 46). The major and minor components MUST use decimal digits (0 to 9) only. For
# example, use "2.5" to mean a major version of 2 and a minor version of 5. The use and
# interpretation of the version number is left at the discretion of the applications
# treating the Service Description.
if(isset($this->version))
$this->responseData['version'] = $this->version;
#summary OPTIONAL. A String value that summarizes the purpose of the service. This SHOULD
# be kept to a maximum of 5 sentences and often limited a single phrase like, "The News
# Search service allows you to search the Internet for news stories."
if(isset($this->summary))
$this->responseData['summary'] = $this->summary;
#help OPTIONAL. A String value that is a URL from where human-readable documentation about
# the service may be obtained.
if(isset($this->help))
$this->responseData['help'] = $this->help;
#address OPTIONAL. A String value that is the URL of the service end-point to which the remote
# procedure calls can be targeted. The protocol scheme of this URL SHOULD be http or https.
# Although this value is optional, it is highly RECOMMENDED that a service always publish its
# address so that a service description obtained indirectly can be used nonetheless to locate
# the service.
if(isset($this->address))
$this->responseData['address'] = $this->address;
#procs OPTIONAL. An Array value whose element contain Service Procedure Description objects,
# each of uniquely describes a single procedure. If the only description of each procedure
# that a service has is its name, then it MAY instead supply an Array of String elements for
# this member and where each element uniquely names a procedure.
$this->responseData['procs'] = array();
foreach($this->publicToPrivateMap as $publicName => $privateName){
#A procedure description is a JSON Object with the following members and properties:
$proc = array();
#name REQUIRED. A String value that provides a simple name for the method.
$proc['name'] = $publicName;
#params OPTIONAL. An Array value whose elements are either Procedure Parameter Description
# objects or String values. If an element each of uniquely describes a single parameter of
# the procedure. If the only description that is available of each parameter is its name,
# then a service MAY instead supply an Array of String elements for this member and where
# each element uniquely names a parameter and the parameter is assumed to be typed as "any".
# In either case, the elements of the array MUST be ordered after the formal argument list of
# the procedure being described. If this member is missing or the Null value then the procedure
# does not expect any parameters.
$proc['params'] = array();
foreach($parametersForPrivateProcs[$privateName] as $param){
array_push($proc['params'], array("name" => $param['name'], "type" => $param['type']));
}
#summary OPTIONAL. A String value that summarizes the purpose of the service. This SHOULD be
# kept to a maximum of 3 sentences and often limited to a single phrase like, "Lets you
# search for hyperlinks that have been tagged by particular tags."
#help OPTIONAL. A String value that is a URL from where human-readable documentation about the
# procedure may be obtained.
#idempotent OPTIONAL. A Boolean value that indicates whether the procedure is idempotent and
# therefore essentially safe to invoke over an HTTP GET transaction. This member MUST be
# present and true for the procedure to be considered idempotent.
#return OPTIONAL. An Object value that is structured after the Procedure Parameter Description
# and which describes the output from the procedure. Otherwise, if it is a String value, then
# it defines the type of the return value. If this member is missing or is the Null value
# then the return type of the procedure is defined to be "any".
array_push($this->responseData['procs'], $proc);
}
array_push($this->responseData['procs'],
array('name' => "system.setJSONDateFormat", 'params' =>
array(array('name' => 'formatName', 'type' => 'str')) #, 'default' => false
)
);
array_push($this->responseData['procs'],
array('name' => "system.setDBResultIndexType", 'params' =>
array(array('name' => 'indexType', 'type' => 'str')) #, 'default' => false
)
);
}
#system.listMethods -- This method may be used to enumerate the methods implemented by the
# XML-RPC server. The system.listMethods method requires no parameters. It returns an
# array of strings, each of which is the name of a method implemented by the server.
# From <http://xmlrpc.usefulinc.com/doc/reserved.html>
else if($this->publicMethodName == 'system.listMethods'){
$this->responseData = array_keys($this->publicToPrivateMap);
array_push($this->responseData, "system.setJSONDateFormat");
array_push($this->responseData, "system.setDBResultIndexType");
}
#system.methodSignature -- It returns an array of possible signatures for this method.
# A signature is an array of types. The first of these types is the return type of
# the method, the rest are parameters.
# From <http://xmlrpc.usefulinc.com/doc/reserved.html>
//else if($this->publicMethodName == 'system.methodSignature'){
// trigger_error('This implementation does not support "system.methodSignature" since PHP does not have type declarations.');
//
// #This method takes one parameter, the name of a method implemented by the XML-RPC server.
// if(count($requestParams) != 1) #!function_exists($requestParams[0])
// trigger_error('The "system.methodSignature" method must be supplied a method name.');
// if(!isset($this->publicToPrivateMap[$requestParams[0]]))
// trigger_error('The supplied method name "' . $requestParams[0] . '" does not exist.');
//}
#system.methodHelp -- This method takes one parameter, the name of a method
# implemented by the XML-RPC server. It returns a documentation string describing
# the use of that method. If no such string is available, an empty string is returned.
# From <http://xmlrpc.usefulinc.com/doc/reserved.html>
//else if($this->publicMethodName == 'system.methodHelp'){
// trigger_error('This implementation does not support "system.methodHelp" since PHP does not have type declarations.');
//}
#system.describeMethods -- This method accepts an optional array argument which is a list of
# strings representing method names. It returns a struct containing method and type
# descriptions. Each method description is a complex nested struct. If no argument is given,
# it will return descriptions for all methods which have a description registered. (Note that
# this may not be a comprehensive list of all available methods. system.listMethods should be
# used for that.) Each type description is a complex nested struct. All types referenced by
# the returned methods should be in the type list. Additional types may also be present.
# See <[email protected]>
//else if($this->publicMethodName == 'system.describeMethods'){
// trigger_error('This implementation does not support "system.describeMethods" since PHP does not have type declarations.');
//}
else if($this->publicMethodName == 'system.setJSONDateFormat'){
if(!count($requestParams))
trigger_error('Parameter must be either "ISO8601", "classHinting", "@ticks@", or "ASP.NET".');
$keys = array_keys($requestParams);
$this->responseData = $this->setJSONDateFormat($requestParams[$keys[0]]);
}
else if($this->publicMethodName == 'system.setDBResultIndexType'){
if(!count($requestParams))
trigger_error('Parameter must be either "NUM" or "ASSOC"..');
$keys = array_keys($requestParams);
$this->responseData = $this->setDBResultIndexType($requestParams[$keys[0]]);
}
#Execute the user-defined requested method ###########################################
else {
#Verify that the supplied method is valid
if(!isset($this->publicToPrivateMap[$this->publicMethodName]) || !function_exists($this->publicToPrivateMap[$this->publicMethodName])){
$this->isMethodNotFound = true;
trigger_error("The function referred to by the public method \"" . $this->publicMethodName . "\" does not exist.", E_USER_ERROR);
}
#If the value of the params member is any other type except JSON Object or Array,
# then the server MUST reject the call with an error.