/**
  * Will acquire a lock with the given name, 
  * if no other lock has been acquired by this process.
  * 
  * If the same lock has been acquired before (and not been released),
  * in internal counter is increased. Therefore you can acquire the same
  * lock multiple times, but you will then have to release them 
  * the same amount of times
  *
  * @return a SafeLock instance or NULL if timed out
  */
 public static function acquireLock($name, $timeout = 60)
 {
     if (self::$_acquired_lock == NULL) {
         // it's free, we'll try to take it
         $lock = new CRM_Core_Lock($name, $timeout);
         if (version_compare(CRM_Utils_System::version(), '4.6', '>=')) {
             // before 4.6, a new lock would be automatically acquired
             $lock->acquire();
         }
         if ($lock != NULL && $lock->isAcquired()) {
             // we got it!
             self::$_acquired_lock = new CRM_Utils_SepaSafeLock($lock, $name);
             //error_log('acquired ' . getmypid());
             return self::$_acquired_lock;
         } else {
             // timed out
             return NULL;
         }
     } elseif (self::$_acquired_lock->getName() == $name) {
         // this means acquiring 'our' lock again:
         $lock = self::$_acquired_lock;
         $lock->counter += 1;
         //error_log('acquired ' . getmypid() . "[{$lock->counter}]");
         return $lock;
     } else {
         // this is the BAD case: somebody's trying to acquire ANOTHER LOCK,
         //  while we still own another one
         $lock_name = $self::$_acquired_lock->getName();
         throw new Exception("This process cannot acquire more than one lock! It still owns lock '{$lock_name}'.");
     }
 }
/**
 * Mailjet.ProcessBounces API
 *
 * @param array $params
 * @return array API result descriptor
 * @see civicrm_api3_create_success
 * @see civicrm_api3_create_error
 * @throws API_Exception
 */
