You are here

asaf.module in Asaf (ajax submit for any form) 8

Same filename and directory in other branches
  1. 7 asaf.module

Main module file.

File

asaf.module
View source
<?php

/**
 * @file
 * Main module file.
 */

/**
 * @todo
 * - splpit administration form to route and controller
 */
define('ASAF_SETTINGS_STATUS_MESSAGES', 'status messages');
define('ASAF_SETTINGS_PAGE_CACHE', 'page cache');
define('ASAF_SETTINGS_CALL_COMMANDS_ALTER', 'call commands alter');
define('ASAF_SETTINGS_CALL_COMMANDS_ALTER_ALWAYS', 'always');
define('ASAF_SETTINGS_CALL_COMMANDS_ALTER_IF_NO_ERRORS', 'no errors');
function asaf_menu() {
  $items = array();
  $items['system/ajax/asaf/pagecache'] = array(
    'title' => 'ASAF callback',
    'page callback' => 'asaf_pagecache_form_callback',
    'delivery callback' => 'ajax_deliver',
    'access callback' => TRUE,
    'theme callback' => 'ajax_base_page_theme',
    'type' => MENU_CALLBACK,
    'file' => 'asaf.pages.inc',
  );
  return $items;
}
function asaf_form_alter(&$form, &$form_state, $form_id) {
  $buttons = asaf_is_handled_form($form_id);
  if (\Drupal::config('asaf_settings.config')
    ->get('asaf_show_form_ids')) {
    drupal_set_message(t('Form id: %form_id', array(
      '%form_id' => $form_id,
    )));
  }
  if (isset($buttons) && is_array($buttons)) {
    $options = array();
    if (\Drupal::config('asaf_settings.config')
      ->get('asaf_autoload_form_stuff')) {
      $options['needed_files'] = asaf_get_form_stuff($form, $form_state, $form_id);
    }
    asaf_prepare_form($form, $form_state, $buttons, $options);
  }
}
function asaf_batch_alter(&$batch) {
  if (asaf_is_asaf()) {
    list($form, $form_state) = _asaf_get_form_details_from_stacktrace();
    if (isset($form_state['asaf']['form_path']) && $form_state['asaf']['form_path']) {
      $batch['source_url'] = $form_state['asaf']['form_path'];
    }
    if (isset($batch['form_state']['no_redirect'])) {
      $batch['form_state']['no_redirect'] = FALSE;
    }
  }
}

/*
 * For the asaf processed forms we should prevent sending Location header. Instead this we send json with standart ajax command "redirect"
 */
function asaf_drupal_goto_alter(&$path, &$options, &$http_response_code) {

  // Checking is asaf enabled
  if (asaf_is_asaf()) {

    // Trying to find actual $form and $form_state from stacktrace
    // TODO: This code a little bit smells, and should be refactored (if it's possible)
    list($form, $form_state) = _asaf_get_form_details_from_stacktrace();
    if (is_array($form) && is_array($form_state) && isset($form_state['triggering_element']['#asaf_control'])) {
      $form_state['redirect'] = array(
        $path,
        $options,
      );
      $page_callback_result = asaf_ajax_callback($form, $form_state);

      // Emulate correct response
      $router_item = menu_get_item(request_path());
      $default_delivery_callback = isset($router_item) && $router_item ? $router_item['delivery_callback'] : NULL;
      drupal_deliver_page($page_callback_result, $default_delivery_callback);
      exit;
    }
  }
}
function asaf_hook_info() {
  $hooks = array(
    'asaf_form_ajax_commands_alter' => array(
      'group' => 'forms',
    ),
  );
  return $hooks;
}

