Adds documentation and token checking to weather bot chat bot example, a=chris

Chris Pollett [2017-01-18 01:Jan:th]
Adds documentation and token checking to weather bot chat bot example, a=chris
Filename
src/controllers/components/SocialComponent.php
src/examples/bot_examples/weather/WeatherBot.php
src/library/Utility.php
src/models/ProfileModel.php
src/models/SigninModel.php
diff --git a/src/controllers/components/SocialComponent.php b/src/controllers/components/SocialComponent.php
index 6fd461b49..5f885f4cd 100644
--- a/src/controllers/components/SocialComponent.php
+++ b/src/controllers/components/SocialComponent.php
@@ -1215,11 +1215,12 @@ class SocialComponent extends Component implements CrawlConstants
                     foreach ($bot_followers as $bot_follower) {
                         $bots[] = $bot_follower['USER_NAME'];
                     }
-                    if (@preg_match_all('/(?<!\w)@(\w+) (.*)/', $description,
+                    if (preg_match_all('/(?<!\w)@(\w+) (.*)/si', $description,
                         $matches)) {
                         foreach ($matches[1] as $match) {
-                            $index = array_search($match,$bots);
-                            if ($index) {
+                            $match = mb_strtolower($match);
+                            $index = array_search($match, $bots);
+                            if ($index !== false) {
                                 $bots_called[] = $bot_followers[$index];
                             }
                         }
@@ -1269,10 +1270,14 @@ class SocialComponent extends Component implements CrawlConstants
                     $num_bots = count($bots_called);
                     $sites = [];
                     $post_data = [];
+                    $time = time();
                     for ($i = 0; $i < $num_bots; $i++) {
                         $sites[$i][CrawlConstants::URL] =
                             $bots_called[$i]['CALLBACK_URL'];
-                        $post_data[$i] = "post=$post_followed";
+                        $post_data[$i] = "post=$post_followed&bot_token=" .
+                            hash("sha256", $bots_called[$i]['BOT_TOKEN'] .
+                            $time . $post_followed) . "*" . $time .
+                            "&bot_name=". $bots_called[$i]['USER_NAME'];
                     }
                     $outputs = [];
                     if (count($sites) > 0) {
diff --git a/src/examples/bot_examples/weather/WeatherBot.php b/src/examples/bot_examples/weather/WeatherBot.php
index 4b0886478..e5337a390 100644
--- a/src/examples/bot_examples/weather/WeatherBot.php
+++ b/src/examples/bot_examples/weather/WeatherBot.php
@@ -33,58 +33,117 @@
 namespace seekquarry\yioop\examples\weatherbot;

 /**
- * Bot work instruction:
- * The current folder has to be moved to root path
- * Create a bot with CALLBACK_URL as http://HOSTNAME/weather/WeatherBot.php
- * Add the bot to the group you wanted to converse
- * Talk to your bot in yioop Groups by calling bot as @bot_name in comment
- */
-/**
- * This file contains an example bot called weather bot
- * to get weather updates whenever you ask
+ * This class demonstrates a simple Weather Chat Bot using the Yioop
+ * ChatBot APIs for Yioop Discussion Groups.
+ * To use this bot:
+ * (1) Move this file to some folder of a web server you have access to.
+ *     Denote by some_url the url of this folder. If you point your
+ *     browser at this folder you should see a message that begins with:
+ *     There was a configuration issue with your query.
+ * (2) Create a new Yioop User.
+ * (3) Under Manage Accounts, click on the lock symbol next to Account Details
+ * (4) Check the Bot User check bot, click save.
+ * (5) Two form variables should appear: Bot Unique Token and Bot Callback URL.
+ *      Fill in a value for Bot Unique Token that matches the value set
+ *      for ACCESS_TOKEN in the code within the WeatherBot class.
+ *      Fill in some_url (as defined in step (1)) for the value of Bot Callback
+ *      URL
+ * (6) Add the the user you created in Yioop to the group that you would like
+ *     the bot to service. Let the name of this user be user_name.
+ * (7) Talk to your bot in yioop in this groups by commenting on an
+ *     already existing thread with a message beginning with @user_name.
  */
 class WeatherBot
 {
     /**
-     * @param string $access_token
+     * Url of site that this bot gets weather information from
+     */
+    const WEATHER_URL = "http://query.yahooapis.com/v1/public/yql";
+    /**
+     * Token given when setting up the bot in Yioop  for callback requests
+     * This bots checks that a request from a Yioop Intance  sends
+     * a timestamp as well as the hash of this timestamp with the bot_token
+     * and post data and that these match the expected values
+     */
+    const ACCESS_TOKEN = "bot_token";
+    /**
+     * Number of seconds that the passed timestamp can differ from the current
+     * time on the WeatherBot machine.
+     */
+    const TIME_WINDOW = 60;
+    /**
+     * This is the method called to get the WeatherBot to handle an incoming
+     * HTTP request, and echo a weather realted message
      */
-    function setKey($access_token)
+    function processRequest()
     {
-        $this->access_token = $access_token;
+        $result = "There was a configuration issue with your query.";
+        if ($this->checkBotToken() && !empty($_REQUEST['post']) &&
+            !empty($_REQUEST['bot_name'])) {
+            $location = filter_var($_REQUEST['post'], \FILTER_SANITIZE_STRING);
+            $location = trim(mb_strtolower($location));
+            $result = $this->getWeather($location);
+            if (empty($result)) {
+                $result = "I failed to find the weather for that location.\n".
+                    "I respond to queries in the format:\n" .
+                    " @{$_REQUEST['bot_name']} some_location";
+            }
+        }
+        echo $result;
     }
     /**
-     * Get weather information
+     * This method is used to check a request that it comes from a site
+     * that knows the bot_token in use by this WeatherBot.
+     */
+    function checkBotToken()
+    {
+        if (!empty($_REQUEST['bot_token'])) {
+            $token_parts = explode("*", $_REQUEST['bot_token']);
+            $post = empty($_REQUEST["post"]) ? "" : $_REQUEST["post"];
+            $hash = hash("sha256", self::ACCESS_TOKEN . $token_parts[1].
+                $post);
+            if (isset($token_parts[1]) &&
+                abs(time() - $token_parts[1]) < self::TIME_WINDOW) {
+                // second check avoids timing attacks, works for > php 5.6
+                if ((!function_exists('hash_equals') &&
+                    $hash == $token_parts[0]) ||
+                    hash_equals($hash, $token_parts[0])) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+    /**
+     * Get weather information about a location
      *
      * @param string $location the location to get weather updates for
      * @return string weather information
      */
     function getWeather($location)
     {
-        $WEATHER_URL = "http://query.yahooapis.com/v1/public/yql";
         $yql_query = "select * from weather.forecast where woeid in
-            (select woeid from geo.places(1) where text='".$location
+            (select woeid from geo.places(1) where text='" . $location
             ."')";
-        $url = $WEATHER_URL . "?q=" . urlencode($yql_query) . "&format=json";
+        $url = self::WEATHER_URL . "?q=" .
+            urlencode($yql_query) . "&format=json";
         $ch = curl_init();
         curl_setopt($ch, CURLOPT_URL, $url);
         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
         $data = curl_exec($ch);
         curl_close($ch);
-        $result = json_decode($data);
-        $temp = $result->query->results->channel->item->condition->temp;
-        $text = $result->query->results->channel->item->condition->text;
-        return "The weather is $temp and $text in $location";
+        $result = @json_decode($data);
+        $temp = empty($result->query->results->channel->item->condition->temp) ?
+            "" : $result->query->results->channel->item->condition->temp;
+        $text = empty($result->query->results->channel->item->condition->text) ?
+            "" : mb_strtolower(
+            $result->query->results->channel->item->condition->text);
+        if (empty($temp) || empty($text)) {
+            return "";
+        }
+        return "The weather is $temp and $text in $location.";
     }
 }
-$access_token ="bot_token";
 $bot = new WeatherBot();
-$bot->setKey($access_token);
-$location = "";
-$result = "";
-if (!empty($_REQUEST['post'])){
-    $location = explode(" ", $_REQUEST['post']);
-}
-if (!empty($location[4])) {
-    $result = $bot->getWeather($location[4]);
-}
-echo $result;
+$bot->processRequest();
+
diff --git a/src/library/Utility.php b/src/library/Utility.php
index e4c428106..fa10b1df4 100755
--- a/src/library/Utility.php
+++ b/src/library/Utility.php
@@ -1552,7 +1552,6 @@ function crawlCrypt($string, $salt = null)
     }
     return crypt($string, $salt);
 }
-
 /**
  * Used by a controller to take a table and return those rows in the
  * table that a given queue_server would be responsible for handling
@@ -1578,7 +1577,6 @@ function partitionByHash($table, $field, $num_partition, $instance,
     foreach ($table as $row) {
         $cell = ($field === null) ? $row : $row[$field];
         $hash_int = calculatePartition($cell, $num_partition, $callback);
-
         if ($hash_int  == $instance) {
             $out_table[] = $row;
         }
@@ -2461,4 +2459,4 @@ function bchexdec($hex)
             bcpow('16', strval($len - $i))));
     }
     return $dec;
-}
\ No newline at end of file
+}
diff --git a/src/models/ProfileModel.php b/src/models/ProfileModel.php
index 30e8bda11..c07b385b4 100755
--- a/src/models/ProfileModel.php
+++ b/src/models/ProfileModel.php
@@ -382,8 +382,6 @@ class ProfileModel extends Model
         $n = [];
         $n[] = <<<EOT
 <?php
-namespace seekquarry\yioop\configs;
-
 /**
  * SeekQuarry/Yioop --
  * Open Source Pure PHP Search Engine, Crawler, and Indexer
@@ -407,15 +405,18 @@ namespace seekquarry\yioop\configs;
  *
  * END LICENSE
  *
- * Computer generated file giving the key defines of directory locations
- * as well as database settings used to run the SeekQuarry/Yioop search engine
- *
  * @author Chris Pollett chris@pollett.org
  * @license http://www.gnu.org/licenses/ GPL3
  * @link http://www.seekquarry.com/
  * @copyright 2009-2012
  * @filesource
  */
+namespace seekquarry\yioop\configs;
+
+/**
+ * Computer generated file giving the key defines of directory locations
+ * as well as database settings used to run the SeekQuarry/Yioop search engine
+ */
 EOT;
         $base_url = C\NAME_SERVER;
         if (C\nsdefined("BASE_URL")) {
@@ -767,4 +768,4 @@ EOT;
         }
         return $match;
     }
-}
\ No newline at end of file
+}
diff --git a/src/models/SigninModel.php b/src/models/SigninModel.php
index 1c89e13b7..38122cee3 100755
--- a/src/models/SigninModel.php
+++ b/src/models/SigninModel.php
@@ -56,7 +56,12 @@ class SigninModel extends Model
             return false;
         }
         $row = $db->fetchArray($result);
-        return L\crawlCrypt($password, $row['PASSWORD']) == $row['PASSWORD'] ;
+        // avoid timeing attacks if possible
+        if (function_exists('hash_equals')) {
+            return hash_equals(L\crawlCrypt($password, $row['PASSWORD']),
+                $row['PASSWORD']);
+        }
+        return L\crawlCrypt($password, $row['PASSWORD']) == $row['PASSWORD'];
     }
     /**
      * Get user details from database
ViewGit