Merge branch 'dev'
authorAndrew Engelbrecht <andrew@fsf.org>
Thu, 5 Jan 2023 23:40:21 +0000 (18:40 -0500)
committerAndrew Engelbrecht <andrew@fsf.org>
Thu, 5 Jan 2023 23:40:21 +0000 (18:40 -0500)
Deploying code for extended membership status checking

docs/fsf-drupal-auth.md
extlib/bootstrap.inc
lib/Auth/Source/FSFDrupalAuth.php
themes/fsftheme/default/includes/footer.php

index 4ebbd028726811cdb88493559c82789c72f30db6..fc0c079fb350b4aa5567663e5a63c0d698cde03f 100644 (file)
@@ -10,14 +10,27 @@ SQL queries in `config/authsources.php` should be something like the following:
 
         // custom fsf authentication source wrapped by ratelimit auth source
         'fsfdrupalauth:FSFDrupalAuth',
+
         'dsn' => 'mysql:host=example.com;port=3306;dbname=drupal',
         'username' => '$DB_USERNAME',
         'password' => '$DB_PASSWORD',
+
         'query_main' => 'SELECT pass, mail FROM users WHERE name = :username AND status = "1" limit 1;',
         // don't filter with 'and c.is_test = 0' because these may be useful for FSF staff
         'query_membership' => "select c.status_id from drupal.users a inner join civicrm.civicrm_uf_match b on a.uid=b.uf_id inner join civicrm.civicrm_membership c on b.contact_id=c.contact_id inner join civicrm.civicrm_contact d on c.contact_id=d.id where a.status = 1 and d.is_deleted = 0 and c.status_id is not NULL and a.name = :username and (c.status_id = 1 or c.status_id = 2 or c.status_id = 3 or c.status_id = 4) order by c.status_id limit 10;",
-        'query_staff' => "select a.name as is_fsf_staff from drupal.users a inner join civicrm.civicrm_uf_match b on a.uid=b.uf_id inner join civicrm.civicrm_contact c on b.contact_id=c.id inner join civicrm.civicrm_relationship d on c.id=d.contact_id_a where a.name=:username and a.status=1 and c.is_deleted=0 and d.relationship_type_id=4 and d.contact_id_b=FOOBAR and d.is_active=1 and (d.end_date>NOW() or d.end_date is NULL) limit 1;",
-
+        'query_staff' => "select a.name as is_fsf_staff from drupal.users a inner join civicrm.civicrm_uf_match b on a.uid=b.uf_id inner join civicrm.civicrm_contact c on b.contact_id=c.id inner join civicrm.civicrm_relationship d on c.id=d.contact_id_a where a.name=:username and a.status=1 and c.is_deleted=0 and d.relationship_type_id=4 and d.contact_id_b = :fsf_org_id and d.is_active=1 and (d.end_date>NOW() or d.end_date is NULL) limit 1;",
+        'query_nomination_process_donations' => "select d.total_amount as amount, d.receive_date as receive_date, f.membership_type_id as member_type_id from drupal.users a inner join civicrm.civicrm_uf_match b on a.uid=b.uf_id inner join civicrm.civicrm_contact c on b.contact_id=c.id inner join civicrm.civicrm_contribution d on c.id=d.contact_id left join civicrm.civicrm_membership_payment e on d.id=e.contribution_id left join civicrm.civicrm_membership f on e.membership_id=f.id where a.name = :username and d.contribution_status_id = 1 and d.receive_date > subdate(:start_date, interval 1 year) and d.receive_date < :end_date;",
+        'query_nomination_process_gift_receipt' => "select count(*) as gift_memberships_count from drupal.users a inner join civicrm.civicrm_uf_match b on a.uid=b.uf_id inner join civicrm.civicrm_contact c on b.contact_id=c.id inner join civicrm.civicrm_contribution d on c.id=d.contact_id where a.name = :username and d.contribution_page_id = :gift_redeem_page_id and d.receive_date > :start_date and d.receive_date < :end_date;",
+        'query_nomination_process_adhoc' => "select count(*) as is_adhoc_member from drupal.users a inner join civicrm.civicrm_uf_match b on a.uid=b.uf_id inner join civicrm.civicrm_contact c on b.contact_id=c.id inner join civicrm.civicrm_group_contact d on c.id=d.contact_id where a.name=:username and a.status=1 and c.is_deleted=0 and d.group_id = :adhoc_access_group_id and d.status = 'Added' limit 1;",
+
+        'fsf_org_id' => '739106',
+
+        'nomination_process_contrib_start_date' => '2017-01-01',
+        'nomination_process_contrib_end_date' => '2022-01-01',
+        'gift_redeem_page_id' => '63',
+        'membership_monthly_rate' => '10',
+        'student_membership_monthly_rate' => '5',
+        'adhoc_access_group_id' => '1786',
     ],
 
 ## License
