You are here

blogapi.module in Blog API 7.2

Same filename and directory in other branches
  1. 8 blogapi.module
  2. 7 blogapi.module

Enable users to post using applications that support BlogAPIs.

File

blogapi.module
View 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

Namesort descending 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