File indexing completed on 2024-12-15 03:45:06
0001 <?php 0002 /* 0003 SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org> 0004 0005 SPDX-License-Identifier: MIT 0006 */ 0007 0008 require_once('compat.php'); 0009 require_once('datastore.php'); 0010 require_once('product.php'); 0011 require_once('restexception.php'); 0012 require_once('schemaentry.php'); 0013 require_once('schemaentryelement.php'); 0014 0015 /** Represents a data sample received from a user. */ 0016 class Sample 0017 { 0018 /** JSON for all data of the given product. */ 0019 public static function echoDataAsJson(DataStore $db, Product $product) 0020 { 0021 echo '['; 0022 $first = false; 0023 $i = 0; 0024 0025 // query scalar table 0026 $scalarSql = 'SELECT col_id, col_timestamp '; 0027 foreach ($product->schema as $entry) { 0028 if (!$entry->isScalar()) 0029 continue; 0030 foreach ($entry->elements as $elem) 0031 $scalarSql .= ', ' . $elem->dataColumnName(); 0032 } 0033 $scalarSql .= ' FROM ' . $product->dataTableName() . ' ORDER BY col_timestamp ASC'; 0034 $scalarStmt = $db->prepare($scalarSql); 0035 $db->execute($scalarStmt); 0036 foreach ($scalarStmt as $scalarRow) { 0037 $rowData = array(); 0038 $rowData['id'] = intval($scalarRow['col_id']); 0039 $rowData['timestamp'] = $scalarRow['col_timestamp']; 0040 foreach ($product->schema as $entry) { 0041 if ($entry->isScalar()) { 0042 $entryData = null; 0043 foreach ($entry->elements as $elem) { 0044 $value = self::valueFromDb($elem, $scalarRow[$elem->dataColumnName()]); 0045 if (!is_null($value)) 0046 $entryData[$elem->name] = $value; 0047 } 0048 if (!is_null($entryData)) 0049 $rowData[$entry->name] = $entryData; 0050 } else { 0051 $sql = 'SELECT col_sample_id'; 0052 if ($entry->type == SchemaEntry::MAP_TYPE) 0053 $sql .= ', col_key'; 0054 foreach ($entry->elements as $elem) 0055 $sql .= ', ' . $elem->dataColumnName(); 0056 $sql .= ' FROM ' . $entry->dataTableName() . ' WHERE col_sample_id = ' . $rowData['id'] . ' ORDER BY col_id ASC '; 0057 $stmt = $db->prepare($sql); 0058 $db->execute($stmt); 0059 foreach ($stmt as $row) { 0060 $entryData = null; 0061 foreach ($entry->elements as $elem) { 0062 $value = self::valueFromDb($elem, $row[$elem->dataColumnName()]); 0063 if (!is_null($value)) 0064 $entryData[$elem->name] = $value; 0065 } 0066 if (!array_key_exists($entry->name, $rowData)) 0067 $rowData[$entry->name] = array(); 0068 if ($entry->type == SchemaEntry::MAP_TYPE) 0069 $rowData[$entry->name][$row['col_key']] = $entryData; 0070 else 0071 array_push($rowData[$entry->name], $entryData); 0072 } 0073 } 0074 } 0075 0076 if ($i != 0) 0077 echo ','; 0078 echo(json_encode($rowData)); 0079 $i++; 0080 } 0081 echo ']'; 0082 } 0083 0084 /** Insert a received sample for @p product into the data store. */ 0085 public static function insert(DataStore $db, $jsonData, Product $product) 0086 { 0087 $jsonObj = json_decode($jsonData); 0088 if (!is_object($jsonObj)) 0089 throw new RESTException('Invalid data sample format.', 400); 0090 if (empty(get_object_vars($jsonObj))) 0091 return; // happens when application is just querying for surveys, or on entirely invalid input 0092 if (property_exists($jsonObj, 'id') || property_exists($jsonObj, 'timestamp')) 0093 throw new RESTException('Invalid data sample.', 400); 0094 0095 $sampleId = self::insertScalar($db, $jsonObj, $product); 0096 0097 foreach ($product->schema as $entry) { 0098 if ($entry->isScalar()) 0099 continue; 0100 self::insertNonScalar($db, $jsonObj, $entry, $sampleId); 0101 } 0102 } 0103 0104 /** Insert a array of samples previously exported for @p product. 0105 * Unlike Sample::insert, this will preserve ids and timestamps. 0106 */ 0107 public static function import(DataStore $db, $jsonData, Product $product) 0108 { 0109 $jsonArray = json_decode($jsonData); 0110 if (!is_array($jsonArray)) 0111 throw new RESTException('Invalid data sample format.', 400); 0112 0113 foreach ($jsonArray as $sampleObj) { 0114 if (property_exists($sampleObj, 'id') || !property_exists($sampleObj, 'timestamp')) 0115 throw new RESTException('Invalid data sample.', 400); 0116 0117 $sampleId = self::insertScalar($db, $sampleObj, $product); 0118 foreach ($product->schema as $entry) { 0119 if ($entry->isScalar()) 0120 continue; 0121 self::insertNonScalar($db, $sampleObj, $entry, $sampleId); 0122 } 0123 } 0124 } 0125 0126 /** Insert scalar data for @p product. 0127 * @return The sample id for use in non-scalar data tables. 0128 */ 0129 private static function insertScalar(DataStore $db, $jsonObj, Product $product) 0130 { 0131 $columns = array(); 0132 $binds = array(); 0133 $values = array(); 0134 0135 if (property_exists($jsonObj, 'timestamp')) { 0136 array_push($columns, 'col_timestamp'); 0137 array_push($binds, ':timestamp'); 0138 $values[':timestamp'] = array($jsonObj->timestamp, PDO::PARAM_STR); 0139 } 0140 0141 foreach ($product->schema as $entry) { 0142 if (!$entry->isScalar()) 0143 continue; 0144 if (!property_exists($jsonObj, $entry->name) || !is_object($jsonObj->{$entry->name})) 0145 continue; 0146 foreach ($entry->elements as $elem) { 0147 if (!property_exists($jsonObj->{$entry->name}, $elem->name)) 0148 continue; 0149 0150 $v = $jsonObj->{$entry->name}->{$elem->name}; 0151 if (!self::isCorrectType($elem, $v)) 0152 continue; 0153 0154 $bind = ':' . $elem->dataColumnName(); 0155 array_push($columns, $elem->dataColumnName()); 0156 array_push($binds, $bind); 0157 $values[$bind] = array($v, self::pdoParamType($elem)); 0158 } 0159 } 0160 0161 $sql = 'INSERT INTO ' . $product->dataTableName(); 0162 if (count($columns) > 0) { 0163 $sql .= ' (' . implode(', ', $columns) . ') VALUES ('; 0164 $sql .= implode(', ', $binds) . ')'; 0165 } else { 0166 if ($db->driver() == 'sqlite') 0167 $sql .= ' DEFAULT VALUES'; 0168 else 0169 $sql .= ' VALUES ()'; 0170 } 0171 $stmt = $db->prepare($sql); 0172 foreach ($values as $key => $data) { 0173 $stmt->bindValue($key, $data[0], $data[1]); 0174 } 0175 $db->execute($stmt); 0176 0177 return $db->pdoHandle()->lastInsertId(); 0178 } 0179 0180 /** Insert non-scalar elements in seconadary data tables. */ 0181 private static function insertNonScalar(DataStore $db, $jsonObj, SchemaEntry $schemaEntry, $sampleId) 0182 { 0183 if (!property_exists($jsonObj, $schemaEntry->name)) 0184 return; 0185 $data = $jsonObj->{$schemaEntry->name}; 0186 switch ($schemaEntry->type) { 0187 case SchemaEntry::LIST_TYPE: 0188 if (!is_array($data)) 0189 return; 0190 break; 0191 case SchemaEntry::MAP_TYPE: 0192 if (!is_object($data)) 0193 return; 0194 break; 0195 default: 0196 Utils::httpError(500, "Unknown non-scalar schema entry type."); 0197 } 0198 0199 $columns = array('col_sample_id'); 0200 $binds = array(':sampleId'); 0201 if ($schemaEntry->type == SchemaEntry::MAP_TYPE) { 0202 array_push($columns, 'col_key'); 0203 array_push($binds, ':key'); 0204 } 0205 foreach ($schemaEntry->elements as $elem) { 0206 array_push($columns, $elem->dataColumnName()); 0207 array_push($binds, ':' . $elem->dataColumnName()); 0208 } 0209 $sql = 'INSERT INTO ' . $schemaEntry->dataTableName() 0210 . ' (' . implode(', ', $columns) . ') VALUES (' 0211 . implode(', ', $binds) . ')'; 0212 $stmt = $db->prepare($sql); 0213 0214 foreach ($data as $key => $entry) { 0215 if (!is_object($entry)) 0216 continue; 0217 $stmt->bindValue(':sampleId', $sampleId, PDO::PARAM_INT); 0218 if ($schemaEntry->type == SchemaEntry::MAP_TYPE) { 0219 if (!is_string($key) || strlen($key) == 0) 0220 continue; 0221 $stmt->bindValue(':key', $key, PDO::PARAM_STR); 0222 } 0223 foreach ($schemaEntry->elements as $elem) { 0224 $stmt->bindValue(':' . $elem->dataColumnName(), null, self::pdoParamType($elem)); 0225 if (!property_exists($entry, $elem->name)) 0226 continue; 0227 $v = $entry->{$elem->name}; 0228 if (!self::isCorrectType($elem, $v)) 0229 continue; 0230 $stmt->bindValue(':' . $elem->dataColumnName(), $v, self::pdoParamType($elem)); 0231 } 0232 $db->execute($stmt); 0233 } 0234 } 0235 0236 /** Fix database output that lost the correct type for a schema entry element. */ 0237 private static function valueFromDb(SchemaEntryElement $elem, $dbValue) 0238 { 0239 if (is_null($dbValue)) 0240 return null; 0241 switch ($elem->type) { 0242 case SchemaEntryElement::STRING_TYPE: 0243 return strval($dbValue); 0244 case SchemaEntryElement::INT_TYPE: 0245 return intval($dbValue); 0246 case SchemaEntryElement::NUMBER_TYPE: 0247 return floatval($dbValue); 0248 case SchemaEntryElement::BOOL_TYPE: 0249 return boolval($dbValue); 0250 } 0251 Utils::httpError(500, "Invalid schema entry element type."); 0252 } 0253 0254 /** Check if the given input value @p $v matches the expected type for element @p $elem. */ 0255 private static function isCorrectType(SchemaEntryElement $elem, $v) 0256 { 0257 switch ($elem->type) { 0258 case SchemaEntryElement::STRING_TYPE: 0259 if (!is_string($v)) 0260 return false; 0261 break; 0262 case SchemaEntryElement::INT_TYPE: 0263 if (!is_int($v)) 0264 return false; 0265 break; 0266 case SchemaEntryElement::NUMBER_TYPE: 0267 if (!is_float($v)) 0268 return false; 0269 break; 0270 case SchemaEntryElement::BOOL_TYPE: 0271 if (!is_bool($v)) 0272 return false; 0273 break; 0274 } 0275 return true; 0276 } 0277 0278 /** Determine PDO column type for the given element. */ 0279 private static function pdoParamType(SchemaEntryElement $elem) 0280 { 0281 switch ($elem->type) { 0282 case SchemaEntryElement::STRING_TYPE: 0283 return PDO::PARAM_STR; 0284 case SchemaEntryElement::INT_TYPE: 0285 return PDO::PARAM_INT; 0286 case SchemaEntryElement::NUMBER_TYPE: 0287 return PDO::PARAM_STR; // yes, really... 0288 case SchemaEntryElement::BOOL_TYPE: 0289 return PDO::PARAM_BOOL; 0290 } 0291 throw new RESTException('Unsupported element type.', 500); 0292 } 0293 } 0294 0295 ?>