Textpattern PHP Cross Reference Content Management Systems

Source: /rpc/TXP_RPCServer.php - 1110 lines - 36208 bytes - Summary - Text - Print

   1  <?php
   2  
   3  /*
   4   * Textpattern Content Management System
   5   * https://textpattern.com/
   6   *
   7   * XML-RPC Server for Textpattern 4.0.x
   8   * https://web.archive.org/web/20150119065246/http://txp.kusor.com/rpc-api
   9   *
  10   * Copyright (C) 2020 The Textpattern Development Team
  11   * Author: Pedro Palazón
  12   *
  13   * This file is part of Textpattern.
  14   *
  15   * Textpattern is free software; you can redistribute it and/or
  16   * modify it under the terms of the GNU General Public License
  17   * as published by the Free Software Foundation, version 2.
  18   *
  19   * Textpattern is distributed in the hope that it will be useful,
  20   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22   * GNU General Public License for more details.
  23   *
  24   * You should have received a copy of the GNU General Public License
  25   * along with Textpattern. If not, see <https://www.gnu.org/licenses/>.
  26   */
  27  
  28  if (!defined('txpath')) {
  29      die('txpath is undefined.');
  30  }
  31  
  32  require_once txpath.'/lib/txplib_html.php';
  33  
  34  class TXP_RPCServer extends IXR_IntrospectionServer
  35  {
  36      function __construct()
  37      {
  38          global $enable_xmlrpc_server, $HTTP_RAW_POST_DATA;
  39  
  40          if (!$HTTP_RAW_POST_DATA) {
  41              $HTTP_RAW_POST_DATA = file_get_contents('php://input');
  42          }
  43  
  44          parent::__construct();
  45  
  46          // Add API Methods as callbacks.
  47          if ($enable_xmlrpc_server) {
  48              // Blogger API [https://developers.google.com/blogger/docs/2.0/reference] - add as
  49              // server capability.
  50              $this->capabilities['bloggerAPI'] = array(
  51                  'specUrl'     => 'https://developers.google.com/blogger/docs/2.0/reference',
  52                  'specVersion' => 2,
  53              );
  54              $this->addCallback(
  55                  'blogger.newPost',
  56                  'this:blogger_newPost',
  57                  array('int', 'string', 'string', 'string', 'string', 'string', 'boolean'),
  58                  'makes a new post to a designated blog'
  59              );
  60              $this->addCallback(
  61                  'blogger.editPost',
  62                  'this:blogger_editPost',
  63                  array('boolean', 'string', 'string', 'string', 'string', 'string', 'boolean'),
  64                  'changes the contents of a given post'
  65              );
  66              $this->addCallback(
  67                  'blogger.getUsersBlogs',
  68                  'this:blogger_getUsersBlogs',
  69                  array('struct', 'string', 'string', 'string'),
  70                  'return information about all the blogs a given user is member of'
  71              );
  72              $this->addCallback(
  73                  'blogger.getUserInfo',
  74                  'this:blogger_getUserInfo',
  75                  array('struct', 'string', 'string', 'string'),
  76                  'return information about the current user'
  77              );
  78              $this->addCallback(
  79                  'blogger.getTemplate',
  80                  'this:blogger_getTemplate',
  81                  array('string', 'string', 'string', 'string', 'string', 'string'),
  82                  'return section template - main will return default template, archiveIndex will return section template'
  83              );
  84              $this->addCallback(
  85                  'blogger.setTemplate',
  86                  'this:blogger_setTemplate',
  87                  array('boolean', 'string', 'string', 'string', 'string', 'string', 'string'),
  88                  'updates section template - main=default template, archiveIndex=section template'
  89              );
  90  
  91              // Non-official Blogger API methods - supported by XML-RPC clients
  92              // as BloggerAPI2. Place all this info on a public URI.
  93              $this->addCallback(
  94                  'blogger.getPost',
  95                  'this:blogger_getPost',
  96                  array('struct', 'string', 'string', 'string', 'string'),
  97                  'retrieves contents for the given postid'
  98              );
  99              $this->addCallback(
 100                  'blogger.deletePost',
 101                  'this:blogger_deletePost',
 102                  array('boolean', 'string', 'string', 'string', 'string', 'boolean'),
 103                  'deletes a given post'
 104              );
 105              $this->addCallback(
 106                  'blogger.getRecentPosts',
 107                  'this:blogger_getRecentPosts',
 108                  array('array', 'string', 'string', 'string', 'string', 'int'),
 109                  'retrieves a list of posts (default 10)'
 110              );
 111  
 112              // metaWeblog API[http://www.xmlrpc.com/metaWeblogApi] - add as
 113              // server capability.
 114              $this->capabilities['metaWeblog API'] = array(
 115                  'specUrl'     => 'http://www.xmlrpc.com/metaWeblogApi',
 116                  'specVersion' => 1,
 117              );
 118              // Implements also MovableType extension of the API methods.
 119              $this->addCallback(
 120                  'metaWeblog.getPost',
 121                  'this:metaWeblog_getPost',
 122                  array('struct', 'string', 'string', 'string'),
 123                  'retrieves contents for the given postid'
 124              );
 125              $this->addCallback(
 126                  'metaWeblog.newPost',
 127                  'this:metaWeblog_newPost',
 128                  array('string', 'string', 'string', 'string', 'struct', 'boolean'),
 129                  'creates a new post'
 130              );
 131              $this->addCallback(
 132                  'metaWeblog.editPost',
 133                  'this:metaWeblog_editPost',
 134                  array('boolean', 'string', 'string', 'string', 'struct', 'boolean'),
 135                  'creates a new post'
 136              );
 137              $this->addCallback(
 138                  'metaWeblog.getCategories',
 139                  'this:metaWeblog_getCategories',
 140                  array('struct', 'string', 'string', 'string'),
 141                  'retrieves a list of categories for the current blog'
 142              );
 143              $this->addCallback(
 144                  'metaWeblog.getRecentPosts',
 145                  'this:metaWeblog_getRecentPosts',
 146                  array('array', 'string', 'string', 'string', 'int'),
 147                  'retrieves a given number of recent posts'
 148              );
 149  
 150  // TODO: metaWeblog.newMediaObject (blogid, username, password, struct) returns struct. See https://github.com/textpattern/textpattern/issues/1050
 151  
 152              // MovableType API[] - add as server capability.
 153              $this->capabilities['MovableType API'] = array(
 154                  'specUrl'     => 'http://www.sixapart.com/movabletype/docs/mtmanual_programmatic.html#xmlrpc%20api',
 155                  'specVersion' => 1,
 156              );
 157              // Not completely implemented.
 158              $this->addCallback(
 159                  'mt.getRecentPostTitles',
 160                  'this:mt_getRecentPostTitles',
 161                  array('array', 'string', 'string', 'string', 'id'),
 162                  'returns a bandwidth-friendly list of the most recent posts in the system'
 163              );
 164              $this->addCallback(
 165                  'mt.getCategoryList',
 166                  'this:mt_getCategoryList',
 167                  array('array', 'string', 'string', 'string'),
 168                  'returns a list of all categories defined in the weblog'
 169              );
 170              $this->addCallback(
 171                  'mt.supportedMethods',
 172                  'this:listMethods',
 173                  array('array'),
 174                  'return the XML-RPC Methods supported by the server(Redundant).'
 175              );
 176              $this->addCallback(
 177                  'mt.supportedTextFilters',
 178                  'this:mt_supportedTextFilters',
 179                  array('array'),
 180                  'return the format filters suported by the server.'
 181              );
 182              $this->addCallback(
 183                  'mt.getPostCategories',
 184                  'this:mt_getPostCategories',
 185                  array('array', 'string', 'string', 'string'),
 186                  'returns a list of all categories for the given article'
 187              );
 188              $this->addCallback(
 189                  'mt.setPostCategories',
 190                  'this:mt_setPostCategories',
 191                  array('boolean', 'string', 'string', 'string', 'array'),
 192                  'sets categories for the given article'
 193              );
 194              $this->addCallback(
 195                  'mt.publishPost',
 196                  'this:mt_publishPost',
 197                  array('boolean', 'string', 'string', 'string'),
 198                  'changes the status of the current article to published'
 199              );
 200          }
 201      }
 202  
 203      // Override serve method in order to keep requests logs too while dealing
 204      // with unknown clients.
 205      function serve($data = false)
 206      {
 207          if (!$data) {
 208              global $HTTP_RAW_POST_DATA;
 209  
 210              if (!$HTTP_RAW_POST_DATA) {
 211                  // RSD lets them find us via GET requests.
 212                  $this->rsd();
 213                  exit;
 214              }
 215  
 216              $rx = '/<?xml.*encoding=[\'"](.*?)[\'"].*?>/m';
 217  
 218              // First, handle the known UAs in order to serve proper content.
 219              if (strpos('w.bloggar', $_SERVER['HTTP_USER_AGENT']) !== false) {
 220                  $encoding = 'iso-8859-1';
 221              }
 222              // Find for supplied encoding before to try other things.
 223              elseif (preg_match($rx, $HTTP_RAW_POST_DATA, $xml_enc)) {
 224                  $encoding = strtolower($xml_enc[1]);
 225              }
 226              // Try UTF-8 detect.
 227              elseif (preg_match('/^([\x00-\x7f]|[\xc2-\xdf][\x80-\xbf]|\xe0[\xa0-\xbf][\x80-\xbf]|[\xe1-\xec][\x80-\xbf]{2}|\xed[\x80-\x9f][\x80-\xbf]|[\xee-\xef][\x80-\xbf]{2}|f0[\x90-\xbf][\x80-\xbf]{2}|[\xf1-\xf3][\x80-\xbf]{3}|\xf4[\x80-\x8f][\x80-\xbf]{2})*$/', $HTTP_RAW_POST_DATA) === 1) {
 228                  $encoding = 'utf-8';
 229              }
 230              // Otherwise, use ISO-8859-1.
 231              else {
 232                  $encoding = 'iso-8859-1';
 233              }
 234  
 235              switch ($encoding) {
 236                  case 'utf-8':
 237                      $data = $HTTP_RAW_POST_DATA;
 238                      break;
 239  
 240                  case 'iso-8859-1':
 241  // TODO: if utf8 conversion fails, throw: 32701 ---> parse error. unsupported encoding?
 242  // see: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php & https://github.com/textpattern/textpattern/issues/1051
 243                      // This will fail on parser if utf8_encode is unavailiable.
 244                      $data = (function_exists('utf8_encode') && is_callable('utf8_encode'))
 245                          ? utf8_encode($HTTP_RAW_POST_DATA)
 246                          : $HTTP_RAW_POST_DATA;
 247                      break;
 248  
 249                  default:
 250  // TODO: if utf8 conversion fails, throw: 32701 ---> parse error. unsupported encoding? See https://github.com/textpattern/textpattern/issues/1051
 251                      // This will fail on parser if mb_convert_encoding is unavailiable.
 252                      $data = (function_exists('mb_convert_encoding') && is_callable('mb_convert_encoding'))
 253                          ? mb_convert_encoding($HTTP_RAW_POST_DATA, 'utf-8', $encoding)
 254                          : $HTTP_RAW_POST_DATA;
 255                      break;
 256              }
 257          }
 258  
 259          $this->message = new IXR_Message($data);
 260  
 261          if (!$this->message->parse()) {
 262              $this->error(-32700, 'parse error. not well formed');
 263          }
 264  
 265          if ($this->message->messageType != 'methodCall') {
 266              $this->error(-32600, 'server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall');
 267          }
 268  
 269          $result = $this->call($this->message->methodName, $this->message->params);
 270  
 271          // Is the result an error?
 272          if (is_a($result, 'IXR_Error')) {
 273              $this->error($result);
 274          }
 275  
 276          // Encode the result.
 277          $r = new IXR_Value($result);
 278          $resultxml = $r->getXml();
 279  
 280          // Create the XML.
 281          $xml = <<<EOD
 282  <methodResponse>
 283      <params>
 284          <param>
 285              <value>
 286                  $resultxml
 287              </value>
 288          </param>
 289      </params>
 290  </methodResponse>
 291  
 292  EOD;
 293  
 294          return $this->output($xml, $encoding);
 295      }
 296  
 297      // Override default UTF-8 output, if needed.
 298      function output($xml, $enc = 'utf-8')
 299      {
 300          // Be kind with non-UTF-8 capable clients.
 301          if ($enc != 'utf-8') {
 302              if ($enc == 'iso-8859-1' && function_exists('utf8_decode') && is_callable('utf8_decode')) {
 303                  $xml = utf8_decode($xml);
 304              } elseif (function_exists('mb_convert_encoding') && is_callable('mb_convert_encoding')) {
 305                  $xml = mb_convert_encoding($xml, $enc, 'utf-8');
 306              } else {
 307                  // TODO: shouldn't this throw an error instead of serving non-UTF-8 content as UTF-8?
 308                  // If no decoding possible, serve contents as UTF-8. See https://github.com/textpattern/textpattern/issues/1052
 309                  $enc = 'utf-8';
 310              }
 311          }
 312  
 313          $xml = '<?xml version="1.0" encoding="'.$enc.'" ?>'.n.$xml;
 314          $length = strlen($xml);
 315          header('Connection: close');
 316          header('Content-Length: '.$length);
 317          header('Content-Type: text/xml charset='.$enc);
 318          header('Date: '.date('r'));
 319          echo $xml;
 320          exit;
 321      }
 322  
 323  //---------------------------------------------------------
 324  
 325      // Really Simple Discoverability 1.0 response.
 326      function rsd()
 327      {
 328          global $enable_xmlrpc_server;
 329  
 330          if ($enable_xmlrpc_server) {
 331              $this->output(
 332                  tag(
 333                      n.tag(
 334                          n.tag('Textpattern', 'engineName').
 335                          n.tag('https://textpattern.com/', 'engineLink').
 336                          n.tag(hu, 'homePageLink').
 337                          n.tag(
 338                              n.'<api name="Movable Type" blogID="" preferred="true" apiLink="'.txrpcpath.'" />'.
 339                              n.'<api name="MetaWeblog" blogID="" preferred="false" apiLink="'.txrpcpath.'" />'.
 340                              n.'<api name="Blogger" blogID="" preferred="false" apiLink="'.txrpcpath.'" />'.n,
 341                              'apis').n,
 342                      'service').n,
 343                      'rsd', ' version="1.0" xmlns="http://archipelago.phrasewise.com/rsd"')
 344                  );
 345          } else {
 346              header('Status: 501 Not Implemented');
 347              header('HTTP/1.1 501 Not Implemented');
 348              die('Not Implemented');
 349          }
 350      }
 351  
 352  //---------------------------------------------------------
 353  
 354      // Blogger API.
 355      function blogger_newPost($params)
 356      {
 357          list($appkey, $blogid, $username, $password, $content, $publish) = $params;
 358  
 359          $txp = new TXP_Wrapper($username, $password);
 360  
 361          if (!$txp->loggedin) {
 362              return new IXR_Error(100, gTxt('bad_login'));
 363          }
 364  
 365          $contents = $this->_getBloggerContents($content);
 366  
 367          $contents['Section'] = $blogid;
 368          $contents['Status']  = $publish ? '4' : '1';
 369  
 370          $rs = $txp->newArticle($contents);
 371  
 372          if (false === $rs) {
 373              return new IXR_Error(201, gTxt('problem_creating_article'));
 374          }
 375  
 376          return intval($rs);
 377      }
 378  
 379      function blogger_editPost($params)
 380      {
 381          list($appkey, $postid, $username, $password, $content, $publish) = $params;
 382  
 383          $txp = new TXP_Wrapper($username, $password);
 384  
 385          if (!$txp->loggedin) {
 386              return new IXR_Error(100, gTxt('bad_login'));
 387          }
 388  
 389          $id = $txp->getArticleID($postid, 'ID');
 390  
 391          if (!$id) {
 392              return new IXR_Error(404, gTxt('invalid_article_id'));
 393          }
 394  
 395          $contents = $this->_getBloggerContents($content);
 396  
 397          $contents['Status'] = $publish ? '4' : '1';
 398  
 399          $rs = $txp->updateArticleID($postid, $contents);
 400  
 401          if (false === $rs) {
 402              return new IXR_Error(202, gTxt('problem_updating_article'));
 403          }
 404  
 405          return true;
 406      }
 407  
 408      function blogger_getUsersBlogs($params)
 409      {
 410          list($appkey, $username, $password) = $params;
 411  
 412          global $permlink_mode;
 413  
 414          $txp = new TXP_Wrapper($username, $password);
 415  
 416          if (!$txp->loggedin) {
 417              return new IXR_Error(100, gTxt('bad_login'));
 418          }
 419  
 420          $rs = $txp->getSectionsList();
 421  
 422          if (false === $rs) {
 423              return new IXR_Error(203, gTxt('problem_retrieving_sections'));
 424          }
 425  
 426          $sections = array();
 427  
 428          foreach ($rs as $section) {
 429              $sections[] = array(
 430                  'blogid'   => $section['name'],
 431                  'blogName' => $section['title'],
 432                  'url'      => pagelinkurl(array('s' => $section['name'])),
 433              );
 434          }
 435  
 436          return $sections;
 437      }
 438  
 439      function blogger_getUserInfo($params)
 440      {
 441          list($appkey, $username, $password) = $params;
 442  
 443          $txp = new TXP_Wrapper($username, $password);
 444  
 445          if (!$txp->loggedin) {
 446              return new IXR_Error(100, gTxt('bad_login'));
 447          }
 448  
 449          $rs = $txp->getUser();
 450  
 451          if (!$rs) {
 452              return new IXR_Error(204, gTxt('unable_retrieve_user'));
 453          }
 454  
 455          extract($rs);
 456  
 457          if (strpos($RealName, ' ') != 0) {
 458              list($firstname, $lastname) = explode(' ', $RealName);
 459          } else {
 460              $firstname = $RealName;
 461              $lastname  = '';
 462          }
 463  
 464          $uinfo = array(
 465              'userid'    => $user_id,
 466              'firstname' => $firstname,
 467              'lastname'  => $lastname,
 468              'nickname'  => $name,
 469              'email'     => $email,
 470              'url'       => hu,
 471          );
 472  
 473          return $uinfo;
 474      }
 475  
 476      function blogger_getTemplate($params)
 477      {
 478          list($appkey, $blogid, $username, $password, $templateType) = $params;
 479  
 480          $txp = new TXP_Wrapper($username, $password);
 481  
 482          if (!$txp->loggedin) {
 483              return new IXR_Error(100, gTxt('bad_login'));
 484          }
 485  
 486          if ($templateType == 'archiveIndex' && $blogid != 'default') {
 487              $section = $txp->getSection($blogid);
 488  
 489              if (!$section) {
 490                  return new IXR_Error(208, gTxt('unable_retrieve_template'));
 491              }
 492  
 493              $name = $section['page'];
 494          } else {
 495              $name = 'default';
 496          }
 497  
 498          $rs = $txp->getTemplate($name);
 499  
 500          if (!$rs) {
 501              return new IXR_Error(208, gTxt('unable_retrieve_template'));
 502          }
 503  
 504          return $rs;
 505      }
 506  
 507      function blogger_setTemplate($params)
 508      {
 509          list($appkey, $blogid, $username, $password, $template, $templateType) = $params;
 510  
 511          $txp = new TXP_Wrapper($username, $password);
 512  
 513          if (!$txp->loggedin) {
 514              return new IXR_Error(100, gTxt('bad_login'));
 515          }
 516  
 517          if ($templateType == 'archiveIndex' && $blogid != 'default') {
 518              $section = $txp->getSection($blogid);
 519  
 520              if (!$section) {
 521                  return new IXR_Error(209, gTxt('unable_set_template'));
 522              }
 523  
 524              $name = $section['page'];
 525          } else {
 526              $name = 'default';
 527          }
 528  
 529          $rs = $txp->setTemplate($name, $template);
 530  
 531          if (!$rs) {
 532              return new IXR_Error(209, gTxt('unable_set_template'));
 533          }
 534  
 535          return true;
 536      }
 537  
 538  //---------------------------------------------------------
 539  
 540      // Blogger 2.0.
 541      function blogger_getPost($params)
 542      {
 543          list($appkey, $postid, $username, $password) = $params;
 544  
 545          $txp = new TXP_Wrapper($username, $password);
 546  
 547          if (!$txp->loggedin) {
 548              return new IXR_Error(100, gTxt('bad_login'));
 549          }
 550  
 551          $rs = $txp->getArticleID($postid, 'ID, Body, AuthorId, unix_timestamp(Posted) as uPosted');
 552  
 553          if (!$rs) {
 554              return new IXR_Error(205, gTxt('problem_retrieving_article'));
 555          }
 556  
 557          $out = array(
 558              'content'     => $rs['Body'],
 559              'userId'      => $rs['AuthorId'],
 560              'postId'      => $rs['ID'],
 561              'dateCreated' => new IXR_Date($rs['uPosted'] + tz_offset()),
 562          );
 563  
 564          return $out;
 565      }
 566  
 567      function blogger_deletePost($params)
 568      {
 569          list($appkey, $postid, $username, $password, $publish) = $params;
 570  
 571          $txp = new TXP_Wrapper($username, $password);
 572  
 573          if (!$txp->loggedin) {
 574              return new IXR_Error(100, gTxt('bad_login'));
 575          }
 576  
 577          // Always delete, no matter of publish
 578          $rs = $txp->deleteArticleID($postid);
 579  
 580          if (!$rs) {
 581              return new IXR_Error(206, gTxt('problem_deleting_article'));
 582          }
 583  
 584          return $rs;
 585      }
 586  
 587      function blogger_getRecentPosts($params)
 588      {
 589          list($appkey, $blogid, $username, $password, $numberOfPosts) = $params;
 590  
 591          $txp = new TXP_Wrapper($username, $password);
 592  
 593          if (!$txp->loggedin) {
 594              return new IXR_Error(100, gTxt('bad_login'));
 595          }
 596  
 597          $articles = $txp->getArticleList('ID, Body, AuthorId, unix_timestamp(Posted) as uPosted', "Section='".doSlash($blogid)."'", '0', $numberOfPosts, false);
 598  
 599          if (false === $articles) {
 600              return new IXR_Error(207, gTxt('problem_getting_articles'));
 601          }
 602  
 603          foreach ($articles as $rs) {
 604              $out[] = array(
 605                  'content'     => $rs['Body'],
 606                  'userId'      => $rs['AuthorId'],
 607                  'postId'      => $rs['ID'],
 608                  'dateCreated' => new IXR_Date($rs['uPosted'] + tz_offset()),
 609              );
 610          }
 611  
 612          return $out;
 613      }
 614  
 615  //---------------------------------------------------------
 616  
 617      // metaWeblog API.
 618      function metaWeblog_getPost($params)
 619      {
 620          list($postid, $username, $password) = $params;
 621  
 622          $txp = new TXP_Wrapper($username, $password);
 623  
 624          if (!$txp->loggedin) {
 625              return new IXR_Error(100, gTxt('bad_login'));
 626          }
 627  
 628          $rs = $txp->getArticleID($postid, 'ID, Title, Body, Excerpt, Annotate, Keywords, Section, Category1, Category2, textile_body, url_title, unix_timestamp(Posted) as uPosted');
 629  
 630          if (!$rs) {
 631              return new IXR_Error(205, gTxt('problem_retrieving_article'));
 632          }
 633  
 634          return $this->_buildMetaWeblogStruct($rs, $txp);
 635      }
 636  
 637      function metaWeblog_newPost($params)
 638      {
 639          list($blogid, $username, $password, $struct, $publish) = $params;
 640  
 641          $txp = new TXP_Wrapper($username, $password);
 642  
 643          if (!$txp->loggedin) {
 644              return new IXR_Error(100, gTxt('bad_login'));
 645          }
 646  
 647          $contents = $this->_getMetaWeblogContents($struct, $publish, $txp);
 648  
 649          $contents['Section'] = $blogid;
 650  
 651          $rs = $txp->newArticle($contents);
 652  
 653          if (false === $rs) {
 654              return new IXR_Error(201, gTxt('problem_creating_article'));
 655          }
 656  
 657          // TODO: why "" quoted $rs?
 658          return "$rs";
 659      }
 660  
 661      function metaWeblog_editPost($params)
 662      {
 663          list($postid, $username, $password, $struct, $publish) = $params;
 664  
 665          $txp = new TXP_Wrapper($username, $password);
 666  
 667          if (!$txp->loggedin) {
 668              return new IXR_Error(100, gTxt('bad_login'));
 669          }
 670  
 671          $contents = $this->_getMetaWeblogContents($struct, $publish, $txp);
 672  
 673          $rs = $txp->updateArticleID($postid, $contents);
 674  
 675          if (false === $rs) {
 676              return new IXR_Error(201, gTxt('problem_updating_article'));
 677          }
 678  
 679          return true;
 680      }
 681  
 682      function metaWeblog_getCategories($params)
 683      {
 684          list($blogid, $username, $password) = $params;
 685  
 686          global $permlink_mode;
 687  
 688          $txp = new TXP_Wrapper($username, $password);
 689  
 690          if (!$txp->loggedin) {
 691              return new IXR_Error(100, gTxt('bad_login'));
 692          }
 693  
 694          $rs = $txp->getCategoryList();
 695  
 696          if (false === $rs) {
 697              return new IXR_Error(210, gTxt('problem_retrieving_categories'));
 698          }
 699  
 700          $cats = array();
 701  
 702          foreach ($rs as $c) {
 703              $cats[] = array(
 704                  'categoryName' => $c['title'],
 705                  'description'  => $c['title'],
 706                  'htmlUrl'      => pagelinkurl(array('c' => $c['name'])),
 707                  'rssUrl'       => hu.'?rss=1&#38;category='.$c['name'],
 708              );
 709          }
 710  
 711          return $cats;
 712      }
 713  
 714      function metaWeblog_getRecentPosts($params)
 715      {
 716          list($blogid, $username, $password, $numberOfPosts) = $params;
 717  
 718          $txp = new TXP_Wrapper($username, $password);
 719  
 720          if (!$txp->loggedin) {
 721              return new IXR_Error(100, gTxt('bad_login'));
 722          }
 723  
 724          $articles = $txp->getArticleList('ID, Title, url_title, Body, Excerpt, Annotate, Keywords, Section, Category1, Category2, textile_body, AuthorID, unix_timestamp(Posted) as uPosted',
 725              "Section='".doSlash($blogid)."'", '0', $numberOfPosts, false);
 726  
 727          if (false === $articles) {
 728              return new IXR_Error(207, gTxt('problem_getting_articles'));
 729          }
 730  
 731          $out = array();
 732  
 733          foreach ($articles as $rs) {
 734              $out[] = $this->_buildMetaWeblogStruct($rs, $txp);
 735          }
 736  
 737          return $out;
 738      }
 739  
 740  //---------------------------------------------------------
 741  
 742      // MovableType API.
 743      function mt_getRecentPostTitles($params)
 744      {
 745          list($blogid, $username, $password, $numberOfPosts) = $params;
 746  
 747          global $prefs;
 748  
 749          $txp = new TXP_Wrapper($username, $password);
 750  
 751          if (!$txp->loggedin) {
 752              return new IXR_Error(100, gTxt('bad_login'));
 753          }
 754  
 755          $articles = $txp->getArticleList('ID, Title, AuthorID, unix_timestamp(Posted) as uPosted', "Section='".doSlash($blogid)."'", '0', $numberOfPosts, false);
 756  
 757          if (false === $articles) {
 758              return new IXR_Error(207, gTxt('problem_getting_articles'));
 759          }
 760  
 761          extract($prefs);
 762  
 763          $out = array();
 764  
 765          foreach ($articles as $rs) {
 766              $out[] = array(
 767                  'userid'      => $username,
 768                  'postid'      => $rs['ID'],
 769                  'dateCreated' => new IXR_Date($rs['uPosted'] + tz_offset()),
 770                  'title'       => $rs['Title'],
 771              );
 772          }
 773  
 774          return $out;
 775      }
 776  
 777      function mt_getCategoryList($params)
 778      {
 779          list($blogid, $username, $password) = $params;
 780  
 781          $txp = new TXP_Wrapper($username, $password);
 782  
 783          if (!$txp->loggedin) {
 784              return new IXR_Error(100, gTxt('bad_login'));
 785          }
 786  
 787          $rs = $txp->getCategoryList();
 788  
 789          if (false === $rs) {
 790              return new IXR_Error(210, gTxt('problem_retrieving_categories'));
 791          }
 792  
 793          $cats = array();
 794  
 795          foreach ($rs as $c) {
 796              $cats[] = array(
 797                  'categoryName' => $c['title'],
 798                  'categoryId'   => $c['id'],
 799              );
 800          }
 801  
 802          return $cats;
 803      }
 804  
 805      function mt_supportedTextFilters($params)
 806      {
 807          $filters = array(
 808              array('key' => '0', 'label' => 'LEAVE_TEXT_UNTOUCHED'),
 809              array('key' => '1', 'label' => 'USE_TEXTILE'),
 810              array('key' => '2', 'label' => 'CONVERT_LINEBREAKS'),
 811          );
 812  
 813          return $filters;
 814      }
 815  
 816      function mt_getPostCategories($params)
 817      {
 818          list($postid, $username, $password) = $params;
 819  
 820          $txp = new TXP_Wrapper($username, $password);
 821  
 822          if (!$txp->loggedin) {
 823              return new IXR_Error(100, gTxt('bad_login'));
 824          }
 825  
 826          $post = $txp->getArticleID($postid, 'Category1, Category2');
 827  
 828          if (!$post) {
 829              return new IXR_Error(211, gTxt('problem_retrieving_article_categories'));
 830          }
 831  
 832          $out = array();
 833          $isPrimary = true;
 834          $cats[] = $post['Category1'];
 835          $cats[] = $post['Category2'];
 836  
 837          foreach ($cats as $category) {
 838              if (!empty($category)) {
 839                  $rs = $txp->getCategory($category);
 840  
 841                  // TODO: remove?
 842                  // if (!$rs) return new IXR_Error(212, gTxt('problem_retrieving_category_info'));
 843  
 844                  $ct['categoryId']   = $rs['id'];
 845                  $ct['categoryName'] = $rs['title'];
 846                  $ct['isPrimary']    = $isPrimary;
 847  
 848                  $out[] = $ct;
 849              }
 850  
 851              if ($isPrimary) {
 852                  $isPrimary = false;
 853              }
 854          }
 855  
 856          return $out;
 857      }
 858  
 859      // TODO: explain what 'expecific' is ;)
 860      // Supported to avoid some client expecific behaviour.
 861      function mt_publishPost($params)
 862      {
 863          list($postid, $username, $password) = $params;
 864  
 865          $txp = new TXP_Wrapper($username, $password);
 866  
 867          if (!$txp->loggedin) {
 868              return new IXR_Error(100, gTxt('bad_login'));
 869          }
 870  
 871          $published = $txp->updateArticleField($postid, 'Status', '4');
 872  
 873          if (!$published) {
 874              return new IXR_Error(201, gTxt('problem_updating_article'));
 875          }
 876  
 877          return true;
 878      }
 879  
 880      function mt_setPostCategories($params)
 881      {
 882          list($postid, $username, $password, $categories) = $params;
 883  
 884          $txp = new TXP_Wrapper($username, $password);
 885  
 886          if (!$txp->loggedin) {
 887              return new IXR_Error(100, gTxt('bad_login'));
 888          }
 889  
 890          $Category1 = '';
 891          $Category2 = '';
 892  
 893          foreach ($categories as $category) {
 894              extract($category);
 895  
 896              $rs = $txp->getCategoryId($categoryId);
 897  
 898              if (!$rs) {
 899                  return new IXR_Error(213, gTxt('trying_to_assign_unexisting_category_to_the_article'));
 900              }
 901  
 902              if (empty($Category1)) {
 903                  $Category1 = $rs['name'];
 904              } else {
 905                  $Category2 = $rs['name'];
 906              }
 907          }
 908  
 909          $ct1 = $txp->updateArticleField($postid, 'Category1', $Category1);
 910          $ct2 = $txp->updateArticleField($postid, 'Category2', $Category2);
 911  
 912          if (!$ct1 || !$ct2) {
 913              return new IXR_Error(214, gTxt('problem_saving_article_categories'));
 914          }
 915  
 916          return true;
 917      }
 918  
 919      // TODO ???
 920      // MediaObjects
 921      /*
 922       metaWeblog.newMediaObject
 923       Description: Uploads a file to your webserver.
 924       Parameters: String blogid, String username, String password, struct file
 925       Return value: URL to the uploaded file.
 926       Notes: the struct file should contain two keys: base64 bits (the base64-encoded contents of the file)
 927       and String name (the name of the file). The type key (media type of the file) is currently ignored.
 928       */
 929  
 930      // Code refactoring for blogger_newPost and blogger_editPost.
 931      function _getBloggerContents($content)
 932      {
 933          $body = $content;
 934  
 935          // Trick to add title, category and excerpts using XML-RPC.
 936          if (preg_match('/<title>(.*)<\/title>(.*)/s', $content, $matches)) {
 937              $body  = $matches[2];
 938              $title = $matches[1];
 939          }
 940  
 941          $contents = array(
 942              'Body' => str_replace('\n', n, $body),
 943          );
 944  
 945          if (isset($title)) {
 946              $contents['Title'] = $title;
 947          }
 948  
 949          return $contents;
 950      }
 951  
 952      // Code refactoring for metaWeblog_newPost and metaweblog_EditPost.
 953      function _getMetaWeblogContents($struct, $publish, $txp)
 954      {
 955          global $gmtoffset, $is_dst;
 956  
 957          $contents = array(
 958              'Body'   => str_replace('\n', n, $struct['description']),
 959              'Status' => $publish ? '4' : '1',
 960              'Title'  => $struct['title'],
 961          );
 962  
 963          if (!empty($struct['categories'])) {
 964              if (!empty($struct['categories'][0])) {
 965                  $c = $txp->getCategoryTitle($struct['categories'][0]);
 966                  $contents['Category1'] = $c['name'];
 967              }
 968  
 969              if (!empty($struct['categories'][1])) {
 970                  $c = $txp->getCategoryTitle($struct['categories'][1]);
 971                  $contents['Category2'] = $c['name'];
 972              }
 973          }
 974  
 975          if (isset($struct['date_created_gmt'])) {
 976              $struct['dateCreated'] = $struct['date_created_gmt'];
 977              $struct['dateCreated']->tz = 'Z'; // force GMT timezone
 978          }
 979  
 980          if (isset($struct['dateCreated'])) {
 981              if ($struct['dateCreated']->tz == 'Z') {
 982                  // GMT-based posting time; transform into server time zone.
 983                  $posted = $struct['dateCreated']->getTimestamp() - tz_offset() + $gmtoffset + ($is_dst ? 3600 : 0);
 984              } elseif (!$struct['dateCreated']->tz) {
 985                  // Posting in an unspecified time zone: Assume site time.
 986                  $posted = $struct['dateCreated']->getTimestamp() - tz_offset();
 987              } else {
 988                  // Numeric time zone offsets.
 989                  if (preg_match('/([+-][0-9]{2})([0-9]{2})/', $struct['dateCreated']->tz, $t)) {
 990                      $tz = $t[1] * 3600 + $t[2] * 60;
 991                      $posted = $struct['dateCreated']->getTimestamp() - tz_offset() + $gmtoffset + ($is_dst ? 3600 : 0) - $tz;
 992                  }
 993              }
 994          }
 995  
 996          if (isset($posted)) {
 997              $contents['Posted'] = date('Y-m-d H:i:s', $posted);
 998          }
 999  
1000          // MovableType implementation add-ons
1001          if (isset($struct['mt_allow_comments'])) {
1002              $contents['Annotate'] = $struct['mt_allow_comments'];
1003          }
1004  
1005          if (isset($struct['mt_convert_breaks'])) {
1006              $contents['textile_body'] = $contents['textile_excerpt'] = intval($struct['mt_convert_breaks']);
1007          }
1008  
1009          if (isset($struct['mt_text_more'])) {
1010              $contents['Body'] .= n.n.str_replace('\n', n, $struct['mt_text_more']);
1011          }
1012  
1013          if (isset($struct['mt_excerpt'])) {
1014              $contents['Excerpt'] = str_replace('\n', n, $struct['mt_excerpt']);
1015          }
1016  
1017          if (isset($struct['mt_keywords'])) {
1018              $contents['Keywords'] = $struct['mt_keywords'];
1019          }
1020  
1021          if (isset($struct['mt_basename'])) {
1022              $contents['url_title'] = stripSpace($struct['mt_basename']);
1023          } elseif (isset($struct['wp_slug'])) {
1024              $contents['url_title'] = stripSpace($struct['wp_slug']);
1025          }
1026  
1027          return $contents;
1028      }
1029  
1030      // Common code to metaWeblog_getPost and metaWeblog_getRecentPosts could
1031      // not be this placed on a different file from taghandlers?
1032      // Remove if it is the case.
1033      function _buildMetaWeblogStruct($rs, $txp)
1034      {
1035          global $permlink_mode, $is_dst, $gmtoffset;
1036  
1037          switch ($permlink_mode) {
1038              case 'section_id_title':
1039                  $url = hu.join('/', array($rs['Section'], $rs['ID'], $rs['url_title']));
1040                  break;
1041  
1042              case 'year_month_day_title':
1043                  $url = hu.join('/', array(
1044                      date("Y", $rs['uPosted']),
1045                      date("m", $rs['uPosted']),
1046                      date("d", $rs['uPosted']),
1047                      $rs['url_title'],
1048                  ));
1049                  break;
1050  
1051              case 'title_only':
1052                  $url = hu.$rs['url_title'];
1053                  break;
1054  
1055              case 'section_title':
1056                  $url = hu.join('/', array($rs['Section'], $rs['url_title']));
1057                  break;
1058  
1059              case 'id_title':
1060                  $url = hu.join('/', array($rs['ID'], $rs['url_title']));
1061                  break;
1062  
1063              default:
1064                  // Assume messy mode?
1065                  $url = hu.'?id='.$rs['ID'];
1066                  break;
1067          }
1068  
1069          $cat  = $txp->getCategory($rs['Category1']);
1070          $cat1 = $cat['title'];
1071          $cat  = $txp->getCategory($rs['Category2']);
1072          $cat2 = $cat['title'];
1073  
1074          $out = array(
1075              'categories'  => array($cat1, $cat2),
1076              'description' => $rs['Body'],
1077              'userid'      => $txp->txp_user,
1078              'postid'      => $rs['ID'],
1079              'dateCreated' => new IXR_Date($rs['uPosted'] + tz_offset() - $gmtoffset - ($is_dst ? 3600 : 0)),
1080              'link'        => $url,
1081              'permaLink'   => $url,
1082              'title'       => $rs['Title'],
1083          );
1084  
1085          $out['dateCreated']->tz = 'Z'; // GMT.
1086  
1087          // MovableType Implementation add-ons.
1088          if (isset($rs['Annotate']) && !empty($rs['Annotate'])) {
1089              $out['mt_allow_comments'] = intval($rs['Annotate']);
1090          }
1091  
1092          if (isset($rs['textile_body']) && !empty($rs['textile_body'])) {
1093              $out['mt_convert_breaks'] = strval($rs['textile_body']);
1094          }
1095  
1096          if (isset($rs['Excerpt']) && !empty($rs['Excerpt'])) {
1097              $out['mt_excerpt'] = $rs['Excerpt'];
1098          }
1099  
1100          if (isset($rs['Keywords']) && !empty($rs['Keywords'])) {
1101              $out['mt_keywords'] = $rs['Keywords'];
1102          }
1103  
1104          if (isset($rs['url_title']) && !empty($rs['url_title'])) {
1105              $out['mt_basename'] = $out['wp_slug'] = $rs['url_title'];
1106          }
1107  
1108          return $out;
1109      }
1110  }

title

Description

title

Description

title

Description

title

title

Body