blogapi.module 24.3 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
28
29
  return array(
    'administer content with blog api' => t('Manage website content from external tools.'),
  );
30
31
}

32
33
34
/**
 * Implementation of hook_xmlrpc().
 */
Dries's avatar
 
Dries committed
35
function blogapi_xmlrpc() {
36
37
38
39
40
  return array(
    array(
      'blogger.getUsersBlogs',
      'blogapi_blogger_get_users_blogs',
      array('array', 'string', 'string', 'string'),
41
      t('Returns a list of blogs to which an author has posting privileges.')),
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    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.')),
57
58
59
60
61
    array(
      'blogger.getPost',
      'blogapi_blogger_get_post',
      array('struct', 'string', 'string', 'string', 'string'),
      t('Returns information about a specific post.')),
62
63
64
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
    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'),
111
      t('Returns a list of all categories defined in the blog.')),
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
    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',
134
      'blogapi_mt_publish_post',
135
      array('boolean', 'string', 'string', 'string'),
136
      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
137
138
}

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

151
    return $structs;
Dries's avatar
 
Dries committed
152
153
  }
  else {
154
    return blogapi_error($user);
Dries's avatar
 
Dries committed
155
156
157
  }
}

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

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

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

188
189
190
191
192
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
    // Return an error if not configured type.
    return $error;
  }

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

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

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

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

224
  node_invoke_nodeapi($edit, 'blogapi new');
225
226
227
228
229
230

  node_validate($edit);
  if ($errors = form_get_errors()) {
    return blogapi_error(implode("\n", $errors));
  }

231
  $node = node_submit($edit);
232
  node_save($node);