index ce2c617e2f7d234b274d851149120f4737945da1..48d471f47d8c0b66ab4802e49474b803658d6f7a 100644 (file)
@@ -8,7 +8,7 @@
 /**
  * The current system version.
  */
-define('VERSION', '7.83');
+define('VERSION', '7.95-dev');
 
 /**
  * Core API compatibility.
@@ -18,7 +18,7 @@ define('DRUPAL_CORE_COMPATIBILITY', '7.x');
 /**
  * Minimum supported version of PHP.
  */
-define('DRUPAL_MINIMUM_PHP', '5.2.4');
+define('DRUPAL_MINIMUM_PHP', '5.3.3');
 
 /**
  * Minimum recommended value of PHP memory_limit.
@@ -809,6 +809,16 @@ function drupal_settings_initialize() {
     if (!empty($_SERVER['HTTP_HOST'])) {
       $cookie_domain = _drupal_get_cookie_domain($_SERVER['HTTP_HOST']);
     }
+
+    // Drupal 7.83 included a security improvement whereby www. is no longer
+    // stripped from the cookie domain. However, this can cause problems with
+    // existing session cookies where some users are left unable to login. In
+    // order to avoid that, prepend a leading dot to the session_name that was
+    // derived from the base_url when a www. subdomain is in use.
+    // @see https://www.drupal.org/project/drupal/issues/2522002
+    if (strpos($session_name, 'www.') === 0) {
+      $session_name = '.' . $session_name;
+    }
   }
   // Per RFC 2109, cookie domains must contain at least one dot other than the
   // first. For hosts such as 'localhost' or IP Addresses we don't set a cookie domain.
@@ -1592,7 +1602,7 @@ function drupal_page_header() {
  */
 function drupal_serve_page_from_cache(stdClass $cache) {
   // Negotiate whether to use compression.
-  $page_compression = !empty($cache->data['page_compressed']);
+  $page_compression = !empty($cache->data['page_compressed']) && !empty($cache->data['body']);
   $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE;
 
   // Get headers set in hook_boot(). Keys are lower-case.
@@ -1895,7 +1905,7 @@ function format_string($string, array $args = array()) {
  * @ingroup sanitization
  */
 function check_plain($text) {
-  return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
+  return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8');
 }
 
 /**
@@ -1923,7 +1933,7 @@ function check_plain($text) {
  *   TRUE if the text is valid UTF-8, FALSE if not.
  */
 function drupal_validate_utf8($text) {
-  if (strlen($text) == 0) {
+  if (strlen((string) $text) == 0) {
     return TRUE;
   }
   // With the PCRE_UTF8 modifier 'u', preg_match() fails silently on strings
@@ -2338,7 +2348,7 @@ function drupal_random_bytes($count)  {
     // the microtime() - is prepended rather than appended. This is to avoid
     // directly leaking $random_state via the $output stream, which could
     // allow for trivial prediction of further "random" numbers.
-    if (strlen($bytes) < $count) {
+    if (strlen((string) $bytes) < $count) {
       // Initialize on the first call. The contents of $_SERVER includes a mix of
       // user-specific and system information that varies a little with each page.
       if (!isset($random_state)) {
@@ -3939,6 +3949,14 @@ function drupal_setcookie($name, $value, $options) {
     setcookie($name, $value, $options);
   }
   else {
+    $defaults = array(
+      'expires' => 0,
+      'path' => '',
+      'domain' => '',
+      'secure' => FALSE,
+      'httponly' => FALSE,
+    );
+    $options += $defaults;
     setcookie($name, $value, $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']);
   }
 }
index 6695cc0b854830671494f498913b7212de96ae95..21b1e7fb629dadeb30124bc25e791b7a361c600c 100644 (file)
@@ -44,6 +44,21 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
     private $query_main;
     private $query_membership;
     private $query_staff;
+    private $query_nomination_process_donations;
+    private $query_nomination_process_gift_receipt;
+    private $query_nomination_process_adhoc;
+
+    /**
+     * SQL query parameters, or variables that help determine which attributes
+     * someone has
+     */
+    private $fsf_org_id;
+    private $nomination_process_contrib_start_date;
+    private $nomination_process_contrib_end_date;
+    private $gift_redeem_page_id;
+    private $membership_monthly_rate;
+    private $student_membership_monthly_rate;
+    private $adhoc_access_group_id;
 
     /**
      * Constructor for this authentication source.
@@ -60,7 +75,17 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
         parent::__construct($info, $config);
 
         // Make sure that all required parameters are present.
-        foreach (['dsn', 'username', 'password', 'query_main', 'query_membership', 'query_staff'] as $param) {
+       foreach (['dsn', 'username', 'password', 'query_main',
+               'query_membership', 'query_staff',
+               'query_nomination_process_donations',
+               'query_nomination_process_gift_receipt',
+               'query_nomination_process_adhoc', 'gift_redeem_page_id',
+               'fsf_org_id', 'membership_monthly_rate',
+               'student_membership_monthly_rate',
+               'nomination_process_contrib_start_date',
+               'nomination_process_contrib_end_date', 'adhoc_access_group_id']
+               as $param) {
+
             if (!array_key_exists($param, $config)) {
                 throw new Exception('Missing required attribute \''.$param.
                     '\' for authentication source '.$this->authId);
@@ -72,14 +97,10 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
                     ' to be a string. Instead it was: '.
                     var_export($config[$param], true));
             }
+
+            $this->$param = $config[$param];
         }
 
-        $this->dsn = $config['dsn'];
-        $this->username = $config['username'];
-        $this->password = $config['password'];
-        $this->query_main =       $config['query_main'];
-        $this->query_membership = $config['query_membership'];
-        $this->query_staff =      $config['query_staff'];
         if (isset($config['options'])) {
             $this->options = $config['options'];
         }
@@ -167,18 +188,18 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
             $return_value = proc_close($process);
 
             //Logger::debug('fsfdrupalauth:'.$this->authId.': authenticator stdout: '.$result);
-    
+
             $errors_found_yet = false;
             if ($errors != "") {
                 Logger::error('fsfdrupalauth:'.$this->authId.': authenticator stderr: '.$errors);
                 $errors_found_yet = true;
             }
-    
+
             if ($return_value != 0) {
                 Logger::error('fsfdrupalauth:'.$this->authId.': authenticator non-zero return code: '.$return_value);
                 $errors_found_yet = true;
             }
-    
+
             return (!$errors_found_yet && is_string($result) && rtrim($result) == "true");
 
         } else {
@@ -194,7 +215,7 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
      * query the database with arbitrary queries that only require a user name.
      *
      */
-    private function query_db($queryname, $username)
+    private function query_db($queryname, $query_params)
     {
         assert(is_string($queryname));
         assert(is_string($username));
@@ -209,7 +230,7 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
         }
 
         try {
-            $sth->execute(['username' => $username]);
+            $sth->execute($query_params);
         } catch (PDOException $e) {
             throw new Exception('fsfdrupalauth:'.$this->authId.
                 ': - Failed to execute queryname: '.$queryname.': '.$e->getMessage());
@@ -237,7 +258,7 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
         // query on membership
         //
 
-        $membership_data = $this->query_db('query_membership', $username);
+        $membership_data = $this->query_db('query_membership', ['username' => $username]);
 
         if (count($membership_data) === 0) {
             // No rows returned - invalid username
@@ -264,11 +285,109 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
             }
         }
 
+        //
+        // query for access to board nomination process
+        //
+
+       $start_date = $this->nomination_process_contrib_start_date;
+       $end_date   = $this->nomination_process_contrib_end_date;
+
+       /**
+         * @param string $query_name  Name of query in authsources
+         * @param array $extra_params  Associative array of parameters to include in query
+        */
+       $donation_query = function ($query_name, $extra_params)
+           use ($username) {
+
+               $parameters = ['username' => $username];
+
+               foreach ($extra_params as $key => $value) {
+                       $parameters[$key] = $value;
+               }
+
+               return $this->query_db($query_name, $parameters);
+       };
+
+       $compare_res = function ($result, $amount) {
+               foreach ($result[0] as $key => $value) {
+                       if (intval($value) >= $amount) {
+                               return true;
+                       }
+               }
+               return false;
+       };
+
+       // looks for memberships / comparable donations in time window. also
+       // looks for a membership or donation (included as a param) that
+       // occurred up to a year before, and that would have carried over into
+       // the time window with a single donation. this approximates whether
+       // the person was, or would have been, a member during the configured
+       // time window.
+       $analyze_history = function ($selective_donations_history)
+           use ($start_date, $end_date) {
+
+               $eligible = false;
+
+               $start_date_obj = new \DateTime($start_date);
+               $end_date_obj = new \DateTime($end_date);
+
+               foreach ($selective_donations_history as $row) {
+
+                       $amount = intval($row['amount']);
+                       $member_type_id = $row['member_type_id'];
+                       $receive_date_obj = new \DateTime($row['receive_date']);
+
+                       if ($amount < 5) {
+                               continue;
+
+                       } elseif ($receive_date_obj >= $start_date_obj and $receive_date_obj <= $end_date_obj) {
+                               return true;
+
+                       } elseif ($receive_date_obj < $start_date_obj) {
+                               switch ($member_type_id) {
+                                       case '1':
+                                       case '2':
+                                               $rate = intval($this->student_membership_monthly_rate);
+                                               break;
+                                       case '8':
+                                       case '9':
+                                       case null:
+                                       default:
+                                               $rate = intval($this->membership_monthly_rate);
+                                               break;
+                               }
+                               $membership_end_date_obj = new \DateTime($row['receive_date']);
+                               $membership_end_date_obj->add(new \DateInterval("P" . ceil($amount / $rate) . "M"));
+
+                               if ($membership_end_date_obj >= $start_date_obj) {
+                                       return true;
+                               }
+                       }
+               }
+               return false;
+       };
+
+       $donation_params    = ['start_date' => $start_date, 'end_date' => $end_date];
+       $gift_member_params = ['start_date' => $start_date, 'end_date' => $end_date, 'gift_redeem_page_id' => intval($this->gift_redeem_page_id)];
+       $adhoc_params       = ['adhoc_access_group_id' => intval($this->adhoc_access_group_id)];
+
+       if (($analyze_history($donation_query('query_nomination_process_donations', $donation_params))
+               || $compare_res($donation_query('query_nomination_process_gift_receipt', $gift_member_params), 1)
+            ) && ($attributes['is_member'] == ['true'])
+             || $compare_res($donation_query('query_nomination_process_adhoc', $adhoc_params), 1)) {
+
+               $attributes['nomination_process'] = ['true'];
+       } else {
+               Logger::debug('fsfdrupalauth:'.$this->authId.
+                       ': Not a member / comparable donor during window for board process.');
+               $attributes['nomination_process'] = ['false'];
+       }
+
         //
         // query on staff
         //
 
-        $staff_data = $this->query_db('query_staff', $username);
+        $staff_data = $this->query_db('query_staff', ['username' => $username, 'fsf_org_id' => $this->fsf_org_id]);
 
         if (count($staff_data) === 0) {
             // No rows returned - invalid username
@@ -335,7 +454,7 @@ class FSFDrupalAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
         //Logger::debug('fsfdrupalauth:'.$this->authId.': entered password: '.$password);
 
 
-        $user_data = $this->query_db('query_main', $username);
+        $user_data = $this->query_db('query_main', ['username' => $username]);
 
 
         if (count($user_data) === 0) {
index 39761fd23f0bfc8be8fa00dbebad3ef8be5be1df..f3896d7db463f126de5b9a6d29fd3a7ef7362d5e 100644 (file)
@@ -9,7 +9,7 @@ if (!empty($this->data['htmlinject']['htmlContentPost'])) {
             <div id="footer">
                 <hr />
                 <img src="/<?php echo $this->data['baseurlpath']; ?>resources/icons/ssplogo-fish-small.png" alt="Small fish logo" style="float: right" />              
-                    Copyright &copy; 2007-2021 UNINETT AS
+                    Copyright &copy; 2007-2022 UNINETT AS
                     <br>
                     Modifications Copyright &copy; 2020-2022 Free Software Foundation