blogapi.module in Blog API 7.2
Same filename and directory in other branches
Enable users to post using applications that support BlogAPIs.
File
blogapi.moduleView source
<?php
/**
* @file
* Enable users to post using applications that support BlogAPIs.
*/
/**
* Implements hook_permission().
*/
function blogapi_permission() {
return array(
'manage content with blogapi' => array(
'title' => t('Manage content with BlogAPI'),
),
'administer blogapi' => array(
'title' => t('Administer BlogAPI settings'),
),
);
}
/**
* Implements hook_menu().
*/
function blogapi_menu() {
$items = array();
$items['blogapi/rsd'] = array(
'title' => 'RSD',
'page callback' => 'blogapi_rsd',
'access arguments' => array(
'access content',
),
'type' => MENU_CALLBACK,
);
$items['admin/config/services/blogapi'] = array(
'title' => 'BlogAPI',
'description' => 'Configure content types and file settings for external blogging clients.',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'blogapi_admin_settings_form',
),
'access arguments' => array(
'administer blogapi',
),
'type' => MENU_NORMAL_ITEM,
'file' => 'blogapi.admin.inc',
);
return $items;
}
/**
* Implements hook_init().
*/
function blogapi_init() {
if (drupal_is_front_page()) {
drupal_add_html_head_link(array(
'rel' => 'EditURI',
'type' => 'application/rsd+xml',
'title' => t('RSD'),
'href' => url('blogapi/rsd', array(
'absolute' => TRUE,
)),
), TRUE);
}
}
/**
* Ensure that a given user has permission to use BlogAPI
*/
function blogapi_validate_user($username, $password) {
global $user;
// Check the username and password.
$uid = user_authenticate($username, $password);
if (is_numeric($uid)) {
$user = user_load($uid);
if ($user->uid) {
user_login_finalize();
services_remove_user_data($user);
if (user_access('manage content with blogapi', $user)) {
// User has appropriate permissions.
return $user;
}
else {
return services_error(t('You do not have permission to edit this blog'), 405);
}
}
}
watchdog('user', 'Invalid login attempt for %username.', array(
'%username' => $username,
));
return services_error(t('Invalid username or password'), 401);
}
/**
* Return a BlogAPI RSD for XML-RPC APIs
*
* @todo: Implement apiLink correctly using service endpoint URL
* @todo: Implement multi-user blogs
*/
function blogapi_rsd() {
global $base_url;
$base = url('', array(
'absolute' => TRUE,
));
$xmlrpc_apis = blogapi_get_info('xmlrpc');
$default_xmlrpc_api = variable_get('blogapi_xmlrpc_default_provider', NULL);
// Until we figure out how to handle multiple bloggers, we'll just use a
// hardcoded blogid.
$blogid = 1;
drupal_add_http_header('Content-Type', 'application/rsd+xml; charset=utf-8');
// The extra whitespace in this function is to preserve code alignment in
// the output.
print <<<__RSD__
<?xml version="1.0"?>
<rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
<service>
<engineName>Drupal</engineName>
<engineLink>http://drupal.org/</engineLink>
<homePageLink>{<span class="php-variable">$base</span>}</homePageLink>
<apis>
__RSD__;
foreach ($xmlrpc_apis as $module => $info) {
$default = 'false';
if ($module == $default_xmlrpc_api) {
$default = 'true';
}
$endpoint = "{$base_url}/blogapi/{$info['type']}";
print "\n <api name='{$info['name']}' preferred='{$default}' apiLink='{$endpoint}' blogID='{$blogid}' />";
}
print <<<__RSD__
</apis>
</service>
</rsd>
__RSD__;
}
/**
* Get all BlogAPI information, optionally filtered by API type
*/
function blogapi_get_info($api_type = NULL) {
$api_information = array();
// Invoke hook_blogapi_info().
foreach (module_implements('blogapi_info') as $k => $module) {
$info = module_invoke($module, 'blogapi_info');
if ($info['api_version'] == 2) {
$api_information[$module] = $info;
}
}
// If we don't have an API type filter, then allow the info to be altered
// and return it.
if (is_null($api_type)) {
drupal_alter('blogapi_info', $api_information);
return $api_information;
}
// If we have a filter parameter, return filtered information instead.
$filtered_api_info = array();
foreach ($api_information as $name => $info) {
if ($info['type'] == $api_type) {
$filtered_api_info[$name] = $info;
}
}
drupal_alter('blogapi_info', $filtered_api_info);
return $filtered_api_info;
}
/**
* Get a list of API types currently available to BlogAPI.
*/
function blogapi_get_api_types() {
$apis = blogapi_get_info();
$api_types = array();
foreach ($apis as $info) {
// Only include API types using the correct API version.
if ($info['api_version'] == 2) {
$api_types[$info['type']] = $info['type'];
}
}
return $api_types;
}
/**
* Implements hook_ctools_plugin_api().
*/
function blogapi_ctools_plugin_api() {
list($module, $api) = func_get_args();
if ($module == "services" && $api == "services") {
return array(
"version" => "3",
);
}
}
/**
* Implements hook_default_services_endpoint().
*
* This function is enabling all resources by default, but that behavior
* can be overridden via hook_blogapi_default_services_alter().
*/
function blogapi_default_services_endpoint() {
$export = array();
$api_types = blogapi_get_api_types();
foreach ($api_types as $type) {
$endpoint = new stdClass();
$endpoint->disabled = FALSE;
$endpoint->api_version = 3;
$endpoint->name = 'blogapi_' . $type;
$endpoint->server = $type . '_server';
$endpoint->path = 'blogapi/' . $type;
$endpoint->authentication = array();
$endpoint->server_settings = '';
// Get all resources for $type APIs.
$info = blogapi_get_info($type);
$resources = array();
foreach ($info as $module => $api_info) {
$resources += module_invoke($module, 'services_resources');
}
$endpoint->resources = $resources;
$endpoint->debug = 0;
$export['blogapi_' . $type] = $endpoint;
}
drupal_alter('blogapi_default_services', $export);
return $export;
}
/**
* Helper function. Returns the latest few nodes created by a given user.
*/
function blogapi_get_recent_posts($blogid, $username, $password, $number_of_posts = 10, $bodies = TRUE) {
// Validate the user.
$user = blogapi_validate_user($username, $password);
// Validate the content type.
blogapi_validate_content_type($blogid);
$query = new EntityFieldQuery();
$query
->entityCondition('entity_type', 'node')
->entityCondition('bundle', $blogid)
->propertyCondition('uid', $user->uid)
->propertyOrderBy('created', 'DESC')
->range(0, $number_of_posts);
$result = $query
->execute();
if (!empty($result['node'])) {
$blog_nids = array();
$posts = array();
foreach ($result['node'] as $node) {
$blog_nids[] = $node->nid;
}
$blogs = node_load_multiple($blog_nids);
foreach ($blogs as $blog) {
$posts[] = blogapi_format_post_for_xmlrpc($blog, $bodies);
}
return $posts;
}
return array();
}
/**
* Validate that a content type is configured to work with BlogAPI
*
* @param string $content_type
* The machine name of the content type to validate
*
* @return TRUE|array
* TRUE if the content type is configured for use with BlogAPI or
* an error array if not.
*/
function blogapi_validate_content_type($content_type) {
$types = blogapi_get_node_types();
if (in_array($content_type, $types, TRUE)) {
return TRUE;
}
return services_error(t('BlogAPI is not configured to support the @type content type.', array(
'@type' => $content_type,
)), 403);
}
/**
* Helper function. Adds appropriate metadata to the XML-RPC return values.
*/
function blogapi_format_post_for_xmlrpc($node, $bodies = TRUE) {
$xmlrpcval = array(
'userid' => $node->name,
'dateCreated' => xmlrpc_date($node->created),
'title' => $node->title,
'postid' => $node->nid,
'link' => url('node/' . $node->nid, array(
'absolute' => TRUE,
)),
'permaLink' => url('node/' . $node->nid, array(
'absolute' => TRUE,
)),
);
if ($bodies) {
$body = !empty($node->body) ? $node->body[LANGUAGE_NONE][0]['value'] : '';
$format = !empty($node->body) ? $node->body[LANGUAGE_NONE][0]['format'] : 0;
if ($node->comment == 1) {
$comment = 2;
}
elseif ($node->comment == 2) {
$comment = 1;
}
$xmlrpcval['content'] = "<title>{$node->title}</title>{$body}";
$xmlrpcval['description'] = $body;
// Add MT specific fields.
$xmlrpcval['mt_allow_comments'] = (int) $comment;
$xmlrpcval['mt_convert_breaks'] = $format;
}
// Allow altering the XML-RPC response.
drupal_alter('blogapi_xmlrpc_response', $xmlrpcval);
return $xmlrpcval;
}
/**
* Helper function. Find allowed taxonomy terms for a node type.
*/
function blogapi_validate_terms($node) {
// We do a lot of heavy lifting here since taxonomy module doesn't have a
// stand-alone validation function.
if (module_exists('taxonomy')) {
$found_terms = array();
if (!empty($node->taxonomy)) {
$term_list = array_unique($node->taxonomy);
$terms = taxonomy_term_load_multiple($term_list, array(
'type' => $node->type,
));
$found_terms = array();
$found_count = 0;
foreach ($terms as $term) {
$found_terms[$term->vid][$term->tid] = $term->tid;
$found_count++;
}
// If the counts don't match, some terms are invalid or not accessible to
// this user.
if (count($term_list) != $found_count) {
$error_data = array(
'message' => t('Invalid categories were submitted.'),
'error_code' => 405,
);
return $error_data;
}
}
// Look up all the vocabularies for this node type.
$vocabularies = taxonomy_vocabulary_load_multiple(array(), array(
'type' => $node->type,
));
// Check each vocabulary associated with this node type.
foreach ($vocabularies as $vocabulary) {
// Required vocabularies must have at least one term.
if ($vocabulary->required && empty($found_terms[$vocabulary->vid])) {
$error_data = array(
'message' => t('A category from the @vocabulary_name vocabulary is required.', array(
'@vocabulary_name' => $vocabulary->name,
)),
'error_code' => 403,
);
return $error_data;
}
// Vocabularies that don't allow multiple terms may have at most one.
if (!$vocabulary->multiple && (isset($found_terms[$vocabulary->vid]) && count($found_terms[$vocabulary->vid]) > 1)) {
$error_data = array(
'message' => t('You may only choose one category from the @vocabulary_name vocabulary.', array(
'@vocabulary_name' => $vocabulary->name,
)),
'error_code' => 403,
);
return $error_data;
}
}
}
elseif (!empty($node->taxonomy)) {
$error_data = array(
'message' => t('Error saving categories. This feature is not available.'),
'error_code' => 405,
);
return $error_data;
}
return TRUE;
}
/**
* Helper function. Get BlogAPI node types.
*/
function blogapi_get_node_types() {
$node_types = array_map('check_plain', node_type_get_names());
$defaults = !empty($node_types['article']) ? array(
'article' => 'article',
) : array();
$node_types = array_filter(variable_get('blogapi_node_types', $defaults));
return $node_types;
}
/**
* Check that the user has permission to save the node with the chosen status.
*/
function blogapi_status_error_check($node) {
$node = (object) $node;
$original_status = $node->status;
$node_type_default = variable_get('node_options_' . $node->type, array(
'status',
'promote',
));
// If we don't have the 'administer nodes' permission and the status is
// changing or for a new node the status is not the content type's default,
// then return an error.
if (!user_access('administer nodes') && ($node->status != $original_status || empty($node->nid) && $node->status != in_array('status', $node_type_default))) {
if ($node->status) {
return services_error(t('You do not have permission to publish this type of post. Please save it as a draft instead.'), 403);
}
else {
return services_error(t('You do not have permission to save this post as a draft. Please publish it instead.'), 403);
}
}
}
/**
* Helper function. Return the amount of space used by a given user.
*/
function blogapi_space_used($uid) {
return db_query('SELECT SUM(filesize) FROM {blogapi_files} f WHERE f.uid = :uid', array(
':uid' => $uid,
))
->fetchField();
}
/**
* Service allback for metaWeblog.editPost
*/
function blogapi_edit_post($postid, $username, $password, $content, $publish = 1) {
// Validate the user.
$user = blogapi_validate_user($username, $password);
$old_node = node_load($postid);
$new_node = new stdClass();
if (!$old_node) {
return services_error(t('Node @nid not found', array(
'@nid' => $postid,
)), 404);
}
if (!node_access('update', $old_node, $user)) {
return services_error(t('You do not have permission to update this post.'), 403);
}
// Save the original status for validation of permissions.
$new_node->status = $publish;
$new_node->type = $old_node->type;
$xmlrpc = xmlrpc_server_get();
$api = $xmlrpc->message->methodname;
$type = explode(".", $api);
$apiType = $type[0];
// $content can be empty sometimes, in mt.publishPost for example
if (!empty($content)) {
// Let the teaser be re-generated.
unset($old_node->teaser);
if (is_string($content) || !empty($content['title'])) {
$new_node->title = is_string($content) ? blogapi_blogger_extract_title($content) : $content['title'];
}
if (is_string($content) || !empty($content['description'])) {
$lang = $old_node->language;
if ($apiType === "metaWeblog" || $apiType === "movabletype") {
$new_node->body[$lang][0]['value'] = is_string($content) ? blogapi_blogger_extract_body($content) : $content['description'];
}
else {
$new_node->body[LANGUAGE_NONE][0]['value'] = is_string($content) ? blogapi_blogger_extract_body($content) : $content['description'];
}
}
if (empty($content['date']) && user_access('administer nodes')) {
$new_node->date = format_date($old_node->created, 'custom', 'Y-m-d H:i:s O');
}
if (!empty($content['taxonomies'])) {
foreach ($content['taxonomies'] as $field_name => $field) {
$new_node->{$field_name} = $field;
}
}
if (function_exists('_blogapi_mt_extra') && is_array($content)) {
_blogapi_mt_extra($new_node, $content);
}
}
return blogapi_submit_node($new_node, $old_node);
}
/**
* Creates a new node. Utility function for backend modules.
*/
function blogapi_new_post($username, $password, $postdata) {
// Validate the user.
$user = blogapi_validate_user($username, $password);
if (!node_access('create', $postdata['type'], $user)) {
return services_error(t('You do not have permission to create this type of post.'), 403);
}
blogapi_validate_content_type($postdata['type']);
// @todo make more beautiful reassigning
// Get the node type defaults.
$node_type_default = variable_get('node_options_' . $postdata['type'], array(
'status',
'promote',
));
$node = new stdClass();
$node->type = $postdata['type'];
$node->promote = in_array('promote', $node_type_default);
$node->uid = $user->uid;
$node->status = $postdata['status'];
$node->name = $user->name;
$node->title = $postdata['title'];
$node->language = LANGUAGE_NONE;
$node->body = array(
LANGUAGE_NONE => array(
array(
'format' => filter_default_format($user),
'value' => $postdata['body'],
),
),
);
if (empty($postdata['date']) && user_access('administer nodes')) {
$node->date = format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s O');
}
return blogapi_submit_node($node, $node);
}
/**
* Get vocabularies which are available as taxonomy_term_reference field in given content type.
*/
function blogapi_get_vocabularies_per_content_type($type) {
$vocabularies = array();
foreach (field_info_fields() as $field) {
if ($field['type'] == 'taxonomy_term_reference' && is_array($field['bundles']['node'])) {
foreach ($field['bundles']['node'] as $content_type) {
if ($content_type == $type) {
foreach ($field['settings']['allowed_values'] as $value) {
$vocabularies[] = $value['vocabulary'];
}
}
}
}
}
// Return only unique items, because bundle can have some taxonomy reference fields with the same vocabulary
return array_unique($vocabularies);
}
/**
* Get vocabularies which are available as taxonomy_term_reference field in given content type.
*/
function blogapi_get_node_taxonomy_term_fields($nid) {
$fields = array();
$node = node_load($nid);
foreach (field_info_fields() as $field) {
if ($field['type'] == 'taxonomy_term_reference' && is_array($field['bundles']['node']) && in_array($node->type, $field['bundles']['node'])) {
$fields[] = $field['field_name'];
}
}
return $fields;
}
/**
* Service callback for metaWeblog.getCategories
* @TODO simplify this callback if possible
*/
function blogapi_get_categories($blogid, $username, $password) {
// Validate the user.
blogapi_validate_user($username, $password);
blogapi_validate_content_type($blogid);
$categories = array();
$vocabularies = blogapi_get_vocabularies_per_content_type($blogid);
if (!empty($vocabularies)) {
foreach ($vocabularies as $vocabulary_machine_name) {
$vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_machine_name);
$terms = taxonomy_get_tree($vocabulary->vid);
foreach ($terms as $term) {
$categories[] = array(
'categoryName' => $term->name,
'categoryId' => $term->tid,
);
}
}
}
return $categories;
}
/**
* Service callback for metaWeblog.getCategories
* @TODO simplify this callback if possible
*/
function blogapi_get_node_categories($postid, $username, $password) {
// Validate the user.
blogapi_validate_user($username, $password);
$node = node_load($postid);
if (!$node) {
return services_error(t('Node @nid not found', array(
'@nid' => $postid,
)), 404);
}
if (!node_access('view', $node, $user) || !user_access('administer nodes')) {
// User does not have permission to view the node.
return services_error(t('You are not authorized to view post @postid', array(
'@postid' => $postid,
)), 403);
}
$taxonomy_fields = blogapi_get_node_taxonomy_term_fields($node->nid);
$categories = array();
if (!empty($taxonomy_fields)) {
foreach ($taxonomy_fields as $field) {
$terms = field_get_items('node', $node, $field);
if (!empty($terms)) {
foreach ($terms as $term) {
$term = taxonomy_term_load($term['tid']);
$categories[] = array(
'categoryName' => $term->name,
'categoryId' => $term->tid,
'isPrimary' => TRUE,
);
}
}
}
}
return $categories;
}
/**
* Emulates node_form submission as services module does
*
* @param $new_node
* Object, data to be submitted to node_form
* @param $old_node
* Object, old node data to be submitted to node_form.
* May be the same as $new_node, if it is node creation
* @return bool|mixed
* TRUE if node was updated or $node->nid if $node was created
*/
function blogapi_submit_node($new_node, $old_node) {
$is_new_node = empty($old_node->nid);
blogapi_status_error_check($new_node);
// Load the required includes for drupal_execute
module_load_include('inc', 'node', 'node.pages');
if (!$is_new_node) {
node_object_prepare($old_node);
module_invoke_all('blogapi_node_edit', $new_node);
}
else {
module_invoke_all('blogapi_node_create', $new_node);
}
// Setup form_state.
$form_state = array();
$form_state['values'] = (array) $new_node;
$form_state['values']['op'] = t('Save');
if (!$is_new_node) {
$form_state['node'] = $old_node;
}
drupal_form_submit($old_node->type . '_node_form', $form_state, $old_node);
if ($errors = form_get_errors()) {
return services_error(implode(" ", $errors), 406, array(
'form_errors' => $errors,
));
}
else {
watchdog('content', '@type: updated %title using Blog API.', array(
'@type' => $old_node->type,
'%title' => $new_node->title,
), WATCHDOG_NOTICE, l(t('view'), "node/{$old_node->nid}"));
return $is_new_node ? $form_state['nid'] : TRUE;
}
}
/**
* Get all taxonomy fields and their settings to get vocabulraies
*
* @param array $types list of bundles
*
* @return array(
* field_name => field_settings
* ...
* )
*/
function blogapi_get_taxonomy_term_reference_fields_with_vocabularies($types = array()) {
$query = db_select('field_config', 'fc');
$query
->join('field_config_instance', 'fci', 'fc.id = fci.field_id');
$query
->fields('fc', array(
'field_name',
'data',
))
->condition('fc.type', 'taxonomy_term_reference');
if (!empty($types)) {
$query
->condition('fci.bundle', $types, 'IN');
}
$results = $query
->execute()
->fetchAllKeyed();
if (!empty($results)) {
foreach ($results as &$result) {
$result = unserialize($result);
}
}
return $results;
}
Functions
Name | Description |
---|---|
blogapi_ctools_plugin_api | Implements hook_ctools_plugin_api(). |
blogapi_default_services_endpoint | Implements hook_default_services_endpoint(). |
blogapi_edit_post | Service allback for metaWeblog.editPost |
blogapi_format_post_for_xmlrpc | Helper function. Adds appropriate metadata to the XML-RPC return values. |
blogapi_get_api_types | Get a list of API types currently available to BlogAPI. |
blogapi_get_categories | Service callback for metaWeblog.getCategories @TODO simplify this callback if possible |
blogapi_get_info | Get all BlogAPI information, optionally filtered by API type |
blogapi_get_node_categories | Service callback for metaWeblog.getCategories @TODO simplify this callback if possible |
blogapi_get_node_taxonomy_term_fields | Get vocabularies which are available as taxonomy_term_reference field in given content type. |
blogapi_get_node_types | Helper function. Get BlogAPI node types. |
blogapi_get_recent_posts | Helper function. Returns the latest few nodes created by a given user. |
blogapi_get_taxonomy_term_reference_fields_with_vocabularies | Get all taxonomy fields and their settings to get vocabulraies |
blogapi_get_vocabularies_per_content_type | Get vocabularies which are available as taxonomy_term_reference field in given content type. |
blogapi_init | Implements hook_init(). |
blogapi_menu | Implements hook_menu(). |
blogapi_new_post | Creates a new node. Utility function for backend modules. |
blogapi_permission | Implements hook_permission(). |
blogapi_rsd | Return a BlogAPI RSD for XML-RPC APIs |
blogapi_space_used | Helper function. Return the amount of space used by a given user. |
blogapi_status_error_check | Check that the user has permission to save the node with the chosen status. |
blogapi_submit_node | Emulates node_form submission as services module does |
blogapi_validate_content_type | Validate that a content type is configured to work with BlogAPI |
blogapi_validate_terms | Helper function. Find allowed taxonomy terms for a node type. |
blogapi_validate_user | Ensure that a given user has permission to use BlogAPI |