/**
 * Attach ajax handlers to the specified buttons.
 *
 * @param $form
 *   An associative array containing the structure of the form.
 * @param $form_state
 *   A keyed array containing the current state of the form. Asaf processed flag
 *   asaf options will save in $form_state.
 * @param $buttons
 *   (optional) Array containing information about buttons, which should be handled. Empty array
 *   means all buttons should be handled. Not empty associative array can have the following keys:
 *   - included: list of buttons which should be handled;
 *     @code
 *       $buttons = array(
 *         'included' => array(
 *           'actions][submit' => array(...),
 *           'actions][preview' => '',
 *         );
 *       );
 *     @endcode
 *     If value of buttons item is array, it will be added to the button form element.
 *   - excluded: list of buttons which should not be handled.
 *   Inclided section have prioriry over excluded section.
 * @param $options
 *   (optional) An associative array with asaf options. Can have the folowing keys:
 *   - needed_files: Array of files which should be loaded for the correct form handling. File item
 *     can be module name instead of correct filename. In this case all *.inc files from this module
 *     folder will be loaded automatically.
 *
 *   @code
 *     $options = array(
 *       'needed_files' => array(
 *         'modules/user/user.pages.inc', // this file will be loaded
 *         'webform'                      // all inc files from webform module folder will be loaded
 *       );
 *     );
 *   @endcode
 *
 */
function asaf_prepare_form(&$form, &$form_state, array $buttons = array(), array $options = array()) {
  $options += asaf_default_options();
  $form_state += array(
    'asaf' => array(),
  );
  $form_state['asaf']['form_path'] = isset($form_state['values']['asaf_form_path']) ? $form_state['values']['asaf_form_path'] : request_path();
  $form_state['asaf']['buttons'] = $buttons;
  $form_state['asaf']['options'] = $options;

  // Default renderable area
  $form['#asaf_area'] = 'form';
  asaf_mark_buttons($form, '', $form_state);
  asaf_handle_buttons($form, '', $form_state);
  asaf_handle_areas($form, '', $form_state);

  // Need for fast way decoding asaf forms instead of a slow call _asaf_get_form_details_from_stacktrace()
  $form['asaf_form'] = array(
    '#type' => 'hidden',
  );
  if (isset($options['needed_files']) && !empty($options['needed_files'])) {
    asaf_register_needed_files($form_state, $options['needed_files']);
  }
  if ($options[ASAF_SETTINGS_PAGE_CACHE]) {
    $form['asaf_form_path'] = array(
      '#type' => 'hidden',
      '#value' => $form_state['asaf']['form_path'],
    );
    $form['asaf_id_prefix'] = array(
      '#type' => 'hidden',
      '#value' => $form_state['asaf']['id_prefix'],
    );
    $form['#process'][] = 'asaf_form_pagecache_process';
  }
  $form['#attached']['js'][] = array(
    'type' => 'setting',
    'data' => array(
      'asaf' => array(
        'submitByEnter' => \Drupal::config('asaf_settings.config')
          ->get('asaf_form_submit_by_enter'),
      ),
    ),
  );
}
function asaf_form_pagecache_process($form, &$form_state) {
  $form['asaf_form']['#value'] = asaf_get_security_token($form['#form_id'], $form['#build_id']);
  $form_options = array(
    'needed_files' => !empty($form_state['build_info']['files']) ? $form_state['build_info']['files'] : array(),
  );

  // Saving form options (first of all needed_files) for the half of the year
  cache_set('asaf_form_' . $form['#form_id'] . '_options', $form_options, 'cache_form', REQUEST_TIME + 15768000);
  return $form;
}

