Simpletest Coverage - modules/blogapi/blogapi.module

1 <?php
2 // $Id: blogapi.module,v 1.158 2009/07/05 18:00:07 dries Exp $
3
4 /**
5 * @file
6 * Enable users to post using applications that support XML-RPC blog APIs.
7 */
8
9 /**
10 * Implement hook_help().
11 */
12 function blogapi_help($path, $arg) {
13 switch ($path) {
14 case 'admin/help#blogapi':
15 $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>';
16 $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>';
17 $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>';
18 $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;
20 }
21 }
22
23 /**
24 * Implement hook_permission().
25 */
26 function blogapi_permission() {
27 return array(
28 'administer content with blog api' => array(
29 'title' => t('Administer content with blog API'),
30 'description' => t('Manage website content from external tools.'),
31 ),
32 );
33 }
34
35 /**
36 * Implement hook_xmlrpc().
37 */
38 function blogapi_xmlrpc() {
39 return array(
40 array(
41 'blogger.getUsersBlogs',
42 'blogapi_blogger_get_users_blogs',
43 array('array', 'string', 'string', 'string'),
44 t('Returns a list of blogs to which an author has posting privileges.')),
45 array(
46 'blogger.getUserInfo',
47 'blogapi_blogger_get_user_info',
48 array('struct', 'string', 'string', 'string'),
49 t('Returns information about an author in the system.')),
50 array(
51 'blogger.newPost',
52 'blogapi_blogger_new_post',
53 array('string', 'string', 'string', 'string', 'string', 'string', 'boolean'),
54 t('Creates a new post, and optionally publishes it.')),
55 array(
56 'blogger.editPost',
57 'blogapi_blogger_edit_post',
58 array('boolean', 'string', 'string', 'string', 'string', 'string', 'boolean'),
59 t('Updates the information about an existing post.')),
60 array(
61 'blogger.getPost',
62 'blogapi_blogger_get_post',
63 array('struct', 'string', 'string', 'string', 'string'),
64 t('Returns information about a specific post.')),
65 array(
66 'blogger.deletePost',
67 'blogapi_blogger_delete_post',
68 array('boolean', 'string', 'string', 'string', 'string', 'boolean'),
69 t('Deletes a post.')),
70 array(
71 'blogger.getRecentPosts',
72 'blogapi_blogger_get_recent_posts',
73 array('array', 'string', 'string', 'string', 'string', 'int'),
74 t('Returns a list of the most recent posts in the system.')),
75 array(
76 'metaWeblog.newPost',
77 'blogapi_metaweblog_new_post',
78 array('string', 'string', 'string', 'string', 'struct', 'boolean'),
79 t('Creates a new post, and optionally publishes it.')),
80 array(
81 'metaWeblog.editPost',
82 'blogapi_metaweblog_edit_post',
83 array('boolean', 'string', 'string', 'string', 'struct', 'boolean'),
84 t('Updates information about an existing post.')),
85 array(
86 'metaWeblog.getPost',
87 'blogapi_metaweblog_get_post',
88 array('struct', 'string', 'string', 'string'),
89 t('Returns information about a specific post.')),
90 array(
91 'metaWeblog.newMediaObject',
92 'blogapi_metaweblog_new_media_object',
93 array('string', 'string', 'string', 'string', 'struct'),
94 t('Uploads a file to your webserver.')),
95 array(
96 'metaWeblog.getCategories',
97 'blogapi_metaweblog_get_category_list',
98 array('struct', 'string', 'string', 'string'),
99 t('Returns a list of all categories to which the post is assigned.')),
100 array(
101 'metaWeblog.getRecentPosts',
102 'blogapi_metaweblog_get_recent_posts',
103 array('array', 'string', 'string', 'string', 'int'),
104 t('Returns a list of the most recent posts in the system.')),
105 array(
106 'mt.getRecentPostTitles',
107 'blogapi_mt_get_recent_post_titles',
108 array('array', 'string', 'string', 'string', 'int'),
109 t('Returns a bandwidth-friendly list of the most recent posts in the system.')),
110 array(
111 'mt.getCategoryList',
112 'blogapi_mt_get_category_list',
113 array('array', 'string', 'string', 'string'),
114 t('Returns a list of all categories defined in the blog.')),
115 array(
116 'mt.getPostCategories',
117 'blogapi_mt_get_post_categories',
118 array('array', 'string', 'string', 'string'),
119 t('Returns a list of all categories to which the post is assigned.')),
120 array(
121 'mt.setPostCategories',
122 'blogapi_mt_set_post_categories',
123 array('boolean', 'string', 'string', 'string', 'array'),
124 t('Sets the categories for a post.')),
125 array(
126 'mt.supportedMethods',
127 'xmlrpc_server_list_methods',
128 array('array'),
129 t('Retrieve information about the XML-RPC methods supported by the server.')),
130 array(
131 'mt.supportedTextFilters',
132 'blogapi_mt_supported_text_filters',
133 array('array'),
134 t('Retrieve information about the text formatting plugins supported by the server.')),
135 array(
136 '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).')));
140 }
141
142 /**
143 * Blogging API callback. Finds the URL of a user's blog.
144 */
145 function blogapi_blogger_get_users_blogs($appid, $username, $password) {
146 $user = blogapi_validate_user($username, $password);
147 if ($user->uid) {
148 $types = _blogapi_get_node_types();
149 $structs = array();
150 foreach ($types as $type) {
151 $structs[] = array('url' => url('user/' . $user->uid, array('absolute' => TRUE)), 'blogid' => $type, 'blogName' => $user->name . ": " . $type);
152 }
153
154 return $structs;
155 }
156 else {
157 return blogapi_error($user);
158 }
159 }
160
161 /**
162 * Blogging API callback. Returns profile information about a user.
163 */
164 function blogapi_blogger_get_user_info($appkey, $username, $password) {
165 $user = blogapi_validate_user($username, $password);
166
167 if ($user->uid) {
168 $name = explode(' ', $user->realname ? $user->realname : $user->name, 2);
169 return array(
170 'userid' => $user->uid,
171 'lastname' => $name[1],
172 'firstname' => $name[0],
173 'nickname' => $user->name,
174 'email' => $user->mail,
175 'url' => url('user/' . $user->uid, array('absolute' => TRUE)));
176 }
177 else {
178 return blogapi_error($user);
179 }
180 }
181
182 /**
183 * Blogging API callback. Inserts a new blog post as a node.
184 */
185 function blogapi_blogger_new_post($appkey, $blogid, $username, $password, $content, $publish) {
186 $user = blogapi_validate_user($username, $password);
187 if (!$user->uid) {
188 return blogapi_error($user);
189 }
190
191 if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
192 // Return an error if not configured type.
193 return $error;
194 }
195
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 $edit['uid'] = $user->uid;
201 $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);
205 $edit['format'] = FILTER_FORMAT_DEFAULT;
206 $edit['status'] = $publish;
207
208 // Check for bloggerAPI vs. metaWeblogAPI.
209 if (is_array($content)) {
210 $edit['title'] = $content['title'];
211 $edit['body'] = $content['description'];
212 _blogapi_mt_extra($edit, $content);
213 }
214 else {
215 $edit['title'] = blogapi_blogger_title($content);
216 $edit['body'][0]['value'] = $content;
217 }
218
219 if (!node_access('create', $edit['type'])) {
220 return blogapi_error(t('You do not have permission to create this type of post.'));
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 module_invoke_all('node_blogapi_new', $edit);
228
229 $valid = blogapi_status_error_check($edit, $publish);
230 if ($valid !== TRUE) {
231 return $valid;
232 }
233
234 node_validate($edit);
235 if ($errors = form_get_errors()) {
236 return blogapi_error(implode("\n", $errors));
237 }
238
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";
245 }
246
247 return blogapi_error(t('Error storing post.'));
248 }
249
250 /**
251 * Blogging API callback. Modifies the specified blog node.
252 */
253 function blogapi_blogger_edit_post($appkey, $postid, $username, $password, $content, $publish) {
254 $user = blogapi_validate_user($username, $password);
255
256 if (!$user->uid) {
257 return blogapi_error($user);
258 }
259
260 $node = node_load($postid);
261 if (!$node) {
262 return blogapi_error(t('n/a'));
263 }
264 // Let the teaser be re-generated.
265 unset($node->teaser);
266
267 if (!node_access('update', $node)) {
268 return blogapi_error(t('You do not have permission to update this post.'));
269 }
270 // Save the original status for validation of permissions.
271 $original_status = $node->status;
272 $node->status = $publish;
273
274 // Check for bloggerAPI vs. metaWeblogAPI.
275 if (is_array($content)) {
276 $node->title = $content['title'];
277 $node->body[0]['value'] = $content['description'];
278 _blogapi_mt_extra($node, $content);
279 }
280 else {
281 $node->title = blogapi_blogger_title($content);
282 $node->body[0]['value'] = $content;
283 }
284
285 module_invoke_all('node_blogapi_edit', $node);
286
287 $valid = blogapi_status_error_check($node, $original_status);
288 if ($valid !== TRUE) {
289 return $valid;
290 }
291
292 node_validate($node);
293 if ($errors = form_get_errors()) {
294 return blogapi_error(implode("\n", $errors));
295 }
296
297 if (user_access('administer nodes') && !isset($edit['date'])) {
298 $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
299 }
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;
305 }
306
307 return blogapi_error(t('Error storing post.'));
308 }
309
310 /**
311 * Blogging API callback. Returns a specified blog node.
312 */
313 function blogapi_blogger_get_post($appkey, $postid, $username, $password) {
314 $user = blogapi_validate_user($username, $password);
315 if (!$user->uid) {
316 return blogapi_error($user);
317 }
318
319 $node = node_load($postid);
320
321 return _blogapi_get_post($node, TRUE);
322 }
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 */
330 function blogapi_status_error_check($node, $original_status) {
331
332 $node = (object) $node;
333
334 $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.
339 if (!user_access('administer nodes') && (($node->status != $original_status) || (empty($node->nid) && $node->status != in_array('status', $node_type_default)))) {
340 if ($node->status) {
341 return blogapi_error(t('You do not have permission to publish this type of post. Please save it as a draft instead.'));
342 }
343 else {
344 return blogapi_error(t('You do not have permission to save this post as a draft. Please publish it instead.'));
345 }
346 }
347 return TRUE;
348 }
349
350
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);
356 if (!$user->uid) {
357 return blogapi_error($user);
358 }
359
360 node_delete($postid);
361 return TRUE;
362 }
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 */
369 function blogapi_blogger_get_recent_posts($appkey, $blogid, $username, $password, $number_of_posts, $bodies = TRUE) {
370 // Remove unused appkey (from bloggerAPI).
371 $user = blogapi_validate_user($username, $password);
372 if (!$user->uid) {
373 return blogapi_error($user);
374 }
375
376 if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
377 // Return an error if not configured type.
378 return $error;
379 }
380
381 if ($bodies) {
382 $result = db_query_range("SELECT n.nid, n.title, 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 ':type' => $blogid,
384 ':uid' => $user->uid
385 ), 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 ':type' => $blogid,
390 ':uid' => $user->uid
391 ), 0, $number_of_posts);
392 }
393 $blogs = array();
394 foreach ($result as $blog) {
395 $blogs[] = _blogapi_get_post($blog, $bodies);
396 }
397
398 return $blogs;
399 }
400
401 function blogapi_metaweblog_new_post($blogid, $username, $password, $content, $publish) {
402 return blogapi_blogger_new_post('0123456789ABCDEF', $blogid, $username, $password, $content, $publish);
403 }
404
405 function blogapi_metaweblog_edit_post($postid, $username, $password, $content, $publish) {
406 return blogapi_blogger_edit_post('0123456789ABCDEF', $postid, $username, $password, $content, $publish);
407 }
408
409 function blogapi_metaweblog_get_post($postid, $username, $password) {
410 return blogapi_blogger_get_post('01234567890ABCDEF', $postid, $username, $password);
411 }
412
413 /**
414 * Blogging API callback. Inserts a file into Drupal.
415 */
416 function blogapi_metaweblog_new_media_object($blogid, $username, $password, $file) {
417 $user = blogapi_validate_user($username, $password);
418 if (!$user->uid) {
419 return blogapi_error($user);
420 }
421
422 $usersize = 0;
423 $uploadsize = 0;
424
425 $roles = array_intersect(user_roles(FALSE, 'administer content with blog api'), $user->roles);
426
427 foreach ($roles as $rid => $name) {
428 $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')));
429 $usersize = max($usersize, variable_get("blogapi_usersize_$rid", variable_get('blogapi_usersize_default', 1)) * 1024 * 1024);
430 $uploadsize = max($uploadsize, variable_get("blogapi_uploadsize_$rid", variable_get('blogapi_uploadsize_default', 1)) * 1024 * 1024);
431 }
432
433 $filesize = strlen($file['bits']);
434
435 if ($filesize > $uploadsize) {
436 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))));
437 }
438
439 if (_blogapi_space_used($user->uid) + $filesize > $usersize) {
440 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))));
441 }
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
447 $whitelist = array_unique(explode(' ', trim($extensions)));
448
449 $name = basename($file['name']);
450
451 if ($extension_position = strrpos($name, '.')) {
452 $filename = drupal_substr($name, 0, $extension_position);
453 $final_extension = drupal_substr($name, $extension_position + 1);
454
455 if (!in_array(strtolower($final_extension), $whitelist)) {
456 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))));
457 }
458
459 $filename = str_replace('.', '_', $filename);
460 $filename .= '.' . $final_extension;
461 }
462
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 $row = new stdClass();
474 $row->uid = $user->uid;
475 $row->filepath = $file;
476 $row->filesize = $filesize;
477
478 drupal_write_record('blogapi_files', $row);
479
480 // Return the successful result.
481 return array('url' => file_create_url($file), 'struct');
482 }
483 /**
484 * Blogging API callback. Returns a list of the taxonomy terms that can be
485 * associated with a blog node.
486 */
487 function blogapi_metaweblog_get_category_list($blogid, $username, $password) {
488 $user = blogapi_validate_user($username, $password);
489 if (!$user->uid) {
490 return blogapi_error($user);
491 }
492
493 if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
494 // Return an error if not configured type.
495 return $error;
496 }
497
498 $vocabularies = module_invoke('taxonomy', 'get_vocabularies', $blogid, 'vid');
499 $categories = array();
500 if ($vocabularies) {
501 foreach ($vocabularies as $vocabulary) {
502 $terms = module_invoke('taxonomy', 'get_tree', $vocabulary->vid);
503 foreach ($terms as $term) {
504 $term_name = $term->name;
505 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);
509 }
510 }
511 }
512
513 return $categories;
514 }
515
516 function blogapi_metaweblog_get_recent_posts($blogid, $username, $password, $number_of_posts) {
517 return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid, $username, $password, $number_of_posts, TRUE);
518 }
519
520 function blogapi_mt_get_recent_post_titles($blogid, $username, $password, $number_of_posts) {
521 return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid, $username, $password, $number_of_posts, FALSE);
522 }
523
524 function blogapi_mt_get_category_list($blogid, $username, $password) {
525 return blogapi_metaweblog_get_category_list($blogid, $username, $password);
526 }
527
528 /**
529 * Blogging API callback. Returns a list of the taxonomy terms that are
530 * assigned to a particular node.
531 */
532 function blogapi_mt_get_post_categories($postid, $username, $password) {
533 $user = blogapi_validate_user($username, $password);
534 if (!$user->uid) {
535 return blogapi_error($user);
536 }
537
538 $node = node_load($postid);
539 $terms = module_invoke('taxonomy', 'node_get_terms', $node, 'tid');
540 $categories = array();
541 foreach ($terms as $term) {
542 $term_name = $term->name;
543 foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
544 $term_name = $parent->name . '/' . $term_name;
545 }
546 $categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid, 'isPrimary' => TRUE);
547 }
548
549 return $categories;
550 }
551
552 /**
553 * Blogging API callback. Assigns taxonomy terms to a particular node.
554 */
555 function blogapi_mt_set_post_categories($postid, $username, $password, $categories) {
556 $user = blogapi_validate_user($username, $password);
557 if (!$user->uid) {
558 return blogapi_error($user);
559 }
560
561 $node = node_load($postid);
562 $node->taxonomy = array();
563 foreach ($categories as $category) {
564 $node->taxonomy[] = $category['categoryId'];
565 }
566 $validated = blogapi_mt_validate_terms($node);
567 if ($validated !== TRUE) {
568 return $validated;
569 }
570 node_save($node);
571
572 return TRUE;
573 }
574
575 /**
576 * Blogging API helper - find allowed taxonomy terms for a node type.
577 */
578 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.
581 if (module_exists('taxonomy')) {
582 $found_terms = array();
583 if (!empty($node->taxonomy)) {
584 $term_list = array_unique($node->taxonomy);
585 $terms = taxonomy_term_load_multiple($term_list, array('type' => $node->type));
586 $found_terms = array();
587 $found_count = 0;
588 foreach ($terms as $term) {
589 $found_terms[$term->vid][$term->tid] = $term->tid;
590 $found_count++;
591 }
592 // If the counts don't match, some terms are invalid or not accessible to this user.
593 if (count($term_list) != $found_count) {
594 return blogapi_error(t('Invalid categories submitted.'));
595 }
596 }
597 // Look up all the vocabularies for this node type.
598 $vocabularies = taxonomy_vocabulary_load_multiple(array(), array('type' => $node->type));
599 // Check each vocabulary associated with this node type.
600 foreach ($vocabularies as $vocabulary) {
601 // Required vocabularies must have at least one term.
602 if ($vocabulary->required && empty($found_terms[$vocabulary->vid])) {
603 return blogapi_error(t('A category from the @vocabulary_name vocabulary is required.', array('@vocabulary_name' => $vocabulary->name)));
604 }
605 // Vocabularies that don't allow multiple terms may have at most one.
606 if (!($vocabulary->multiple) && (isset($found_terms[$vocabulary->vid]) && count($found_terms[$vocabulary->vid]) > 1)) {
607 return blogapi_error(t('You may only choose one category from the @vocabulary_name vocabulary.'), array('@vocabulary_name' => $vocabulary->name));
608 }
609 }
610 }
611 elseif (!empty($node->taxonomy)) {
612 return blogapi_error(t('Error saving categories. This feature is not available.'));
613 }
614 return TRUE;
615 }
616
617 /**
618 * Blogging API callback. Sends a list of available text formats.
619 */
620 function blogapi_mt_supported_text_filters() {
621 // NOTE: we're only using anonymous' formats because the MT spec
622 // does not allow for per-user formats.
623 $formats = filter_formats();
624
625 $filters = array();
626 foreach ($formats as $format) {
627 $filter['key'] = $format->format;
628 $filter['label'] = $format->name;
629 $filters[] = $filter;
630 }
631
632 return $filters;
633 }
634
635 /**
636 * Blogging API callback. Publishes the given node.
637 */
638 function blogapi_mt_publish_post($postid, $username, $password) {
639 $user = blogapi_validate_user($username, $password);
640
641 if (!$user->uid) {
642 return blogapi_error($user);
643 }
644 $node = node_load($postid);
645
646 if (!$node) {
647 return blogapi_error(t('Invalid post.'));
648 }
649
650 // Nothing needs to be done if already published.
651 if ($node->status) {
652 return;
653 }
654
655 if (!node_access('update', $node) || !user_access('administer nodes')) {
656 return blogapi_error(t('You do not have permission to update this post.'));
657 }
658
659 $node->status = 1;
660 node_save($node);
661
662 return TRUE;
663 }
664
665 /**
666 * Prepare an error message for returning to the XMLRPC caller.
667 */
668 function blogapi_error($message) {
669 static $xmlrpcusererr;
670
671 if (!is_array($message)) {
672 $message = array($message);
673 }
674
675 $message = implode(' ', $message);
676
677 return xmlrpc_error($xmlrpcusererr + 1, strip_tags($message));
678 }
679
680 /**
681 * Ensure that the given user has permission to edit a blog.
682 */
683 function blogapi_validate_user($username, $password) {
684 $form_state['values']['name'] = $username;
685 $form_state['values']['pass'] = $password;
686 $form_state['values']['op'] = t('Login');
687 drupal_form_submit('user_login', $form_state);
688
689 if (!form_get_error()) {
690 if (user_access('administer content with blog api', $user)) {
691 return $GLOBALS['user'];
692 }
693 else {
694 return t('You do not have permission to edit this blog.');
695 }
696 }
697 else {
698 return t('Wrong username or password.');
699 }
700 }
701
702 /**
703 * For the blogger API, extract the node title from the contents field.
704 */
705 function blogapi_blogger_title(&$contents) {
706 if (preg_match('/<title>(.*?)<\/title>/i', $contents, $title)) {
707 $title = strip_tags($title[0]);
708 $contents = preg_replace('/<title>.*?<\/title>/i', '', $contents);
709 }
710 else {
711 list($title, $contents) = explode("\n", $contents, 2);
712 }
713
714 return $title;
715 }
716
717 /**
718 * Add some settings to the admin_settings form.
719 */
720 function blogapi_admin_settings() {
721 $node_types = array_map('check_plain', node_type_get_names());
722 $defaults = isset($node_types['blog']) ? array('blog' => 1) : array();
723 $form['blogapi_node_types'] = array(
724 '#type' => 'checkboxes',
725 '#title' => t('Enable for external blogging clients'),
726 '#required' => TRUE,
727 '#default_value' => variable_get('blogapi_node_types', $defaults),
728 '#options' => $node_types,
729 '#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.')
730 );
731
732 $blogapi_extensions_default = variable_get('blogapi_extensions_default', 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp');
733 $blogapi_uploadsize_default = variable_get('blogapi_uploadsize_default', 1);
734 $blogapi_usersize_default = variable_get('blogapi_usersize_default', 1);
735
736 $form['settings_general'] = array(
737 '#type' => 'fieldset',
738 '#title' => t('File settings'),
739 '#collapsible' => TRUE,
740 );
741
742 $form['settings_general']['blogapi_extensions_default'] = array(
743 '#type' => 'textfield',
744 '#title' => t('Default permitted file extensions'),
745 '#default_value' => $blogapi_extensions_default,
746 '#maxlength' => 255,
747 '#description' => t('Default extensions that users can upload. Separate extensions with a space and do not include the leading dot.'),
748 );
749
750 $form['settings_general']['blogapi_uploadsize_default'] = array(
751 '#type' => 'textfield',
752 '#title' => t('Default maximum file size per upload'),
753 '#default_value' => $blogapi_uploadsize_default,
754 '#size' => 5,
755 '#maxlength' => 5,
756 '#description' => t('The default maximum file size a user can upload.'),
757 '#field_suffix' => t('MB')
758 );
759
760 $form['settings_general']['blogapi_usersize_default'] = array(
761 '#type' => 'textfield',
762 '#title' => t('Default total file size per user'),
763 '#default_value' => $blogapi_usersize_default,
764 '#size' => 5,
765 '#maxlength' => 5,
766 '#description' => t('The default maximum size of all files a user can have on the site.'),
767 '#field_suffix' => t('MB')
768 );
769
770 $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>');
771
772 $roles = user_roles(FALSE, 'administer content with blog api');
773 $form['roles'] = array('#type' => 'value', '#value' => $roles);
774
775 foreach ($roles as $rid => $role) {
776 $form['settings_role_' . $rid] = array(
777 '#type' => 'fieldset',
778 '#title' => t('Settings for @role', array('@role' => $role)),
779 '#collapsible' => TRUE,
780 '#collapsed' => TRUE,
781 );
782 $form['settings_role_' . $rid]['blogapi_extensions_' . $rid] = array(
783 '#type' => 'textfield',
784 '#title' => t('Permitted file extensions'),
785 '#default_value' => variable_get('blogapi_extensions_' . $rid, $blogapi_extensions_default),
786 '#maxlength' => 255,
787 '#description' => t('Extensions that users in this role can upload. Separate extensions with a space and do not include the leading dot.'),
788 );
789 $form['settings_role_' . $rid]['blogapi_uploadsize_' . $rid] = array(
790 '#type' => 'textfield',
791 '#title' => t('Maximum file size per upload'),
792 '#default_value' => variable_get('blogapi_uploadsize_' . $rid, $blogapi_uploadsize_default),
793 '#size' => 5,
794 '#maxlength' => 5,
795 '#description' => t('The maximum size of a file a user can upload (in megabytes).'),
796 );
797 $form['settings_role_' . $rid]['blogapi_usersize_' . $rid] = array(
798 '#type' => 'textfield',
799 '#title' => t('Total file size per user'),
800 '#default_value' => variable_get('blogapi_usersize_' . $rid, $blogapi_usersize_default),
801 '#size' => 5,
802 '#maxlength' => 5,
803 '#description' => t('The maximum size of all files a user can have on the site (in megabytes).'),
804 );
805 }
806
807 return system_settings_form($form, FALSE);
808 }
809
810 /**
811 * Implement hook_menu().
812 */
813 function blogapi_menu() {
814 $items['blogapi/rsd'] = array(
815 'title' => 'RSD',
816 'page callback' => 'blogapi_rsd',
817 'access arguments' => array('access content'),
818 'type' => MENU_CALLBACK,
819 );
820 $items['admin/settings/blogapi'] = array(
821 'title' => 'Blog API',
822 'description' => 'Configure the content types available to external blogging clients.',
823 'page callback' => 'drupal_get_form',
824 'page arguments' => array('blogapi_admin_settings'),
825 'access arguments' => array('administer site configuration'),
826 'type' => MENU_NORMAL_ITEM,
827 );
828
829 return $items;
830 }
831
832 /**
833 * Implement hook_init().
834 */
835 function blogapi_init() {
836 if (drupal_is_front_page()) {
837 drupal_add_link(array('rel' => 'EditURI',
838 'type' => 'application/rsd+xml',
839 'title' => t('RSD'),
840 'href' => url('blogapi/rsd', array('absolute' => TRUE))));
841 }
842 }
843
844 function blogapi_rsd() {
845 global $base_url;
846
847 $xmlrpc = $base_url . '/xmlrpc.php';
848 $base = url('', array('absolute' => TRUE));
849 $blogid = 1; # until we figure out how to handle multiple bloggers
850
851 drupal_set_header('Content-Type', 'application/rsd+xml; charset=utf-8');
852 print <<<__RSD__
853 <?xml version="1.0"?>
854 <rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
855 <service>
856 <engineName>Drupal</engineName>
857 <engineLink>http://drupal.org/</engineLink>
858 <homePageLink>$base</homePageLink>
859 <apis>
860 <api name="MetaWeblog" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
861 <api name="Blogger" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
862 <api name="MovableType" preferred="true" apiLink="$xmlrpc" blogID="$blogid" />
863 </apis>
864 </service>
865 </rsd>
866 __RSD__;
867 }
868
869 /**
870 * Handles extra information sent by clients according to MovableType's spec.
871 */
872 function _blogapi_mt_extra($node, $struct) {
873 if (is_array($node)) {
874 $was_array = TRUE;
875 $node = (object)$node;
876 }
877
878 if (array_key_exists('mt_allow_comments', $struct)) {
879 switch ($struct['mt_allow_comments']) {
880 case 0:
881 $node->comment = COMMENT_NODE_HIDDEN;
882 break;
883 case 1:
884 $node->comment = COMMENT_NODE_OPEN;
885 break;
886 case 2:
887 $node->comment = COMMENT_NODE_CLOSED;
888 break;
889 }
890 }
891
892 // Merge the 3 body sections (description, mt_excerpt, mt_text_more) into one body.
893 if ($struct['mt_excerpt']) {
894 $node->body[0]['value'] = $struct['mt_excerpt'] . '<!--break-->' . $node->body[0]['value'];
895 }
896 if ($struct['mt_text_more']) {
897 $node->body[0]['value'] = $node->body[0]['value'] . '<!--extended-->' . $struct['mt_text_more'];
898 }
899
900 if ($struct['mt_convert_breaks']) {
901 $node->body[0]['format'] = $struct['mt_convert_breaks'];
902 }
903
904 if ($struct['dateCreated']) {
905 $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');
906 }
907
908 if ($was_array) {
909 $node = (array)$node;
910 }
911 }
912
913 function _blogapi_get_post($node, $bodies = TRUE) {
914 $xmlrpcval = array(
915 'userid' => $node->name,
916 'dateCreated' => xmlrpc_date($node->created),
917 'title' => $node->title,
918 'postid' => $node->nid,
919 'link' => url('node/' . $node->nid, array('absolute' => TRUE)),
920 'permaLink' => url('node/' . $node->nid, array('absolute' => TRUE)),
921 );
922
923 if ($bodies) {
924 $body = $node->body[0]['value'];
925 $format = $node->body[0]['format'];
926 if ($node->comment == 1) {
927 $comment = 2;
928 }
929 elseif ($node->comment == 2) {
930 $comment = 1;
931 }
932 $xmlrpcval['content'] = "<title>$node->title</title>$body";
933 $xmlrpcval['description'] = $body;
934 // Add MT specific fields
935 $xmlrpcval['mt_allow_comments'] = (int) $comment;
936 $xmlrpcval['mt_convert_breaks'] = $format;
937 }
938
939 return $xmlrpcval;
940 }
941
942 /**
943 * Validate blog ID, which maps to a content type in Drupal.
944 *
945 * Only content types configured to work with Blog API are supported.
946 *
947 * @return
948 * TRUE if the content type is supported and the user has permission
949 * to post, or a blogapi_error() XML construct otherwise.
950 */
951 function _blogapi_validate_blogid($blogid) {
952 $types = _blogapi_get_node_types();
953 if (in_array($blogid, $types, TRUE)) {
954 return TRUE;
955 }
956
957 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)));
958 }
959
960 function _blogapi_get_node_types() {
961 $available_types = array_keys(array_filter(variable_get('blogapi_node_types', array('blog' => 1))));
962 $types = array();
963 foreach (node_type_get_types() as $type => $name) {
964 if (node_access('create', $type) && in_array($type, $available_types)) {
965 $types[] = $type;
966 }
967 }
968
969 return $types;
970 }
971
972 function _blogapi_space_used($uid) {
973 return db_query('SELECT SUM(filesize) FROM {blogapi_files} f WHERE f.uid = :uid', array(':uid' => $uid))->fetchField();
974 }
975
976

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.