function civicrm_api3_mailjet_processbounces($params)
{
    $lock = new CRM_Core_Lock('civimail.job.MailjetProcessor');
    if (!$lock->isAcquired()) {
        return civicrm_api3_create_error('Could not acquire lock, another MailjetProcessor process is running');
    }
    $mailingId = CRM_Utils_Array::value('mailing_id', $params);
    //G: this is called when click on "Manually refresh Mailjet's stats" button
    if (!CRM_Utils_Mail_MailjetProcessor::processBounces($mailingId)) {
        $lock->release();
        return civicrm_api3_create_error('Process Bounces failed');
    }
    $lock->release();
    // FIXME: processBounces doesn't return true/false on success/failure
    $values = array();
    return civicrm_api3_create_success($values, $params, 'mailjet', 'bounces');
}
 function run()
 {
     require_once 'CRM/Core/Lock.php';
     $lock = new CRM_Core_Lock('CiviReportMail');
     if ($lock->isAcquired()) {
         // try to unset any time limits
         if (!ini_get('safe_mode')) {
             set_time_limit(0);
         }
         // if there are named sets of settings, use them - otherwise use the default (null)
         require_once 'CRM/Report/Utils/Report.php';
         $result = CRM_Report_Utils_Report::processReport();
         echo $result['messages'];
     } else {
         throw new Exception('Could not acquire lock, another CiviReportMail process is running');
     }
     $lock->release();
 }
    } else {
        CRM_Utils_Mail_EmailProcessor::processBounces();
    }
    $lock->release();
} else {
    session_start();
    require_once '../civicrm.config.php';
    require_once 'CRM/Core/Config.php';
    $config = CRM_Core_Config::singleton();
    CRM_Utils_System::authenticateScript(TRUE);
    require_once 'CRM/Utils/System.php';
    CRM_Utils_System::loadBootStrap();
    //log the execution of script
    CRM_Core_Error::debug_log_message('EmailProcessor.php');
    require_once 'CRM/Core/Lock.php';
    $lock = new CRM_Core_Lock('EmailProcessor');
    if (!$lock->isAcquired()) {
        throw new Exception('Could not acquire lock, another EmailProcessor process is running');
    }
    // try to unset any time limits
    if (!ini_get('safe_mode')) {
        set_time_limit(0);
    }
    require_once 'CRM/Utils/Mail/EmailProcessor.php';
    // cleanup directories with old mail files (if they exist): CRM-4452
    CRM_Utils_Mail_EmailProcessor::cleanupDir($config->customFileUploadDir . DIRECTORY_SEPARATOR . 'CiviMail.ignored');
    CRM_Utils_Mail_EmailProcessor::cleanupDir($config->customFileUploadDir . DIRECTORY_SEPARATOR . 'CiviMail.processed');
    // check if the script is being used for civimail processing or email to
    // activity processing.
    $isCiviMail = !empty($_REQUEST['emailtoactivity']) ? FALSE : TRUE;
    CRM_Utils_Mail_EmailProcessor::process($isCiviMail);
Exemplo n.º 5
0
 /**
  * before we run jobs, we need to split the jobs
  * @param int $offset
  * @param null $mode
  */
 public static function runJobs_pre($offset = 200, $mode = NULL)
 {
     $job = new CRM_Mailing_BAO_MailingJob();
     $jobTable = CRM_Mailing_DAO_MailingJob::getTableName();
     $mailingTable = CRM_Mailing_DAO_Mailing::getTableName();
     $currentTime = date('YmdHis');
     $mailingACL = CRM_Mailing_BAO_Mailing::mailingACL('m');
     $workflowClause = CRM_Mailing_BAO_MailingJob::workflowClause();
     $domainID = CRM_Core_Config::domainID();
     $modeClause = 'AND m.sms_provider_id IS NULL';
     if ($mode == 'sms') {
         $modeClause = 'AND m.sms_provider_id IS NOT NULL';
     }
     // Select all the mailing jobs that are created from
     // when the mailing is submitted or scheduled.
     $query = "\n    SELECT   j.*\n      FROM   {$jobTable}     j,\n         {$mailingTable} m\n     WHERE   m.id = j.mailing_id AND m.domain_id = {$domainID}\n                 {$workflowClause}\n                 {$modeClause}\n       AND   j.is_test = 0\n       AND   ( ( j.start_date IS null\n       AND       j.scheduled_date <= {$currentTime}\n       AND       j.status = 'Scheduled'\n       AND       j.end_date IS null ) )\n       AND ((j.job_type is NULL) OR (j.job_type <> 'child'))\n    ORDER BY j.scheduled_date,\n         j.start_date";
     $job->query($query);
     // For each of the "Parent Jobs" we find, we split them into
     // X Number of child jobs
     while ($job->fetch()) {
         // still use job level lock for each child job
         $lockName = "civimail.job.{$job->id}";
         $lock = new CRM_Core_Lock($lockName);
         if (!$lock->isAcquired()) {
             continue;
         }
         // Re-fetch the job status in case things
         // changed between the first query and now
         // to avoid race conditions
         $job->status = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingJob', $job->id, 'status', 'id', TRUE);
         if ($job->status != 'Scheduled') {
             $lock->release();
             continue;
         }
         $job->split_job($offset);
         // update the status of the parent job
         $transaction = new CRM_Core_Transaction();
         $saveJob = new CRM_Mailing_DAO_MailingJob();
         $saveJob->id = $job->id;
         $saveJob->start_date = date('YmdHis');
         $saveJob->status = 'Running';
         $saveJob->save();
         $transaction->commit();
         // Release the job lock
         $lock->release();
     }
 }
 /**
  * load the smart group cache for a saved search
  *
  * @param object  $group - the smart group that needs to be loaded
  * @param boolean $force - should we force a search through
  *
  */
 static function load(&$group, $force = FALSE)
 {
     $groupID = $group->id;
     $savedSearchID = $group->saved_search_id;
     if (array_key_exists($groupID, self::$_alreadyLoaded) && !$force) {
         return;
     }
     // grab a lock so other processes dont compete and do the same query
     $lockName = "civicrm.group.{$groupID}";
     $lock = new CRM_Core_Lock($lockName);
     if (!$lock->isAcquired()) {
         // this can cause inconsistent results since we dont know if the other process
         // will fill up the cache before our calling routine needs it.
         // however this routine does not return the status either, so basically
         // its a "lets return and hope for the best"
         return;
     }
     self::$_alreadyLoaded[$groupID] = 1;
     // we now have the lock, but some other proces could have actually done the work
     // before we got here, so before we do any work, lets ensure that work needs to be
     // done
     // we allow hidden groups here since we dont know if the caller wants to evaluate an
     // hidden group
     if (!$force && !self::shouldGroupBeRefreshed($groupID, TRUE)) {
         $lock->release();
         return;
     }
     $sql = NULL;
     $idName = 'id';
     $customClass = NULL;
     if ($savedSearchID) {
         $ssParams = CRM_Contact_BAO_SavedSearch::getSearchParams($savedSearchID);
         // rectify params to what proximity search expects if there is a value for prox_distance
         // CRM-7021
         if (!empty($ssParams)) {
             CRM_Contact_BAO_ProximityQuery::fixInputParams($ssParams);
         }
         $returnProperties = array();
         if (CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_SavedSearch', $savedSearchID, 'mapping_id')) {
             $fv = CRM_Contact_BAO_SavedSearch::getFormValues($savedSearchID);
             $returnProperties = CRM_Core_BAO_Mapping::returnProperties($fv);
         }
         if (isset($ssParams['customSearchID'])) {
             // if custom search
             // we split it up and store custom class
             // so temp tables are not destroyed if they are used
             // hence customClass is defined above at top of function
             $customClass = CRM_Contact_BAO_SearchCustom::customClass($ssParams['customSearchID'], $savedSearchID);
             $searchSQL = $customClass->contactIDs();
             $searchSQL = str_replace('ORDER BY contact_a.id ASC', '', $searchSQL);
             $idName = 'contact_id';
         } else {
             $formValues = CRM_Contact_BAO_SavedSearch::getFormValues($savedSearchID);
             $query = new CRM_Contact_BAO_Query($ssParams, $returnProperties, NULL, FALSE, FALSE, 1, TRUE, TRUE, FALSE, CRM_Utils_Array::value('display_relationship_type', $formValues), CRM_Utils_Array::value('operator', $formValues, 'AND'));
             $query->_useDistinct = FALSE;
             $query->_useGroupBy = FALSE;
             $searchSQL = $query->searchQuery(0, 0, NULL, FALSE, FALSE, FALSE, TRUE, TRUE, NULL, NULL, NULL, TRUE);
         }
         $groupID = CRM_Utils_Type::escape($groupID, 'Integer');
         $sql = $searchSQL . " AND contact_a.id NOT IN (\n                              SELECT contact_id FROM civicrm_group_contact\n                              WHERE civicrm_group_contact.status = 'Removed'\n                              AND   civicrm_group_contact.group_id = {$groupID} ) ";
     }
     if ($sql) {
         $sql = preg_replace("/^\\s*SELECT/", "SELECT {$groupID} as group_id, ", $sql);
     }
     // lets also store the records that are explicitly added to the group
     // this allows us to skip the group contact LEFT JOIN
     $sqlB = "\nSELECT {$groupID} as group_id, contact_id as {$idName}\nFROM   civicrm_group_contact\nWHERE  civicrm_group_contact.status = 'Added'\n  AND  civicrm_group_contact.group_id = {$groupID} ";
     $groupIDs = array($groupID);
     self::remove($groupIDs);
     $processed = FALSE;
     $tempTable = 'civicrm_temp_group_contact_cache' . rand(0, 2000);
     foreach (array($sql, $sqlB) as $selectSql) {
         if (!$selectSql) {
             continue;
         }
         $insertSql = "CREATE TEMPORARY TABLE {$tempTable} ({$selectSql});";
         $processed = TRUE;
         $result = CRM_Core_DAO::executeQuery($insertSql);
         CRM_Core_DAO::executeQuery("INSERT IGNORE INTO civicrm_group_contact_cache (contact_id, group_id)\n        SELECT DISTINCT {$idName}, group_id FROM {$tempTable}\n      ");
         CRM_Core_DAO::executeQuery(" DROP TABLE {$tempTable}");
     }
     self::updateCacheTime($groupIDs, $processed);
     if ($group->children) {
         //Store a list of contacts who are removed from the parent group
         $sql = "\nSELECT contact_id\nFROM civicrm_group_contact\nWHERE  civicrm_group_contact.status = 'Removed'\nAND  civicrm_group_contact.group_id = {$groupID} ";
         $dao = CRM_Core_DAO::executeQuery($sql);
         $removed_contacts = array();
         while ($dao->fetch()) {
             $removed_contacts[] = $dao->contact_id;
         }
         $childrenIDs = explode(',', $group->children);
         foreach ($childrenIDs as $childID) {
             $contactIDs = CRM_Contact_BAO_Group::getMember($childID, FALSE);
             //Unset each contact that is removed from the parent group
             foreach ($removed_contacts as $removed_contact) {
                 unset($contactIDs[$removed_contact]);
             }
             $values = array();
             foreach ($contactIDs as $contactID => $dontCare) {
                 $values[] = "({$groupID},{$contactID})";
             }
             self::store($groupIDs, $values);
         }
     }
     $lock->release();
 }
Exemplo n.º 7
0
 static function processQueue($mode = NULL)
 {
     $config =& CRM_Core_Config::singleton();
     //   CRM_Core_Error::debug_log_message("Beginning processQueue run: {$config->mailerJobsMax}, {$config->mailerJobSize}");
     if ($mode == NULL && CRM_Core_BAO_MailSettings::defaultDomain() == "EXAMPLE.ORG") {
         CRM_Core_Error::fatal(ts('The <a href="%1">default mailbox</a> has not been configured. You will find <a href="%2">more info in the online user and administrator guide</a>', array(1 => CRM_Utils_System::url('civicrm/admin/mailSettings', 'reset=1'), 2 => "http://book.civicrm.org/user/advanced-configuration/email-system-configuration/")));
     }
     // check if we are enforcing number of parallel cron jobs
     // CRM-8460
     $gotCronLock = FALSE;
     if (property_exists($config, 'mailerJobsMax') && $config->mailerJobsMax && $config->mailerJobsMax > 1) {
         $lockArray = range(1, $config->mailerJobsMax);
         shuffle($lockArray);
         // check if we are using global locks
         $serverWideLock = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME, 'civimail_server_wide_lock');
         foreach ($lockArray as $lockID) {
             $cronLock = new CRM_Core_Lock("civimail.cronjob.{$lockID}", NULL, $serverWideLock);
             if ($cronLock->isAcquired()) {
                 $gotCronLock = TRUE;
                 break;
             }
         }
         // exit here since we have enuf cronjobs running
         if (!$gotCronLock) {
             CRM_Core_Error::debug_log_message('Returning early, since max number of cronjobs running');
             return TRUE;
         }
     }
     // load bootstrap to call hooks
     // Split up the parent jobs into multiple child jobs
     $mailerJobSize = property_exists($config, 'mailerJobSize') ? $config->mailerJobSize : NULL;
     CRM_Mailing_BAO_MailingJob::runJobs_pre($mailerJobSize, $mode);
     CRM_Mailing_BAO_MailingJob::runJobs(NULL, $mode);
     CRM_Mailing_BAO_MailingJob::runJobs_post($mode);
     // lets release the global cron lock if we do have one
     if ($gotCronLock) {
         $cronLock->release();
     }
     //   CRM_Core_Error::debug_log_message('Ending processQueue run');
     return TRUE;
 }