/*
function asaf_default_options() {
  return array(
    ASAF_SETTINGS_STATUS_MESSAGES => 'default',
    ASAF_SETTINGS_PAGE_CACHE => variable_get('cache', FALSE),
    ASAF_SETTINGS_CALL_COMMANDS_ALTER => ASAF_SETTINGS_CALL_COMMANDS_ALTER_IF_NO_ERRORS,
  );
}
*/
function asaf_mark_buttons(&$element, $key, &$form_state) {
  $buttons = $form_state['asaf']['buttons'];
  if (!isset($element['#asaf_control']) && isset($element['#type']) && $element['#type'] == 'submit' && asaf_is_handled_button($key, $buttons)) {
    $element['#asaf_control'] = 'asaf_submit';
    $element += asaf_get_handled_button_options($key, $buttons);
  }
  foreach (element_children($element) as $child_key) {
    asaf_mark_buttons($element[$child_key], $key ? $key . '][' . $child_key : $child_key, $form_state);
  }
}
function asaf_handle_buttons(&$element, $key, &$form_state) {
  if (!empty($element['#asaf_control'])) {
    $element += array(
      '#type' => 'submit',
      '#attributes' => array(),
      '#id' => drupal_html_id(asaf_get_id($key, $form_state)),
      '#ajax' => array(
        'callback' => 'asaf_ajax_callback',
        'path' => $form_state['asaf']['options'][ASAF_SETTINGS_PAGE_CACHE] ? 'system/ajax/asaf/pagecache' : 'system/ajax',
        'wrapper' => asaf_get_area_wrapper_id(!empty($element['#asaf_target_area']) ? is_array($element['#asaf_target_area']) ? reset($element['#asaf_target_area']) : $element['#asaf_target_area'] : 'form', $form_state),
        'progress' => array(
          'type' => 'throbber',
          'message' => '',
        ),
      ),
    );
    $element['#attributes'] += array(
      'class' => array(),
    );
    $element['#attributes']['class'][] = 'asaf-control-' . $element['#asaf_control'];
    $element['#attached']['js'][] = drupal_get_path('module', 'asaf') . '/js/asaf.js';
  }
  foreach (element_children($element) as $child_key) {
    asaf_handle_buttons($element[$child_key], $key ? $key . '][' . $child_key : $child_key, $form_state);
  }
}
function asaf_handle_areas(&$element, $key, &$form_state) {
  if (!empty($element['#asaf_area'])) {
    $element['#asaf_area_id'] = asaf_get_area_wrapper_id($element['#asaf_area'], $form_state);

    /* When the form inserted directly in block, like user_login_block, or any other form which implemented in the
     * following way:
     *
     * function HOOK_block_view($delta = '') {
     *   return array(
     *     'subject' => t('Form title'),
     *     'content' => drupal_get_form('form_constructor'),
     *   );
     * }
     *
     * form area wrapper doesn't work correctly because block module use block.tpl.php as theme wrapper for block
     * content. In this case block wrapper HTML (block wrapper and block title) will be rendered inside form area
     * wrapper, and it will be loosed after first form update with asaf. To resolve this bug we have to wrap form
     * manually on the client-side using data-asaf-area-wrapper-id attribute */
    if ($element['#asaf_area'] == 'form') {
      $element['#attributes']['data-asaf-area-name'] = $element['#asaf_area'];
      $element['#attributes']['data-asaf-area-wrapper-id'] = $element['#asaf_area_id'];
    }
    else {
      $element['#prefix'] = '<div id="' . $element['#asaf_area_id'] . '" class="asaf-area-wrapper asaf-' . $element['#asaf_area'] . '-area-wrapper">' . (isset($element['#prefix']) ? $element['#prefix'] : '');
      $element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '</div>';
      $element['#attributes']['data-asaf-area-name'] = $element['#asaf_area'];
    }
  }
  foreach (element_children($element) as $child_key) {
    asaf_handle_areas($element[$child_key], $key ? $key . '][' . $child_key : $child_key, $form_state);
  }
}
function asaf_find_area($areaName, $element) {
  if (!isset($element['#asaf_area']) || $element['#asaf_area'] != $areaName) {
    foreach (element_children($element) as $key) {
      $child = asaf_find_area($areaName, $element[$key]);
      if (isset($child['#asaf_area']) && $child['#asaf_area'] == $areaName) {
        $element = $child;
        break;
      }
    }
  }
  return $element;
}
function asaf_ajax_callback(&$form, &$form_state) {
  $commands = array();
  if ($form_state['asaf']['options'][ASAF_SETTINGS_STATUS_MESSAGES] == 'hide') {
    theme('status_messages');
  }
  if (isset($form_state['redirect'])) {
    if (!is_array($form_state['redirect'])) {
      $form_state['redirect'] = array(
        $form_state['redirect'],
        array(
          'query' => array(),
        ),
      );
    }
    $commands[] = asaf_ajax_command_redirect($form_state['redirect']);
  }
  else {
    $processed_areas = array();
    $targets = !empty($form_state['triggering_element']['#asaf_target_area']) ? $form_state['triggering_element']['#asaf_target_area'] : 'form';
    $targets = !is_array($targets) ? array(
      $targets,
    ) : $targets;
    $area_render_command_callback = isset($form_state['asaf']['options']['form_render_command_callback']) && $form_state['asaf']['options']['form_render_command_callback'] && function_exists($form_state['asaf']['options']['form_render_command_callback']) ? $form_state['asaf']['options']['form_render_command_callback'] : 'asaf_default_area_render_command';
    foreach ($targets as $areaName) {
      $area = asaf_find_area($areaName, $form);
      if ($area['#asaf_area'] == $areaName && !isset($processed_areas[$areaName])) {
        $area_command = $area_render_command_callback($area, $form_state);
        if (!empty($area_command)) {
          $commands[] = $area_command;
        }
        $processed_areas[$areaName] = TRUE;
      }
    }

    // Send to the client new form_buid_id when pagecache mode enabled and form rebuild partially, without whole form
    if ($form_state['asaf']['options'][ASAF_SETTINGS_PAGE_CACHE] && !isset($processed_areas['form'])) {
      $commands[] = ajax_command_invoke('#' . asaf_get_area_wrapper_id('form', $form_state) . ' input[name="form_build_id"]', 'val', array(
        $form['#build_id'],
      ));
    }
  }

  // Flag allow some command alter hooks returns different list of command for validation errors case.
  $form_state['asaf_form_status'] = form_get_errors() ? 'errors' : '';
  if ($form_state['asaf_form_status'] == 'errors' && $form_state['asaf']['options'][ASAF_SETTINGS_STATUS_MESSAGES] == 'none') {
    $form_state['asaf']['options'][ASAF_SETTINGS_STATUS_MESSAGES] = 'default';
  }
  if (isset($form_state['asaf_ajax_commands']) && is_array($form_state['asaf_ajax_commands'])) {
    $commands = array_merge($commands, $form_state['asaf_ajax_commands']);
  }
  if ($form_state['asaf']['options'][ASAF_SETTINGS_STATUS_MESSAGES] == 'default') {
    $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
  }
  if ($form_state['asaf_form_status'] != 'errors' || $form_state['asaf']['options'][ASAF_SETTINGS_CALL_COMMANDS_ALTER] == ASAF_SETTINGS_CALL_COMMANDS_ALTER_ALWAYS) {
    drupal_alter('asaf_form_ajax_commands', $commands, $form, $form_state, $form['#form_id']);
    drupal_alter('asaf_form_' . $form['#form_id'] . '_ajax_commands', $commands, $form, $form_state);

    // We don't use $form_id because we need real form id, not $form['#id'] or autogenerated value
  }
  unset($form_state['asaf_ajax_commands']);
  return array(
    '#type' => 'ajax',
    '#commands' => $commands,
  );
}
function asaf_default_area_render_command($area, &$form_state) {
  if ($form_state['asaf']['options'][ASAF_SETTINGS_STATUS_MESSAGES] == 'area') {
    $messages = theme('status_messages');
    if ($messages) {
      $area = array(
        'asaf_messages' => array(
          '#markup' => $messages,
          '#weight' => -99999999,
        ),
      ) + $area;
    }
  }
  return ajax_command_replace('#' . asaf_get_area_wrapper_id($area['#asaf_area'], $form_state), drupal_render($area));
}
function asaf_register_needed_files(&$form_state, array $files) {
  if (!isset($form_state['build_info']['files'])) {
    $form_state['build_info']['files'] = array();
  }
  foreach ($files as $file) {
    if (is_array($file) && isset($file['module']) || is_string($file) && file_exists($file)) {
      $form_state['build_info']['files'][] = $file;
    }
    elseif (is_string($file) && module_exists($file)) {

      // Loading all includes of the module
      $path = drupal_get_path('module', $file);
      $destination = DRUPAL_ROOT . '/' . $path;
      $pattern = '/.inc$/';
      $matches = array_keys(file_scan_directory($destination, $pattern));
      if (is_array($matches)) {
        foreach ($matches as $inc) {
          $parts = explode(DRUPAL_ROOT . '/', $inc);
          if (isset($parts[1]) && $parts[1]) {
            $form_state['build_info']['files'][] = $parts[1];
          }
        }
      }
    }
  }
}
function asaf_get_needed_files_list(array $files) {
  $list = array();
  foreach ($files as $file) {
    if (is_array($file) && isset($file['module']) || is_string($file) && file_exists($file)) {
      $list[] = $file;
    }
    elseif (is_string($file) && module_exists($file)) {

      // Loading all includes of the module
      $path = drupal_get_path('module', $file);
      $destination = DRUPAL_ROOT . '/' . $path;
      $list[] = $path . '/' . $file . '.module';
      $pattern = '/.inc$/';
      $matches = array_keys(file_scan_directory($destination, $pattern));
      if (is_array($matches)) {
        foreach ($matches as $inc) {
          $parts = explode(DRUPAL_ROOT . '/', $inc);
          if (isset($parts[1]) && $parts[1]) {
            $list[] = $parts[1];
          }
        }
      }
    }
  }
  return $list;
}
function asaf_load_needed_files(array $files) {
  foreach ($files as $file) {
    if (is_array($file) && isset($file['module'])) {
      $file += array(
        'type' => 'inc',
        'name' => $file['module'],
      );
      module_load_include($file['type'], $file['module'], $file['name']);
    }
    elseif (file_exists($file)) {
      require_once DRUPAL_ROOT . '/' . $file;
    }
  }
}
function asaf_is_handled_form($form_id) {
  $forms =& drupal_static(__FUNCTION__, NULL);
  if (!isset($forms)) {
    $forms = asaf_get_handled_forms_list();
  }
  return isset($form_id) && isset($forms[$form_id]) ? $forms[$form_id] : FALSE;
}
function asaf_is_handled_button($key, $buttons) {
  $handled = TRUE;
  if (isset($buttons['included'])) {
    $handled = isset($buttons['included'][$key]);
  }
  elseif (isset($buttons['excluded'])) {
    $handled = !isset($buttons['excluded'][$key]);
  }
  return $handled;
}
function asaf_get_handled_button_options($key, $buttons) {
  return isset($buttons['included'][$key]) && is_array($buttons['included'][$key]) ? $buttons['included'][$key] : array();
}
function asaf_get_handled_forms_list() {
  return;
  $forms = array();
  $text = \Drupal::config('asaf_settings.config')
    ->get('asaf_forms');
  $lines = explode("\n", $text);
  foreach ($lines as $line) {
    $parts = explode('@', trim($line));
    $form_id = isset($parts[0]) ? $parts[0] : '';
    $buttons = isset($parts[1]) ? $parts[1] : '';
    $form_id = trim($form_id);
    $buttons = trim($buttons);
    if ($form_id) {
      $forms[$form_id] = array();
      if ($buttons) {
        $forms[$form_id] = array(
          'included' => array(),
          'excluded' => array(),
        );
        if ($buttons[0] != '+' && $buttons[0] != '-') {
          $buttons = '+' . $buttons;
        }
        preg_match_all('/([\\+\\-][^\\+\\-]+)/', $buttons, $matches);
        if (isset($matches[0]) && is_array($matches[0])) {
          foreach ($matches[0] as $button) {
            $op = $button[0];
            $button = substr($button, 1);
            $forms[$form_id][$op == '+' ? 'included' : 'excluded'][$button] = $button;
          }
        }
        if (!empty($forms[$form_id]['included'])) {
          unset($forms[$form_id]['excluded']);
        }
        else {
          unset($forms[$form_id]['included']);
        }
      }
    }
  }
  drupal_alter('asaf_forms_list', $forms);
  return $forms;
}
function asaf_get_area_wrapper_id($area, &$form_state) {
  return asaf_get_id($area . '-area-wrapper', $form_state);
}
function asaf_get_id($id, &$form_state) {
  asaf_init_prefix($form_state);
  return $form_state['asaf']['id_prefix'] . '-' . $id;
}
function asaf_init_prefix(&$form_state) {
  if (!isset($form_state['asaf']['id_prefix'])) {
    $form_state['asaf']['id_prefix'] = asaf_get_prefix();
  }
}
function asaf_get_prefix() {
  $prefixes =& drupal_static(__FUNCTION__, array());
  $i = 0;
  do {
    $prefix = 'asaf-' . time() . ($i ? '-' . $i : '');
    $i++;
  } while (isset($prefixes[$prefix]));
  $prefixes[$prefix] = $prefix;
  return $prefix;
}
function asaf_is_asaf() {
  $is_asaf = isset($_POST['form_build_id']) && isset($_POST['asaf_form']);
  return $is_asaf;
}
function asaf_get_security_token($form_id, $form_build_id) {
  return md5('asaf-' . $form_id . '-form-' . $form_build_id . '-security-token');
}
function asaf_get_form_stuff($form, $form_state, $form_id) {
  $stuff = _asaf_get_form_parent_module($form_id, $form_state);
  return is_array($stuff) && !empty($stuff) ? $stuff : array();
}
function _asaf_get_form_parent_module($form_id, $form_state) {
  $parent = array();

  // If in the same folder we well find a few modules we will load all of them
  $constructor = _asaf_get_form_constructor($form_id, $form_state);
  if (function_exists($constructor) && class_exists('ReflectionFunction')) {
    $reflection = new ReflectionFunction($constructor);
    $file = $reflection
      ->getFileName();
    $directory = dirname($file);
    $pattern = '/.module$/';
    $i = 20;

    // just in case :)
    while (empty($parents) && $directory != DRUPAL_ROOT && $i) {
      $matches = array_keys(file_scan_directory($directory, $pattern, array(
        'recurse' => FALSE,
      )));
      if (is_array($matches)) {
        foreach ($matches as $module) {
          $info = pathinfo($module);
          $parent[] = $info['filename'];
        }
      }
      $directory = dirname($directory);
      $i--;
    }
  }
  return !empty($parent) ? $parent : FALSE;
}
function _asaf_get_form_constructor($form_id, $form_state) {
  if (!function_exists($form_id) && isset($form_state['build_info']['base_form_id'])) {
    $form_id = $form_state['build_info']['base_form_id'];
  }
  return function_exists($form_id) ? $form_id : FALSE;
}

