blogapi.module 33.7 KB
Newer Older
Dries's avatar
 
Dries committed
1 2 3
<?php
// $Id$

Dries's avatar
 
Dries committed
4 5 6 7 8
/**
 * @file
 * Enable users to post using applications that support XML-RPC blog APIs.
 */

9 10 11
/**
 * Implementation of hook_help().
 */
12 13
function blogapi_help($path, $arg) {
  switch ($path) {
14
    case 'admin/help#blogapi':
15 16 17 18
      $output = '<p>' . t("The Blog API module allows your site's users to access and post to their blogs from external blogging clients. External blogging clients are available for a wide range of desktop operating systems, and generally provide a feature-rich graphical environment for creating and editing posts.") . '</p>';
      $output .= '<p>' . t('<a href="@ecto-link">Ecto</a>, a blogging client available for both Mac OS X and Microsoft Windows, can be used with Blog API. Blog API also supports <a href="@blogger-api">Blogger API</a>, <a href="@metaweblog-api">MetaWeblog API</a>, and most of the <a href="@movabletype-api">Movable Type API</a>. Blogging clients and other services (e.g. <a href="@flickr">Flickr\'s</a> "post to blog") that support these APIs may also be compatible.', array('@ecto-link' => url('http://infinite-sushi.com/software/ecto/'), '@blogger-api' => url('http://www.blogger.com/developers/api/1_docs/'), '@metaweblog-api' => url('http://www.xmlrpc.com/metaWeblogApi'), '@movabletype-api' => url('http://www.movabletype.org/docs/mtmanual_programmatic.html'), '@flickr' => url('http://www.flickr.com'))) . '</p>';
      $output .= '<p>' . t('Select the content types available to external clients on the <a href="@blogapi-settings">Blog API settings page</a>. If supported and available, each content type will be displayed as a separate "blog" by the external client.', array('@blogapi-settings' => url('admin/settings/blogapi'))) . '</p>';
      $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@blogapi">Blog API module</a>.', array('@blogapi' => url('http://drupal.org/handbook/modules/blogapi/'))) . '</p>';
19
      return $output;
Dries's avatar
 
Dries committed
20 21 22
  }
}

23 24 25 26
/**
 * Implementation of hook_perm().
 */
function blogapi_perm() {
27
  return array(
28 29 30 31
    'administer content with blog api' => array(
      'title' => t('Administer content with blog API'),
      'description' => t('Manage website content from external tools.'),
    ),
32
  );
33 34
}

35 36 37
/**
 * Implementation of hook_xmlrpc().
 */
Dries's avatar
 
Dries committed
38
function blogapi_xmlrpc() {
39 40 41 42 43
  return array(
    array(
      'blogger.getUsersBlogs',
      'blogapi_blogger_get_users_blogs',
      array('array', 'string', 'string', 'string'),
44
      t('Returns a list of blogs to which an author has posting privileges.')),
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
    array(
      'blogger.getUserInfo',
      'blogapi_blogger_get_user_info',
      array('struct', 'string', 'string', 'string'),
      t('Returns information about an author in the system.')),
    array(
      'blogger.newPost',
      'blogapi_blogger_new_post',
      array('string', 'string', 'string', 'string', 'string', 'string', 'boolean'),
      t('Creates a new post, and optionally publishes it.')),
    array(
      'blogger.editPost',
      'blogapi_blogger_edit_post',
      array('boolean', 'string', 'string', 'string', 'string', 'string', 'boolean'),
      t('Updates the information about an existing post.')),
60 61 62 63 64
    array(
      'blogger.getPost',
      'blogapi_blogger_get_post',
      array('struct', 'string', 'string', 'string', 'string'),
      t('Returns information about a specific post.')),
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
    array(
      'blogger.deletePost',
      'blogapi_blogger_delete_post',
      array('boolean', 'string', 'string', 'string', 'string', 'boolean'),
      t('Deletes a post.')),
    array(
      'blogger.getRecentPosts',
      'blogapi_blogger_get_recent_posts',
      array('array', 'string', 'string', 'string', 'string', 'int'),
      t('Returns a list of the most recent posts in the system.')),
    array(
      'metaWeblog.newPost',
      'blogapi_metaweblog_new_post',
      array('string', 'string', 'string', 'string', 'struct', 'boolean'),
      t('Creates a new post, and optionally publishes it.')),
    array(
      'metaWeblog.editPost',
      'blogapi_metaweblog_edit_post',
      array('boolean', 'string', 'string', 'string', 'struct', 'boolean'),
      t('Updates information about an existing post.')),
    array(
      'metaWeblog.getPost',
      'blogapi_metaweblog_get_post',
      array('struct', 'string', 'string', 'string'),
      t('Returns information about a specific post.')),
    array(
      'metaWeblog.newMediaObject',
      'blogapi_metaweblog_new_media_object',
      array('string', 'string', 'string', 'string', 'struct'),
      t('Uploads a file to your webserver.')),
    array(
      'metaWeblog.getCategories',
      'blogapi_metaweblog_get_category_list',
      array('struct', 'string', 'string', 'string'),
      t('Returns a list of all categories to which the post is assigned.')),
    array(
      'metaWeblog.getRecentPosts',
      'blogapi_metaweblog_get_recent_posts',
      array('array', 'string', 'string', 'string', 'int'),
      t('Returns a list of the most recent posts in the system.')),
    array(
      'mt.getRecentPostTitles',
      'blogapi_mt_get_recent_post_titles',
      array('array', 'string', 'string', 'string', 'int'),
      t('Returns a bandwidth-friendly list of the most recent posts in the system.')),
    array(
      'mt.getCategoryList',
      'blogapi_mt_get_category_list',
      array('array', 'string', 'string', 'string'),
114
      t('Returns a list of all categories defined in the blog.')),
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
    array(
      'mt.getPostCategories',
      'blogapi_mt_get_post_categories',
      array('array', 'string', 'string', 'string'),
      t('Returns a list of all categories to which the post is assigned.')),
    array(
      'mt.setPostCategories',
      'blogapi_mt_set_post_categories',
      array('boolean', 'string', 'string', 'string', 'array'),
      t('Sets the categories for a post.')),
    array(
      'mt.supportedMethods',
      'xmlrpc_server_list_methods',
      array('array'),
      t('Retrieve information about the XML-RPC methods supported by the server.')),
    array(
      'mt.supportedTextFilters',
      'blogapi_mt_supported_text_filters',
      array('array'),
      t('Retrieve information about the text formatting plugins supported by the server.')),
    array(
      'mt.publishPost',
137
      'blogapi_mt_publish_post',
138
      array('boolean', 'string', 'string', 'string'),
139
      t('Publish (rebuild) all of the static files related to an entry from your blog. Equivalent to saving an entry in the system (but without the ping).')));
Dries's avatar
 
Dries committed
140 141
}

142 143 144
/**
 * Blogging API callback. Finds the URL of a user's blog.
 */
145
function blogapi_blogger_get_users_blogs($appid, $username, $password) {
146
  $user = blogapi_validate_user($username, $password);
Dries's avatar
 
Dries committed
147
  if ($user->uid) {
148 149 150
    $types = _blogapi_get_node_types();
    $structs = array();
    foreach ($types as $type) {
151
      $structs[] = array('url' => url('blog/' . $user->uid, array('absolute' => TRUE)), 'blogid' => $type, 'blogName' => $user->name . ": " . $type);
152
    }
153

154
    return $structs;
Dries's avatar
 
Dries committed
155 156
  }
  else {
157
    return blogapi_error($user);
Dries's avatar
 
Dries committed
158 159 160
  }
}

161 162 163
/**
 * Blogging API callback. Returns profile information about a user.
 */
164 165
function blogapi_blogger_get_user_info($appkey, $username, $password) {
  $user = blogapi_validate_user($username, $password);
166

Dries's avatar
 
Dries committed
167
  if ($user->uid) {
168
    $name = explode(' ', $user->realname ? $user->realname : $user->name, 2);
169 170 171 172 173 174
    return array(
      'userid' => $user->uid,
      'lastname' => $name[1],
      'firstname' => $name[0],
      'nickname' => $user->name,
      'email' => $user->mail,
175
      'url' => url('blog/' . $user->uid, array('absolute' => TRUE)));
Dries's avatar
 
Dries committed
176 177
  }
  else {
178
    return blogapi_error($user);
Dries's avatar
 
Dries committed
179 180 181
  }
}

182 183 184
/**
 * Blogging API callback. Inserts a new blog post as a node.
 */
185 186
function blogapi_blogger_new_post($appkey, $blogid, $username, $password, $content, $publish) {
  $user = blogapi_validate_user($username, $password);
Dries's avatar
 
Dries committed
187
  if (!$user->uid) {
188
    return blogapi_error($user);
Dries's avatar
 
Dries committed
189 190
  }

191 192 193 194 195
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
    // Return an error if not configured type.
    return $error;
  }

196
  $edit = array();
197
  $edit['type'] = $blogid;
198
  // Get the node type defaults.
199
  $node_type_default = variable_get('node_options_' . $edit['type'], array('status', 'promote'));
200 201
  $edit['uid'] = $user->uid;
  $edit['name'] = $user->name;
202
  $edit['promote'] = in_array('promote', $node_type_default);
203
  $edit['comment'] = variable_get('comment_' . $edit['type'], 2);
204
  $edit['revision'] = in_array('revision', $node_type_default);
Dries's avatar
 
Dries committed
205
  $edit['format'] = FILTER_FORMAT_DEFAULT;
206
  $edit['status'] = $publish;
Dries's avatar
 
Dries committed
207

208
  // Check for bloggerAPI vs. metaWeblogAPI.
209 210 211 212
  if (is_array($content)) {
    $edit['title'] = $content['title'];
    $edit['body'] = $content['description'];
    _blogapi_mt_extra($edit, $content);
Dries's avatar
 
Dries committed
213 214
  }
  else {
215 216
    $edit['title'] = blogapi_blogger_title($content);
    $edit['body'] = $content;
Dries's avatar
 
Dries committed
217
  }
Dries's avatar
 
Dries committed
218

219
  if (!node_access('create', $edit['type'])) {
220
    return blogapi_error(t('You do not have permission to create this type of post.'));
Dries's avatar
 
Dries committed
221 222
  }

223
  if (user_access('administer nodes') && !isset($edit['date'])) {
224
    $edit['date'] = format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s O');
225
  }
226

227
  node_invoke_node($edit, 'blogapi_new');
228

229 230 231 232 233
  $valid = blogapi_status_error_check($edit, $publish);
  if ($valid !== TRUE) {
    return $valid;
  }

234 235 236 237 238
  node_validate($edit);
  if ($errors = form_get_errors()) {
    return blogapi_error(implode("\n", $errors));
  }

239
  $node = node_submit($edit);
240
  node_save($node);
241
  if ($node->nid) {
242
    watchdog('content', '@type: added %title using blog API.', array('@type' => $node->type, '%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
243
    // blogger.newPost returns a string so we cast the nid to a string by putting it in double quotes.
244
    return "$node->nid";
Dries's avatar
 
Dries committed
245 246
  }

247
  return blogapi_error(t('Error storing post.'));
Dries's avatar
 
Dries committed
248 249
}

250 251 252
/**
 * Blogging API callback. Modifies the specified blog node.
 */
253 254
function blogapi_blogger_edit_post($appkey, $postid, $username, $password, $content, $publish) {
  $user = blogapi_validate_user($username, $password);
255

Dries's avatar
 
Dries committed
256
  if (!$user->uid) {
257
    return blogapi_error($user);
Dries's avatar
 
Dries committed
258 259
  }

260
  $node = node_load($postid);
Dries's avatar
 
Dries committed
261
  if (!$node) {
262
    return blogapi_error(t('n/a'));
Dries's avatar
 
Dries committed
263
  }
264 265
  // Let the teaser be re-generated.
  unset($node->teaser);
Dries's avatar
 
Dries committed
266

267
  if (!node_access('update', $node)) {
268
    return blogapi_error(t('You do not have permission to update this post.'));
Dries's avatar
 
Dries committed
269
  }
270 271
  // Save the original status for validation of permissions.
  $original_status = $node->status;
272
  $node->status = $publish;
273

Dries's avatar
 
Dries committed
274
  // check for bloggerAPI vs. metaWeblogAPI
275 276 277 278
  if (is_array($content)) {
    $node->title = $content['title'];
    $node->body = $content['description'];
    _blogapi_mt_extra($node, $content);
Dries's avatar
 
Dries committed
279 280
  }
  else {
281 282
    $node->title = blogapi_blogger_title($content);
    $node->body = $content;
Dries's avatar
 
Dries committed
283
  }
Dries's avatar
 
Dries committed
284

285
  node_invoke_node($node, 'blogapi_edit');
286

287 288 289 290 291
  $valid = blogapi_status_error_check($node, $original_status);
  if ($valid !== TRUE) {
    return $valid;
  }

292
  node_validate($node);
293 294
  if ($errors = form_get_errors()) {
    return blogapi_error(implode("\n", $errors));
Dries's avatar
 
Dries committed
295 296
  }

297 298 299
  if (user_access('administer nodes') && !isset($edit['date'])) {
    $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
  }
300
  $node = node_submit($node);
301
  node_save($node);
302
  if ($node->nid) {
303
    watchdog('content', '@type: updated %title using Blog API.', array('@type' => $node->type, '%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
304
    return TRUE;
Dries's avatar
 
Dries committed
305 306
  }

307
  return blogapi_error(t('Error storing post.'));
Dries's avatar
 
Dries committed
308 309
}

310 311 312 313 314 315 316 317 318 319 320
/**
 * Blogging API callback. Returns a specified blog node.
 */
function blogapi_blogger_get_post($appkey, $postid, $username, $password) {
  $user = blogapi_validate_user($username, $password);
  if (!$user->uid) {
    return blogapi_error($user);
  }

  $node = node_load($postid);

321
  return _blogapi_get_post($node, TRUE);
322 323
}

324 325 326 327 328 329 330
/**
 * Check that the user has permission to save the node with the chosen status.
 *
 * @return
 *   TRUE if no error, or the blogapi_error().
 */
function blogapi_status_error_check($node, $original_status) {
331

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
  $node = (object) $node;

  $node_type_default = variable_get('node_options_'. $node->type, array('status', 'promote'));

  // If we don't have the 'administer nodes' permission and the status is
  // changing or for a new node the status is not the content type's default,
  // then return an error.
  if (!user_access('administer nodes') && (($node->status != $original_status) || (empty($node->nid) && $node->status != in_array('status', $node_type_default)))) {
    if ($node->status) {
      return blogapi_error(t('You do not have permission to publish this type of post. Please save it as a draft instead.'));
    }
    else {
      return blogapi_error(t('You do not have permission to save this post as a draft. Please publish it instead.'));
    }
  }
  return TRUE;
}


351
/**
352
 * Blogging API callback. Removes the specified blog node.
353
 */
354
function blogapi_blogger_delete_post($appkey, $postid, $username, $password, $publish) {
355
  $user = blogapi_validate_user($username, $password);
Dries's avatar
 
Dries committed
356
  if (!$user->uid) {
357
    return blogapi_error($user);
Dries's avatar
 
Dries committed
358 359
  }

360
  node_delete($postid);
361
  return TRUE;
362
}
363

364 365 366 367 368 369 370 371 372 373 374
/**
 * Blogging API callback. Returns the latest few postings in a user's blog. $bodies TRUE
 * <a href="http://movabletype.org/docs/mtmanual_programmatic.html#item_mt%2EgetRecentPostTitles">
 * returns a bandwidth-friendly list</a>.
 */
function blogapi_blogger_get_recent_posts($appkey, $blogid, $username, $password, $number_of_posts, $bodies = TRUE) {
  // Remove unused appkey (from bloggerAPI).
  $user = blogapi_validate_user($username, $password);
  if (!$user->uid) {
    return blogapi_error($user);
  }
Dries's avatar
 
Dries committed
375

376 377 378 379 380
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
    // Return an error if not configured type.
    return $error;
  }

381
  if ($bodies) {
382
    $result = db_query_range("SELECT n.nid, n.title, r.body, r.format, n.comment, n.created, u.name FROM {node} n, {node_revision} r, {users} u WHERE n.uid = u.uid AND n.vid = r.vid AND n.type = :type AND n.uid = :uid ORDER BY n.created DESC",  array(
383 384 385
      ':type' => $blogid,
      ':uid' => $user->uid
    ), 0, $number_of_posts);
386 387
  }
  else {
388
    $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 = :type AND n.uid = :uid ORDER BY n.created DESC", array(
389 390 391
      ':type' => $blogid,
      ':uid' => $user->uid
    ), 0, $number_of_posts);
392
  }
393
  $blogs = array();
394
  foreach ($result as $blog) {
395 396
    $blogs[] = _blogapi_get_post($blog, $bodies);
  }
397

398 399 400 401 402 403 404 405 406
  return $blogs;
}

function blogapi_metaweblog_new_post($blogid, $username, $password, $content, $publish) {
  return blogapi_blogger_new_post('0123456789ABCDEF', $blogid, $username, $password, $content, $publish);
}

function blogapi_metaweblog_edit_post($postid, $username, $password, $content, $publish) {
  return blogapi_blogger_edit_post('0123456789ABCDEF', $postid, $username, $password, $content, $publish);
Dries's avatar
 
Dries committed
407 408
}

409
function blogapi_metaweblog_get_post($postid, $username, $password) {
410
  return blogapi_blogger_get_post('01234567890ABCDEF', $postid, $username, $password);
Dries's avatar
 
Dries committed
411 412
}

413 414 415
/**
 * Blogging API callback. Inserts a file into Drupal.
 */
416 417
function blogapi_metaweblog_new_media_object($blogid, $username, $password, $file) {
  $user = blogapi_validate_user($username, $password);
418 419 420 421
  if (!$user->uid) {
    return blogapi_error($user);
  }

422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
  $usersize = 0;
  $uploadsize = 0;

  $roles = array_intersect(user_roles(FALSE, 'administer content with blog api'), $user->roles);

  foreach ($roles as $rid => $name) {
    $extensions .= ' ' . strtolower(variable_get("blogapi_extensions_$rid", variable_get('blogapi_extensions_default', 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp')));
    $usersize = max($usersize, variable_get("blogapi_usersize_$rid", variable_get('blogapi_usersize_default', 1)) * 1024 * 1024);
    $uploadsize = max($uploadsize, variable_get("blogapi_uploadsize_$rid", variable_get('blogapi_uploadsize_default', 1)) * 1024 * 1024);
  }

  $filesize = strlen($file['bits']);

  if ($filesize > $uploadsize) {
    return blogapi_error(t('It is not possible to upload the file, because it exceeded the maximum filesize of @maxsize.', array('@maxsize' => format_size($uploadsize))));
  }

  if (_blogapi_space_used($user->uid) + $filesize > $usersize) {
    return blogapi_error(t('The file can not be attached to this post, because the disk quota of @quota has been reached.', array('@quota' => format_size($usersize))));
  }

  // Only allow files with whitelisted extensions and convert remaining dots to
  // underscores to prevent attacks via non-terminal executable extensions with
  // files such as exploit.php.jpg.

  $whitelist = array_unique(explode(' ', trim($extensions)));

449
  $name = basename($file['name']);
450 451 452 453 454 455 456 457 458 459 460 461 462

  if ($extension_position = strrpos($name, '.')) {
    $filename = drupal_substr($name, 0, $extension_position);
    $final_extension = drupal_substr($name, $extension_position + 1);

    if (!in_array(strtolower($final_extension), $whitelist)) {
      return blogapi_error(t('It is not possible to upload the file, because it is only possible to upload files with the following extensions: @extensions', array('@extensions' => implode(' ', $whitelist))));
    }

    $filename = str_replace('.', '_', $filename);
    $filename .= '.' . $final_extension;
  }

463
  $data = $file['bits'];
464 465

  if (!$data) {
466
    return blogapi_error(t('No file sent.'));
467
  }
468

469
  if (!$file = file_unmanaged_save_data($data, $filename)) {
470
    return blogapi_error(t('Error storing file.'));
471
  }
472

473 474 475 476 477 478 479
  $row = new stdClass();
  $row->uid = $user->uid;
  $row->filepath = $file;
  $row->filesize = $filesize;

  drupal_write_record('blogapi_files', $row);

480
  // Return the successful result.
481
  return array('url' => file_create_url($file), 'struct');
Dries's avatar
 
Dries committed
482
}
483 484 485 486
/**
 * Blogging API callback. Returns a list of the taxonomy terms that can be
 * associated with a blog node.
 */
487
function blogapi_metaweblog_get_category_list($blogid, $username, $password) {
488 489 490 491 492
  $user = blogapi_validate_user($username, $password);
  if (!$user->uid) {
    return blogapi_error($user);
  }

493 494 495 496 497 498
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
    // Return an error if not configured type.
    return $error;
  }

  $vocabularies = module_invoke('taxonomy', 'get_vocabularies', $blogid, 'vid');
499
  $categories = array();
500 501
  if ($vocabularies) {
    foreach ($vocabularies as $vocabulary) {
502
      $terms = module_invoke('taxonomy', 'get_tree', $vocabulary->vid);
503 504 505
      foreach ($terms as $term) {
        $term_name = $term->name;
        foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
506
          $term_name = $parent->name . '/' . $term_name;
507
        }
508
        $categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid);
Dries's avatar
 
Dries committed
509 510 511
      }
    }
  }
512

513 514 515
  return $categories;
}

516
function blogapi_metaweblog_get_recent_posts($blogid, $username, $password, $number_of_posts) {
517 518 519 520 521 522 523 524 525
  return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid, $username, $password, $number_of_posts, TRUE);
}

function blogapi_mt_get_recent_post_titles($blogid, $username, $password, $number_of_posts) {
  return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid, $username, $password, $number_of_posts, FALSE);
}

function blogapi_mt_get_category_list($blogid, $username, $password) {
  return blogapi_metaweblog_get_category_list($blogid, $username, $password);
Dries's avatar
 
Dries committed
526 527
}

528 529 530 531
/**
 * Blogging API callback. Returns a list of the taxonomy terms that are
 * assigned to a particular node.
 */
532 533
function blogapi_mt_get_post_categories($postid, $username, $password) {
  $user = blogapi_validate_user($username, $password);
Dries's avatar
 
Dries committed
534
  if (!$user->uid) {
535
    return blogapi_error($user);
Dries's avatar
 
Dries committed
536 537
  }

538 539
  $node = node_load($postid);
  $terms = module_invoke('taxonomy', 'node_get_terms', $node, 'tid');
Dries's avatar
 
Dries committed
540
  $categories = array();
541
  foreach ($terms as $term) {
Dries's avatar
 
Dries committed
542
    $term_name = $term->name;
543
    foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
544
      $term_name = $parent->name . '/' . $term_name;
Dries's avatar
 
Dries committed
545
    }
546
    $categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid, 'isPrimary' => TRUE);
Dries's avatar
 
Dries committed
547
  }
548

549
  return $categories;
Dries's avatar
 
Dries committed
550 551
}

552 553 554
/**
 * Blogging API callback. Assigns taxonomy terms to a particular node.
 */
555 556
function blogapi_mt_set_post_categories($postid, $username, $password, $categories) {
  $user = blogapi_validate_user($username, $password);
Dries's avatar
 
Dries committed
557
  if (!$user->uid) {
558
    return blogapi_error($user);
Dries's avatar
 
Dries committed
559 560
  }

561 562
  $node = node_load($postid);
  $node->taxonomy = array();
563
  foreach ($categories as $category) {
564
    $node->taxonomy[] = $category['categoryId'];
Dries's avatar
 
Dries committed
565
  }
566 567 568 569
  $validated = blogapi_mt_validate_terms($node);
  if ($validated !== TRUE) {
    return $validated;
  }
570
  node_save($node);
571

572
  return TRUE;
573 574
}

575 576 577 578 579 580 581 582 583 584 585 586
/**
 * Blogging API helper - find allowed taxonomy terms for a node type.
 */
function blogapi_mt_validate_terms($node) {
  // We do a lot of heavy lifting here since taxonomy module doesn't have a
  // stand-alone validation function.
  if (module_exists('taxonomy')) {
    $found_terms = array();
    if (!empty($node->taxonomy)) {
      $term_list = array_unique($node->taxonomy);
      $params = $term_list;
      $params[] = $node->type;
587
      $result = db_query(db_rewrite_sql("SELECT t.tid, t.vid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_vocabulary_node_type} n ON t.vid = n.vid WHERE t.tid IN (". db_placeholders($term_list) .") AND n.type = '%s'", 't', 'tid'), $params);
588 589 590 591 592 593 594 595 596 597 598 599
      $found_terms = array();
      $found_count = 0;
      while ($term = db_fetch_object($result)) {
        $found_terms[$term->vid][$term->tid] = $term->tid;
        $found_count++;
      }
      // If the counts don't match, some terms are invalid or not accessible to this user.
      if (count($term_list) != $found_count) {
        return blogapi_error(t('Invalid categories submitted.'));
      }
    }
    // Look up all the vocabularies for this node type.
600
    $result2 = db_query(db_rewrite_sql("SELECT v.vid, v.name, v.required, v.multiple FROM {taxonomy_vocabulary} v INNER JOIN {taxonomy_vocabulary_node_type} n ON v.vid = n.vid WHERE n.type = '%s'", 'v', 'vid'), $node->type);
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
    // Check each vocabulary associated with this node type.
    while ($vocabulary = db_fetch_object($result2)) {
      // Required vocabularies must have at least one term.
      if ($vocabulary->required && empty($found_terms[$vocabulary->vid])) {
        return blogapi_error(t('A category from the @vocabulary_name vocabulary is required.', array('@vocabulary_name' => $vocabulary->name)));
      }
      // Vocabularies that don't allow multiple terms may have at most one.
      if (!($vocabulary->multiple) && (isset($found_terms[$vocabulary->vid]) && count($found_terms[$vocabulary->vid]) > 1)) {
        return blogapi_error(t('You may only choose one category from the @vocabulary_name vocabulary.'), array('@vocabulary_name' => $vocabulary->name));
      }
    }
  }
  elseif (!empty($node->taxonomy)) {
    return blogapi_error(t('Error saving categories. This feature is not available.'));
  }
  return TRUE;
}

619
/**
620
 * Blogging API callback. Sends a list of available text formats.
621
 */
622
function blogapi_mt_supported_text_filters() {
623 624 625 626 627 628
  // NOTE: we're only using anonymous' formats because the MT spec
  // does not allow for per-user formats.
  $formats = filter_formats();

  $filters = array();
  foreach ($formats as $format) {
629 630 631
    $filter['key'] = $format->format;
    $filter['label'] = $format->name;
    $filters[] = $filter;
632 633
  }

634
  return $filters;
635 636 637
}

/**
638
 * Blogging API callback. Publishes the given node.
639
 */
640
function blogapi_mt_publish_post($postid, $username, $password) {
641
  $user = blogapi_validate_user($username, $password);
642

643 644 645
  if (!$user->uid) {
    return blogapi_error($user);
  }
646
  $node = node_load($postid);
647

648
  if (!$node) {
649
    return blogapi_error(t('Invalid post.'));
650 651
  }

652 653 654 655 656 657
  // Nothing needs to be done if already published.
  if ($node->status) {
    return;
  }

  if (!node_access('update', $node) || !user_access('administer nodes')) {
658
    return blogapi_error(t('You do not have permission to update this post.'));
659 660
  }

661
  $node->status = 1;
662 663
  node_save($node);

664
  return TRUE;
Dries's avatar
 
Dries committed
665 666
}

667 668 669
/**
 * Prepare an error message for returning to the XMLRPC caller.
 */
Dries's avatar
 
Dries committed
670
function blogapi_error($message) {
671
  static $xmlrpcusererr;
672

Dries's avatar
 
Dries committed
673 674
  if (!is_array($message)) {
    $message = array($message);
675 676
  }

Dries's avatar
 
Dries committed
677 678
  $message = implode(' ', $message);

679
  return xmlrpc_error($xmlrpcusererr + 1, strip_tags($message));
Dries's avatar
 
Dries committed
680 681
}

682 683 684
/**
 * Ensure that the given user has permission to edit a blog.
 */
Dries's avatar
 
Dries committed
685 686
function blogapi_validate_user($username, $password) {
  global $user;
Dries's avatar
 
Dries committed
687

688
  $user = user_authenticate(array('name' => $username, 'pass' => $password));
689

690
  if ($user->uid) {
691
    if (user_access('administer content with blog api', $user)) {
692 693 694
      return $user;
    }
    else {
695
      return t('You do not have permission to edit this blog.');
696 697 698 699
    }
  }
  else {
    return t('Wrong username or password.');
Dries's avatar
 
Dries committed
700 701 702
  }
}

703 704 705
/**
 * For the blogger API, extract the node title from the contents field.
 */
Dries's avatar
 
Dries committed
706
function blogapi_blogger_title(&$contents) {
707
  if (preg_match('/<title>(.*?)<\/title>/i', $contents, $title)) {
708
    $title = strip_tags($title[0]);
709
    $contents = preg_replace('/<title>.*?<\/title>/i', '', $contents);
Dries's avatar
 
Dries committed
710 711
  }
  else {
712
    list($title, $contents) = explode("\n", $contents, 2);
Dries's avatar
 
Dries committed
713
  }
714

Dries's avatar
 
Dries committed
715 716
  return $title;
}
Dries's avatar
 