Exemplo n.º 8
0
/**
 * This api reloads all the smart groups. If the org has a large number of smart groups
 * it is recommended that they use the limit clause to limit the number of smart groups
 * evaluated on a per job basis. Might also help to increase the smartGroupCacheTimeout
 * and use the cache
 */
function civicrm_api3_job_group_rebuild($params)
{
    $lock = new CRM_Core_Lock('civimail.job.groupRebuild');
    if (!$lock->isAcquired()) {
        return civicrm_api3_create_error('Could not acquire lock, another EmailProcessor process is running');
    }
    $limit = CRM_Utils_Array::value('limit', $params, 0);
    CRM_Contact_BAO_GroupContactCache::loadAll(null, $limit);
    $lock->release();
    return civicrm_api3_create_success();
}
Exemplo n.º 9
0
 /**
  * @return null|string
  */
 public function release()
 {
     if ($this->_hasLock) {
         if (defined('CIVICRM_LOCK_DEBUG')) {
             CRM_Core_Error::debug_log_message('release lock for ' . $this->_name);
         }
         $this->_hasLock = FALSE;
         if (self::$jobLog == $this->_name) {
             self::$jobLog = FALSE;
         }
         $query = "SELECT RELEASE_LOCK( %1 )";
         $params = array(1 => array($this->_name, 'String'));
         return CRM_Core_DAO::singleValueQuery($query, $params);
     }
 }
