You are here

yoast_seo.module in Real-time SEO for Drupal 7

Same filename and directory in other branches
  1. 8.2 yoast_seo.module
  2. 8 yoast_seo.module

Primary hook implementations for Yoast SEO for Drupal module.

File

yoast_seo.module
View source
<?php

/**
 * @file
 * Primary hook implementations for Yoast SEO for Drupal module.
 */

/**
 * Implements hook_permission().
 */
function yoast_seo_permission() {
  $permissions['administer yoast seo'] = array(
    'title' => t('Administer Real-time SEO for Drupal'),
    'restrict access' => TRUE,
    'description' => t('Control the main settings pages and configure the settings per content types.'),
  );
  $permissions['use yoast seo'] = array(
    'title' => t('Use Real-time SEO for Drupal'),
    'description' => t('Modify Realtime SEO for Drupal configuration per individual nodes.'),
  );
  return $permissions;
}

/**
 * Implements hook_menu().
 */
function yoast_seo_menu() {
  $items['admin/config/search/yoast'] = array(
    'title' => 'Real-time SEO for Drupal',
    'description' => 'Configure Real-time SEO for Drupal settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'yoast_seo_admin_settings_form',
    ),
    'access arguments' => array(
      'administer yoast seo',
    ),
    'file' => 'includes/yoast_seo.admin.inc',
  );
  return $items;
}

/**
 * Implements hook_views_api().
 */
function yoast_seo_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'yoast_seo') . '/views',
  );
}

/**
 * Build a FAPI array for editing meta tags.
 *
 * @param array $form
 *   The current FAPI array.
 * @param string $instance
 *   The configuration instance key to use, e.g. "node:article".
 * @param array $options
 *   (optional) An array of options including the following keys and values:
 *   - token types: An array of token types to be passed to theme_token_tree().
 */
