<?php
// $Id: authcache.module,v 1.8 2010/02/22 06:26:08 jonahellison Exp $

/**
 * @file
 * Authenticated User Page Caching (and anonymous users, too!)
 * This file is limited to Drupal specific functions & hooks only.
 * 
 * @see authcache.helpers.inc for cache helper functions
 * @see authcache.admin.inc for admin page functionality
 */

// Global variables
$is_page_authcache = FALSE;
$authcache_debug_info = NULL; // Store reason for disabling page caching

// Default caching rules (Never cache these pages)
define('AUTHCACHE_NOCACHE_DEFAULT', 'admin*
user*
node/add/*
node/*/edit
node/*/track
tracker*
comment/edit*
civicrm*
cart*
*/ajax/*
*/autocomplete/*
ajax_comments*
filefield/*');

// Default non-HTML caching rules (don't append JS to page content)
define('AUTHCACHE_NONHTML_DEFAULT', '');

// Functions specifically for caching
require dirname(__FILE__) . '/authcache.helpers.inc';

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

  $items['admin/settings/performance/drupal'] = array(
    'title' => t('Drupal core'),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
    'page callback' => 'drupal_goto', // For admin_menu.module
    'page arguments' => array('admin/settings/performance'),
    'access arguments' => array('administer site configuration'),
  );

  $items['admin/settings/performance/authcache/config'] = array(
    'title' => 'Configuration',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );

  $items['admin/settings/performance/authcache'] = array(
    'title' => 'Authcache',
    'description' => "Configure Authcache.",
    'page callback' => 'drupal_get_form',
    'page arguments' => array('authcache_admin_config'),
    'access arguments' => array('administer site configuration'),
    'file' => 'authcache.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  );
  $items['admin/settings/performance/authcache/pagecaching'] = array(
    'title' => t('Page Caching Settings'),
    'description' => "Configure page cache settings.",
    'page callback' => 'drupal_get_form',
    'page arguments' => array('authcache_admin_pagecaching'),
    'access arguments' => array('administer site configuration'),
    'file' => 'authcache.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 20,
  );
  /* @todo
  $items['admin/settings/performance/authcache/advanced'] = array(
    'title' => 'Advanced Rulesets',
    'description' => "Configure advanced caching rulesets.",
    'page callback' => 'drupal_get_form',
    'page arguments' => array('authcache_admin_advanced'),
    'access arguments' => array('administer site configuration'),
    'file' => 'authcache.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 30,
  );
  $items['admin/settings/performance/authcache/lookup'] = array(
    'title' => t('View Cached'),
    'page callback' => 'authcache_admin_lookup',
    'access arguments' => array('administer site configuration'),
    'file' => 'authcache.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 30,
  );
  */
  $items['authcache/ahah'] = array(
    'title' => 'Javascript Ahah Callback',
    'page callback' => 'authcache_ahah',
    'access arguments' => array('administer site configuration'),
    'file' => 'authcache.admin.inc',
    'type' => MENU_CALLBACK,
  );


  return $items;
}

/**
 * Implements hook_init().
 */
