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