Issues (1)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

source/Spiral/ODM/DocumentEntity.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ODM;
9
10
use MongoDB\BSON\ObjectID;
11
use Spiral\Core\Component;
12
use Spiral\Core\Exceptions\ScopeException;
13
use Spiral\Core\Traits\SaturateTrait;
14
use Spiral\Models\AccessorInterface;
15
use Spiral\Models\SchematicEntity;
16
use Spiral\Models\Traits\SolidableTrait;
17
use Spiral\ODM\Entities\DocumentCompositor;
18
use Spiral\ODM\Entities\DocumentInstantiator;
19
use Spiral\ODM\Exceptions\AccessException;
20
use Spiral\ODM\Exceptions\AggregationException;
21
use Spiral\ODM\Exceptions\DocumentException;
22
use Spiral\ODM\Helpers\AggregationHelper;
23
use Spiral\ODM\Schemas\Definitions\CompositionDefinition;
24
25
/**
26
 * Primary class for spiral ODM, provides ability to pack it's own updates in a form of atomic
27
 * updates.
28
 *
29
 * You can use same properties to configure entity as in DataEntity + schema property.
30
 *
31
 * Example:
32
 *
33
 * class Test extends DocumentEntity
34
 * {
35
 *    const SCHEMA = [
36
 *       'name' => 'string'
37
 *    ];
38
 * }
39
 *
40
 * Configuration properties:
41
 * - schema
42
 * - defaults
43
 * - secured (* by default)
44
 * - fillable
45
 */
