1<?php 2/* 3 Copyright (C) 2016 Volker Krause <vkrause@kde.org> 4 5 Permission is hereby granted, free of charge, to any person obtaining 6 a copy of this software and associated documentation files (the 7 "Software"), to deal in the Software without restriction, including 8 without limitation the rights to use, copy, modify, merge, publish, 9 distribute, sublicense, and/or sell copies of the Software, and to 10 permit persons to whom the Software is furnished to do so, subject to 11 the following conditions: 12 13 The above copyright notice and this permission notice shall be included 14 in all copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23*/ 24 25require_once('compat.php'); 26require_once('datastore.php'); 27require_once('product.php'); 28require_once('restexception.php'); 29require_once('schemaentry.php'); 30require_once('schemaentryelement.php'); 31 32/** Represents a data sample received from a user. */ 33class Sample 34{ 35 /** JSON for all data of the given product. */ 36 public static function dataAsJson(DataStore $db, Product $product) 37 { 38 $data = array(); 39 $sampleIdIndex = array(); 40 $i = 0; 41 42 // query scalar table 43 $scalarSql = 'SELECT col_id, col_timestamp '; 44 foreach ($product->schema as $entry) { 45 if (!$entry->isScalar()) 46 continue; 47 foreach ($entry->elements as $elem) 48 $scalarSql .= ', ' . $elem->dataColumnName(); 49 } 50 $scalarSql .= ' FROM ' . $product->dataTableName() . ' ORDER BY col_timestamp ASC'; 51 $scalarStmt = $db->prepare($scalarSql); 52 $db->execute($scalarStmt); 53 foreach ($scalarStmt as $scalarRow) { 54 $rowData = array(); 55 $rowData['id'] = intval($scalarRow['col_id']); 56 $rowData['timestamp'] = $scalarRow['col_timestamp']; 57 foreach ($product->schema as $entry) { 58 if (!$entry->isScalar()) 59 continue; 60 $entryData = null; 61 foreach ($entry->elements as $elem) { 62 $value = self::valueFromDb($elem, $scalarRow[$elem->dataColumnName()]); 63 if (!is_null($value)) 64 $entryData[$elem->name] = $value; 65 } 66 if (!is_null($entryData)) 67 $rowData[$entry->name] = $entryData; 68 } 69 array_push($data, $rowData); 70 $sampleIdIndex[$rowData['id']] = $i++; 71 } 72 73 // query each non-scalar table 74 foreach ($product->schema as $entry) { 75 if ($entry->isScalar()) 76 continue; 77 $sql = 'SELECT col_sample_id'; 78 if ($entry->type == SchemaEntry::MAP_TYPE) 79 $sql .= ', col_key'; 80 foreach ($entry->elements as $elem) 81 $sql .= ', ' . $elem->dataColumnName(); 82 $sql .= ' FROM ' . $entry->dataTableName() . ' ORDER BY col_id ASC'; 83 $stmt = $db->prepare($sql); 84 $db->execute($stmt); 85 foreach ($stmt as $row) { 86 $entryData = null; 87 foreach ($entry->elements as $elem) { 88 $value = self::valueFromDb($elem, $row[$elem->dataColumnName()]); 89 if (!is_null($value)) 90 $entryData[$elem->name] = $value; 91 } 92 $idx = $sampleIdIndex[$row['col_sample_id']]; 93 if (!array_key_exists($entry->name, $data[$idx])) 94 $data[$idx][$entry->name] = array(); 95 if ($entry->type == SchemaEntry::MAP_TYPE) 96 $data[$idx][$entry->name][$row['col_key']] = $entryData; 97 else 98 array_push($data[$idx][$entry->name], $entryData); 99 } 100 } 101 102 return json_encode($data); 103 } 104 105 /** Insert a received sample for @p product into the data store. */ 106 public static function insert(DataStore $db, $jsonData, Product $product) 107 { 108 $jsonObj = json_decode($jsonData); 109 if (!is_object($jsonObj)) 110 throw new RESTException('Invalid data sample format.', 400); 111 if (property_exists($jsonObj, 'id') || property_exists($jsonObj, 'timestamp')) 112 throw new RESTException('Invalid data sample.', 400); 113 114 $sampleId = self::insertScalar($db, $jsonObj, $product); 115 116 foreach ($product->schema as $entry) { 117 if ($entry->isScalar()) 118 continue; 119 self::insertNonScalar($db, $jsonObj, $entry, $sampleId); 120 } 121 } 122 123 /** Insert a array of samples previously exported for @p product. 124 * Unlike Sample::insert, this will preserve ids and timestamps. 125 */ 126 public static function import(DataStore $db, $jsonData, Product $product) 127 { 128 $jsonArray = json_decode($jsonData); 129 if (!is_array($jsonArray)) 130 throw new RESTException('Invalid data sample format.', 400); 131 132 foreach ($jsonArray as $sampleObj) { 133 if (property_exists($sampleObj, 'id') || !property_exists($sampleObj, 'timestamp')) 134 throw new RESTException('Invalid data sample.', 400); 135 136 $sampleId = self::insertScalar($db, $sampleObj, $product); 137 foreach ($product->schema as $entry) { 138 if ($entry->isScalar()) 139 continue; 140 self::insertNonScalar($db, $sampleObj, $entry, $sampleId); 141 } 142 } 143 } 144 145 /** Insert scalar data for @p product. 146 * @return The sample id for use in non-scalar data tables. 147 */ 148 private static function insertScalar(DataStore $db, $jsonObj, Product $product) 149 { 150 $columns = array(); 151 $binds = array(); 152 $values = array(); 153 154 if (property_exists($jsonObj, 'timestamp')) { 155 array_push($columns, 'col_timestamp'); 156 array_push($binds, ':timestamp'); 157 $values[':timestamp'] = array($jsonObj->timestamp, PDO::PARAM_STR); 158 } 159 160 foreach ($product->schema as $entry) { 161 if (!$entry->isScalar()) 162 continue; 163 if (!property_exists($jsonObj, $entry->name) || !is_object($jsonObj->{$entry->name})) 164 continue; 165 foreach ($entry->elements as $elem) { 166 if (!property_exists($jsonObj->{$entry->name}, $elem->name)) 167 continue; 168 169 $v = $jsonObj->{$entry->name}->{$elem->name}; 170 if (!self::isCorrectType($elem, $v)) 171 continue; 172 173 $bind = ':' . $elem->dataColumnName(); 174 array_push($columns, $elem->dataColumnName()); 175 array_push($binds, $bind); 176 $values[$bind] = array($v, self::pdoParamType($elem)); 177 } 178 } 179 180 $sql = 'INSERT INTO ' . $product->dataTableName(); 181 if (count($columns) > 0) { 182 $sql .= ' (' . implode(', ', $columns) . ') VALUES ('; 183 $sql .= implode(', ', $binds) . ')'; 184 } else { 185 if ($db->driver() == 'sqlite') 186 $sql .= ' DEFAULT VALUES'; 187 else 188 $sql .= ' VALUES ()'; 189 } 190 $stmt = $db->prepare($sql); 191 foreach ($values as $key => $data) { 192 $stmt->bindValue($key, $data[0], $data[1]); 193 } 194 $db->execute($stmt); 195 196 return $db->pdoHandle()->lastInsertId(); 197 } 198 199 /** Insert non-scalar elements in seconadary data tables. */ 200 private static function insertNonScalar(DataStore $db, $jsonObj, SchemaEntry $schemaEntry, $sampleId) 201 { 202 if (!property_exists($jsonObj, $schemaEntry->name)) 203 return; 204 $data = $jsonObj->{$schemaEntry->name}; 205 switch ($schemaEntry->type) { 206 case SchemaEntry::LIST_TYPE: 207 if (!is_array($data)) 208 return; 209 break; 210 case SchemaEntry::MAP_TYPE: 211 if (!is_object($data)) 212 return; 213 break; 214 default: 215 Utils::httpError(500, "Unknown non-scalar schema entry type."); 216 } 217 218 $columns = array('col_sample_id'); 219 $binds = array(':sampleId'); 220 if ($schemaEntry->type == SchemaEntry::MAP_TYPE) { 221 array_push($columns, 'col_key'); 222 array_push($binds, ':key'); 223 } 224 foreach ($schemaEntry->elements as $elem) { 225 array_push($columns, $elem->dataColumnName()); 226 array_push($binds, ':' . $elem->dataColumnName()); 227 } 228 $sql = 'INSERT INTO ' . $schemaEntry->dataTableName() 229 . ' (' . implode(', ', $columns) . ') VALUES (' 230 . implode(', ', $binds) . ')'; 231 $stmt = $db->prepare($sql); 232 233 foreach ($data as $key => $entry) { 234 if (!is_object($entry)) 235 continue; 236 $stmt->bindValue(':sampleId', $sampleId, PDO::PARAM_INT); 237 if ($schemaEntry->type == SchemaEntry::MAP_TYPE) { 238 if (!is_string($key) || strlen($key) == 0) 239 continue; 240 $stmt->bindValue(':key', $key, PDO::PARAM_STR); 241 } 242 foreach ($schemaEntry->elements as $elem) { 243 $stmt->bindValue(':' . $elem->dataColumnName(), null, self::pdoParamType($elem)); 244 if (!property_exists($entry, $elem->name)) 245 continue; 246 $v = $entry->{$elem->name}; 247 if (!self::isCorrectType($elem, $v)) 248 continue; 249 $stmt->bindValue(':' . $elem->dataColumnName(), $v, self::pdoParamType($elem)); 250 } 251 $db->execute($stmt); 252 } 253 } 254 255 /** Fix database output that lost the correct type for a schema entry element. */ 256 private static function valueFromDb(SchemaEntryElement $elem, $dbValue) 257 { 258 if (is_null($dbValue)) 259 return null; 260 switch ($elem->type) { 261 case SchemaEntryElement::STRING_TYPE: 262 return strval($dbValue); 263 case SchemaEntryElement::INT_TYPE: 264 return intval($dbValue); 265 case SchemaEntryElement::NUMBER_TYPE: 266 return floatval($dbValue); 267 case SchemaEntryElement::BOOL_TYPE: 268 return boolval($dbValue); 269 } 270 Utils::httpError(500, "Invalid schema entry element type."); 271 } 272 273 /** Check if the given input value @p $v matches the expected type for element @p $elem. */ 274 private static function isCorrectType(SchemaEntryElement $elem, $v) 275 { 276 switch ($elem->type) { 277 case SchemaEntryElement::STRING_TYPE: 278 if (!is_string($v)) 279 return false; 280 break; 281 case SchemaEntryElement::INT_TYPE: 282 if (!is_int($v)) 283 return false; 284 break; 285 case SchemaEntryElement::NUMBER_TYPE: 286 if (!is_float($v)) 287 return false; 288 break; 289 case SchemaEntryElement::BOOL_TYPE: 290 if (!is_bool($v)) 291 return false; 292 break; 293 } 294 return true; 295 } 296 297 /** Determine PDO column type for the given element. */ 298 private static function pdoParamType(SchemaEntryElement $elem) 299 { 300 switch ($elem->type) { 301 case SchemaEntryElement::STRING_TYPE: 302 return PDO::PARAM_STR; 303 case SchemaEntryElement::INT_TYPE: 304 return PDO::PARAM_INT; 305 case SchemaEntryElement::NUMBER_TYPE: 306 return PDO::PARAM_STR; // yes, really... 307 case SchemaEntryElement::BOOL_TYPE: 308 return PDO::PARAM_BOOL; 309 } 310 throw new RESTException('Unsupported element type.', 500); 311 } 312} 313 314?> 315