function yoast_seo_configuration_form(array &$form, $instance, array $options = array()) {

  // Making sure we have the necessary form elements and we have access to the
  // form elements. Otherwise we're executing code we'll never use.
  if (!empty($form['path']['#access']) && !empty($form['metatags']['#access']) && (user_access('use yoast seo') || user_access('administer yoast seo'))) {

    // Work out the language code to use, default to NONE.
    if (!empty($form['#entity_type'])) {
      if (!empty($form['#entity'])) {
        $langcode = entity_language($form['#entity_type'], (object) $form['#entity']);
      }
      if (empty($langcode)) {
        $langcode = LANGUAGE_NONE;
      }
    }

    // Merge in the default options.
    $options += array(
      'instance' => $instance,
    );
    $form['yoast_seo'] = array(
      '#type' => 'container',
      '#title' => t('Real-time SEO'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#multilingual' => TRUE,
      '#tree' => TRUE,
      '#weight' => 35,
      '#language' => $langcode,
      '#attributes' => array(
        'class' => array(
          'yoast-seo-form',
        ),
      ),
    );
    $form['yoast_seo'][$langcode] = array(
      '#type' => 'container',
      '#multilingual' => TRUE,
      '#tree' => TRUE,
    );

    // Put Yoast in a vertical tab if setting is set.
    $yoast_seo_vertical_tab = variable_get('yoast_seo_vertical_tab');
    if (!empty($yoast_seo_vertical_tab)) {
      $form['yoast_seo']['#type'] = 'fieldset';
    }

    // Only support vertical tabs if there is a vertical tab element.
    foreach (element_children($form) as $key) {
      if (isset($form[$key]['#type']) && $form[$key]['#type'] == 'vertical_tabs') {
        $form['yoast_seo']['#group'] = $key;
        break;
      }
    }
    $metatag_config = metatag_config_load_with_defaults($options['instance']);
    $new_entity = empty($form['#entity']->nid) ? TRUE : FALSE;

    // Add the focus keyword field.
    $form['yoast_seo'][$langcode]['focus_keyword'] = array(
      '#type' => 'textfield',
      '#title' => t('Focus keyword'),
      '#description' => t('Pick the main keyword or keyphrase that this post/page is about.'),
      '#default_value' => !empty($form['#entity']->yoast_seo[$langcode]['focus_keyword']) ? $form['#entity']->yoast_seo[$langcode]['focus_keyword'] : '',
      '#id' => drupal_html_id('focus-keyword'),
    );

    // Add the SEO status field.
    $form['yoast_seo'][$langcode]['seo_status'] = array(
      '#type' => 'hidden',
      '#default_value' => !empty($form['#entity']->yoast_seo[$langcode]['seo_status']) ? $form['#entity']->yoast_seo[$langcode]['seo_status'] : 0,
      '#attributes' => array(
        'id' => drupal_html_id('seo-status'),
      ),
    );

    // Set placeholder tekst if there is a default in metatags and it is a new
    // entity. This means we have some default configuration for the title.
    if (!empty($form['metatags'][$langcode]['basic']['title'])) {

      // Checking to see if we have to empty the default value or not.
      if ($form['metatags'][$langcode]['basic']['title']['value']['#default_value'] == $metatag_config['title']['value']) {
        if ($new_entity) {
          $form['metatags'][$langcode]['basic']['title']['value']['#default_value'] = $metatag_config['title']['value'];
        }
        else {
          $form['metatags'][$langcode]['basic']['title']['value']['#default_value'] = token_replace($metatag_config['title']['value'], array(
            'node' => $form['#entity'],
          ));
        }
      }
    }

    // Set placeholder tekst if there is a default in metatags and it is a new
    // entity. This means we have some default configuration for the
    // description.
    if (!empty($form['metatags'][$langcode]['basic']['description']) && isset($metatag_config['description'])) {

      // Checking to see if we have to empty the default value or not.
      if ($form['metatags'][$langcode]['basic']['description']['value']['#default_value'] == $metatag_config['description']['value']) {
        if ($new_entity) {
          $form['metatags'][$langcode]['basic']['description']['value']['#default_value'] = $metatag_config['description']['value'];
        }
        else {
          $form['metatags'][$langcode]['basic']['description']['value']['#default_value'] = strip_tags(token_replace($metatag_config['description']['value'], array(
            'node' => $form['#entity'],
          )));
        }
      }
    }

    // Add a form after build to add the JavaScript.
    $form['#after_build'][] = 'yoast_seo_configuration_form_after_build';

    // Add a submit handler to compare the submitted values against the default
    // values.
    $form += array(
      '#submit' => array(),
    );
    array_unshift($form['#submit'], 'yoast_seo_configuration_form_submit');
  }
}

/**
 * Actually we need access to all fields listed or selected.
 *
 * @param array $form
 *
 * @return bool
 */
function _yoast_check_fields_access(array $form) {
  $return = array();
  $bundle = $form['#bundle'];
  $required_fields = array(
    'yoast_seo',
    'path',
    'metatags',
  );
  $fields = variable_get('yoast_seo_body_fields_' . $bundle, array());
  $default_fields = array_merge($required_fields, $fields);
  if (isset($form['body'])) {
    $return[] = $form['body']['#access'];
  }
  elseif (empty($fields)) {

    // We don't have body and fields selected.
    $return[] = FALSE;
  }
  foreach ($default_fields as $field) {
    $return[] = !empty($form[$field]['#access']);
  }
  return array_sum($return) === count($return);
}

/**
 * Form API submission callback.
 *
 * Moving submitted values back into the metatag fields.
 */
function yoast_seo_configuration_form_submit($form, &$form_state) {
  if (!empty($form_state['values']['metatags'])) {
    $langcode = $form_state['values']['language'];

    // Moving the title field value.
    if (isset($form_state['values']['metatags'][$langcode]['title']['value']) && isset($form_state['values']['metatags'][$langcode]['title']['default'])) {
      $title = trim($form_state['values']['metatags'][$langcode]['title']['value']);

      // If the title is the placeholder we had before, we would like to replace
      // it with Metatag defaults.
      if (empty($title)) {
        $form_state['values']['metatags'][$langcode]['title']['value'] = $form_state['values']['metatags'][$langcode]['title']['default'];
      }
    }

    // Moving the description field value.
    if (isset($form_state['values']['metatags'][$langcode]['description']['value']) && isset($form_state['values']['metatags'][$langcode]['description']['default'])) {
      $description = trim($form_state['values']['metatags'][$langcode]['description']['value']);

      // If the description is the placeholder we had before, we would like to
      // replace it with Metatag defaults.
      if (empty($description)) {
        $form_state['values']['metatags'][$langcode]['description']['value'] = $form_state['values']['metatags'][$langcode]['description']['default'];
      }
    }
  }
}

/**
 * In this afterbuild function we add the configuration to the JavaScript.
 *
 * At this point we have access to the HTML id's of form elements we need for
 * the JavaScript to function.
 */
function yoast_seo_configuration_form_after_build($form, &$form_state) {

  // Work out the language code to use, default to NONE. This is used on the
  // yoast_seo settings per language and for metatags..
  if (!empty($form['#entity_type'])) {
    if (!empty($form['#entity'])) {
      $langcode = entity_language($form['#entity_type'], (object) $form['#entity']);
    }
  }
  if (empty($langcode)) {
    $langcode = LANGUAGE_NONE;
  }

  // Lets put the container in the bottom of page, above additional_settings
  // tabs.
  $form['yoast_seo']['#weight'] = $form['additional_settings']['#weight'] - 2;

  // Generate unique HTML IDs.
  $wrapper_target_id = drupal_html_id('yoast-wrapper');
  $output_target_id = drupal_html_id('yoast-output');
  $overallscore_target_id = drupal_html_id('yoast-overallscore');
  $snippet_target_id = drupal_html_id('yoast-snippet');
  $score = !empty($form['yoast_seo'][$langcode]['seo_status']['#value']) ? yoast_seo_score_rating($form['yoast_seo'][$langcode]['seo_status']['#value']) : yoast_seo_score_rating(0);

  // Add the Yoast SEO title before the wrapper.
  $form['yoast_seo'][$langcode]['focus_keyword']['#prefix'] = '<h3 class="wrapper-title">' . t('Real-time SEO for Drupal') . '</h3>';

  // Output the overall score next to the focus keyword.
  $form['yoast_seo'][$langcode]['focus_keyword']['#suffix'] = '<div id="yoast-overallscore" class="overallScore ' . $score . '">
  <div class="score_circle"></div>
  <span class="score_title">' . t('SEO') . ': <strong>' . $score . '</strong></span>
  </div>';

  // Output the markup for the snippet and overall score above the body field.
  $form['yoast_seo'][$langcode]['snippet_analysis'] = array(
    '#weight' => $form['yoast_seo']['#weight'] - 1,
    '#markup' => '<div id="' . $wrapper_target_id . '"><div id="wpseo_meta"></div><label>' . t('Snippet editor') . '</label><div id="' . $snippet_target_id . '"></div><label>' . t('Content analysis') . '</label><div id="' . $output_target_id . '"></div></div>',
  );

  // Minified JS file.
  drupal_add_js(yoast_seo_library_path('js-text-analysis') . '/yoast-seo.min.js', array(
    'type' => 'external',
    'scope' => 'footer',
  ));

  // Our own JavaScript.
  drupal_add_js(drupal_get_path('module', 'yoast_seo') . '/js/yoast_seo.js');

  // Our own CSS.
  drupal_add_css(drupal_get_path('module', 'yoast_seo') . '/css/yoast_seo.css');
  global $base_root;
  $default_url = '';
  if (!empty($form['path']['alias']['#default_value'])) {
    $default_url = $form['path']['alias']['#default_value'];
  }
  elseif (!empty($form['path']['source']['#value'])) {
    $default_url = $form['path']['source']['#value'];
  }

  // Check if the SEO title field is overwritten.
  $seo_title_overwritten = FALSE;
  if (!empty($form['metatags'][$langcode]['basic']['title']['value']['#default_value'])) {
    $seo_title_overwritten = TRUE;
  }

  // Collect all body fields and field id's.
  $text_content = array();
  if (isset($form['body'])) {

    // We use the language of the body element so we know where to add the
    // correct yoast_seo settings / fields and elements.
    $body_language = $form['body']['#language'];
    $text_content += _yoast_seo_process_field($form['body'][$body_language]);
  }
  $yoast_fields = variable_get('yoast_seo_body_fields_' . $form['#bundle'], array());
  foreach ($yoast_fields as $yoast_field) {

    // We use the language of the field element.
    $field_language = $form[$yoast_field]['#language'];
    $text_content += _yoast_seo_process_field($form[$yoast_field][$field_language]);
  }

  // Create the configuration we will sent to the content analysis library.
  $configuration = array(
    'analyzer' => TRUE,
    'snippetPreview' => TRUE,
    'targetId' => $wrapper_target_id,
    'targets' => array(
      'output' => $output_target_id,
      'overall' => $overallscore_target_id,
      'snippet' => $snippet_target_id,
    ),
    'defaultText' => array(
      'url' => $default_url,
      'title' => !empty($form['metatags'][$langcode]['basic']['title']['value']['#default_value']) ? $form['metatags'][$langcode]['basic']['title']['value']['#default_value'] : '',
      'keyword' => !empty($form['yoast_seo'][$langcode]['focus_keyword']['#default_value']) ? $form['yoast_seo'][$langcode]['focus_keyword']['#default_value'] : '',
      'meta' => !empty($form['metatags'][$langcode]['basic']['description']['value']['#default_value']) ? $form['metatags'][$langcode]['basic']['description']['value']['#default_value'] : '',
      'body' => trim(implode(PHP_EOL, $text_content)),
    ),
    'fields' => array(
      'focusKeyword' => $form['yoast_seo'][$langcode]['focus_keyword']['#id'],
      'seoStatus' => $form['yoast_seo'][$langcode]['seo_status']['#attributes']['id'],
      'pageTitle' => module_exists('seo_ui') ? $form['seo_vtab']['metatags_title']['title']['value']['#id'] : $form['metatags'][$langcode]['basic']['title']['value']['#id'],
      'nodeTitle' => $form['title']['#id'],
      'description' => module_exists('seo_ui') ? $form['seo_vtab']['metatags'][$langcode]['basic']['description']['value']['#id'] : $form['metatags'][$langcode]['basic']['description']['value']['#id'],
      'bodyText' => array_keys($text_content),
      'url' => module_exists('seo_ui') ? $form['seo_vtab']['path']['alias']['#id'] : $form['path']['alias']['#id'],
    ),
    'placeholderText' => array(
      'title' => t('Please click here to alter your page meta title'),
      'description' => t('Please click here and alter your page meta description.'),
      'url' => t('example-post'),
    ),
    'SEOTitleOverwritten' => $seo_title_overwritten,
    'textFormat' => !empty($form['body'][$langcode][0]['format']['#id']) ? $form['body'][$langcode][0]['format']['#id'] : '',
    'baseRoot' => $base_root,
  );

  // Allow other modules to alter the configuration we sent to the content
  // analysis library by implementing hook_yoast_seo_configuration_alter().
  drupal_alter('yoast_seo_configuration', $configuration);

  // Add the configuration to the front-end for the content analysis library.
  drupal_add_js(array(
    'yoast_seo' => $configuration,
  ), 'setting');
  return $form;
}

/**
 * Helper function that extracts field values and id's.
 *
 * @param array $field
 *
 * @return array
 */
function _yoast_seo_process_field(array $field) {
  $fields_content = array();
  if ((int) $field['#cardinality'] === 1) {

    // Single filed
    // Store the id of field, it can be filled later and we need them for js.
    $fields_content[$field[0]['value']['#id']] = '';
    if (isset($field[0]['value']['#default_value']) && !empty($field[0]['value']['#default_value'])) {
      $fields_content[$field[0]['value']['#id']] = $field[0]['value']['#default_value'];
    }
  }
  else {

    // Multi field.
    for ($i = 0; $i <= $field['#max_delta']; $i++) {
      $fields_content[$field[$i]['value']['#id']] = '';
      if (isset($field[$i]['value']['#default_value']) && !empty($field[$i]['value']['#default_value'])) {
        $fields_content[$field[$i]['value']['#id']] = $field[$i]['value']['#default_value'];
      }
    }
  }
  return $fields_content;
}

/**
 * Implements hook_field_attach_form().
 */
function yoast_seo_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {

  // Entity_Translation will trigger this hook again, skip it.
  if (!empty($form_state['entity_translation']['is_translation'])) {
    return;
  }
  list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
  if (!yoast_seo_entity_supports_yoast_seo($entity_type, $bundle)) {
    return;
  }
  $instance = "{$entity_type}:{$bundle}";
  $options['token types'] = array(
    token_get_entity_mapping('entity', $entity_type),
  );
  $options['context'] = $entity_type;

  // Allow hook_metatag_token_types_alter() to modify the defined tokens. We do
  // this because we are using metatags own fields.
  drupal_alter('metatag_token_types', $options);
  $form['#yoast_seo'] = array(
    'instance' => $instance,
    'options' => $options,
  );
}

/**
 * Implements hook_form_alter().
 *
 * @todo Remove this when https://www.drupal.org/node/1284642 is fixed in core.
 */
function yoast_seo_form_alter(&$form, $form_state, $form_id) {
  if (!empty($form['#yoast_seo']) && !isset($form['yoast_seo'])) {
    extract($form['#yoast_seo']);
    yoast_seo_configuration_form($form, $instance, $options);
    unset($form['#yoast_seo']);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add the custom Yoast score to the overview if the user has access.
 */
function yoast_seo_form_node_admin_content_alter(&$form, &$form_state, $form_id) {
  if (user_access('use yoast seo')) {

    // Add our own CSS.
    drupal_add_css(drupal_get_path('module', 'yoast_seo') . '/css/yoast_seo.css');
    if (!empty($form['admin']['nodes']['#options'])) {

      // Get all the current options node nids.
      $nids = array_keys($form['admin']['nodes']['#options']);

      // Add our custom SEO score header to the admin overview.
      $form['admin']['nodes']['#header']['yoast_seo'] = t('SEO score');

      // For every result, we fetch the score from the database on nid.
      foreach (entity_load('node', $nids) as $node) {
        list($nid, , $bundle) = entity_extract_ids('node', $node);

        // Check if entity has SEO availability otherwise show a message.
        if (yoast_seo_entity_supports_yoast_seo('node', $bundle)) {

          // Score will be either 0 or a higher int. 0 is default.
          $score = yoast_seo_get_score($nid);

          // Class will represent classname for current score. Like poor or bad
          // it's used for theming purposes.
          $class = yoast_seo_score_rating($score);

          // Add Yoast score to the overview.
          $form['admin']['nodes']['#options'][$nid]['yoast_seo'] = '<div id="yoast-overallscore" class="overallScore ' . $class . '"><div class="score_circle"></div></div>';
        }
        else {
          $form['admin']['nodes']['#options'][$nid]['yoast_seo'] = '';
        }
      }
    }
  }
}

/**
 * Load an entity's SEO info.
 *
 * @param string $entity_type
 *   The entity type to load.
 * @param int $entity_id
 *   The ID of the entity to load.
 *
 * @return array
 *   An array of SEO info data keyed by revision ID and language.
 */
function yoast_seo_configuration_load($entity_type, $entity_id) {
  $seo_info = yoast_seo_configuration_load_multiple($entity_type, array(
    $entity_id,
  ));
  return !empty($seo_info) ? reset($seo_info) : array();
}

/**
 * Custom function which will get the SEO score from a nid.
 *
 * @param int $entity_id
 *   The entity id of the score we need to load.
 *
 * @return int
 *   An int representing the SEO score.
 */
function yoast_seo_get_score($entity_id) {

  // Get the SEO score for this entity.
  $result = db_select('yoast_seo', 'y')
    ->fields('y', array(
    'seo_status',
  ))
    ->condition('y.entity_id', $entity_id)
    ->execute()
    ->fetchAssoc();
  return !empty($result['seo_status']) ? $result['seo_status'] : '0';
}

/**
 * Custom function to build up score div, same as in scoreToRating.js.
 *
 * @param int $score
 *   The score of the node.
 *
 * @return string
 *   A string that we can use as classname and automatically will be styled.
 */
function yoast_seo_score_rating($score) {
  $score_rate = '';
  if ($score <= 4) {
    $score_rate = "bad";
  }
  if ($score > 4 && $score <= 7) {
    $score_rate = "ok";
  }
  if ($score > 7) {
    $score_rate = "good";
  }
  if ($score == 0) {
    $score_rate = "na";
  }
  return $score_rate;
}

/**
 * Load SEO info for multiple entities.
 *
 * @param string $entity_type
 *   The entity type to load.
 * @param array $entity_ids
 *   The list of entity IDs.
 *
 * @return array
 *   An array of SEO info data, keyed by entity ID, revision ID and language.
 */
function yoast_seo_configuration_load_multiple($entity_type, array $entity_ids, array $revision_ids = array()) {

  // Double check entity IDs are numeric thanks to Entity API module.
  $entity_ids = array_filter($entity_ids, 'is_numeric');
  if (empty($entity_ids)) {
    return array();
  }

  // Also need to check if the Yoast SEO table exists since this condition could
  // fire before the table has been installed yet.
  if (!db_table_exists('yoast_seo')) {
    watchdog('yoast_seo', 'The system tried to load Real-time SEO for Drupal data before the schema was fully loaded.', array(), WATCHDOG_WARNING);
    return array();
  }

  // Get all translations of data for this entity.
  $query = db_select('yoast_seo', 'y')
    ->fields('y', array(
    'entity_id',
    'revision_id',
    'language',
    'focus_keyword',
    'seo_status',
  ))
    ->condition('y.entity_type', $entity_type)
    ->orderBy('entity_id')
    ->orderBy('revision_id');

  // Filter by revision_ids if they are available. If not, filter by entity_ids.
  if (!empty($revision_ids)) {
    $query
      ->condition('y.revision_id', $revision_ids, 'IN');
  }
  else {
    $query
      ->condition('y.entity_id', $entity_ids, 'IN');
  }
  $result = $query
    ->execute();

  // Marshal it into an array keyed by entity ID. Each value is an array of
  // translations keyed by language code.
  $seo_info = array();
  while ($record = $result
    ->fetchObject()) {
    $data = array(
      'focus_keyword' => $record->focus_keyword,
      'seo_status' => $record->seo_status,
    );
    $seo_info[$record->entity_id][$record->revision_id][$record->language] = $data;
  }
  return $seo_info;
}

/**
 * Implements hook_entity_load().
 */
function yoast_seo_entity_load($entities, $entity_type) {
  if (yoast_seo_entity_supports_yoast_seo($entity_type)) {

    // Get the revision_ids.
    $revision_ids = array();

    // Track the entity IDs for values to load.
    $entity_ids = array();

    // Some entities don't support revisions.
    $supports_revisions = TRUE;

    // Extract the revision ID and verify the entity's bundle is supported.
    foreach ($entities as $key => $entity) {
      list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);

      // Verify that each entity bundle supports Yoast SEO.
      if (yoast_seo_entity_supports_yoast_seo($entity_type, $bundle)) {
        $entity_ids[] = $entity_id;
        if (!empty($revision_id)) {
          $revision_ids[] = $revision_id;
        }
      }
    }

    // Only proceed if either there were revision IDs identified, or the
    // entity doesn't support revisions anyway.
    if (!empty($entity_ids)) {

      // Load all SEO info for these entities.
      $seo_info = yoast_seo_configuration_load_multiple($entity_type, $entity_ids, $revision_ids);

      // Assign the metatag records for the correct revision ID.
      if (!empty($seo_info)) {
        foreach ($entities as $entity_id => $entity) {
          list($entity_id, $revision_id) = entity_extract_ids($entity_type, $entity);
          $revision_id = intval($revision_id);
          $entities[$entity_id]->yoast_seo = isset($seo_info[$entity_id][$revision_id]) ? $seo_info[$entity_id][$revision_id] : array();
        }
      }
    }
  }
}

/**
 * Implements hook_entity_insert().
 */
function yoast_seo_entity_insert($entity, $entity_type) {
  if (isset($entity->yoast_seo)) {
    list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);

    // Verify that this entity type / bundle is supported.
    if (!yoast_seo_entity_supports_yoast_seo($entity_type, $bundle)) {
      return;
    }
    $revision_id = intval($revision_id);

    // Determine the entity's language.
    $langcode = entity_language($entity_type, $entity);

    // Unfortunately due to how core works, the normal entity_language()
    // function returns 'und' instead of the node's language during node
    // creation.
    if ((empty($langcode) || $langcode == LANGUAGE_NONE) && !empty($entity->language)) {
      $langcode = $entity->language;
    }

    // If no language was still found, use the 'no language' value.
    if (empty($langcode)) {
      $langcode = LANGUAGE_NONE;
    }

    // Work-around for initial entity creation where a language was selection
    // but where it's different to the form's value.
    if (!isset($entity->yoast_seo[$langcode]) && isset($entity->yoast_seo[LANGUAGE_NONE])) {
      $entity->yoast_seo[$langcode] = $entity->yoast_seo[LANGUAGE_NONE];
      unset($entity->yoast_seo[LANGUAGE_NONE]);
    }
    yoast_seo_configuration_save($entity_type, $entity_id, $revision_id, $entity->yoast_seo, $langcode);
  }
}

/**
 * Implements hook_entity_update().
 */
function yoast_seo_entity_update($entity, $entity_type) {
  list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);

  // If this entity object isn't allowed meta tags, don't continue.
  if (!yoast_seo_entity_supports_yoast_seo($entity_type, $bundle)) {
    return;
  }
  $revision_id = intval($revision_id);
  if (isset($entity->yoast_seo)) {

    // Determine the entity's new language. This will always be accurate as the
    // language value will already have been updated by the time this function
    // executes, and it will be loaded for the correct edit process.
    $new_language = entity_language($entity_type, (object) $entity);
    if (empty($new_language)) {
      $new_language = LANGUAGE_NONE;
    }

    // If applicable, determine the entity's original language. This cannot be
    // obtained via the normal API as that data will already have been updated,
    // instead check to see if the entity has an old-fasioned 'language' value.
    if (isset($entity->original) && isset($entity->language) && isset($entity->original->language)) {
      $old_language = $entity->original->language;

      // If the language has changed then additional checking needs to be done.
      // Need to compare against the entity's raw language value as they will
      // be different when updating a translated entity, versus an untranslated
      // entity or a source entity for translation, and give a false positive.
      if ($new_language == $entity->language && $new_language != $old_language) {

        // If this entity is not translated, or if it is translated but the
        // translation was previously created, then some language cleanup needs
        // to be done.
        if (!isset($entity->translation) || isset($entity->translation) && !empty($entity->translation['created'])) {

          // Delete the old language record. This will not affect old revisions.
          db_delete('yoast_seo')
            ->condition('entity_type', $entity_type)
            ->condition('entity_id', $entity_id)
            ->condition('revision_id', $revision_id)
            ->condition('language', $old_language)
            ->execute();

          // Swap out the SEO values for the two languages.
          if (isset($entity->yoast_seo[$old_language])) {
            $entity->yoast_seo[$new_language] = $entity->yoast_seo[$old_language];
            unset($entity->yoast_seo[$old_language]);
          }
        }
      }
    }

    // Save the record.
    $old_vid = isset($entity->old_vid) ? $entity->old_vid : NULL;
    yoast_seo_configuration_save($entity_type, $entity_id, $revision_id, $entity->yoast_seo, $new_language, $old_vid);
  }
}

