* @license GPLV3 gpl.com * @link itsblue.de */ class BlueWeather { private $_config; private $_con; /** * Constructor * * @param mixed $_config config array containing some stuff * * @return void */ public function __construct($_config) { $this->_config = $_config; $this->_con = mysqli_connect( $_config['dbhost'], $_config['dbuser'], $_config['dbpassword'], $_config['dbname'] ); if (!$this->_con) { echo "Error connecting to database: " . mysqli_connect_error(); http_response_code(500); die(); } } // ------------------- // - user management - // ------------------- /** * Function to login users * * @param string $username username * @param string $password passowrd * * @return string '' or session token */ public function loginUser($username, $password) { $sql = "SELECT * FROM `users` WHERE`username`=\"".$this->_con->real_escape_string($username)."\""; $result = $this->_con->query($sql); // only one row will be returned $data = $result->fetch_assoc(); if (!password_verify($password, $data['password'])) { return ""; } //generate random token $length = 10; $str = ""; $characters = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9')); $max = count($characters) - 1; for ($i = 0; $i < $length; $i++) { $rand = mt_rand(0, $max); $str .= $characters[$rand]; } $token_hash = password_hash($str, PASSWORD_DEFAULT); $sql = 'INSERT INTO `sessions` (userId, session) VALUES ("'. $data['id'] .'", "'. $token_hash .'")'; if (!$this->_con->query($sql)) { return ""; } return $token_hash; } /** * Function to check if a session token exists and get the corresponding user * * @param string $session session token * * @return int (-1: does not exist; >0: userId) */ public function checkSession($session) { $sql = "SELECT * FROM `sessions` WHERE`session`=\"".$this->_con->real_escape_string($session)."\""; $result = $this->_con->query($sql); if (!$result->num_rows > 0) { return -1; } // only one row will be returned $data = $result->fetch_assoc(); return($data['userId']); } /** * Function to user information * * @param int $userId id of the user * * @return mixed (array with some user information, or -1 if user not found) */ public function getUserInfo($userId) { $sql = "SELECT username,realname,email FROM `users` WHERE`id`=\"".$this->_con->real_escape_string($userId)."\""; $result = $this->_con->query($sql); if (!$result->num_rows > 0) { return -1; } // only one row will be returned $data = $result->fetch_assoc(); $data["emailHash"] = md5(strtolower(trim($data["email"]))); return($data); } /** * Function to destroy a user session * * @param string $session session-token * * @return int 200: OK; 401: session does not exist */ public function destroySession($session) { $sql = "DELETE FROM `sessions` WHERE `session`=\"".$this->_con->real_escape_string($session)."\""; if ($this->_con->query($sql)) { return 200; } else { return 500; } } // -------------------- // - getter functions - // -------------------- /** * Function to get all locations from the database * * @return mixed (array with all locations) */ public function getAllLocations() { // get all locations $sql = "SELECT * FROM `locations`"; $result = $this->_con->query($sql); //loop through the returned data while ($row = $result->fetch_assoc()) { $locations[] = $row; } return $locations; } /** * Function to get the data of a specific location * * @param int $locId id of the location to return data of * @param mixed $range contains 'from' and 'to' as unix timestamps * @param mixed $maxVals array containing: * 'count': maximum measvals to be transmitted * 'mode': * - 'newest': will return the newest values * - 'oldest': will return the oldest values * - 'avg' : sub avarages will be calculated * * @return mixed object with all information about the location (see docs) */ function getLocationData($locId, $range, $maxVals) { $locId = $this->_con->real_escape_string($locId); $range["from"] = $this->_con->real_escape_string($range["from"]); $range["to"] = $this->_con->real_escape_string($range["to"]); $sql = "SELECT * FROM `locations` WHERE`id`=$locId"; $result = $this->_con->query($sql); // only one row will be returned $data = $result->fetch_assoc(); if (!isset($range['from']) || $range['from'] === "") { $range['from'] = time() - 24 * 60 * 60; } if ($range['from'] < 0) { $range['from'] = time() + $range['from']; } if (!isset($range['to']) || $range['to'] === "") { $range['to'] = time(); } // get all measvalues of given location $sql = "SELECT M.measvalue,M.sensorid,M.timestamp FROM measvalues M JOIN sensors S ON M.sensorid = S.id JOIN locations L ON S.locationid=L.id WHERE L.id=$locId AND M.timestamp > " . $range['from'] . " AND M.timestamp < " . $range['to'] . " ORDER BY timestamp ASC"; $result = $this->_con->query($sql); while ($row = $result->fetch_assoc()) { $measvalues[] = $row; } // get all sensors of given location $sql = "SELECT * FROM `sensors` WHERE `locationid` = $locId"; $result = $this->_con->query($sql); //loop through the returned data while ($row = $result->fetch_assoc()) { unset($row['locationid']); // remove locId as it is redundant $sensors[] = $row; } // get all value types $sql = "SELECT * FROM `valuetypes`"; $result = $this->_con->query($sql); // get all necessaray valuetypes while ($row = $result->fetch_assoc()) { foreach ($sensors as $sensor) { if ($sensor['valuetypeid'] == $row['id']) { $valuetypes[] = $row; break; } } } if (isset($maxVals) && isset($maxVals['count']) && isset($maxVals['mode'])) { // build the new measvalues array with respect to maxVals for each sensor $finalMeasvals = array(); foreach ($sensors as $sensor) { $rawMeasvalsOfThisSensor = array(); $finalMeasvalsOfThisSensor = array(); // get all measvalues of the current sensor foreach ($measvalues as $measvalue) { if ($measvalue["sensorid"] === $sensor["id"]) { array_push($rawMeasvalsOfThisSensor, $measvalue); } } if (count($rawMeasvalsOfThisSensor) <= $maxVals['count']) { // measvls don't exceed maxVals -> nothng needs to be done $finalMeasvals = array_merge( $finalMeasvals, $rawMeasvalsOfThisSensor ); continue; } switch($maxVals['mode']) { case "newest": $finalMeasvalsOfThisSensor = array_slice( $rawMeasvalsOfThisSensor, count($rawMeasvalsOfThisSensor) - $maxVals['count'], count($rawMeasvalsOfThisSensor) ); break; case "oldest": $finalMeasvalsOfThisSensor = array_slice( $rawMeasvalsOfThisSensor, 0, $maxVals['count'] ); break; case "avg": // always sum up the same amount of values to get a new array // which doesn't have more than $maxVals values $countOfValuesForAvarage = intval( round(count($rawMeasvalsOfThisSensor) / $maxVals['count'], 0) ); $takenValuesForNextSum = 0; $tmpMeasvalueSum = 0; $tmpTimestampSum = 0; for ($i = 0; $i < count($rawMeasvalsOfThisSensor); $i++) { // loop through all measvals of the sensor $measvalue = $rawMeasvalsOfThisSensor[$i]; if ($measvalue["sensorid"] === $sensor["id"]) { $tmpMeasvalueSum += $measvalue["measvalue"]; $tmpTimestampSum += $measvalue["timestamp"]; $takenValuesForNextSum += 1; } if ($takenValuesForNextSum === $countOfValuesForAvarage || $i === count($rawMeasvalsOfThisSensor) - 1 ) { array_push( $finalMeasvalsOfThisSensor, array( "measvalue" => round( $tmpMeasvalueSum / $takenValuesForNextSum, 2 ), "timestamp" => round( $tmpTimestampSum / $takenValuesForNextSum, 0 ), "sensorid" => $sensor["id"] ) ); $takenValuesForNextSum = 0; $tmpMeasvalueSum = 0; $tmpTimestampSum = 0; } } break; } // insert the new vals of this sensor into the global new vals $finalMeasvals = array_merge( $finalMeasvals, $finalMeasvalsOfThisSensor ); } $measvalues = $finalMeasvals; } // find actual range $min = null; $max = null; foreach ($measvalues as $value) { if ($value['timestamp'] < $min || !isset($min)) { $min = $value['timestamp']; } else if ($value['timestamp'] > $max || !isset($max)) { $max = $value['timestamp']; } } // add sensors and value types to data object $data['measvalues'] = $measvalues; $data['sensors'] = $sensors; $data['valuetypes'] = $valuetypes; $data['range'] = array('from' => $min, 'to' => $max); return $data; } // -------------------- // - setter functions - // -------------------- /** * Function to generate an api key for specific user * * @param int $userId userId * * @return bool */ function createApiKey($userId) { //generate random token $length = 10; $str = ""; $characters = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9')); $max = count($characters) - 1; for ($i = 0; $i < $length; $i++) { $rand = mt_rand(0, $max); $str .= $characters[$rand]; } $apiKey = password_hash($str, PASSWORD_DEFAULT); $sql = "INSERT INTO `apikeys` (userid, apikey) VALUES (\"$userId\", \"$apiKey\")"; return $this->_con->query($sql); } /** * Function to validate and insert submited sensor data * * @param mixed $sensorData object containing * - identity (int) * - signature (string) * - data (string: JSON array[array[sensorId,measvalue,timestamp]]) * * @return array * 'status': 200: was inserted * 400: format of request was wrong * 401: signture verify failed * 404: key identity was not found * 500: internal error * 900: format of data JSON string was invalid * 901: signature was fine but one or more sensors have been ignored due to some error (see 'data') all other sensors were processes successfully * 'data': Array[Array[sensorId, errorCode]] sensors which were ignored due to some error (401: user doesn't own sensor; 404: senso wasn't found) * only set if status is 901 */ function processSensorData($sensorData) { // check if request structure is valid if (!isset($sensorData['identity']) || !isset($sensorData['signature']) || !isset($sensorData['data'])) { return array("status"=>400); } // get private key from database $thisIdentity = $this->_con->real_escape_string($sensorData['identity']); $thisData = $sensorData['data']; // get public key of user $sql = "SELECT * FROM `apikeys` WHERE id=$thisIdentity"; $result = $this->_con->query($sql); if ($result->num_rows < 1) { // public key was not found return array("status"=>404); } // only one row will be returned $data = $result->fetch_assoc(); $key = $data['apikey']; $thisUserId = $data["userid"]; // check signature $signedHash = hash_hmac("sha256", $thisData, $key); if ($signedHash !== strtolower($sensorData['signature'])) { return array("status"=>401); } // signature is valid $measvalues = json_decode($thisData, true); if (!$measvalues) { // the format of the data JSON is invalid return array("status"=>901, "data"=>"malformed data JSON"); } $allSensors = array(); $validSensors = array(); $ignoredSensors = array(); // find all sensors foreach ($measvalues as $measvalue) { $thisSensorId = $this->_con->real_escape_string($measvalue[0]); if (!in_array($thisSensorId, $allSensors)) { array_push($allSensors, $thisSensorId); } } // check if all sensors are valid and belong to the user foreach ($allSensors as $sensor) { // check if sensor belongs to user $sql = "SELECT userid FROM `sensors` WHERE id=\"$sensor\""; $result = $this->_con->query($sql); if ($result->num_rows < 1) { // sensor was not found array_push($ignoredSensors, array($sensor, 404)); continue; } // only one row will be returned $data = $result->fetch_assoc(); if ($data['userid'] !== $thisUserId) { // sensor does not belong to user array_push($ignoredSensors, array($sensor, 401)); continue; } array_push($validSensors, $sensor); } // iterate through all measvalues // and insert them into the database if the sensor is valid foreach ($measvalues as $measvalue) { if (!in_array($measvalue[0], $validSensors)) { // if the sensor is not valid -> continue continue; } $thisSensorId = $this->_con->real_escape_string($measvalue[0]); $thisMeasvalue = $this->_con->real_escape_string($measvalue[1]); $thisTimestamp = $this->_con->real_escape_string($measvalue[2]); $sql = "INSERT INTO `measvalues` (sensorid,measvalue,timestamp) VALUES (\"$thisSensorId\", \"$thisMeasvalue\", \"$thisTimestamp\")"; $this->_con->query($sql); } if (count($ignoredSensors) > 0) { return array("status"=>901, "data"=>$ignoredSensors); } return array("status"=>200); } // -------------------- // - Helper functions - // -------------------- /** * Function to convert a string to hex * * @param string $string the string to be converted * * @return string */ function strToHex($string) { $hex = ''; for ($i=0; $i