233
  if ($node->nid) {
234
    watchdog('content', '@type: added %title using blog API.', array('@type' => $node->type, '%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
235
    // blogger.newPost returns a string so we cast the nid to a string by putting it in double quotes.
236
    return "$node->nid";
Dries's avatar
 
Dries committed
237
238
  }

239
  return blogapi_error(t('Error storing post.'));
Dries's avatar
 
Dries committed
240
241
}

242
243
244
/**
 * Blogging API callback. Modifies the specified blog node.
 */
245
246
function blogapi_blogger_edit_post($appkey, $postid, $username, $password, $content, $publish) {
  $user = blogapi_validate_user($username, $password);
247

Dries's avatar
 
Dries committed
248
  if (!$user->uid) {
249
    return blogapi_error($user);
Dries's avatar
 
Dries committed
250
251
  }

252
  $node = node_load($postid);
Dries's avatar
 
Dries committed
253
  if (!$node) {
254
    return blogapi_error(t('n/a'));
Dries's avatar
 
Dries committed
255
  }
256
257
  // Let the teaser be re-generated.
  unset($node->teaser);
Dries's avatar
 
Dries committed
258

259
  if (!node_access('update', $node)) {
260
    return blogapi_error(t('You do not have permission to update this post.'));
Dries's avatar
 
Dries committed
261
262
  }

263
  $node->status = $publish;
264

Dries's avatar
 
Dries committed
265
  // check for bloggerAPI vs. metaWeblogAPI
266
267
268
269
  if (is_array($content)) {
    $node->title = $content['title'];
    $node->body = $content['description'];
    _blogapi_mt_extra($node, $content);
Dries's avatar
 
Dries committed
270
271
  }
  else {
272
273
    $node->title = blogapi_blogger_title($content);
    $node->body = $content;
Dries's avatar
 
Dries committed
274
  }
Dries's avatar
   
Dries committed
275

276
277
  node_invoke_nodeapi($node, 'blogapi edit');

278
  node_validate($node);
279
280
  if ($errors = form_get_errors()) {
    return blogapi_error(implode("\n", $errors));
Dries's avatar
 
Dries committed
281
282
  }

283
284
285
  if (user_access('administer nodes') && !isset($edit['date'])) {
    $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
  }
286
  $node = node_submit($node);
287
  node_save($node);
288
  if ($node->nid) {
289
    watchdog('content', '@type: updated %title using Blog API.', array('@type' => $node->type, '%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
290
    return TRUE;
Dries's avatar
 
Dries committed
291
292
  }

293
  return blogapi_error(t('Error storing post.'));
Dries's avatar
 
Dries committed
294
295
}

296
297
298
299
300
301
302
303
304
305
306
/**
 * 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);

307
  return _blogapi_get_post($node, TRUE);
308
309
}

310
/**
311
 * Blogging API callback. Removes the specified blog node.
312
 */
313
function blogapi_blogger_delete_post($appkey, $postid, $username, $password, $publish) {
314
  $user = blogapi_validate_user($username, $password);
Dries's avatar
 
Dries committed
315
  if (!$user->uid) {
316
    return blogapi_error($user);
Dries's avatar
 
Dries committed
317
318
  }

319
  node_delete($postid);
320
  return TRUE;
321
}
322

323
324
325
326
327
328
329
330
331
332
333
/**
 * 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
334

335
336
337
338
339
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
    // Return an error if not configured type.
    return $error;
  }

340
  if ($bodies) {
341
342
343
344
    $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(
      ':type' => $blogid,
      ':uid' => $user->uid
    ), 0, $number_of_posts);
345
346
  }
  else {
347
348
349
350
    $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(
      ':type' => $blogid,
      ':uid' => $user->uid
    ), 0, $number_of_posts);
351
  }
352
  $blogs = array();
353
  foreach ($result as $blog) {
354
355
    $blogs[] = _blogapi_get_post($blog, $bodies);
  }
356

357
358
359
360
361
362
363
364
365
  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
366
367
}

368
function blogapi_metaweblog_get_post($postid, $username, $password) {
369
  return blogapi_blogger_get_post('01234567890ABCDEF', $postid, $username, $password);
Dries's avatar
 
Dries committed
370
371
}

372
373
374
/**
 * Blogging API callback. Inserts a file into Drupal.
 */
375
376
function blogapi_metaweblog_new_media_object($blogid, $username, $password, $file) {
  $user = blogapi_validate_user($username, $password);
377
378
379
380
  if (!$user->uid) {
    return blogapi_error($user);
  }

381
382
  $name = basename($file['name']);
  $data = $file['bits'];
383
384

  if (!$data) {
385
    return blogapi_error(t('No file sent.'));
386
  }
387

388
  if (!$filepath = file_save_data($data, $name)) {
389
    return blogapi_error(t('Error storing file.'));
390
  }
391

392
  // Return the successful result.
393
  return array('url' => file_create_url($filepath), 'struct');
Dries's avatar
 
Dries committed
394
}
395
396
397
398
/**
 * Blogging API callback. Returns a list of the taxonomy terms that can be
 * associated with a blog node.
 */
399
function blogapi_metaweblog_get_category_list($blogid, $username, $password) {
400
401
402
403
404
405
  if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
    // Return an error if not configured type.
    return $error;
  }

  $vocabularies = module_invoke('taxonomy', 'get_vocabularies', $blogid, 'vid');
406
  $categories = array();
407
408
  if ($vocabularies) {
    foreach ($vocabularies as $vocabulary) {
409
      $terms = module_invoke('taxonomy', 'get_tree', $vocabulary->vid, 0, -1);
410
411
412
      foreach ($terms as $term) {
        $term_name = $term->name;
        foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
413
          $term_name = $parent->name . '/' . $term_name;
414
        }
415
        $categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid);
Dries's avatar
 
Dries committed
416
417
418
      }
    }
  }
419

420
421
422
  return $categories;
}

423
function blogapi_metaweblog_get_recent_posts($blogid, $username, $password, $number_of_posts) {
424
425
426
427
428
429
430
431
432
  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
433
434
}

435
436
437
438
/**
 * Blogging API callback. Returns a list of the taxonomy terms that are
 * assigned to a particular node.
 */
439
440
function blogapi_mt_get_post_categories($postid, $username, $password) {
  $user = blogapi_validate_user($username, $password);
Dries's avatar
 
Dries committed
441
  if (!$user->uid) {
442
    return blogapi_error($user);
Dries's avatar
 
Dries committed
443
444
  }

445
446
  $node = node_load($postid);
  $terms = module_invoke('taxonomy', 'node_get_terms', $node, 'tid');
Dries's avatar
 
Dries committed
447
  $categories = array();
448
  foreach ($terms as $term) {
Dries's avatar
 
Dries committed
449
    $term_name = $term->name;
450
    foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
451
      $term_name = $parent->name . '/' . $term_name;
Dries's avatar
 
Dries committed
452
    }
453
    $categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid, 'isPrimary' => TRUE);
Dries's avatar
 
Dries committed
454
  }
455

456
  return $categories;
Dries's avatar
 
Dries committed
457
458
}

459
460
461
/**
 * Blogging API callback. Assigns taxonomy terms to a particular node.
 */
