blob: 3bad2ea39c6b11c43a6e83e19432347b462eb6f2 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace Adldap\Models;
4
5use DateTime;
6use ArrayAccess;
7use Adldap\Utilities;
8use JsonSerializable;
9use Adldap\Query\Builder;
10use Illuminate\Support\Arr;
11use Adldap\Query\Collection;
12use InvalidArgumentException;
13use UnexpectedValueException;
14use Adldap\Models\Attributes\Sid;
15use Adldap\Models\Attributes\Guid;
16use Adldap\Schemas\SchemaInterface;
17use Adldap\Models\Attributes\MbString;
18use Adldap\Connections\ConnectionException;
19use Adldap\Models\Attributes\DistinguishedName;
20
21/**
22 * Class Model.
23 *
24 * Represents an LDAP record and provides the ability
25 * to modify / retrieve data from the record.
26 */
27abstract class Model implements ArrayAccess, JsonSerializable
28{
29 use Concerns\HasEvents;
30 use Concerns\HasAttributes;
31
32 /**
33 * Indicates if the model exists.
34 *
35 * @var bool
36 */
37 public $exists = false;
38
39 /**
40 * The current query builder instance.
41 *
42 * @var Builder
43 */
44 protected $query;
45
46 /**
47 * The current LDAP attribute schema.
48 *
49 * @var SchemaInterface
50 */
51 protected $schema;
52
53 /**
54 * Contains the models modifications.
55 *
56 * @var array
57 */
58 protected $modifications = [];
59
60 /**
61 * Constructor.
62 *
63 * @param array $attributes
64 * @param Builder $builder
65 */
66 public function __construct(array $attributes, Builder $builder)
67 {
68 $this->setQuery($builder)
69 ->setSchema($builder->getSchema())
70 ->fill($attributes);
71 }
72
73 /**
74 * Returns the models distinguished name when the model is converted to a string.
75 *
76 * @return null|string
77 */
78 public function __toString()
79 {
80 return $this->getDn();
81 }
82
83 /**
84 * Sets the current query builder.
85 *
86 * @param Builder $builder
87 *
88 * @return $this
89 */
90 public function setQuery(Builder $builder)
91 {
92 $this->query = $builder;
93
94 return $this;
95 }
96
97 /**
98 * Returns the current query builder.
99 *
100 * @return Builder
101 */
102 public function getQuery()
103 {
104 return $this->query;
105 }
106
107 /**
108 * Returns a new query builder instance.
109 *
110 * @return Builder
111 */
112 public function newQuery()
113 {
114 return $this->query->newInstance();
115 }
116
117 /**
118 * Returns a new batch modification.
119 *
120 * @param string|null $attribute
121 * @param string|int|null $type
122 * @param array $values
123 *
124 * @return BatchModification
125 */
126 public function newBatchModification($attribute = null, $type = null, $values = [])
127 {
128 return new BatchModification($attribute, $type, $values);
129 }
130
131 /**
132 * Returns a new collection with the specified items.
133 *
134 * @param mixed $items
135 *
136 * @return Collection
137 */
138 public function newCollection($items = [])
139 {
140 return new Collection($items);
141 }
142
143 /**
144 * Sets the current model schema.
145 *
146 * @param SchemaInterface $schema
147 *
148 * @return $this
149 */
150 public function setSchema(SchemaInterface $schema)
151 {
152 $this->schema = $schema;
153
154 return $this;
155 }
156
157 /**
158 * Returns the current model schema.
159 *
160 * @return SchemaInterface
161 */
162 public function getSchema()
163 {
164 return $this->schema;
165 }
166
167 /**
168 * Determine if the given offset exists.
169 *
170 * @param string $offset
171 *
172 * @return bool
173 */
174 public function offsetExists($offset)
175 {
176 return !is_null($this->getAttribute($offset));
177 }
178
179 /**
180 * Get the value for a given offset.
181 *
182 * @param string $offset
183 *
184 * @return mixed
185 */
186 public function offsetGet($offset)
187 {
188 return $this->getAttribute($offset);
189 }
190
191 /**
192 * Set the value at the given offset.
193 *
194 * @param string $offset
195 * @param mixed $value
196 *
197 * @return void
198 */
199 public function offsetSet($offset, $value)
200 {
201 $this->setAttribute($offset, $value);
202 }
203
204 /**
205 * Unset the value at the given offset.
206 *
207 * @param string $offset
208 *
209 * @return void
210 */
211 public function offsetUnset($offset)
212 {
213 unset($this->attributes[$offset]);
214 }
215
216 /**
217 * Determine if an attribute exists on the model.
218 *
219 * @param string $key
220 *
221 * @return bool
222 */
223 public function __isset($key)
224 {
225 return $this->offsetExists($key);
226 }
227
228 /**
229 * Convert the object into something JSON serializable.
230 *
231 * @return array
232 */
233 public function jsonSerialize()
234 {
235 $attributes = $this->getAttributes();
236
237 array_walk_recursive($attributes, function (&$val) {
238 if (MbString::isLoaded()) {
239 // If we're able to detect the attribute
240 // encoding, we'll encode only the
241 // attributes that need to be.
242 if (!MbString::isUtf8($val)) {
243 $val = utf8_encode($val);
244 }
245 } else {
246 // If the mbstring extension is not loaded, we'll
247 // encode all attributes to make sure
248 // they are encoded properly.
249 $val = utf8_encode($val);
250 }
251 });
252
253 // We'll replace the binary GUID and SID with
254 // their string equivalents for convenience.
255 return array_replace($attributes, [
256 $this->schema->objectGuid() => $this->getConvertedGuid(),
257 $this->schema->objectSid() => $this->getConvertedSid(),
258 ]);
259 }
260
261 /**
262 * Reload a fresh model instance from the directory.
263 *
264 * @return static|null
265 */
266 public function fresh()
267 {
268 $model = $this->query->newInstance()->findByDn($this->getDn());
269
270 return $model instanceof self ? $model : null;
271 }
272
273 /**
274 * Synchronizes the current models attributes with the directory values.
275 *
276 * @return bool
277 */
278 public function syncRaw()
279 {
280 if ($model = $this->fresh()) {
281 $this->setRawAttributes($model->getAttributes());
282
283 return true;
284 }
285
286 return false;
287 }
288
289 /**
290 * Returns the models batch modifications to be processed.
291 *
292 * @return array
293 */
294 public function getModifications()
295 {
296 $this->buildModificationsFromDirty();
297
298 return $this->modifications;
299 }
300
301 /**
302 * Sets the models modifications array.
303 *
304 * @param array $modifications
305 *
306 * @return $this
307 */
308 public function setModifications(array $modifications = [])
309 {
310 $this->modifications = $modifications;
311
312 return $this;
313 }
314
315 /**
316 * Adds a batch modification to the models modifications array.
317 *
318 * @param array|BatchModification $mod
319 *
320 * @throws InvalidArgumentException
321 *
322 * @return $this
323 */
324 public function addModification($mod = [])
325 {
326 if ($mod instanceof BatchModification) {
327 $mod = $mod->get();
328 }
329
330 if ($this->isValidModification($mod)) {
331 $this->modifications[] = $mod;
332
333 return $this;
334 }
335
336 throw new InvalidArgumentException(
337 "The batch modification array does not include the mandatory 'attrib' or 'modtype' keys."
338 );
339 }
340
341 /**
342 * Returns the model's distinguished name string.
343 *
344 * @link https://msdn.microsoft.com/en-us/library/aa366101(v=vs.85).aspx
345 *
346 * @return string|null
347 */
348 public function getDistinguishedName()
349 {
350 return $this->getFirstAttribute($this->schema->distinguishedName());
351 }
352
353 /**
354 * Sets the model's distinguished name attribute.
355 *
356 * @param string|DistinguishedName $dn
357 *
358 * @return $this
359 */
360 public function setDistinguishedName($dn)
361 {
362 $this->setFirstAttribute($this->schema->distinguishedName(), (string) $dn);
363
364 return $this;
365 }
366
367 /**
368 * Returns the model's distinguished name string.
369 *
370 * (Alias for getDistinguishedName())
371 *
372 * @link https://msdn.microsoft.com/en-us/library/aa366101(v=vs.85).aspx
373 *
374 * @return string|null
375 */
376 public function getDn()
377 {
378 return $this->getDistinguishedName();
379 }
380
381 /**
382 * Returns a DistinguishedName object for modifying the current models DN.
383 *
384 * @return DistinguishedName
385 */
386 public function getDnBuilder()
387 {
388 // If we currently don't have a distinguished name, we'll set
389 // it to our base, otherwise we'll use our query's base DN.
390 $dn = $this->getDistinguishedName() ?: $this->query->getDn();
391
392 return $this->getNewDnBuilder($dn);
393 }
394
395 /**
396 * Returns the models distinguished name components.
397 *
398 * @param bool $removeAttributePrefixes
399 *
400 * @return array
401 */
402 public function getDnComponents($removeAttributePrefixes = true)
403 {
404 if ($components = Utilities::explodeDn($this->getDn(), $removeAttributePrefixes)) {
405 unset($components['count']);
406
407 return $components;
408 }
409
410 return [];
411 }
412
413 /**
414 * Returns the distinguished name that the model is a leaf of.
415 *
416 * @return string
417 */
418 public function getDnRoot()
419 {
420 $components = $this->getDnComponents(false);
421
422 // Shift off the beginning of the array;
423 // This contains the models RDN.
424 array_shift($components);
425
426 return implode(',', $components);
427 }
428
429 /**
430 * Returns a new DistinguishedName object for building onto.
431 *
432 * @param string $baseDn
433 *
434 * @return DistinguishedName
435 */
436 public function getNewDnBuilder($baseDn = '')
437 {
438 return new DistinguishedName($baseDn);
439 }
440
441 /**
442 * Sets the model's distinguished name attribute.
443 *
444 * (Alias for setDistinguishedName())
445 *
446 * @param string $dn
447 *
448 * @return $this
449 */
450 public function setDn($dn)
451 {
452 return $this->setDistinguishedName($dn);
453 }
454
455 /**
456 * Returns the model's hex object SID.
457 *
458 * @link https://msdn.microsoft.com/en-us/library/ms679024(v=vs.85).aspx
459 *
460 * @return string
461 */
462 public function getObjectSid()
463 {
464 return $this->getFirstAttribute($this->schema->objectSid());
465 }
466
467 /**
468 * Returns the model's binary object GUID.
469 *
470 * @link https://msdn.microsoft.com/en-us/library/ms679021(v=vs.85).aspx
471 *
472 * @return string
473 */
474 public function getObjectGuid()
475 {
476 return $this->getFirstAttribute($this->schema->objectGuid());
477 }
478
479 /**
480 * Returns the model's GUID.
481 *
482 * @return string|null
483 */
484 public function getConvertedGuid()
485 {
486 try {
487 return (string) new Guid($this->getObjectGuid());
488 } catch (InvalidArgumentException $e) {
489 return;
490 }
491 }
492
493 /**
494 * Returns the model's SID.
495 *
496 * @return string|null
497 */
498 public function getConvertedSid()
499 {
500 try {
501 return (string) new Sid($this->getObjectSid());
502 } catch (InvalidArgumentException $e) {
503 return;
504 }
505 }
506
507 /**
508 * Returns the model's common name.
509 *
510 * @link https://msdn.microsoft.com/en-us/library/ms675449(v=vs.85).aspx
511 *
512 * @return string
513 */
514 public function getCommonName()
515 {
516 return $this->getFirstAttribute($this->schema->commonName());
517 }
518
519 /**
520 * Sets the model's common name.
521 *
522 * @param string $name
523 *
524 * @return $this
525 */
526 public function setCommonName($name)
527 {
528 return $this->setFirstAttribute($this->schema->commonName(), $name);
529 }
530
531 /**
532 * Returns the model's name. An LDAP alias for the CN attribute.
533 *
534 * @link https://msdn.microsoft.com/en-us/library/ms675449(v=vs.85).aspx
535 *
536 * @return string
537 */
538 public function getName()
539 {
540 return $this->getFirstAttribute($this->schema->name());
541 }
542
543 /**
544 * Sets the model's name.
545 *
546 * @param string $name
547 *
548 * @return Model
549 */
550 public function setName($name)
551 {
552 return $this->setFirstAttribute($this->schema->name(), $name);
553 }
554
555 /**
556 * Returns the model's display name.
557 *
558 * @return string
559 */
560 public function getDisplayName()
561 {
562 return $this->getFirstAttribute($this->schema->displayName());
563 }
564
565 /**
566 * Sets the model's display name.
567 *
568 * @param string $displayName
569 *
570 * @return $this
571 */
572 public function setDisplayName($displayName)
573 {
574 return $this->setFirstAttribute($this->schema->displayName(), $displayName);
575 }
576
577 /**
578 * Returns the model's samaccountname.
579 *
580 * @link https://msdn.microsoft.com/en-us/library/ms679635(v=vs.85).aspx
581 *
582 * @return string
583 */
584 public function getAccountName()
585 {
586 return $this->getFirstAttribute($this->schema->accountName());
587 }
588
589 /**
590 * Sets the model's samaccountname.
591 *
592 * @param string $accountName
593 *
594 * @return Model
595 */
596 public function setAccountName($accountName)
597 {
598 return $this->setFirstAttribute($this->schema->accountName(), $accountName);
599 }
600
601 /**
602 * Returns the model's userPrincipalName.
603 *
604 * @link https://docs.microsoft.com/en-us/windows/win32/adschema/a-userprincipalname
605 *
606 * @return string
607 */
608 public function getUserPrincipalName()
609 {
610 return $this->getFirstAttribute($this->schema->userPrincipalName());
611 }
612
613 /**
614 * Sets the model's userPrincipalName.
615 *
616 * @param string $upn
617 *
618 * @return Model
619 */
620 public function setUserPrincipalName($upn)
621 {
622 return $this->setFirstAttribute($this->schema->userPrincipalName(), $upn);
623 }
624
625 /**
626 * Returns the model's samaccounttype.
627 *
628 * @link https://msdn.microsoft.com/en-us/library/ms679637(v=vs.85).aspx
629 *
630 * @return string
631 */
632 public function getAccountType()
633 {
634 return $this->getFirstAttribute($this->schema->accountType());
635 }
636
637 /**
638 * Returns the model's `whenCreated` time.
639 *
640 * @link https://msdn.microsoft.com/en-us/library/ms680924(v=vs.85).aspx
641 *
642 * @return string
643 */
644 public function getCreatedAt()
645 {
646 return $this->getFirstAttribute($this->schema->createdAt());
647 }
648
649 /**
650 * Returns the created at time in a mysql formatted date.
651 *
652 * @return string
653 */
654 public function getCreatedAtDate()
655 {
656 return (new DateTime())->setTimestamp($this->getCreatedAtTimestamp())->format($this->dateFormat);
657 }
658
659 /**
660 * Returns the created at time in a unix timestamp format.
661 *
662 * @return float
663 */
664 public function getCreatedAtTimestamp()
665 {
666 return DateTime::createFromFormat($this->timestampFormat, $this->getCreatedAt())->getTimestamp();
667 }
668
669 /**
670 * Returns the model's `whenChanged` time.
671 *
672 * @link https://msdn.microsoft.com/en-us/library/ms680921(v=vs.85).aspx
673 *
674 * @return string
675 */
676 public function getUpdatedAt()
677 {
678 return $this->getFirstAttribute($this->schema->updatedAt());
679 }
680
681 /**
682 * Returns the updated at time in a mysql formatted date.
683 *
684 * @return string
685 */
686 public function getUpdatedAtDate()
687 {
688 return (new DateTime())->setTimestamp($this->getUpdatedAtTimestamp())->format($this->dateFormat);
689 }
690
691 /**
692 * Returns the updated at time in a unix timestamp format.
693 *
694 * @return float
695 */
696 public function getUpdatedAtTimestamp()
697 {
698 return DateTime::createFromFormat($this->timestampFormat, $this->getUpdatedAt())->getTimestamp();
699 }
700
701 /**
702 * Returns the Container of the current Model.
703 *
704 * @link https://msdn.microsoft.com/en-us/library/ms679012(v=vs.85).aspx
705 *
706 * @return Container|Entry|bool
707 */
708 public function getObjectClass()
709 {
710 return $this->query->findByDn($this->getObjectCategoryDn());
711 }
712
713 /**
714 * Returns the CN of the model's object category.
715 *
716 * @return null|string
717 */
718 public function getObjectCategory()
719 {
720 $category = $this->getObjectCategoryArray();
721
722 if (is_array($category) && array_key_exists(0, $category)) {
723 return $category[0];
724 }
725 }
726
727 /**
728 * Returns the model's object category DN in an exploded array.
729 *
730 * @return array|false
731 */
732 public function getObjectCategoryArray()
733 {
734 return Utilities::explodeDn($this->getObjectCategoryDn());
735 }
736
737 /**
738 * Returns the model's object category DN string.
739 *
740 * @return null|string
741 */
742 public function getObjectCategoryDn()
743 {
744 return $this->getFirstAttribute($this->schema->objectCategory());
745 }
746
747 /**
748 * Returns the model's primary group ID.
749 *
750 * @link https://msdn.microsoft.com/en-us/library/ms679375(v=vs.85).aspx
751 *
752 * @return string
753 */
754 public function getPrimaryGroupId()
755 {
756 return $this->getFirstAttribute($this->schema->primaryGroupId());
757 }
758
759 /**
760 * Returns the model's instance type.
761 *
762 * @link https://msdn.microsoft.com/en-us/library/ms676204(v=vs.85).aspx
763 *
764 * @return int
765 */
766 public function getInstanceType()
767 {
768 return $this->getFirstAttribute($this->schema->instanceType());
769 }
770
771 /**
772 * Returns the distinguished name of the user who is assigned to manage this object.
773 *
774 * @return string|null
775 */
776 public function getManagedBy()
777 {
778 return $this->getFirstAttribute($this->schema->managedBy());
779 }
780
781 /**
782 * Returns the user model of the user who is assigned to manage this object.
783 *
784 * Returns false otherwise.
785 *
786 * @return User|bool
787 */
788 public function getManagedByUser()
789 {
790 if ($dn = $this->getManagedBy()) {
791 return $this->query->newInstance()->findByDn($dn);
792 }
793
794 return false;
795 }
796
797 /**
798 * Sets the user who is assigned to managed this object.
799 *
800 * @param Model|string $dn
801 *
802 * @return $this
803 */
804 public function setManagedBy($dn)
805 {
806 if ($dn instanceof self) {
807 $dn = $dn->getDn();
808 }
809
810 return $this->setFirstAttribute($this->schema->managedBy(), $dn);
811 }
812
813 /**
814 * Returns the model's max password age.
815 *
816 * @return string
817 */
818 public function getMaxPasswordAge()
819 {
820 return $this->getFirstAttribute($this->schema->maxPasswordAge());
821 }
822
823 /**
824 * Returns the model's max password age in days.
825 *
826 * @return int
827 */
828 public function getMaxPasswordAgeDays()
829 {
830 $age = $this->getMaxPasswordAge();
831
832 return (int) (abs($age) / 10000000 / 60 / 60 / 24);
833 }
834
835 /**
836 * Determine if the current model is located inside the given OU.
837 *
838 * If a model instance is given, the strict parameter is ignored.
839 *
840 * @param Model|string $ou The organizational unit to check.
841 * @param bool $strict Whether the check is case-sensitive.
842 *
843 * @return bool
844 */
845 public function inOu($ou, $strict = false)
846 {
847 if ($ou instanceof self) {
848 // If we've been given an OU model, we can
849 // just check if the OU's DN is inside
850 // the current models DN.
851 return (bool) strpos($this->getDn(), $ou->getDn());
852 }
853
854 $suffix = $strict ? '' : 'i';
855
856 return (bool) preg_grep("/{$ou}/{$suffix}", $this->getDnBuilder()->getComponents('ou'));
857 }
858
859 /**
860 * Returns true / false if the current model is writable
861 * by checking its instance type integer.
862 *
863 * @return bool
864 */
865 public function isWritable()
866 {
867 return (int) $this->getInstanceType() === 4;
868 }
869
870 /**
871 * Saves the changes to LDAP and returns the results.
872 *
873 * @param array $attributes The attributes to update or create for the current entry.
874 *
875 * @return bool
876 */
877 public function save(array $attributes = [])
878 {
879 $this->fireModelEvent(new Events\Saving($this));
880
881 $saved = $this->exists ? $this->update($attributes) : $this->create($attributes);
882
883 if ($saved) {
884 $this->fireModelEvent(new Events\Saved($this));
885 }
886
887 return $saved;
888 }
889
890 /**
891 * Updates the model.
892 *
893 * @param array $attributes The attributes to update for the current entry.
894 *
895 * @return bool
896 */
897 public function update(array $attributes = [])
898 {
899 $this->fill($attributes);
900
901 $modifications = $this->getModifications();
902
903 if (count($modifications) > 0) {
904 $this->fireModelEvent(new Events\Updating($this));
905
906 // Push the update.
907 if ($this->query->getConnection()->modifyBatch($this->getDn(), $modifications)) {
908 // Re-sync attributes.
909 $this->syncRaw();
910
911 $this->fireModelEvent(new Events\Updated($this));
912
913 // Re-set the models modifications.
914 $this->modifications = [];
915
916 return true;
917 }
918
919 // Modification failed, return false.
920 return false;
921 }
922
923 // We need to return true here because modify batch will
924 // return false if no modifications are made
925 // but this may not always be the case.
926 return true;
927 }
928
929 /**
930 * Creates the entry in LDAP.
931 *
932 * @param array $attributes The attributes for the new entry.
933 *
934 * @throws UnexpectedValueException
935 *
936 * @return bool
937 */
938 public function create(array $attributes = [])
939 {
940 $this->fill($attributes);
941
942 if (empty($this->getDn())) {
943 // If the model doesn't currently have a distinguished
944 // name set, we'll create one automatically using
945 // the current query builders base DN.
946 $dn = $this->getCreatableDn();
947
948 // If the dn we receive is the same as our queries base DN, we need
949 // to throw an exception. The LDAP object must have a valid RDN.
950 if ($dn->get() == $this->query->getDn()) {
951 throw new UnexpectedValueException("An LDAP object must have a valid RDN to be created. '$dn' given.");
952 }
953
954 $this->setDn($dn);
955 }
956
957 $this->fireModelEvent(new Events\Creating($this));
958
959 // Create the entry.
960 $created = $this->query->getConnection()->add($this->getDn(), $this->getCreatableAttributes());
961
962 if ($created) {
963 // If the entry was created we'll re-sync
964 // the models attributes from the server.
965 $this->syncRaw();
966
967 $this->fireModelEvent(new Events\Created($this));
968
969 return true;
970 }
971
972 return false;
973 }
974
975 /**
976 * Creates an attribute on the current model.
977 *
978 * @param string $attribute The attribute to create
979 * @param mixed $value The value of the new attribute
980 * @param bool $sync Whether to re-sync all attributes
981 *
982 * @return bool
983 */
984 public function createAttribute($attribute, $value, $sync = true)
985 {
986 if (
987 $this->exists &&
988 $this->query->getConnection()->modAdd($this->getDn(), [$attribute => $value])
989 ) {
990 if ($sync) {
991 $this->syncRaw();
992 }
993
994 return true;
995 }
996
997 return false;
998 }
999
1000 /**
1001 * Updates the specified attribute with the specified value.
1002 *
1003 * @param string $attribute The attribute to modify
1004 * @param mixed $value The new value for the attribute
1005 * @param bool $sync Whether to re-sync all attributes
1006 *
1007 * @return bool
1008 */
1009 public function updateAttribute($attribute, $value, $sync = true)
1010 {
1011 if (
1012 $this->exists &&
1013 $this->query->getConnection()->modReplace($this->getDn(), [$attribute => $value])
1014 ) {
1015 if ($sync) {
1016 $this->syncRaw();
1017 }
1018
1019 return true;
1020 }
1021
1022 return false;
1023 }
1024
1025 /**
1026 * Deletes an attribute on the current entry.
1027 *
1028 * @param string|array $attributes The attribute(s) to delete
1029 * @param bool $sync Whether to re-sync all attributes
1030 *
1031 * Delete specific values in attributes:
1032 *
1033 * ["memberuid" => "username"]
1034 *
1035 * Delete an entire attribute:
1036 *
1037 * ["memberuid" => []]
1038 *
1039 * @return bool
1040 */
1041 public function deleteAttribute($attributes, $sync = true)
1042 {
1043 // If we've been given a string, we'll assume we're removing a
1044 // single attribute. Otherwise, we'll assume it's
1045 // an array of attributes to remove.
1046 $attributes = is_string($attributes) ? [$attributes => []] : $attributes;
1047
1048 if (
1049 $this->exists &&
1050 $this->query->getConnection()->modDelete($this->getDn(), $attributes)
1051 ) {
1052 if ($sync) {
1053 $this->syncRaw();
1054 }
1055
1056 return true;
1057 }
1058
1059 return false;
1060 }
1061
1062 /**
1063 * Deletes the current entry.
1064 *
1065 * Throws a ModelNotFoundException if the current model does
1066 * not exist or does not contain a distinguished name.
1067 *
1068 * @param bool $recursive Whether to recursively delete leaf nodes (models that are children).
1069 *
1070 * @throws ModelDoesNotExistException
1071 *
1072 * @return bool
1073 */
1074 public function delete($recursive = false)
1075 {
1076 $dn = $this->getDn();
1077
1078 if ($this->exists === false || empty($dn)) {
1079 // Make sure the record exists before we can delete it.
1080 // Otherwise, we'll throw an exception.
1081 throw (new ModelDoesNotExistException())->setModel(get_class($this));
1082 }
1083
1084 $this->fireModelEvent(new Events\Deleting($this));
1085
1086 if ($recursive) {
1087 // If recursive is requested, we'll retrieve all direct leaf nodes
1088 // by executing a 'listing' and delete each resulting model.
1089 $this->newQuery()->listing()->in($this->getDn())->get()->each(function (self $model) use ($recursive) {
1090 $model->delete($recursive);
1091 });
1092 }
1093
1094 if ($this->query->getConnection()->delete($dn)) {
1095 // If the deletion was successful, we'll mark the model
1096 // as non-existing and fire the deleted event.
1097 $this->exists = false;
1098
1099 $this->fireModelEvent(new Events\Deleted($this));
1100
1101 return true;
1102 }
1103
1104 return false;
1105 }
1106
1107 /**
1108 * Moves the current model into the given new parent.
1109 *
1110 * For example: $user->move($ou);
1111 *
1112 * @param Model|string $newParentDn The new parent of the current model.
1113 * @param bool $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved.
1114 *
1115 * @return bool
1116 */
1117 public function move($newParentDn, $deleteOldRdn = true)
1118 {
1119 // First we'll explode the current models distinguished name and keep their attributes prefixes.
1120 $parts = Utilities::explodeDn($this->getDn(), $removeAttrPrefixes = false);
1121
1122 // If the current model has an empty RDN, we can't move it.
1123 if ((int) Arr::first($parts) === 0) {
1124 throw new UnexpectedValueException('Current model does not contain an RDN to move.');
1125 }
1126
1127 // Looks like we have a DN. We'll retrieve the leftmost RDN (the identifier).
1128 $rdn = Arr::get($parts, 0);
1129
1130 return $this->rename($rdn, $newParentDn, $deleteOldRdn);
1131 }
1132
1133 /**
1134 * Renames the current model to a new RDN and new parent.
1135 *
1136 * @param string $rdn The models new relative distinguished name. Example: "cn=JohnDoe"
1137 * @param Model|string|null $newParentDn The models new parent distinguished name (if moving). Leave this null if you are only renaming. Example: "ou=MovedUsers,dc=acme,dc=org"
1138 * @param bool|true $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved.
1139 *
1140 * @return bool
1141 */
1142 public function rename($rdn, $newParentDn = null, $deleteOldRdn = true)
1143 {
1144 if ($newParentDn instanceof self) {
1145 $newParentDn = $newParentDn->getDn();
1146 }
1147
1148 $moved = $this->query->getConnection()->rename($this->getDn(), $rdn, $newParentDn, $deleteOldRdn);
1149
1150 if ($moved) {
1151 // If the model was successfully moved, we'll set its
1152 // new DN so we can sync it's attributes properly.
1153 $this->setDn("{$rdn},{$newParentDn}");
1154
1155 $this->syncRaw();
1156
1157 return true;
1158 }
1159
1160 return false;
1161 }
1162
1163 /**
1164 * Constructs a new distinguished name that is creatable in the directory.
1165 *
1166 * @return DistinguishedName|string
1167 */
1168 protected function getCreatableDn()
1169 {
1170 return $this->getDnBuilder()->addCn($this->getCommonName());
1171 }
1172
1173 /**
1174 * Returns the models creatable attributes.
1175 *
1176 * @return mixed
1177 */
1178 protected function getCreatableAttributes()
1179 {
1180 return Arr::except($this->getAttributes(), [$this->schema->distinguishedName()]);
1181 }
1182
1183 /**
1184 * Determines if the given modification is valid.
1185 *
1186 * @param mixed $mod
1187 *
1188 * @return bool
1189 */
1190 protected function isValidModification($mod)
1191 {
1192 return is_array($mod) &&
1193 array_key_exists(BatchModification::KEY_MODTYPE, $mod) &&
1194 array_key_exists(BatchModification::KEY_ATTRIB, $mod);
1195 }
1196
1197 /**
1198 * Builds the models modifications from its dirty attributes.
1199 *
1200 * @return array
1201 */
1202 protected function buildModificationsFromDirty()
1203 {
1204 foreach ($this->getDirty() as $attribute => $values) {
1205 // Make sure values is always an array.
1206 $values = (is_array($values) ? $values : [$values]);
1207
1208 // Create a new modification.
1209 $modification = $this->newBatchModification($attribute, null, $values);
1210
1211 if (array_key_exists($attribute, $this->original)) {
1212 // If the attribute we're modifying has an original value, we'll give the
1213 // BatchModification object its values to automatically determine
1214 // which type of LDAP operation we need to perform.
1215 $modification->setOriginal($this->original[$attribute]);
1216 }
1217
1218 // Build the modification from its
1219 // possible original values.
1220 $modification->build();
1221
1222 if ($modification->isValid()) {
1223 // Finally, we'll add the modification to the model.
1224 $this->addModification($modification);
1225 }
1226 }
1227
1228 return $this->modifications;
1229 }
1230
1231 /**
1232 * Validates that the current LDAP connection is secure.
1233 *
1234 * @throws ConnectionException
1235 *
1236 * @return void
1237 */
1238 protected function validateSecureConnection()
1239 {
1240 if (!$this->query->getConnection()->canChangePasswords()) {
1241 throw new ConnectionException(
1242 'You must be connected to your LDAP server with TLS or SSL to perform this operation.'
1243 );
1244 }
1245 }
1246
1247 /**
1248 * Converts the inserted string boolean to a PHP boolean.
1249 *
1250 * @param string $bool
1251 *
1252 * @return null|bool
1253 */
1254 protected function convertStringToBool($bool)
1255 {
1256 $bool = strtoupper($bool);
1257
1258 if ($bool === strtoupper($this->schema->false())) {
1259 return false;
1260 } elseif ($bool === strtoupper($this->schema->true())) {
1261 return true;
1262 } else {
1263 return;
1264 }
1265 }
1266}