_binary From 9c505c0785b28d8c51b2c3f6397630e4101d48b6 Mon Sep 17 00:00:00 2001
From: Pragya Rana <pragya.rana9@gmail.com>
Date: Sat, 29 Apr 2017 21:43:33 -0700
Subject: [PATCH] Add Differential Privacy to Group Statistics Page

* Implemented differential privacy for each item type and time period in
  the group's statistics page.
* Added a new table ITEM_IMPRESSION_STAT to store actual and fuzzified
  statistics of the data for each item type and time period.
* Fixed a bug from previous commit.If encryption is enabled in the group,
  decrypt the group items before displaying in the statistics page.
---
 src/configs/Config.php                         |  2 +-
 src/controllers/components/SocialComponent.php | 78 +++++++++++++++++++++++++-
 src/library/VersionFunctions.php               | 11 ++++
 src/models/GroupModel.php                      | 18 ------
 src/models/ImpressionModel.php                 | 75 +++++++++++++++++++++++++
 src/models/ProfileModel.php                    |  1 +
 src/views/elements/ManagegroupsElement.php     |  4 +-
 7 files changed, 167 insertions(+), 22 deletions(-)

diff --git a/src/configs/Config.php b/src/configs/Config.php
index d9feae8f..858664c5 100755
--- a/src/configs/Config.php
+++ b/src/configs/Config.php
@@ -112,7 +112,7 @@ function nsconddefine($constant, $value)
  * Version number for upgrade function
  * @var int
  */
-nsdefine('YIOOP_VERSION', 50);
+nsdefine('YIOOP_VERSION', 51);
 /**
  * Minimum Version fo Yioop for which keyword ad script
  * still works with this version
diff --git a/src/controllers/components/SocialComponent.php b/src/controllers/components/SocialComponent.php
index 40e14ede..bf824cff 100644
--- a/src/controllers/components/SocialComponent.php
+++ b/src/controllers/components/SocialComponent.php
@@ -678,6 +678,7 @@ class SocialComponent extends Component implements CrawlConstants
                     $impression_model = $parent->model("impression");
                     $periods = [C\ONE_HOUR, C\ONE_DAY, C\ONE_MONTH, C\ONE_YEAR,
                         C\FOREVER];
+                    $stat_types = [C\GROUP_IMPRESSION, C\THREAD_IMPRESSION, C\WIKI_IMPRESSION];
                     $filter = (empty($_REQUEST['filter'])) ? "" :
                         $parent->clean($_REQUEST['filter'], 'string');
                     $data['FILTER'] = $filter;
@@ -692,6 +693,80 @@ class SocialComponent extends Component implements CrawlConstants
                             $impression_model->getStatistics(C\WIKI_IMPRESSION,
                             $period, $filter, $group_id);
                     }
+                    // Add Differential Privacy for each group
+                    // statistics if enabled
+                    if (C\DIFFERENTIAL_PRIVACY) {
+                        foreach ($stat_types as $field) {
+                            $i = 0;
+                            foreach ($periods as  $period) {
+                                if ($field == C\GROUP_IMPRESSION) {
+                                    if (!empty($data["STATISTICS"][$field][$period])) {
+                                        $view_stat = $impression_model->getImpressionStat(
+                                            $data["STATISTICS"][$field][$period][0]['ID'],
+                                            $field, $period);
+                                        $fuzzy_views = $view_stat[1];
+                                        if ($view_stat[0] != $data["STATISTICS"][$field][$period][0]['NUM_VIEWS']) {
+                                            $fuzzy_views = $parent->addDifferentialPrivacy(
+                                                $data["STATISTICS"][$field][$period][0]['NUM_VIEWS']);
+                                            /* Make sure each time period's
+                                            fuzzified view is at least as large as
+                                            previous time period's value */
+                                            if ($i > 0) {
+                                                if ($tmp_data[$i - 1] > $fuzzy_views) {
+                                                    $fuzzy_views = $tmp_data[$i - 1];
+                                                }
+                                            }
+                                            $impression_model->updateImpressionStat(
+                                                $data["STATISTICS"][$field][$period][0]['ID'],
+                                                $field, $period, $data["STATISTICS"][$field][$period][0]['NUM_VIEWS'],
+                                                $fuzzy_views);
+                                        }
+                                        $data["STATISTICS"][$field][$period][0]
+                                            ['NUM_VIEWS'] = ($fuzzy_views == 0)?
+                                            tl('managegroups_element_no_activity'):
+                                            $fuzzy_views;
+                                        $tmp_data[$i] = $fuzzy_views;
+                                        $i++;
+                                    }
+                                } else {
+                                    if (!empty($data['STATISTICS'][$field]
+                                        [$period])) {
+                                        foreach ($data['STATISTICS'][$field]
+                                            [$period] as $item_name =>
+                                            $item_data) {
+                                            $view_stat = $impression_model->getImpressionStat($item_data[0]['ID'], $field, $period);
+                                            $fuzzy_views = $view_stat[1];
+                                            if ($view_stat[0] != $item_data[0]['NUM_VIEWS']) {
+                                                $fuzzy_views = $parent->addDifferentialPrivacy($item_data[0]['NUM_VIEWS']);
+                                                if ($i > 0) {
+                                                    if ($tmp_data[$item_name][$i - 1] > $fuzzy_views) {
+                                                        $fuzzy_views = $tmp_data[$item_name][$i - 1];
+                                                    }
+                                                }
+                                                $impression_model->updateImpressionStat($item_data[0]['ID'],
+                                                    $field, $period, $item_data[0]['NUM_VIEWS'], $fuzzy_views);
+                                            }
+                                            $data["STATISTICS"][$field][$period][$item_name][0]['NUM_VIEWS']=
+                                                ($fuzzy_views == 0)?
+                                                tl('managegroups_element_no_activity'):$fuzzy_views;
+                                            $tmp_data[$item_name][$i] =
+                                                $fuzzy_views;
+                                            // Decrypt group items if encrypted before displaying
+                                            if ($field == C\THREAD_IMPRESSION &&
+                                                $group_model->isGroupEncrypted($group_id)) {
+                                                // Decrypt thread's title
+                                                $key = $group_model->getGroupKey($group_id);
+                                                $decrypted_item_name = $group_model->cipher($item_name, $key, 1);
+                                                $data['STATISTICS'][$field][$period][$decrypted_item_name] = $item_data;
+                                                unset($data['STATISTICS'][$field][$period][$item_name]);
+                                            }
+                                        }
+                                        $i++;
+                                    }
+                                }
+                            }
+                        }
+                    }
                     $this->getGroupUsersData($data, $group_id);
                     break;
                 case "unsubscribe":