/*
 * Getting $form and $form_state from stacktrace
 *
 * @return
 *   An array containing the $form and $form_state.
 */
function _asaf_get_form_details_from_stacktrace() {
  static $form;
  static $form_state;
  if (!$form || !$form_state) {
    $stacktrace = debug_backtrace();
    foreach ($stacktrace as $step) {
      $args = isset($step['args']) ? $step['args'] : FALSE;
      if (is_array($args)) {
        foreach ($args as $arg) {
          if (!$form && is_array($arg) && isset($arg['#type']) && $arg['#type'] == 'form') {
            $form = $arg;
          }
          if (!$form_state && is_array($arg) && isset($arg['build_info']) && is_array($arg['build_info'])) {
            $form_state = $arg;
          }
        }
        if ($form && $form_state) {
          break;
        }
      }
    }
  }
  return array(
    $form,
    $form_state,
  );
}
function asaf_ajax_command_redirect($href, $window = 'current') {
  if (is_array($href)) {
    $href = call_user_func_array('url', $href);
  }
  elseif (strpos($href, '://') === FALSE) {
    $href = url($href, array(
      'absolute' => TRUE,
    ));
  }
  return array(
    'command' => 'asafRedirect',
    'href' => $href,
    'window' => $window,
  );
}
function asaf_ajax_command_reload($window = 'current') {
  return array(
    'command' => 'asafReload',
    'window' => $window,
  );
}