Exemplo n.º 10
0
 /**
  * Initiate all pending/ready jobs
  *
  * @return void
  * @access public
  * @static
  */
 public static function runJobs($testParams = null)
 {
     $job =& new CRM_Mailing_BAO_Job();
     $mailing =& new CRM_Mailing_DAO_Mailing();
     $config =& CRM_Core_Config::singleton();
     $jobTable = CRM_Mailing_DAO_Job::getTableName();
     $mailingTable = CRM_Mailing_DAO_Mailing::getTableName();
     if (!empty($testParams)) {
         $query = "\nSELECT *\n  FROM {$jobTable}\n WHERE id = {$testParams['job_id']}";
         $job->query($query);
     } else {
         $currentTime = date('YmdHis');
         /* FIXME: we might want to go to a progress table.. */
         $query = "\nSELECT   j.*\n  FROM   {$jobTable}     j,\n         {$mailingTable} m\n WHERE   m.id = j.mailing_id\n   AND   j.is_test = 0\n   AND   ( ( j.start_date IS null\n   AND       j.scheduled_date <= {$currentTime}\n   AND       j.status = 'Scheduled' )\n    OR     ( j.status = 'Running'\n   AND       j.end_date IS null ) )\nORDER BY j.scheduled_date,\n         j.start_date";
         $job->query($query);
     }
     require_once 'CRM/Core/Lock.php';
     /* TODO We should parallelize or prioritize this */
     while ($job->fetch()) {
         // fix for cancel job at run time which is in queue, CRM-4246
         if (CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_Job', $job->id, 'status') == 'Canceled') {
             continue;
         }
         $lockName = "civimail.job.{$job->id}";
         // get a lock on this job id
         $lock = new CRM_Core_Lock($lockName);
         if (!$lock->isAcquired()) {
             continue;
         }
         /* Queue up recipients for all jobs being launched */
         if ($job->status != 'Running') {
             require_once 'CRM/Core/Transaction.php';
             $transaction = new CRM_Core_Transaction();
             $job->queue($testParams);
             /* Start the job */
             // use a seperate DAO object to protect the loop
             // integrity. I think transactions messes it up
             // check CRM-2469
             $saveJob = new CRM_Mailing_DAO_Job();
             $saveJob->id = $job->id;
             $saveJob->start_date = date('YmdHis');
             $saveJob->status = 'Running';
             $saveJob->save();
             $transaction->commit();
         }
         $mailer =& $config->getMailer();
         /* Compose and deliver */
         $isComplete = $job->deliver($mailer, $testParams);
         require_once 'CRM/Utils/Hook.php';
         CRM_Utils_Hook::post('create', 'CRM_Mailing_DAO_Spool', $job->id, $isComplete);
         if ($isComplete) {
             /* Finish the job */
             require_once 'CRM/Core/Transaction.php';
             $transaction = new CRM_Core_Transaction();
             // use a seperate DAO object to protect the loop
             // integrity. I think transactions messes it up
             // check CRM-2469
             $saveJob = new CRM_Mailing_DAO_Job();
             $saveJob->id = $job->id;
             $saveJob->end_date = date('YmdHis');
             $saveJob->status = 'Complete';
             $saveJob->save();
             $mailing->reset();
             $mailing->id = $job->mailing_id;
             $mailing->is_completed = true;
             $mailing->save();
             $transaction->commit();
         }
         $lock->release();
         if ($testParams) {
             return $isComplete;
         }
     }
 }