/**
 * Implements hook_entity_delete().
 */
function yoast_seo_entity_delete($entity, $type) {

  // Currently we only support nodes, so checking for a node ID seems like a
  // good idea.
  if (!empty($entity->nid)) {

    // Delete the record from our table.
    db_delete('yoast_seo')
      ->condition('entity_type', $type)
      ->condition('entity_id', $entity->nid)
      ->execute();
  }
}

/**
 * Save an entity's SEO settings.
 *
 * @param string $entity_type
 *   The entity type to load.
 * @param int $entity_id
 *   The entity's ID.
 * @param int $revision_id
 *   The entity's VID.
 * @param array $seo_info
 *   All of the SEO information.
 * @param string $langcode
 *   The language of the translation set.
 * @param int $old_vid
 *   (optional) The old revision.
 */
function yoast_seo_configuration_save($entity_type, $entity_id, $revision_id, $seo_info, $langcode, $old_vid = NULL) {

  // If no language assigned, or the language doesn't exist, use the
  // has-no-language language.
  $languages = language_list();
  if (empty($langcode) || !isset($languages[$langcode])) {
    $langcode = LANGUAGE_NONE;
  }

  // Check that $entity_id is numeric because of Entity API and string IDs.
  if (!is_numeric($entity_id)) {
    return;
  }

  // The revision_id must be a numeric value; some entities use NULL for the
  // revision so change that to a zero.
  if (is_null($revision_id)) {
    $revision_id = 0;
  }

  // Handle scenarios where the seo info is completely empty.
  if (empty($seo_info)) {
    $seo_info = array();

    // Add an empty array record for each language.
    $languages = db_query("SELECT language, ''\n        FROM {yoast_seo}\n        WHERE (entity_type = :type)\n        AND (entity_id = :id)\n        AND (revision_id = :revision)", array(
      ':type' => $entity_type,
      ':id' => $entity_id,
      ':revision' => $revision_id,
    ))
      ->fetchAllKeyed();
    foreach ($languages as $oldlang => $empty) {
      $seo_info[$oldlang] = array();
    }
  }

  // Update each of the per-language SEO info configurations in turn.
  foreach ($seo_info as $langcode => $new_seo_info) {

    // If the data array is empty, there is no data to actually save, so just
    // delete the record from the database.
    if (empty($new_seo_info)) {
      db_delete('yoast_seo')
        ->condition('entity_type', $entity_type)
        ->condition('entity_id', $entity_id)
        ->condition('revision_id', $revision_id)
        ->condition('language', $langcode)
        ->execute();
    }
    else {
      db_merge('yoast_seo')
        ->key(array(
        'entity_type' => $entity_type,
        'entity_id' => $entity_id,
        'language' => $langcode,
        'revision_id' => $revision_id,
      ))
        ->fields(array(
        'focus_keyword' => $new_seo_info['focus_keyword'],
        'seo_status' => $new_seo_info['seo_status'],
      ))
        ->execute();
    }
  }
}

