Completed
Push — master ( b57500...813b2e )
by Russell
06:35
created

VerifiableController::getStatus()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 7
nop 2
dl 0
loc 45
rs 7.6444
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @author  Russell Michell 2018 <[email protected]>
5
 * @package silverstripe-verifiable
6
 */
7
8
namespace PhpTek\Verifiable\Controller;
9
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\HTTPRequest;
12
use SilverStripe\Versioned\Versioned;
13
use PhpTek\Verifiable\ORM\FieldType\ChainpointProof;
14
use SilverStripe\ORM\ValidationException;
15
16
/**
17
 * Accepts incoming requests for data verification e.g. from within the CMS
18
 * or framework's admin area, proxies them through {@link VerifiableService} and
19
 * sends them on their way.
20
 *
21
 * Will proxy validation requests to the currently configured backend for both
22
 * {@link SiteTree} and {@link DataObject} subclasses.
23
 */
24
class VerifiableController extends Controller
25
{
26
    /**
27
     * No local proof was found for this version. If this is the first version,
28
     * you can safely ignore this message. Otherwise, this is evidence that this
29
     * version has been tampered-with.
30
     *
31
     * @var string
32
     */
33
    const STATUS_LOCAL_PROOF_NONE = 'Local Proof Not Found';
34
35
    /**
36
     * One or more key components of the local proof, were found to be invalid.
37
     * Evidence that the record has been tampered-with.
38
     *
39
     * @var string
40
     */
41
    const STATUS_LOCAL_COMPONENT_INVALID = 'Local Components Invalid';
42
43
    /**
44
     * A mismatch exists between the stored hash for this version, and the data the
45
     * hash was generated from. Evidence that the record has been tampered-with.
46
     *
47
     * @var string
48
     */
49
    const STATUS_LOCAL_HASH_INVALID = 'Local Hash Invalid';
50
51
    /**
52
     * All checks passed. This version's hash and proof are intact and verified.
53
     *
54
     * @var string
55
     */
56
    const STATUS_VERIFIED = 'Verified';
57
58
    /**
59
     * This version is unverified. If this state persists, something is not working
60
     * correctly. Please consult a developer.
61
     *
62
     * @var string
63
     */
64
    const STATUS_UNVERIFIED = 'Unverified';
65
66
    /**
67
     * This version's hash confirmation is currently pending. If it's been more than
68
     * two hours since submission, try again.
69
     *
70
     * @var string
71
     */
72
    const STATUS_PENDING = 'Pending';
73
74
    /**
75
     * The verification process encountered a network error communicating with the
76
     * backend. Try again in a moment.
77
     *
78
     * @var string
79
     */
80
    const STATUS_UPSTREAM_ERROR = 'Upstream Error';
81
82
    /**
83
     * @var array
84
     */
85
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
86
        'verifyhash',
87
    ];
88
89
    /**
90
     * Verify the integrity of arbitrary data by means of a single hash.
91
     *
92
     * Responds to URIs of the following prototype: /verifiable/verify/<model>/<ID>/<VID>
93
     * by echoing a JSON response for consumption by client-side logic.
94
     *
95
     * @param  HTTPRequest $request
96
     * @return void
97
     */
98
    public function verifyhash(HTTPRequest $request)
