1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\System\Classes;
9:
10: use Aurora\System\EventEmitter;
11: use Aurora\System\Validator;
12: use Illuminate\Database\Eloquent\Model as Eloquent;
13: use Illuminate\Database\Eloquent\Relations\BelongsTo;
14:
15: /**
16: * Aurora\System\Classes\Model
17: *
18: * @property int $Id Object primary key
19: * @property array $Properties Custom properties for use by other modules
20: *
21: * @property-read mixed $entity_id
22: * @method static \Illuminate\Database\Eloquent\Builder|\Aurora\System\Classes\Model firstWhere(Closure|string|array|\Illuminate\Database\Query\Expression $column, mixed $operator = null, mixed $value = null, string $boolean = 'and')
23: * @method static \Illuminate\Database\Eloquent\Builder|Model newModelQuery()
24: * @method static \Illuminate\Database\Eloquent\Builder|Model newQuery()
25: * @method static \Illuminate\Database\Eloquent\Builder|Model query()
26: * @method static \Illuminate\Database\Eloquent\Builder|\Aurora\System\Classes\Model where(Closure|string|array|\Illuminate\Database\Query\Expression $column, mixed $operator = null, mixed $value = null, string $boolean = 'and')
27: * @method static \Illuminate\Database\Eloquent\Builder|\Aurora\System\Classes\Model whereIn(string $column, mixed $values, string $boolean = 'and', bool $not = false)
28: * @mixin \Eloquent
29: */
30: class Model extends Eloquent
31: {
32: use DisabledModulesTrait;
33:
34: /**
35: * The module name of the model.
36: *
37: * @var string
38: */
39: protected $moduleName = '';
40:
41: /**
42: * The primary key for the model.
43: *
44: * @var string
45: */
46: protected $primaryKey = 'Id';
47:
48: /**
49: * The parent type for the model.
50: *
51: * @var string
52: */
53: protected $parentType = null;
54:
55: /**
56: * The parent key for the model.
57: *
58: * @var string
59: */
60: protected $parentKey = null;
61:
62: /**
63: *
64: * @var string
65: */
66: protected $foreignModel = '';
67:
68: /**
69: *
70: * @var string
71: */
72: protected $foreignModelIdColumn = '';
73:
74: /**
75: *
76: * @var string
77: */
78: protected $foreignModelPrimaryKey = '';
79:
80: /**
81: * Inherited attributes.
82: *
83: * @var array
84: */
85: protected $parentInheritedAttributes = [];
86:
87: /**
88: * @var array
89: */
90: protected $validationRules = [];
91:
92: /**
93: * @var array
94: */
95: protected $validationMessages = [];
96:
97: /**
98: * The name of the "created at" column.
99: *
100: * @var string|null
101: */
102: public const CREATED_AT = 'CreatedAt';
103:
104: /**
105: * The name of the "updated at" column.
106: *
107: * @var string|null
108: */
109: public const UPDATED_AT = 'UpdatedAt';
110:
111: protected static $inheritedAttributes = [];
112:
113: protected function castAttribute($key, $value)
114: {
115: if (is_null($value)) {
116: switch ($this->getCastType($key)) {
117: case 'array':
118: $value = [];
119: break;
120:
121: case 'string':
122: $value = '';
123: break;
124:
125: case 'boolean':
126: $value = false;
127: break;
128: }
129:
130: return $value;
131: }
132:
133: return parent::castAttribute($key, $value);
134: }
135:
136: public function getInheritedAttributes()
137: {
138: return InheritedAttributes::getAttributes(static::class);
139: }
140:
141: /**
142: * @param string $key
143: * @return bool
144: */
145: public function isInheritedAttribute($key)
146: {
147: return InheritedAttributes::hasAttribute(static::class, $key);
148: }
149:
150: /**
151: * @param string $key
152: * @return mixed
153: */
154: public function getInheritedValue($key)
155: {
156: $value = null;
157: $parent = $this->parent();
158: if ($parent instanceof BelongsTo && isset($this->parent)) {
159: $value = $this->parent->$key;
160: }
161: if ($value === null && is_subclass_of($this->parentType, \Aurora\System\AbstractSettings::class)) {
162: if ($this->parentType === \Aurora\System\Settings::class) {
163: $value = \Aurora\System\Api::GetSettings()->GetValue($key);
164: }
165: if ($this->parentType === \Aurora\System\Module\Settings::class) {
166: if (strpos($key, '::') !== false) {
167: [$moduleName, $key] = \explode('::', $key);
168: } else {
169: $moduleName = $this->moduleName;
170: }
171: $oModule = \Aurora\System\Api::GetModule($moduleName);
172: if ($oModule instanceof \Aurora\System\Module\AbstractModule) {
173: $value = $oModule->getConfig($key, $value);
174: }
175: }
176: }
177:
178: return $value;
179: }
180:
181: public function getOrphanIds()
182: {
183: if (!$this->foreignModel || !$this->foreignModelIdColumn) {
184: return ['status' => -1, 'message' => 'Foreign field doesn\'t exist'];
185: }
186:
187: $container = \Aurora\System\Api::GetContainer();
188: $connection = $container['capsule']->getConnection($this->getConnectionName());
189: $tableName = $this->getTable();
190: $foreignObject = new $this->foreignModel();
191: $foreignConnection = $container['capsule']->getConnection($foreignObject->getConnectionName());
192: $foreignTable = $foreignObject->getTable();
193:
194: // Adding prefix is required because current module work with custom DB connection without prefix. See MtaConnector module
195: if ($connection->getName() != 'default' && $foreignConnection->getName() == 'default') {
196: $foreignTable = $foreignConnection->getTablePrefix() . $foreignTable;
197: }
198:
199: $foreignPK = !empty($this->foreignModelPrimaryKey) ? $this->foreignModelPrimaryKey : $foreignObject->primaryKey;
200:
201: $query = self::query();
202: if ($this->foreignModelIdColumn === 'UserId' || $this->foreignModelIdColumn === 'IdUser') {
203: $query = $query->where("$tableName.$this->foreignModelIdColumn", '<>', -1);
204: }
205:
206: $orphanIds = $query->pluck($this->primaryKey)->diff(
207: self::query()->leftJoin($foreignTable, "$tableName.$this->foreignModelIdColumn", '=', "$foreignTable.$foreignPK")
208: ->whereNotNull("$foreignTable.$foreignPK")->pluck("$tableName.$this->primaryKey")
209: )->all();
210:
211: $message = $orphanIds ? "$tableName table has orphans: " . count($orphanIds) . "." : "Orphans were not found.";
212: $oResult = ['status' => $orphanIds ? 1 : 0, 'message' => $message, 'orphansIds' => $orphanIds];
213:
214: return $oResult;
215: }
216:
217: /**
218: * Dynamically set attributes on the model.
219: *
220: * @param string $key
221: * @param mixed $value
222: * @return void
223: */
224: public function __set($key, $value)
225: {
226: if (strpos($key, '::') !== false) {
227: $this->setExtendedProp($key, $value);
228: } else {
229: parent::__set($key, $value);
230: }
231: }
232:
233: public function getEntityIdAttribute()
234: {
235: return $this->Id;
236: }
237:
238: /**
239: * Dynamically retrieve attributes on the model.
240: *
241: * @param string $key
242: * @return mixed
243: */
244: public function __get($key)
245: {
246: $value = parent::__get($key);
247: if ($key !== 'Properties') {
248: if (isset($this->Properties[$key])) {
249: $value = $this->Properties[$key];
250: }
251: if ($value === null && $this->isInheritedAttribute($key)) {
252: $value = $this->getInheritedValue($key);
253: }
254: }
255: return $value;
256: }
257:
258: /**
259: * Determine if an attribute or relation exists on the model.
260: *
261: * @param string $key
262: * @return bool
263: */
264: public function __isset($key)
265: {
266: $value = parent::__isset($key);
267: if (!$value) {
268: $value = isset($this->Properties[$key]);
269: }
270:
271: return $value;
272: }
273:
274: /**
275: * Unset an attribute on the model.
276: *
277: * @param string $key
278: * @return void
279: */
280: public function __unset($key)
281: {
282: parent::__unset($key);
283: if (isset($this->Properties[$key])) {
284: unset($this->Properties[$key]);
285: }
286: }
287:
288: public function getExtendedProp($key, $default = null)
289: {
290: $mResult = null;
291: if (isset($this->Properties[$key])) {
292: $mResult = $this->Properties[$key];
293: } else {
294: $mResult = ($this->isInheritedAttribute($key)) ? $this->getInheritedValue($key) : $default;
295: }
296:
297: return $mResult;
298: }
299:
300: public function getExtendedProps()
301: {
302: return $this->Properties;
303: }
304:
305: public function setExtendedProp($key, $value)
306: {
307: $properties = $this->Properties;
308: $properties[$key] = $value;
309: $this->Properties = $properties;
310: }
311:
312: public function unsetExtendedProp($key)
313: {
314: $properties = $this->Properties;
315: if (isset($properties[$key])) {
316: unset($properties[$key]);
317: }
318: $this->Properties = $properties;
319: }
320:
321: public function setExtendedProps($props)
322: {
323: $properties = is_array($this->Properties) ? $this->Properties : [];
324: $this->Properties = array_merge($properties, $props);
325: }
326:
327: /**
328: * @return BelongsTo
329: */
330: public function parent()
331: {
332: $result = null;
333: if (isset($this->parentType) && is_subclass_of($this->parentType, \Aurora\System\Classes\Model::class)) {
334: $result = $this->belongsTo($this->parentType, $this->parentKey, $this->primaryKey);
335: }
336:
337: return $result;
338: }
339:
340: protected function isEncryptAttribute($attributeName)
341: {
342: $result = false;
343: $casts = $this->getCasts();
344: if (isset($casts[$attributeName]) && $casts[$attributeName] === \Aurora\System\Casts\Encrypt::class) {
345: $result = true;
346: }
347:
348: return $result;
349: }
350:
351: /**
352: * Convert the model instance to an array that can be sent with the response payload.
353: *
354: * @return array
355: */
356: public function toResponseArray()
357: {
358: $array = $this->attributesToArray();
359:
360: $relations = $this->getRelations();
361: if (count($relations) > 0) {
362: foreach ($relations as $key => $attribute) {
363: $array[$key] = $attribute->toResponseArray();
364: }
365: }
366:
367: $parentInheritedAttributes = $this->getInheritedAttributes();
368: if (count($parentInheritedAttributes) > 0) {
369: foreach ($parentInheritedAttributes as $attribute) {
370: $value = null;
371: if (isset($array[$attribute])) {
372: $value = $array[$attribute];
373: }
374: if ($value === null) {
375: $array[$attribute] = $this->getInheritedValue($attribute);
376: }
377: }
378: }
379:
380: if (isset($array['Properties'])) {
381: foreach ($array['Properties'] as $key => $value) {
382: if ($value !== null) {
383: $aArgs = ['Model' => $this, 'PropertyName' => $key];
384: EventEmitter::getInstance()->emit('System', 'CastExtendedProp', $aArgs, $value);
385: $array[$key] = $value;
386: }
387: }
388: unset($array['Properties']);
389: }
390:
391: foreach ($array as $key => $value) {
392: if ($this->isEncryptAttribute($key)) {
393: $array[$key] = '*****';
394: }
395: }
396:
397: if (!isset($array['UUID'])) {
398: $array['UUID'] = '';
399: }
400: $array['ParentUUID'] = '';
401: $array['ModuleName'] = $this->moduleName;
402:
403: return $array;
404: }
405:
406: public function validate()
407: {
408: Validator::validate($this->getAttributes(), $this->validationRules, $this->validationMessages);
409:
410: return true;
411: }
412:
413: /**
414: * Returns a pseudo-random v4 UUID
415: *
416: * This function is based on a comment by Andrew Moore on php.net
417: *
418: * @see http://www.php.net/manual/en/function.uniqid.php#94959
419: * @return string
420: */
421: public function generateUUID()
422: {
423: return sprintf(
424: '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
425:
426: // 32 bits for "time_low"
427: mt_rand(0, 0xffff),
428: mt_rand(0, 0xffff),
429:
430: // 16 bits for "time_mid"
431: mt_rand(0, 0xffff),
432:
433: // 16 bits for "time_hi_and_version",
434: // four most significant bits holds version number 4
435: mt_rand(0, 0x0fff) | 0x4000,
436:
437: // 16 bits, 8 bits for "clk_seq_hi_res",
438: // 8 bits for "clk_seq_low",
439: // two most significant bits holds zero and one for variant DCE1.1
440: mt_rand(0, 0x3fff) | 0x8000,
441:
442: // 48 bits for "node"
443: mt_rand(0, 0xffff),
444: mt_rand(0, 0xffff),
445: mt_rand(0, 0xffff)
446: );
447: }
448:
449: /**
450: * Checks if a string is a valid UUID.
451: *
452: * @param string $uuid
453: * @return bool
454: */
455: public function validateUUID($uuid)
456: {
457: return preg_match(
458: '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i',
459: $uuid
460: ) == true;
461: }
462:
463: /**
464: *
465: * @param array $aProperties
466: */
467: public function populate($aProperties)
468: {
469: foreach ($aProperties as $key => $value) {
470: if (in_array($key, $this->fillable) || strpos($key, '::') !== false) {
471: $this->$key = $value;
472: }
473: }
474: }
475:
476: /**
477: *
478: * @return string
479: */
480: public function getName()
481: {
482: return \get_class($this);
483: }
484:
485: /**
486: * Save the model to the database.
487: *
488: * @param array $options
489: * @return bool
490: */
491: public function save(array $options = [])
492: {
493: if ($this->validate()) {
494: return parent::save($options);
495: }
496:
497: return false;
498: }
499: }
500: