Code coverage for /20081101/modules/blogapi/blogapi.module

Line #Times calledCode
1
<?php
2
// $Id: blogapi.module,v 1.132 2008/10/12 02:58:23 webchick Exp $
3
4
/**
5
 * @file
6
 * Enable users to post using applications that support XML-RPC blog APIs.
7
 */
8
9
/**
10
 * Implementation of hook_help().
11
 */
1225
function blogapi_help($path, $arg) {
13
  switch ($path) {
1411
    case 'admin/help#blogapi':
150
      $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>';
160
      $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>';
170
      $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>';
180
      $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>';
190
      return $output;
200
  }
2111
}
22
23
/**
24
 * Implementation of hook_perm().
25
 */
2625
function blogapi_perm() {
27
  return array(
28
    'administer content with blog api' => array(
291
      'title' => t('Administer content with blog API'),
301
      'description' => t('Manage website content from external tools.'),
311
    ),
321
  );
330
}
34
35
/**
36
 * Implementation of hook_xmlrpc().
37
 */
3825
function blogapi_xmlrpc() {
39
  return array(
40
    array(
418
      'blogger.getUsersBlogs',
428
      'blogapi_blogger_get_users_blogs',
438
      array('array', 'string', 'string', 'string'),
448
      t('Returns a list of blogs to which an author has posting
privileges.')),
45
    array(
468
      'blogger.getUserInfo',
478
      'blogapi_blogger_get_user_info',
488
      array('struct', 'string', 'string', 'string'),
498
      t('Returns information about an author in the system.')),
50
    array(
518
      'blogger.newPost',
528
      'blogapi_blogger_new_post',
538
      array('string', 'string', 'string', 'string', 'string', 'string',
'boolean'),
548
      t('Creates a new post, and optionally publishes it.')),
55
    array(
568
      'blogger.editPost',
578
      'blogapi_blogger_edit_post',
588
      array('boolean', 'string', 'string', 'string', 'string', 'string',
'boolean'),
598
      t('Updates the information about an existing post.')),
60
    array(
618
      'blogger.getPost',
628
      'blogapi_blogger_get_post',
638
      array('struct', 'string', 'string', 'string', 'string'),
648
      t('Returns information about a specific post.')),
65
    array(
668
      'blogger.deletePost',
678
      'blogapi_blogger_delete_post',
688
      array('boolean', 'string', 'string', 'string', 'string', 'boolean'),
698
      t('Deletes a post.')),
70
    array(
718
      'blogger.getRecentPosts',
728
      'blogapi_blogger_get_recent_posts',
738
      array('array', 'string', 'string', 'string', 'string', 'int'),
748
      t('Returns a list of the most recent posts in the system.')),
75
    array(
768
      'metaWeblog.newPost',
778
      'blogapi_metaweblog_new_post',
788
      array('string', 'string', 'string', 'string', 'struct', 'boolean'),
798
      t('Creates a new post, and optionally publishes it.')),
80
    array(
818
      'metaWeblog.editPost',
828
      'blogapi_metaweblog_edit_post',
838
      array('boolean', 'string', 'string', 'string', 'struct', 'boolean'),
848
      t('Updates information about an existing post.')),
85
    array(
868
      'metaWeblog.getPost',
878
      'blogapi_metaweblog_get_post',
888
      array('struct', 'string', 'string', 'string'),
898
      t('Returns information about a specific post.')),
90
    array(
918
      'metaWeblog.newMediaObject',
928
      'blogapi_metaweblog_new_media_object',
938
      array('string', 'string', 'string', 'string', 'struct'),
948
      t('Uploads a file to your webserver.')),
95
    array(
968
      'metaWeblog.getCategories',
978
      'blogapi_metaweblog_get_category_list',
988
      array('struct', 'string', 'string', 'string'),
998
      t('Returns a list of all categories to which the post is
assigned.')),
100
    array(
1018
      'metaWeblog.getRecentPosts',
1028
      'blogapi_metaweblog_get_recent_posts',
1038
      array('array', 'string', 'string', 'string', 'int'),
1048
      t('Returns a list of the most recent posts in the system.')),
105
    array(
1068
      'mt.getRecentPostTitles',
1078
      'blogapi_mt_get_recent_post_titles',
1088
      array('array', 'string', 'string', 'string', 'int'),
1098
      t('Returns a bandwidth-friendly list of the most recent posts in the
system.')),
110
    array(
1118
      'mt.getCategoryList',
1128
      'blogapi_mt_get_category_list',
1138
      array('array', 'string', 'string', 'string'),
1148
      t('Returns a list of all categories defined in the blog.')),
115
    array(
1168
      'mt.getPostCategories',
1178
      'blogapi_mt_get_post_categories',
1188
      array('array', 'string', 'string', 'string'),
1198
      t('Returns a list of all categories to which the post is
assigned.')),
120
    array(
1218
      'mt.setPostCategories',
1228
      'blogapi_mt_set_post_categories',
1238
      array('boolean', 'string', 'string', 'string', 'array'),
1248
      t('Sets the categories for a post.')),
125
    array(
1268
      'mt.supportedMethods',
1278
      'xmlrpc_server_list_methods',
1288
      array('array'),
1298
      t('Retrieve information about the XML-RPC methods supported by the
server.')),
130
    array(
1318
      'mt.supportedTextFilters',
1328
      'blogapi_mt_supported_text_filters',
1338
      array('array'),
1348
      t('Retrieve information about the text formatting plugins supported
by the server.')),
135
    array(
1368
      'mt.publishPost',
1378
      'blogapi_mt_publish_post',
1388
      array('boolean', 'string', 'string', 'string'),
1398
      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).')));
1400
}
141
142
/**
143
 * Blogging API callback. Finds the URL of a user's blog.
144
 */
14525
function blogapi_blogger_get_users_blogs($appid, $username, $password) {
1461
  $user = blogapi_validate_user($username, $password);
1471
  if ($user->uid) {
1481
    $types = _blogapi_get_node_types();
1491
    $structs = array();
1501
    foreach ($types as $type) {
1511
      $structs[] = array('url' => url('blog/' . $user->uid,
array('absolute' => TRUE)), 'blogid' => $type, 'blogName' => $user->name .
": " . $type);
1521
    }
153
1541
    return $structs;
1550
  }
156
  else {
1570
    return blogapi_error($user);
158
  }
1590
}
160
161
/**
162
 * Blogging API callback. Returns profile information about a user.
163
 */
16425
function blogapi_blogger_get_user_info($appkey, $username, $password) {
1650
  $user = blogapi_validate_user($username, $password);
166
1670
  if ($user->uid) {
1680
    $name = explode(' ', $user->realname ? $user->realname : $user->name,
2);
169
    return array(
1700
      'userid' => $user->uid,
1710
      'lastname' => $name[1],
1720
      'firstname' => $name[0],
1730
      'nickname' => $user->name,
1740
      'email' => $user->mail,
1750
      'url' => url('blog/' . $user->uid, array('absolute' => TRUE)));
1760
  }
177
  else {
1780
    return blogapi_error($user);
179
  }
1800
}
181
182
/**
183
 * Blogging API callback. Inserts a new blog post as a node.
184
 */
18525
function blogapi_blogger_new_post($appkey, $blogid, $username, $password,
$content, $publish) {
1861
  $user = blogapi_validate_user($username, $password);
1871
  if (!$user->uid) {
1880
    return blogapi_error($user);
1890
  }
190
1911
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
192
    // Return an error if not configured type.
1930
    return $error;
1940
  }
195
1961
  $edit = array();
1971
  $edit['type'] = $blogid;
198
  // Get the node type defaults.
1991
  $node_type_default = variable_get('node_options_' . $edit['type'],
array('status', 'promote'));
2001
  $edit['uid'] = $user->uid;
2011
  $edit['name'] = $user->name;
2021
  $edit['promote'] = in_array('promote', $node_type_default);
2031
  $edit['comment'] = variable_get('comment_' . $edit['type'], 2);
2041
  $edit['revision'] = in_array('revision', $node_type_default);
2051
  $edit['format'] = FILTER_FORMAT_DEFAULT;
2061
  $edit['status'] = $publish;
207
208
  // Check for bloggerAPI vs. metaWeblogAPI.
2091
  if (is_array($content)) {
2100
    $edit['title'] = $content['title'];
2110
    $edit['body'] = $content['description'];
2120
    _blogapi_mt_extra($edit, $content);
2130
  }
214
  else {
2151
    $edit['title'] = blogapi_blogger_title($content);
2161
    $edit['body'] = $content;
217
  }
218
2191
  if (!node_access('create', $edit['type'])) {
2200
    return blogapi_error(t('You do not have permission to create this type
of post.'));
2210
  }
222
2231
  if (user_access('administer nodes') && !isset($edit['date'])) {
2240
    $edit['date'] = format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s O');
2250
  }
226
2271
  node_invoke_nodeapi($edit, 'blogapi_new');
228
2291
  $valid = blogapi_status_error_check($edit, $publish);
2301
  if ($valid !== TRUE) {
2310
    return $valid;
2320
  }
233
2341
  node_validate($edit);
2351
  if ($errors = form_get_errors()) {
2360
    return blogapi_error(implode("\n", $errors));
2370
  }
238
2391
  $node = node_submit($edit);
2401
  node_save($node);
2411
  if ($node->nid) {
2421
    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.
2441
    return "$node->nid";
2450
  }
246
2470
  return blogapi_error(t('Error storing post.'));
2480
}
249
250
/**
251
 * Blogging API callback. Modifies the specified blog node.
252
 */
25325
function blogapi_blogger_edit_post($appkey, $postid, $username, $password,
$content, $publish) {
2541
  $user = blogapi_validate_user($username, $password);
255
2561
  if (!$user->uid) {
2570
    return blogapi_error($user);
2580
  }
259
2601
  $node = node_load($postid);
2611
  if (!$node) {
2620
    return blogapi_error(t('n/a'));
2630
  }
264
  // Let the teaser be re-generated.
2651
  unset($node->teaser);
266
2671
  if (!node_access('update', $node)) {
2680
    return blogapi_error(t('You do not have permission to update this
post.'));
2690
  }
270
  // Save the original status for validation of permissions.
2711
  $original_status = $node->status;
2721
  $node->status = $publish;
273
274
  // check for bloggerAPI vs. metaWeblogAPI
2751
  if (is_array($content)) {
2760
    $node->title = $content['title'];
2770
    $node->body = $content['description'];
2780
    _blogapi_mt_extra($node, $content);
2790
  }
280
  else {
2811
    $node->title = blogapi_blogger_title($content);
2821
    $node->body = $content;
283
  }
284
2851
  node_invoke_nodeapi($node, 'blogapi_edit');
286
2871
  $valid = blogapi_status_error_check($node, $original_status);
2881
  if ($valid !== TRUE) {
2890
    return $valid;
2900
  }
291
2921
  node_validate($node);
2931
  if ($errors = form_get_errors()) {
2940
    return blogapi_error(implode("\n", $errors));
2950
  }
296
2971
  if (user_access('administer nodes') && !isset($edit['date'])) {
2980
    $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
2990
  }
3001
  $node = node_submit($node);
3011
  node_save($node);
3021
  if ($node->nid) {
3031
    watchdog('content', '@type: updated %title using Blog API.',
array('@type' => $node->type, '%title' => $node->title), WATCHDOG_NOTICE,
l(t('view'), "node/$node->nid"));
3041
    return TRUE;
3050
  }
306
3070
  return blogapi_error(t('Error storing post.'));
3080
}
309
310
/**
311
 * Blogging API callback. Returns a specified blog node.
312
 */
31325
function blogapi_blogger_get_post($appkey, $postid, $username, $password) {
3140
  $user = blogapi_validate_user($username, $password);
3150
  if (!$user->uid) {
3160
    return blogapi_error($user);
3170
  }
318
3190
  $node = node_load($postid);
320
3210
  return _blogapi_get_post($node, TRUE);
3220
}
323
324
/**
325
 * Check that the user has permission to save the node with the chosen
status.
326
 *
327
 * @return
328
 *   TRUE if no error, or the blogapi_error().
329
 */
33025
function blogapi_status_error_check($node, $original_status) {
331
  
3322
  $node = (object) $node;
333
3342
  $node_type_default = variable_get('node_options_'. $node->type,
array('status', 'promote'));
335
336
  // If we don't have the 'administer nodes' permission and the status is
337
  // changing or for a new node the status is not the content type's
default,
338
  // then return an error.
3392
  if (!user_access('administer nodes') && (($node->status !=
$original_status) || (empty($node->nid) && $node->status !=
in_array('status', $node_type_default)))) {
3400
    if ($node->status) {
3410
      return blogapi_error(t('You do not have permission to publish this
type of post. Please save it as a draft instead.'));
3420
    }
343
    else {
3440
      return blogapi_error(t('You do not have permission to save this post
as a draft. Please publish it instead.'));
345
    }
3460
  }
3472
  return TRUE;
3480
}
349
350
351
/**
352
 * Blogging API callback. Removes the specified blog node.
353
 */
35425
function blogapi_blogger_delete_post($appkey, $postid, $username,
$password, $publish) {
3551
  $user = blogapi_validate_user($username, $password);
3561
  if (!$user->uid) {
3570
    return blogapi_error($user);
3580
  }
359
3601
  node_delete($postid);
3611
  return TRUE;
3620
}
363
364
/**
365
 * Blogging API callback. Returns the latest few postings in a user's blog.
$bodies TRUE
366
 * <a
href="http://movabletype.org/docs/mtmanual_programmatic.html#item_mt%2EgetRecentPostTitles">
367
 * returns a bandwidth-friendly list</a>.
368
 */
36925
function blogapi_blogger_get_recent_posts($appkey, $blogid, $username,
$password, $number_of_posts, $bodies = TRUE) {
370
  // Remove unused appkey (from bloggerAPI).
3711
  $user = blogapi_validate_user($username, $password);
3721
  if (!$user->uid) {
3730
    return blogapi_error($user);
3740
  }
375
3761
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
377
    // Return an error if not configured type.
3780
    return $error;
3790
  }
380
3811
  if ($bodies) {
3821
    $result = db_query_range("SELECT n.nid, n.title, r.body, r.format,
n.comment, 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 = :type AND n.uid = :uid
ORDER BY n.created DESC",  array(
3831
      ':type' => $blogid,
3841
      ':uid' => $user->uid
3851
    ), 0, $number_of_posts);
3861
  }
387
  else {
3880
    $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(
3890
      ':type' => $blogid,
3900
      ':uid' => $user->uid
3910
    ), 0, $number_of_posts);
392
  }
3931
  $blogs = array();
3941
  foreach ($result as $blog) {
3951
    $blogs[] = _blogapi_get_post($blog, $bodies);
3961
  }
397
3981
  return $blogs;
3990
}
400
40125
function blogapi_metaweblog_new_post($blogid, $username, $password,
$content, $publish) {
4020
  return blogapi_blogger_new_post('0123456789ABCDEF', $blogid, $username,
$password, $content, $publish);
4030
}
404
40525
function blogapi_metaweblog_edit_post($postid, $username, $password,
$content, $publish) {
4060
  return blogapi_blogger_edit_post('0123456789ABCDEF', $postid, $username,
$password, $content, $publish);
4070
}
408
40925
function blogapi_metaweblog_get_post($postid, $username, $password) {
4100
  return blogapi_blogger_get_post('01234567890ABCDEF', $postid, $username,
$password);
4110
}
412
413
/**
414
 * Blogging API callback. Inserts a file into Drupal.
415
 */
41625
function blogapi_metaweblog_new_media_object($blogid, $username, $password,
$file) {
4171
  $user = blogapi_validate_user($username, $password);
4181
  if (!$user->uid) {
4190
    return blogapi_error($user);
4200
  }
421
4221
  $usersize = 0;
4231
  $uploadsize = 0;
424
4251
  $roles = array_intersect(user_roles(FALSE, 'administer content with blog
api'), $user->roles);
426
4271
  foreach ($roles as $rid => $name) {
4281
    $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')));
4291
    $usersize = max($usersize, variable_get("blogapi_usersize_$rid",
variable_get('blogapi_usersize_default', 1)) * 1024 * 1024);
4301
    $uploadsize = max($uploadsize, variable_get("blogapi_uploadsize_$rid",
variable_get('blogapi_uploadsize_default', 1)) * 1024 * 1024);
4311
  }
432
4331
  $filesize = strlen($file['bits']);
434
4351
  if ($filesize > $uploadsize) {
4360
    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))));
4370
  }
438
4391
  if (_blogapi_space_used($user->uid) + $filesize > $usersize) {
4400
    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))));
4410
  }
442
443
  // Only allow files with whitelisted extensions and convert remaining
dots to
444
  // underscores to prevent attacks via non-terminal executable extensions
with
445
  // files such as exploit.php.jpg.
446
4471
  $whitelist = array_unique(explode(' ', trim($extensions)));
448
4491
  $name = basename($file['name']);
450
4511
  if ($extension_position = strrpos($name, '.')) {
4521
    $filename = drupal_substr($name, 0, $extension_position);
4531
    $final_extension = drupal_substr($name, $extension_position + 1);
454
4551
    if (!in_array(strtolower($final_extension), $whitelist)) {
4560
      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))));
4570
    }
458
4591
    $filename = str_replace('.', '_', $filename);
4601
    $filename .= '.' . $final_extension;
4611
  }
462
4631
  $data = $file['bits'];
464
4651
  if (!$data) {
4660
    return blogapi_error(t('No file sent.'));
4670
  }
468
4691
  if (!$file = file_unmanaged_save_data($data, $filename)) {
4700
    return blogapi_error(t('Error storing file.'));
4710
  }
472
4731
  $row = new stdClass();
4741
  $row->uid = $user->uid;
4751
  $row->filepath = $file;
4761
  $row->filesize = $filesize;
477
4781
  drupal_write_record('blogapi_files', $row);
479
480
  // Return the successful result.
4811
  return array('url' => file_create_url($file), 'struct');
4820
}
483
/**
484
 * Blogging API callback. Returns a list of the taxonomy terms that can be
485
 * associated with a blog node.
486
 */
48725
function blogapi_metaweblog_get_category_list($blogid, $username,
$password) {
4880
  $user = blogapi_validate_user($username, $password);
4890
  if (!$user->uid) {
4900
    return blogapi_error($user);
4910
  }
492
4930
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
494
    // Return an error if not configured type.
4950
    return $error;
4960
  }
497
4980
  $vocabularies = module_invoke('taxonomy', 'get_vocabularies', $blogid,
'vid');
4990
  $categories = array();
5000
  if ($vocabularies) {
5010
    foreach ($vocabularies as $vocabulary) {
5020
      $terms = module_invoke('taxonomy', 'get_tree', $vocabulary->vid, 0,
-1);
5030
      foreach ($terms as $term) {
5040
        $term_name = $term->name;
5050
        foreach (module_invoke('taxonomy', 'get_parents', $term->tid,
'tid') as $parent) {
5060
          $term_name = $parent->name . '/' . $term_name;
5070
        }
5080
        $categories[] = array('categoryName' => $term_name, 'categoryId' =>
$term->tid);
5090
      }
5100
    }
5110
  }
512
5130
  return $categories;
5140
}
515
51625
function blogapi_metaweblog_get_recent_posts($blogid, $username, $password,
$number_of_posts) {
5170
  return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid,
$username, $password, $number_of_posts, TRUE);
5180
}
519
52025
function blogapi_mt_get_recent_post_titles($blogid, $username, $password,
$number_of_posts) {
5210
  return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid,
$username, $password, $number_of_posts, FALSE);
5220
}
523
52425
function blogapi_mt_get_category_list($blogid, $username, $password) {
5250
  return blogapi_metaweblog_get_category_list($blogid, $username,
$password);
5260
}
527
528
/**
529
 * Blogging API callback. Returns a list of the taxonomy terms that are
530
 * assigned to a particular node.
531
 */
53225
function blogapi_mt_get_post_categories($postid, $username, $password) {
5331
  $user = blogapi_validate_user($username, $password);
5341
  if (!$user->uid) {
5350
    return blogapi_error($user);
5360
  }
537
5381
  $node = node_load($postid);
5391
  $terms = module_invoke('taxonomy', 'node_get_terms', $node, 'tid');
5401
  $categories = array();
5411
  foreach ($terms as $term) {
5421
    $term_name = $term->name;
5431
    foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as
$parent) {
5440
      $term_name = $parent->name . '/' . $term_name;
5450
    }
5461
    $categories[] = array('categoryName' => $term_name, 'categoryId' =>
$term->tid, 'isPrimary' => TRUE);
5471
  }
548
5491
  return $categories;
5500
}
551
552
/**
553
 * Blogging API callback. Assigns taxonomy terms to a particular node.
554
 */
55525
function blogapi_mt_set_post_categories($postid, $username, $password,
$categories) {
5561
  $user = blogapi_validate_user($username, $password);
5571
  if (!$user->uid) {
5580
    return blogapi_error($user);
5590
  }
560
5611
  $node = node_load($postid);
5621
  $node->taxonomy = array();
5631
  foreach ($categories as $category) {
5641
    $node->taxonomy[] = $category['categoryId'];
5651
  }
5661
  $validated = blogapi_mt_validate_terms($node);
5671
  if ($validated !== TRUE) {
5680
    return $validated;
5690
  }
5701
  node_save($node);
571
5721
  return TRUE;
5730
}
574
575
/**
576
 * Blogging API helper - find allowed taxonomy terms for a node type.
577
 */
57825
function blogapi_mt_validate_terms($node) {
579
  // We do a lot of heavy lifting here since taxonomy module doesn't have a
580
  // stand-alone validation function.
5811
  if (module_exists('taxonomy')) {
5821
    $found_terms = array();
5831
    if (!empty($node->taxonomy)) {
5841
      $term_list = array_unique($node->taxonomy);
5851
      $params = $term_list;
5861
      $params[] = $node->type;
5871
      $result = db_query(db_rewrite_sql("SELECT t.tid, t.vid FROM
{term_data} t INNER JOIN {vocabulary_node_types} n ON t.vid = n.vid WHERE
t.tid IN (". db_placeholders($term_list) .") AND n.type = '%s'", 't',
'tid'), $params);
5881
      $found_terms = array();
5891
      $found_count = 0;
5901
      while ($term = db_fetch_object($result)) {
5911
        $found_terms[$term->vid][$term->tid] = $term->tid;
5921
        $found_count++;
5931
      }
594
      // If the counts don't match, some terms are invalid or not
accessible to this user.
5951
      if (count($term_list) != $found_count) {
5960
        return blogapi_error(t('Invalid categories submitted.'));
5970
      }
5981
    }
599
    // Look up all the vocabularies for this node type.
6001
    $result2 = db_query(db_rewrite_sql("SELECT v.vid, v.name, v.required,
v.multiple FROM {vocabulary} v INNER JOIN {vocabulary_node_types} n ON
v.vid = n.vid WHERE n.type = '%s'", 'v', 'vid'), $node->type);
601
    // Check each vocabulary associated with this node type.
6021
    while ($vocabulary = db_fetch_object($result2)) {
603
      // Required vocabularies must have at least one term.
6041
      if ($vocabulary->required && empty($found_terms[$vocabulary->vid])) {
6050
        return blogapi_error(t('A category from the @vocabulary_name
vocabulary is required.', array('@vocabulary_name' => $vocabulary->name)));
6060
      }
607
      // Vocabularies that don't allow multiple terms may have at most one.
6081
      if (!($vocabulary->multiple) &&
(isset($found_terms[$vocabulary->vid]) &&
count($found_terms[$vocabulary->vid]) > 1)) {
6090
        return blogapi_error(t('You may only choose one category from the
@vocabulary_name vocabulary.'), array('@vocabulary_name' =>
$vocabulary->name));
6100
      }
6111
    }
6121
  }
6130
  elseif (!empty($node->taxonomy)) {
6140
    return blogapi_error(t('Error saving categories. This feature is not
available.'));
6150
  }
6161
  return TRUE;
6170
}
618
619
/**
620
 * Blogging API callback. Sends a list of available input formats.
621
 */
62225
function blogapi_mt_supported_text_filters() {
623
  // NOTE: we're only using anonymous' formats because the MT spec
624
  // does not allow for per-user formats.
6250
  $formats = filter_formats();
626
6270
  $filters = array();
6280
  foreach ($formats as $format) {
6290
    $filter['key'] = $format->format;
6300
    $filter['label'] = $format->name;
6310
    $filters[] = $filter;
6320
  }
633
6340
  return $filters;
6350
}
636
637
/**
638
 * Blogging API callback. Publishes the given node.
639
 */
64025
function blogapi_mt_publish_post($postid, $username, $password) {
6410
  $user = blogapi_validate_user($username, $password);
642
6430
  if (!$user->uid) {
6440
    return blogapi_error($user);
6450
  }
6460
  $node = node_load($postid);
647
6480
  if (!$node) {
6490
    return blogapi_error(t('Invalid post.'));
6500
  }
651
652
  // Nothing needs to be done if already published.
6530
  if ($node->status) {
6540
    return;
6550
  }
656
6570
  if (!node_access('update', $node) || !user_access('administer nodes')) {
6580
    return blogapi_error(t('You do not have permission to update this
post.'));
6590
  }
660
6610
  $node->status = 1;
6620
  node_save($node);
663
6640
  return TRUE;
6650
}
666
667
/**
668
 * Prepare an error message for returning to the XMLRPC caller.
669
 */
67025
function blogapi_error($message) {
6710
  static $xmlrpcusererr;
672
6730
  if (!is_array($message)) {
6740
    $message = array($message);
6750
  }
676
6770
  $message = implode(' ', $message);
678
6790
  return xmlrpc_error($xmlrpcusererr + 1, strip_tags($message));
6800
}
681
682
/**
683
 * Ensure that the given user has permission to edit a blog.
684
 */
68525
function blogapi_validate_user($username, $password) {
6868
  global $user;
687
6888
  $user = user_authenticate(array('name' => $username, 'pass' =>
$password));
689
6908
  if ($user->uid) {
6918
    if (user_access('administer content with blog api', $user)) {
6928
      return $user;
6930
    }
694
    else {
6950
      return t('You do not have permission to edit this blog.');
696
    }
6970
  }
698
  else {
6990
    return t('Wrong username or password.');
700
  }
7010
}
702
703
/**
704
 * For the blogger API, extract the node title from the contents field.
705
 */
70625
function blogapi_blogger_title(&$contents) {
7072
  if (preg_match('/<title>(.*?)<\/title>/i', $contents, $title)) {
7080
    $title = strip_tags($title[0]);
7090
    $contents = preg_replace('/<title>.*?<\/title>/i', '', $contents);
7100
  }
711
  else {
7122
    list($title, $contents) = explode("\n", $contents, 2);
713
  }
714
7152
  return $title;
7160
}
717
718
/**
719
 * Add some settings to the admin_settings form.
720
 */
72125
function blogapi_admin_settings() {
7220
  $node_types = array_map('check_plain', node_get_types('names'));
7230
  $defaults = isset($node_types['blog']) ? array('blog' => 1) : array();
7240
  $form['blogapi_node_types'] = array(
7250
    '#type' => 'checkboxes',
7260
    '#title' => t('Enable for external blogging clients'),
7270
    '#required' => TRUE,
7280
    '#default_value' => variable_get('blogapi_node_types', $defaults),
7290
    '#options' => $node_types,
7300
    '#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.')
7310
  );
732
7330
  $blogapi_extensions_default = variable_get('blogapi_extensions_default',
'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp');
7340
  $blogapi_uploadsize_default = variable_get('blogapi_uploadsize_default',
1);
7350
  $blogapi_usersize_default = variable_get('blogapi_usersize_default', 1);
736
7370
  $form['settings_general'] = array(
7380
    '#type' => 'fieldset',
7390
    '#title' => t('File settings'),
7400
    '#collapsible' => TRUE,
741
  );
742
7430
  $form['settings_general']['blogapi_extensions_default'] = array(
7440
    '#type' => 'textfield',
7450
    '#title' => t('Default permitted file extensions'),
7460
    '#default_value' => $blogapi_extensions_default,
7470
    '#maxlength' => 255,
7480
    '#description' => t('Default extensions that users can upload. Separate
extensions with a space and do not include the leading dot.'),
749
  );
750
7510
  $form['settings_general']['blogapi_uploadsize_default'] = array(
7520
    '#type' => 'textfield',
7530
    '#title' => t('Default maximum file size per upload'),
7540
    '#default_value' => $blogapi_uploadsize_default,
7550
    '#size' => 5,
7560
    '#maxlength' => 5,
7570
    '#description' => t('The default maximum file size a user can
upload.'),
7580
    '#field_suffix' => t('MB')
7590
  );
760
7610
  $form['settings_general']['blogapi_usersize_default'] = array(
7620
    '#type' => 'textfield',
7630
    '#title' => t('Default total file size per user'),
7640
    '#default_value' => $blogapi_usersize_default,
7650
    '#size' => 5,
7660
    '#maxlength' => 5,
7670
    '#description' => t('The default maximum size of all files a user can
have on the site.'),
7680
    '#field_suffix' => t('MB')
7690
  );
770
7710
  $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>');
772
7730
  $roles = user_roles(FALSE, 'administer content with blog api');
7740
  $form['roles'] = array('#type' => 'value', '#value' => $roles);
775
7760
  foreach ($roles as $rid => $role) {
7770
    $form['settings_role_' . $rid] = array(
7780
      '#type' => 'fieldset',
7790
      '#title' => t('Settings for @role', array('@role' => $role)),
7800
      '#collapsible' => TRUE,
7810
      '#collapsed' => TRUE,
782
    );
7830
    $form['settings_role_' . $rid]['blogapi_extensions_' . $rid] = array(
7840
      '#type' => 'textfield',
7850
      '#title' => t('Permitted file extensions'),
7860
      '#default_value' => variable_get('blogapi_extensions_' . $rid,
$blogapi_extensions_default),
7870
      '#maxlength' => 255,
7880
      '#description' => t('Extensions that users in this role can upload.
Separate extensions with a space and do not include the leading dot.'),
789
    );
7900
    $form['settings_role_' . $rid]['blogapi_uploadsize_' . $rid] = array(
7910
      '#type' => 'textfield',
7920
      '#title' => t('Maximum file size per upload'),
7930
      '#default_value' => variable_get('blogapi_uploadsize_' . $rid,
$blogapi_uploadsize_default),
7940
      '#size' => 5,
7950
      '#maxlength' => 5,
7960
      '#description' => t('The maximum size of a file a user can upload (in
megabytes).'),
797
    );
7980
    $form['settings_role_' . $rid]['blogapi_usersize_' . $rid] = array(
7990
      '#type' => 'textfield',
8000
      '#title' => t('Total file size per user'),
8010
      '#default_value' => variable_get('blogapi_usersize_' . $rid,
$blogapi_usersize_default),
8020
      '#size' => 5,
8030
      '#maxlength' => 5,
8040
      '#description' => t('The maximum size of all files a user can have on
the site (in megabytes).'),
805
    );
8060
  }
807
8080
  return system_settings_form($form);
8090
}
810
811
/**
812
 * Implementation of hook_menu().
813
 */
81425
function blogapi_menu() {
8151
  $items['blogapi/rsd'] = array(
8161
    'title' => 'RSD',
8171
    'page callback' => 'blogapi_rsd',
8181
    'access arguments' => array('access content'),
8191
    'type' => MENU_CALLBACK,
820
  );
8211
  $items['admin/settings/blogapi'] = array(
8221
    'title' => 'Blog API',
8231
    'description' => 'Configure the content types available to external
blogging clients.',
8241
    'page callback' => 'drupal_get_form',
8251
    'page arguments' => array('blogapi_admin_settings'),
8261
    'access arguments' => array('administer site configuration'),
8271
    'type' => MENU_NORMAL_ITEM,
828
  );
829
8301
  return $items;
8310
}
832
833
/**
834
 * Implementaton of hook_init().
835
 */
83625
function blogapi_init() {
83724
  if (drupal_is_front_page()) {
83810
    drupal_add_link(array('rel' => 'EditURI',
83910
                          'type' => 'application/rsd+xml',
84010
                          'title' => t('RSD'),
84110
                          'href' => url('blogapi/rsd', array('absolute' =>
TRUE))));
84210
  }
84324
}
844
84525
function blogapi_rsd() {
8460
  global $base_url;
847
8480
  $xmlrpc = $base_url . '/xmlrpc.php';
8490
  $base = url('', array('absolute' => TRUE));
8500
  $blogid = 1; # until we figure out how to handle multiple bloggers
851
8520
  drupal_set_header('Content-Type: application/rsd+xml; charset=utf-8');
853
  print <<<__RSD__
8540
<?xml version="1.0"?>
855
<rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
856
  <service>
857
    <engineName>Drupal</engineName>
858
    <engineLink>http://drupal.org/</engineLink>
8590
    <homePageLink>$base</homePageLink>
860
    <apis>
8610
      <api name="MetaWeblog" preferred="false" apiLink="$xmlrpc"
blogID="$blogid" />
8620
      <api name="Blogger" preferred="false" apiLink="$xmlrpc"
blogID="$blogid" />
8630
      <api name="MovableType" preferred="true" apiLink="$xmlrpc"
blogID="$blogid" />
864
    </apis>
865
  </service>
8660
</rsd>
8670
__RSD__;
8680
}
869
870
/**
871
 * Handles extra information sent by clients according to MovableType's
spec.
872
 */
87325
function _blogapi_mt_extra(&$node, $struct) {
8740
  if (is_array($node)) {
8750
    $was_array = TRUE;
8760
    $node = (object)$node;
8770
  }
878
8790
  if (array_key_exists('mt_allow_comments', $struct)) {
8800
    switch ($struct['mt_allow_comments']) {
8810
      case 0:
8820
        $node->comment = COMMENT_NODE_DISABLED;
8830
        break;
8840
      case 1:
8850
        $node->comment = COMMENT_NODE_READ_WRITE;
8860
        break;
8870
      case 2:
8880
        $node->comment = COMMENT_NODE_READ_ONLY;
8890
        break;
8900
    }
8910
  }
892
893
  // Merge the 3 body sections (description, mt_excerpt, mt_text_more) into
one body.
8940
  if ($struct['mt_excerpt']) {
8950
    $node->body = $struct['mt_excerpt'] . '<!--break-->' . $node->body;
8960
  }
8970
  if ($struct['mt_text_more']) {
8980
    $node->body = $node->body . '<!--extended-->' .
$struct['mt_text_more'];
8990
  }
900
9010
  if ($struct['mt_convert_breaks']) {
9020
    $node->format = $struct['mt_convert_breaks'];
9030
  }
904
9050
  if ($struct['dateCreated']) {
9060
    $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');
9070
  }
908
9090
  if ($was_array) {
9100
    $node = (array)$node;
9110
  }
9120
}
913
91425
function _blogapi_get_post($node, $bodies = TRUE) {
915
  $xmlrpcval = array(
9161
    'userid' => $node->name,
9171
    'dateCreated' => xmlrpc_date($node->created),
9181
    'title' => $node->title,
9191
    'postid' => $node->nid,
9201
    'link' => url('node/' . $node->nid, array('absolute' => TRUE)),
9211
    'permaLink' => url('node/' . $node->nid, array('absolute' => TRUE)),
9221
  );
923
9241
  if ($bodies) {
9251
    if ($node->comment == 1) {
9260
      $comment = 2;
9270
    }
9281
    elseif ($node->comment == 2) {
9291
      $comment = 1;
9301
    }
9311
    $xmlrpcval['content'] = "<title>$node->title</title>$node->body";
9321
    $xmlrpcval['description'] = $node->body;
933
    // Add MT specific fields
9341
    $xmlrpcval['mt_allow_comments'] = (int) $comment;
9351
    $xmlrpcval['mt_convert_breaks'] = $node->format;
9361
  }
937
9381
  return $xmlrpcval;
9390
}
940
941
/**
942
 * Validate blog ID, which maps to a content type in Drupal.
943
 *
944
 * Only content types configured to work with Blog API are supported.
945
 *
946
 * @return
947
 *   TRUE if the content type is supported and the user has permission
948
 *   to post, or a blogapi_error() XML construct otherwise.
949
 */
95025
function _blogapi_validate_blogid($blogid) {
9512
  $types = _blogapi_get_node_types();
9522
  if (in_array($blogid, $types, TRUE)) {
9532
    return TRUE;
9540
  }
955
9560
  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)));
9570
}
958
95925
function _blogapi_get_node_types() {
9603
  $available_types =
array_keys(array_filter(variable_get('blogapi_node_types', array('blog' =>
1))));
9613
  $types = array();
9623
  foreach (node_get_types() as $type => $name) {
9633
    if (node_access('create', $type) && in_array($type, $available_types))
{
9643
      $types[] = $type;
9653
    }
9663
  }
967
9683
  return $types;
9690
}
970
97125
function _blogapi_space_used($uid) {
9721
  return db_query('SELECT SUM(filesize) FROM {blogapi_files} f WHERE f.uid
= :uid', array(':uid' => $uid))->fetchField();
9730
}
974
97525