2 // $Id: blogapi.module 144 2007-03-28 07:52:20Z thierry $
6 * Enable users to post using applications that support XML-RPC blog APIs.
10 * Implementation of hook_help().
12 function blogapi_help($section) {
14 case 'admin/help#blogapi':
15 $output = '<p>'. t('The blog API module enables a post to be posted to a site via external GUI applications. Many users prefer to use external tools to improve their ability to read and post responses in a customized way. The blog api provides users the freedom to use the blogging tools they want but still have the blogging server of choice.') .'</p>';
16 $output .= '<p>'. t('When this module is enabled and configured you can use programs like <a href="%external-http-ecto-kung-foo-tv">Ecto</a> to create and publish posts from your desktop. Blog API module supports several XML-RPC based blogging APIs such as the <a href="%-">Blogger API</a>, <a href="%external-http-www-xmlrpc-com-metaWeblogApi">MetaWeblog API</a>, and most of the <a href="%external-http-www-movabletype-org-docs-mtmanual_programmatic-html">Movable Type API</a>. Any desktop blogging tools or other services (e.g. <a href="%external-http-www-flickr-com">Flickr\'s</a> "post to blog") that support these APIs should work with this site.', array('%external-http-ecto-kung-foo-tv' => 'http://ecto.kung-foo.tv/', '%-' => url('http://www.blogger.com/developers/api/1_docs/'), '%external-http-www-xmlrpc-com-metaWeblogApi' => 'http://www.xmlrpc.com/metaWeblogApi', '%external-http-www-movabletype-org-docs-mtmanual_programmatic-html' => 'http://www.movabletype.org/docs/mtmanual_programmatic.html', '%external-http-www-flickr-com' => 'http://www.flickr.com')) .'</p>';
17 $output .= '<p>'. t('This module also allows site administrators to configure which content types can be posted via the external applications. So, for instance, users can post forum topics as well as blog posts. Where supported, the external applications will display each content type as a separate "blog".<!--break-->') .'</p>';
18 $output .= t('<p>You can</p>
20 <li>view the XML-RPC page on your site at >> <a href="%file-xmlrpc">xmlrpc.php</a>.</li>
21 <li><a href="%admin-settings-blogapi">administer >> settings >> blog api</a>.</li>
23 ', array('%file-xmlrpc' => 'xmlrpc.php', '%admin-settings-blogapi' => url('admin/settings/blogapi')));
24 $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%blogapi">BlogApi page</a>.', array('%blogapi' => 'http://drupal.org/handbook/modules/blogapi/')) .'</p>';
26 case 'admin/modules#description':
27 return t('Allows users to post content using applications that support XML-RPC blog APIs.');
32 * Implementation of hook_xmlrpc().
34 function blogapi_xmlrpc() {
37 'blogger.getUsersBlogs',
38 'blogapi_blogger_get_users_blogs',
39 array('array', 'string', 'string', 'string'),
40 t('Returns a list of weblogs to which an author has posting privileges.')),
42 'blogger.getUserInfo',
43 'blogapi_blogger_get_user_info',
44 array('struct', 'string', 'string', 'string'),
45 t('Returns information about an author in the system.')),
48 'blogapi_blogger_new_post',
49 array('string', 'string', 'string', 'string', 'string', 'string', 'boolean'),
50 t('Creates a new post, and optionally publishes it.')),
53 'blogapi_blogger_edit_post',
54 array('boolean', 'string', 'string', 'string', 'string', 'string', 'boolean'),
55 t('Updates the information about an existing post.')),
58 'blogapi_blogger_get_post',
59 array('struct', 'string', 'string', 'string', 'string'),
60 t('Returns information about a specific post.')),
63 'blogapi_blogger_delete_post',
64 array('boolean', 'string', 'string', 'string', 'string', 'boolean'),
65 t('Deletes a post.')),
67 'blogger.getRecentPosts',
68 'blogapi_blogger_get_recent_posts',
69 array('array', 'string', 'string', 'string', 'string', 'int'),
70 t('Returns a list of the most recent posts in the system.')),
73 'blogapi_metaweblog_new_post',
74 array('string', 'string', 'string', 'string', 'struct', 'boolean'),
75 t('Creates a new post, and optionally publishes it.')),
77 'metaWeblog.editPost',
78 'blogapi_metaweblog_edit_post',
79 array('boolean', 'string', 'string', 'string', 'struct', 'boolean'),
80 t('Updates information about an existing post.')),
83 'blogapi_metaweblog_get_post',
84 array('struct', 'string', 'string', 'string'),
85 t('Returns information about a specific post.')),
87 'metaWeblog.newMediaObject',
88 'blogapi_metaweblog_new_media_object',
89 array('string', 'string', 'string', 'string', 'struct'),
90 t('Uploads a file to your webserver.')),
92 'metaWeblog.getCategories',
93 'blogapi_metaweblog_get_category_list',
94 array('struct', 'string', 'string', 'string'),
95 t('Returns a list of all categories to which the post is assigned.')),
97 'metaWeblog.getRecentPosts',
98 'blogapi_metaweblog_get_recent_posts',
99 array('array', 'string', 'string', 'string', 'int'),
100 t('Returns a list of the most recent posts in the system.')),
102 'mt.getRecentPostTitles',
103 'blogapi_mt_get_recent_post_titles',
104 array('array', 'string', 'string', 'string', 'int'),
105 t('Returns a bandwidth-friendly list of the most recent posts in the system.')),
107 'mt.getCategoryList',
108 'blogapi_mt_get_category_list',
109 array('array', 'string', 'string', 'string'),
110 t('Returns a list of all categories defined in the weblog.')),
112 'mt.getPostCategories',
113 'blogapi_mt_get_post_categories',
114 array('array', 'string', 'string', 'string'),
115 t('Returns a list of all categories to which the post is assigned.')),
117 'mt.setPostCategories',
118 'blogapi_mt_set_post_categories',
119 array('boolean', 'string', 'string', 'string', 'array'),
120 t('Sets the categories for a post.')),
122 'mt.supportedMethods',
123 'xmlrpc_server_list_methods',
125 t('Retrieve information about the XML-RPC methods supported by the server.')),
127 'mt.supportedTextFilters',
128 'blogapi_mt_supported_text_filters',
130 t('Retrieve information about the text formatting plugins supported by the server.')),
132 'mt.getTrackbackPings',
133 'blogapi_mt_get_trackback_pings',
134 array('array', 'string'),
135 t('Retrieve the list of TrackBack pings posted to a particular entry. This could be used to programmatically retrieve the list of pings for a particular entry, then iterate through each of those pings doing the same, until one has built up a graph of the web of entries referencing one another on a particular topic.')),
138 'blogap_mti_publish_post',
139 array('boolean', 'string', 'string', 'string'),
140 t('Publish (rebuild) all of the static files related to an entry from your weblog. Equivalent to saving an entry in the system (but without the ping).')));
144 * Blogging API callback. Finds the URL of a user's blog.
147 function blogapi_blogger_get_users_blogs($appid, $username, $password) {
149 $user = blogapi_validate_user($username, $password);
151 $types = _blogapi_get_node_types();
153 foreach ($types as $type) {
154 $structs[] = array('url' => url('blog/' . $user->uid, NULL, NULL, true), 'blogid' => $type, 'blogName' => $user->name . ": " . $type);
159 return blogapi_error($user);
164 * Blogging API callback. Returns profile information about a user.
166 function blogapi_blogger_get_user_info($appkey, $username, $password) {
167 $user = blogapi_validate_user($username, $password);
170 $name = explode(' ', $user->realname ? $user->realname : $user->name, 2);
172 'userid' => $user->uid,
173 'lastname' => $name[1],
174 'firstname' => $name[0],
175 'nickname' => $user->name,
176 'email' => $user->mail,
177 'url' => url('blog/' . $user->uid, NULL, NULL, true));
180 return blogapi_error($user);
185 * Blogging API callback. Inserts a new blog post as a node.
187 function blogapi_blogger_new_post($appkey, $blogid, $username, $password, $content, $publish) {
188 $user = blogapi_validate_user($username, $password);
190 return blogapi_error($user);
194 $edit['type'] = _blogapi_blogid($blogid);
195 // get the node type defaults
196 $node_type_default = variable_get('node_options_'. $edit['type'], array('status', 'promote'));
197 $edit['uid'] = $user->uid;
198 $edit['name'] = $user->name;
199 $edit['promote'] = in_array('promote', $node_type_default);
200 $edit['comment'] = variable_get('comment_'. $edit['type'], 2);
201 $edit['moderate'] = in_array('moderate', $node_type_default);
202 $edit['revision'] = in_array('revision', $node_type_default);
203 $edit['format'] = FILTER_FORMAT_DEFAULT;
204 $edit['status'] = $publish;
206 // check for bloggerAPI vs. metaWeblogAPI
207 if (is_array($content)) {
208 $edit['title'] = $content['title'];
209 $edit['body'] = $content['description'];
210 _blogapi_mt_extra($edit, $content);
213 $edit['title'] = blogapi_blogger_title($content);
214 $edit['body'] = $content;
217 if (!node_access('create', $edit['type'])) {
218 return blogapi_error(t('You do not have permission to create the type of post you wanted to create.'));
221 if (user_access('administer nodes') && !isset($edit['date'])) {
222 $edit['date'] = format_date(time(), 'custom', 'Y-m-d H:i:s O');
225 node_validate($edit);
226 if ($errors = form_get_errors()) {
227 return blogapi_error(implode("\n", $errors));
230 $node = node_submit($edit);
233 watchdog('content', t('%type: added %title using blog API.', array('%type' => '<em>'. t($node->type) .'</em>', '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
234 // blogger.newPost returns a string so we cast the nid to a string by putting it in double quotes:
238 return blogapi_error(t('Error storing post.'));
242 * Blogging API callback. Modifies the specified blog node.
244 function blogapi_blogger_edit_post($appkey, $postid, $username, $password, $content, $publish) {
246 $user = blogapi_validate_user($username, $password);
249 return blogapi_error($user);
252 $node = node_load($postid);
254 return blogapi_error(message_na());
256 // Let the teaser be re-generated.
257 unset($node->teaser);
259 if (!node_access('update', $node)) {
260 return blogapi_error(t('You do not have permission to update this post.'));
263 $node->status = $publish;
265 // check for bloggerAPI vs. metaWeblogAPI
266 if (is_array($content)) {
267 $node->title = $content['title'];
268 $node->body = $content['description'];
269 _blogapi_mt_extra($node, $content);
272 $node->title = blogapi_blogger_title($content);
273 $node->body = $content;
276 node_validate($node);
277 if ($errors = form_get_errors()) {
278 return blogapi_error(implode("\n", $errors));
281 if (user_access('administer nodes') && !isset($edit['date'])) {
282 $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
284 $node = node_submit($node);
287 watchdog('content', t('%type: updated %title using blog API.', array('%type' => '<em>'. t($node->type) .'</em>', '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
291 return blogapi_error(t('Error storing post.'));
295 * Blogging API callback. Returns a specified blog node.
297 function blogapi_blogger_get_post($appkey, $postid, $username, $password) {
298 $user = blogapi_validate_user($username, $password);
300 return blogapi_error($user);
303 $node = node_load($postid);
305 return _blogapi_get_post($node, true);
309 * Blogging API callback. Removes the specified blog node.
311 function blogapi_blogger_delete_post($appkey, $postid, $username, $password, $publish) {
312 $user = blogapi_validate_user($username, $password);
314 return blogapi_error($user);
317 node_delete($postid);
322 * Blogging API callback. Returns the latest few postings in a user's blog. $bodies TRUE
323 * <a href="http://movabletype.org/docs/mtmanual_programmatic.html#item_mt%2EgetRecentPostTitles">
324 * returns a bandwidth-friendly list</a>.
326 function blogapi_blogger_get_recent_posts($appkey, $blogid, $username, $password, $number_of_posts, $bodies = TRUE) {
327 // Remove unused appkey (from bloggerAPI).
328 $user = blogapi_validate_user($username, $password);
330 return blogapi_error($user);
333 $type = _blogapi_blogid($blogid);
335 $result = db_query_range("SELECT n.nid, n.title, r.body, n.created, u.name FROM {node} n, {node_revisions} r, {users} u WHERE n.uid = u.uid AND n.vid = r.vid AND n.type = '%s' AND n.uid = %d ORDER BY n.created DESC", $type, $user->uid, 0, $number_of_posts);
338 $result = db_query_range("SELECT n.nid, n.title, n.created, u.name FROM {node} n, {users} u WHERE n.uid = u.uid AND n.type = '%s' AND n.uid = %d ORDER BY n.created DESC", $type, $user->uid, 0, $number_of_posts);
341 while ($blog = db_fetch_object($result)) {
342 $blogs[] = _blogapi_get_post($blog, $bodies);
347 function blogapi_metaweblog_new_post($blogid, $username, $password, $content, $publish) {
348 return blogapi_blogger_new_post('0123456789ABCDEF', $blogid, $username, $password, $content, $publish);
351 function blogapi_metaweblog_edit_post($postid, $username, $password, $content, $publish) {
352 return blogapi_blogger_edit_post('0123456789ABCDEF', $postid, $username, $password, $content, $publish);
355 function blogapi_metaweblog_get_post($postid, $username, $password) {
356 return blogapi_blogger_get_post('01234567890ABCDEF', $postid, $username, $password);
360 * Blogging API callback. Inserts a file into Drupal.
362 function blogapi_metaweblog_new_media_object($blogid, $username, $password, $file) {
363 $user = blogapi_validate_user($username, $password);
365 return blogapi_error($user);
368 $name = basename($file['name']);
369 $data = $file['bits'];
372 return blogapi_error(t('No file sent.'));
375 if (!$file = file_save_data($data, $name)) {
376 return blogapi_error(t('Error storing file.'));
379 // Return the successful result.
380 return array('url' => file_create_url($file), 'struct');
383 * Blogging API callback. Returns a list of the taxonomy terms that can be
384 * associated with a blog node.
386 function blogapi_metaweblog_get_category_list($blogid, $username, $password) {
387 $type = _blogapi_blogid($blogid);
388 $vocabularies = module_invoke('taxonomy', 'get_vocabularies', $type, 'vid');
389 $categories = array();
391 foreach ($vocabularies as $vocabulary) {
392 $terms = module_invoke('taxonomy', 'get_tree', $vocabulary->vid, 0, -1);
393 foreach ($terms as $term) {
394 $term_name = $term->name;
395 foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
396 $term_name = $parent->name . '/' . $term_name;
398 $categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid);
405 function blogapi_metaweblog_get_recent_posts($blogid, $username, $password, $number_of_posts) {
406 return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid, $username, $password, $number_of_posts, TRUE);
410 function blogapi_mt_get_recent_post_titles($blogid, $username, $password, $number_of_posts) {
411 return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid, $username, $password, $number_of_posts, FALSE);
415 function blogapi_mt_get_category_list($blogid, $username, $password) {
416 return blogapi_metaweblog_get_category_list($blogid, $username, $password);
420 * Blogging API callback. Returns a list of the taxonomy terms that are
421 * assigned to a particular node.
423 function blogapi_mt_get_post_categories($postid, $username, $password) {
424 $user = blogapi_validate_user($username, $password);
426 return blogapi_error($user);
429 $terms = module_invoke('taxonomy', 'node_get_terms', $postid, 'tid');
430 $categories = array();
431 foreach ($terms as $term) {
432 $term_name = $term->name;
433 foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
434 $term_name = $parent->name . '/' . $term_name;
436 $categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid, 'isPrimary' => true);
442 * Blogging API callback. Assigns taxonomy terms to a particular node.
444 function blogapi_mt_set_post_categories($postid, $username, $password, $categories) {
445 $user = blogapi_validate_user($username, $password);
447 return blogapi_error($user);
450 $node = node_load($postid);
451 $node->taxonomy = array();
452 foreach ($categories as $category) {
453 $node->taxonomy[] = $category['categoryId'];
460 * Blogging API callback. Sends a list of available input formats.
462 function blogapi_mt_supported_text_filters() {
463 // NOTE: we're only using anonymous' formats because the MT spec
464 // does not allow for per-user formats.
465 $formats = filter_formats();
468 foreach ($formats as $format) {
469 $filter['key'] = $format->format;
470 $filter['label'] = $format->name;
471 $filters[] = $filter;
478 * Blogging API callback. Can not be implemented without support from
481 function blogapi_mt_get_trackback_pings() {
482 return blogapi_error(t('Not implemented.'));
486 * Blogging API callback. Publishes the given node
488 function blogap_mti_publish_post($postid, $username, $password) {
489 $user = blogapi_validate_user($username, $password);
491 return blogapi_error($user);
493 $node = node_load($postid);
495 return blogapi_error(t('Invalid post.'));
499 if (!node_access('update', $node)) {
500 return blogapi_error(t('You do not have permission to update this post.'));
509 * Prepare an error message for returning to the XMLRPC caller.
511 function blogapi_error($message) {
512 static $xmlrpcusererr;
513 if (!is_array($message)) {
514 $message = array($message);
517 $message = implode(' ', $message);
519 return xmlrpc_error($xmlrpcusererr + 1, strip_tags($message));
523 * Ensure that the given user has permission to edit a blog.
525 function blogapi_validate_user($username, $password) {
528 $user = user_authenticate($username, $password);
531 if (user_access('edit own blog', $user)) {
535 return t("You either tried to edit somebody else's blog or you don't have permission to edit your own blog.");
539 return t('Wrong username or password.');
544 * For the blogger API, extract the node title from the contents field.
546 function blogapi_blogger_title(&$contents) {
547 if (eregi('<title>([^<]*)</title>', $contents, $title)) {
548 $title = strip_tags($title[0]);
549 $contents = ereg_replace('<title>[^<]*</title>', '', $contents);
552 list($title, $contents) = explode("\n", $contents, 2);
557 function blogapi_settings() {
558 $form['blogapi_engine'] = array(
559 '#type' => 'select', '#title' => t('XML-RPC Engine'), '#default_value' => variable_get('blogapi_engine', 0),
560 '#options' => array(0 => 'Blogger', 1 => 'MetaWeblog', 2 => 'Movabletype'),
561 '#description' => t('RSD or Really-Simple-Discovery is a mechanism which allows external blogger tools to discover the APIs they can use to interact with Drupal. Here you can set the preferred method for blogger tools to interact with your site. The common XML-RPC engines are Blogger, MetaWeblog and Movabletype. If you are not sure which is the correct setting, choose Blogger.')
564 $node_types = node_get_types();
565 $defaults = isset($node_types['blog']) ? array('blog' => 1) : array();
566 $form['blogapi_node_types'] = array(
567 '#type' => 'checkboxes', '#title' => t('Blog types'), '#required' => TRUE,
568 '#default_value' => variable_get('blogapi_node_types', $defaults), '#options' => $node_types,
569 '#description' => t('Select the content types for which you wish to enable posting via blogapi. Each type will appear as a different "blog" in the client application (if supported).')
575 function blogapi_menu($may_cache) {
578 if (drupal_is_front_page()) {
579 drupal_add_link(array('rel' => 'EditURI',
580 'type' => 'application/rsd+xml',
582 'href' => url('blogapi/rsd', NULL, NULL, TRUE)));
586 $items[] = array('path' => 'blogapi', 'title' => t('RSD'), 'callback' => 'blogapi_blogapi', 'access' => user_access('access content'), 'type' => MENU_CALLBACK);
592 function blogapi_blogapi() {
603 function blogapi_rsd() {
606 $xmlrpc = $base_url .'/'. 'xmlrpc.php';
607 $base = url('', NULL, NULL, TRUE);
608 $blogid = 1; # until we figure out how to handle multiple bloggers
610 drupal_set_header('Content-Type: application/rsd+xml; charset=utf-8');
612 <?xml version="1.0"?>
613 <rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
615 <engineName>Drupal</engineName>
616 <engineLink>http://drupal.org/</engineLink>
617 <homePageLink>$base</homePageLink>
619 <api name="MetaWeblog" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
620 <api name="Blogger" preferred="true" apiLink="$xmlrpc" blogID="$blogid" />
621 <api name="MovableType" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
629 * Handles extra information sent by clients according to MovableType's spec.
631 function _blogapi_mt_extra(&$node, $struct) {
632 if (is_array($node)) {
634 $node = (object)$node;
638 if (array_key_exists('mt_allow_comments', $struct)) {
639 switch ($struct['mt_allow_comments']) {
641 $node->comment = COMMENT_NODE_DISABLED;
644 $node->comment = COMMENT_NODE_READ_WRITE;
647 $node->comment = COMMENT_NODE_READ_ONLY;
652 // merge the 3 body sections (description, mt_excerpt, mt_text_more) into
654 if ($struct['mt_excerpt']) {
655 $node->body = $struct['mt_excerpt'] .'<!--break-->'.$node->body;
657 if ($struct['mt_text_more']) {
658 $node->body = $node->body . '<!--extended-->' . $struct['mt_text_more'];
662 if (function_exists('trackback_send')) {
663 if (is_array($struct['mt_tb_ping_urls'])) {
664 foreach ($struct['mt_tb_ping_urls'] as $tb_ping_url) {
665 $node->tb_url = $tb_ping_url->getVal();
666 trackback_send($node);
667 unset($node->tb_url); // make sure we don't ping twice
671 $node->tb_url = $struct['mt_tb_ping_urls'];
676 if ($struct['mt_convert_breaks']) {
677 $node->format = $struct['mt_convert_breaks'];
681 if ($struct['dateCreated']) {
682 $node->date = format_date(mktime($struct['dateCreated']->hour, $struct['dateCreated']->minute, $struct['dateCreated']->second, $struct['dateCreated']->month, $struct['dateCreated']->day, $struct['dateCreated']->year), 'custom', 'Y-m-d H:i:s O');
686 $node = (array)$node;
690 function _blogapi_get_post($node, $bodies = true) {
692 'userid' => $node->name,
693 'dateCreated' => xmlrpc_date($node->created),
694 'title' => $node->title,
695 'postid' => $node->nid,
696 'link' => url('node/'.$node->nid, NULL, NULL, true),
697 'permaLink' => url('node/'.$node->nid, NULL, NULL, true),
700 if ($node->comment = 1) {
703 if ($node->comment = 2) {
707 $xmlrpcval['content'] = "<title>$node->title</title>$node->body";
708 $xmlrpcval['description'] = $node->body;
709 // Add MT specific fields
710 $xmlrpcval['mt_allow_comments'] = $comment;
711 $xmlrpcval['mt_convert_breaks'] = $node->format;
717 function _blogapi_blogid($id) {
718 if (is_numeric($id)) {
726 function _blogapi_get_node_types() {
727 $available_types = array_keys(array_filter(variable_get('blogapi_node_types', array('blog' => 1))));
729 foreach (node_get_types() as $type => $name) {
730 if (node_access('create', $type) && in_array($type, $available_types)) {