function authcache_init() {
  global $user, $is_page_authcache;

  // Can this page request be cached?
  if (_authcache_is_cacheable()) {
    global $conf;
    
    $is_page_authcache = TRUE;
    
    // Don't allow format_date() to use the user's local timezone
    $conf['configurable_timezones'] = FALSE;
    
    // Debug mode?
    if (variable_get('authcache_debug_all', FALSE)) {
      drupal_add_js(array('authcacheDebug' => 1), 'setting');
    }

    drupal_add_js(drupal_get_path('module', 'authcache') .'/js/authcache.js', 'module', 'header');
    
    // Initialize Authcache after all other JS is loaded
    drupal_add_js('jQuery(function() { Authcache.init(); });', 'inline', 'header');

    // Forcibly disable Drupal's built-in SQL caching
    // (No need to cache page twice for anonymous users)
    if (!$user->uid && variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED) {
      variable_set('cache', CACHE_DISABLED);
    }

    // drupal_set_message('Caching disabled.'); // Testing (serves "cached" page but won't save to cache)
    
    register_shutdown_function('_authcache_shutdown_save_page');
    ob_start();
  }

  // Remove debug cookies
  if (isset($_COOKIE['authcache_debug']) && !variable_get('authcache_debug_all', FALSE)) {
    if (!$user->uid || !in_array($user->name, variable_get('authcache_debug_users',array()))) {
      setcookie('authcache_debug', "", time() - 84000); // Delete JS cookie
      setcookie('authcache_debug', "", time() - 84000, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      setcookie('nocache', 1, 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      setcookie('nocache_temp', 1, 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
    }
  }  
}

/**
 * Implements hook_user().
 */
function authcache_user($op, &$edit, &$account, $category = NULL) {
  global $is_page_authcache;

  // Cookie expiration
  $expires = ini_get('session.cookie_lifetime');
  $expires = (!empty($expires) && is_numeric($expires) ? time() + (int)$expires : 0);

  $no_cache = ($account->uid == 1 || !isset($_COOKIE['has_js']));

  switch ($op) {
    case 'load':
      break;
    case 'update':
      break;
    case 'insert':
      break;
    case 'register':
      break;
    case 'login':
      
      if ($no_cache) {
        setcookie('nocache', 1, $expires, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      } else {
        setcookie('authcache', _authcache_key($account), $expires, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      }
      
      // Authcache debugging
      if (in_array($account->name, variable_get('authcache_debug_users',array()))) {
        setcookie('authcache_debug', 1, $expires, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      }
      
      // Required to differentiate from anonymous users
      setcookie('drupal_user', $account->name, $expires, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      setcookie('drupal_uid', $account->uid, $expires, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      break;
    case 'logout':
      // Note: include same cookie deletion in ajax_authcache.module
      setcookie('drupal_user', "", time() - 86400, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      setcookie('drupal_uid', "", time() - 86400, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      setcookie('authcache', "", time() - 86400, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');

      if (isset($_COOKIE['nocache'])) {
        setcookie('nocache', "", time() - 86400, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      }
      if (isset($_COOKIE['authcache_debug'])) {
        setcookie('authcache_debug', "", time() - 86400, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      }
      
      foreach($_COOKIE as $key => $value) {
        if (substr($key, 0, 3) == 'nid') {
          setcookie($key, "", time() - 86400, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
        }
      }

      break;
    default:
      break;
  }
}

/**
 * Implements hook_form_alter().
 */
function authcache_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  global $user, $is_page_authcache;
  
  if ($is_page_authcache) {
    switch ($op) {
      case 'load':
        // Don't cache poll results
        if ($node->type == 'poll') {
          if (user_access('vote on polls') && $node->active) {
            $node->allowvotes = TRUE;
          }
        }
        break;
    }
  }
}

/**
 * Implements hook_form_alter(),
 */
function authcache_form_alter(&$form, $form_state, $form_id) {
  global $user, $is_page_authcache, $is_ajax_authcache;
  
  // Forms for logged-in users
  if ($user->uid && $is_page_authcache) {

    // Remove user-specific form token
    if (isset($form['form_token'])) {
      if (isset($form['form_token']['#default_value'])) $form['form_token']['#default_value'] = '';
      if (isset($form['form_token']['#value'])) $form['form_token']['#value'] = '';
    }   

    // Token will be generated via ajax_authcache.php, but correct id is needed
    $form['form_token_id'] = array(
      '#type' => 'hidden',
      '#value' => (isset($form['#token'])) ? $form['#token'] : $form_id,
    );
    
    // Views exposed form (Views uses custom form rendering functions)
    if (is_array($form['#theme']) && in_array('views_exposed_form', $form['#theme'])) {
      unset($form['#token']); // Prevents validation error
      unset($form['form_token_id']);
    }
    
    // Modify specific forms
    switch ($form_id) {
      // Don't cache name on comment form
      case 'comment_form':
        $form['_author']['#value'] = '<span class="authcache-user"></span>';
        break;
      
      // Remove default values on contact form (hook_authcache_ajax will retrieve defaults)
      case 'contact_mail_page':
        $form['name']['#default_value'] = $form['mail']['#default_value'] = '';
        $form['form_token_id']['#value'] = 'contact_mail_page'; // Don't use user's email
        break;
        
      //  /user/%uid/contact URL shouldn't even be cached
      case 'contact_mail_user': 
        break;
    }
  }
  
  // Anonymous & authenticatd cacheable forms
  if ($is_page_authcache) {
    // Forms won't be cached on cached pages, so no need for build ids
    unset($form['form_build_id']);
    unset($form['#build_id']);
  }
  
  // Forms being rendered during Ajax phase
  if ($is_ajax_authcache) {
    $form['#action'] = "";
  }

  if ($is_page_authcache || $is_ajax_authcache) {
    switch ($form_id) {
      // poll vote/results form may be ajax; must keep track of submit for cache invalidation
      case 'poll_view_voting':
        $form['vote']['#submit'][] = 'authcache_form_submit';
        break;
      case 'poll_cancel_form':
        $form['submit']['#submit'][] = 'authcache_form_submit';
        break;
    }
  }
  
  // Alter all forms
  switch ($form_id) {
    // Alter Drupal's "Performance" admin form
    case 'system_performance_settings':
      $form['page_cache']['#description'] .= ' <strong>'.t('If Authcache is enabled for the "anonymous user" role, Drupal\'s built-in page caching will be automatically disabled since all page caching is done through Authcache API instead of Drupal core.').'</strong>';
      $account->uid = 0;
      if (_authcache_is_account_cacheable($account)) {
        $form['page_cache']['cache']['#options'] = array(0 => t('Disabled') . ' ' . t('by') . ' Authcache');
      }
      break;
    case 'user_profile_form':
      // Don't allow user local timezone
      if (_authcache_is_account_cacheable()) {
        unset($form['timezone']);
      }
      break;
  }
  
}

/**
 * Generic form submit handler
 * Set nid cookie for cache invalidation (e.g., poll node)
 */
function authcache_form_submit(&$form, &$form_state) {

  $nid = false;
  if (isset($form['#node']) && $form['#node']->type == 'poll') {
    $nid = $form['#node']->nid;
  }
  
  if (isset($form['#nid'])) {
    $nid = $form['#nid'];
  }
  
  if ($nid) {
    setcookie('nid' . $nid, time(), 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
  }
}

/**
 * Implements hook_link_alter().
 */
function authcache_link_alter(&$links, $node) {
  global $is_page_authcache;
  if ($is_page_authcache) {
    // Remove number of new/unread comments for user
    unset($links['comment_new_comments']); 
  }
}

/**
 * Implements hook_exit(). 
 *
 * Called on drupal_goto() redirect.
 * Make sure status messages show up, if applicable.
 */
function authcache_exit($destination = NULL) {
  global $is_page_authcache;

  if ($destination !== NULL) {
    $is_page_authcache = FALSE;

    // Cookie will inform Authcache not to display cached page on next request
    if (drupal_set_message()) {
      setcookie('nocache', 1, 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      setcookie('nocache_temp', 1, 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
    }
  }
}

/**
 * Implements hook_theme_registry_alter().
 */
function authcache_theme_registry_alter(&$theme_registry) {
  // Best way to modify $links variable in comment view??
  // (Since the comment.module preprocess function flatens $links array)
  $theme_registry['comment_view']['function'] = 'theme_authcache_comment_view';
}


//
// Preprocess functions
//

/**
 * Process all template variables
 */
function authcache_preprocess(&$variables, $hook) {
  global $is_page_authcache;
  
  // Define variables for templates files
  
  $variables['is_page_authcache'] = $is_page_authcache;

  if ($variables['user']->uid) {
    $variables['user_name'] = ($is_page_authcache)
      ? '<span class="authcache-user"></span>'
      : $variables['user']->name;

    $variables['user_link'] = ($is_page_authcache)
      ? '<a href="" class="authcache-user-link">!username</a>'
      : l($variables['user']->name, "user", array('alias' => TRUE));
  }
}

/**
 * Process page template variables
 */
function authcache_preprocess_page(&$variables) {
  global $is_page_authcache;

  if ($variables['messages']) { // Status message(s) found, do not cache page
    $is_page_authcache = $variables['is_page_authcache'] = FALSE;

    global $authcache_debug_info;
    $authcache_debug_info = 'Status message displayed.';
  }
  
  // Required if tabs need to be generated dynamically
  if ($is_page_authcache) {
    $variables['tabs'] = '<span id="authcache-tabs">' . $variables['tabs'] . '</span>';
  }
}

/**
 * Process block template variables
 */
function authcache_preprocess_block(&$variables) {
  if ($variables['is_page_authcache']) {
    // User navigation block
    if ($variables['block']->module == 'user' && $variables['user']->name == $variables['block']->subject) {
      $variables['block']->subject = '<span class="authcache-user"></span>';
    }
  }
}

/**
 * Process comment template variables
 *
 * @see comment.module
 * Remove "new"
 */
function authcache_preprocess_comment_folded(&$variables) {
  if ($variables['is_page_authcache']) {
    $variables['new'] = '';
  }
}

/**
 * Process comment template variables
 *
 * @see comment.module
 * Replace "new" marker with empty span containing timestamp info
 * Add "edit" uid span for JS phase
 */
function authcache_preprocess_comment(&$variables) {

  // Will use Ajax to determine whether to display "new" marker for user
  if ($variables['is_page_authcache']) {
    $variables['new'] = '<span class="authcache-comment-new timestamp-' . $variables['comment']->timestamp . '"></span>';

    // These comments are still editable
    if ($variables['user']->uid && comment_num_replies($variables['comment']->cid) == 0) {
      $variables['links'] .= '<span class="authcache-comment-edit comment-uid-' . $variables['comment']->uid . ' comment-id-' . $variables['comment']->cid . '"></span>';
    }
  }
}


/**
 * Process forum template variables
 *
 * @see forum.module
 * Remove "new" marker
 */
function authcache_preprocess_forum_list(&$variables) {

  // Will use Ajax to determine whether to display "new" marker for user
  if ($variables['is_page_authcache']) {
    foreach ($variables['forums'] as $id => $forum) {
      if ($variables['user']->uid) {
        if ($forum->num_topics) {
          $forum->num_topics .= '<span class="authcache-topic-new forum-id-' . $id . '"></span>';
        }
      }
      $variables['forums'][$id]->new_text = '';
      $variables['forums'][$id]->new_url = '';
    }
  }
}

/**
 * Process forum template variables
 *
 * @see forum.module
 * Remove "new" marker
 */
function authcache_preprocess_forum_topic_list(&$variables) {
  if ($variables['is_page_authcache']) {
    if (!empty($variables['topics'])) {
      foreach ($variables['topics'] as $id => $topic) {
        $nid = $variables['topics'][$id]->nid;

        // Replace "new" icons.  If you are using custom icons, make sure
        // the filenames have the same format as Drupal core
        $icon = str_replace('hot-new', 'hot', $variables['topics'][$id]->icon);
        $icon = str_replace('new', 'default', $variables['topics'][$id]->icon);
        $variables['topics'][$id]->icon =
          '<span class="authcache-topic-icon nid-' . $nid . '">' .
          $icon .
          '</span>';
          
        $variables['topics'][$id]->title .= '<span class="authcache-topic-info timestamp-' . $variables['topics'][$id]->timestamp . ' nid-' . $nid . '"></span>';
        
        // "New" reply count will be handle via Ajax
        if ($topic->num_comments) {
          $variables['topics'][$id]->num_comments .= '<span class="authcache-topic-replies nid-' . $nid . '"></span>';
          $variables['topics'][$id]->new_text = '';
          $variables['topics'][$id]->new_url = '';
        }
      }
    }
  }
}

/**
 * Save poll node id
 *
 * @see poll.module
 */
function authcache_preprocess_poll_vote(&$variables) {
  if ($variables['is_page_authcache']) {
    $variables['choice'] .= '<span class="authcache-poll nid-' . $variables['form']['#node']->nid . '"></span>';
  }
}

//
// Theme overrides
//

/**
 * Overridden to handle $links
 * @see comment.module
 */
function theme_authcache_comment_view($comment, $node, $links = array(), $visible = TRUE) {
  global $user, $is_page_authcache;
  
  if ($user->uid && $is_page_authcache) {
    unset($links['comment_edit']);
  }
  
  return theme_comment_view($comment, $node, $links, $visible);
}

//
// Authcache hooks
//

/**
 * Implements hook_authcache_ajax().
 *
 * Modifies footer JSON for Ajax call
 */
function authcache_authcache_ajax() {
  global $user;
  
  $authcache_ajax = array();

  $node = (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == '') ? node_load(arg(1)) : FALSE;

  // @see node.module
  if ($user->uid && $node) {
    $authcache_ajax['node_history'] = arg(1);
  }
  
  // @see contact.module
  if ($user->uid && arg(0) == 'contact') {
    $authcache_ajax['contact'] = 1;
  }

  // @see statistics.module
  if (module_exists('statistics')) {
    if ($node && variable_get('statistics_count_content_views', 0)) {
      $authcache_ajax['statistics'] = 1;
    }

    if (variable_get('statistics_enable_access_log', 0) && (module_invoke('throttle', 'status') == 0)) {
      $authcache_ajax['statistics'] = 1;
    }
  }
  
  return $authcache_ajax;
}

/**
 * Implements hook_authcache_info().
 *
 * Modifies footer JSON for JavaScript info
 */
function authcache_authcache_info() {
  global $user;
  
  $authcache_info = array();
  
  $node = (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == '') ? node_load(arg(1)) : FALSE;

  if ($node) {
    $authcache_info['node_author'] = $node->name;
    
    // Comment functionality for users
    if ($user->uid && $node->comment_count) {
      $authcache_info['t']['new'] = t('new');   // "new" marker
      $authcache_info['t']['edit'] = t('edit'); // "edit" marker
      $authcache_info['comment_usertime'] = node_last_viewed($node->nid); // For first page request and to inform JS phase that comments exist
    }

    // Hide "Edit" primary tab
    // This assumes standard "edit any [xxx]" permission format
    
    // For custom node modules, get permissions format
    $edit_any_string = FALSE;
    if (function_exists("{$node->type}_perm")) {
      $funcName = "{$node->type}_perm";
      foreach($funcName() as $perm) {
        if (strpos($perm, "edit any") !== FALSE) {
          $edit_any_string = $perm;
        }
      }
    }
    
    if (
      $user->uid &&
      !user_access('administer nodes', $user) &&
      !user_access('edit any '. $node->type .' content', $user)
      &&
      (!$edit_any_string || ($edit_any_string && !user_access($edit_any_string, $user)))
      ) {

      $authcache_info['tab_hide'][] = t('Edit');
    }
  }
  
  return $authcache_info;
}