@@ -1712,6 +1787,7 @@ class SocialComponent extends Component implements CrawlConstants
     {
         $parent = $this->parent;
         $group_model = $parent->model("group");
+        $impression_model = $parent->model("impression");
         $user_model = $parent->model("user");
         $item_count = $group_model->getGroupItemCount($search_array, $user_id,
             $for_group);
@@ -1737,7 +1813,7 @@ class SocialComponent extends Component implements CrawlConstants
                     // fuzzify the number of views to add privacy
                     $fuzzy_views =
                         $parent->addDifferentialPrivacy($page['NUM_VIEWS']);
-                    $group_model->updatePrivacyViews($page['ID'],
+                    $impression_model->updatePrivacyViews($page['ID'],
                         $page['NUM_VIEWS'], $fuzzy_views);
                     $page['NUM_VIEWS'] = $fuzzy_views;
                 } else {
diff --git a/src/library/VersionFunctions.php b/src/library/VersionFunctions.php
index dce2a3f2..29a165c3 100644
--- a/src/library/VersionFunctions.php
+++ b/src/library/VersionFunctions.php
@@ -1392,3 +1392,14 @@ function upgradeDatabaseVersion50(&$db)
         SELECT GROUP_ID, GROUP_NAME, CREATED_TIME, OWNER_ID, REGISTER_TYPE,
         MEMBER_ACCESS, VOTE_ACCESS, POST_LIFETIME FROM GROUPS_OLD");
 }
+/**
++ * Upgrades a Version 50 version of the Yioop! database to a Version 51 version
++ * @param object $db datasource to use to upgrade.
++ */
+function upgradeDatabaseVersion51(&$db)
+{
+
+    $db->execute("CREATE TABLE ITEM_IMPRESSION_STAT(ITEM_ID INTEGER,
+        ITEM_TYPE INTEGER, UPDATE_PERIOD INTEGER, SUM INTEGER DEFAULT -1,
+        FUZZY_NUM_VIEWS INTEGER DEFAULT -1");
+}
diff --git a/src/models/GroupModel.php b/src/models/GroupModel.php
index d676bcaf..698f548f 100644
--- a/src/models/GroupModel.php
+++ b/src/models/GroupModel.php
@@ -989,24 +989,6 @@ class GroupModel extends Model implements MediaConstants
         return $groups;
     }
     /**
-     * Used to update the fuzzified view counts of a thread item.
-     *
-     * @param int $item_id id of the thread item to update fuzzified counts for
-     * @param int $num_views current number of views for item. This value
-     *  is stored in the TMP_NUM_VIEWS column to remember the last time the
-     *  FUZZY_NUM_VIEWS column was updated. Only when the TMP_NUM_VIEWS column
-     *  differs from the NUM_VIEWS COLUMN will this method need to be called.
-     * @param int $fuzzy_num_views number of views after epsilon privacy
-     *  fuzzification applied.
-     */
-    public function updatePrivacyViews($item_id, $num_views, $fuzzy_num_views)
-    {
-        $sql = "UPDATE ITEM_IMPRESSION_SUMMARY SET FUZZY_NUM_VIEWS=?,
-            TMP_NUM_VIEWS=? WHERE ITEM_ID=? AND ITEM_TYPE=" .
-            C\THREAD_IMPRESSION;
-        $this->db->execute($sql, [$fuzzy_num_views, $num_views, $item_id]);
-    }
-    /**
      * Gets the number of group feed items visible to a user with $user_id
      * and which match the supplied search criteria found in $search_array
      *
diff --git a/src/models/ImpressionModel.php b/src/models/ImpressionModel.php
index 2b93330d..43256fac 100644
--- a/src/models/ImpressionModel.php
+++ b/src/models/ImpressionModel.php
@@ -336,6 +336,62 @@ class ImpressionModel extends Model
         return [$last_timestamp, $period];
     }
     /**
+     * Used to update the fuzzified view counts of a thread item.
+     *
+     * @param int $item_id id of the thread item to update fuzzified counts for
+     * @param int $num_views current number of views for item. This value
+     *  is stored in the TMP_NUM_VIEWS column to remember the last time the
+     *  FUZZY_NUM_VIEWS column was updated. Only when the TMP_NUM_VIEWS column
+     *  differs from the NUM_VIEWS COLUMN will this method need to be called.
+     * @param int $fuzzy_num_views number of views after epsilon privacy
+     *  fuzzification applied.
+     * @param int $time_period time period for which to update the view
+     */
+    public function updatePrivacyViews($item_id, $num_views, $fuzzy_num_views)
+    {
+        $sql = "UPDATE ITEM_IMPRESSION_SUMMARY SET FUZZY_NUM_VIEWS=?,
+            TMP_NUM_VIEWS=? WHERE ITEM_ID=? AND ITEM_TYPE=" .
+            C\THREAD_IMPRESSION;
+        $this->db->execute($sql, [$fuzzy_num_views, $num_views, $item_id]);
+    }
+    /**
+     * Used to update the statistics of impression items.
+     *
+     * @param int $item_id id of the item to update the statistics
+     * @param int $item_type type of the item
+     * @param int $period time period of the item
+     * @param int $num_views number of views of the item for
+     *  specified time period
+     * @param int $fuzzy_num_views fuzzified views of the item
+     *  for specified time period
+     */
+    public function updateImpressionStat($item_id, $item_type, $period,
+        $num_views, $fuzzy_num_views)
+    {
+            $sql = "UPDATE ITEM_IMPRESSION_STAT SET FUZZY_NUM_VIEWS=?,
+                SUM=? WHERE ITEM_ID=? AND ITEM_TYPE=? AND UPDATE_PERIOD=?";
+            $parameters = [$fuzzy_num_views, $num_views, $item_id,
+                $item_type, $period];
+            $result = $this->db->execute($sql, $parameters);
+    }
+    /**
+     * Returns the statistics of the specified impression item.
+     *
+     * @param int $item_id id of the item to return the statistics
+     * @param int $item_type type of the item
+     * @param int $period time period of the item
+     * @return array values of $sum and $fuzzy_num_views
+     */
+    public function getImpressionStat($item_id, $item_type, $period)
+    {
+        $sql = "SELECT SUM, FUZZY_NUM_VIEWS FROM ITEM_IMPRESSION_STAT WHERE
+            ITEM_ID=? AND ITEM_TYPE=? AND UPDATE_PERIOD=?";
+        $parameters = [$item_id, $item_type, $period];
+        $result = $this->db->execute($sql, $parameters);
+        $row = $this->db->fetchArray($result);
+        return [$row['SUM'], $row['FUZZY_NUM_VIEWS']];
+    }
+    /**
      * Used to create a new counter related to a particular user for a
      * particular activity in the impression analytics. This entails adding both
      * a log-like record of when the activity happened and creating  a
@@ -386,6 +442,21 @@ class ImpressionModel extends Model
             ITEM_ID=? AND ITEM_TYPE=? AND UPDATE_PERIOD = ". C\FOREVER . "
             AND UPDATE_TIMESTAMP = 0";
         $db->execute($sql, [C\PUBLIC_USER_ID, $item_id, $type_id]);
+        // Also add records for each time period in ITEM_IMPRESSION_STAT
+        $periods = [C\ONE_HOUR, C\ONE_DAY, C\ONE_MONTH, C\ONE_YEAR,
+                        C\FOREVER];
+        foreach ($periods as $period) {
+            $sql = "SELECT * FROM ITEM_IMPRESSION_STAT WHERE ITEM_ID=? AND
+                ITEM_TYPE=? AND UPDATE_PERIOD=?";
+            $result = $db->execute($sql, [$item_id, $type_id, $period]);
+            $row = $db->fetchArray($result);
+            // Update ITEM_IMPRESSION_STAT only once in the database
+            if (empty($row)) {
+                $sql = "INSERT INTO ITEM_IMPRESSION_STAT VALUES
+                        (?, ?, ?, -1, -1)";
+                $db->execute($sql, [$item_id, $type_id, $period]);
+            }
+        }
     }
     /**
      * Used to delete information related to a particular user from the
@@ -411,5 +482,9 @@ class ImpressionModel extends Model
             ITEM_ID=? AND ITEM_TYPE=?";
         $db->execute($sql, [$user_id, $item_id, $type_id]);
         $db->execute($sql, [C\PUBLIC_USER_ID, $item_id, $type_id]);
+        // Also delete records from ITEM_IMPRESSION_STAT
+        $sql = "DELETE FROM ITEM_IMPRESSION_STAT WHERE ITEM_ID=? AND
+            ITEM_TYPE=?";
+        $db->execute($sql, [$item_id, $type_id]);
     }
 }
diff --git a/src/models/ProfileModel.php b/src/models/ProfileModel.php
index 24159a45..9a4f37ef 100755
--- a/src/models/ProfileModel.php
+++ b/src/models/ProfileModel.php
@@ -230,6 +230,7 @@ class ProfileModel extends Model
             "ITEM_IMPRESSION" => "CREATE TABLE ITEM_IMPRESSION(
                 USER_ID INTEGER, ITEM_ID INTEGER, ITEM_TYPE INTEGER,
                 VIEW_DATE NUMERIC(" . C\TIMESTAMP_LEN . "))",
+            "ITEM_IMPRESSION_STAT" => "CREATE TABLE ITEM_IMPRESSION_STAT(ITEM_ID INTEGER, ITEM_TYPE INTEGER, UPDATE_PERIOD INTEGER, SUM INTEGER DEFAULT -1, FUZZY_NUM_VIEWS INTEGER DEFAULT -1)",
             "ITEM_IMPRESSION_SUMMARY" => "CREATE TABLE ITEM_IMPRESSION_SUMMARY(
                 USER_ID INTEGER, ITEM_ID INTEGER, ITEM_TYPE INTEGER,
                 UPDATE_PERIOD INTEGER,
diff --git a/src/views/elements/ManagegroupsElement.php b/src/views/elements/ManagegroupsElement.php
index 6f9907eb..cd245ee1 100755
--- a/src/views/elements/ManagegroupsElement.php
+++ b/src/views/elements/ManagegroupsElement.php
@@ -879,7 +879,7 @@ class ManagegroupsElement extends Element
                             "</p>");
                     } else {
                         if ($time_period == C\ONE_HOUR ||
-                           $time_period == C\FOREVER) {
+                           $time_period == C\FOREVER || C\DIFFERENTIAL_PRIVACY) {
                             e("<p><b>$period_heading</b>: ".
                                 $data['STATISTICS'][$field]
                                 [$time_period][0]['NUM_VIEWS'].
@@ -904,7 +904,7 @@ class ManagegroupsElement extends Element
                         foreach ($data['STATISTICS'][$field][
                             $time_period] as $item_name => $item_data) {
                             if ($time_period == C\ONE_HOUR ||
-                                $time_period == C\FOREVER) {
+                                $time_period == C\FOREVER || C\DIFFERENTIAL_PRIVACY) {
                                 e("<p>$item_name: ". $item_data[0]['NUM_VIEWS'].
                                 "</p>");
                             } else {
-- 
2.11.0 (Apple Git-81)

