You are here

fac.module in Fast Autocomplete 7

Same filename and directory in other branches
  1. 8 fac.module

This file contains the main functions of the Fast Autocomplete module.

File

fac.module
View source
<?php

/**
 * @file
 * This file contains the main functions of the Fast Autocomplete module.
 */
define('FAC_JSON_FILES_DIRECTORY', 'public://fac-json');

/**
 * Implements hook_menu().
 */
function fac_menu() {

  // The browser requests a json file with results from the public files folder.
  // If the file does not exist, Drupal kicks in and a page callback is fired
  // that generates the json file in the public files folder and returns the
  // json result.
  $include_path = drupal_get_path('module', 'fac') . '/inc';
  $fac_file_path = variable_get('file_public_path', conf_path() . '/files') . '/fac-json/';
  $part_count = substr_count($fac_file_path, '/') + 2;
  foreach (array_keys(language_list()) as $language) {
    $file_path = $fac_file_path . $language . '/%/%';
    $items[$file_path] = array(
      'title' => 'Generate Fast Autocomplete JSON',
      'page callback' => 'fac_generate_json',
      'page arguments' => array(
        $language,
        $part_count,
      ),
      'access arguments' => array(
        'access content',
      ),
      'type' => MENU_CALLBACK,
      'file' => 'fac.json.inc',
      'file path' => $include_path,
    );
  }
  $items['admin/config/search/fac'] = array(
    'title' => 'Fast Autocomplete',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'fac_settings_form',
    ),
    'access arguments' => array(
      'administer fac settings',
    ),
    'type' => MENU_NORMAL_ITEM,
    'description' => 'Configure the Fast Autocomplete module',
    'file' => 'fac.admin.inc',
    'file path' => drupal_get_path('module', 'fac') . '/inc',
  );
  $items['admin/config/search/fac/general'] = array(
    'title' => 'General settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'fac_settings_form',
    ),
    'access arguments' => array(
      'administer fac settings',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'file' => 'fac.admin.inc',
    'file path' => drupal_get_path('module', 'fac') . '/inc',
  );
  $items['admin/config/search/fac/backend'] = array(
    'title' => 'Backend settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'fac_backend_settings_form',
    ),
    'access arguments' => array(
      'administer fac settings',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'fac.admin.inc',
    'file path' => drupal_get_path('module', 'fac') . '/inc',
    'weight' => 5,
  );
  $items['admin/config/search/fac/delete'] = array(
    'title' => 'Delete json files',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'fac_delete_form',
    ),
    'access arguments' => array(
      'administer fac settings',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'fac.admin.inc',
    'file path' => drupal_get_path('module', 'fac') . '/inc',
    'weight' => 10,
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function fac_permission() {
  return array(
    'administer fac settings' => array(
      'title' => t('Administer Fast Autocomplete settings'),
      'description' => t('Adminster the Fast Autocomplete module settings'),
    ),
  );
}

/**
 * Implements hook_library().
 */
function fac_library() {
  $fac_path = drupal_get_path('module', 'fac');
  $libraries['fastautocomplete'] = array(
    'title' => 'Fast Autocomplete jQuery plugin',
    'website' => 'http://drupal.org/project/fac',
    'version' => 'n/a ',
    'js' => array(
      $fac_path . '/js/jquery.fastautocomplete.js' => array(),
    ),
  );
  return $libraries;
}

/**
 * Implements hook_libraries_info().
 */
function fac_libraries_info() {
  $libraries['highlight'] = array(
    'name' => 'Highlight jQuery plugin',
    'vendor url' => 'http://bartaz.github.io/sandbox.js/jquery.highlight.html',
    'download url' => 'http://github.com/bartaz/sandbox.js/raw/master/jquery.highlight.js',
    'version' => 'n/a',
    'files' => array(
      'js' => array(
        'jquery.highlight.js',
      ),
    ),
  );
  return $libraries;
}

/**
 * Implements hook_page_build().
 *
 * Add the Fast Autocomplete javaScript.
 */
function fac_page_build(&$page) {
  global $language;

  // Add the Fast Autcomplete jQuery plugin to the output.
  $page['content']['#attached']['library'][] = array(
    'fac',
    'fastautocomplete',
  );
  $highlight_enabled = FALSE;
  if (($library_check = libraries_detect('highlight')) && !empty($library_check['installed'])) {
    $highlight_enabled = TRUE;

    // Add the Highlight jQuery plugin to the output.
    $page['content']['#attached']['libraries_load'][] = array(
      'highlight',
    );
  }
  $empty_result = variable_get('fac_empty_result', '');

  // Allow other modules to modify the empty result.
  drupal_alter('fac_empty_result', $empty_result);
  $account = variable_get('fac_anonymous_search', TRUE) ? drupal_anonymous_user() : $GLOBALS['user'];

  // Add the Fast Autocomplete settings to the output.
  $fac_settings = array(
    'jsonFilesPath' => base_path() . variable_get('file_public_path', conf_path() . '/files') . '/fac-json/' . $language->language . '/' . _fac_get_role_hmac($account) . '/',
    'inputSelectors' => variable_get('fac_input_selectors', ''),
    'keyMinLength' => variable_get('fac_key_min_length', 1),
    'keyMaxLength' => variable_get('fac_key_max_length', 5),
    'breakpoint' => variable_get('fac_breakpoint', 0),
    'emptyResult' => $empty_result,
    'allResultsLink' => variable_get('fac_all_results_link', TRUE),
    'allResultsLinkThreshold' => variable_get('fac_all_results_link_threshold', -1),
    'highlightEnabled' => $highlight_enabled,
    'resultLocation' => variable_get('fac_result_location', FALSE),
  );
  $page['content']['#attached']['js'][] = array(
    'data' => array(
      'fac' => $fac_settings,
    ),
    'type' => 'setting',
  );

  // Add the Fast Autocomplete JavaScript to the output.
  $page['content']['#attached']['js'][] = array(
    'data' => drupal_get_path('module', 'fac') . '/js/fac.js',
    'type' => 'file',
  );
  if (variable_get('fac_use_module_css', TRUE)) {

    // Add the Fast Autocomplete CSS to the output.
    $page['content']['#attached']['css'][] = drupal_get_path('module', 'fac') . '/css/fac.css';
  }
}

/**
 * Implements hook_preprocess_node().
 *
 * Hide the 'submitted by' information on nodes when viewed in the Fast
 * Autocomplete view mode.
 */
function fac_preprocess_node(&$variables) {
  if ($variables['view_mode'] == variable_get('fac_view_mode', 'fac')) {
    $variables['display_submitted'] = FALSE;
  }
}

/**
 * Implements hook_cron().
 *
 * If cleaning up the Fast Autocomplete files is activated the expired files
 * are deleted.
 */
function fac_cron() {

  // Change the HMAC key. Defaults to once a week.
  if ($interval = variable_get('fac_change_hmac_key_interval', 60 * 60 * 24 * 7)) {
    $key_timestamp = variable_get('fac_hmac_key_timestamp', 0);
    if (time() > $key_timestamp + $interval) {
      variable_set('fac_hmac_key', drupal_random_key());
      variable_set('fac_hmac_key_timestamp', time());

      // Clean up files; the paths are no longer valid with the new key.
      fac_delete_json_files();
    }
  }
  if (variable_get('fac_bulk_generate_json_enabled', FALSE)) {
    if (time() > variable_get('fac_bulk_generate_json_next_run', 0)) {
      $queue = DrupalQueue::get('fac_bulk_generate_json');
      $size = variable_get('fac_bulk_generate_json_size', 2);
      $keys = _fac_create_search_array($size);
      $languages = language_list('enabled');
      $languages = $languages[1];
      foreach ($languages as $language) {
        foreach ($keys as $key) {
          $data = new stdClass();
          $data->language = $language->language;
          $data->key = $key;
          $queue
            ->createItem($data);
        }
      }
      variable_set('fac_bulk_generate_json_next_run', time() + 24 * 60 * 60);
    }
  }
  if (variable_get('fac_clean_up_files', TRUE)) {
    $expire_time = strtotime(variable_get('fac_files_expire_time', '-1 day'));
    fac_delete_json_files($expire_time);
  }
}

/**
 * Implements hook_cron_queue_info().
 */
function fac_cron_queue_info() {
  $queues['fac_bulk_generate_json'] = array(
    'worker callback' => 'fac_bulk_generate_json',
    'time' => 60,
  );
  return $queues;
}

/**
 * Bulk generate json files worker callack.
 */
function fac_bulk_generate_json($data) {
  fac_generate_json_for_key($data->language, $data->key);
}

/**
 * Generates the json file for a specific search key.
 *
 * @param string $key
 *   The key to search for.
 *
 * @return string
 *   The json string hat is saved or an empty string on failure.
 */
function fac_generate_json_for_key($language, $key) {
  $json_result = '';
  $original_user = $GLOBALS['user'];

  // For security reasons we can only perform a search as an anonymous user.
  // Otherwise the JSON files might expose information that should be private.
  // If the risk is deemed acceptable this behavior can be overridden by
  // setting the variable "fac_anonymous_search_only to FALSE and the search
  // will be performed as the current user.
  if (variable_get('fac_anonymous_search', TRUE)) {

    // Prevent session information from being rendered.
    drupal_save_session(FALSE);

    // Force the current user to anonymous to prevent access bypass.
    $GLOBALS['user'] = drupal_anonymous_user();
  }

  // Force the language url to the requested language to make sure the urls that
  // are generated possibly contain the correct language path.
  $original_language_url = $GLOBALS['language_url'];
  $language_list = language_list();
  $GLOBALS['language_url'] = $language_list[$language];
  $backend_service = variable_get('fac_backend_service', '');
  if (!empty($backend_service)) {
    $service = new $backend_service();
    $result = $service
      ->search($key, $language);
    $items = array();
    if (!empty($result['items'])) {
      foreach ($result['items'] as $item_info) {
        $entities = entity_load($item_info['entity_type'], array(
          $item_info['etid'],
        ));
        $entity = reset($entities);
        $entity_views = entity_view($item_info['entity_type'], array(
          $entity,
        ), variable_get('fac_view_mode', 'fac'), $language);
        $entity_view = reset($entity_views);
        $items[] = render($entity_view);
      }
    }
    $object = new stdClass();
    $object->items = $items;
    $json_result = json_encode($object);
    $directory = FAC_JSON_FILES_DIRECTORY . '/' . $language . '/' . _fac_get_role_hmac($GLOBALS['user']);
    if (file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) {
      $destination = $directory . '/' . $key . '.json';
      file_unmanaged_save_data($json_result, $destination, FILE_EXISTS_REPLACE);
    }
  }

  // Restore the language_url.
  $GLOBALS['language_url'] = $original_language_url;

  // Restore the original user.
  $GLOBALS['user'] = $original_user;
  drupal_save_session(TRUE);
  return $json_result;
}

/**
 * Delete Fast Autocomplete json files.
 *
 * @param string $expire_time
 *   An optional expire time to only delete files older than the given date.
 */
function fac_delete_json_files($expire_time = '') {
  if (empty($expire_time)) {

    // No date and time given so just delete the entire directory.
    file_unmanaged_delete_recursive(FAC_JSON_FILES_DIRECTORY);
  }
  else {

    // Get all Fast Autocomplete json files.
    $json_files = file_scan_directory(FAC_JSON_FILES_DIRECTORY, '/.*\\.json$/');

    // Loop through all the files and delete those that have expired.
    foreach ($json_files as $json_file) {
      if (filectime($json_file->uri) < $expire_time) {
        file_unmanaged_delete($json_file->uri);
      }
    }
  }
}

/**
 * Implements hook_entity_info_alter().
 *
 * Adds the Fast Autocomplete view mode to all entities.
 */
function fac_entity_info_alter(&$entity_info) {
  foreach ($entity_info as &$info) {
    $info['view modes']['fac'] = array(
      'label' => t('Fast Autocomplete'),
      'custom settings' => FALSE,
    );
  }
}

/**
 * Returns either a list of all available service infos, or a specific one.
 *
 * @param string|null $id
 *   The ID of the service info to retrieve.
 *
 * @return array
 *   If $id was not specified, an array of all available service classes.
 *   Otherwise, either the service info with the specified id (if it exists),
 *   or NULL. Service class information is formatted as specified by
 *   hook_fac_service_info(), with the addition of a "module" key
 *   specifying the module that adds a certain class.
 *
 * @see hook_fac_service_info()
 */
function fac_get_service_info($id = NULL) {
  $services =& drupal_static(__FUNCTION__);
  if (!isset($services)) {

    // Inlined version of module_invoke_all() to add "module" keys.
    $services = array();
    foreach (module_implements('fac_service_info') as $module) {
      $function = $module . '_fac_service_info';
      if (function_exists($function)) {
        $new_services = $function();
        if (isset($new_services) && is_array($new_services)) {
          foreach ($new_services as $service => $info) {
            $new_services[$service] += array(
              'module' => $module,
            );
          }
        }
        $services += $new_services;
      }
    }
  }
  if (isset($id)) {
    return isset($services[$id]) ? $services[$id] : NULL;
  }
  return $services;
}

/**
 * Implements hook_modules_enabled().
 */
function fac_modules_enabled() {

  // New modules might offer additional item types or service classes,
  // invalidating the cached information.
  drupal_static_reset('fac_get_service_info');
}

/**
 * Implements hook_modules_disabled().
 */
function fac_modules_disabled() {

  // The disabled modules might have offered item types or service classes,
  // invalidating the cached information.
  drupal_static_reset('fac_get_service_info');
}

/**
 * Implements hook_fac_service_info().
 */
function fac_fac_service_info() {
  $services['fac_basic_title_search_service'] = array(
    'name' => t('Basic title search service'),
    'class' => 'Drupal\\fac\\SearchService\\BasicTitleSearchService',
  );
  if (module_exists('search')) {
    $services['fac_core_search_service'] = array(
      'name' => t('Core search service'),
      'class' => 'Drupal\\fac\\SearchService\\CoreSearchService',
    );
  }
  if (module_exists('search_api')) {
    $services['fac_search_api_search_service'] = array(
      'name' => t('Search API search service'),
      'class' => 'Drupal\\fac\\SearchService\\SearchApiSearchService',
    );
  }
  if (module_exists('apachesolr')) {
    $services['fac_apachesolr_search_service'] = array(
      'name' => t('Apachesolr search service'),
      'class' => 'Drupal\\fac\\SearchService\\ApachesolrSearchService',
    );
  }
  return $services;
}

/**
 * Implements hook_stage_file_proxy_excluded_paths_alter().
 */
function fac_stage_file_proxy_excluded_paths_alter(&$excluded_paths) {
  $excluded_paths[] = '/fac-json';
}

/**
 * Creates an array with letter combinations to search for.
 *
 * @return array
 *   An array with letter combinations.
 */
function _fac_create_search_array($size) {
  $search_array = array();
  $letters = range('a', 'z');
  $numbers = range('0', '9');
  $chars = array_merge($letters, $numbers);
  $i = 1;
  while ($i <= $size) {
    $temp_array = _fac_keys_sampling($chars, $i);
    $search_array = array_merge($search_array, $temp_array);
    $i++;
  }
  return $search_array;
}

/**
 * Generates an array with all possible character combinations.
 *
 * @param array $chars
 *   The characters to create all possible combinations for.
 * @param int $size
 *   The size of the required combinations.
 *
 * @return array
 *   An array of all possible character combinations with the given size.
 */
function _fac_keys_sampling(array $chars, $size, $combinations = array()) {

  // If it's the first iteration, the first set of combinations is the same as
  // the set of characters.
  if (empty($combinations)) {
    $combinations = $chars;
  }

  // We're done if we're at size 1.
  if ($size == 1) {
    return $combinations;
  }

  // Initialise array to put new values in.
  $new_combinations = array();

  // Loop through existing combinations and character set to create strings.
  foreach ($combinations as $combination) {
    foreach ($chars as $char) {
      $new_combinations[] = $combination . $char;
    }
  }

  // Call same function again for the next iteration.
  return _fac_keys_sampling($chars, $size - 1, $new_combinations);
}

/**
 * Gets a HMAC based on account roles.
 *
 * @param object $account
 *   User account object.
 *
 * @return string
 *   A HMAC signed with the site-specific secret key.
 */
function _fac_get_role_hmac($account) {
  $rids = array_keys($account->roles);
  sort($rids);

  // Prevent user 1 accounts without the administrator role leaking information
  // via DRUPAL_AUTHENTICATED_RID.
  if ($account->uid == 1) {
    $rids[] = -1;
  }
  return drupal_hmac_base64('fac-' . implode('|', $rids), _fac_get_hmac_key());
}

/**
 * Gets a fac specific HMAC key.
 *
 * A key is generated and stored if it doesn't exist yet.
 *
 * @return string
 *   The current HMAC key.
 */
function _fac_get_hmac_key() {
  if (!($key = variable_get('fac_hmac_key', FALSE))) {
    $key = drupal_random_key();
    variable_set('fac_hmac_key', $key);
    variable_set('fac_hmac_key_timestamp', REQUEST_TIME);
  }
  return $key;
}

Functions

Namesort descending Description
fac_bulk_generate_json Bulk generate json files worker callack.
fac_cron Implements hook_cron().
fac_cron_queue_info Implements hook_cron_queue_info().
fac_delete_json_files Delete Fast Autocomplete json files.
fac_entity_info_alter Implements hook_entity_info_alter().
fac_fac_service_info Implements hook_fac_service_info().
fac_generate_json_for_key Generates the json file for a specific search key.
fac_get_service_info Returns either a list of all available service infos, or a specific one.
fac_libraries_info Implements hook_libraries_info().
fac_library Implements hook_library().
fac_menu Implements hook_menu().
fac_modules_disabled Implements hook_modules_disabled().
fac_modules_enabled Implements hook_modules_enabled().
fac_page_build Implements hook_page_build().
fac_permission Implements hook_permission().
fac_preprocess_node Implements hook_preprocess_node().
fac_stage_file_proxy_excluded_paths_alter Implements hook_stage_file_proxy_excluded_paths_alter().
_fac_create_search_array Creates an array with letter combinations to search for.
_fac_get_hmac_key Gets a fac specific HMAC key.
_fac_get_role_hmac Gets a HMAC based on account roles.
_fac_keys_sampling Generates an array with all possible character combinations.

Constants

Namesort descending Description
FAC_JSON_FILES_DIRECTORY @file This file contains the main functions of the Fast Autocomplete module.