CRM-14676 - need to escape names before inserting into prev_next_cache
[civicrm-core.git] / CRM / Contact / Selector.php
index 14261fe63c9426d59cb2d2daf50f9118fc83fe27..ad3f6f119b2216522acfc11037052d98895d2103 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
  +--------------------------------------------------------------------+
- | CiviCRM version 4.3                                                |
+ | CiviCRM version 4.4                                                |
  +--------------------------------------------------------------------+
  | Copyright CiviCRM LLC (c) 2004-2013                                |
  +--------------------------------------------------------------------+
@@ -326,6 +326,19 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
    */
   function &getColumnHeaders($action = NULL, $output = NULL) {
     $headers = NULL;
+
+    // unset return property elements that we don't care
+    if (!empty($this->_returnProperties)) {
+      $doNotCareElements = array(
+        'contact_type',
+        'contact_sub_type',
+        'sort_name',
+      );
+      foreach ( $doNotCareElements as $value) {
+        unset($this->_returnProperties[$value]);
+      }
+    }
+
     if ($output == CRM_Core_Selector_Controller::EXPORT) {
       $csvHeaders = array(ts('Contact Id'), ts('Contact Type'));
       foreach ($this->getColHeads($action, $output) as $column) {
@@ -428,10 +441,6 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
       $properties = self::makeProperties($this->_returnProperties);
 
       foreach ($properties as $prop) {
-        if ($prop == 'contact_type' || $prop == 'contact_sub_type' || $prop == 'sort_name') {
-          continue;
-        }
-
         if (strpos($prop, '-')) {
           list($loc, $fld, $phoneType) = CRM_Utils_System::explode('-', $prop, 3);
           $title = $this->_query->_fields[$fld]['title'];
@@ -442,7 +451,8 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
         }
         elseif (isset($this->_query->_fields[$prop]) && isset($this->_query->_fields[$prop]['title'])) {
           $title = $this->_query->_fields[$prop]['title'];
-        } else {
+        }
+        else {
           $title = '';
         }
 
@@ -467,7 +477,15 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
    * @access public
    */
   function getTotalCount($action) {
-    return $this->_query->searchQuery(0, 0, NULL, TRUE);
+    // Use count from cache during paging/sorting
+    if (!empty($_GET['crmPID']) || !empty($_GET['crmSID'])) {
+      $count = CRM_Core_BAO_Cache::getItem('Search Results Count', $this->_key);
+    }
+    if (empty($count)) {
+      $count = $this->_query->searchQuery(0, 0, NULL, TRUE);
+      CRM_Core_BAO_Cache::setItem($count, 'Search Results Count', $this->_key);
+    }
+    return $count;
   }
 
   /**
@@ -498,7 +516,13 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
     // note the formvalues were given by CRM_Contact_Form_Search to us
     // and contain the search criteria (parameters)
     // note that the default action is basic
-    $result = $this->_query->searchQuery($offset, $rowCount, $sort, FALSE, $includeContactIds);
+    if ($rowCount) {
+      $cacheKey = $this->buildPrevNextCache($sort);
+      $result = $this->_query->getCachedContacts($cacheKey, $offset, $rowCount, $includeContactIds);
+    }
+    else {
+      $result = $this->_query->searchQuery($offset, $rowCount, $sort, FALSE, $includeContactIds);
+    }
 
     // process the result of the query
     $rows = array();
@@ -575,34 +599,7 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
     $groupID = CRM_Utils_Array::key('1', $this->_formValues['group']);
 
     $pseudoconstants = array();
-    if (!empty($this->_fields)) {
-      // get all the pseudoconstant values
-      foreach ($this->_fields as $name => $values) {
-        if (isset($this->_fields[$name]['pseudoconstant'])) {
-          $pseudoconstants[$name] =
-            array(
-              'dbName' => $this->_fields[$name]['name'],
-              'values' => CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', $name),
-            );
-        }
-      }
-    }
-
     // for CRM-3157 purposes
-    if (in_array('country', $names)) {
-      $pseudoconstants['country'] = array(
-        'dbName' => 'country_id',
-        'values' => CRM_Core_PseudoConstant::country()
-      );
-    }
-
-    if (in_array('state_province', $names)) {
-      $pseudoconstants['state_province'] = array(
-        'dbName' => 'state_province_id',
-        'values' => CRM_Core_PseudoConstant::stateProvince()
-      );
-    }
-
     if (in_array('world_region', $names)) {
       $pseudoconstants['world_region'] = array(
         'dbName' => 'world_region_id',
@@ -613,6 +610,7 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
     $seenIDs = array();
     while ($result->fetch()) {
       $row = array();
+      $this->_query->convertToPseudoNames($result);
 
       // the columns we are interested in
       foreach ($names as $property) {
@@ -780,35 +778,38 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
       }
     }
 
-    $this->buildPrevNextCache($sort);
-
     return $rows;
   }
 
   function buildPrevNextCache($sort) {
-    $cacheKey = CRM_Utils_Array::value('qfKey', $this->_formValues);
+    $cacheKey = 'civicrm search ' . $this->_key;
+
+    // Get current page requested
+    $pageNum = CRM_Utils_Request::retrieve('crmPID', 'Integer', CRM_Core_DAO::$_nullObject);
+    // When starting from scratch, clear any old cache
+    if (!$pageNum) {
+      CRM_Core_BAO_PrevNextCache::deleteItem(NULL, $cacheKey, 'civicrm_contact');
+      $pageNum = 1;
+    }
 
-    //for prev/next pagination
-    $crmPID = CRM_Utils_Request::retrieve('crmPID', 'Integer', CRM_Core_DAO::$_nullObject);
+    $pageSize = CRM_Utils_Request::retrieve('crmRowCount', 'Integer', CRM_Core_DAO::$_nullObject, FALSE, 50);
+    $firstRecord = ($pageNum - 1) * $pageSize;
 
     //for alphabetic pagination selection save
     $sortByCharacter = CRM_Utils_Request::retrieve('sortByCharacter', 'String', CRM_Core_DAO::$_nullObject);
 
     //for text field pagination selection save
-    $countRow = CRM_Core_BAO_PrevNextCache::getCount("%civicrm search {$cacheKey}%", NULL, "entity_table = 'civicrm_contact'", "LIKE");
+    $countRow = CRM_Core_BAO_PrevNextCache::getCount($cacheKey, NULL, "entity_table = 'civicrm_contact'");
 
-    if ((!$crmPID || $countRow == 0) && !$sortByCharacter) {
-      $this->fillupPrevNextCache($sort);
+    // $sortByCharacter triggers a refresh in the prevNext cache
+    if ($sortByCharacter && $sortByCharacter != 'all') {
+      $cacheKey .= "_alphabet";
+      $this->fillupPrevNextCache($sort, $cacheKey);
     }
-    elseif ($sortByCharacter) {
-      $cacheKeyCharacter = "civicrm search {$cacheKey}_alphabet";
-      if ($sortByCharacter == 'all') {
-        //delete the alphabet key corresponding records in prevnext_cache
-        CRM_Core_BAO_PrevNextCache::deleteItem(NULL, $cacheKeyCharacter, 'civicrm_contact');
-        $cacheKeyCharacter = NULL;
-      }
-      $this->fillupPrevNextCache($sort, $cacheKeyCharacter);
+    elseif ($firstRecord >= $countRow) {
+      $this->fillupPrevNextCache($sort, $cacheKey, $countRow, $firstRecord + 500);
     }
+    return $cacheKey;
   }
 
   function addActions(&$rows) {
@@ -885,24 +886,24 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
     }
   }
 
-  function fillupPrevNextCache($sort, $cacheKey = NULL) {
-    if (!$cacheKey) {
-      $cacheKey = "civicrm search {$this->_key}";
-    }
-
-    CRM_Core_BAO_PrevNextCache::deleteItem(NULL, $cacheKey, 'civicrm_contact');
-
-    // lets fill up the prev next cache here, so view can scroll thru
+  /**
+   * @param object $sort
+   * @param string $cacheKey
+   * @param int $start
+   * @param int $end
+   */
+  function fillupPrevNextCache($sort, $cacheKey, $start = 0, $end = 500) {
+    $coreSearch = TRUE;
+    // For custom searches, use the contactIDs method
     if (is_a($this, 'CRM_Contact_Selector_Custom')) {
-      $sql = $this->_search->contactIDs(0, 0, $sort, TRUE);
+      $sql = $this->_search->contactIDs($start, $end, $sort, TRUE);
       $replaceSQL = "SELECT contact_a.id as contact_id";
+      $coreSearch = FALSE;
     }
+    // For core searches use the searchQuery method
     else {
-      $sql = $this->_query->searchQuery(
-        0, 0, $sort,
-        FALSE, FALSE,
-        FALSE, TRUE, TRUE, NULL
-      );
+      $sql = $this->_query->searchQuery($start, $end, $sort, FALSE, $this->_query->_includeContactIds,
+        FALSE, TRUE, TRUE);
       $replaceSQL = "SELECT contact_a.id as id";
     }
 
@@ -922,22 +923,59 @@ SELECT 'civicrm_contact', contact_a.id, contact_a.id, '$cacheKey', contact_a.dis
 
     $sql = str_replace($replaceSQL, $insertSQL, $sql);
 
-
     CRM_Core_Error::ignoreException();
     $result = CRM_Core_DAO::executeQuery($sql);
     CRM_Core_Error::setCallback();
 
     if (is_a($result, 'DB_Error')) {
-      // oops the above query failed, so lets just ignore it
-      // and return
-      // we print a sorry cant figure it out on view page
-      return;
+      // check if we get error during core search
+      if ($coreSearch) {
+        // in the case of error, try rebuilding cache using full sql which is used for search selector display
+        // this fixes the bugs reported in CRM-13996 & CRM-14438
+        $this->rebuildPreNextCache($start, $end, $sort, $cacheKey);
+      }
+      else {
+        // return if above query fails
+        return;
+      }
     }
 
     // also record an entry in the cache key table, so we can delete it periodically
     CRM_Core_BAO_Cache::setItem($cacheKey, 'CiviCRM Search PrevNextCache', $cacheKey);
   }
 
+  /**
+   * This function is called to rebuild prev next cache using full sql in case of core search ( excluding custom search)
+   *
+   * @param int $start start for limit clause
+   * @param int $end end for limit clause
+   * @param $object $sort sort object
+   * @param string $cacheKey cache key
+   *
+   * @return void
+   */
+  function rebuildPreNextCache($start, $end, $sort, $cacheKey) {
+    // generate full SQL
+    $sql = $this->_query->searchQuery($start, $end, $sort, FALSE, $this->_query->_includeContactIds,
+      FALSE, FALSE, TRUE);
+
+    $dao = CRM_Core_DAO::executeQuery($sql);
+
+    // build insert query, note that currently we build cache for 500 contact records at a time, hence below approach
+    $insertValues = array();
+    while($dao->fetch()) {
+      $insertValues[] = "('civicrm_contact', {$dao->contact_id}, {$dao->contact_id}, '{$cacheKey}', '" . CRM_Core_DAO::escapeString($dao->sort_name) . "')";
+    }
+
+    //update pre/next cache using single insert query
+    if (!empty($insertValues)) {
+      $sql = 'INSERT INTO civicrm_prevnext_cache ( entity_table, entity_id1, entity_id2, cacheKey, data ) VALUES
+'.implode(',', $insertValues);
+
+      $result = CRM_Core_DAO::executeQuery($sql);
+    }
+  }
+
   /**
    * Given the current formValues, gets the query in local
    * language
@@ -1073,9 +1111,16 @@ SELECT 'civicrm_contact', contact_a.id, contact_a.id, '$cacheKey', contact_a.dis
     $properties = array();
     foreach ($returnProperties as $name => $value) {
       if ($name != 'location') {
+        // special handling for group and tag
         if (in_array($name, array('group', 'tag'))) {
           $name = "{$name}s";
         }
+
+        // special handling for notes
+        if (in_array($name, array('note', 'note_subject', 'note_body'))) {
+          $name = "notes";
+        }
+
         $properties[] = $name;
       }
       else {