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 ?>