Dries committed
717

718 719 720
/**
 * Add some settings to the admin_settings form.
 */
Dries's avatar
Dries committed
721
function blogapi_admin_settings() {
722
  $node_types = array_map('check_plain', node_get_types('names'));
723
  $defaults = isset($node_types['blog']) ? array('blog' => 1) : array();
724
  $form['blogapi_node_types'] = array(
Dries's avatar
Dries committed
725
    '#type' => 'checkboxes',
726
    '#title' => t('Enable for external blogging clients'),
Dries's avatar
Dries committed
727 728 729
    '#required' => TRUE,
    '#default_value' => variable_get('blogapi_node_types', $defaults),
    '#options' => $node_types,
730
    '#description' => t('Select the content types available to external blogging clients via Blog API. If supported, each enabled content type will be displayed as a separate "blog" by the external client.')
731
  );
732

733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807
  $blogapi_extensions_default = variable_get('blogapi_extensions_default', 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp');
  $blogapi_uploadsize_default = variable_get('blogapi_uploadsize_default', 1);
  $blogapi_usersize_default = variable_get('blogapi_usersize_default', 1);

  $form['settings_general'] = array(
    '#type' => 'fieldset',
    '#title' => t('File settings'),
    '#collapsible' => TRUE,
  );

  $form['settings_general']['blogapi_extensions_default'] = array(
    '#type' => 'textfield',
    '#title' => t('Default permitted file extensions'),
    '#default_value' => $blogapi_extensions_default,
    '#maxlength' => 255,
    '#description' => t('Default extensions that users can upload. Separate extensions with a space and do not include the leading dot.'),
  );

  $form['settings_general']['blogapi_uploadsize_default'] = array(
    '#type' => 'textfield',
    '#title' => t('Default maximum file size per upload'),
    '#default_value' => $blogapi_uploadsize_default,
    '#size' => 5,
    '#maxlength' => 5,
    '#description' => t('The default maximum file size a user can upload.'),
    '#field_suffix' => t('MB')
  );

  $form['settings_general']['blogapi_usersize_default'] = array(
    '#type' => 'textfield',
    '#title' => t('Default total file size per user'),
    '#default_value' => $blogapi_usersize_default,
    '#size' => 5,
    '#maxlength' => 5,
    '#description' => t('The default maximum size of all files a user can have on the site.'),
    '#field_suffix' => t('MB')
  );

  $form['settings_general']['upload_max_size'] = array('#value' => '<p>'. t('Your PHP settings limit the maximum file size per upload to %size.', array('%size' => format_size(file_upload_max_size()))).'</p>');

  $roles = user_roles(FALSE, 'administer content with blog api');
  $form['roles'] = array('#type' => 'value', '#value' => $roles);

  foreach ($roles as $rid => $role) {
    $form['settings_role_' . $rid] = array(
      '#type' => 'fieldset',
      '#title' => t('Settings for @role', array('@role' => $role)),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $form['settings_role_' . $rid]['blogapi_extensions_' . $rid] = array(
      '#type' => 'textfield',
      '#title' => t('Permitted file extensions'),
      '#default_value' => variable_get('blogapi_extensions_' . $rid, $blogapi_extensions_default),
      '#maxlength' => 255,
      '#description' => t('Extensions that users in this role can upload. Separate extensions with a space and do not include the leading dot.'),
    );
    $form['settings_role_' . $rid]['blogapi_uploadsize_' . $rid] = array(
      '#type' => 'textfield',
      '#title' => t('Maximum file size per upload'),
      '#default_value' => variable_get('blogapi_uploadsize_' . $rid, $blogapi_uploadsize_default),
      '#size' => 5,
      '#maxlength' => 5,
      '#description' => t('The maximum size of a file a user can upload (in megabytes).'),
    );
    $form['settings_role_' . $rid]['blogapi_usersize_' . $rid] = array(
      '#type' => 'textfield',
      '#title' => t('Total file size per user'),
      '#default_value' => variable_get('blogapi_usersize_' . $rid, $blogapi_usersize_default),
      '#size' => 5,
      '#maxlength' => 5,
      '#description' => t('The maximum size of all files a user can have on the site (in megabytes).'),
    );
  }

808
  return system_settings_form($form, FALSE);
Dries's avatar
 
Dries committed
809 810
}

811 812 813
/**
 * Implementation of hook_menu().
 */
814 815
function blogapi_menu() {
  $items['blogapi/rsd'] = array(
816
    'title' => 'RSD',
817 818 819 820 821
    'page callback' => 'blogapi_rsd',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/blogapi'] = array(
822 823
    'title' => 'Blog API',
    'description' => 'Configure the content types available to external blogging clients.',
824 825 826 827 828 829 830 831
    'page callback' => 'drupal_get_form',
    'page arguments' => array('blogapi_admin_settings'),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_NORMAL_ITEM,
  );

  return $items;
}
Dries's avatar
 
Dries committed
832

833
/**
834
 * Implementation of hook_init().
835
 */
836
function blogapi_init() {
837
  if (drupal_is_front_page()) {
838 839 840
    drupal_add_link(array('rel' => 'EditURI',
                          'type' => 'application/rsd+xml',
                          'title' => t('RSD'),
841
                          'href' => url('blogapi/rsd', array('absolute' => TRUE))));
Dries's avatar
 
Dries committed
842 843 844 845 846 847
  }
}

function blogapi_rsd() {
  global $base_url;

848
  $xmlrpc = $base_url . '/xmlrpc.php';
849
  $base = url('', array('absolute' => TRUE));
Dries's avatar
 
Dries committed
850 851
  $blogid = 1; # until we figure out how to handle multiple bloggers

852
  drupal_set_header('Content-Type: application/rsd+xml; charset=utf-8');
Dries's avatar
 
Dries committed
853 854 855 856 857
  print <<<__RSD__
<?xml version="1.0"?>
<rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
  <service>
    <engineName>Drupal</engineName>
858
    <engineLink>http://drupal.org/</engineLink>
Dries's avatar
 
Dries committed
859 860
    <homePageLink>$base</homePageLink>
    <apis>
861
      <api name="MetaWeblog" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
862 863
      <api name="Blogger" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
      <api name="MovableType" preferred="true" apiLink="$xmlrpc" blogID="$blogid" />
Dries's avatar
 
Dries committed
864 865 866 867 868 869
    </apis>
  </service>
</rsd>
__RSD__;
}

870 871 872 873 874
/**
 * Handles extra information sent by clients according to MovableType's spec.
 */
function _blogapi_mt_extra(&$node, $struct) {
  if (is_array($node)) {
875
    $was_array = TRUE;
876
    $node = (object)$node;
877 878 879 880 881
  }

  if (array_key_exists('mt_allow_comments', $struct)) {
    switch ($struct['mt_allow_comments']) {
      case 0:
882
        $node->comment = COMMENT_NODE_DISABLED;
883 884
        break;
      case 1:
885
        $node->comment = COMMENT_NODE_READ_WRITE;
886 887
        break;
      case 2:
888
        $node->comment = COMMENT_NODE_READ_ONLY;
889 890 891 892
        break;
    }
  }

893
  // Merge the 3 body sections (description, mt_excerpt, mt_text_more) into one body.
894
  if ($struct['mt_excerpt']) {
895
    $node->body = $struct['mt_excerpt'] . '<!--break-->' . $node->body;
896 897
  }
  if ($struct['mt_text_more']) {
898
    $node->body = $node->body . '<!--extended-->' . $struct['mt_text_more'];
899 900 901 902 903 904 905
  }

  if ($struct['mt_convert_breaks']) {
    $node->format = $struct['mt_convert_breaks'];
  }

  if ($struct['dateCreated']) {
906
    $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');
907 908 909
  }

  if ($was_array) {
910
    $node = (array)$node;
911 912 913
  }
}

914
function _blogapi_get_post($node, $bodies = TRUE) {
915
  $xmlrpcval = array(
916 917 918 919
    'userid' => $node->name,
    'dateCreated' => xmlrpc_date($node->created),
    'title' => $node->title,
    'postid' => $node->nid,
920 921
    'link' => url('node/' . $node->nid, array('absolute' => TRUE)),
    'permaLink' => url('node/' . $node->nid, array('absolute' => TRUE)),
922
  );
923

924
  if ($bodies) {
925
    if ($node->comment == 1) {
926 927
      $comment = 2;
    }
928
    elseif ($node->comment == 2) {
929 930
      $comment = 1;
    }
931 932
    $xmlrpcval['content'] = "<title>$node->title</title>$node->body";
    $xmlrpcval['description'] = $node->body;
933
    // Add MT specific fields
934
    $xmlrpcval['mt_allow_comments'] = (int) $comment;
935
    $xmlrpcval['mt_convert_breaks'] = $node->format;
936 937
  }

938
  return $xmlrpcval;
939
}
940

941 942 943 944 945 946 947 948 949 950 951 952 953
/**
 * Validate blog ID, which maps to a content type in Drupal.
 *
 * Only content types configured to work with Blog API are supported.
 *
 * @return
 *   TRUE if the content type is supported and the user has permission
 *   to post, or a blogapi_error() XML construct otherwise.
 */
function _blogapi_validate_blogid($blogid) {
  $types = _blogapi_get_node_types();
  if (in_array($blogid, $types, TRUE)) {
    return TRUE;
954
  }
955

956
  return blogapi_error(t("Blog API module is not configured to support the %type content type, or you don't have sufficient permissions to post this type of content.", array('%type' => $blogid)));
957 958 959
}

function _blogapi_get_node_types() {
960
  $available_types = array_keys(array_filter(variable_get('blogapi_node_types', array('blog' => 1))));
961
  $types = array();