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