99
    {
100
        $class = $request->param('ClassName');
101
        $id = $request->param('ModelID');
102
        $version = $request->param('VersionID');
103
104
        if (empty($id) || !is_numeric($id) ||
105
                empty($version) || !is_numeric($version) ||
106
                empty($class)) {
107
            return $this->httpError(400, 'Bad request');
108
        }
109
110
        if (!$record = Versioned::get_version($class, $id, $version)) {
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of SilverStripe\Versioned\Versioned::get_version(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
Bug introduced by
$version of type string is incompatible with the type integer expected by parameter $version of SilverStripe\Versioned\Versioned::get_version(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
        if (!$record = Versioned::get_version($class, $id, /** @scrutinizer ignore-type */ $version)) {
Loading history...
111
            return $this->httpError(400, 'Bad request');
112
        }
113
114
        try {
115
            $status = $this->getStatus($record, $record->getExtraByIndex());
116
        } catch (ValidationException $ex) {
117
            $status = self::STATUS_UPSTREAM_ERROR;
118
        }
119
120
        $response = json_encode([
121
            'RecordID' => "$record->RecordID",
122
            'Version' => "$record->Version",
123
            'Class' => get_class($record),
124
            'StatusNice' => $status,
125
            'StatusCode' => $this->getCodeMeta($status, 'code'),
126
            'StatusDefn' => $this->getCodeMeta($status, 'defn'),
127
            'SubmittedAt' => $record->dbObject('Proof')->getSubmittedAt(),
128
            'SubmittedTo' => $record->dbObject('Extra')->getStoreAsArray(),
129
        ], JSON_UNESCAPED_UNICODE);
130
131
        $this->renderJSON($response);
132
    }
133
134
    /**
135
     * Return data used for verifiable statuses.
136
     *
137
     * @param  string $status
138
     * @param  string $key
139
     * @return mixed
140
     */
141
    private function getCodeMeta($status, $key)
142
    {
143
        $refl = new \ReflectionClass(__CLASS__);
144
        $const = array_search($status, $refl->getConstants());
145
        $keyJson = file_get_contents(realpath(__DIR__) . '/../../statuses.json');
146
        $keyMap = json_decode($keyJson, true);
147
        $defn = '';
148
149
        foreach ($keyMap as $map) {
150
            if (isset($map[$const])) {
151
                $defn = $map[$const];
152
            }
153
        }
154
155
        $data = [
156
            'code' => $const,
157
            'defn' => $defn,
158
        ];
159
160
        return $data[$key] ?? $data;
161
162
    }
163
164
    /**
165
     * Gives us the current verification status of the given record. Takes into
166
     * account the state of the saved proof as well as by making a backend
167
     * verification call.
168
     *
169
     * @param  DataObject $record
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Controller\DataObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
170
     * @param  array      $nodes
171
     * @return string
172
     */
173
    public function getStatus($record, $nodes)
174
    {
175
        // Set some extra data on the service. In this case, the actual chainpoint
176
        // node addresses, used to submit hashes for the given $record
177
        $this->verifiableService->setExtra($nodes);
0 ignored issues
show
Bug Best Practice introduced by
The property verifiableService does not exist on PhpTek\Verifiable\Controller\VerifiableController. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
The method setExtra() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

177
        $this->verifiableService->/** @scrutinizer ignore-call */ 
178
                                  setExtra($nodes);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
178
        $proof = $record->dbObject('Proof');
179
180
        // Basic existence of proof
181
        if (!$proof->getValue()) {
182
            return self::STATUS_LOCAL_PROOF_NONE;
183
        }
184
185
        if ($proof->isPending()) {
186
            return self::STATUS_PENDING;
187
        }
188
189
        // So the saved proof claims to be full and verified eh?
190
        // Let's see about that. Perform some rudimentary checks before we send a
191
        // full-blown verification request to the backend
192
        if ($proof->isVerified()) {
193
            // Tests 3 of the key components of the local proof. Sending a verification
194
            // request will do this and much more for us, but rudimentary local checks
195
            // may prevent a network request even being needed.
196
            if (!$proof->getHashIdNode() || !$proof->getHash() || !$proof->getSubmittedAt()) {
197
                return self::STATUS_LOCAL_COMPONENT_INVALID;
198
            }
199
200
            // OK, so we have an intact local hash, let's ensure it still matches a hash
201
            // of the data it purports to represent
202
            if ($proof->getHash() !== $this->verifiableService->hash($record->source())) {
203
                return self::STATUS_LOCAL_HASH_INVALID;
204
            }
205
206
            // We've got this far. The local proof seems pretty good. Let's verify against
207
            // the backend and make sure
208
            $response = $this->verifiableService->call('verify', $record->getField('Proof'));
209
210
            // TODO
211
            if ($response->getStatusCode() !== 209) {
212
                return self::STATUS_VERIFIED;
213
            }
214
        }
215
216
        // Default status
217
        return self::STATUS_UNVERIFIED;
218
    }
219
220
    /**
221
     * Properly return JSON, allowing consumers to render returned JSON correctly.
222
     *
223
     * @param  string $json
224
     * @return void
225
     */
226
    private function renderJSON(string $json)
227
    {
228
        header('Content-Type: application/json');
229
        echo $json;
230
        exit(0);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
231
    }
232
233
}
234