462
463
function blogapi_mt_set_post_categories($postid, $username, $password, $categories) {
  $user = blogapi_validate_user($username, $password);
Dries's avatar
 
Dries committed
464
  if (!$user->uid) {
465
    return blogapi_error($user);
Dries's avatar
 
Dries committed
466
467
  }

468
469
  $node = node_load($postid);
  $node->taxonomy = array();
470
  foreach ($categories as $category) {
471
    $node->taxonomy[] = $category['categoryId'];
Dries's avatar
 
Dries committed
472
  }
473
  node_save($node);
474

475
  return TRUE;
476
477
478
479
480
}

/**
 * Blogging API callback. Sends a list of available input formats.
 */
481
function blogapi_mt_supported_text_filters() {
482
483
484
485
486
487
  // 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) {
488
489
490
    $filter['key'] = $format->format;
    $filter['label'] = $format->name;
    $filters[] = $filter;
491
492
  }

493
  return $filters;
494
495
496
}

/**
497
 * Blogging API callback. Publishes the given node.
498
 */
499
function blogapi_mt_publish_post($postid, $username, $password) {
500
  $user = blogapi_validate_user($username, $password);
501

502
503
504
  if (!$user->uid) {
    return blogapi_error($user);
  }
505
  $node = node_load($postid);
506

507
  if (!$node) {
508
    return blogapi_error(t('Invalid post.'));
509
510
511
512
  }

  $node->status = 1;
  if (!node_access('update', $node)) {
513
    return blogapi_error(t('You do not have permission to update this post.'));
514
515
516
517
  }

  node_save($node);

518
  return TRUE;
Dries's avatar
 
Dries committed
519
520
}

521
522
523
/**
 * Prepare an error message for returning to the XMLRPC caller.
 */
Dries's avatar
 
Dries committed
524
function blogapi_error($message) {
525
  static $xmlrpcusererr;
526

Dries's avatar
   
Dries committed
527
528
  if (!is_array($message)) {
    $message = array($message);
529
530
  }

Dries's avatar
   
Dries committed
531
532
  $message = implode(' ', $message);

533
  return xmlrpc_error($xmlrpcusererr + 1, strip_tags($message));
Dries's avatar
 
Dries committed
534
535
}

536
537
538
/**
 * Ensure that the given user has permission to edit a blog.
 */
Dries's avatar
 
Dries committed
539
540
function blogapi_validate_user($username, $password) {
  global $user;
Dries's avatar
   
Dries committed
541

542
  $user = user_authenticate(array('name' => $username, 'pass' => $password));
543

544
  if ($user->uid) {
545
    if (user_access('administer content with blog api', $user)) {
546
547
548
      return $user;
    }
    else {
549
      return t('You do not have permission to edit this blog.');
550
551
552
553
    }
  }
  else {
    return t('Wrong username or password.');
Dries's avatar
 
Dries committed
554
555
556
  }
}

557
558
559
/**
 * For the blogger API, extract the node title from the contents field.
 */
Dries's avatar
 
Dries committed
560
function blogapi_blogger_title(&$contents) {
561
  if (preg_match('/<title>(.*?)<\/title>/i', $contents, $title)) {
562
    $title = strip_tags($title[0]);
563
    $contents = preg_replace('/<title>.*?<\/title>/i', '', $contents);
Dries's avatar
 
Dries committed
564
565
  }
  else {
566
    list($title, $contents) = explode("\n", $contents, 2);
Dries's avatar
 
Dries committed
567
  }
568

Dries's avatar
 
Dries committed
569
570
  return $title;
}
Dries's avatar
   
Dries committed
571

572
573
574
/**
 * Add some settings to the admin_settings form.
 */
Dries's avatar
Dries committed
575
function blogapi_admin_settings() {
576
  $node_types = array_map('check_plain', node_get_types('names'));
577
  $defaults = isset($node_types['blog']) ? array('blog' => 1) : array();
578
  $form['blogapi_node_types'] = array(
Dries's avatar
Dries committed
579
    '#type' => 'checkboxes',
580
    '#title' => t('Enable for external blogging clients'),
Dries's avatar
Dries committed
581
582
583
    '#required' => TRUE,
    '#default_value' => variable_get('blogapi_node_types', $defaults),
    '#options' => $node_types,
584
    '#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.')
585
  );
586

587
  return system_settings_form($form);
Dries's avatar
   
Dries committed
588
589
}

590
591
592
/**
 * Implementation of hook_menu().
 */
