1 <?php
2
3 namespace HieuLe\WordpressXmlrpcClient;
4
5 6 7 8 9 10 11 12 13
14 class WordpressClient
15 {
16
17 private $username;
18 private $password;
19 private $endPoint;
20 private $request;
21 private $responseHeader = array();
22 private $error;
23 private $proxyConfig = false;
24 private $authConfig = false;
25 private $userAgent;
26
27 28 29
30 private $_callbacks = array();
31
32 33 34 35 36 37 38 39
40 public function __construct($xmlrpcEndPoint = null, $username = null, $password = null, $logger = null)
41 {
42 $this->setCredentials($xmlrpcEndPoint, $username, $password);
43 $this->userAgent = $this->getDefaultUserAgent();
44 }
45
46 47 48 49 50 51 52
53 function onError($callback)
54 {
55 $this->_callbacks['error'][] = $callback;
56 }
57
58 59 60 61 62 63 64
65 function onSending($callback)
66 {
67 $this->_callbacks['sending'][] = $callback;
68 }
69
70 71 72 73 74 75 76 77 78
79 function setCredentials($xmlrpcEndPoint, $username, $password)
80 {
81
82 $scheme = parse_url($xmlrpcEndPoint, PHP_URL_SCHEME);
83 if (!$scheme) {
84 $xmlrpcEndPoint = "http://{$xmlrpcEndPoint}";
85 }
86
87
88 $host = parse_url($xmlrpcEndPoint, PHP_URL_HOST);
89 if (substr($host, -14) == '.wordpress.com') {
90 $xmlrpcEndPoint = preg_replace('|http://|', 'https://', $xmlrpcEndPoint, 1);
91 }
92
93
94 $this->endPoint = $xmlrpcEndPoint;
95 $this->username = $username;
96 $this->password = $password;
97 }
98
99 100 101 102 103 104 105
106 function getEndPoint()
107 {
108 return $this->endPoint;
109 }
110
111 112 113 114 115 116 117
118 function getDefaultUserAgent()
119 {
120 $phpVersion = phpversion();
121 $curlVersion = curl_version();
122
123 return "XML-RPC client (hieu-le/wordpress-xmlrpc-client 2.4.0) PHP {$phpVersion} cUrl {$curlVersion['version']}";
124 }
125
126 127 128 129 130 131 132
133 function getUserAgent()
134 {
135 return $this->userAgent;
136 }
137
138 139 140 141 142 143 144
145 function setUserAgent($userAgent)
146 {
147 if ($userAgent) {
148 $this->userAgent = $userAgent;
149 } else {
150 $this->userAgent = $this->getDefaultUserAgent();
151 }
152 }
153
154 155 156 157 158 159 160
161 function getErrorMessage()
162 {
163 return $this->error;
164 }
165
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
183 function setProxy($proxyConfig)
184 {
185 if ($proxyConfig === false || is_array($proxyConfig)) {
186 $this->proxyConfig = $proxyConfig;
187 } else {
188 throw new \InvalidArgumentException(__METHOD__ . " only accept boolean 'false' or an array as parameter.");
189 }
190 }
191
192 193 194 195 196 197
198 function getProxy()
199 {
200 return $this->proxyConfig;
201 }
202
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
218 function setAuth($authConfig)
219 {
220 if ($authConfig === false || is_array($authConfig)) {
221 $this->authConfig = $authConfig;
222 } else {
223 throw new \InvalidArgumentException(__METHOD__ . " only accept boolean 'false' or an array as parameter.");
224 }
225 }
226
227 228 229 230 231 232
233 function getAuth()
234 {
235 return $this->authConfig;
236 }
237
238 239 240 241 242 243 244 245 246
247 function getPost($postId, array $fields = array())
248 {
249 if (empty($fields)) {
250 $params = array(1, $this->username, $this->password, $postId);
251 } else {
252 $params = array(1, $this->username, $this->password, $postId, $fields);
253 }
254
255 return $this->sendRequest('wp.getPost', $params);
256 }
257
258 259 260 261 262 263 264 265 266
267 function getPosts(array $filters = array(), array $fields = array())
268 {
269 $params = array(1, $this->username, $this->password, $filters);
270 if (!empty($fields)) {
271 $params[] = $fields;
272 }
273
274 return $this->sendRequest('wp.getPosts', $params);
275 }
276
277 278 279 280 281 282 283 284 285 286 287 288 289
290 function newPost($title, $body, array $content = array())
291 {
292 $default = array(
293 'post_type' => 'post',
294 'post_status' => 'publish',
295 );
296 $content = array_merge($default, $content);
297 $content['post_title'] = $title;
298 $content['post_content'] = $body;
299
300 $params = array(1, $this->username, $this->password, $content);
301
302 return $this->sendRequest('wp.newPost', $params);
303 }
304
305 306 307 308 309 310 311 312 313 314 315 316 317 318
319 function editPost($postId, array $content)
320 {
321 $params = array(1, $this->username, $this->password, $postId, $content);
322
323 return $this->sendRequest('wp.editPost', $params);
324 }
325
326 327 328 329 330 331 332 333 334
335 function deletePost($postId)
336 {
337 $params = array(1, $this->username, $this->password, $postId);
338
339 return $this->sendRequest('wp.deletePost', $params);
340 }
341
342 343 344 345 346 347 348 349 350 351
352 function getPostType($postTypeName, array $fields = array())
353 {
354 $params = array(1, $this->username, $this->password, $postTypeName, $fields);
355
356 return $this->sendRequest('wp.getPostType', $params);
357 }
358
359 360 361 362 363 364 365 366 367 368
369 function getPostTypes(array $filter = array(), array $fields = array())
370 {
371 $params = array(1, $this->username, $this->password, $filter, $fields);
372
373 return $this->sendRequest('wp.getPostTypes', $params);
374 }
375
376 377 378 379 380 381 382
383 function getPostFormats()
384 {
385 $params = array(1, $this->username, $this->password);
386
387 return $this->sendRequest('wp.getPostFormats', $params);
388 }
389
390 391 392 393 394 395 396
397 function getPostStatusList()
398 {
399 $params = array(1, $this->username, $this->password);
400
401 return $this->sendRequest('wp.getPostStatusList', $params);
402 }
403
404 405 406 407 408 409 410 411 412
413 function getTaxonomy($taxonomy)
414 {
415 $params = array(1, $this->username, $this->password, $taxonomy);
416
417 return $this->sendRequest('wp.getTaxonomy', $params);
418 }
419
420 421 422 423 424 425 426
427 function getTaxonomies()
428 {
429 $params = array(1, $this->username, $this->password);
430
431 return $this->sendRequest('wp.getTaxonomies', $params);
432 }
433
434 435 436 437 438 439 440 441 442 443
444 function getTerm($termId, $taxonomy)
445 {
446 $params = array(1, $this->username, $this->password, $taxonomy, $termId);
447
448 return $this->sendRequest('wp.getTerm', $params);
449 }
450
451 452 453 454 455 456 457 458 459 460
461 function getTerms($taxonomy, array $filter = array())
462 {
463 $params = array(1, $this->username, $this->password, $taxonomy, $filter);
464
465 return $this->sendRequest('wp.getTerms', $params);
466 }
467
468 469 470 471 472 473 474 475 476 477 478 479 480
481 function newTerm($name, $taxomony, $slug = null, $description = null, $parentId = null)
482 {
483 $content = array(
484 'name' => $name,
485 'taxonomy' => $taxomony,
486 );
487 if ($slug) {
488 $content['slug'] = $slug;
489 }
490 if ($description) {
491 $content['description'] = $description;
492 }
493 if ($parentId) {
494 $content['parent'] = $parentId;
495 }
496 $params = array(1, $this->username, $this->password, $content);
497
498 return $this->sendRequest('wp.newTerm', $params);
499 }
500
501 502 503 504 505 506 507 508 509 510 511
512 function editTerm($termId, $taxonomy, array $content = array())
513 {
514 $content['taxonomy'] = $taxonomy;
515 $params = array(1, $this->username, $this->password, $termId, $content);
516
517 return $this->sendRequest('wp.editTerm', $params);
518 }
519
520 521 522 523 524 525 526 527 528 529
530 function deleteTerm($termId, $taxonomy)
531 {
532 $params = array(1, $this->username, $this->password, $taxonomy, $termId);
533
534 return $this->sendRequest('wp.deleteTerm', $params);
535 }
536
537 538 539 540 541 542 543 544 545
546 function getMediaItem($itemId)
547 {
548 $params = array(1, $this->username, $this->password, $itemId);
549
550 return $this->sendRequest('wp.getMediaItem', $params);
551 }
552
553 554 555 556 557 558 559 560 561
562 function getMediaLibrary(array $filter = array())
563 {
564 $params = array(1, $this->username, $this->password, $filter);
565
566 return $this->sendRequest('wp.getMediaLibrary', $params);
567 }
568
569 570 571 572 573 574 575 576 577 578 579 580 581
582 function uploadFile($name, $mime, $bits, $overwrite = null, $postId = null)
583 {
584 xmlrpc_set_type($bits, 'base64');
585 $struct = array(
586 'name' => $name,
587 'type' => $mime,
588 'bits' => $bits,
589 );
590 if ($overwrite !== null) {
591 $struct['overwrite'] = $overwrite;
592 }
593 if ($postId !== null) {
594 $struct['post_id'] = (int)$postId;
595 }
596 $params = array(1, $this->username, $this->password, $struct);
597
598 return $this->sendRequest('wp.uploadFile', $params);
599 }
600
601 602 603 604 605 606 607 608 609
610 function getCommentCount($postId)
611 {
612 $params = array(1, $this->username, $this->password, $postId);
613
614 return $this->sendRequest('wp.getCommentCount', $params);
615 }
616
617 618 619 620 621 622 623 624 625
626 function getComment($commentId)
627 {
628 $params = array(1, $this->username, $this->password, $commentId);
629
630 return $this->sendRequest('wp.getComment', $params);
631 }
632
633 634 635 636 637 638 639 640 641
642 function getComments(array $filter = array())
643 {
644 $params = array(1, $this->username, $this->password, $filter);
645
646 return $this->sendRequest('wp.getComments', $params);
647 }
648
649 650 651 652 653 654 655 656 657 658
659 function newComment($post_id, array $comment)
660 {
661 $params = array(1, $this->username, $this->password, $post_id, $comment);
662
663 return $this->sendRequest('wp.newComment', $params);
664 }
665
666 667 668 669 670 671 672 673 674 675
676 function editComment($commentId, array $comment)
677 {
678 $params = array(1, $this->username, $this->password, $commentId, $comment);
679
680 return $this->sendRequest('wp.editComment', $params);
681 }
682
683 684 685 686 687 688 689 690 691
692 function deleteComment($commentId)
693 {
694 $params = array(1, $this->username, $this->password, $commentId);
695
696 return $this->sendRequest('wp.deleteComment', $params);
697 }
698
699 700 701 702 703 704 705
706 function getCommentStatusList()
707 {
708 $params = array(1, $this->username, $this->password);
709
710 return $this->sendRequest('wp.getCommentStatusList', $params);
711 }
712
713 714 715 716 717 718 719 720 721
722 function getOptions(array $options = array())
723 {
724 if (empty($options)) {
725 $params = array(1, $this->username, $this->password);
726 } else {
727 $params = array(1, $this->username, $this->password, $options);
728 }
729
730 return $this->sendRequest('wp.getOptions', $params);
731 }
732
733 734 735 736 737 738 739 740 741
742 function setOptions(array $options)
743 {
744 $params = array(1, $this->username, $this->password, $options);
745
746 return $this->sendRequest('wp.setOptions', $params);
747 }
748
749 750 751 752 753 754 755
756 function getUsersBlogs()
757 {
758 $params = array($this->username, $this->password);
759
760 return $this->sendRequest('wp.getUsersBlogs', $params);
761 }
762
763 764 765 766 767 768 769 770 771 772
773 function getUser($userId, array $fields = array())
774 {
775 $params = array(1, $this->username, $this->password, $userId);
776 if (!empty($fields)) {
777 $params[] = $fields;
778 }
779
780 return $this->sendRequest('wp.getUser', $params);
781 }
782
783 784 785 786 787 788 789 790 791 792
793 function getUsers(array $filters = array(), array $fields = array())
794 {
795 $params = array(1, $this->username, $this->password, $filters);
796 if (!empty($fields)) {
797 $params[] = $fields;
798 }
799
800 return $this->sendRequest('wp.getUsers', $params);
801 }
802
803 804 805 806 807 808 809 810 811
812 function getProfile(array $fields = array())
813 {
814 $params = array(1, $this->username, $this->password);
815 if (!empty($fields)) {
816 $params[] = $fields;
817 }
818
819 return $this->sendRequest('wp.getProfile', $params);
820 }
821
822 823 824 825 826 827 828 829 830
831 function editProfile(array $content)
832 {
833 $params = array(1, $this->username, $this->password, $content);
834
835 return $this->sendRequest('wp.editProfile', $params);
836 }
837
838 839 840 841 842 843 844 845 846 847
848 public function callCustomMethod($method, $params)
849 {
850 return $this->sendRequest($method, $params);
851 }
852
853 854 855 856 857 858 859
860 public function createXMLRPCDateTime($datetime)
861 {
862 $value = $datetime->format('Ymd\TH:i:sO');
863 xmlrpc_set_type($value, 'datetime');
864
865 return $value;
866 }
867
868 protected function performRequest()
869 {
870 if (function_exists('curl_init')) {
871 return $this->requestWithCurl();
872 } else {
873 return $this->requestWithFile();
874 }
875 }
876
877 protected function getRequest()
878 {
879 return $this->request;
880 }
881
882 private function sendRequest($method, $params)
883 {
884 if (!$this->endPoint) {
885 $this->error = "Invalid endpoint " . json_encode(array(
886 'endpoint' => $this->endPoint,
887 'username' => $this->username,
888 'password' => $this->password,
889 ));
890 $this->logError();
891 throw new \Exception($this->error);
892 }
893 $this->responseHeader = array();
894
895
896
897 if (version_compare(PHP_VERSION, '7.0.0', '<')) {
898 $this->setXmlrpcType($params);
899 }
900
901 $this->request = xmlrpc_encode_request($method, $params,
902 array('encoding' => 'UTF-8', 'escaping' => 'markup', 'version' => 'xmlrpc'));
903 $body = "";
904
905 $callbacks = $this->getCallback('sending');
906 $event = array(
907 'event' => 'sending',
908 'endpoint' => $this->endPoint,
909 'username' => $this->username,
910 'password' => $this->password,
911 'method' => $method,
912 'params' => $params,
913 'request' => $this->request,
914 'proxy' => $this->proxyConfig,
915 'auth' => $this->authConfig,
916 );
917 foreach ($callbacks as $callback) {
918 call_user_func($callback, $event);
919 }
920 $body = $this->performRequest();
921 $response = xmlrpc_decode($body, 'UTF-8');
922 if (is_array($response) && xmlrpc_is_fault($response)) {
923 $this->error = ("xmlrpc: {$response['faultString']} ({$response['faultCode']})");
924 $this->logError();
925 throw new Exception\XmlrpcException($response['faultString'], $response['faultCode']);
926 }
927
928 return $response;
929 }
930
931 932 933 934 935 936 937
938 private function setXmlrpcType(&$array)
939 {
940 foreach ($array as $key => $element) {
941 if (is_a($element, '\DateTime')) {
942 $array[$key] = $element->format("Ymd\TH:i:sO");
943 xmlrpc_set_type($array[$key], 'datetime');
944 } elseif (is_array($element)) {
945 $this->setXmlrpcType($array[$key]);
946 }
947 }
948 }
949
950 private function requestWithCurl()
951 {
952 $ch = curl_init($this->endPoint);
953 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
954 curl_setopt($ch, CURLOPT_POST, 1);
955 curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
956 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
957 curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent);
958 if ($this->proxyConfig != false) {
959 if (isset($this->proxyConfig['proxy_ip'])) {
960 curl_setopt($ch, CURLOPT_PROXY, $this->proxyConfig['proxy_ip']);
961 }
962 if (isset($this->proxyConfig['proxy_port'])) {
963 curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxyConfig['proxy_port']);
964 }
965 if (isset($this->proxyConfig['proxy_user']) && isset($this->proxyConfig['proxy_pass'])) {
966 curl_setopt($ch, CURLOPT_PROXYUSERPWD,
967 "{$this->proxyConfig['proxy_user']}:{$this->proxyConfig['proxy_pass']}");
968 }
969 if (isset($this->proxyConfig['proxy_mode'])) {
970 curl_setopt($ch, CURLOPT_PROXYAUTH, $this->proxyConfig['proxy_mode']);
971 }
972 }
973 if ($this->authConfig) {
974 if (isset($this->authConfig['auth_user']) && isset($this->authConfig['auth_pass'])) {
975 curl_setopt($ch, CURLOPT_USERPWD,
976 "{$this->authConfig['auth_user']}:{$this->authConfig['auth_pass']}");
977 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
978 }
979 if (isset($this->authConfig['auth_mode'])) {
980 curl_setopt($ch, CURLOPT_HTTPAUTH, $this->authConfig['auth_mode']);
981 }
982 }
983 $response = curl_exec($ch);
984 if (curl_errno($ch)) {
985 $message = curl_error($ch);
986 $code = curl_errno($ch);
987 $this->error = "curl: {$message} ({$code})";
988 $this->logError();
989 curl_close($ch);
990 throw new Exception\NetworkException($message, $code);
991 }
992 $httpStatusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
993 if ($httpStatusCode >= 400) {
994 $message = $response;
995 $code = $httpStatusCode;
996 $this->error = "http: {$message} ({$code})";
997 $this->logError();
998 curl_close($ch);
999 throw new Exception\NetworkException($message, $code);
1000 }
1001 curl_close($ch);
1002
1003 return $response;
1004 }
1005
1006 private function requestWithFile()
1007 {
1008 $contextOptions = array(
1009 'http' => array(
1010 'method' => "POST",
1011 'user_agent' => $this->userAgent,
1012 'header' => "Content-Type: text/xml\r\n",
1013 'content' => $this->request,
1014 ),
1015 );
1016
1017 if ($this->proxyConfig != false) {
1018 if (isset($this->proxyConfig['proxy_ip']) && isset($this->proxyConfig['proxy_port'])) {
1019 $contextOptions['http']['proxy'] = "tcp://{$this->proxyConfig['proxy_ip']}:{$this->proxyConfig['proxy_port']}";
1020 $contextOptions['http']['request_fulluri'] = true;
1021 }
1022 if (isset($this->proxyConfig['proxy_user']) && isset($this->proxyConfig['proxy_pass'])) {
1023 $auth = base64_encode("{$this->proxyConfig['proxy_user']}:{$this->proxyConfig['proxy_pass']}");
1024 $contextOptions['http']['header'] .= "Proxy-Authorization: Basic {$auth}\r\n";
1025 }
1026 if (isset($this->proxyConfig['proxy_mode'])) {
1027 throw new \InvalidArgumentException('Cannot use NTLM proxy authorization without cURL extension');
1028 }
1029 }
1030 if ($this->authConfig) {
1031 if (isset($this->authConfig['auth_user']) && isset($this->authConfig['auth_pass'])) {
1032 $auth = base64_encode("{$this->authConfig['auth_user']}:{$this->authConfig['auth_pass']}");
1033 $contextOptions['http']['header'] .= "Authorization: Basic {$auth}\r\n";
1034 }
1035 if (isset($this->authConfig['auth_mode'])) {
1036 throw new \InvalidArgumentException('Cannot use other authentication method without cURL extension');
1037 }
1038 }
1039 $context = stream_context_create($contextOptions);
1040 $http_response_header = array();
1041 try {
1042 $file = @file_get_contents($this->endPoint, false, $context);
1043 if ($file === false) {
1044 $error = error_get_last();
1045 $error = $error ? trim($error['message']) : "error";
1046 $this->error = "file_get_contents: {$error}";
1047 $this->logError();
1048 throw new Exception\NetworkException($error, 127);
1049 }
1050 } catch (\Exception $ex) {
1051 $this->error = ("file_get_contents: {$ex->getMessage()} ({$ex->getCode()})");
1052 $this->logError();
1053 throw new Exception\NetworkException($ex->getMessage(), $ex->getCode());
1054 }
1055
1056 return $file;
1057 }
1058
1059 private function logError()
1060 {
1061 $callbacks = $this->getCallback('error');
1062 $event = array(
1063 'event' => 'error',
1064 'endpoint' => $this->endPoint,
1065 'request' => $this->request,
1066 'proxy' => $this->proxyConfig,
1067 'auth' => $this->authConfig,
1068 );
1069 foreach ($callbacks as $callback) {
1070 call_user_func($callback, $this->error, $event);
1071 }
1072 }
1073
1074 private function getCallback($name)
1075 {
1076 $callbacks = array();
1077 if (isset($this->_callbacks[$name]) && is_array($this->_callbacks[$name])) {
1078 $callbacks = $this->_callbacks[$name];
1079 }
1080
1081 return $callbacks;
1082 }
1083
1084 }
1085