1<?php 2/********************************************************************* 3 class.organization.php 4 5 Peter Rotich <peter@osticket.com> 6 Jared Hancock <jared@osticket.com> 7 Copyright (c) 2014 osTicket 8 http://www.osticket.com 9 10 Released under the GNU General Public License WITHOUT ANY WARRANTY. 11 See LICENSE.TXT for details. 12 13 vim: expandtab sw=4 ts=4 sts=4: 14**********************************************************************/ 15require_once(INCLUDE_DIR . 'class.orm.php'); 16require_once(INCLUDE_DIR . 'class.forms.php'); 17require_once(INCLUDE_DIR . 'class.dynamic_forms.php'); 18require_once(INCLUDE_DIR . 'class.user.php'); 19require_once INCLUDE_DIR . 'class.search.php'; 20 21class OrganizationModel extends VerySimpleModel { 22 static $meta = array( 23 'table' => ORGANIZATION_TABLE, 24 'pk' => array('id'), 25 'joins' => array( 26 'users' => array( 27 'reverse' => 'User.org', 28 ), 29 'cdata' => array( 30 'constraint' => array('id' => 'OrganizationCdata.org_id'), 31 ), 32 'entries' => array( 33 'constraint' => array( 34 'id' => 'DynamicFormEntry.object_id', 35 "'O'" => 'DynamicFormEntry.object_type', 36 ), 37 'list' => true, 38 ), 39 ), 40 ); 41 42 const COLLAB_ALL_MEMBERS = 0x0001; 43 const COLLAB_PRIMARY_CONTACT = 0x0002; 44 const ASSIGN_AGENT_MANAGER = 0x0004; 45 46 const SHARE_PRIMARY_CONTACT = 0x0008; 47 const SHARE_EVERYBODY = 0x0010; 48 49 const PERM_CREATE = 'org.create'; 50 const PERM_EDIT = 'org.edit'; 51 const PERM_DELETE = 'org.delete'; 52 53 static protected $perms = array( 54 self::PERM_CREATE => array( 55 'title' => /* @trans */ 'Create', 56 'desc' => /* @trans */ 'Ability to create new organizations', 57 'primary' => true, 58 ), 59 self::PERM_EDIT => array( 60 'title' => /* @trans */ 'Edit', 61 'desc' => /* @trans */ 'Ability to manage organizations', 62 'primary' => true, 63 ), 64 self::PERM_DELETE => array( 65 'title' => /* @trans */ 'Delete', 66 'desc' => /* @trans */ 'Ability to delete organizations', 67 'primary' => true, 68 ), 69 ); 70 71 var $_manager; 72 73 function getId() { 74 return $this->id; 75 } 76 77 function getName() { 78 return $this->name; 79 } 80 81 function getNumUsers() { 82 return $this->users->count(); 83 } 84 85 function getAccountManager() { 86 if (!isset($this->_manager)) { 87 if ($this->manager[0] == 't') 88 $this->_manager = Team::lookup(substr($this->manager, 1)); 89 elseif ($this->manager[0] == 's') 90 $this->_manager = Staff::lookup(substr($this->manager, 1)); 91 else 92 $this->_manager = ''; // None. 93 } 94 95 return $this->_manager; 96 } 97 98 function getAccountManagerId() { 99 return $this->manager; 100 } 101 102 function autoAddCollabs() { 103 return $this->check(self::COLLAB_ALL_MEMBERS | self::COLLAB_PRIMARY_CONTACT); 104 } 105 106 function autoAddPrimaryContactsAsCollabs() { 107 return $this->check(self::COLLAB_PRIMARY_CONTACT); 108 } 109 110 function autoAddMembersAsCollabs() { 111 return $this->check(self::COLLAB_ALL_MEMBERS); 112 } 113 114 function autoAssignAccountManager() { 115 return $this->check(self::ASSIGN_AGENT_MANAGER); 116 } 117 118 function autoFlagChanged($flag, $var) { 119 if (($flag && !$var) || (!$flag && $var)) 120 return true; 121 } 122 123 function shareWithPrimaryContacts() { 124 return $this->check(self::SHARE_PRIMARY_CONTACT); 125 } 126 127 function shareWithEverybody() { 128 return $this->check(self::SHARE_EVERYBODY); 129 } 130 131 function sharingFlagChanged($flag, $var, $title) { 132 if (($flag && !$var) || (!$flag && $var == $title)) 133 return true; 134 } 135 136 function getUpdateDate() { 137 return $this->updated; 138 } 139 140 function getCreateDate() { 141 return $this->created; 142 } 143 144 function check($flag) { 145 return 0 !== ($this->status & $flag); 146 } 147 148 protected function clearStatus($flag) { 149 return $this->set('status', $this->get('status') & ~$flag); 150 } 151 152 protected function setStatus($flag) { 153 return $this->set('status', $this->get('status') | $flag); 154 } 155 156 function allMembers() { 157 return $this->users; 158 } 159 160 static function getPermissions() { 161 return self::$perms; 162 } 163} 164include_once INCLUDE_DIR.'class.role.php'; 165RolePermission::register(/* @trans */ 'Organizations', 166 OrganizationModel::getPermissions()); 167 168class OrganizationCdata extends VerySimpleModel { 169 static $meta = array( 170 'table' => ORGANIZATION_CDATA_TABLE, 171 'pk' => array('org_id'), 172 'joins' => array( 173 'org' => array( 174 'constraint' => array('ord_id' => 'OrganizationModel.id'), 175 ), 176 ), 177 ); 178} 179 180class Organization extends OrganizationModel 181implements TemplateVariable, Searchable { 182 var $_entries; 183 var $_forms; 184 var $_queue; 185 186 function addDynamicData($data) { 187 $entry = $this->addForm(OrganizationForm::objects()->one(), 1, $data); 188 // FIXME: For some reason, the second save here is required or the 189 // custom data is not properly saved 190 $entry->save(); 191 192 return $entry; 193 } 194 195 function getDynamicData($create=true) { 196 if (!isset($this->_entries)) { 197 $this->_entries = DynamicFormEntry::forObject($this->id, 'O')->all(); 198 if (!$this->_entries && $create) { 199 $g = OrganizationForm::getInstance($this->id, true); 200 $g->save(); 201 $this->_entries[] = $g; 202 } 203 } 204 205 return $this->_entries ?: array(); 206 } 207 208 function getForms($data=null) { 209 210 if (!isset($this->_forms)) { 211 $this->_forms = array(); 212 foreach ($this->getDynamicData() as $entry) { 213 $entry->addMissingFields(); 214 if(!$data 215 && ($form = $entry->getDynamicForm()) 216 && $form->get('type') == 'O' ) { 217 foreach ($entry->getFields() as $f) { 218 if ($f->get('name') == 'name') 219 $f->value = $this->getName(); 220 } 221 } 222 223 $this->_forms[] = $entry; 224 } 225 } 226 227 return $this->_forms; 228 } 229 230 function getInfo() { 231 232 $base = array_filter($this->ht, 233 function ($e) { return !is_object($e); } 234 ); 235 236 foreach (array( 237 'collab-all-flag' => Organization::COLLAB_ALL_MEMBERS, 238 'collab-pc-flag' => Organization::COLLAB_PRIMARY_CONTACT, 239 'assign-am-flag' => Organization::ASSIGN_AGENT_MANAGER, 240 'sharing-primary' => Organization::SHARE_PRIMARY_CONTACT, 241 'sharing-all' => Organization::SHARE_EVERYBODY, 242 ) as $ck=>$flag) { 243 if ($this->check($flag)) 244 $base[$ck] = true; 245 } 246 return $base; 247 } 248 249 function isMappedToDomain($domain) { 250 if (!$domain || !$this->domain) 251 return false; 252 foreach (explode(',', $this->domain) as $d) { 253 $d = trim($d); 254 if ($d[0] == '.') { 255 // Subdomain syntax (.osticket.com accepts all subdomains of 256 // osticket.com) 257 if (strcasecmp(mb_substr($domain, -mb_strlen($d)), $d) === 0) 258 return true; 259 } 260 elseif (strcasecmp($domain, $d) === 0) { 261 return true; 262 } 263 } 264 return false; 265 } 266 267 static function forDomain($domain) { 268 if (!$domain) 269 return null; 270 foreach (static::objects()->filter(array( 271 'domain__gt'=>'', 272 'domain__contains'=>$domain 273 )) as $org) { 274 if ($org->isMappedToDomain($domain)) { 275 return $org; 276 } 277 } 278 } 279 280 function addForm($form, $sort=1, $data=null) { 281 $entry = $form->instanciate($sort, $data); 282 $entry->set('object_type', 'O'); 283 $entry->set('object_id', $this->getId()); 284 $entry->save(); 285 return $entry; 286 } 287 288 function getFilterData() { 289 $vars = array(); 290 foreach ($this->getDynamicData() as $entry) { 291 $vars += $entry->getFilterData(); 292 293 // Add special `name` field in Org form 294 if ($entry->getDynamicForm()->get('type') != 'O') 295 continue; 296 297 if ($f = $entry->getField('name')) 298 $vars['field.'.$f->get('id')] = $this->getName(); 299 } 300 301 return $vars; 302 } 303 304 function removeUser($user) { 305 306 if (!$user instanceof User) 307 return false; 308 309 if (!$user->setOrganization(null, false)) 310 return false; 311 312 // House cleaning - remove user from org contact..etc 313 $user->setPrimaryContact(false); 314 315 return $user->save(); 316 } 317 318 function to_json() { 319 320 $info = array( 321 'id' => $this->getId(), 322 'name' => (string) $this->getName() 323 ); 324 325 return JsonDataEncoder::encode($info); 326 } 327 328 329 function __toString() { 330 return (string) $this->getName(); 331 } 332 333 function asVar() { 334 return (string) $this->getName(); 335 } 336 337 function getVar($tag) { 338 $tag = mb_strtolower($tag); 339 foreach ($this->getDynamicData() as $e) 340 if ($a = $e->getAnswer($tag)) 341 return $a; 342 343 switch ($tag) { 344 case 'members': 345 return new UserList($this->users); 346 case 'manager': 347 return $this->getAccountManager(); 348 case 'contacts': 349 return new UserList($this->users->filter(array( 350 'status' => User::PRIMARY_ORG_CONTACT 351 ))); 352 } 353 } 354 355 static function getVarScope() { 356 $base = array( 357 'contacts' => array('class' => 'UserList', 'desc' => __('Primary Contacts')), 358 'manager' => __('Account Manager'), 359 'members' => array('class' => 'UserList', 'desc' => __('Organization Members')), 360 'name' => __('Name'), 361 ); 362 $extra = VariableReplacer::compileFormScope(OrganizationForm::getInstance()); 363 return $base + $extra; 364 } 365 366 static function getSearchableFields() { 367 $base = array(); 368 $uform = OrganizationForm::objects()->one(); 369 $base = array(); 370 foreach ($uform->getFields() as $F) { 371 $fname = $F->get('name') ?: ('field_'.$F->get('id')); 372 if (!$F->hasData() || $F->isPresentationOnly()) 373 continue; 374 if (!$F->isStorable()) 375 $base[$fname] = $F; 376 else 377 $base["cdata__{$fname}"] = $F; 378 } 379 return $base; 380 } 381 382 static function supportsCustomData() { 383 return true; 384 } 385 386 function updateProfile($vars, &$errors) { 387 if ($vars['domain']) { 388 foreach (explode(',', $vars['domain']) as $d) { 389 if (!Validator::is_email('t@' . trim($d))) { 390 $errors['domain'] = __('Enter a valid email domain, like domain.com'); 391 } 392 } 393 } 394 395 if ($vars['manager']) { 396 switch ($vars['manager'][0]) { 397 case 's': 398 if ($staff = Staff::lookup(substr($vars['manager'], 1))) 399 break; 400 case 't': 401 if ($vars['manager'][0] == 't' 402 && $team = Team::lookup(substr($vars['manager'], 1))) 403 break; 404 default: 405 $errors['manager'] = __('Select an agent or team from the list'); 406 } 407 } 408 409 // Attempt to valid & update dynamic data even on errors 410 if (!$this->update($vars, $errors)) 411 $errors['error'] = __('Unable to update organization form'); 412 413 if ($errors) 414 return false; 415 416 return $this->save(); 417 } 418 419 function update($vars, &$errors) { 420 $valid = true; 421 $forms = $this->getForms($vars); 422 foreach ($forms as $entry) { 423 if (!$entry->isValid()) 424 $valid = false; 425 if ($entry->getDynamicForm()->get('type') == 'O' 426 && ($f = $entry->getField('name')) 427 && $f->getClean() 428 && ($o=Organization::lookup(array('name'=>$f->getClean()))) 429 && $o->id != $this->getId()) { 430 $valid = false; 431 $f->addError(__('Organization with the same name already exists')); 432 } 433 } 434 if (!$valid || $errors) 435 return false; 436 437 // Save dynamic data. 438 foreach ($this->getDynamicData() as $entry) { 439 $fields = $entry->getFields(); 440 foreach ($fields as $field) { 441 $changes = $field->getChanges(); 442 if ((is_array($changes) && $changes[0]) || $changes && !is_array($changes)) { 443 $type = array('type' => 'edited', 'key' => $field->getLabel()); 444 Signal::send('object.edited', $this, $type); 445 } 446 } 447 if ($entry->getDynamicForm()->get('type') == 'O' 448 && ($name = $entry->getField('name')) 449 ) { 450 if ($this->name != $name->getClean()) { 451 $type = array('type' => 'edited', 'key' => 'Name'); 452 Signal::send('object.edited', $this, $type); 453 } 454 $this->name = $name->getClean(); 455 $this->save(); 456 } 457 $entry->setSource($vars); 458 if ($entry->save()) 459 $this->updated = SqlFunction::NOW(); 460 } 461 462 if ($auditCollabAll = $this->autoFlagChanged($this->autoAddMembersAsCollabs(), 463 $vars['collab-all-flag'])) 464 $key = 'collab-all-flag'; 465 if ($auditCollabPc = $this->autoFlagChanged($this->autoAddPrimaryContactsAsCollabs(), 466 $vars['collab-pc-flag'])) 467 $key = 'collab-pc-flag'; 468 if ($auditAssignAm = $this->autoFlagChanged($this->autoAssignAccountManager(), 469 $vars['assign-am-flag'])) 470 $key = 'assign-am-flag'; 471 472 if ($auditCollabAll || $auditCollabPc || $auditAssignAm) { 473 $type = array('type' => 'edited', 'key' => $key); 474 Signal::send('object.edited', $this, $type); 475 } 476 477 foreach ($vars as $key => $value) { 478 // Primary Contacts List Changes 479 if ($key == 'contacts') { 480 $ogContacts = $value; 481 if ($contacts = $this->getVar('contacts')) { 482 $allContacts = array(); 483 foreach ($contacts as $key => $value) 484 $allContacts[] = strval($value->getId()); 485 486 if ($ogContacts != $allContacts) { 487 $type = array('type' => 'edited', 'key' => 'contacts'); 488 Signal::send('object.edited', $this, $type); 489 } 490 } 491 } 492 if ($key != 'id' && $this->get($key) && $value != $this->get($key)) { 493 $type = array('type' => 'edited', 'key' => $key); 494 Signal::send('object.edited', $this, $type); 495 } 496 } 497 498 $sharingPrimary = $this->sharingFlagChanged($this->shareWithPrimaryContacts(), 499 $vars['sharing'], 'sharing-primary'); 500 $sharingEverybody = $this->sharingFlagChanged($this->shareWithEverybody(), 501 $vars['sharing'], 'sharing-all'); 502 503 // Set flags 504 foreach (array( 505 'collab-all-flag' => Organization::COLLAB_ALL_MEMBERS, 506 'collab-pc-flag' => Organization::COLLAB_PRIMARY_CONTACT, 507 'assign-am-flag' => Organization::ASSIGN_AGENT_MANAGER, 508 ) as $ck=>$flag) { 509 if ($vars[$ck]) 510 $this->setStatus($flag); 511 else 512 $this->clearStatus($flag); 513 } 514 515 foreach (array( 516 'sharing-primary' => Organization::SHARE_PRIMARY_CONTACT, 517 'sharing-all' => Organization::SHARE_EVERYBODY, 518 ) as $ck=>$flag) { 519 if (($sharingPrimary || $sharingEverybody) && $vars['sharing'] == $ck) { 520 $type = array('type' => 'edited', 'key' => 'sharing'); 521 Signal::send('object.edited', $this, $type); 522 } 523 if ($vars['sharing'] == $ck) 524 $this->setStatus($flag); 525 else 526 $this->clearStatus($flag); 527 } 528 529 // Set staff and primary contacts 530 $this->set('domain', $vars['domain']); 531 $this->set('manager', $vars['manager'] ?: ''); 532 if ($vars['contacts'] && is_array($vars['contacts'])) { 533 foreach ($this->allMembers() as $u) { 534 $u->setPrimaryContact(array_search($u->id, $vars['contacts']) !== false); 535 $u->save(); 536 } 537 } else { 538 $members = $this->allMembers(); 539 $members->update(array( 540 'status' => SqlExpression::bitand( 541 new SqlField('status'), ~User::PRIMARY_ORG_CONTACT) 542 )); 543 } 544 545 return true; 546 } 547 548 function delete() { 549 if (!parent::delete()) 550 return false; 551 552 // Clear organization from session to avoid refetch failure 553 unset($_SESSION[':Q:orgs'], $_SESSION[':O:tickets']); 554 $type = array('type' => 'deleted'); 555 Signal::send('object.deleted', $this, $type); 556 557 // Remove users from this organization 558 User::objects() 559 ->filter(array('org' => $this)) 560 ->update(array('org_id' => 0)); 561 562 foreach ($this->getDynamicData(false) as $entry) { 563 if (!$entry->delete()) 564 return false; 565 } 566 return true; 567 } 568 569 static function getLink($id) { 570 global $thisstaff; 571 572 if (!$id || !$thisstaff) 573 return false; 574 575 return ROOT_PATH . sprintf('scp/orgs.php?id=%s', $id); 576 } 577 578 static function fromVars($vars) { 579 $vars['name'] = Format::striptags($vars['name']); 580 if (!($org = static::lookup(array('name' => $vars['name'])))) { 581 $org = static::create(array( 582 'name' => $vars['name'], 583 'updated' => new SqlFunction('NOW'), 584 )); 585 $org->save(true); 586 $org->addDynamicData($vars); 587 } 588 589 Signal::send('organization.created', $org); 590 $type = array('type' => 'created'); 591 Signal::send('object.created', $org, $type); 592 return $org; 593 } 594 595 static function fromForm($form) { 596 597 if (!$form) 598 return null; 599 600 //Validate the form 601 $valid = true; 602 if (!$form->isValid()) 603 $valid = false; 604 605 // Make sure the name is not in-use 606 if (($field=$form->getField('name')) 607 && $field->getClean() 608 && static::lookup(array('name' => $field->getClean()))) { 609 $field->addError(__('Organization with the same name already exists')); 610 $valid = false; 611 } 612 613 return $valid ? self::fromVars($form->getClean()) : null; 614 } 615 616 static function create($vars=false) { 617 $org = new static($vars); 618 619 $org->created = new SqlFunction('NOW'); 620 $org->setStatus(self::SHARE_PRIMARY_CONTACT); 621 return $org; 622 } 623 624 // Custom create called by installer/upgrader to load initial data 625 static function __create($ht, &$error=false) { 626 627 $org = static::create($ht); 628 // Add dynamic data (if any) 629 if ($ht['fields']) { 630 $org->save(true); 631 $org->addDynamicData($ht['fields']); 632 } 633 634 return $org; 635 } 636 637 function getTicketsQueue() { 638 global $thisstaff; 639 640 if (!$this->_queue) { 641 $name = $this->getName(); 642 $this->_queue = new AdhocSearch(array( 643 'id' => 'adhoc,orgid'.$this->getId(), 644 'root' => 'T', 645 'staff_id' => $thisstaff->getId(), 646 'title' => $name 647 )); 648 $this->_queue->config = [[ 649 'user__org__name', 'equal', $name 650 ]]; 651 } 652 653 return $this->_queue; 654 } 655} 656 657class OrganizationForm extends DynamicForm { 658 static $instance; 659 static $form; 660 661 static $cdata = array( 662 'table' => ORGANIZATION_CDATA_TABLE, 663 'object_id' => 'org_id', 664 'object_type' => ObjectModel::OBJECT_TYPE_ORG, 665 ); 666 667 static function objects() { 668 $os = parent::objects(); 669 return $os->filter(array('type'=>'O')); 670 } 671 672 static function getDefaultForm() { 673 if (!isset(static::$form)) { 674 if (($o = static::objects()) && $o[0]) 675 static::$form = $o[0]; 676 else //TODO: Remove the code below and move it to task?? 677 static::$form = self::__loadDefaultForm(); 678 } 679 680 return static::$form; 681 } 682 683 static function getInstance($object_id=0, $new=false, $data=null) { 684 if ($new || !isset(static::$instance)) 685 static::$instance = static::getDefaultForm()->instanciate(1, $data); 686 687 static::$instance->object_type = 'O'; 688 689 if ($object_id) 690 static::$instance->object_id = $object_id; 691 692 return static::$instance; 693 } 694 695 static function __loadDefaultForm() { 696 require_once(INCLUDE_DIR.'class.i18n.php'); 697 698 $i18n = new Internationalization(); 699 $tpl = $i18n->getTemplate('form.yaml'); 700 foreach ($tpl->getData() as $f) { 701 if ($f['type'] == 'O') { 702 $form = DynamicForm::create($f); 703 $form->save(); 704 break; 705 } 706 } 707 708 if (!$form || !($o=static::objects())) 709 return false; 710 711 // Create sample organization. 712 if (($orgs = $i18n->getTemplate('organization.yaml')->getData())) 713 foreach($orgs as $org) 714 Organization::__create($org); 715 716 return $o[0]; 717 } 718 719} 720Filter::addSupportedMatches(/*@trans*/ 'Organization Data', function() { 721 $matches = array(); 722 foreach (OrganizationForm::getInstance()->getFields() as $f) { 723 if (!$f->hasData()) 724 continue; 725 $matches['field.'.$f->get('id')] = __('Organization').' / '.$f->getLabel(); 726 if (($fi = $f->getImpl()) && $fi->hasSubFields()) { 727 foreach ($fi->getSubFields() as $p) { 728 $matches['field.'.$f->get('id').'.'.$p->get('id')] 729 = __('Organization').' / '.$f->getLabel().' / '.$p->getLabel(); 730 } 731 } 732 } 733 return $matches; 734},40); 735?> 736