593
594
function blogapi_menu() {
  $items['blogapi/rsd'] = array(
595
    'title' => 'RSD',
596
597
598
599
600
    'page callback' => 'blogapi_rsd',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/blogapi'] = array(
601
602
    'title' => 'Blog API',
    'description' => 'Configure the content types available to external blogging clients.',
603
604
605
606
607
608
609
610
    '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
611

612
613
614
/**
 * Implementaton of hook_init().
 */
615
function blogapi_init() {
616
  if (drupal_is_front_page()) {
617
618
619
    drupal_add_link(array('rel' => 'EditURI',
                          'type' => 'application/rsd+xml',
                          'title' => t('RSD'),
620
                          'href' => url('blogapi/rsd', array('absolute' => TRUE))));
Dries's avatar
   
Dries committed
621
622
623
624
625
626
  }
}

function blogapi_rsd() {
  global $base_url;

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

631
  drupal_set_header('Content-Type: application/rsd+xml; charset=utf-8');
Dries's avatar
   
Dries committed
632
633
634
635
636
  print <<<__RSD__
<?xml version="1.0"?>
<rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
  <service>
    <engineName>Drupal</engineName>
637
    <engineLink>http://drupal.org/</engineLink>
Dries's avatar
   
Dries committed
638
639
    <homePageLink>$base</homePageLink>
    <apis>
640
      <api name="MetaWeblog" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
641
642
      <api name="Blogger" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
      <api name="MovableType" preferred="true" apiLink="$xmlrpc" blogID="$blogid" />
Dries's avatar
   
Dries committed
643
644
645
646
647
648
    </apis>
  </service>
</rsd>
__RSD__;
}

649
650
651
652
653
/**
 * Handles extra information sent by clients according to MovableType's spec.
 */
function _blogapi_mt_extra(&$node, $struct) {
  if (is_array($node)) {
654
    $was_array = TRUE;
655
    $node = (object)$node;
656
657
658
659
660
  }

  if (array_key_exists('mt_allow_comments', $struct)) {
    switch ($struct['mt_allow_comments']) {
      case 0:
661
        $node->comment = COMMENT_NODE_DISABLED;
662
663
        break;
      case 1:
664
        $node->comment = COMMENT_NODE_READ_WRITE;
665
666
        break;
      case 2:
667
        $node->comment = COMMENT_NODE_READ_ONLY;
668
669
670
671
        break;
    }
  }

672
  // Merge the 3 body sections (description, mt_excerpt, mt_text_more) into one body.
673
  if ($struct['mt_excerpt']) {
674
    $node->body = $struct['mt_excerpt'] . '<!--break-->' . $node->body;
675
676
  }
  if ($struct['mt_text_more']) {
677
    $node->body = $node->body . '<!--extended-->' . $struct['mt_text_more'];
678
679
680
681
682
683
684
  }

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

  if ($struct['dateCreated']) {
685
    $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
687
688
  }

  if ($was_array) {
689
    $node = (array)$node;
690
691
692
  }
}

693
function _blogapi_get_post($node, $bodies = TRUE) {
694
  $xmlrpcval = array(
695
696
697
698
    'userid' => $node->name,
    'dateCreated' => xmlrpc_date($node->created),
    'title' => $node->title,
    'postid' => $node->nid,
699
700
    'link' => url('node/' . $node->nid, array('absolute' => TRUE)),
    'permaLink' => url('node/' . $node->nid, array('absolute' => TRUE)),
701
  );
702

703
  if ($bodies) {
704
    if ($node->comment == 1) {
705
706
      $comment = 2;
    }
707
    elseif ($node->comment == 2) {
708
709
      $comment = 1;
    }
710
711
    $xmlrpcval['content'] = "<title>$node->title</title>$node->body";
    $xmlrpcval['description'] = $node->body;
712
    // Add MT specific fields
713
    $xmlrpcval['mt_allow_comments'] = (int) $comment;
714
    $xmlrpcval['mt_convert_breaks'] = $node->format;
715
716
  }

717
  return $xmlrpcval;
718
}
719

720
721
722
723
724
725
726
727
728
729
730
731
732
/**
 * 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;
733
  }
734

735
  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)));
736
737
738
}

function _blogapi_get_node_types() {
739
  $available_types = array_keys(array_filter(variable_get('blogapi_node_types', array('blog' => 1))));
740
  $types = array();
741
  foreach (node_get_types() as $type => $name) {
742
743
744
745
746
747
748
    if (node_access('create', $type) && in_array($type, $available_types)) {
      $types[] = $type;
    }
  }

  return $types;
}