Passed
Push — master ( 13ffe5...c8742d )
by Russell
02:50
created

VerifiableAdminController::renderJSON()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

133
        if (!$record = Versioned::get_version($class, $id, /** @scrutinizer ignore-type */ $version)) {
Loading history...
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

133
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
134
            return $this->httpError(400, 'Bad request');
135
        }
136
137
        try {
138
            $status = $this->getStatus($record, $record->getExtraByIndex());
139
        } catch (ValidationException $ex) {
140
            $status = self::STATUS_UPSTREAM_ERROR;
141
        }
142
143
        $response = json_encode([
144
            'RecordID' => "$record->RecordID",
145
            'Version' => "$record->Version",
146
            'Class' => get_class($record),
147
            'StatusNice' => $status,
148
            'StatusCode' => $this->getCodeMeta($status, 'code'),
149
            'StatusDefn' => $this->getCodeMeta($status, 'defn'),
150
            'SubmittedAt' => $record->dbObject('Proof')->getSubmittedAt(),
151
            'SubmittedTo' => $record->dbObject('Extra')->getStoreAsArray(),
152
        ], JSON_UNESCAPED_UNICODE);
153
154
        $this->renderJSON($response);
155
    }
156
157
    /**
158
     * Return data used for verifiable statuses.
159
     *
160
     * @param  string $status
161
     * @param  string $key
162
     * @return mixed
163
     */
164
    private function getCodeMeta($status, $key)
165
    {
166
        $refl = new \ReflectionClass(__CLASS__);
167
        $const = array_search($status, $refl->getConstants());
168
        $keyJson = file_get_contents(realpath(__DIR__) . '/../../statuses.json');
169
        $keyMap = json_decode($keyJson, true);
170
        $defn = '';
171
172
        foreach ($keyMap as $map) {
173
            if (isset($map[$const])) {
174
                $defn = $map[$const];
175
            }
176
        }
177
178
        $data = [
179
            'code' => $const,
180
            'defn' => $defn,
181
        ];
182
183
        return $data[$key] ?? $data;
184
185
    }
186
187
    /**
188
     * Gives us the current verification status of the given record. Takes into
189
     * account the state of the saved proof as well as by making a backend
190
     * verification call.
191
     *
192
     * @param  DataObject $record The versioned record we're checking
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...
193
     * @param  array      $nodes  An array of cached chainpoint node IPs
194
     * @return string
195
     */
196
    public function getStatus($record, $nodes)
197
    {
198
        // Set some extra data on the service. In this case, the actual chainpoint
199
        // node addresses, used to submit hashes for the given $record
200
        $this->service->setExtra($nodes);
0 ignored issues
show
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

200
        $this->service->/** @scrutinizer ignore-call */ 
201
                        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...
Bug Best Practice introduced by
The property service does not exist on PhpTek\Verifiable\Contro...rifiableAdminController. Since you implemented __get, consider adding a @property annotation.
Loading history...
201
        $proof = $record->dbObject('Proof');
202
203
        // Basic existence of proof
204
        if (!$proof->getValue()) {
205
            return self::STATUS_LOCAL_PROOF_NONE;
206
        }
207
208
        if ($proof->isInitial()) {
209
            return self::STATUS_INITIAL;
210
        }
211
212
        if ($proof->isPending()) {
213
            return self::STATUS_PENDING;
214
        }
215
216
        // So the saved proof claims to be full. Perform some rudimentary checks
217
        // before we send a full-blown verification request to the backend
218
        if ($proof->isFull()) {
219
            // Tests 3 of the key components of the local proof. Sending a verification
220
            // request will do this and much more for us, but rudimentary local checks
221
            // can prevent a network request
222
            if (!$proof->getHashIdNode() || !$proof->getProof() || !count($proof->getAnchorsComplete())) {
223
                return self::STATUS_LOCAL_COMPONENT_INVALID;
224
            }
225
226
            // We've got this far. The local proof seems to be good. Let's verify
227
            // it against the backend
228
            $response = $this->service->call('verify', $record->getField('Proof'));
229
            $isVerified = ChainpointProof::create()
230
                    ->setValue($response)
231
                    ->isVerified();
232
233
            if (!$isVerified) {
234
                return self::STATUS_VERIFIED_FAIL;
235
            }
236
237
            // OK, so we have an intact local full proof, let's ensure it still
238
            // matches a hash of the data it purports to represent
239
            $remoteHash = ChainpointProof::create()
240
                    ->setValue($response)
241
                    ->getHash();
242
243
            if ($this->service->hash($record->source()) !== $remoteHash) {
244
                return self::STATUS_LOCAL_HASH_INVALID;
245
            }
246
247
            // All is well. As you were...
248
            return self::STATUS_VERIFIED_OK;
249
        }
250
251
        // Default status
252
        return self::STATUS_UNVERIFIED;
253
    }
254
255
    /**
256
     * Properly return JSON, allowing consumers to render returned JSON correctly.
257
     *
258
     * @param  string $json
259
     * @return void
260
     */
261
    private function renderJSON(string $json)
262
    {
263
        header('Content-Type: application/json');
264
        echo $json;
265
        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...
266
    }
267
268
}
269