46
abstract class DocumentEntity extends SchematicEntity implements CompositableInterface
47
{
48
    use SaturateTrait, SolidableTrait;
49
50
    /*
51
     * Begin set of behaviour and description constants.
52
     * ================================================
53
     */
54
55
    /**
56
     * Set of schema sections needed to describe entity behaviour.
57
     */
58
    const SH_INSTANTIATION = 0;
59
    const SH_DEFAULTS      = 1;
60
    const SH_COMPOSITIONS  = 5;
61
    const SH_AGGREGATIONS  = 6;
62
63
    /**
64
     * Constants used to describe aggregation relations (also used internally to identify
65
     * composition).
66
     *
67
     * Example:
68
     * 'items' => [self::MANY => Item::class, ['parentID' => 'key::_id']]
69
     *
70
     * @see DocumentEntity::SCHEMA
71
     */
72
    const MANY = 778;
73
    const ONE  = 899;
74
75
    /*
76
     * ================================================
77
     * End set of behaviour and description constants.
78
     */
79
80
    /**
81
     * Class responsible for instance construction.
82
     */
83
    const INSTANTIATOR = DocumentInstantiator::class;
84
85
    /**
86
     * Document fields, accessors and relations. ODM will generate setters and getters for some
87
     * fields based on their types.
88
     *
89
     * Example, fields:
90
     * const SCHEMA = [
91
     *      '_id'    => 'MongoId', //Primary key field
92
     *      'value'  => 'string',  //Default string field
93
     *      'values' => ['string'] //ScalarArray accessor will be applied for fields like that
94
     * ];
95
     *
96
     * Compositions:
97
     * const SCHEMA = [
98
     *     ...,
99
     *     'child'       => Child::class,   //One document are composited, for example user Profile
100
     *     'many'        => [Child::class]  //Compositor accessor will be applied, allows to
101
     *                                      //composite many document instances
102
     * ];
103
     *
104
     * Documents can extend each other, in this case schema will also be inherited.
105
     *
106
     * Attention, make sure you properly set FILLABLE option in parent class to use constructions
107
     * like:
108
     * $parent->child = [...];
109
     *
110
     * or
111
     * $parent->setFields(['child'=>[...]]);
112
     *
113
     * @var array
114
     */
115
    const SCHEMA = [];
116
117
    /**
118
     * Default field values.
119
     *
120
     * @var array
121
     */
122
    const DEFAULTS = [];
123
124
    /**
125
     * Model behaviour configurations.
126
     */
127
    const SECURED   = '*';
128
    const FILLABLE  = [];
129
    const SETTERS   = [];
130
    const GETTERS   = [];
131
    const ACCESSORS = [];
132
133
    /**
134
     * Document behaviour schema.
135
     *
136
     * @var array
137
     */
138
    private $documentSchema = [];
139
140
    /**
141
     * Document field updates (changed values).
142
     *
143
     * @var array
144
     */
145
    private $changes = [];
146
147
    /**
148
     * Parent ODM instance, responsible for aggregations and lazy loading operations.
149
     *
150
     * @invisible
151
     * @var ODMInterface
152
     */
153
    protected $odm;
154
155
    /**
156
     * {@inheritdoc}
157
     *
158
     * @param ODMInterface $odm To lazy create nested document ang aggregations.
159
     *
160
     * @throws ScopeException When no ODM instance can be resolved.
161
     */
162
    public function __construct(array $data = [], ODMInterface $odm = null, array $schema = null)
163
    {
164
        //We can use global container as fallback if no default values were provided
165
        $this->odm = $this->saturate($odm, ODMInterface::class);
166
167
        //Use supplied schema or fetch one from ODM
168
        $this->documentSchema = !empty($schema) ? $schema : $this->odm->define(
169
            static::class,
170
            ODMInterface::D_SCHEMA
171
        );
172
173
        $data = is_array($data) ? $data : [];
174
        if (!empty($this->documentSchema[self::SH_DEFAULTS])) {
175
            //Merging with default values
176
            $data = array_replace_recursive($this->documentSchema[self::SH_DEFAULTS], $data);
177
        }
178
179
        parent::__construct($data, $this->documentSchema);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function getField(string $name, $default = null, bool $filter = true)
186
    {
187
        $this->assertField($name);
188
189
        return parent::getField($name, $default, $filter);
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     *
195
     * Tracks field changes.
196
     */
197
    public function setField(string $name, $value, bool $filter = true)
198
    {
199
        $this->assertField($name);
200
        $this->registerChange($name);
201
202
        parent::setField($name, $value, $filter);
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     *
208
     * Will restore default value if presented.
209
     */
210
    public function __unset($offset)
211
    {
212
        if (!$this->isNullable($offset)) {
213
            throw new AccessException("Unable to unset not nullable field '{$offset}'");
214
        }
215
216
        $this->setField($offset, null, false);
217
    }
218
219
    /**
220
     * Provides ability to invoke document aggregation.
221
     *
222
     * @param string $method
223
     * @param array  $arguments
224
     *
225
     * @return mixed|null|AccessorInterface|CompositableInterface|Document|Entities\DocumentSelector
226
     */
227
    public function __call($method, array $arguments)
228
    {
229
        if (isset($this->documentSchema[self::SH_AGGREGATIONS][$method])) {
230
            if (!empty($arguments)) {
231
                throw new AggregationException("Aggregation method call except 0 parameters");
232
            }
233
234
            $helper = new AggregationHelper($this, $this->odm);
235
236
            return $helper->createAggregation($method);
237
        }
238
239
        throw new DocumentException("Undefined method call '{$method}' in '" . get_called_class() . "'");
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     *
245
     * @param string $field Check once specific field changes.
246
     */
247
    public function hasChanges(string $field = null): bool
248
    {
249
        //Check updates for specific field
250
        if (!empty($field)) {
251
            if (array_key_exists($field, $this->changes)) {
252
                return true;
253
            }
254
255
            //Do not force accessor creation
256
            $value = $this->getField($field, null, false);
257
            if ($value instanceof CompositableInterface && $value->hasChanges()) {
258
                return true;
259
            }
260
261
            return false;
262
        }
263
264
        if (!empty($this->changes)) {
265
            return true;
266
        }
267
268
        //Do not force accessor creation
269
        foreach ($this->getFields(false) as $value) {
270
            //Checking all fields for changes (handled internally)
271
            if ($value instanceof CompositableInterface && $value->hasChanges()) {
272
                return true;
273
            }
274
        }
275
276
        return false;
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     */
282
    public function buildAtomics(string $container = null): array
283
    {
284
        if (!$this->hasChanges() && !$this->isSolid()) {
285
            return [];
286
        }
287
288
        if ($this->isSolid()) {
289
            if (!empty($container)) {
290
                //Simple nested document in solid state
291
                return ['$set' => [$container => $this->packValue(false)]];
292
            }
293
294
            //No parent container
295
            return ['$set' => $this->packValue(false)];
296
        }
297
298
        //Aggregate atomics from every nested composition
299
        $atomics = [];
300
301
        foreach ($this->getFields(false) as $field => $value) {
302
            if ($value instanceof CompositableInterface) {
303
                $atomics = array_merge_recursive(
304
                    $atomics,
305
                    $value->buildAtomics((!empty($container) ? $container . '.' : '') . $field)
306
                );
307
308
                continue;
309
            }
310
311
            foreach ($atomics as $atomic => $operations) {
312
                if (array_key_exists($field, $operations) && $atomic != '$set') {
313
                    //Property already changed by atomic operation
314
                    continue;
315
                }
316
            }
317
318
            if (array_key_exists($field, $this->changes)) {
319
                //Generating set operation for changed field
320
                $atomics['$set'][(!empty($container) ? $container . '.' : '') . $field] = $value;
321
            }
322
        }
323
324
        return $atomics;
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330
    public function flushChanges()
331
    {
332
        $this->changes = [];
333
334
        foreach ($this->getFields(false) as $field => $value) {
335
            if ($value instanceof CompositableInterface) {
336
                $value->flushChanges();
337
            }
338
        }
339
    }
340
341
    /**
342
     * {@inheritdoc}
343
     *
344
     * @param bool $includeID Set to false to exclude _id from packed fields.
345
     */
346
    public function packValue(bool $includeID = true)
347
    {
348
        $values = parent::packValue();
349
350
        if (!$includeID) {
351
            unset($values['_id']);
352
        }
353
354
        return $values;
355
    }
356
357
    /**
358
     * Since most of ODM documents might contain ObjectIDs and other fields we will try to normalize
359
     * them into string values.
360
     *
361
     * @return array
362
     */
363
    public function publicValue(): array
364
    {
365
        $public = parent::publicValue();
366
367
        array_walk_recursive($public, function (&$value) {
368
            if ($value instanceof ObjectID) {
0 ignored issues
show
The class MongoDB\BSON\ObjectID does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
369
                $value = (string)$value;
370
            }
371
        });
372
373
        return $public;
374
    }
375
376
    /**
377
     * Cloning will be called when object will be embedded into another document.
378
     */
379
    public function __clone()
380
    {
381
        //De-serialize document in order to ensure that all compositions are recreated
382
        $this->setValue($this->packValue());
383
384
        //Since document embedded as one piece let's ensure that it is solid
385
        $this->solidState = true;
386
        $this->changes = [];
387
    }
388
389
    /**
390
     * @return array
391
     */
392
    public function __debugInfo()
393
    {
394
        return [
395
            'fields'  => $this->getFields(),
396
            'atomics' => $this->hasChanges() ? $this->buildAtomics() : [],
397
        ];
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     *
403
     * @see CompositionDefinition
404
     */
405
    protected function getMutator(string $field, string $mutator)
406
    {
407
        /**
408
         * Every document composition is valid accessor but defined a bit differently.
409
         */
410
        if (isset($this->documentSchema[self::SH_COMPOSITIONS][$field])) {
411
            return $this->documentSchema[self::SH_COMPOSITIONS][$field];
412
        }
413
414
        return parent::getMutator($field, $mutator);
415
    }
416
417
    /**
418
     * {@inheritdoc}
419
     */
420
    protected function isNullable(string $field): bool
421
    {
422
        if (array_key_exists($field, $this->documentSchema[self::SH_DEFAULTS])) {
423
            //Only fields with default null value can be nullable
424
            return is_null($this->documentSchema[self::SH_DEFAULTS][$field]);
425
        }
426
427
        //Values unknown to schema always nullable
428
        return true;
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     *
434
     * DocumentEntity will pass ODM instance as part of accessor context.
435
     *
436
     * @see CompositionDefinition
437
     */
438
    protected function createAccessor(
439
        $accessor,
440
        string $name,
441
        $value,
442
        array $context = []
443
    ): AccessorInterface {
444
        if (is_array($accessor)) {
445
            //We are working with definition of composition.
446
            switch ($accessor[0]) {
447
                case self::ONE:
448
                    //Singular embedded document
449
                    return $this->odm->make($accessor[1], $value, false);
450
                case self::MANY:
451
                    return new DocumentCompositor($accessor[1], $value, $this->odm);
452
            }
453
454
            throw new AccessException("Invalid accessor definition for field '{$name}'");
455
        }
456
457
        //Field as a context
458
        return parent::createAccessor($accessor, $name, $value, $context + ['odm' => $this->odm]);
459
    }
460
461
    /**
462
     * {@inheritdoc}
463
     */
464
    protected function iocContainer()
465
    {
466
        if ($this->odm instanceof Component) {
467
            //Forwarding IoC scope to parent ODM instance
468
            return $this->odm->iocContainer();
469
        }
470
471
        return parent::iocContainer();
472
    }
473
474
    /**
475
     * @param string $name
476
     */
477
    private function registerChange(string $name)
478
    {
479
        $original = $this->getField($name, null, false);
480
481
        if (!array_key_exists($name, $this->changes)) {
482
            //Let's keep track of how field looked before first change
483
            $this->changes[$name] = $original instanceof AccessorInterface
484
                ? $original->packValue()
485
                : $original;
486
        }
487
    }
488
489
    /**
490
     * @param string $name
491
     *
492
     * @throws AccessException
493
     */
494
    private function assertField(string $name)
495
    {
496
        if (!$this->hasField($name)) {
497
            throw new AccessException(sprintf(
498
                "No such property '%s' in '%s', check schema being relevant",
499
                $name,
500
                get_called_class()
501
            ));
502
        }
503
    }
504
}
505