1<?php 2 3declare(strict_types=1); 4 5 6/** 7 * Circles - Bring cloud-users closer together. 8 * 9 * This file is licensed under the Affero General Public License version 3 or 10 * later. See the COPYING file. 11 * 12 * @author Maxence Lange <maxence@artificial-owl.com> 13 * @copyright 2021 14 * @license GNU AGPL version 3 or any later version 15 * 16 * This program is free software: you can redistribute it and/or modify 17 * it under the terms of the GNU Affero General Public License as 18 * published by the Free Software Foundation, either version 3 of the 19 * License, or (at your option) any later version. 20 * 21 * This program is distributed in the hope that it will be useful, 22 * but WITHOUT ANY WARRANTY; without even the implied warranty of 23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 * GNU Affero General Public License for more details. 25 * 26 * You should have received a copy of the GNU Affero General Public License 27 * along with this program. If not, see <http://www.gnu.org/licenses/>. 28 * 29 */ 30 31 32namespace OCA\Circles\Model; 33 34use ArtificialOwl\MySmallPhpTools\Db\Nextcloud\nc22\INC22QueryRow; 35use ArtificialOwl\MySmallPhpTools\Exceptions\InvalidItemException; 36use ArtificialOwl\MySmallPhpTools\IDeserializable; 37use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22Deserialize; 38use ArtificialOwl\MySmallPhpTools\Traits\TArrayTools; 39use DateTime; 40use JsonSerializable; 41use OCA\Circles\AppInfo\Capabilities; 42use OCA\Circles\Exceptions\MemberNotFoundException; 43use OCA\Circles\Exceptions\MembershipNotFoundException; 44use OCA\Circles\Exceptions\ParseMemberLevelException; 45use OCA\Circles\Exceptions\RequestBuilderException; 46use OCA\Circles\Exceptions\UnknownInterfaceException; 47use OCA\Circles\Exceptions\UserTypeNotFoundException; 48use OCA\Circles\IFederatedUser; 49use OCA\Circles\IEntity; 50use OCA\Circles\Model\Federated\RemoteInstance; 51 52/** 53 * Class Member 54 * 55 * @package OCA\Circles\Model 56 */ 57class Member extends ManagedModel implements 58 IEntity, 59 IFederatedUser, 60 IDeserializable, 61 INC22QueryRow, 62 JsonSerializable { 63 use TArrayTools; 64 use TNC22Deserialize; 65 66 67 public const LEVEL_NONE = 0; 68 public const LEVEL_MEMBER = 1; 69 public const LEVEL_MODERATOR = 4; 70 public const LEVEL_ADMIN = 8; 71 public const LEVEL_OWNER = 9; 72 73 public const TYPE_SINGLE = 0; 74 public const TYPE_USER = 1; 75 public const TYPE_GROUP = 2; 76 public const TYPE_MAIL = 4; 77 public const TYPE_CONTACT = 8; 78 public const TYPE_CIRCLE = 16; 79 public const TYPE_APP = 10000; 80 81 public const APP_CIRCLES = 10001; 82 public const APP_OCC = 10002; 83 84 85 public static $TYPE = [ 86 0 => 'single', 87 1 => 'user', 88 2 => 'group', 89 4 => 'mail', 90 8 => 'contact', 91 16 => 'circle', 92 10000 => 'app' 93 ]; 94 95 /** 96 * Note: When editing those values, update lib/Application/Capabilities.php 97 * 98 * @see Capabilities::generateConstantsMember() 99 */ 100 public const STATUS_INVITED = 'Invited'; 101 public const STATUS_REQUEST = 'Requesting'; 102 public const STATUS_MEMBER = 'Member'; 103 public const STATUS_BLOCKED = 'Blocked'; 104 105 106 /** 107 * Note: When editing those values, update lib/Application/Capabilities.php 108 * 109 * @see Capabilities::generateConstantsMember() 110 * @var array 111 */ 112 public static $DEF_LEVEL = [ 113 1 => 'Member', 114 4 => 'Moderator', 115 8 => 'Admin', 116 9 => 'Owner' 117 ]; 118 119 120 public static $DEF_TYPE_MAX = 31; 121 122 123 /** @var string */ 124 private $id = ''; 125 126 /** @var string */ 127 private $circleId = ''; 128 129 /** @var string */ 130 private $singleId = ''; 131 132 /** @var string */ 133 private $userId = ''; 134 135 /** @var int */ 136 private $userType = 0; 137 138 /** @var Circle */ 139 private $basedOn; 140 141 /** @var Member */ 142 private $inheritanceFrom; 143 144 /** @var FederatedUser */ 145 private $inheritedBy; 146 147 /** @var string */ 148 private $instance = ''; 149 150 /** @var FederatedUser */ 151 private $invitedBy; 152 153 /** @var RemoteInstance */ 154 private $remoteInstance; 155 156 /** @var bool */ 157 private $local = false; 158 159 /** @var int */ 160 private $level = 0; 161 162 /** @var string */ 163 private $status = 'Unknown'; 164 165 /** @var array */ 166 private $notes = []; 167 168 /** @var string */ 169 private $displayName = ''; 170 171 /** @var int */ 172 private $displayUpdate = 0; 173 174 /** @var string */ 175 private $contactId = ''; 176 177 /** @var string */ 178 private $contactMeta = ''; 179 180 /** @var Circle */ 181 private $circle; 182 183 /** @var int */ 184 private $joined = 0; 185 186 /** @var Membership[] */ 187 private $memberships = null; 188 189 190 /** 191 * Member constructor. 192 */ 193 public function __construct() { 194 } 195 196 197 /** 198 * @param string $id 199 * 200 * @return $this 201 */ 202 public function setId(string $id): self { 203 $this->id = $id; 204 205 return $this; 206 } 207 208 /** 209 * @return string 210 */ 211 public function getId(): string { 212 return $this->id; 213 } 214 215 216 /** 217 * @param string $circleId 218 * 219 * @return Member 220 */ 221 public function setCircleId(string $circleId): self { 222 $this->circleId = $circleId; 223 224 return $this; 225 } 226 227 /** 228 * @return string 229 */ 230 public function getCircleId(): string { 231 return $this->circleId; 232 } 233 234 235 /** 236 * This should replace user_id, user_type and instance; and will use the data from Circle with 237 * Config=CFG_SINGLE 238 * 239 * @param string $singleId 240 * 241 * @return $this 242 */ 243 public function setSingleId(string $singleId): self { 244 $this->singleId = $singleId; 245 246 return $this; 247 } 248 249 /** 250 * @return string 251 */ 252 public function getSingleId(): string { 253 return $this->singleId; 254 } 255 256 257 /** 258 * @param string $userId 259 * 260 * @return Member 261 */ 262 public function setUserId(string $userId): self { 263 $this->userId = $userId; 264 if ($this->displayName === '') { 265 $this->displayName = $userId; 266 } 267 268 return $this; 269 } 270 271 /** 272 * @return string 273 */ 274 public function getUserId(): string { 275 return $this->userId; 276 } 277 278 279 /** 280 * @param int $userType 281 * 282 * @return Member 283 */ 284 public function setUserType(int $userType): self { 285 $this->userType = $userType; 286 287 return $this; 288 } 289 290 /** 291 * @return int 292 */ 293 public function getUserType(): int { 294 return $this->userType; 295 } 296 297 /** 298 * @return int 299 * @deprecated 22.0.0 300 */ 301 public function getType(): int { 302 return $this->getUserType(); 303 } 304 305 306 /** 307 * @param string $instance 308 * 309 * @return Member 310 */ 311 public function setInstance(string $instance): self { 312 $this->instance = $instance; 313 314 return $this; 315 } 316 317 /** 318 * @return string 319 */ 320 public function getInstance(): string { 321 return $this->instance; 322 } 323 324 325 /** 326 * @return bool 327 */ 328 public function isLocal(): bool { 329 return $this->getManager()->isLocalInstance($this->getInstance()); 330 } 331 332 333 /** 334 * @param FederatedUser $invitedBy 335 * 336 * @return Member 337 */ 338 public function setInvitedBy(FederatedUser $invitedBy): Member { 339 $this->invitedBy = $invitedBy; 340 341 return $this; 342 } 343 344 /** 345 * @return FederatedUser 346 */ 347 public function getInvitedBy(): FederatedUser { 348 return $this->invitedBy; 349 } 350 351 /** 352 * @return bool 353 */ 354 public function hasInvitedBy(): bool { 355 return !is_null($this->invitedBy); 356 } 357 358 359 /** 360 * @return bool 361 */ 362 public function hasRemoteInstance(): bool { 363 return !is_null($this->remoteInstance); 364 } 365 366 /** 367 * @param RemoteInstance $remoteInstance 368 * 369 * @return Member 370 */ 371 public function setRemoteInstance(RemoteInstance $remoteInstance): self { 372 $this->remoteInstance = $remoteInstance; 373 374 return $this; 375 } 376 377 /** 378 * @return RemoteInstance 379 */ 380 public function getRemoteInstance(): RemoteInstance { 381 return $this->remoteInstance; 382 } 383 384 385 /** 386 * @return bool 387 */ 388 public function hasBasedOn(): bool { 389 return !is_null($this->basedOn); 390 } 391 392 /** 393 * @param Circle $basedOn 394 * 395 * @return $this 396 */ 397 public function setBasedOn(Circle $basedOn): self { 398 $this->basedOn = $basedOn; 399 400 return $this; 401 } 402 403 /** 404 * @return Circle 405 */ 406 public function getBasedOn(): Circle { 407 return $this->basedOn; 408 } 409 410 411 /** 412 * @return bool 413 */ 414 public function hasInheritedBy(): bool { 415 return !is_null($this->inheritedBy); 416 } 417 418 /** 419 * @param FederatedUser $inheritedBy 420 * 421 * @return $this 422 */ 423 public function setInheritedBy(FederatedUser $inheritedBy): self { 424 $this->inheritedBy = $inheritedBy; 425 426 return $this; 427 } 428 429 /** 430 * @return FederatedUser 431 */ 432 public function getInheritedBy(): FederatedUser { 433 return $this->inheritedBy; 434 } 435 436 437 /** 438 * @return bool 439 */ 440 public function hasInheritanceFrom(): bool { 441 return !is_null($this->inheritanceFrom); 442 } 443 444 /** 445 * @param Member $inheritanceFrom 446 * 447 * @return $this 448 */ 449 public function setInheritanceFrom(Member $inheritanceFrom): self { 450 $this->inheritanceFrom = $inheritanceFrom; 451 452 return $this; 453 } 454 455 /** 456 * @return Member|null 457 */ 458 public function getInheritanceFrom(): ?Member { 459 return $this->inheritanceFrom; 460 } 461 462 463 /** 464 * @param int $level 465 * 466 * @return Member 467 */ 468 public function setLevel(int $level): self { 469 $this->level = $level; 470 471 return $this; 472 } 473 474 /** 475 * @return int 476 */ 477 public function getLevel(): int { 478 return $this->level; 479 } 480 481 482 /** 483 * @param string $status 484 * 485 * @return Member 486 */ 487 public function setStatus(string $status): self { 488 $this->status = $status; 489 490 return $this; 491 } 492 493 /** 494 * @return string 495 */ 496 public function getStatus(): string { 497 return $this->status; 498 } 499 500 501 /** 502 * @param array $notes 503 * 504 * @return Member 505 */ 506 public function setNotes(array $notes): self { 507 $this->notes = $notes; 508 509 return $this; 510 } 511 512 /** 513 * @return array 514 */ 515 public function getNotes(): array { 516 return $this->notes; 517 } 518 519 520 /** 521 * @param string $key 522 * 523 * @return string 524 */ 525 public function getNote(string $key): string { 526 return $this->get($key, $this->notes); 527 } 528 529 /** 530 * @param string $key 531 * 532 * @return array 533 */ 534 public function getNoteArray(string $key): array { 535 return $this->getArray($key, $this->notes); 536 } 537 538 /** 539 * @param string $key 540 * @param string $note 541 * 542 * @return $this 543 */ 544 public function setNote(string $key, string $note): self { 545 $this->notes[$key] = $note; 546 547 return $this; 548 } 549 550 /** 551 * @param string $key 552 * @param array $note 553 * 554 * @return $this 555 */ 556 public function setNoteArray(string $key, array $note): self { 557 $this->notes[$key] = $note; 558 559 return $this; 560 } 561 562 /** 563 * @param string $key 564 * @param JsonSerializable $obj 565 * 566 * @return $this 567 */ 568 public function setNoteObj(string $key, JsonSerializable $obj): self { 569 $this->notes[$key] = $obj; 570 571 return $this; 572 } 573 574 575 /** 576 * @param string $displayName 577 * 578 * @return Member 579 */ 580 public function setDisplayName(string $displayName): self { 581 if ($displayName !== '') { 582 $this->displayName = $displayName; 583 } 584 585 return $this; 586 } 587 588 589 /** 590 * @param int $displayUpdate 591 * 592 * @return Member 593 */ 594 public function setDisplayUpdate(int $displayUpdate): self { 595 $this->displayUpdate = $displayUpdate; 596 597 return $this; 598 } 599 600 /** 601 * @return int 602 */ 603 public function getDisplayUpdate(): int { 604 return $this->displayUpdate; 605 } 606 607 608 /** 609 * @return string 610 */ 611 public function getDisplayName(): string { 612 return $this->displayName; 613 } 614 615 616 /** 617 * @param string $contactId 618 * 619 * @return Member 620 */ 621 public function setContactId(string $contactId): self { 622 $this->contactId = $contactId; 623 624 return $this; 625 } 626 627 /** 628 * @return string 629 */ 630 public function getContactId(): string { 631 return $this->contactId; 632 } 633 634 635 /** 636 * @param string $contactMeta 637 * 638 * @return Member 639 */ 640 public function setContactMeta(string $contactMeta): self { 641 $this->contactMeta = $contactMeta; 642 643 return $this; 644 } 645 646 /** 647 * @return string 648 */ 649 public function getContactMeta(): string { 650 return $this->contactMeta; 651 } 652 653 654 /** 655 * @param Circle $circle 656 * 657 * @return self 658 */ 659 public function setCircle(Circle $circle): self { 660 $this->circle = $circle; 661 662 return $this; 663 } 664 665 /** 666 * @return Circle 667 */ 668 public function getCircle(): Circle { 669 return $this->circle; 670 } 671 672 /** 673 * @return bool 674 */ 675 public function hasCircle(): bool { 676 return (!is_null($this->circle)); 677 } 678 679 680 /** 681 * @param int $joined 682 * 683 * @return Member 684 */ 685 public function setJoined(int $joined): self { 686 $this->joined = $joined; 687 688 return $this; 689 } 690 691 /** 692 * @return int 693 */ 694 public function getJoined(): int { 695 return $this->joined; 696 } 697 698 699 /** 700 * @return bool 701 */ 702 public function hasMemberships(): bool { 703 return !is_null($this->memberships); 704 } 705 706 /** 707 * @param array $memberships 708 * 709 * @return self 710 */ 711 public function setMemberships(array $memberships): IEntity { 712 $this->memberships = $memberships; 713 714 return $this; 715 } 716 717 /** 718 * @return Membership[] 719 */ 720 public function getMemberships(): array { 721 if (is_null($this->memberships)) { 722 $this->getManager()->getMemberships($this); 723 } 724 725 return $this->memberships; 726 } 727 728 729 /** 730 * @param string $singleId 731 * @param bool $detailed 732 * 733 * @return Membership 734 * @throws MembershipNotFoundException 735 * @throws RequestBuilderException 736 */ 737 public function getLink(string $singleId, bool $detailed = false): Membership { 738 $this->getManager()->getLink($this, $singleId, $detailed); 739 740 throw new MembershipNotFoundException(); 741 } 742 743 /** 744 * @param string $circleId 745 * @param bool $detailed 746 * 747 * @return Membership 748 * @throws MembershipNotFoundException 749 * @throws RequestBuilderException 750 * @deprecated - use getLink(); 751 */ 752 public function getMembership(string $circleId, bool $detailed = false): Membership { 753 return $this->getLink($circleId, $detailed); 754 } 755 756 757 /** 758 * @param Member $member 759 * @param bool $full 760 * 761 * @return bool 762 */ 763 public function compareWith(Member $member, bool $full = true): bool { 764 if ($this->getId() !== $member->getId() 765 || $this->getCircleId() !== $member->getCircleId() 766 || $this->getSingleId() !== $member->getSingleId() 767 || $this->getUserId() !== $member->getUserId() 768 || $this->getUserType() <> $member->getUserType() 769 || $this->getInstance() !== $member->getInstance()) { 770 return false; 771 } 772 773 if ($full 774 && ($this->getLevel() <> $member->getLevel() 775 || $this->getStatus() !== $member->getStatus())) { 776 return false; 777 } 778 779 return true; 780 } 781 782 783 /** 784 * @param array $data 785 * 786 * @return $this 787 * @throws InvalidItemException 788 */ 789 public function import(array $data): IDeserializable { 790 if ($this->get('userId', $data) === '') { 791 throw new InvalidItemException(); 792 } 793 794 $this->setId($this->get('id', $data)); 795 $this->setCircleId($this->get('circleId', $data)); 796 $this->setSingleId($this->get('singleId', $data)); 797 $this->setUserId($this->get('userId', $data)); 798 $this->setUserType($this->getInt('userType', $data)); 799 $this->setInstance($this->get('instance', $data)); 800 $this->setLevel($this->getInt('level', $data)); 801 $this->setStatus($this->get('status', $data)); 802 $this->setDisplayName($this->get('displayName', $data)); 803 $this->setDisplayUpdate($this->getInt('displayUpdate', $data)); 804 $this->setNotes($this->getArray('notes', $data)); 805 $this->setContactId($this->get('contactId', $data)); 806 $this->setContactMeta($this->get('contactMeta', $data)); 807 $this->setJoined($this->getInt('joined', $data)); 808 809 try { 810 /** @var Circle $circle */ 811 $circle = $this->deserialize($this->getArray('circle', $data), Circle::class); 812 $this->setCircle($circle); 813 } catch (InvalidItemException $e) { 814 } 815 816 try { 817 /** @var Circle $circle */ 818 $circle = $this->deserialize($this->getArray('basedOn', $data), Circle::class); 819 $this->setBasedOn($circle); 820 } catch (InvalidItemException $e) { 821 } 822 823 try { 824 /** @var FederatedUser $invitedBy */ 825 $invitedBy = $this->deserialize($this->getArray('invitedBy', $data), FederatedUser::class); 826 $this->setInvitedBy($invitedBy); 827 } catch (InvalidItemException $e) { 828 } 829 830 try { 831 /** @var FederatedUSer $inheritedBy */ 832 $inheritedBy = $this->deserialize($this->getArray('inheritedBy', $data), Membership::class); 833 $this->setInheritedBy($inheritedBy); 834 } catch (InvalidItemException $e) { 835 } 836 837 return $this; 838 } 839 840 841 /** 842 * @param array $data 843 * @param string $prefix 844 * 845 * @return INC22QueryRow 846 * @throws MemberNotFoundException 847 */ 848 public function importFromDatabase(array $data, string $prefix = ''): INC22QueryRow { 849 if ($this->get($prefix . 'single_id', $data) === '') { 850 throw new MemberNotFoundException(); 851 } 852 853 $this->setId($this->get($prefix . 'member_id', $data)); 854 $this->setCircleId($this->get($prefix . 'circle_id', $data)); 855 $this->setSingleId($this->get($prefix . 'single_id', $data)); 856 $this->setUserId($this->get($prefix . 'user_id', $data)); 857 $this->setUserType($this->getInt($prefix . 'user_type', $data)); 858 $this->setInstance($this->get($prefix . 'instance', $data)); 859 $this->setLevel($this->getInt($prefix . 'level', $data)); 860 $this->setStatus($this->get($prefix . 'status', $data)); 861 $this->setDisplayName($this->get($prefix . 'cached_name', $data)); 862 $this->setNotes($this->getArray($prefix . 'note', $data)); 863 $this->setContactId($this->get($prefix . 'contact_id', $data)); 864 $this->setContactMeta($this->get($prefix . 'contact_meta', $data)); 865 866 $cachedUpdate = $this->get($prefix . 'cached_update', $data); 867 if ($cachedUpdate !== '') { 868 $this->setDisplayUpdate(DateTime::createFromFormat('Y-m-d H:i:s', $cachedUpdate)->getTimestamp()); 869 } 870 871 $joined = $this->get($prefix . 'joined', $data); 872 if ($joined !== '') { 873 $this->setJoined(DateTime::createFromFormat('Y-m-d H:i:s', $joined)->getTimestamp()); 874 } 875 876 if ($this->getInstance() === '') { 877 $this->setInstance($this->getManager()->getLocalInstance()); 878 } 879 880 $this->getManager()->manageImportFromDatabase($this, $data, $prefix); 881 882 // in case invitedBy is not obtainable from 'invited_by', we reach data from 'note' 883 if (!$this->hasInvitedBy()) { 884 $invitedByArray = $this->getNoteArray('invitedBy'); 885 if (!empty($invitedByArray)) { 886 try { 887 $invitedBy = new FederatedUser(); 888 $this->setInvitedBy($invitedBy->import($invitedByArray)); 889 } catch (InvalidItemException $e) { 890 } 891 } 892 } 893 894 return $this; 895 } 896 897 898 /** 899 * @return string[] 900 * @throws UnknownInterfaceException 901 */ 902 public function jsonSerialize(): array { 903 $arr = [ 904 'id' => $this->getId(), 905 'circleId' => $this->getCircleId(), 906 'singleId' => $this->getSingleId(), 907 'userId' => $this->getUserId(), 908 'userType' => $this->getUserType(), 909 'instance' => $this->getManager()->fixInstance($this->getInstance()), 910 'local' => $this->isLocal(), 911 'level' => $this->getLevel(), 912 'status' => $this->getStatus(), 913 'displayName' => $this->getDisplayName(), 914 'displayUpdate' => $this->getDisplayUpdate(), 915 'notes' => $this->getNotes(), 916 'contactId' => $this->getContactId(), 917 'contactMeta' => $this->getContactMeta(), 918 'joined' => $this->getJoined() 919 ]; 920 921 if ($this->hasInvitedBy()) { 922 $arr['invitedBy'] = $this->getInvitedBy(); 923 } 924 925 if ($this->hasBasedOn()) { 926 $arr['basedOn'] = $this->getBasedOn(); 927 } 928 929 if ($this->hasInheritedBy()) { 930 $arr['inheritedBy'] = $this->getInheritedBy(); 931 } 932 933 if ($this->hasInheritanceFrom()) { 934 $arr['inheritanceFrom'] = $this->getInheritanceFrom(); 935 } 936 937 if ($this->hasCircle()) { 938 $arr['circle'] = $this->getCircle(); 939 } 940 941 if ($this->hasMemberships()) { 942 $arr['memberships'] = $this->getMemberships(); 943 } 944 945 if ($this->hasRemoteInstance()) { 946 $arr['remoteInstance'] = $this->getRemoteInstance(); 947 } 948 949 return $arr; 950 } 951 952 953 /** 954 * @param int $level 955 * 956 * @return int 957 * @throws ParseMemberLevelException 958 */ 959 public static function parseLevelInt(int $level): int { 960 if (!array_key_exists($level, self::$DEF_LEVEL)) { 961 $all = implode(', ', array_keys(self::$DEF_LEVEL)); 962 throw new ParseMemberLevelException('Available levels: ' . $all, 121); 963 } 964 965 return $level; 966 } 967 968 969 /** 970 * @param string $levelString 971 * 972 * @return int 973 * @throws ParseMemberLevelException 974 */ 975 public static function parseLevelString(string $levelString): int { 976 $levelString = ucfirst(strtolower($levelString)); 977 $level = array_search($levelString, Member::$DEF_LEVEL); 978 979 if (!$level) { 980 $all = implode(', ', array_values(self::$DEF_LEVEL)); 981 throw new ParseMemberLevelException('Available levels: ' . $all, 121); 982 } 983 984 return (int)$level; 985 } 986 987 /** 988 * @param string $typeString 989 * 990 * @return int 991 * @throws UserTypeNotFoundException 992 */ 993 public static function parseTypeString(string $typeString): int { 994 $typeString = strtolower($typeString); 995 if (array_key_exists($typeString, Member::$TYPE)) { 996 return (int)$typeString; 997 } 998 999 $type = array_search($typeString, Member::$TYPE); 1000 if ($type === false) { 1001 $all = implode(', ', array_values(self::$TYPE)); 1002 throw new UserTypeNotFoundException('Available types: ' . $all); 1003 } 1004 1005 return (int)$type; 1006 } 1007} 1008