Exemplo n.º 11
0
function civicrm_api3_job_fetch_activities($params)
{
    require_once 'CRM/Utils/Mail/EmailProcessor.php';
    require_once 'CRM/Core/Lock.php';
    $lock = new CRM_Core_Lock('EmailProcessor');
    if (!$lock->isAcquired()) {
        return civicrm_api3_create_error("Could not acquire lock, another EmailProcessor process is running");
    }
    try {
        CRM_Utils_Mail_EmailProcessor::processActivities();
        $values = array();
        $lock->release();
        return civicrm_api3_create_success($values, $params, 'mailing', 'activities');
    } catch (Exception $e) {
        $lock->release();
        return civicrm_api3_create_error("Process Activities failed");
    }
}
Exemplo n.º 12
0
 /**
  * Store an item in the DB cache
  *
  * @param object $data  (required) A reference to the data that will be serialized and stored
  * @param string $group (required) The group name of the item
  * @param string $path  (required) The path under which this item is stored
  * @param int    $componentID The optional component ID (so componenets can share the same name space)
  *
  * @return void
  * @static
  * @access public
  */
 static function setItem(&$data, $group, $path, $componentID = NULL)
 {
     if (self::$_cache === NULL) {
         self::$_cache = array();
     }
     $dao = new CRM_Core_DAO_Cache();
     $dao->group_name = $group;
     $dao->path = $path;
     $dao->component_id = $componentID;
     // get a lock so that multiple ajax requests on the same page
     // dont trample on each other
     // CRM-11234
     $lockName = "civicrm.cache.{$group}_{$path}._{$componentID}";
     $lock = new CRM_Core_Lock($lockName);
     if (!$lock->isAcquired()) {
         CRM_Core_Error::fatal();
     }
     $dao->find(TRUE);
     $dao->data = serialize($data);
     $dao->created_date = date('YmdHis');
     $dao->save();
     $lock->release();
     $dao->free();
     // cache coherency - refresh or remove dependent caches
     $argString = "CRM_CT_{$group}_{$path}_{$componentID}";
     $cache = CRM_Utils_Cache::singleton();
     $data = unserialize($dao->data);
     self::$_cache[$argString] = $data;
     $cache->set($argString, $data);
     $argString = "CRM_CT_CI_{$group}_{$componentID}";
     unset(self::$_cache[$argString]);
     $cache->delete($argString);
 }
Exemplo n.º 13
0
 public static function runJobs_pre($offset = 200)
 {
     $job = new CRM_Mailing_BAO_Job();
     $config = CRM_Core_Config::singleton();
     $jobTable = CRM_Mailing_DAO_Job::getTableName();
     $mailingTable = CRM_Mailing_DAO_Mailing::getTableName();
     $currentTime = date('YmdHis');
     $mailingACL = CRM_Mailing_BAO_Mailing::mailingACL('m');
     // add an additional check and only process
     // jobs that are approved
     $workflowClause = null;
     require_once 'CRM/Mailing/Info.php';
     if (CRM_Mailing_Info::workflowEnabled()) {
         require_once 'CRM/Core/OptionGroup.php';
         $approveOptionID = CRM_Core_OptionGroup::getValue('mail_approval_status', 'Approved', 'name');
         if ($approveOptionID) {
             $workflowClause = " AND m.approval_status_id = {$approveOptionID} ";
         }
     }
     // Select all the mailing jobs that are created from
     // when the mailing is submitted or scheduled.
     $query = "\n\t\tSELECT   j.*\n\t\t  FROM   {$jobTable}     j,\n\t\t\t\t {$mailingTable} m\n\t\t WHERE   m.id = j.mailing_id\n                 {$workflowClause}\n\t\t   AND   j.is_test = 0\n\t\t   AND   ( ( j.start_date IS null\n\t\t   AND       j.scheduled_date <= {$currentTime}\n\t\t   AND       j.status = 'Scheduled'\n\t\t   AND       j.end_date IS null ) )\n\t\t   AND ((j.job_type is NULL) OR (j.job_type <> 'child'))\n\t\tORDER BY j.scheduled_date,\n\t\t\t\t j.start_date";
     $job->query($query);
     require_once 'CRM/Core/Lock.php';
     // For reach of the "Parent Jobs" we find, we split them into
     // X Number of child jobs
     while ($job->fetch()) {
         // still use job level lock for each child job
         $lockName = "civimail.job.{$job->id}";
         $lock = new CRM_Core_Lock($lockName);
         if (!$lock->isAcquired()) {
             continue;
         }
         // refetch the job status in case things
         // changed between the first query and now
         // avoid race conditions
         $job->status = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_Job', $job->id, 'status');
         if ($job->status != 'Scheduled') {
             $lock->release();
             continue;
         }
         $job->split_job($offset);
         // update the status of the parent job
         require_once 'CRM/Core/Transaction.php';
         $transaction = new CRM_Core_Transaction();
         $saveJob = new CRM_Mailing_DAO_Job();
         $saveJob->id = $job->id;
         $saveJob->start_date = date('YmdHis');
         $saveJob->status = 'Running';
         $saveJob->save();
         $transaction->commit();
         // Release the job lock
         $lock->release();
     }
 }
