KXStudio Website https://kx.studio/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

538 lines
20KB

  1. <?php
  2. /**
  3. * File: XML-RPC
  4. * Extensible XML-RPC interface for remotely controlling your Chyrp install.
  5. */
  6. define('XML_RPC', true);
  7. require_once 'common.php';
  8. require_once INCLUDES_DIR.'/lib/ixr.php';
  9. if (!defined('XML_RPC_FEATHER')) define('XML_RPC_FEATHER', 'text');
  10. # Use the Main controller for any Route calls.
  11. Route::current(MainController::current());
  12. #
  13. # Class: XMLRPC
  14. # Provides functionality for using external clients, services, etc. for accessing and adding to Chyrp.
  15. #
  16. class XMLRPC extends IXR_Server {
  17. #
  18. # Function: __construct
  19. # Registers the various XMLRPC methods.
  20. #
  21. public function __construct() {
  22. set_error_handler('XMLRPC::error_handler');
  23. set_exception_handler('XMLRPC::exception_handler');
  24. $methods = array('pingback.ping' => 'this:pingback_ping',
  25. # MetaWeblog
  26. 'metaWeblog.getRecentPosts' => 'this:metaWeblog_getRecentPosts',
  27. 'metaWeblog.getCategories' => 'this:metaWeblog_getCategories',
  28. 'metaWeblog.newMediaObject' => 'this:metaWeblog_newMediaObject',
  29. 'metaWeblog.newPost' => 'this:metaWeblog_newPost',
  30. 'metaWeblog.getPost' => 'this:metaWeblog_getPost',
  31. 'metaWeblog.editPost' => 'this:metaWeblog_editPost',
  32. # Blogger
  33. 'blogger.deletePost' => 'this:blogger_deletePost',
  34. 'blogger.getUsersBlogs' => 'this:blogger_getUsersBlogs',
  35. 'blogger.getUserInfo' => 'this:blogger_getUserInfo',
  36. # MovableType
  37. 'mt.getRecentPostTitles' => 'this:mt_getRecentPostTitles',
  38. 'mt.getCategoryList' => 'this:mt_getCategoryList',
  39. 'mt.getPostCategories' => 'this:mt_getPostCategories',
  40. 'mt.setPostCategories' => 'this:mt_setPostCategories',
  41. 'mt.supportedTextFilters' => 'this:mt_supportedTextFilters',
  42. 'mt.supportedMethods' => 'this:listMethods',
  43. # Chyrp
  44. "chyrp.getRecentPosts" => "this:chyrp_getRecentPosts",
  45. "chyrp.newPost" => "this:chyrp_newPost",
  46. "chyrp.getPost" => "this:chyrp_getPost",
  47. "chyrp.editPost" => "this:chyrp_editPost");
  48. Trigger::current()->filter($methods, "xmlrpc_methods");
  49. $this->IXR_Server($methods);
  50. }
  51. #
  52. # Function: pingback_ping
  53. # Receive and register pingbacks. Calls the @pingback@ trigger.
  54. #
  55. public function pingback_ping($args) {
  56. $config = Config::current();
  57. $linked_from = str_replace('&amp;', '&', $args[0]);
  58. $linked_to = str_replace('&amp;', '&', $args[1]);
  59. $cleaned_url = str_replace(array("http://www.", "http://"), "", $config->url);
  60. if ($linked_to == $linked_from)
  61. return new IXR_ERROR(0, __("The from and to URLs cannot be the same."));
  62. if (!substr_count($linked_to, $cleaned_url))
  63. return new IXR_Error(0, __("There doesn't seem to be a valid link in your request."));
  64. if (preg_match("/url=([^&#]+)/", $linked_to, $url))
  65. $post = new Post(array("url" => $url[1]));
  66. else
  67. $post = MainController::current()->post_from_url(null,
  68. str_replace(rtrim($config->url, "/"), "/", $linked_to),
  69. true);
  70. if (!$post)
  71. return new IXR_Error(33, __("I can't find a post from that URL."));
  72. # Wait for the "from" server to publish
  73. sleep(1);
  74. $from = parse_url($linked_from);
  75. if (empty($from["host"]))
  76. return false;
  77. if (empty($from["scheme"]) or $from["scheme"] != "http")
  78. $linked_from = "http://".$linked_from;
  79. # Grab the page that linked here.
  80. $content = get_remote($linked_from);
  81. # Get the title of the page.
  82. preg_match("/<title>([^<]+)<\/title>/i", $content, $title);
  83. $title = $title[1];
  84. if (empty($title))
  85. return new IXR_Error(32, __("There isn't a title on that page."));
  86. $content = strip_tags($content, "<a>");
  87. $url = preg_quote($linked_to, "/");
  88. if (!preg_match("/<a[^>]*{$url}[^>]*>([^>]*)<\/a>/", $content, $context)) {
  89. $url = str_replace("&", "&amp;", preg_quote($linked_to, "/"));
  90. if (!preg_match("/<a[^>]*{$url}[^>]*>([^>]*)<\/a>/", $content, $context)) {
  91. $url = str_replace("&", "&#038;", preg_quote($linked_to, "/"));
  92. if (!preg_match("/<a[^>]*{$url}[^>]*>([^>]*)<\/a>/", $content, $context))
  93. return false;
  94. }
  95. }
  96. $context[1] = truncate($context[1], 100, "...", true);
  97. $excerpt = strip_tags(str_replace($context[0], $context[1], $content));
  98. $match = preg_quote($context[1], "/");
  99. $excerpt = preg_replace("/.*?\s(.{0,100}{$match}.{0,100})\s.*/s", "\\1", $excerpt);
  100. $excerpt = "[...] ".trim(normalize($excerpt))." [...]";
  101. Trigger::current()->call("pingback", $post, $linked_to, $linked_from, $title, $excerpt);
  102. return _f("Pingback from %s to %s registered!", array($linked_from, $linked_to));
  103. }
  104. #
  105. # Function: metaWeblog_getRecentPosts
  106. # Returns a list of the most recent posts.
  107. #
  108. public function metaWeblog_getRecentPosts($args) {
  109. $this->auth($args[1], $args[2]);
  110. $config = Config::current();
  111. $trigger = Trigger::current();
  112. $result = array();
  113. foreach ($this->getRecentPosts($args[3]) as $post) {
  114. $struct = array(
  115. 'postid' => $post->id,
  116. 'userid' => $post->user_id,
  117. 'title' => $post->title,
  118. 'dateCreated' => new IXR_Date(when('Ymd\TH:i:s', $post->created_at)),
  119. 'description' => $post->body,
  120. 'link' => $post->url(),
  121. 'permaLink' => $post->url(),
  122. 'mt_basename' => $post->url,
  123. 'mt_allow_pings' => (int) $config->enable_trackbacking);
  124. $result[] = $trigger->filter($struct, 'metaWeblog_getPost', $post);
  125. }
  126. return $result;
  127. }
  128. #
  129. # Function: metaWeblog_getCategories
  130. # Returns a list of all categories to which the post is assigned.
  131. #
  132. public function metaWeblog_getCategories($args) {
  133. $this->auth($args[1], $args[2]);
  134. $categories = array();
  135. return Trigger::current()->filter($categories, 'metaWeblog_getCategories');
  136. }
  137. #
  138. # Function: metaWeblog_newMediaObject
  139. # Uploads a file to the server.
  140. #
  141. public function metaWeblog_newMediaObject($args) {
  142. $this->auth($args[1], $args[2]);
  143. $config = Config::current();
  144. $file = unique_filename(trim($args[3]['name'], ' /'));
  145. $path = MAIN_DIR.$config->uploads_path.$file;
  146. if (file_put_contents($path, $args[3]['bits']) === false)
  147. return new IXR_Error(500, __("Failed to write file."));
  148. $url = $config->chyrp_url.$config->uploads_path.str_replace('+', '%20', urlencode($file));
  149. Trigger::current()->filter($url, 'metaWeblog_newMediaObject', $path);
  150. return array('url' => $url);
  151. }
  152. #
  153. # Function: metaWeblog_getPost
  154. # Retrieves a specified post.
  155. #
  156. public function metaWeblog_getPost($args) {
  157. $this->auth($args[1], $args[2]);
  158. $post = new Post($args[0], array('filter' => false));
  159. $struct = array(
  160. 'postid' => $post->id,
  161. 'userid' => $post->user_id,
  162. 'title' => $post->title,
  163. 'dateCreated' => new IXR_Date(when('Ymd\TH:i:s', $post->created_at)),
  164. 'description' => $post->body,
  165. 'link' => $post->url(),
  166. 'permaLink' => $post->url(),
  167. 'mt_basename' => $post->url,
  168. 'mt_allow_pings' => (int) Config::current()->enable_trackbacking);
  169. Trigger::current()->filter($struct, 'metaWeblog_getPost', $post);
  170. return array($struct);
  171. }
  172. #
  173. # Function: metaWeblog_newPost
  174. # Creates a new post.
  175. #
  176. public function metaWeblog_newPost($args) {
  177. $this->auth($args[1], $args[2], 'add');
  178. global $user;
  179. # Support for extended body
  180. $body = $args[3]['description'];
  181. if (!empty($args[3]['mt_text_more']))
  182. $body .= '<!--more-->'.$args[3]['mt_text_more'];
  183. # Add excerpt to body so it isn't lost
  184. if (!empty($args[3]['mt_excerpt']))
  185. $body = $args[3]['mt_excerpt']."\n\n".$body;
  186. if (trim($body) === '')
  187. return new IXR_Error(500, __("Body can't be blank."));
  188. $clean = sanitize(oneof(@$args[3]['mt_basename'], $args[3]['title']));
  189. $url = Post::check_url($clean);
  190. $_POST['user_id'] = $user->id;
  191. $_POST['feather'] = XML_RPC_FEATHER;
  192. $_POST['created_at'] = oneof($this->convertFromDateCreated($args[3]), datetime());
  193. if ($user->group->can('add_post'))
  194. $_POST['status'] = ($args[4]) ? 'public' : 'draft';
  195. else
  196. $_POST['status'] = 'draft';
  197. $trigger = Trigger::current();
  198. $trigger->call('metaWeblog_newPost_preQuery', $args[3]);
  199. $post = Post::add(
  200. array(
  201. 'title' => $args[3]['title'],
  202. 'body' => $body),
  203. $clean,
  204. $url);
  205. if ($post->no_results)
  206. return new IXR_Error(500, __("Post not found."));
  207. $trigger->call('metaWeblog_newPost', $args[3], $post);
  208. # Send any and all pingbacks to URLs in the body
  209. if (Config::current()->send_pingbacks)
  210. send_pingbacks($args[3]['description'], $post);
  211. return $post->id;
  212. }
  213. #
  214. # Function: metaWeblog_editPost
  215. # Updates a specified post.
  216. #
  217. public function metaWeblog_editPost($args) {
  218. $this->auth($args[1], $args[2], 'edit');
  219. global $user;
  220. if (!Post::exists($args[0]))
  221. return new IXR_Error(500, __("Post not found."));
  222. # Support for extended body
  223. $body = $args[3]['description'];
  224. if (!empty($args[3]['mt_text_more']))
  225. $body .= '<!--more-->'.$args[3]['mt_text_more'];
  226. # Add excerpt to body so it isn't lost
  227. if (!empty($args[3]['mt_excerpt']))
  228. $body = $args[3]['mt_excerpt']."\n\n".$body;
  229. if (trim($body) === '')
  230. return new IXR_Error(500, __("Body can't be blank."));
  231. $post = new Post($args[0], array('filter' => false));
  232. # More specific permission check
  233. if (!$post->editable($user))
  234. return new IXR_Error(500, __("You don't have permission to edit this post."));
  235. # Enforce post status when necessary
  236. if (!$user->group->can('edit_own_post', 'edit_post'))
  237. $status = 'draft';
  238. else if ($post->status !== 'public' and $post->status !== 'draft')
  239. $status = $post->status;
  240. else
  241. $status = ($args[4]) ? 'public' : 'draft';
  242. $trigger = Trigger::current();
  243. $trigger->call('metaWeblog_editPost_preQuery', $args[3], $post);
  244. $post->update(
  245. array('title' => $args[3]['title'], 'body' => $body ),
  246. null,
  247. null,
  248. $status,
  249. sanitize(oneof(@$args[3]['mt_basename'], $args[3]['title'])),
  250. oneof($this->convertFromDateCreated($args[3]), $post->created_at));
  251. $trigger->call('metaWeblog_editPost', $args[3], $post);
  252. return true;
  253. }
  254. #
  255. # Function: blogger_deletePost
  256. # Deletes a specified post.
  257. #
  258. public function blogger_deletePost($args) {
  259. $this->auth($args[2], $args[3], 'delete');
  260. global $user;
  261. $post = new Post($args[1], array('filter' => false));
  262. if ($post->no_results)
  263. return new IXR_Error(500, __("Post not found."));
  264. else if (!$post->deletable($user))
  265. return new IXR_Error(500, __("You don't have permission to delete this post."));
  266. Post::delete($args[1]);
  267. return true;
  268. }
  269. #
  270. # Function: blogger_getUsersBlogs
  271. # Returns information about the Chyrp installation.
  272. #
  273. public function blogger_getUsersBlogs($args) {
  274. $this->auth($args[1], $args[2]);
  275. $config = Config::current();
  276. return array(array(
  277. 'url' => $config->url,
  278. 'blogName' => $config->name,
  279. 'blogid' => 1));
  280. }
  281. #
  282. # Function: blogger_getUserInfo
  283. # Retrieves a specified user.
  284. #
  285. public function blogger_getUserInfo($args) {
  286. $this->auth($args[1], $args[2]);
  287. global $user;
  288. return array(array(
  289. 'userid' => $user->id,
  290. 'nickname' => $user->full_name,
  291. 'firstname' => '',
  292. 'lastname' => '',
  293. 'email' => $user->email,
  294. 'url' => $user->website));
  295. }
  296. #
  297. # Function: mt_getRecentPostTitles
  298. # Returns a bandwidth-friendly list of the most recent posts.
  299. #
  300. public function mt_getRecentPostTitles($args) {
  301. $this->auth($args[1], $args[2]);
  302. $result = array();
  303. foreach ($this->getRecentPosts($args[3]) as $post) {
  304. $result[] = array(
  305. 'postid' => $post->id,
  306. 'userid' => $post->user_id,
  307. 'title' => $post->title,
  308. 'dateCreated' => new IXR_Date(when('Ymd\TH:i:s', $post->created_at)));
  309. }
  310. return $result;
  311. }
  312. #
  313. # Function: mt_getCategoryList
  314. # Returns a list of categories.
  315. #
  316. public function mt_getCategoryList($args) {
  317. $this->auth($args[1], $args[2]);
  318. $categories = array();
  319. return Trigger::current()->filter(
  320. $categories,
  321. 'mt_getCategoryList');
  322. }
  323. #
  324. # Function: mt_getPostCategories
  325. # Returns a list of all categories to which the post is assigned.
  326. #
  327. public function mt_getPostCategories($args) {
  328. $this->auth($args[1], $args[2]);
  329. if (!Post::exists($args[0]))
  330. return new IXR_Error(500, __("Post not found."));
  331. $categories = array();
  332. return Trigger::current()->filter(
  333. $categories,
  334. 'mt_getPostCategories',
  335. new Post($args[0], array('filter' => false)));
  336. }
  337. #
  338. # Function: mt_setPostCategories
  339. # Sets the categories for a post.
  340. #
  341. public function mt_setPostCategories($args) {
  342. $this->auth($args[1], $args[2], 'edit');
  343. global $user;
  344. $post = new Post($args[0], array('filter' => false));
  345. if ($post->no_results)
  346. return new IXR_Error(500, __("Post not found."));
  347. else if (!$post->deletable($user))
  348. return new IXR_Error(500, __("You don't have permission to edit this post."));
  349. Trigger::current()->call('mt_setPostCategories', $args[3], $post);
  350. return true;
  351. }
  352. #
  353. # Function: mt_supportedTextFilters
  354. # Returns an empty array, as this is not applicable for Chyrp.
  355. #
  356. public function mt_supportedTextFilters() {
  357. return array();
  358. }
  359. #
  360. # Function: getRecentPosts
  361. # Returns an array of the most recent posts.
  362. #
  363. private function getRecentPosts($limit) {
  364. global $user;
  365. if (!in_array(XML_RPC_FEATHER, Config::current()->enabled_feathers))
  366. throw new Exception(_f("The %s feather is not enabled.", array(XML_RPC_FEATHER)));
  367. $where = array('feather' => XML_RPC_FEATHER);
  368. if ($user->group->can('view_own_draft', 'view_draft'))
  369. $where['status'] = array('public', 'draft');
  370. else
  371. $where['status'] = 'public';
  372. if (!$user->group->can('view_draft', 'edit_draft', 'edit_post', 'delete_draft', 'delete_post'))
  373. $where['user_id'] = $user->id;
  374. return Post::find(
  375. array(
  376. 'where' => $where,
  377. 'order' => 'created_at DESC, id DESC',
  378. 'limit' => $limit),
  379. array('filter' => false));
  380. }
  381. #
  382. # Function: convertFromDateCreated
  383. # Converts an IXR_Date (in $args['dateCreated']) to SQL date format.
  384. #
  385. private function convertFromDateCreated($args) {
  386. if (array_key_exists('dateCreated', $args))
  387. return when('Y-m-d H:i:s', $args['dateCreated']->getIso());
  388. else
  389. return null;
  390. }
  391. #
  392. # Function: auth
  393. # Authenticates a given login and password, and checks for appropriate permission
  394. #
  395. private function auth($login, $password, $do = 'add') {
  396. if (!Config::current()->enable_xmlrpc)
  397. throw new Exception(__("XML-RPC support is disabled for this site."));
  398. global $user;
  399. if (!User::authenticate($login, $password))
  400. throw new Exception(__("Login incorrect."));
  401. else
  402. $user = new User(
  403. null,
  404. array(
  405. 'where' => array(
  406. 'login' => $login
  407. )
  408. )
  409. );
  410. if (!$user->group->can("{$do}_own_post", "{$do}_post", "{$do}_draft", "{$do}_own_draft"))
  411. throw new Exception(_f("You don't have permission to %s posts/drafts.", array($do)));
  412. }
  413. #
  414. # Function: error_handler
  415. #
  416. public static function error_handler($errno, $errstr, $errfile, $errline) {
  417. if (error_reporting() === 0 or $errno == E_STRICT) return;
  418. throw new Exception(sprintf("%s in %s on line %s", $errstr, $errfile, $errline));
  419. }
  420. #
  421. # Function: exception_handler
  422. #
  423. public static function exception_handler($exception) {
  424. $ixr_error = new IXR_Error(500, $exception->getMessage());
  425. echo $ixr_error->getXml();
  426. }
  427. }
  428. $server = new XMLRPC();
  429. ?>