/**
 * Implements hook_node_type_delete().
 *
 * Remove configuration when the content type is removed.
 */
function yoast_seo_node_type_delete($info) {
  variable_del('yoast_seo_enable_node__' . $info->type);
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds extra settings to the node content type edit page.
 */
function yoast_seo_form_node_type_form_alter(&$form, $form_state) {
  form_load_include($form_state, 'inc', 'yoast_seo', 'includes/yoast_seo.forms');
  _yoast_seo_process_node_settings_form($form);
}

/**
 * Check whether the requested entity type (and bundle) support Yoast SEO.
 *
 * By default the entities are disabled, only certain entities will have been
 * enabled during installation. If an entity type is enabled it is assumed that
 * the entity bundles will also be enabled by default.
 *
 * @see metatag_entity_supports_metatags()
 */
function yoast_seo_entity_supports_yoast_seo($entity_type = NULL, $bundle = NULL) {
  $entity_types =& drupal_static(__FUNCTION__);

  // Identify which entities & bundles are supported the first time the
  // function is called.
  if (!isset($entity_types)) {
    foreach (entity_get_info() as $entity_name => $entity_info) {

      // The entity type technically supports entities.
      // @todo, make this more generic when we are supporting more entities.
      if ($entity_name == 'node') {

        // Entity types are enabled by default.
        // Allow entities to be disabled by assigning a variable
        // 'metatag_enable_{$entity_type}' the value FALSE, e.g.:
        //
        // // Disable metatags for file_entity.
        // $conf['metatag_enable_file'] = FALSE;
        // .
        // @see Settings page.
        if (variable_get('yoast_seo_enable_' . $entity_name, FALSE) == FALSE) {
          $entity_types[$entity_name] = FALSE;
        }
        else {
          $entity_types[$entity_name] = array();
          foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {

            // Allow bundles to be disabled by assigning a variable
            // 'yoast_seo_enable_{$entity_type}__{$bundle}' the value FALSE,
            // e.g.:
            if (variable_get('yoast_seo_enable_' . $entity_name . '__' . $bundle_name, TRUE) == FALSE) {
              $entity_types[$entity_name][$bundle_name] = FALSE;
            }
            else {
              $entity_types[$entity_name][$bundle_name] = TRUE;
            }
          }
        }
      }
    }
  }

  // It was requested to check a specific entity.
  if (isset($entity_type)) {

    // It was also requested to check a specific bundle for this entity.
    if (isset($bundle)) {
      $supported = !empty($entity_types[$entity_type][$bundle]);
    }
    else {
      $supported = !empty($entity_types[$entity_type]);
    }
    return $supported;
  }

  // If nothing specific was requested, return the complete list of supported
  // entities & bundles.
  return $entity_types;
}

/**
 * Enable support for a specific entity type.
 *
 * @param string $entity_type
 *   The entity's type.
 * @param string $bundle
 *   The entity's bundle.
 */
function yoast_seo_entity_type_enable($entity_type, $bundle = NULL) {

  // The bundle was defined.
  if (isset($bundle)) {
    variable_set('yoast_seo_enable_' . $entity_type . '__' . $bundle, TRUE);
  }

  // Always enable the entity type, because otherwise there's no point in
  // enabling the bundle.
  variable_set('yoast_seo_enable_' . $entity_type, TRUE);

  // Clear the static cache so that the entity type / bundle will work.
  drupal_static_reset('yoast_seo_entity_supports_yoast_seo');
}

/**
 * Disable support for a specific entity type.
 *
 * @param string $entity_type
 *   The entity's type.
 * @param string $bundle
 *   The entity's bundle.
 */
function yoast_seo_entity_type_disable($entity_type, $bundle = NULL) {

  // The bundle was defined.
  if (isset($bundle)) {
    variable_set('yoast_seo_enable_' . $entity_type . '__' . $bundle, FALSE);
  }
  else {
    variable_set('yoast_seo_enable_' . $entity_type, FALSE);
  }

  // Clear the static cache so that the entity type / bundle will work.
  drupal_static_reset('yoast_seo_entity_supports_yoast_seo');
}

/**
 * Generates the path to the Yoast SEO JavaScript library files.
 *
 * @param string $mode
 *   (optional) Mode the path should be generated as.
 *
 * @return string
 *   Generated path to the library in the mode that was requested.
 */
function yoast_seo_library_path($library = 'js-text-analysis', $mode = 'relative') {
  $lib_path = drupal_get_path('module', 'yoast_seo') . '/js';
  if ($mode == 'local') {
    return './' . $lib_path . '/' . $library;
  }
  else {
    return base_path() . $lib_path . '/' . $library;
  }
}

Functions

Namesort descending Description
yoast_seo_configuration_form Build a FAPI array for editing meta tags.
yoast_seo_configuration_form_after_build In this afterbuild function we add the configuration to the JavaScript.
yoast_seo_configuration_form_submit Form API submission callback.
yoast_seo_configuration_load Load an entity's SEO info.
yoast_seo_configuration_load_multiple Load SEO info for multiple entities.
yoast_seo_configuration_save Save an entity's SEO settings.
yoast_seo_entity_delete Implements hook_entity_delete().
yoast_seo_entity_insert Implements hook_entity_insert().
yoast_seo_entity_load Implements hook_entity_load().
yoast_seo_entity_supports_yoast_seo Check whether the requested entity type (and bundle) support Yoast SEO.
yoast_seo_entity_type_disable Disable support for a specific entity type.
yoast_seo_entity_type_enable Enable support for a specific entity type.
yoast_seo_entity_update Implements hook_entity_update().
yoast_seo_field_attach_form Implements hook_field_attach_form().
yoast_seo_form_alter Implements hook_form_alter().
yoast_seo_form_node_admin_content_alter Implements hook_form_FORM_ID_alter().
yoast_seo_form_node_type_form_alter Implements hook_form_FORM_ID_alter().
yoast_seo_get_score Custom function which will get the SEO score from a nid.
yoast_seo_library_path Generates the path to the Yoast SEO JavaScript library files.
yoast_seo_menu Implements hook_menu().
yoast_seo_node_type_delete Implements hook_node_type_delete().
yoast_seo_permission Implements hook_permission().
yoast_seo_score_rating Custom function to build up score div, same as in scoreToRating.js.
yoast_seo_views_api Implements hook_views_api().
_yoast_check_fields_access Actually we need access to all fields listed or selected.
_yoast_seo_process_field Helper function that extracts field values and id's.