/**
 * Job.iATSRecurringContributions API
 *
 * @param array $params
 * @return array API result descriptor
 * @see civicrm_api3_create_success
 * @see civicrm_api3_create_error
 * @throws API_Exception
 */
function civicrm_api3_job_iatsrecurringcontributions($params)
{
    // running this job in parallell could generate bad duplicate contributions
    $lock = new CRM_Core_Lock('civimail.job.IatsRecurringContributions');
    if (!$lock->acquire()) {
        return civicrm_api3_create_success(ts('Failed to acquire lock. No contribution records were processed.'));
    }
    $catchup = !empty($params['catchup']);
    unset($params['catchup']);
    $domemberships = empty($params['ignoremembership']);
    unset($params['ignoremembership']);
    // TODO: what kind of extra security do we want or need here to prevent it from being triggered inappropriately? Or does it matter?
    // the next scheduled contribution date field name is civicrm version dependent
    define('IATS_CIVICRM_NSCD_FID', _iats_civicrm_nscd_fid());
    // $config = &CRM_Core_Config::singleton();
    // $debug  = false;
    // do my calculations based on yyyymmddhhmmss representation of the time
    // not sure about time-zone issues
    $dtCurrentDay = date("Ymd", mktime(0, 0, 0, date("m"), date("d"), date("Y")));
    $dtCurrentDayStart = $dtCurrentDay . "000000";
    $dtCurrentDayEnd = $dtCurrentDay . "235959";
    $expiry_limit = date('ym');
    // restrict this method of recurring contribution processing to only these two payment processors
    $args = array(1 => array('Payment_iATSService', 'String'), 2 => array('Payment_iATSServiceACHEFT', 'String'), 3 => array('Payment_iATSServiceSWIPE', 'String'));
    // Before triggering payments, we need to do some housekeeping of the civicrm_contribution_recur records.
    // First update the end_date and then the complete/in-progress values.
    // We do this both to fix any failed settings previously, and also
    // to deal with the possibility that the settings for the number of payments (installments) for an existing record has changed.
    // First check for recur end date values on non-open-ended recurring contribution records that are either complete or in-progress
    $select = 'SELECT cr.id, count(c.id) AS installments_done, cr.installments, cr.end_date, NOW() as test_now
      FROM civicrm_contribution_recur cr
      INNER JOIN civicrm_contribution c ON cr.id = c.contribution_recur_id
      INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id
      WHERE
        (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3)
        AND (cr.installments > 0)
        AND (cr.contribution_status_id IN (1,5))
        AND (c.contribution_status_id IN (1,2))
      GROUP BY c.contribution_recur_id';
    $dao = CRM_Core_DAO::executeQuery($select, $args);
    while ($dao->fetch()) {
        // check for end dates that should be unset because I haven't finished
        if ($dao->installments_done < $dao->installments) {
            // at least one more installment todo
            if ($dao->end_date > 0 && $dao->end_date <= $dao->test_now) {
                // unset the end_date
                $update = 'UPDATE civicrm_contribution_recur SET end_date = NULL, contribution_status_id = 5 WHERE id = %1';
                CRM_Core_DAO::executeQuery($update, array(1 => array($dao->id, 'Int')));
            }
        } elseif ($dao->installments_done >= $dao->installments) {
            // I'm done with installments
            if (empty($dao->end_date) || $dao->end_date >= $dao->test_now) {
                // this interval complete, set the end_date to an hour ago
                $update = 'UPDATE civicrm_contribution_recur SET end_date = DATE_SUB(NOW(),INTERVAL 1 HOUR) WHERE id = %1';
                CRM_Core_DAO::executeQuery($update, array(1 => array($dao->id, 'Int')));
            }
        }
    }
    // Second, make sure any open-ended recurring contributions have no end date set
    $update = 'UPDATE civicrm_contribution_recur cr
      INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id
      SET
        cr.end_date = NULL
      WHERE
        cr.contribution_status_id IN (1,5)
        AND NOT(cr.installments > 0)
        AND (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3)
        AND NOT(ISNULL(cr.end_date))';
    $dao = CRM_Core_DAO::executeQuery($update, $args);
    // Third, we update the status_id of the all in-progress or completed recurring contribution records
    // Unexpire uncompleted cycles
    $update = 'UPDATE civicrm_contribution_recur cr
      INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id
      SET
        cr.contribution_status_id = 5
      WHERE
        cr.contribution_status_id = 1
        AND (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3)
        AND (cr.end_date IS NULL OR cr.end_date > NOW())';
    $dao = CRM_Core_DAO::executeQuery($update, $args);
    // Expire or badly-defined completed cycles
    $update = 'UPDATE civicrm_contribution_recur cr
      INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id
      SET
        cr.contribution_status_id = 1
      WHERE
        cr.contribution_status_id = 5
        AND (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3)
        AND (
          (NOT(cr.end_date IS NULL) AND cr.end_date <= NOW())
          OR
          ISNULL(cr.frequency_unit)
          OR
          (frequency_interval = 0)
        )';
    $dao = CRM_Core_DAO::executeQuery($update, $args);
    // Now we're ready to trigger payments
    // Select the ongoing recurring payments for iATSServices where the next scheduled contribution date (NSCD) is before the end of of the current day
    $select = 'SELECT cr.*, icc.customer_code, icc.expiry as icc_expiry, icc.cid as icc_contact_id, pp.class_name as pp_class_name, pp.url_site as url_site, pp.is_test
      FROM civicrm_contribution_recur cr
      INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id
      INNER JOIN civicrm_iats_customer_codes icc ON cr.id = icc.recur_id
      WHERE
        cr.contribution_status_id = 5
        AND (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3)';
    //      AND pp.is_test = 0
    if (!empty($params['recur_id'])) {
        // in case the job was called to execute a specific recurring contribution id -- not yet implemented!
        $select .= ' AND icc.recur_id = %4';
        $args[4] = array($params['recur_id'], 'Int');
    } else {
        // if (!empty($params['scheduled'])) {
        //normally, process all recurring contributions due today or earlier
        $select .= ' AND cr.' . IATS_CIVICRM_NSCD_FID . ' <= %4';
        $args[4] = array($dtCurrentDayEnd, 'String');
        // ' AND cr.next_sched_contribution >= %2
        // $args[2] = array($dtCurrentDayStart, 'String');
        if (!empty($params['cycle_day'])) {
            // also filter by cycle day
            $select .= ' AND cr.cycle_day = %5';
            $args[5] = array($params['cycle_day'], 'Int');
        }
        if (isset($params['failure_count'])) {
            // also filter by cycle day
            $select .= ' AND cr.failure_count = %6';
            $args[6] = array($params['failure_count'], 'Int');
        }
    }
    $dao = CRM_Core_DAO::executeQuery($select, $args);
    $counter = 0;
    $error_count = 0;
    $output = array();
    $settings = CRM_Core_BAO_Setting::getItem('iATS Payments Extension', 'iats_settings');
    $receipt_recurring = empty($settings['receipt_recurring']) ? 0 : 1;
    /* while ($dao->fetch()) {
        foreach($dao as $key => $value) {
          echo "$value,";
        }
        echo "\n";
      }
      die();  */
    while ($dao->fetch()) {
        // Strategy: create the contribution record with status = 2 (= pending), try the payment, and update the status to 1 if successful
        // Try to get a contribution template for this contribution series - if none matches (e.g. if a donation amount has been changed), we'll just be naive about it.
        $contribution_template = _iats_civicrm_getContributionTemplate(array('contribution_recur_id' => $dao->id, 'total_amount' => $dao->amount));
        $contact_id = $dao->contact_id;
        $total_amount = $dao->amount;
        $hash = md5(uniqid(rand(), true));
        $contribution_recur_id = $dao->id;
        $subtype = substr($dao->pp_class_name, 19);
        $source = "iATS Payments {$subtype} Recurring Contribution (id={$contribution_recur_id})";
        $receive_ts = $catchup ? strtotime($dao->next_sched_contribution_date) : time();
        $receive_date = date("YmdHis", $receive_ts);
        // i.e. now or whenever it was supposed to run if in catchup mode
        // check if we already have an error
        $errors = array();
        if (empty($dao->customer_code)) {
            $errors[] = ts('Recur id %1 is missing a customer code.', array(1 => $contribution_recur_id));
        } else {
            if ($dao->contact_id != $dao->icc_contact_id) {
                $errors[] = ts('Recur id %1 is has a mismatched contact id for the customer code.', array(1 => $contribution_recur_id));
            }
            if ($dao->icc_expiry != '0000' && $dao->icc_expiry < $expiry_limit) {
                // $errors[] = ts('Recur id %1 is has an expired cc for the customer code.', array(1 => $contribution_recur_id));
            }
        }
        if (count($errors)) {
            $source .= ' Errors: ' . implode(' ', $errors);
        }
        $contribution = array('version' => 3, 'contact_id' => $contact_id, 'receive_date' => $receive_date, 'total_amount' => $total_amount, 'payment_instrument_id' => $dao->payment_instrument_id, 'contribution_recur_id' => $contribution_recur_id, 'invoice_id' => $hash, 'source' => $source, 'contribution_status_id' => 2, 'currency' => $dao->currency, 'payment_processor' => $dao->payment_processor_id, 'is_test' => $dao->is_test);
        $get_from_template = array('contribution_campaign_id', 'amount_level');
        foreach ($get_from_template as $field) {
            if (isset($contribution_template[$field])) {
                $contribution[$field] = is_array($contribution_template[$field]) ? implode(', ', $contribution_template[$field]) : $contribution_template[$field];
            }
        }
        if (isset($dao->contribution_type_id)) {
            // 4.2
            $contribution['contribution_type_id'] = $dao->contribution_type_id;
        } else {
            // 4.3+
            $contribution['financial_type_id'] = $dao->financial_type_id;
        }
        if (!empty($contribution_template['line_items'])) {
            $contribution['skipLineItem'] = 1;
            $contribution['api.line_item.create'] = $contribution_template['line_items'];
        }
        if (count($errors)) {
            ++$error_count;
            ++$counter;
            /* create a failed contribution record, don't bother talking to iats */
            $contribution['contribution_status_id'] = 4;
            $contributionResult = civicrm_api('contribution', 'create', $contribution);
            if ($contributionResult['is_error']) {
                $errors[] = $contributionResult['error_message'];
            }
            continue;
        } else {
            // assign basic options
            $options = array('is_email_receipt' => $receipt_recurring, 'customer_code' => $dao->customer_code, 'subtype' => $subtype);
            // if our template contribution is a membership payment, make this one also
            if ($domemberships && !empty($contribution_template['contribution_id'])) {
                try {
                    $membership_payment = civicrm_api('MembershipPayment', 'getsingle', array('version' => 3, 'contribution_id' => $contribution_template['contribution_id']));
                    if (!empty($membership_payment['membership_id'])) {
                        $options['membership_id'] = $membership_payment['membership_id'];
                    }
                } catch (Exception $e) {
                    // ignore, if will fail correctly if there is no membership payment
                }
            }
            // so far so, good ... now create the pending contribution, and save its id
            // and then try to get the money, and do one of: update the contribution to failed, complete the transaction, or update a pending ach/eft with it's transaction id
            $output[] = _iats_process_contribution_payment($contribution, $options);
        }
        /* calculate the next collection date, based on the recieve date (note effect of catchup mode)  */
        /* only move the next sched contribution date forward if the contribution is pending (e.g. ach/eft) or complete */
        if ($contribution['contribution_status_id'] < 3) {
            $next_collectionDate = strtotime("+{$dao->frequency_interval} {$dao->frequency_unit}", $receive_ts);
            $next_collectionDate = date('YmdHis', $next_collectionDate);
            CRM_Core_DAO::executeQuery("\n        UPDATE civicrm_contribution_recur\n           SET " . IATS_CIVICRM_NSCD_FID . " = %1,\n           failure_count = 0\n         WHERE id = %2\n      ", array(1 => array($next_collectionDate, 'String'), 2 => array($dao->id, 'Int')));
        } elseif (4 == $contribution['contribution_status_id']) {
            // i.e. failed
            CRM_Core_DAO::executeQuery("\n        UPDATE civicrm_contribution_recur\n           SET failure_count = failure_count + 1\n         WHERE id = %1\n      ", array(1 => array($dao->id, 'Int')));
        }
        $result = civicrm_api('activity', 'create', array('version' => 3, 'activity_type_id' => 6, 'source_contact_id' => $contact_id, 'source_record_id' => CRM_Utils_Array::value('id', $contributionResult), 'assignee_contact_id' => $contact_id, 'subject' => "Attempted iATS Payments {$subtype} Recurring Contribution for " . $total_amount, 'status_id' => 2, 'activity_date_time' => date("YmdHis")));
        if ($result['is_error']) {
            $output[] = ts('An error occurred while creating activity record for contact id %1: %2', array(1 => $contact_id, 2 => $result['error_message']));
            ++$error_count;
        } else {
            $output[] = ts('Created activity record for contact id %1', array(1 => $contact_id));
        }
        ++$counter;
    }
    // now update the end_dates and status for non-open-ended contribution series if they are complete (so that the recurring contribution status will show correctly)
    // This is a simplified version of what we did before the processing
    $select = 'SELECT cr.id, count(c.id) AS installments_done, cr.installments
      FROM civicrm_contribution_recur cr
      INNER JOIN civicrm_contribution c ON cr.id = c.contribution_recur_id
      INNER JOIN civicrm_payment_processor pp ON cr.payment_processor_id = pp.id
      WHERE
        (pp.class_name = %1 OR pp.class_name = %2 OR pp.class_name = %3)
        AND (cr.installments > 0)
        AND (cr.contribution_status_id  = 5)
      GROUP BY c.contribution_recur_id';
    $dao = CRM_Core_DAO::executeQuery($select, $args);
    while ($dao->fetch()) {
        // check if my end date should be set to now because I have finished
        if ($dao->installments_done >= $dao->installments) {
            // I'm done with installments
            // set this series complete and the end_date to now
            $update = 'UPDATE civicrm_contribution_recur SET contribution_status_id = 1, end_date = NOW() WHERE id = %1';
            CRM_Core_DAO::executeQuery($update, array(1 => array($dao->id, 'Int')));
        }
    }
    $lock->release();
    // If errors ..
    if ($error_count) {
        return civicrm_api3_create_error(ts("Completed, but with %1 errors. %2 records processed.", array(1 => $error_count, 2 => $counter)) . "<br />" . implode("<br />", $output));
    }
    // If no errors and records processed ..
    if ($counter) {
        return civicrm_api3_create_success(ts('%1 contribution record(s) were processed.', array(1 => $counter)) . "<br />" . implode("<br />", $output));
    }
    // No records processed
    return civicrm_api3_create_success(ts('No contribution records were processed.'));
}