1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Statement base object for xAPI structure checking and validation.
19 *
20 * @package    core_xapi
21 * @copyright  2020 Ferran Recio
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace core_xapi\local;
26
27use core_xapi\local\statement\item;
28use core_xapi\local\statement\item_actor;
29use core_xapi\local\statement\item_object;
30use core_xapi\local\statement\item_verb;
31use core_xapi\local\statement\item_result;
32use core_xapi\local\statement\item_attachment;
33use core_xapi\local\statement\item_context;
34use core_xapi\xapi_exception;
35use JsonSerializable;
36use stdClass;
37
38defined('MOODLE_INTERNAL') || die();
39
40/**
41 * Privacy Subsystem for core_xapi implementing null_provider.
42 *
43 * @copyright  2020 Ferran Recio
44 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45 */
46class statement implements JsonSerializable {
47
48    /** @var item_actor The statement actor. */
49    protected $actor = null;
50
51    /** @var item_verb The statement verb. */
52    protected $verb = null;
53
54    /** @var item_object The statement object. */
55    protected $object = null;
56
57    /** @var item_result The statement result. */
58    protected $result = null;
59
60    /** @var item_context The statement context. */
61    protected $context = null;
62
63    /** @var string The statement timestamp. */
64    protected $timestamp = null;
65
66    /** @var string The statement stored. */
67    protected $stored = null;
68
69    /** @var authority The statement authority. */
70    protected $authority = null;
71
72    /** @var string The statement version. */
73    protected $version = null;
74
75    /** @var item_attachment[] The statement attachments. */
76    protected $attachments = null;
77
78    /** @var additionalfields list of additional fields. */
79    private static $additionalsfields = [
80        'timestamp', 'stored', 'version'
81    ];
82
83    /**
84     * Function to create a full statement from xAPI statement data.
85     *
86     * @param stdClass $data the original xAPI statement
87     * @return statement statement object
88     */
89    public static function create_from_data(stdClass $data): self {
90
91        $result  = new self();
92
93        $requiredfields = ['actor', 'verb', 'object'];
94        foreach ($requiredfields as $required) {
95            if (!isset($data->$required)) {
96                throw new xapi_exception("Missing '{$required}'");
97            }
98        }
99        $result->set_actor(item_actor::create_from_data($data->actor));
100        $result->set_verb(item_verb::create_from_data($data->verb));
101        $result->set_object(item_object::create_from_data($data->object));
102
103        if (isset($data->result)) {
104            $result->set_result(item_result::create_from_data($data->result));
105        }
106
107        if (!empty($data->attachments)) {
108            if (!is_array($data->attachments)) {
109                throw new xapi_exception("Attachments must be an array");
110            }
111            foreach ($data->attachments as $attachment) {
112                $result->add_attachment(item_attachment::create_from_data($attachment));
113            }
114        }
115
116        if (isset($data->context)) {
117            $result->set_context(item_context::create_from_data($data->context));
118        }
119
120        if (isset($data->authority)) {
121            $result->set_authority(item_actor::create_from_data($data->authority));
122        }
123
124        // Store other generic xAPI statement fields.
125        foreach (self::$additionalsfields as $additional) {
126            if (isset($data->$additional)) {
127                $method = 'set_'.$additional;
128                $result->$method($data->$additional);
129            }
130        }
131        return $result;
132    }
133
134    /**
135     * Return the data to serialize in case JSON statement is needed.
136     *
137     * @return stdClass the statement data structure
138     */
139    public function jsonSerialize(): stdClass {
140        $result = (object) [
141            'actor' => $this->actor,
142            'verb' => $this->verb,
143            'object' => $this->object,
144        ];
145        if (!empty($this->result)) {
146            $result->result = $this->result;
147        }
148        if (!empty($this->context)) {
149            $result->context = $this->context;
150        }
151        if (!empty($this->authority)) {
152            $result->authority = $this->authority;
153        }
154        if (!empty($this->attachments)) {
155            $result->attachments = $this->attachments;
156        }
157        foreach (self::$additionalsfields as $additional) {
158            if (!empty($this->$additional)) {
159                $result->$additional = $this->$additional;
160            }
161        }
162        return $result;
163    }
164
165    /**
166     * Returns a minified version of a given statement.
167     *
168     * The returned structure is suitable to store in the "other" field
169     * of logstore. xAPI standard specifies a list of attributes that can be calculated
170     * instead of stored literally. This function get rid of these attributes.
171     *
172     * Note: it also converts stdClass to assoc array to make it compatible
173     * with "other" field in the logstore
174     *
175     * @return array the minimal statement needed to be stored a part from logstore data
176     */
177    public function minify(): ?array {
178        $result = [];
179        $fields = ['verb', 'object',  'context', 'result', 'authority', 'attachments'];
180        foreach ($fields as $field) {
181            if (!empty($this->$field)) {
182                $result[$field] = $this->$field;
183            }
184        }
185        return json_decode(json_encode($result), true);
186    }
187
188    /**
189     * Set the statement actor.
190     *
191     * @param item_actor $actor actor item
192     */
193    public function set_actor(item_actor $actor): void {
194        $this->actor = $actor;
195    }
196
197    /**
198     * Set the statement verb.
199     *
200     * @param item_verb $verb verb element
201     */
202    public function set_verb(item_verb $verb): void {
203        $this->verb = $verb;
204    }
205
206    /**
207     * Set the statement object.
208     *
209     * @param item_object $object compatible object item
210     */
211    public function set_object(item_object $object): void {
212        $this->object = $object;
213    }
214
215    /**
216     * Set the statement context.
217     *
218     * @param item_context $context context item element
219     */
220    public function set_context(item_context $context): void {
221        $this->context = $context;
222    }
223
224    /**
225     * Set the statement result.
226     *
227     * @param item_result $result result item element
228     */
229    public function set_result(item_result $result): void {
230        $this->result = $result;
231    }
232
233    /**
234     * Set the statement timestamp.
235     *
236     * @param string $timestamp timestamp element
237     */
238    public function set_timestamp(string $timestamp): void {
239        $this->timestamp = $timestamp;
240    }
241
242    /**
243     * Set the statement stored.
244     *
245     * @param string $stored stored element
246     */
247    public function set_stored(string $stored): void {
248        $this->stored = $stored;
249    }
250
251    /**
252     * Set the statement authority.
253     *
254     * @param item $authority authority item element
255     */
256    public function set_authority(item_actor $authority): void {
257        $this->authority = $authority;
258    }
259
260    /**
261     * Set the statement version.
262     *
263     * @param string $version version element
264     */
265    public function set_version(string $version): void {
266        $this->version = $version;
267    }
268
269    /**
270     * Adds and attachment to the statement.
271     *
272     * @param item $attachments attachments item element
273     */
274    public function add_attachment(item_attachment $attachment): void {
275        if ($this->attachments === null) {
276            $this->attachments = [];
277        }
278        $this->attachments[] = $attachment;
279    }
280
281    /**
282     * Returns the moodle user represented by this statement actor.
283     *
284     * @throws xapi_exception if it's a group statement
285     * @return stdClass user record
286     */
287    public function get_user(): stdClass {
288        if (!$this->actor) {
289            throw new xapi_exception("No actor defined");
290        }
291        return $this->actor->get_user();
292    }
293
294    /**
295     * Return all moodle users represented by this statement actor.
296     *
297     * @return array user records
298     */
299    public function get_all_users(): array {
300        if (!$this->actor) {
301            throw new xapi_exception("No actor defined");
302        }
303        return $this->actor->get_all_users();
304    }
305
306    /**
307     * Return the moodle group represented by this statement actor.
308     *
309     * @throws xapi_exception if it is not a group statement
310     * @return stdClass a group record
311     */
312    public function get_group(): stdClass {
313        if (!$this->actor) {
314            throw new xapi_exception("No actor defined");
315        }
316        if (method_exists($this->actor, 'get_group')) {
317            return $this->actor->get_group();
318        }
319        throw new xapi_exception("Method not valid on this actor");
320    }
321
322    /**
323     * Returns the statement verb ID.
324     *
325     * @throws xapi_exception in case the item is no yet defined
326     * @return string verb ID
327     */
328    public function get_verb_id(): string {
329        if (!$this->verb) {
330            throw new xapi_exception("No verb defined");
331        }
332        return $this->verb->get_id();
333    }
334
335    /**
336     * Returns the statement activity ID.
337     *
338     * @throws xapi_exception in case the item is no yet defined
339     * @return string activity ID
340     */
341    public function get_activity_id(): string {
342        if (!$this->object) {
343            throw new xapi_exception("No object defined");
344        }
345        if (method_exists($this->object, 'get_id')) {
346            return $this->object->get_id();
347        }
348        throw new xapi_exception("Method not valid on this object");
349    }
350
351    /**
352     * Return the statement actor if it is defined.
353     *
354     * @return item_actor|null
355     */
356    public function get_actor(): ?item_actor {
357        return $this->actor;
358    }
359
360    /**
361     * Return the statement verb if it is defined.
362     *
363     * @return item_verb|null
364     */
365    public function get_verb(): ?item_verb {
366        return $this->verb;
367    }
368
369    /**
370     * Return the statement object if it is defined.
371     *
372     * @return item_object|null
373     */
374    public function get_object(): ?item_object {
375        return $this->object;
376    }
377
378    /**
379     * Return the statement context if it is defined.
380     *
381     * @return item|null
382     */
383    public function get_context(): ?item_context {
384        return $this->context;
385    }
386
387    /**
388     * Return the statement result if it is defined.
389     *
390     * @return item|null
391     */
392    public function get_result(): ?item_result {
393        return $this->result;
394    }
395
396    /**
397     * Return the statement timestamp if it is defined.
398     *
399     * @return string|null
400     */
401    public function get_timestamp(): ?string {
402        return $this->timestamp;
403    }
404
405    /**
406     * Return the statement stored if it is defined.
407     *
408     * @return string|null
409     */
410    public function get_stored(): ?string {
411        return $this->stored;
412    }
413
414    /**
415     * Return the statement authority if it is defined.
416     *
417     * @return item|null
418     */
419    public function get_authority(): ?item_actor {
420        return $this->authority;
421    }
422
423    /**
424     * Return the statement version if it is defined.
425     *
426     * @return string|null
427     */
428    public function get_version(): ?string {
429        return $this->version;
430    }
431
432    /**
433     * Return the statement attachments if it is defined.
434     *
435     * @return item_attachment[]|null
436     */
437    public function get_attachments(): ?array {
438        return $this->attachments;
439    }
440}
441