\d+)$',
89 | function($type=NULL, $state=NULL, $uid=NULL, $sid=NULL, $tid=NULL) {
90 | global $thisstaff;
91 |
92 | if (!$thisstaff)
93 | Http::response(403, 'Agent login is required');
94 |
95 | $show = AuditEntry::$show_view_audits;
96 | $data = array();
97 | if ($type) {
98 | $url = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY);
99 | $qarray = explode('&', $url);
100 |
101 | foreach ($qarray as $key => $value) {
102 | list($k, $v) = explode('=', $value);
103 | $data[$k] = $v;
104 | }
105 | foreach (AuditEntry::getTypes() as $abbrev => $info) {
106 | if ($type == $abbrev)
107 | $name = AuditEntry::getObjectName($info[0]);
108 | }
109 | $filename = sprintf('%s-audits-%s.csv', $name, strftime('%Y%m%d'));
110 | $export = array('audit', $filename, '', '', 'csv', $show, $data);
111 | } elseif ($uid) {
112 | $userName = User::getNameById($uid);
113 | $filename = sprintf('%s-audits-%s.csv', $userName->name, strftime('%Y%m%d'));
114 | $export = array('user', $filename, $tableInfo, $uid, 'csv', $show, $data);
115 | } elseif ($sid) {
116 | $staff = Staff::lookup($sid);
117 | $filename = sprintf('%s-audits-%s.csv', $staff->getName(), strftime('%Y%m%d'));
118 | $export = array('staff', $filename, $tableInfo, $sid, 'csv', $show, $data);
119 | } elseif ($tid) {
120 | $ticket = Ticket::lookup($tid);
121 | $filename = sprintf('%s-audits-%s.csv', $ticket->getNumber(), strftime('%Y%m%d'));
122 | $export = array('ticket', $filename, $tableInfo, $tid, 'csv', $show, $data);
123 | }
124 |
125 | try {
126 | $interval = 5;
127 | // Create desired exporter
128 | $exporter = new CsvExporter();
129 | $extra = array('filename' => $filename,
130 | 'interval' => $interval);
131 | // Register the export in the session
132 | Exporter::register($exporter, $extra);
133 | // Flush response / return export id and check interval
134 | Http::flush(201, json_encode(['eid' =>
135 | $exporter->getId(), 'interval' => $interval]));
136 | // Phew... now we're free to do the export
137 | session_write_close(); // Release session for other requests
138 | ignore_user_abort(1); // Leave us alone bro!
139 | @set_time_limit(0); // Useless when safe_mode is on
140 | // Export to the exporter
141 | $export[] = $exporter;
142 | call_user_func_array(array('Export', 'audits'), $export);
143 | $exporter->close();
144 | // Sleep 3 times the interval to allow time for file download
145 | sleep($interval*3);
146 | // Email the export if it exists
147 | $exporter->email($thisstaff);
148 | // Delete the file.
149 | @$exporter->delete();
150 | exit;
151 | } catch (Exception $ex) {
152 | $errors['err'] = __('Unable to prepare the export');
153 | }
154 |
155 | include 'templates/export.tmpl.php';
156 | })
157 | );
158 | });
159 | }
160 |
161 | function enable() {
162 | AuditEntry::autoCreateTable();
163 | return parent::enable();
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/audit/config.php:
--------------------------------------------------------------------------------
1 | new BooleanField(array(
7 | 'label' => __('Show View Audits'),
8 | 'default' => true,
9 | 'configuration' => array(
10 | 'desc' => __('Show Audit Logs for when a User or Agent views Tickets')
11 | )
12 | )),
13 | );
14 | }
15 | function pre_save(&$config, &$errors) {
16 | global $msg;
17 | if (!$errors)
18 | $msg = __('Configuration updated successfully');
19 | return true;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/audit/plugin.php:
--------------------------------------------------------------------------------
1 | 'audit:ticket', # notrans
4 | 'version' => '0.1',
5 | 'name' => 'Help Desk Audit',
6 | 'author' => 'Adriane Alexander',
7 | 'description' => 'Provides a configurable mechanism to audit viewing
8 | and other activity of tickets.',
9 | 'url' => 'http://www.osticket.com/download',
10 | 'plugin' => 'audit.php:AuditPlugin',
11 | );
12 |
13 | ?>
14 |
--------------------------------------------------------------------------------
/audit/templates/agent-audit.tmpl.php:
--------------------------------------------------------------------------------
1 | getId()) {
7 | $events = AuditEntry::getTableInfo($staff);
8 | $total = count($events);
9 | $qwhere = AuditEntry::getQwhere($staff);
10 | $pageNav=AuditEntry::getPageNav($qwhere);
11 | $pageNav->setURL('staff.php', $args);
12 | }
13 |
14 | ?>
15 |
16 |
17 |
18 | " . $staff->getName() . " has performed. "
20 | ); ?>
21 |
22 |
23 |
24 | '.$pageNav->showing().'';
27 | else
28 | echo sprintf(__('%s does not have any audits'), __('Agent'));
29 | ?>
30 |
31 |
32 |
33 |
34 |
35 |
36 | Description
37 | Timestamp
38 | IP Address
39 |
40 |
41 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | ';
61 | if ($staffId) echo ' '.__('Page').':'.$pageNav->getPageLinks('audits').' ';
62 | echo sprintf('%s ',
63 | $staffId,
64 | 'audit-export',
65 | __('Export'));
66 | echo '';
67 | }
68 | ?>
69 |
--------------------------------------------------------------------------------
/audit/templates/auditlogs.tmpl.php:
--------------------------------------------------------------------------------
1 | isAdmin()) die('Access Denied');
3 |
4 | $qs = array();
5 | if($_REQUEST['type'])
6 | $qs += array('type' => Format::htmlchars($_REQUEST['type']));
7 | $type='D';
8 |
9 | if ($_REQUEST['type'])
10 | $type=Format::htmlchars($_REQUEST['type']);
11 |
12 | if($_REQUEST['state'])
13 | $qs += array('state' => Format::htmlchars($_REQUEST['state']));
14 | $state='All';
15 |
16 | if ($_REQUEST['state'])
17 | $state=Format::htmlchars($_REQUEST['state']);
18 |
19 | //dates
20 | $startTime =($_REQUEST['startDate'] && (strlen($_REQUEST['startDate'])>=8))?strtotime($_REQUEST['startDate']):0;
21 | $endTime =($_REQUEST['endDate'] && (strlen($_REQUEST['endDate'])>=8))?strtotime($_REQUEST['endDate']):0;
22 | if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0)){
23 | $errors['err']=__('Entered date span is invalid. Selection ignored.');
24 | $startTime=$endTime=0;
25 | } else {
26 | if($startTime)
27 | $qs += array('startDate' => $_REQUEST['startDate']);
28 | if($endTime)
29 | $qs += array('endDate' => $_REQUEST['endDate']);
30 | }
31 | $order = AuditEntry::getOrder(Format::htmlchars($_REQUEST['order']));
32 | $qs += array('order' => (($order=='DESC') ? 'ASC' : 'DESC'));
33 | $qstr = '&'. Http::build_query($qs);
34 |
35 | $args = array();
36 | parse_str($_SERVER['QUERY_STRING'], $args);
37 | unset($args['p'], $args['_pjax']);
38 |
39 | // Apply pagination
40 | $events = AuditEntry::getTableInfo('AuditEntry');
41 | $total = count($events);
42 | $qwhere = AuditEntry::getQwhere('AuditEntry');
43 | $pageNav=AuditEntry::getPageNav($qwhere);
44 | $pageNav->setURL('audits.php', $args);
45 | ?>
46 |
47 |
89 |
90 |
91 |
155 |
--------------------------------------------------------------------------------
/audit/templates/ticket-audit.tmpl.php:
--------------------------------------------------------------------------------
1 | setURL('tickets.php', $args);
7 | $order = AuditEntry::getOrder($_REQUEST['order']);
8 | $qs = array();
9 | $qsReverse = array();
10 | $qs += array('order' => $order);
11 | $qsReverse += array('order' => ($order=='DESC' ? 'ASC' : 'DESC'));
12 | $qstr = Http::build_query($qs);
13 | $qstrReverse = Http::build_query($qsReverse);
14 | $url = '#audit/ticket/' . $ticketId . '/view?';
15 | $qstr = sprintf('%s&sort=timestamp', $qstr);
16 | ?>
17 |
18 |
19 |
20 |
21 |
22 | '.$pageNav->showing().'';
25 | else
26 | echo sprintf(__('%s does not have any audits'), __('Ticket'));
27 | ?>
28 |
29 |
30 |
31 |
';
66 | ?>
67 |
68 |
69 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
106 |
--------------------------------------------------------------------------------
/audit/templates/user-audit.tmpl.php:
--------------------------------------------------------------------------------
1 | setURL('users.php', $args);
11 | ?>
12 |
13 |
14 |
15 |
16 | '.$pageNav->showing().'';
19 | else
20 | echo sprintf(__('%s does not have any audits'), __('User'));
21 | ?>
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 | ';
50 | echo ' '.__('Page').':'.$pageNav->getPageLinks('audits').' ';
51 | echo sprintf('%s ',
52 | $user->getId(),
53 | 'audit-export',
54 | __('Export'));
55 | echo '';
56 | }
57 | ?>
58 |
--------------------------------------------------------------------------------
/auth-2fa/auth2fa.php:
--------------------------------------------------------------------------------
1 | getConfig();
12 | if ($config->get('custom_issuer'))
13 | Auth2FABackend::$custom_issuer = $config->get('custom_issuer');
14 |
15 | TwoFactorAuthenticationBackend::register('Auth2FABackend');
16 | }
17 |
18 | function enable() {
19 | return parent::enable();
20 | }
21 |
22 | function uninstall(&$errors) {
23 | $errors = array();
24 |
25 | self::disable();
26 |
27 | return parent::uninstall($errors);
28 | }
29 |
30 | function disable() {
31 | $default2fas = ConfigItem::getConfigsByNamespace(false, 'default_2fa', Auth2FABackend::$id);
32 | foreach($default2fas as $default2fa)
33 | $default2fa->delete();
34 |
35 | $tokens = ConfigItem::getConfigsByNamespace(false, Auth2FABackend::$id);
36 | foreach($tokens as $token)
37 | $token->delete();
38 |
39 | return parent::disable();
40 | }
41 | }
42 |
43 | require_once(INCLUDE_DIR.'UniversalClassLoader.php');
44 | use Symfony\Component\ClassLoader\UniversalClassLoader_osTicket;
45 | $loader = new UniversalClassLoader_osTicket();
46 | $loader->registerNamespaceFallbacks(array(
47 | dirname(__file__).'/lib'));
48 | $loader->register();
49 |
--------------------------------------------------------------------------------
/auth-2fa/class.auth2fa.php:
--------------------------------------------------------------------------------
1 | getQRCode($thisstaff);
18 | if ($auth2FA->validateQRCode($thisstaff)) {
19 | return array(
20 | '' => new FreeTextField(array(
21 | 'configuration' => array(
22 | 'content' => sprintf(
23 | '
24 | Use an Authenticator application on your phone to scan
25 | the QR Code below. If you lose the QR Code
26 | on the app, you will need to have your 2FA configurations reset by
27 | a helpdesk Administrator.
28 |
29 |
30 |
31 |
32 |
33 | ',
34 | $thisstaff->getEmail(), $qrCodeURL),
35 | )
36 | )),
37 | );
38 | }
39 | }
40 |
41 | protected function getInputOptions() {
42 | return array(
43 | 'token' => new TextboxField(array(
44 | 'id'=>1, 'label'=>__('Verification Code'), 'required'=>true, 'default'=>'',
45 | 'validator'=>'number',
46 | 'hint'=>__('Please enter the code from your Authenticator app'),
47 | 'configuration'=>array(
48 | 'size'=>40, 'length'=>40,
49 | 'autocomplete' => 'one-time-code',
50 | 'inputmode' => 'numeric',
51 | 'pattern' => '[0-9]*',
52 | 'validator-error' => __('Invalid Code format'),
53 | ),
54 | )),
55 | );
56 | }
57 |
58 | function validate($form, $user) {
59 | // Make sure form is valid and token exists
60 | if (!($form->isValid()
61 | && ($clean=$form->getClean())
62 | && $clean['token']))
63 | return false;
64 |
65 | if (!$this->validateLoginCode($clean['token']))
66 | return false;
67 |
68 | // upstream validation might throw an exception due to expired token
69 | // or too many attempts (timeout). It's the responsibility of the
70 | // caller to catch and handle such exceptions.
71 | $secretKey = $this->getSecretKey();
72 | if (!$this->_validate($secretKey))
73 | return false;
74 |
75 | // Validator doesn't do house cleaning - it's our responsibility
76 | $this->onValidate($user);
77 |
78 | return true;
79 | }
80 |
81 | function send($user) {
82 | global $cfg;
83 |
84 | // Get backend configuration for this user
85 | if (!$cfg || !($info = $user->get2FAConfig($this->getId())))
86 | return false;
87 |
88 | // get configuration
89 | $config = $info['config'];
90 |
91 | // Generate Secret Key
92 | if (!$this->secretKey)
93 | $this->secretKey = $this->getSecretKey($user);
94 |
95 | $this->store($this->secretKey);
96 |
97 | return true;
98 | }
99 |
100 | function store($secretKey) {
101 | global $thisstaff;
102 |
103 | $store = &$_SESSION['_2fa'][$this->getId()];
104 | $store = ['otp' => $secretKey, 'time' => time(), 'strikes' => 0];
105 |
106 | if ($thisstaff) {
107 | $config = array('config' => array('key' => $secretKey, 'external2fa' => true));
108 | $_config = new Config('staff.'.$thisstaff->getId());
109 | $_config->set($this->getId(), JsonDataEncoder::encode($config));
110 | $thisstaff->_config = $_config->getInfo();
111 | $errors['err'] = '';
112 | }
113 |
114 | return $store;
115 | }
116 |
117 | function validateLoginCode($code) {
118 | $auth2FA = new \Sonata\GoogleAuthenticator\GoogleAuthenticator();
119 | $secretKey = $this->getSecretKey();
120 |
121 | return $auth2FA->checkCode($secretKey, $code);
122 | }
123 |
124 | function getSecretKey($staff=false) {
125 | if (!$staff) {
126 | $s = StaffAuthenticationBackend::getUser();
127 | $staff = Staff::lookup($s->getId());
128 | }
129 |
130 | if (!$token = ConfigItem::getConfigsByNamespace('staff.'.$staff->getId(), static::$id)) {
131 | $auth2FA = new \Sonata\GoogleAuthenticator\GoogleAuthenticator();
132 | $this->secretKey = $auth2FA->generateSecret();
133 | $this->store($this->secretKey);
134 | }
135 |
136 | $key = $token->value ?: $this->secretKey;
137 | if (strpos($key, 'config')) {
138 | $key = json_decode($key, true);
139 | $key = $key['config']['key'];
140 | }
141 |
142 | return $key;
143 | }
144 |
145 | function getQRCode($staff=false) {
146 | $staffEmail = $staff->getEmail();
147 | $secretKey = $this->getSecretKey($staff);
148 | $title = preg_replace('/[^A-Za-z0-9]/', '', self::$custom_issuer ?: __('osTicket'));
149 |
150 | return \Sonata\GoogleAuthenticator\GoogleQrUrl::generate($staffEmail, $secretKey, $title);
151 | }
152 |
153 | function validateQRCode($staff=false) {
154 | $auth2FA = new \Sonata\GoogleAuthenticator\GoogleAuthenticator();
155 | $secretKey = $this->getSecretKey($staff);
156 | $code = self::getCode();
157 |
158 | return $auth2FA->checkCode($secretKey, $code);
159 | }
160 |
161 | static function getCode() {
162 | $auth2FA = new \Sonata\GoogleAuthenticator\GoogleAuthenticator();
163 | $self = new Auth2FABackend();
164 | $secretKey = $self->getSecretKey();
165 |
166 | return $auth2FA->getCode($secretKey);
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/auth-2fa/config.php:
--------------------------------------------------------------------------------
1 | new TextboxField(array(
23 | 'label' => __('Issuer'),
24 | 'required' => false,
25 | 'configuration' => array('size'=>40),
26 | 'hint' => __('Customize the Issuer shown in your Authenticator app after scanning a QR Code.'),
27 | )),
28 | );
29 | }
30 |
31 | function pre_save(&$config, &$errors) {
32 | global $msg;
33 | if (!$errors)
34 | $msg = __('Configuration updated successfully');
35 | return true;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/auth-2fa/plugin.php:
--------------------------------------------------------------------------------
1 | '2fa:auth', # notrans
5 | 'version' => '0.3',
6 | 'name' => /* trans */ 'Two Factor Authenticator',
7 | 'author' => 'Adriane Alexander',
8 | 'description' => /* trans */ 'Provides 2 Factor Authentication
9 | using an Authenticator App',
10 | 'url' => 'https://www.osticket.com/download',
11 | 'plugin' => 'auth2fa.php:Auth2FAPlugin',
12 | 'requires' => array(
13 | "sonata-project/google-authenticator" => array(
14 | "version" => "*",
15 | "map" => array(
16 | "sonata-project/google-authenticator/src" => 'lib/Sonata/GoogleAuthenticator',
17 | )
18 | ),
19 | ),
20 | );
21 | ?>
22 |
--------------------------------------------------------------------------------
/auth-cas/authentication.php:
--------------------------------------------------------------------------------
1 | getConfig();
11 |
12 | $enabled = $config->get('cas-enabled');
13 | if (in_array($enabled, array('all', 'staff'))) {
14 | require_once('cas.php');
15 | StaffAuthenticationBackend::register(
16 | new CasStaffAuthBackend($this->getConfig()));
17 | }
18 | if (in_array($enabled, array('all', 'client'))) {
19 | require_once('cas.php');
20 | UserAuthenticationBackend::register(
21 | new CasClientAuthBackend($this->getConfig()));
22 | }
23 | }
24 | }
25 |
26 | require_once(INCLUDE_DIR.'UniversalClassLoader.php');
27 | use Symfony\Component\ClassLoader\UniversalClassLoader_osTicket;
28 | $loader = new UniversalClassLoader_osTicket();
29 | $loader->registerNamespaceFallbacks(array(
30 | dirname(__file__).'/lib'));
31 | $loader->register();
32 |
--------------------------------------------------------------------------------
/auth-cas/cas.php:
--------------------------------------------------------------------------------
1 | config = $config;
11 | }
12 |
13 | function triggerAuth() {
14 | $self = $this;
15 | phpCAS::client(
16 | CAS_VERSION_2_0,
17 | $this->config->get('cas-hostname'),
18 | intval($this->config->get('cas-port')),
19 | $this->config->get('cas-context')
20 | );
21 | if($this->config->get('cas-ca-cert-path')) {
22 | phpCAS::setCasServerCACert($this->config->get('cas-ca-cert-path'));
23 | } else {
24 | phpCAS::setNoCasServerValidation();
25 | }
26 | if(!phpCAS::isAuthenticated()) {
27 | phpCAS::forceAuthentication();
28 | } else {
29 | $this->setUser();
30 | $this->setEmail();
31 | $this->setName();
32 | }
33 | }
34 |
35 | function setUser() {
36 | $_SESSION[':cas']['user'] = phpCAS::getUser();
37 | }
38 |
39 | function getUser() {
40 | return $_SESSION[':cas']['user'];
41 | }
42 |
43 | function setEmail() {
44 | if($this->config->get('cas-email-attribute-key') !== null
45 | && phpCAS::hasAttribute($this->config->get('cas-email-attribute-key'))) {
46 | $_SESSION[':cas']['email'] = phpCAS::getAttribute(
47 | $this->config->get('cas-email-attribute-key'));
48 | } else {
49 | $email = $this->getUser();
50 | if($this->config->get('cas-at-domain') !== null) {
51 | $email .= $this->config->get('cas-at-domain');
52 | }
53 | $_SESSION[':cas']['email'] = $email;
54 | }
55 | }
56 |
57 | function getEmail() {
58 | return $_SESSION[':cas']['email'];
59 | }
60 |
61 | function setName() {
62 | if($this->config->get('cas-name-attribute-key') !== null
63 | && phpCAS::hasAttribute($this->config->get('cas-name-attribute-key'))) {
64 | $_SESSION[':cas']['name'] = phpCAS::getAttribute(
65 | $this->config->get('cas-name-attribute-key'));
66 | } else {
67 | $_SESSION[':cas']['name'] = $this->getUser();
68 | }
69 | }
70 |
71 | function getName() {
72 | return $_SESSION[':cas']['name'];
73 | }
74 |
75 | function getProfile() {
76 | return array(
77 | 'email' => $this->getEmail(),
78 | 'name' => $this->getName()
79 | );
80 | }
81 | }
82 |
83 | class CasStaffAuthBackend extends ExternalStaffAuthenticationBackend {
84 | static $id = "cas";
85 | static $name = /* trans */ "CAS";
86 |
87 | static $service_name = "CAS";
88 |
89 | var $config;
90 |
91 | function __construct($config) {
92 | $this->config = $config;
93 | $this->cas = new CasAuth($config);
94 | }
95 |
96 | function getName() {
97 | $config = $this->config;
98 | list($__, $_N) = $config::translate();
99 | return $__(static::$name);
100 | }
101 |
102 | function signOn() {
103 | if (isset($_SESSION[':cas']['user'])) {
104 | $staff = new StaffSession($this->cas->getEmail());
105 | if ($staff && $staff->getId()) {
106 | return $staff;
107 | } else {
108 | $_SESSION['_staff']['auth']['msg'] = 'Have your administrator create a local account';
109 | }
110 | }
111 | }
112 |
113 | static function signOut($user) {
114 | parent::signOut($user);
115 | unset($_SESSION[':cas']);
116 | }
117 |
118 |
119 | function triggerAuth() {
120 | parent::triggerAuth();
121 | $cas = $this->cas->triggerAuth();
122 | Http::redirect(ROOT_PATH . 'scp/');
123 | }
124 | }
125 |
126 | class CasClientAuthBackend extends ExternalUserAuthenticationBackend {
127 | static $id = "cas.client";
128 | static $name = /* trans */ "CAS";
129 |
130 | static $service_name = "CAS";
131 |
132 | function __construct($config) {
133 | $this->config = $config;
134 | $this->cas = new CasAuth($config);
135 | }
136 |
137 | function getName() {
138 | $config = $this->config;
139 | list($__, $_N) = $config::translate();
140 | return $__(static::$name);
141 | }
142 |
143 | function supportsInteractiveAuthentication() {
144 | return false;
145 | }
146 |
147 | function signOn() {
148 | if (isset($_SESSION[':cas']['user'])) {
149 | $acct = ClientAccount::lookupByUsername($this->cas->getEmail());
150 | $client = null;
151 | if ($acct && $acct->getId()) {
152 | $client = new ClientSession(new EndUser($acct->getUser()));
153 | }
154 |
155 | if ($client) {
156 | return $client;
157 | } else {
158 | return new ClientCreateRequest(
159 | $this, $this->cas->getEmail(), $this->cas->getProfile());
160 | }
161 | }
162 | }
163 |
164 | static function signOut($user) {
165 | parent::signOut($user);
166 | unset($_SESSION[':cas']);
167 | }
168 |
169 | function triggerAuth() {
170 | parent::triggerAuth();
171 | $cas = $this->cas->triggerAuth();
172 | Http::redirect(ROOT_PATH . 'login.php');
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/auth-cas/config.php:
--------------------------------------------------------------------------------
1 | $__('Authentication'),
23 | 'default' => "disabled",
24 | 'choices' => array(
25 | 'disabled' => $__('Disabled'),
26 | 'staff' => $__('Agents Only'),
27 | 'client' => $__('Clients Only'),
28 | 'all' => $__('Agents and Clients'),
29 | ),
30 | ));
31 | return array(
32 | 'cas' => new SectionBreakField(array(
33 | 'label' => $__('CAS Authentication'),
34 | )),
35 | 'cas-hostname' => new TextboxField(array(
36 | 'label' => $__('CAS Server Hostname'),
37 | 'configuration' => array('size'=>60, 'length'=>100),
38 | )),
39 | 'cas-port' => new TextboxField(array(
40 | 'label' => $__('CAS Server Port'),
41 | 'configuration' => array('size'=>10, 'length'=>8),
42 | )),
43 | 'cas-context' => new TextboxField(array(
44 | 'label' => $__('CAS Server Context'),
45 | 'configuration' => array('size'=>60, 'length'=>100),
46 | 'hint' => $__('This value is "/cas" for most installs.'),
47 | )),
48 | 'cas-ca-cert-path' => new TextboxField(array(
49 | 'label' => $__('CAS CA Cert Path'),
50 | 'configuration' => array('size'=>60, 'length'=>100),
51 | )),
52 | 'cas-at-domain' => new TextboxField(array(
53 | 'label' => $__('CAS e-mail suffix'),
54 | 'configuration' => array('size'=>60, 'length'=>100),
55 | 'hint' => $__('Use this field if your CAS server does not
56 | report an e-mail attribute. ex: "@domain.tld"'),
57 | )),
58 | 'cas-name-attribute-key' => new TextboxField(array(
59 | 'label' => $__('CAS name attribute key'),
60 | 'configuration' => array('size'=>60, 'length'=>100),
61 | )),
62 | 'cas-email-attribute-key' => new TextboxField(array(
63 | 'label' => $__('CAS email attribute key'),
64 | 'configuration' => array('size'=>60, 'length'=>100),
65 | )),
66 | 'cas-enabled' => clone $modes,
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/auth-cas/plugin.php:
--------------------------------------------------------------------------------
1 | 'auth:cas', # notrans
4 | 'version' => '0.2',
5 | 'name' => /* trans */ 'JASIG CAS Authentication',
6 | 'author' => 'Kevin O\'Connor',
7 | 'description' => /* trans */ 'Provides a configurable authentication
8 | backend for authenticating staff and clients using CAS.',
9 | 'url' => 'http://www.osticket.com/plugins/auth/cas',
10 | 'plugin' => 'authentication.php:CasAuthPlugin',
11 | 'requires' => array(
12 | "jasig/phpcas" => array(
13 | "version" => "1.3.3",
14 | "map" => array(
15 | "jasig/phpcas/source" => 'lib/jasig/phpcas',
16 | )
17 | ),
18 | ),
19 | );
20 |
21 | ?>
22 |
--------------------------------------------------------------------------------
/auth-ldap/authentication.php:
--------------------------------------------------------------------------------
1 | array(
30 | 'user' => array(
31 | 'filter' => '(objectClass=user)',
32 | 'base' => 'CN=Users',
33 | 'first' => 'givenName',
34 | 'last' => 'sn',
35 | 'full' => 'displayName',
36 | 'email' => 'mail',
37 | 'phone' => 'telephoneNumber',
38 | 'mobile' => false,
39 | 'username' => 'sAMAccountName',
40 | 'dn' => '{username}@{domain}',
41 | 'search' => '(&(objectCategory=person)(objectClass=user)(|(sAMAccountName={q}*)(firstName={q}*)(lastName={q}*)(displayName={q}*)))',
42 | 'lookup' => '(&(objectCategory=person)(objectClass=user)({attr}={q}))',
43 | ),
44 | 'group' => array(
45 | 'ismember' => '(&(objectClass=user)(sAMAccountName={username})
46 | (|(memberOf={distinguishedName})(primaryGroupId={primaryGroupToken})))',
47 | 'lookup' => '(&(objectClass=group)(sAMAccountName={groupname}))',
48 | ),
49 | ),
50 | // A general approach for RFC-2307
51 | '2307' => array(
52 | 'user' => array(
53 | 'filter' => '(objectClass=inetOrgPerson)',
54 | 'first' => 'gn',
55 | 'last' => 'sn',
56 | 'full' => array('displayName', 'gecos', 'cn'),
57 | 'email' => 'mail',
58 | 'phone' => 'telephoneNumber',
59 | 'mobile' => 'mobileTelephoneNumber',
60 | 'username' => 'uid',
61 | 'dn' => 'uid={username},{search_base}',
62 | 'search' => '(&(objectClass=inetOrgPerson)(|(uid={q}*)(displayName={q}*)(cn={q}*)))',
63 | 'lookup' => '(&(objectClass=inetOrgPerson)({attr}={q}))',
64 | ),
65 | ),
66 | );
67 |
68 | var $config;
69 | var $type = 'staff';
70 |
71 | function __construct($config, $type='staff') {
72 | $this->config = $config;
73 | $this->type = $type;
74 | }
75 | function getConfig() {
76 | return $this->config;
77 | }
78 |
79 | static function autodiscover($domain, $dns=array()) {
80 | require_once(PEAR_DIR.'Net/DNS2.php');
81 | // TODO: Lookup DNS server from hosts file if not set
82 | $q = new Net_DNS2_Resolver();
83 | if ($dns)
84 | $q->setServers($dns);
85 |
86 | $servers = array();
87 | try {
88 | $r = $q->query('_ldap._tcp.'.$domain, 'SRV');
89 | } catch (Net_DNS2_Exception $e) {
90 | // TODO: Log warning or something
91 | return $servers;
92 | }
93 | foreach ($r->answer as $srv) {
94 | // TODO: Get the actual IP of the server (?)
95 | $servers[] = array(
96 | 'host' => "{$srv->target}:{$srv->port}",
97 | 'priority' => $srv->priority,
98 | 'weight' => $srv->weight,
99 | );
100 | }
101 | // Sort servers by priority ASC, then weight DESC
102 | usort($servers, function($a, $b) {
103 | return ($a['priority'] << 15) - $a['weight']
104 | - ($b['priority'] << 15) + $b['weight'];
105 | });
106 | return $servers;
107 | }
108 |
109 | function getServers() {
110 | if (!($servers = $this->getConfig()->get('servers'))
111 | || !($servers = preg_split('/\s+/', $servers))) {
112 | if ($domain = $this->getConfig()->get('domain')) {
113 | $dns = preg_split('/,?\s+/', $this->getConfig()->get('dns'));
114 | return self::autodiscover($domain, array_filter($dns));
115 | }
116 | }
117 | if ($servers) {
118 | $hosts = array();
119 | foreach ($servers as $h)
120 | if (preg_match('/((ldaps?:\/\/)?([^:]+)):(\d{1,4})/', $h, $matches))
121 | $hosts[] = array('host' => $matches[1], 'port' => (int) $matches[4]);
122 | else
123 | $hosts[] = array('host' => $h);
124 | return $hosts;
125 | }
126 | }
127 |
128 | function getConnection($force_reconnect=false) {
129 | static $connection = null;
130 |
131 | if ($connection && !$force_reconnect)
132 | return $connection;
133 |
134 | require_once('include/Net/LDAP2.php');
135 | // Set reasonable timeout limits
136 | $defaults = array(
137 | 'options' => array(
138 | 'LDAP_OPT_TIMELIMIT' => 5,
139 | 'LDAP_OPT_NETWORK_TIMEOUT' => 5,
140 | )
141 | );
142 | if ($this->getConfig()->get('tls'))
143 | $defaults['starttls'] = true;
144 | if ($this->getConfig()->get('schema') == 'msad') {
145 | // Special options for Active Directory (2000+) servers
146 | //$defaults['starttls'] = true;
147 | $defaults['options'] += array(
148 | 'LDAP_OPT_PROTOCOL_VERSION' => 3,
149 | 'LDAP_OPT_REFERRALS' => 0,
150 | );
151 | // Active Directory servers almost always use self-signed certs
152 | putenv('LDAPTLS_REQCERT=never');
153 | }
154 |
155 | foreach ($this->getServers() as $s) {
156 | $params = $defaults + $s;
157 | $c = new Net_LDAP2($params);
158 | $r = $c->bind();
159 | if (!PEAR::isError($r)) {
160 | $connection = $c;
161 | return $c;
162 | }
163 | }
164 | }
165 |
166 | /**
167 | * Binds to the directory under the search-user credentials configured
168 | */
169 | function _bind($connection) {
170 | if ($dn = $this->getConfig()->get('bind_dn')) {
171 | $pw = Crypto::decrypt($this->getConfig()->get('bind_pw'),
172 | SECRET_SALT, $this->getConfig()->getNamespace());
173 | $r = $connection->bind($dn, $pw);
174 | unset($pw);
175 | return !PEAR::isError($r);
176 | }
177 | else {
178 | // try anonymous bind
179 | $r = $connection->bind();
180 | return !PEAR::isError($r);
181 | }
182 | }
183 |
184 | function authenticate($username, $password=null) {
185 | // Thanks, http://stackoverflow.com/a/764651
186 | // Binding with an empty password implies an anonymous bind which
187 | // will likely be successful and incorrect
188 | if (!$password)
189 | return null;
190 |
191 | $c = $this->getConnection();
192 | $config = $this->getConfig();
193 | $schema_type = $this->getSchema($c);
194 | $schema = static::$schemas[$schema_type]['user'];
195 | $domain = false;
196 | if ($schema_type == 'msad') {
197 | // Allow username specification of DOMAIN\user, LDAP already
198 | // allows user@domain
199 | if (strpos($username, '\\') !== false)
200 | list($domain, $username) = explode('\\', $username);
201 | else
202 | $domain = $config->get('domain');
203 | }
204 | // Create the DN string for the bind based on the directory schema
205 | $dn = preg_replace_callback(':\{([^}]+)\}:',
206 | function($match) use ($username, $domain, $config) {
207 | switch ($match[1]) {
208 | case 'username':
209 | return $username;
210 | case 'domain':
211 | return $domain;
212 | case 'search_base':
213 | if (!$config->get('search_base'))
214 | return 'dc=' . implode(',dc=',
215 | explode('.', $config->get('domain')));
216 | // Fall through to default
217 | default:
218 | return $config->get($match[1]);
219 | }
220 | },
221 | $schema['dn']
222 | );
223 | $r = $c->bind($dn, $password);
224 | if (!PEAR::isError($r))
225 | return $this->lookupAndSync($username, $dn);
226 |
227 | // Another effort is to search for the user
228 | if (!$this->_bind($c))
229 | return null;
230 |
231 | $r = $c->search(
232 | $this->getSearchBase(),
233 | str_replace(
234 | array('{attr}','{q}'),
235 | // Assume email address if the $username contains an @ sign
236 | array(strpos($username, '@') ? $schema['email'] : $schema['username'],
237 | $username),
238 | $schema['lookup']),
239 | array('sizelimit' => 1)
240 | );
241 | if (PEAR::isError($r) || !$r->count() || !$r->current())
242 | return null;
243 |
244 | // Attempt to bind as the DN of the user looked up with the password
245 | // specified
246 | $bound = $c->bind($r->current()->dn(), $password);
247 | if (PEAR::isError($bound))
248 | return null;
249 |
250 | // TODO: Save the DN in the config table so a lookup isn't necessary
251 | // in the future
252 | return $this->lookupAndSync($username, $r->current()->dn());
253 | }
254 |
255 | /**
256 | * Retrieve currently configured LDAP schema, perhaps by inspecting the
257 | * server's advertised DSE information
258 | */
259 | function getSchema($connection) {
260 | $schema = $this->getConfig()->get('schema');
261 | if (!$schema || $schema == 'auto') {
262 | $dse = $connection->rootDse(array('supportedCapabilities'));
263 | // Microsoft Active Directory
264 | // http://www.alvestrand.no/objectid/1.2.840.113556.1.4.800.html
265 | if (($caps = $dse->getValue('supportedCapabilities'))
266 | && in_array('1.2.840.113556.1.4.800', $caps)) {
267 | $this->getConfig()->set('schema', 'msad');
268 | return 'msad';
269 | }
270 | }
271 | elseif ($schema)
272 | return $schema;
273 |
274 | // Fallback
275 | return '2307';
276 | }
277 |
278 | function lookup($lookup_dn, $bind=true) {
279 | $c = $this->getConnection();
280 | if ($bind && !$this->_bind($c))
281 | return null;
282 |
283 | $schema = static::$schemas[$this->getSchema($c)];
284 | $schema = $schema['user'];
285 | $opts = array(
286 | 'scope' => 'base',
287 | 'sizelimit' => 1,
288 | 'attributes' => array_filter(flatten(array(
289 | $schema['first'], $schema['last'], $schema['full'],
290 | $schema['phone'], $schema['mobile'], $schema['email'],
291 | $schema['username'],
292 | )))
293 | );
294 | $r = $c->search($lookup_dn, '(objectClass=*)', $opts);
295 | if (PEAR::isError($r) || !$r->count())
296 | return null;
297 |
298 | return $this->_getUserInfoArray($r->current(), $schema);
299 | }
300 |
301 | function search($query) {
302 | $c = $this->getConnection();
303 | // TODO: Include bind information
304 | $users = array();
305 | if (!$this->_bind($c))
306 | return $users;
307 |
308 | $schema = static::$schemas[$this->getSchema($c)];
309 | $schema = $schema['user'];
310 | $r = $c->search(
311 | $this->getSearchBase(),
312 | str_replace('{q}', $query, $schema['search']),
313 | array('attributes' => array_filter(flatten(array(
314 | $schema['first'], $schema['last'], $schema['full'],
315 | $schema['phone'], $schema['mobile'], $schema['email'],
316 | $schema['username'], 'dn',
317 | ))))
318 | );
319 | // XXX: Log or return some kind of error?
320 | if (PEAR::isError($r))
321 | return $users;
322 |
323 | foreach ($r as $e)
324 | $users[] = $this->_getUserInfoArray($e, $schema);
325 | return $users;
326 | }
327 |
328 | function getSearchBase() {
329 | $base = $this->getConfig()->get('search_base');
330 | if (!$base && ($domain=$this->getConfig()->get('domain')))
331 | $base = 'dc='.str_replace('.', ',dc=', $domain);
332 | return $base;
333 | }
334 |
335 | function _getValue($entry, $names) {
336 | foreach (array_filter(splat($names)) as $n)
337 | // Support multi-value attributes
338 | foreach (splat($entry->getValue($n, 'all')) as $val)
339 | // Return the first non-bool-false value of the entries
340 | if ($val)
341 | return $val;
342 | }
343 |
344 | function _getUserInfoArray($e, $schema) {
345 | // Detect first and last name if only full name is given
346 | if (!($first = $this->_getValue($e, $schema['first']))
347 | || !($last = $this->_getValue($e, $schema['last']))) {
348 | $name = new PersonsName($this->_getValue($e, $schema['full']));
349 | $first = $name->getFirst();
350 | $last = $name->getLast();
351 | }
352 | else
353 | $name = "$first $last";
354 |
355 | return array(
356 | 'username' => $this->_getValue($e, $schema['username']),
357 | 'first' => $first,
358 | 'last' => $last,
359 | 'name' => $name,
360 | 'email' => $this->_getValue($e, $schema['email']),
361 | 'phone' => $this->_getValue($e, $schema['phone']),
362 | 'mobile' => $this->_getValue($e, $schema['mobile']),
363 | 'dn' => $e->dn(),
364 | );
365 | }
366 |
367 | function lookupAndSync($username, $dn) {
368 | switch ($this->type) {
369 | case 'staff':
370 | if (($user = StaffSession::lookup($username)) && $user->getId()) {
371 | if (!$user instanceof StaffSession) {
372 | // osTicket <= v1.9.7 or so
373 | $user = new StaffSession($user->getId());
374 | }
375 | return $user;
376 | }
377 | break;
378 | case 'client':
379 | $c = $this->getConnection();
380 | if ('msad' == $this->getSchema($c) && stripos($dn, ',dc=') === false) {
381 | // The user login DN will be user@domain. We need an LDAP DN
382 | // -- fetch the real DN which looks like `CN=blah,DC=`
383 | // NOTE: Already bound, so no need to bind again
384 | list($samid) = explode('@', $dn);
385 | $r = $c->search(
386 | $this->getSearchBase(),
387 | sprintf('(|(userPrincipalName=%s)(samAccountName=%s))', $dn, $samid),
388 | $opts);
389 | if (!PEAR::isError($r) && $r->count() && $r->current())
390 | $dn = $r->current()->dn();
391 | }
392 |
393 | // Lookup all the information on the user. Try to get the email
394 | // addresss as well as the username when looking up the user
395 | // locally.
396 | if (!($info = $this->lookup($dn, false)))
397 | return;
398 |
399 | $acct = false;
400 | foreach (array($username, $info['username'], $info['email']) as $name) {
401 | if ($name && ($acct = ClientAccount::lookupByUsername($name)))
402 | break;
403 | }
404 | if (!$acct)
405 | return new ClientCreateRequest($this, $username, $info);
406 |
407 | if (($client = new ClientSession(new EndUser($acct->getUser())))
408 | && !$client->getId())
409 | return;
410 |
411 | return $client;
412 | }
413 |
414 | // TODO: Auto-create users, etc.
415 | }
416 | }
417 |
418 | class StaffLDAPAuthentication extends StaffAuthenticationBackend
419 | implements AuthDirectorySearch {
420 |
421 | static $name = /* trans */ "Active Directory or LDAP";
422 | static $id = "ldap";
423 |
424 | function __construct($config) {
425 | $this->_ldap = new LDAPAuthentication($config);
426 | $this->config = $config;
427 | }
428 |
429 | function authenticate($username, $password=false, $errors=array()) {
430 | return $this->_ldap->authenticate($username, $password);
431 | }
432 |
433 | function getName() {
434 | $config = $this->config;
435 | list($__, $_N) = $config->translate();
436 | return $config->getName() ?: $__(static::$name);
437 | }
438 |
439 | function lookup($dn) {
440 | $hit = $this->_ldap->lookup($dn);
441 | if ($hit) {
442 | $hit['backend'] = static::$id;
443 | $hit['id'] = $this->getBkId() . ':' . $hit['dn'];
444 | }
445 | return $hit;
446 | }
447 |
448 | function search($query) {
449 | if (strlen($query) < 3)
450 | return array();
451 |
452 | $hits = $this->_ldap->search($query);
453 | foreach ($hits as &$h) {
454 | $h['backend'] = static::$id;
455 | $h['id'] = $this->getBkId() . ':' . $h['dn'];
456 | }
457 | return $hits;
458 | }
459 | }
460 |
461 | class ClientLDAPAuthentication extends UserAuthenticationBackend {
462 | static $name = /* trans */ "Active Directory or LDAP";
463 | static $id = "ldap.client";
464 |
465 | function __construct($config) {
466 | $this->_ldap = new LDAPAuthentication($config, 'client');
467 | $this->config = $config;
468 | if ($domain = $config->get('domain'))
469 | self::$name .= sprintf(' (%s)', $domain);
470 | }
471 |
472 | function getName() {
473 | $config = $this->config;
474 | list($__, $_N) = $config->translate();
475 | return $config->getName() ?: $__(static::$name);
476 | }
477 |
478 | function authenticate($username, $password=false, $errors=array()) {
479 | $object = $this->_ldap->authenticate($username, $password);
480 | if ($object instanceof ClientCreateRequest)
481 | $object->setBackend($this);
482 | return $object;
483 | }
484 | }
485 |
486 | require_once(INCLUDE_DIR.'class.plugin.php');
487 | require_once('config.php');
488 | class LdapAuthPlugin extends Plugin {
489 | var $config_class = 'LdapConfig';
490 |
491 | function bootstrap() {
492 | $config = $this->getConfig();
493 | if ($config->get('auth-staff'))
494 | StaffAuthenticationBackend::register(new StaffLDAPAuthentication($config));
495 | if ($config->get('auth-client'))
496 | UserAuthenticationBackend::register(new ClientLDAPAuthentication($config));
497 | }
498 | }
499 |
--------------------------------------------------------------------------------
/auth-ldap/config.php:
--------------------------------------------------------------------------------
1 | new SectionBreakField(array(
25 | 'label' => 'Microsoft® Active Directory',
26 | 'hint' => $__('This section should be all that is required for Active Directory domains'),
27 | )),
28 | 'domain' => new TextboxField(array(
29 | 'label' => $__('Default Domain'),
30 | 'hint' => $__('Default domain used in authentication and searches'),
31 | 'configuration' => array('size'=>40, 'length'=>60),
32 | 'validators' => array(
33 | function($self, $val) use ($__) {
34 | if (strpos($val, '.') === false)
35 | $self->addError(
36 | $__('Fully-qualified domain name is expected'));
37 | }),
38 | )),
39 | 'dns' => new TextboxField(array(
40 | 'label' => $__('DNS Servers'),
41 | 'hint' => $__('(optional) DNS servers to query about AD servers.
42 | Useful if the AD server is not on the same network as
43 | this web server or does not have its DNS configured to
44 | point to the AD servers'),
45 | 'configuration' => array('size'=>40),
46 | 'validators' => array(
47 | function($self, $val) use ($__) {
48 | if (!$val) return;
49 | $servers = explode(',', $val);
50 | foreach ($servers as $s) {
51 | if (!Validator::is_ip(trim($s)))
52 | $self->addError(sprintf(
53 | $__('%s: Expected an IP address'), $s));
54 | }
55 | }),
56 | )),
57 |
58 | 'ldap' => new SectionBreakField(array(
59 | 'label' => $__('Generic configuration for LDAP'),
60 | 'hint' => $__('Not necessary if Active Directory is configured above'),
61 | )),
62 | 'servers' => new TextareaField(array(
63 | 'id' => 'servers',
64 | 'label' => $__('LDAP servers'),
65 | 'configuration' => array('html'=>false, 'rows'=>2, 'cols'=>40),
66 | 'hint' => $__('Use "server" or "server:port". Place one server entry per line'),
67 | )),
68 | 'tls' => new BooleanField(array(
69 | 'id' => 'tls',
70 | 'label' => $__('Use TLS'),
71 | 'configuration' => array(
72 | 'desc' => $__('Use TLS to communicate with the LDAP server'))
73 | )),
74 |
75 | 'conn_info' => new SectionBreakField(array(
76 | 'label' => $__('Connection Information'),
77 | 'hint' => $__('Useful only for information lookups. Not
78 | necessary for authentication. NOTE that this data is not
79 | necessary if your server allows anonymous searches')
80 | )),
81 | 'bind_dn' => new TextboxField(array(
82 | 'label' => $__('Search User'),
83 | 'hint' => $__('Bind DN (distinguished name) to bind to the LDAP
84 | server as in order to perform searches'),
85 | 'configuration' => array('size'=>40, 'length'=>120),
86 | )),
87 | 'bind_pw' => new TextboxField(array(
88 | 'widget' => 'PasswordWidget',
89 | 'label' => $__('Password'),
90 | 'validator' => 'noop',
91 | 'hint' => $__("Password associated with the DN's account"),
92 | 'configuration' => array('size'=>40),
93 | )),
94 | 'search_base' => new TextboxField(array(
95 | 'label' => $__('Search Base'),
96 | 'hint' => $__('Used when searching for users'),
97 | 'configuration' => array('size'=>70, 'length'=>120),
98 | )),
99 | 'schema' => new ChoiceField(array(
100 | 'label' => $__('LDAP Schema'),
101 | 'hint' => $__('Layout of the user data in the LDAP server'),
102 | 'default' => 'auto',
103 | 'choices' => array(
104 | 'auto' => '— '.$__('Automatically Detect').' —',
105 | 'msad' => 'Microsoft® Active Directory',
106 | '2307' => 'Posix Account (rfc 2307)',
107 | ),
108 | )),
109 |
110 | 'auth' => new SectionBreakField(array(
111 | 'label' => $__('Authentication Modes'),
112 | 'hint' => $__('Authentication modes for clients and staff
113 | members can be enabled independently'),
114 | )),
115 | 'auth-staff' => new BooleanField(array(
116 | 'label' => $__('Staff Authentication'),
117 | 'default' => true,
118 | 'configuration' => array(
119 | 'desc' => $__('Enable authentication of staff members')
120 | )
121 | )),
122 | 'auth-client' => new BooleanField(array(
123 | 'label' => $__('Client Authentication'),
124 | 'default' => false,
125 | 'configuration' => array(
126 | 'desc' => $__('Enable authentication of clients')
127 | )
128 | )),
129 | );
130 | }
131 |
132 | function pre_save(&$config, &$errors) {
133 | require_once('include/Net/LDAP2.php');
134 | list($__, $_N) = self::translate();
135 |
136 | global $ost;
137 | if ($ost && !extension_loaded('ldap')) {
138 | $ost->setWarning($__('LDAP extension is not available'));
139 | $errors['err'] = $__('LDAP extension is not available. Please
140 | install or enable the `php-ldap` extension on your web
141 | server');
142 | return;
143 | }
144 |
145 | if ($config['domain'] && !$config['servers']) {
146 | if (!($servers = LDAPAuthentication::autodiscover($config['domain'],
147 | preg_split('/,?\s+/', $config['dns']))))
148 | $this->getForm()->getField('servers')->addError(
149 | $__("Unable to find LDAP servers for this domain. Try giving
150 | an address of one of the DNS servers or manually specify
151 | the LDAP servers for this domain below."));
152 | }
153 | else {
154 | if (!$config['servers'])
155 | $this->getForm()->getField('servers')->addError(
156 | $__("No servers specified. Either specify a Active Directory
157 | domain or a list of servers"));
158 | else {
159 | $servers = array();
160 | foreach (preg_split('/\s+/', $config['servers']) as $host)
161 | if (preg_match('/((ldaps?:\/\/)?([^:]+)):(\d{1,4})/', $host, $matches))
162 | $servers[] = array('host' => $matches[1], 'port' => (int) $matches[4]);
163 | else
164 | $servers[] = array('host' => $host);
165 | }
166 | }
167 | $connection_error = false;
168 | foreach ($servers as $info) {
169 | // Assume MSAD
170 | $info['options']['LDAP_OPT_REFERRALS'] = 0;
171 | if ($config['tls']) {
172 | $info['starttls'] = true;
173 | // Don't require a certificate here
174 | putenv('LDAPTLS_REQCERT=never');
175 | }
176 | if ($config['bind_dn']) {
177 | $info['binddn'] = $config['bind_dn'];
178 | $info['bindpw'] = $config['bind_pw']
179 | ? $config['bind_pw']
180 | : Crypto::decrypt($this->get('bind_pw'), SECRET_SALT,
181 | $this->getNamespace());
182 | }
183 | // Set reasonable timeouts so we dont exceed max_execution_time
184 | $info['options'] = array(
185 | 'LDAP_OPT_TIMELIMIT' => 5,
186 | 'LDAP_OPT_NETWORK_TIMEOUT' => 5,
187 | );
188 | $c = new Net_LDAP2($info);
189 | $r = $c->bind();
190 | if (PEAR::isError($r)) {
191 | if (false === strpos($config['bind_dn'], '@')
192 | && false === strpos($config['bind_dn'], ',dc=')) {
193 | // Assume Active Directory, add the default domain in
194 | $config['bind_dn'] .= '@' . $config['domain'];
195 | $info['bind_dn'] = $config['bind_dn'];
196 | $c = new Net_LDAP2($info);
197 | $r = $c->bind();
198 | }
199 | }
200 | if (PEAR::isError($r)) {
201 | $connection_error = sprintf($__(
202 | '%s: Unable to bind to server %s'),
203 | $r->getMessage(), $info['host']);
204 | }
205 | else {
206 | $connection_error = false;
207 | break;
208 | }
209 | }
210 | if ($connection_error) {
211 | $this->getForm()->getField('servers')->addError($connection_error);
212 | $errors['err'] = $__('Unable to connect any listed LDAP servers');
213 | }
214 |
215 | if (!$errors && $config['bind_pw'])
216 | $config['bind_pw'] = Crypto::encrypt($config['bind_pw'],
217 | SECRET_SALT, $this->getNamespace());
218 | else
219 | $config['bind_pw'] = $this->get('bind_pw');
220 |
221 | global $msg;
222 | if (!$errors)
223 | $msg = $__('LDAP configuration updated successfully');
224 |
225 | return !$errors;
226 | }
227 | }
228 |
229 | ?>
230 |
--------------------------------------------------------------------------------
/auth-ldap/plugin.php:
--------------------------------------------------------------------------------
1 | 'auth:ldap', # notrans
5 | 'version' => '0.6.2',
6 | 'name' => /* trans */ 'LDAP Authentication and Lookup',
7 | 'author' => 'Jared Hancock',
8 | 'description' => /* trans */ 'Provides a configurable authentication backend
9 | which works against Microsoft Active Directory and OpenLdap
10 | servers',
11 | 'url' => 'http://www.osticket.com/plugins/auth/ldap',
12 | 'plugin' => 'authentication.php:LdapAuthPlugin',
13 | 'map' => array(
14 | 'pear-pear.php.net/net_ldap2' => 'include'
15 | ),
16 | );
17 |
18 | ?>
19 |
--------------------------------------------------------------------------------
/auth-oauth2/auth.php:
--------------------------------------------------------------------------------
1 | append(
29 | url_get("$url", function () use($id) {
30 | $id = $id ?: $_SESSION['ext:bk:id'];
31 | if (isset($_GET['code'])
32 | && isset($_GET['state'])
33 | && ($bk=self::getAuthBackend($id))) {
34 | $bk->callback($_GET, $id);
35 | }
36 | // Authentication failed or downstream failed to redirect user.
37 | Http::redirect(ROOT_PATH);
38 | })
39 | );
40 | });
41 | }
42 |
43 | private static function registerAuthBackends(PluginConfig $config) {
44 |
45 | $target = $config->get('auth_target') ?: 'none';
46 | if (in_array($target, array('all', 'agents'))) {
47 | StaffAuthenticationBackend::register(
48 | new OAuth2StaffAuthBackend($config));
49 | }
50 | if (in_array($target, array('all', 'users'))) {
51 | UserAuthenticationBackend::register(
52 | new OAuth2UserAuthBackend($config));
53 | }
54 | }
55 |
56 | private static function getAuthBackend($id) {
57 | // Authentication backends
58 | $bk = AuthenticationBackend::lookupBackend($id);
59 | if ($bk instanceof OAuth2AuthBackend)
60 | return $bk;
61 | // OAuth2 Authorization backends
62 | if (($bk=OAuth2AuthorizationBackend::getBackend($id)))
63 | return $bk;
64 | // OAuth2 Authentication backends
65 | if (($bk=OAuth2AuthenticationBackend::getBackend($id)))
66 | return $bk;
67 | }
68 |
69 | public function getNewInstanceOptions() {
70 | $newOptions = [];
71 | foreach (OAuth2AuthenticationBackend::allRegistered() as $bk) {
72 | $newOptions[] = [
73 | 'name' => $bk::$name,
74 | 'href' => sprintf('plugins.php?id=%d&provider=%s&a=add-instance#instances',
75 | $this->getId(), $bk::$id),
76 | 'class' => '',
77 | 'icon' => $bk::$icon,
78 | ];
79 | }
80 | return $newOptions;
81 | }
82 |
83 | public function getNewInstanceDefaults($options) {
84 | $defaults = ['auth_type' => 'auth'];
85 | if (isset($options['provider'])
86 | && ($id=$options['provider'])
87 | && (($bk=OAuth2AuthenticationBackend::getBackend($id))))
88 | $defaults += $bk->getDefaults();
89 |
90 | return $defaults;
91 | }
92 |
93 | public function init() {
94 | // Register API Endpoint
95 | self::registerEndpoint();
96 | // Register Oauth2 Authorization Providers
97 | OAuth2ProviderBackend::registerProviders([
98 | 'plugin_id' => $this->getId()]);
99 | }
100 |
101 | public function bootstrap() {
102 | // Get sideloaded instance config - this is neccessary for backwards
103 | // compatibility before multi-instance support
104 | $config = $this->getConfig();
105 | // Only register Authentication backends Authorization Backends are
106 | // done on-demand via Email Account interface
107 | if ($config && $config->isAuthen())
108 | self::registerAuthBackends($config);
109 | }
110 | }
111 |
112 |
--------------------------------------------------------------------------------
/auth-oauth2/config.php:
--------------------------------------------------------------------------------
1 | get('auth_type', 'auth');
9 | }
10 |
11 | public function isAutho() {
12 | return ($this->getAuthType()
13 | && !strcasecmp($this->getAuthType(), 'autho'));
14 | }
15 |
16 | public function isAuthen() {
17 | return !$this->isAutho();
18 | }
19 |
20 | public function getName() {
21 | return $this->get('auth_name');
22 | }
23 |
24 | public function getServiceName() {
25 | return $this->get('auth_service');
26 | }
27 |
28 | public function getClientId() {
29 | return $this->get('clientId');
30 | }
31 |
32 | public function getClientSecret() {
33 | return $this->get('clientSecret');
34 | }
35 |
36 | public function getScopes($key='scopes') {
37 | return array_filter(array_map('trim',
38 | explode(',', $this->get($key, []))));
39 | }
40 |
41 | public function getAuthorizationUrl() {
42 | return $this->get('urlAuthorize');
43 | }
44 |
45 | public function getAccessTokenUrl() {
46 | return $this->get('urlAccessToken');
47 | }
48 |
49 | public function getRedirectUri() {
50 | return $this->get('redirectUri');
51 | }
52 |
53 | public function getResourceOwnerDetailstUrl() {
54 | return $this->get('urlResourceOwnerDetails');
55 | }
56 |
57 | public function getAttributeFor($name, $default=null) {
58 | return $this->get("attr_$name", $default);
59 | }
60 |
61 | public function getClientSettings() {
62 | $scopes = $this->getScopes();
63 | $settings = [
64 | 'clientId' => $this->getClientId(),
65 | 'clientSecret' => $this->getClientSecret(),
66 | 'redirectUri' => $this->getRedirectUri(),
67 | 'urlAuthorize' => $this->getAuthorizationUrl(),
68 | 'urlAccessToken' => $this->getAccessTokenUrl(),
69 | 'urlResourceOwnerDetails' => $this->getResourceOwnerDetailstUrl(),
70 | 'scopes' => $scopes,
71 | ];
72 |
73 | // Use comma separator when we have more than one scopes - this is
74 | // because scopes string is comma exploded.
75 | if ($scopes && count($scopes) > 1)
76 | $settings['scopeSeparator'] = ',';
77 |
78 | return $settings;
79 | }
80 |
81 | function translate() {
82 | return Plugin::translate('auth-oauth2');
83 | }
84 |
85 | function getAllOptions() {
86 | list($__, $_N) = self::translate();
87 | return array(
88 | 'auth_settings' => new SectionBreakField(array(
89 | 'label' => $__('Settings'),
90 | 'hint' => $__('General settings'),
91 | )),
92 | 'auth_name' => new TextboxField(array(
93 | 'label' => $__('Name'),
94 | 'hint' => $__('IdP Name e.g Google'),
95 | 'required' => true,
96 | 'configuration' => array(
97 | 'size' => 34,
98 | 'length' => 125
99 | )
100 | )
101 | ),
102 | 'auth_target' => new ChoiceField(array(
103 | 'label' => $__('Authentication Target'),
104 | 'hint' => $__('Target Audience'),
105 | 'required' => true,
106 | 'choices' => array(
107 | 'none' => $__('None (Disabled)'),
108 | 'agents' => $__('Agents Only'),
109 | 'users' => $__('End Users Only'),
110 | 'all' => $__('Agents and End Users'),
111 | ),
112 | 'default' => 'none',
113 | 'visibility' => new VisibilityConstraint(
114 | new Q(array('auth_type__eq' => 'auth')),
115 | VisibilityConstraint::HIDDEN
116 | ),
117 | )
118 | ),
119 | 'auth_service' => new TextboxField(array(
120 | 'label' => $__('Authentication Label'),
121 | 'hint' => $__('Sign in With label'),
122 | 'required' => true,
123 | 'configuration' => array(
124 | 'size' => 34,
125 | 'length' => 64
126 | ),
127 | 'visibility' => new VisibilityConstraint(
128 | new Q(array('auth_type__eq' => 'auth')),
129 | VisibilityConstraint::HIDDEN
130 | ),
131 | )
132 | ),
133 | 'idp' => new SectionBreakField(array(
134 | 'label' => $__('OAuth2 Provider (IdP) Details'),
135 | 'hint' => $__('Authorization instances can be added via Email Account interface'),
136 | )),
137 | 'auth_type' => new ChoiceField(array(
138 | 'label' => $__('Type'),
139 | 'hint' => $__('OAuth2 Client Type'),
140 | 'required' => true,
141 | 'choices' => array(
142 | 'auth' => $__('Authentication'),
143 | 'autho' => $__('Authorization'),
144 | ),
145 | 'configuration' => array(
146 | 'disabled' => true,
147 | ),
148 | 'default' => $this->getAuthType(),
149 | )
150 | ),
151 | 'redirectUri' => new TextboxField(
152 | array(
153 | 'label' => $__('Redirect URI'),
154 | 'hint' => $__('Callback Endpoint'),
155 | 'required' => true,
156 | 'configuration' => array(
157 | 'size' => 64,
158 | 'length' => 0
159 | ),
160 | 'validators' => function($f, $v) {
161 | if (!preg_match('[\.*(/api/auth/oauth2)$]isu', $v))
162 | $f->addError(__('Must be a valid API endpont'));
163 | },
164 | 'default' => OAuth2Plugin::callback_url(),
165 | )
166 | ),
167 | 'clientId' => new TextboxField(
168 | array(
169 | 'label' => $__('Client Id'),
170 | 'hint' => $__('Client Identifier (Id)'),
171 | 'required' => true,
172 | 'configuration' => array(
173 | 'size' => 64,
174 | 'length' => 0,
175 | 'placeholder' => $__('Client Id')
176 | )
177 | )
178 | ),
179 | 'clientSecret' => new PasswordField(
180 | array(
181 | 'widget' => 'PasswordWidget',
182 | 'label' => $__('Client Secret'),
183 | 'hint' => $__('Client Secret'),
184 | 'required' => !$this->getClientSecret(),
185 | 'validator' => 'noop',
186 | 'configuration' => array(
187 | 'size' => 64,
188 | 'length' => 0,
189 | 'key' => $this->getNamespace(),
190 | 'placeholder' => $this->getClientSecret()
191 | ? str_repeat('•', strlen($this->getClientSecret()))
192 | : $__('Client Secret'),
193 | )
194 | )
195 | ),
196 | 'urlAuthorize' => new TextboxField(
197 | array(
198 | 'label' => $__('Authorization Endpoint'),
199 | 'hint' => $__('Authorization URL'),
200 | 'required' => true,
201 | 'configuration' => array(
202 | 'size' => 64,
203 | 'length' => 0
204 | ),
205 | 'default' => '',
206 | )
207 | ),
208 | 'urlAccessToken' => new TextboxField(
209 | array(
210 | 'label' => $__('Token Endpoint'),
211 | 'hint' => $__('Access Token URL'),
212 | 'required' => true,
213 | 'configuration' => array(
214 | 'size' => 64,
215 | 'length' => 0
216 | ),
217 | 'default' => '',
218 | )
219 | ),
220 | 'urlResourceOwnerDetails' => new TextboxField(
221 | array(
222 | 'label' => $__('Resource Details Endpoint'),
223 | 'hint' => $__('User Details URL'),
224 | 'required' => true,
225 | 'configuration' => array(
226 | 'size' => 64,
227 | 'length' => 0
228 | ),
229 | 'default' => '',
230 | )
231 | ),
232 | 'scopes' => new TextboxField(
233 | array(
234 | 'label' => $__('Scopes'),
235 | 'hint' => $__('Comma or Space separated scopes depending on IdP requirements'),
236 | 'required' => true,
237 | 'configuration' => array(
238 | 'size' => 64,
239 | 'length' => 0
240 | ),
241 | )
242 | ),
243 | 'attr_mapping' => new SectionBreakField(array(
244 | 'label' => $__('User Attributes Mapping'),
245 | 'hint' => $__('Consult your IdP documentation for supported attributes and scope'),
246 | )),
247 | 'attr_username' => new TextboxField(array(
248 | 'label' => $__('User Identifier'),
249 | 'hint' => $__('Unique User Identifier - Username or Email address'),
250 | 'required' => true,
251 | 'default' => 'email',
252 | 'configuration' => array(
253 | 'size' => 64,
254 | 'length' => 0
255 | ),
256 | 'visibility' => new VisibilityConstraint(
257 | new Q(array('auth_type__eq' => 'auth')),
258 | VisibilityConstraint::HIDDEN
259 | ),
260 | )),
261 | 'attr_givenname' => new TextboxField(array(
262 | 'label' => $__('Given Name'),
263 | 'hint' => $__('First name'),
264 | 'default' => 'givenname',
265 | 'configuration' => array(
266 | 'size' => 64,
267 | 'length' => 0
268 | ),
269 | 'visibility' => new VisibilityConstraint(
270 | new Q(array('auth_type__eq' => 'auth')),
271 | VisibilityConstraint::HIDDEN
272 | ),
273 |
274 | )),
275 | 'attr_surname' => new TextboxField(array(
276 | 'label' => $__('Surname'),
277 | 'hint' => $__('Last name'),
278 | 'default' => 'surname',
279 | 'configuration' => array(
280 | 'size' => 64,
281 | 'length' => 0
282 | ),
283 | 'visibility' => new VisibilityConstraint(
284 | new Q(array('auth_type__eq' => 'auth')),
285 | VisibilityConstraint::HIDDEN
286 | ),
287 | )),
288 | 'attr_email' => new TextboxField(array(
289 | 'label' => $__('Email Address'),
290 | 'hint' => $__('Email address required to auto-create User accounts. Agents must already exist.'),
291 | 'default' => 'email',
292 | 'configuration' => array(
293 | 'size' => 64,
294 | 'length' => 0
295 | ),
296 | )),
297 | );
298 | }
299 |
300 | function getOptions() {
301 | return $this->getAllOptions();
302 | }
303 |
304 | function getFields() {
305 | list($__, $_N) = self::translate();
306 | switch ($this->getAuthType()) {
307 | case 'autho':
308 | // Authorization fields
309 | $base = array_flip(['idp', 'auth_type', 'redirectUri', 'clientId', 'clientSecret',
310 | 'urlAuthorize', 'urlAccessToken',
311 | 'urlResourceOwnerDetails', 'scopes', 'attr_email',
312 | ]);
313 | $fields = array_merge($base, array_intersect_key(
314 | $this->getAllOptions(), $base));
315 | $fields['attr_email'] = new TextboxField([
316 | 'label' => $__('Email Address Attribute'),
317 | 'hint' => $__('Please consult your provider docs for the correct attribute to use'),
318 | 'required' => true,
319 | ]);
320 | break;
321 | case 'auth':
322 | default:
323 | $fields = $this->getOptions();
324 | break;
325 | }
326 | return $fields;
327 | }
328 |
329 | function pre_save(&$config, &$errors) {
330 | list($__, $_N) = self::translate();
331 | // Authorization instances can only be managed via Email Account
332 | // interface at the moment.
333 | if ($this->isAutho())
334 | $errors['err'] = $__('Authorization instances can only be managed via Email Account interface at the moment');
335 | return !count($errors);
336 | }
337 |
338 | public function getFormOptions() {
339 | list($__, $_N) = self::translate();
340 | return [
341 | 'notice' => $this->isAutho()
342 | ? $__('Authorization instances can only be updated via Email Account interface')
343 | : ($this->getClientId()
344 | ? $__('Be careful - changes might break Authentication of the Target Audience')
345 | : ''
346 | ),
347 | ];
348 | }
349 | }
350 |
351 | class OAuth2EmailConfig extends OAuth2Config {
352 |
353 | public function getAuthType() {
354 | return $this->get('auth_type', 'autho');
355 | }
356 |
357 | // Notices are handled at Email Account level
358 | public function getFormOptions() {
359 | return [];
360 | }
361 |
362 | // This is necessay so the parent can reject updates on Autho instances via plugins
363 | // interface which doesn't have re-authorization capabilities at the
364 | // moment.
365 | function pre_save(&$config, &$errors) {
366 | return true;
367 | }
368 |
369 | function getFields() {
370 | // Remove fields not needed on the Email interface
371 | return array_diff_key(parent::getFields(),
372 | array_flip(['idp', 'auth_type'])
373 | );
374 | }
375 | }
376 |
377 | class OAuth2MicrosoftEmailConfig extends OAuth2EmailConfig {
378 |
379 | function getFields() {
380 | list($__, $_N) = self::translate();
381 | $fields = parent::getFields();
382 | // Add Outlook Mail Scopes field after access token endpoint
383 | $pos = array_search('urlAccessToken', array_keys($fields), true) + 1;
384 | return array_slice($fields, 0, $pos, true) + [
385 | // Outlook Mail Scopes without resource scopes
386 | 'scopes' => new TextboxField([
387 | 'label' => $__('Outlook Scopes'),
388 | 'hint' => $__('Space separated Outlook Scopes for desired services'),
389 | 'required' => true, // Required!
390 | 'configuration' => [
391 | 'size' => 64,
392 | 'length' => 0
393 | ],
394 | ]
395 | ),
396 | ];
397 | }
398 | }
399 |
--------------------------------------------------------------------------------
/auth-oauth2/plugin.php:
--------------------------------------------------------------------------------
1 | 'auth:oath2', # notrans
4 | 'version' => '0.6',
5 | 'ost_version' => '1.17', # Require osTicket v1.17+
6 | 'name' => /* trans */ 'Oauth2 Client',
7 | 'author' => 'Peter Rotich ',
8 | 'description' => /* trans */ 'Provides a configurable Oauth2 authentication and authorization backends. backends.',
9 | 'url' => 'http://www.osticket.com/',
10 | 'plugin' => 'auth.php:OAuth2Plugin',
11 | 'requires' => array(
12 | "league/oauth2-client" => array(
13 | "version" => "*",
14 | "map" => array(
15 | "league/oauth2-client/src" => 'lib/League/OAuth2/Client',
16 | 'guzzlehttp/guzzle/src' => 'lib/GuzzleHttp',
17 | 'guzzlehttp/psr7/src' => 'lib/GuzzleHttp/Psr7',
18 | 'guzzlehttp/promises/src' => 'lib/GuzzleHttp/Promise',
19 | 'psr/http-client/src' => 'lib/Psr/Http/Client',
20 | 'psr/http-factory/src' => 'lib/Psr/Http/Factory',
21 | 'psr/http-message/src' => 'lib/Psr/Http/Message',
22 |
23 | )
24 | ),
25 | ),
26 | );
27 | ?>
28 |
--------------------------------------------------------------------------------
/auth-passthru/authenticate.php:
--------------------------------------------------------------------------------
1 | getId()) {
30 | if (!$user instanceof StaffSession) {
31 | // osTicket <= v1.9.7 or so
32 | $user = new StaffSession($user->getId());
33 | }
34 | return $user;
35 | }
36 |
37 | // TODO: Consider client sessions
38 | }
39 | }
40 | }
41 |
42 | class UserHttpAuthentication extends UserAuthenticationBackend {
43 | static $name = "HTTP Authentication";
44 | static $id = "passthru.client";
45 |
46 | function supportsInteractiveAuthentication() {
47 | return false;
48 | }
49 |
50 | function signOn() {
51 | if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER']))
52 | // User was authenticated by the HTTP server
53 | $username = $_SERVER['REMOTE_USER'];
54 | elseif (isset($_SERVER['REDIRECT_REMOTE_USER'])
55 | && !empty($_SERVER['REDIRECT_REMOTE_USER']))
56 | $username = $_SERVER['REDIRECT_REMOTE_USER'];
57 |
58 | if ($username) {
59 | // Support ActiveDirectory domain specification with either
60 | // "user@domain" or "domain\user" formats
61 | if (strpos($username, '@') !== false)
62 | list($username, $domain) = explode('@', $username, 2);
63 | elseif (strpos($username, '\\') !== false)
64 | list($domain, $username) = explode('\\', $username, 2);
65 | $username = trim(strtolower($username));
66 |
67 | if ($acct = ClientAccount::lookupByUsername($username)) {
68 | if (($client = new ClientSession(new EndUser($acct->getUser())))
69 | && $client->getId())
70 | return $client;
71 | }
72 | else {
73 | // No such account. Attempt a lookup on the username
74 | $users = parent::searchUsers($username);
75 | if (!is_array($users))
76 | return;
77 |
78 | foreach ($users as $u) {
79 | if (0 === strcasecmp($u['username'], $username)
80 | || 0 === strcasecmp($u['email'], $username))
81 | // User information matches HTTP username
82 | return new ClientCreateRequest($this, $username, $u);
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | require_once(INCLUDE_DIR.'class.plugin.php');
90 | require_once('config.php');
91 | class PassthruAuthPlugin extends Plugin {
92 | var $config_class = 'PassthruAuthConfig';
93 |
94 | function bootstrap() {
95 | $config = $this->getConfig();
96 | if ($config->get('auth-staff'))
97 | StaffAuthenticationBackend::register('HttpAuthentication');
98 | if ($config->get('auth-client'))
99 | UserAuthenticationBackend::register('UserHttpAuthentication');
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/auth-passthru/config.php:
--------------------------------------------------------------------------------
1 | new SectionBreakField(array(
22 | 'label' => $__('Authentication Modes'),
23 | 'hint' => $__('Authentication modes for clients and staff
24 | members can be enabled independently. Client discovery
25 | can be supported via a separate backend (such as LDAP)'),
26 | )),
27 | 'auth-staff' => new BooleanField(array(
28 | 'label' => $__('Staff Authentication'),
29 | 'default' => true,
30 | 'configuration' => array(
31 | 'desc' => $__('Enable authentication of staff members')
32 | )
33 | )),
34 | 'auth-client' => new BooleanField(array(
35 | 'label' => $__('Client Authentication'),
36 | 'default' => false,
37 | 'configuration' => array(
38 | 'desc' => $__('Enable authentication and discovery of clients')
39 | )
40 | )),
41 | );
42 | }
43 |
44 | function pre_save(&$config, &$errors) {
45 | global $msg;
46 |
47 | list($__, $_N) = self::translate();
48 | if (!$errors)
49 | $msg = $__('Configuration updated successfully');
50 |
51 | return true;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/auth-passthru/plugin.php:
--------------------------------------------------------------------------------
1 | 'auth:passthru', # notrans
5 | 'version' => '0.2',
6 | 'name' => /* trans */ 'HTTP Passthru Authentication',
7 | 'author' => 'Jared Hancock',
8 | 'description' => /* trans */ 'Allows for the HTTP server (Apache or IIS) to perform
9 | the authentication of the user. osTicket will match the username from the
10 | server authentication to a username defined internally',
11 | 'url' => 'http://www.osticket.com/plugins/auth/passthru',
12 | 'plugin' => 'authenticate.php:PassthruAuthPlugin'
13 | );
14 |
15 | ?>
16 |
--------------------------------------------------------------------------------
/auth-password-policy/auth.php:
--------------------------------------------------------------------------------
1 | new TextboxField(array(
9 | 'required' => true,
10 | 'label' => __('Minimum length'),
11 | 'configuration' => array(
12 | 'validator' => 'regex',
13 | 'regex' => '/(^[1-9]|^[1-9][0-9]|^1[0-1][0-9]|^12[0-8])$/',
14 | 'validator-error' => sprintf('%s %s', __('Minimum'),
15 | __('length must be between 1 and 128')),
16 | 'size' => 4,
17 | ),
18 | 'default' => 8,
19 | 'hint' => __('Minimum characters required'),
20 | )),
21 | 'maxlength' => new TextboxField(array(
22 | 'required' => true,
23 | 'label' => __('Maximum length'),
24 | 'configuration' => array(
25 | 'validator' => 'regex',
26 | 'regex' => '/(^[1-9]|^[1-9][0-9]|^1[0-1][0-9]|^12[0-8])$/',
27 | 'validator-error' => sprintf('%s %s', __('Maximum'),
28 | __('length must be between 1 and 128')),
29 | 'size' => 4,
30 | ),
31 | 'default' => 128,
32 | 'hint' => __('Minimum characters required'),
33 | )),
34 | // Classes of characters
35 | 'classes' => new ChoiceField(array(
36 | 'required' => true,
37 | 'label' => __('Character classes required'),
38 | 'choices' => array(
39 | '2' => sprintf('%s (2)', __('Two')),
40 | '3' => sprintf('%s (3)', __('Three')),
41 | '4' => sprintf('%s (4)', __('Four')),
42 | ),
43 | 'default' => 3,
44 | 'hint' => __('Require this number of character classes: upper, lower, number, and special characters'),
45 | )),
46 | // Entropy
47 | 'entropy' => new ChoiceField(array(
48 | 'required' => false,
49 | 'label' => __('Password strength'),
50 | 'choices' => array(
51 | '' => __('Disable'),
52 | '32' => sprintf('%s (32 bits)', __('Weak')),
53 | '56' => sprintf('%s (56 bits)', __('Good')),
54 | '80' => sprintf('%s (80 bits)', __('Strong')),
55 | '108' => sprintf('%s (108 bits)', __('Awesome')),
56 | ),
57 | 'default' => '',
58 | 'hint' => sprintf('%s %s',
59 | __('Enforce minimum password entropy.'),
60 | __('See the wikipedia page for password strength for more reading on entropy')),
61 | )),
62 | // Enforcement
63 | 'onlogin' => new BooleanField(array(
64 | 'required' => false,
65 | 'label' => __('Enforce on login'),
66 | 'default' => false,
67 | 'configuration'=>array(
68 | 'desc' => __('Enforce password policies on login')
69 | ),
70 | 'hint' => __('Enforce password policies the next time a user login.')
71 | )),
72 | // Reuse
73 | 'reuse' => new BooleanField(array(
74 | 'required' => false,
75 | 'label' => __('Password reuse'),
76 | 'default' => false,
77 | 'configuration'=>array(
78 | 'desc' => __('Allow reuse')
79 | ),
80 | 'hint' => __('Allow password reuse')
81 | )),
82 | // Expiration
83 | 'expires' => new ChoiceField(array(
84 | 'required' => false,
85 | 'label' => __('Password expiration'),
86 | 'choices' => array(
87 | '' => __('Never expires'),
88 | '30' => __('30 days'),
89 | '60' => __('60 days'),
90 | '90' => __('90 days'),
91 | '180' => __('180 days'),
92 | '365' => __('365 days'),
93 | ),
94 | 'default' => '',
95 | 'hint' => __('Password reset frequency')
96 | )),
97 | );
98 | }
99 |
100 | function pre_save(&$config, &$errors) {
101 | if ($config['length'] >= $config['maxlength']) {
102 | $this->getForm()->getField('length')->addError(
103 | __("Minimum length must be smaller than Maximum length"));
104 | $errors['err'] = __('Unable to update the Instance');
105 | }
106 |
107 | global $msg;
108 | if (!$errors)
109 | $msg = __('Instance updated successfully');
110 |
111 | return !$errors;
112 | }
113 | }
114 |
115 | class PasswordManagementPolicy
116 | extends PasswordPolicy {
117 | var $config;
118 | static $id = 'ppp';
119 | static $name = /* @trans */ "Password Management Plugin";
120 |
121 | function __construct($config) {
122 | $this->config = $config;
123 | }
124 |
125 | function onLogin($user, $password) {
126 | if (is_a($user, 'RegisteredUser'))
127 | return;
128 |
129 | // Check password length and strength
130 | if ($this->config->get('onlogin'))
131 | $this->processPassword($password);
132 |
133 | // Check password expiration
134 | if ($this->config->get('expires')
135 | && ($time = $user->getPasswdResetTimestamp())
136 | && ($time < (time()-($this->config->get('expires')*86400))))
137 | throw new ExpiredPassword(__('Expired Password'));
138 | }
139 |
140 | function onSet($password, $current=false) {
141 | return $this->processPassword($password, $current);
142 | }
143 |
144 | private function processPassword($password, $current=false) {
145 |
146 | // Current vs. new password
147 | if ($current
148 | && !$this->config->get('reuse')
149 | && 0 === strcasecmp($passwd, $current)) {
150 | throw new BadPassword(
151 | __('New password MUST be different from the current password!'));
152 | }
153 |
154 | // Password length
155 | $pwdlen = mb_strlen($password);
156 | if ($pwdlen < $this->config->get('length')) {
157 | throw new BadPassword(
158 | sprintf(__('Password is too short — must be %d characters'),
159 | $this->config->get('length'))
160 | );
161 | } elseif ($pwdlen > $this->config->get('maxlength')) {
162 | throw new BadPassword(
163 | sprintf(__('Password is too long — must be a maximum of %d characters'),
164 | $this->config->get('maxlength'))
165 | );
166 | }
167 |
168 | // Class of characters
169 | if ($this->config->get('classes')) {
170 | if (preg_match('/\p{Ll}/u', $password))
171 | $classes++;
172 | if (preg_match('/\p{Lu}/u', $password))
173 | $classes++;
174 | if (preg_match('/\p{N}/u', $password))
175 | $classes++;
176 | if (preg_match('/[\pP\pS\pZ]/u', $password))
177 | $classes++;
178 |
179 | if ($classes < $this->config->get('classes'))
180 | throw new BadPassword(sprintf('%s %s',
181 | __('Password does not meet complexity requirements.'),
182 | __('Add upper, lower case letters, number, and symbols')
183 | ));
184 | }
185 |
186 | // Password strength
187 | if ($this->config->get('entropy')) {
188 | // Calculate total possible char count
189 | if (preg_match('/[a-z]/', $password))
190 | $chars += 26;
191 | if (preg_match('/[A-Z]/', $password))
192 | $chars += 26;
193 | if (preg_match('/[0-9]/', $password))
194 | $chars += 10;
195 | if (preg_match('/[!@#$%^&*()]/', $password))
196 | $chars += 10;
197 | if (preg_match('/ /', $password))
198 | $chars += 1;
199 | if (preg_match('@[`~_=+[{\]}\\|;:\'",<.>/?-]@', $password))
200 | $chars += 20;
201 | // High ASCII / UTF-8
202 | if (preg_match('/[\x80-\xff]/', $password))
203 | $chars += 128;
204 |
205 | $entropy = strlen($password) * log($chars) / log(2);
206 |
207 | if ($entropy < $this->config->get('entropy'))
208 | throw new BadPassword(sprintf('%s %s %s',
209 | __('Password is not complex enough.'),
210 | __('Try a longer one or use upper case letters, number,and symbols.'),
211 | sprintf(__('Score: %d of %d'), $entropy,
212 | $this->config->get('entropy'))
213 | ));
214 | }
215 | }
216 | }
217 |
218 | class PasswordManagementPlugin
219 | extends Plugin {
220 | var $config_class = 'PasswordManagementConfig';
221 |
222 | function bootstrap() {
223 | PasswordPolicy::register(new PasswordManagementPolicy($this->getConfig()));
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/auth-password-policy/plugin.php:
--------------------------------------------------------------------------------
1 | 'auth:password-policy', # notrans
5 | 'version' => '0.1',
6 | 'name' => 'Password Management Policies',
7 | 'author' => 'Jared Hancock, Peter Rotich',
8 | 'description' => 'Aggrivate your users with password management policies!',
9 | 'url' => 'http://www.osticket.com/plugins/auth/password-policy',
10 | 'plugin' => 'auth.php:PasswordManagementPlugin'
11 | );
12 |
13 | ?>
14 |
--------------------------------------------------------------------------------
/doc/auth-oauth.md:
--------------------------------------------------------------------------------
1 | This OAuth plugin provides SSO sign in from many popular external sources
2 | including Google+, GitHub, Facebook, Windows Azure, and many more.
3 |
4 | **At the current time, only Google+ authentication is implemented.**
5 |
6 | Google+ Authentication
7 | ----------------------
8 | To register for Google+ authentication,
9 |
10 | * Visit the Google Cloud Console (https://console.developers.google.com/)
11 | * Sign in to Google via a relevant account
12 | * Create a project (name it whatever -- osTicket Help Desk)
13 | * Manage the project, navigate to APIs and Auth / Credentials and create an
14 | OAuth Client ID
15 | * Register the key with the URL of your helpdesk plus `api/auth/ext`
16 | (`http://support.mycompany.com/api/auth/ext`). This is called the *Redirect
17 | URI*
18 | * Navigate to APIs & Auth / Consent Screen and fill in the relevant information
19 | * Navigate to APIs & Auth / APIs and add / enable the *Google+ API*
20 | * Install the plugin in osTicket **1.9**
21 | * Configure the plugin with your Google+ Client ID and Secret
22 | * Configure the plugin to authenticate agents (staff), users or both
23 | * Enable the OAuth plugin
24 | * Log out and back in with Google+
25 | * Enjoy!
26 |
--------------------------------------------------------------------------------
/doc/i18n.md:
--------------------------------------------------------------------------------
1 | Making Plugins Translatable
2 | ---------------------------
3 |
4 | The plugin base class has a `translate` static method which is used to
5 | retrieve bootstrapped functions for translations inside the plugin. Use it
6 | to translate strings inside your code:
7 |
8 | ```php
9 | class MyPluginsConfig extends PluginConfig {
10 | function getOptions() {
11 | list($__, $_N) = Plugin::translate('my-plugin');
12 | $__('This string is translatable');
13 | }
14 | }
15 | ```
16 |
17 | The `translate` method will return two functions (more may be retrieved in
18 | the future), the first is used to translate a single string. The second is
19 | used to translate plural strings. They mimic the `__()` and `_N()` functions
20 | inside the core osTicket code base.
21 |
22 | The $name and other static properties as well as content in the `plugin.php`
23 | file can also be translated. Simply add a comment immediately before the
24 | strings with the content of `trans`, and then translate it when necessary:
25 |
26 | ```php
27 | class MyPlugin extends Plugin {
28 | static $name = /* trans */ 'A super-awesome plugin that does stuff';
29 |
30 | function getName() {
31 | list($__) = self::translate('my-plugin');
32 | return $__(self::$name);
33 | }
34 | }
35 | ```
36 |
37 | This method overcomes the PHP limitation preventing static properties from
38 | being even remotely dynamic. The PO scanner will recognize the translatable
39 | string by the preceeding `trans` comment. The translated string will be
40 | available in the plugin once translated.
41 |
42 | Compiling PO files
43 | ------------------
44 |
45 | Use the `make-pot` compiler in the osTicket code base to search and compile
46 | the PO file for plugins
47 |
48 | php /path/to/osticket/setup/cli/manage.php i18n make-pot \
49 | -R auth-plugin \
50 | -D auth-plugin \
51 | > auth-plugin.po
52 |
--------------------------------------------------------------------------------
/lib/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osTicket/osTicket-plugins/67a1450f2f79cec0524385de6ad35207369f32e4/lib/.keep
--------------------------------------------------------------------------------
/lib/pear-pear.php.net/net_ldap2/Net/LDAP2/RootDSE.php:
--------------------------------------------------------------------------------
1 |
11 | * @copyright 2009 Jan Wagner
12 | * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
13 | * @version SVN: $Id$
14 | * @link http://pear.php.net/package/Net_LDAP2/
15 | */
16 |
17 | /**
18 | * Includes
19 | */
20 | require_once 'PEAR.php';
21 |
22 | /**
23 | * Getting the rootDSE entry of a LDAP server
24 | *
25 | * @category Net
26 | * @package Net_LDAP2
27 | * @author Jan Wagner
28 | * @license http://www.gnu.org/copyleft/lesser.html LGPL
29 | * @link http://pear.php.net/package/Net_LDAP22/
30 | */
31 | class Net_LDAP2_RootDSE extends PEAR
32 | {
33 | /**
34 | * @access protected
35 | * @var object Net_LDAP2_Entry
36 | **/
37 | protected $_entry;
38 |
39 | /**
40 | * Class constructor
41 | *
42 | * @param Net_LDAP2_Entry &$entry Net_LDAP2_Entry object of the RootDSE
43 | */
44 | public function __construct(&$entry)
45 | {
46 | $this->_entry = $entry;
47 | }
48 |
49 | /**
50 | * Fetches a RootDSE object from an LDAP connection
51 | *
52 | * @param Net_LDAP2 $ldap Directory from which the RootDSE should be fetched
53 | * @param array $attrs Array of attributes to search for
54 | *
55 | * @access static
56 | * @return Net_LDAP2_RootDSE|Net_LDAP2_Error
57 | */
58 | public static function fetch($ldap, $attrs = null)
59 | {
60 | if (!$ldap instanceof Net_LDAP2) {
61 | return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!");
62 | }
63 |
64 | if (is_array($attrs) && count($attrs) > 0 ) {
65 | $attributes = $attrs;
66 | } else {
67 | $attributes = array('vendorName',
68 | 'vendorVersion',
69 | 'namingContexts',
70 | 'altServer',
71 | 'supportedExtension',
72 | 'supportedControl',
73 | 'supportedSASLMechanisms',
74 | 'supportedLDAPVersion',
75 | 'subschemaSubentry' );
76 | }
77 | $result = $ldap->search('', '(objectClass=*)', array('attributes' => $attributes, 'scope' => 'base'));
78 | if (self::isError($result)) {
79 | return $result;
80 | }
81 | $entry = $result->shiftEntry();
82 | if (false === $entry) {
83 | return PEAR::raiseError('Could not fetch RootDSE entry');
84 | }
85 | $ret = new Net_LDAP2_RootDSE($entry);
86 | return $ret;
87 | }
88 |
89 | /**
90 | * Gets the requested attribute value
91 | *
92 | * Same usuage as {@link Net_LDAP2_Entry::getValue()}
93 | *
94 | * @param string $attr Attribute name
95 | * @param array $options Array of options
96 | *
97 | * @access public
98 | * @return mixed Net_LDAP2_Error object or attribute values
99 | * @see Net_LDAP2_Entry::get_value()
100 | */
101 | public function getValue($attr = '', $options = '')
102 | {
103 | return $this->_entry->get_value($attr, $options);
104 | }
105 |
106 | /**
107 | * Alias function of getValue() for perl-ldap interface
108 | *
109 | * @see getValue()
110 | * @return mixed
111 | */
112 | public function get_value()
113 | {
114 | $args = func_get_args();
115 | return call_user_func_array(array( &$this, 'getValue' ), $args);
116 | }
117 |
118 | /**
119 | * Determines if the extension is supported
120 | *
121 | * @param array $oids Array of oids to check
122 | *
123 | * @access public
124 | * @return boolean
125 | */
126 | public function supportedExtension($oids)
127 | {
128 | return $this->checkAttr($oids, 'supportedExtension');
129 | }
130 |
131 | /**
132 | * Alias function of supportedExtension() for perl-ldap interface
133 | *
134 | * @see supportedExtension()
135 | * @return boolean
136 | */
137 | public function supported_extension()
138 | {
139 | $args = func_get_args();
140 | return call_user_func_array(array( &$this, 'supportedExtension'), $args);
141 | }
142 |
143 | /**
144 | * Determines if the version is supported
145 | *
146 | * @param array $versions Versions to check
147 | *
148 | * @access public
149 | * @return boolean
150 | */
151 | public function supportedVersion($versions)
152 | {
153 | return $this->checkAttr($versions, 'supportedLDAPVersion');
154 | }
155 |
156 | /**
157 | * Alias function of supportedVersion() for perl-ldap interface
158 | *
159 | * @see supportedVersion()
160 | * @return boolean
161 | */
162 | public function supported_version()
163 | {
164 | $args = func_get_args();
165 | return call_user_func_array(array(&$this, 'supportedVersion'), $args);
166 | }
167 |
168 | /**
169 | * Determines if the control is supported
170 | *
171 | * @param array $oids Control oids to check
172 | *
173 | * @access public
174 | * @return boolean
175 | */
176 | public function supportedControl($oids)
177 | {
178 | return $this->checkAttr($oids, 'supportedControl');
179 | }
180 |
181 | /**
182 | * Alias function of supportedControl() for perl-ldap interface
183 | *
184 | * @see supportedControl()
185 | * @return boolean
186 | */
187 | public function supported_control()
188 | {
189 | $args = func_get_args();
190 | return call_user_func_array(array(&$this, 'supportedControl' ), $args);
191 | }
192 |
193 | /**
194 | * Determines if the sasl mechanism is supported
195 | *
196 | * @param array $mechlist SASL mechanisms to check
197 | *
198 | * @access public
199 | * @return boolean
200 | */
201 | public function supportedSASLMechanism($mechlist)
202 | {
203 | return $this->checkAttr($mechlist, 'supportedSASLMechanisms');
204 | }
205 |
206 | /**
207 | * Alias function of supportedSASLMechanism() for perl-ldap interface
208 | *
209 | * @see supportedSASLMechanism()
210 | * @return boolean
211 | */
212 | public function supported_sasl_mechanism()
213 | {
214 | $args = func_get_args();
215 | return call_user_func_array(array(&$this, 'supportedSASLMechanism'), $args);
216 | }
217 |
218 | /**
219 | * Checks for existance of value in attribute
220 | *
221 | * @param array $values values to check
222 | * @param string $attr attribute name
223 | *
224 | * @access protected
225 | * @return boolean
226 | */
227 | protected function checkAttr($values, $attr)
228 | {
229 | if (!is_array($values)) $values = array($values);
230 |
231 | foreach ($values as $value) {
232 | if (!@in_array($value, $this->get_value($attr, 'all'))) {
233 | return false;
234 | }
235 | }
236 | return true;
237 | }
238 | }
239 |
240 | ?>
241 |
--------------------------------------------------------------------------------
/lib/pear-pear.php.net/net_ldap2/Net/LDAP2/Schema.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Benedikt Hallinger
12 | * @copyright 2009 Jan Wagner, Benedikt Hallinger
13 | * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
14 | * @version SVN: $Id$
15 | * @link http://pear.php.net/package/Net_LDAP2/
16 | * @todo see the comment at the end of the file
17 | */
18 |
19 | /**
20 | * Includes
21 | */
22 | require_once 'PEAR.php';
23 |
24 | /**
25 | * Syntax definitions
26 | *
27 | * Please don't forget to add binary attributes to isBinary() below
28 | * to support proper value fetching from Net_LDAP2_Entry
29 | */
30 | define('NET_LDAP2_SYNTAX_BOOLEAN', '1.3.6.1.4.1.1466.115.121.1.7');
31 | define('NET_LDAP2_SYNTAX_DIRECTORY_STRING', '1.3.6.1.4.1.1466.115.121.1.15');
32 | define('NET_LDAP2_SYNTAX_DISTINGUISHED_NAME', '1.3.6.1.4.1.1466.115.121.1.12');
33 | define('NET_LDAP2_SYNTAX_INTEGER', '1.3.6.1.4.1.1466.115.121.1.27');
34 | define('NET_LDAP2_SYNTAX_JPEG', '1.3.6.1.4.1.1466.115.121.1.28');
35 | define('NET_LDAP2_SYNTAX_NUMERIC_STRING', '1.3.6.1.4.1.1466.115.121.1.36');
36 | define('NET_LDAP2_SYNTAX_OID', '1.3.6.1.4.1.1466.115.121.1.38');
37 | define('NET_LDAP2_SYNTAX_OCTET_STRING', '1.3.6.1.4.1.1466.115.121.1.40');
38 |
39 | /**
40 | * Load an LDAP Schema and provide information
41 | *
42 | * This class takes a Subschema entry, parses this information
43 | * and makes it available in an array. Most of the code has been
44 | * inspired by perl-ldap( http://perl-ldap.sourceforge.net).
45 | * You will find portions of their implementation in here.
46 | *
47 | * @category Net
48 | * @package Net_LDAP2
49 | * @author Jan Wagner
50 | * @author Benedikt Hallinger
51 | * @license http://www.gnu.org/copyleft/lesser.html LGPL
52 | * @link http://pear.php.net/package/Net_LDAP22/
53 | */
54 | class Net_LDAP2_Schema extends PEAR
55 | {
56 | /**
57 | * Map of entry types to ldap attributes of subschema entry
58 | *
59 | * @access public
60 | * @var array
61 | */
62 | public $types = array(
63 | 'attribute' => 'attributeTypes',
64 | 'ditcontentrule' => 'dITContentRules',
65 | 'ditstructurerule' => 'dITStructureRules',
66 | 'matchingrule' => 'matchingRules',
67 | 'matchingruleuse' => 'matchingRuleUse',
68 | 'nameform' => 'nameForms',
69 | 'objectclass' => 'objectClasses',
70 | 'syntax' => 'ldapSyntaxes'
71 | );
72 |
73 | /**
74 | * Array of entries belonging to this type
75 | *
76 | * @access protected
77 | * @var array
78 | */
79 | protected $_attributeTypes = array();
80 | protected $_matchingRules = array();
81 | protected $_matchingRuleUse = array();
82 | protected $_ldapSyntaxes = array();
83 | protected $_objectClasses = array();
84 | protected $_dITContentRules = array();
85 | protected $_dITStructureRules = array();
86 | protected $_nameForms = array();
87 |
88 |
89 | /**
90 | * hash of all fetched oids
91 | *
92 | * @access protected
93 | * @var array
94 | */
95 | protected $_oids = array();
96 |
97 | /**
98 | * Tells if the schema is initialized
99 | *
100 | * @access protected
101 | * @var boolean
102 | * @see parse(), get()
103 | */
104 | protected $_initialized = false;
105 |
106 |
107 | /**
108 | * Constructor of the class
109 | *
110 | * @access protected
111 | */
112 | public function __construct()
113 | {
114 | parent::__construct('Net_LDAP2_Error'); // default error class
115 | }
116 |
117 | /**
118 | * Fetch the Schema from an LDAP connection
119 | *
120 | * @param Net_LDAP2 $ldap LDAP connection
121 | * @param string $dn (optional) Subschema entry dn
122 | *
123 | * @access public
124 | * @return Net_LDAP2_Schema|NET_LDAP2_Error
125 | */
126 | public static function fetch($ldap, $dn = null)
127 | {
128 | if (!$ldap instanceof Net_LDAP2) {
129 | return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!");
130 | }
131 |
132 | $schema_o = new Net_LDAP2_Schema();
133 |
134 | if (is_null($dn)) {
135 | // get the subschema entry via root dse
136 | $dse = $ldap->rootDSE(array('subschemaSubentry'));
137 | if (false == Net_LDAP2::isError($dse)) {
138 | $base = $dse->getValue('subschemaSubentry', 'single');
139 | if (!Net_LDAP2::isError($base)) {
140 | $dn = $base;
141 | }
142 | }
143 | }
144 |
145 | // Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that incorrectly
146 | // call this entry subSchemaSubentry instead of subschemaSubentry.
147 | // Note the correct case/spelling as per RFC 2251.
148 | if (is_null($dn)) {
149 | // get the subschema entry via root dse
150 | $dse = $ldap->rootDSE(array('subSchemaSubentry'));
151 | if (false == Net_LDAP2::isError($dse)) {
152 | $base = $dse->getValue('subSchemaSubentry', 'single');
153 | if (!Net_LDAP2::isError($base)) {
154 | $dn = $base;
155 | }
156 | }
157 | }
158 |
159 | // Final fallback case where there is no subschemaSubentry attribute
160 | // in the root DSE (this is a bug for an LDAP v3 server so report this
161 | // to your LDAP vendor if you get this far).
162 | if (is_null($dn)) {
163 | $dn = 'cn=Subschema';
164 | }
165 |
166 | // fetch the subschema entry
167 | $result = $ldap->search($dn, '(objectClass=*)',
168 | array('attributes' => array_values($schema_o->types),
169 | 'scope' => 'base'));
170 | if (Net_LDAP2::isError($result)) {
171 | return PEAR::raiseError('Could not fetch Subschema entry: '.$result->getMessage());
172 | }
173 |
174 | $entry = $result->shiftEntry();
175 | if (!$entry instanceof Net_LDAP2_Entry) {
176 | if ($entry instanceof Net_LDAP2_Error) {
177 | return PEAR::raiseError('Could not fetch Subschema entry: '.$entry->getMessage());
178 | } else {
179 | return PEAR::raiseError('Could not fetch Subschema entry (search returned '.$result->count().' entries. Check parameter \'basedn\')');
180 | }
181 | }
182 |
183 | $schema_o->parse($entry);
184 | return $schema_o;
185 | }
186 |
187 | /**
188 | * Return a hash of entries for the given type
189 | *
190 | * Returns a hash of entry for the givene type. Types may be:
191 | * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules,
192 | * matchingruleuses, nameforms, syntaxes
193 | *
194 | * @param string $type Type to fetch
195 | *
196 | * @access public
197 | * @return array|Net_LDAP2_Error Array or Net_LDAP2_Error
198 | */
199 | public function &getAll($type)
200 | {
201 | $map = array('objectclasses' => &$this->_objectClasses,
202 | 'attributes' => &$this->_attributeTypes,
203 | 'ditcontentrules' => &$this->_dITContentRules,
204 | 'ditstructurerules' => &$this->_dITStructureRules,
205 | 'matchingrules' => &$this->_matchingRules,
206 | 'matchingruleuses' => &$this->_matchingRuleUse,
207 | 'nameforms' => &$this->_nameForms,
208 | 'syntaxes' => &$this->_ldapSyntaxes );
209 |
210 | $key = strtolower($type);
211 | $ret = ((key_exists($key, $map)) ? $map[$key] : PEAR::raiseError("Unknown type $type"));
212 | return $ret;
213 | }
214 |
215 | /**
216 | * Return a specific entry
217 | *
218 | * @param string $type Type of name
219 | * @param string $name Name or OID to fetch
220 | *
221 | * @access public
222 | * @return mixed Entry or Net_LDAP2_Error
223 | */
224 | public function &get($type, $name)
225 | {
226 | if ($this->_initialized) {
227 | $type = strtolower($type);
228 | if (false == key_exists($type, $this->types)) {
229 | return PEAR::raiseError("No such type $type");
230 | }
231 |
232 | $name = strtolower($name);
233 | $type_var = &$this->{'_' . $this->types[$type]};
234 |
235 | if (key_exists($name, $type_var)) {
236 | return $type_var[$name];
237 | } elseif (key_exists($name, $this->_oids) && $this->_oids[$name]['type'] == $type) {
238 | return $this->_oids[$name];
239 | } else {
240 | return PEAR::raiseError("Could not find $type $name");
241 | }
242 | } else {
243 | $return = null;
244 | return $return;
245 | }
246 | }
247 |
248 |
249 | /**
250 | * Fetches attributes that MAY be present in the given objectclass
251 | *
252 | * @param string $oc Name or OID of objectclass
253 | *
254 | * @access public
255 | * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
256 | */
257 | public function may($oc)
258 | {
259 | return $this->_getAttr($oc, 'may');
260 | }
261 |
262 | /**
263 | * Fetches attributes that MUST be present in the given objectclass
264 | *
265 | * @param string $oc Name or OID of objectclass
266 | *
267 | * @access public
268 | * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
269 | */
270 | public function must($oc)
271 | {
272 | return $this->_getAttr($oc, 'must');
273 | }
274 |
275 | /**
276 | * Fetches the given attribute from the given objectclass
277 | *
278 | * @param string $oc Name or OID of objectclass
279 | * @param string $attr Name of attribute to fetch
280 | *
281 | * @access protected
282 | * @return array|Net_LDAP2_Error The attribute or Net_LDAP2_Error
283 | */
284 | protected function _getAttr($oc, $attr)
285 | {
286 | $oc = strtolower($oc);
287 | if (key_exists($oc, $this->_objectClasses) && key_exists($attr, $this->_objectClasses[$oc])) {
288 | return $this->_objectClasses[$oc][$attr];
289 | } elseif (key_exists($oc, $this->_oids) &&
290 | $this->_oids[$oc]['type'] == 'objectclass' &&
291 | key_exists($attr, $this->_oids[$oc])) {
292 | return $this->_oids[$oc][$attr];
293 | } else {
294 | return PEAR::raiseError("Could not find $attr attributes for $oc ");
295 | }
296 | }
297 |
298 | /**
299 | * Returns the name(s) of the immediate superclass(es)
300 | *
301 | * @param string $oc Name or OID of objectclass
302 | *
303 | * @access public
304 | * @return array|Net_LDAP2_Error Array of names or Net_LDAP2_Error
305 | */
306 | public function superclass($oc)
307 | {
308 | $o = $this->get('objectclass', $oc);
309 | if (Net_LDAP2::isError($o)) {
310 | return $o;
311 | }
312 | return (key_exists('sup', $o) ? $o['sup'] : array());
313 | }
314 |
315 | /**
316 | * Parses the schema of the given Subschema entry
317 | *
318 | * @param Net_LDAP2_Entry &$entry Subschema entry
319 | *
320 | * @access public
321 | * @return void
322 | */
323 | public function parse(&$entry)
324 | {
325 | foreach ($this->types as $type => $attr) {
326 | // initialize map type to entry
327 | $type_var = '_' . $attr;
328 | $this->{$type_var} = array();
329 |
330 | // get values for this type
331 | if ($entry->exists($attr)) {
332 | $values = $entry->getValue($attr);
333 | if (is_array($values)) {
334 | foreach ($values as $value) {
335 |
336 | unset($schema_entry); // this was a real mess without it
337 |
338 | // get the schema entry
339 | $schema_entry = $this->_parse_entry($value);
340 |
341 | // set the type
342 | $schema_entry['type'] = $type;
343 |
344 | // save a ref in $_oids
345 | $this->_oids[$schema_entry['oid']] = &$schema_entry;
346 |
347 | // save refs for all names in type map
348 | $names = $schema_entry['aliases'];
349 | array_push($names, $schema_entry['name']);
350 | foreach ($names as $name) {
351 | $this->{$type_var}[strtolower($name)] = &$schema_entry;
352 | }
353 | }
354 | }
355 | }
356 | }
357 | $this->_initialized = true;
358 | }
359 |
360 | /**
361 | * Parses an attribute value into a schema entry
362 | *
363 | * @param string $value Attribute value
364 | *
365 | * @access protected
366 | * @return array|false Schema entry array or false
367 | */
368 | protected function &_parse_entry($value)
369 | {
370 | // tokens that have no value associated
371 | $noValue = array('single-value',
372 | 'obsolete',
373 | 'collective',
374 | 'no-user-modification',
375 | 'abstract',
376 | 'structural',
377 | 'auxiliary');
378 |
379 | // tokens that can have multiple values
380 | $multiValue = array('must', 'may', 'sup');
381 |
382 | $schema_entry = array('aliases' => array()); // initilization
383 |
384 | $tokens = $this->_tokenize($value); // get an array of tokens
385 |
386 | // remove surrounding brackets
387 | if ($tokens[0] == '(') array_shift($tokens);
388 | if ($tokens[count($tokens) - 1] == ')') array_pop($tokens); // -1 doesnt work on arrays :-(
389 |
390 | $schema_entry['oid'] = array_shift($tokens); // first token is the oid
391 |
392 | // cycle over the tokens until none are left
393 | while (count($tokens) > 0) {
394 | $token = strtolower(array_shift($tokens));
395 | if (in_array($token, $noValue)) {
396 | $schema_entry[$token] = 1; // single value token
397 | } else {
398 | // this one follows a string or a list if it is multivalued
399 | if (($schema_entry[$token] = array_shift($tokens)) == '(') {
400 | // this creates the list of values and cycles through the tokens
401 | // until the end of the list is reached ')'
402 | $schema_entry[$token] = array();
403 | while ($tmp = array_shift($tokens)) {
404 | if ($tmp == ')') break;
405 | if ($tmp != '$') array_push($schema_entry[$token], $tmp);
406 | }
407 | }
408 | // create a array if the value should be multivalued but was not
409 | if (in_array($token, $multiValue) && !is_array($schema_entry[$token])) {
410 | $schema_entry[$token] = array($schema_entry[$token]);
411 | }
412 | }
413 | }
414 | // get max length from syntax
415 | if (key_exists('syntax', $schema_entry)) {
416 | if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) {
417 | $schema_entry['max_length'] = $matches[1];
418 | }
419 | }
420 | // force a name
421 | if (empty($schema_entry['name'])) {
422 | $schema_entry['name'] = $schema_entry['oid'];
423 | }
424 | // make one name the default and put the other ones into aliases
425 | if (is_array($schema_entry['name'])) {
426 | $aliases = $schema_entry['name'];
427 | $schema_entry['name'] = array_shift($aliases);
428 | $schema_entry['aliases'] = $aliases;
429 | }
430 | return $schema_entry;
431 | }
432 |
433 | /**
434 | * Tokenizes the given value into an array of tokens
435 | *
436 | * @param string $value String to parse
437 | *
438 | * @access protected
439 | * @return array Array of tokens
440 | */
441 | protected function _tokenize($value)
442 | {
443 | $tokens = array(); // array of tokens
444 | $matches = array(); // matches[0] full pattern match, [1,2,3] subpatterns
445 |
446 | // this one is taken from perl-ldap, modified for php
447 | $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x";
448 |
449 | /**
450 | * This one matches one big pattern wherin only one of the three subpatterns matched
451 | * We are interested in the subpatterns that matched. If it matched its value will be
452 | * non-empty and so it is a token. Tokens may be round brackets, a string, or a string
453 | * enclosed by '
454 | */
455 | preg_match_all($pattern, $value, $matches);
456 |
457 | for ($i = 0; $i < count($matches[0]); $i++) { // number of tokens (full pattern match)
458 | for ($j = 1; $j < 4; $j++) { // each subpattern
459 | if (null != trim($matches[$j][$i])) { // pattern match in this subpattern
460 | $tokens[$i] = trim($matches[$j][$i]); // this is the token
461 | }
462 | }
463 | }
464 | return $tokens;
465 | }
466 |
467 | /**
468 | * Returns wether a attribute syntax is binary or not
469 | *
470 | * This method gets used by Net_LDAP2_Entry to decide which
471 | * PHP function needs to be used to fetch the value in the
472 | * proper format (e.g. binary or string)
473 | *
474 | * @param string $attribute The name of the attribute (eg.: 'sn')
475 | *
476 | * @access public
477 | * @return boolean
478 | */
479 | public function isBinary($attribute)
480 | {
481 | $return = false; // default to false
482 |
483 | // This list contains all syntax that should be treaten as
484 | // containing binary values
485 | // The Syntax Definitons go into constants at the top of this page
486 | $syntax_binary = array(
487 | NET_LDAP2_SYNTAX_OCTET_STRING,
488 | NET_LDAP2_SYNTAX_JPEG
489 | );
490 |
491 | // Check Syntax
492 | $attr_s = $this->get('attribute', $attribute);
493 | if (Net_LDAP2::isError($attr_s)) {
494 | // Attribute not found in schema
495 | $return = false; // consider attr not binary
496 | } elseif (isset($attr_s['syntax']) && in_array($attr_s['syntax'], $syntax_binary)) {
497 | // Syntax is defined as binary in schema
498 | $return = true;
499 | } else {
500 | // Syntax not defined as binary, or not found
501 | // if attribute is a subtype, check superior attribute syntaxes
502 | if (isset($attr_s['sup'])) {
503 | foreach ($attr_s['sup'] as $superattr) {
504 | $return = $this->isBinary($superattr);
505 | if ($return) {
506 | break; // stop checking parents since we are binary
507 | }
508 | }
509 | }
510 | }
511 |
512 | return $return;
513 | }
514 |
515 | /**
516 | * See if an schema element exists
517 | *
518 | * @param string $type Type of name, see get()
519 | * @param string $name Name or OID
520 | *
521 | * @return boolean
522 | */
523 | public function exists($type, $name)
524 | {
525 | $entry = $this->get($type, $name);
526 | if ($entry instanceof Net_LDAP2_ERROR) {
527 | return false;
528 | } else {
529 | return true;
530 | }
531 | }
532 |
533 | /**
534 | * See if an attribute is defined in the schema
535 | *
536 | * @param string $attribute Name or OID of the attribute
537 | * @return boolean
538 | */
539 | public function attributeExists($attribute)
540 | {
541 | return $this->exists('attribute', $attribute);
542 | }
543 |
544 | /**
545 | * See if an objectClass is defined in the schema
546 | *
547 | * @param string $ocl Name or OID of the objectClass
548 | * @return boolean
549 | */
550 | public function objectClassExists($ocl)
551 | {
552 | return $this->exists('objectclass', $ocl);
553 | }
554 |
555 |
556 | /**
557 | * See to which ObjectClasses an attribute is assigned
558 | *
559 | * The objectclasses are sorted into the keys 'may' and 'must'.
560 | *
561 | * @param string $attribute Name or OID of the attribute
562 | *
563 | * @return array|Net_LDAP2_Error Associative array with OCL names or Error
564 | */
565 | public function getAssignedOCLs($attribute)
566 | {
567 | $may = array();
568 | $must = array();
569 |
570 | // Test if the attribute type is defined in the schema,
571 | // if so, retrieve real name for lookups
572 | $attr_entry = $this->get('attribute', $attribute);
573 | if ($attr_entry instanceof Net_LDAP2_ERROR) {
574 | return PEAR::raiseError("Attribute $attribute not defined in schema: ".$attr_entry->getMessage());
575 | } else {
576 | $attribute = $attr_entry['name'];
577 | }
578 |
579 |
580 | // We need to get all defined OCLs for this.
581 | $ocls = $this->getAll('objectclasses');
582 | foreach ($ocls as $ocl => $ocl_data) {
583 | // Fetch the may and must attrs and see if our searched attr is contained.
584 | // If so, record it in the corresponding array.
585 | $ocl_may_attrs = $this->may($ocl);
586 | $ocl_must_attrs = $this->must($ocl);
587 | if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) {
588 | array_push($may, $ocl_data['name']);
589 | }
590 | if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) {
591 | array_push($must, $ocl_data['name']);
592 | }
593 | }
594 |
595 | return array('may' => $may, 'must' => $must);
596 | }
597 |
598 | /**
599 | * See if an attribute is available in a set of objectClasses
600 | *
601 | * @param string $attribute Attribute name or OID
602 | * @param array $ocls Names of OCLs to check for
603 | *
604 | * @return boolean TRUE, if the attribute is defined for at least one of the OCLs
605 | */
606 | public function checkAttribute($attribute, $ocls)
607 | {
608 | foreach ($ocls as $ocl) {
609 | $ocl_entry = $this->get('objectclass', $ocl);
610 | $ocl_may_attrs = $this->may($ocl);
611 | $ocl_must_attrs = $this->must($ocl);
612 | if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) {
613 | return true;
614 | }
615 | if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) {
616 | return true;
617 | }
618 | }
619 | return false; // no ocl for the ocls found.
620 | }
621 | }
622 | ?>
--------------------------------------------------------------------------------
/lib/pear-pear.php.net/net_ldap2/Net/LDAP2/SchemaCache.interface.php:
--------------------------------------------------------------------------------
1 |
11 | * @copyright 2009 Benedikt Hallinger
12 | * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
13 | * @version SVN: $Id$
14 | * @link http://pear.php.net/package/Net_LDAP2/
15 | */
16 |
17 | /**
18 | * Interface describing a custom schema cache object
19 | *
20 | * To implement a custom schema cache, one must implement this interface and
21 | * pass the instanciated object to Net_LDAP2s registerSchemaCache() method.
22 | */
23 | interface Net_LDAP2_SchemaCache
24 | {
25 | /**
26 | * Return the schema object from the cache
27 | *
28 | * Net_LDAP2 will consider anything returned invalid, except
29 | * a valid Net_LDAP2_Schema object.
30 | * In case you return a Net_LDAP2_Error, this error will be routed
31 | * to the return of the $ldap->schema() call.
32 | * If you return something else, Net_LDAP2 will
33 | * fetch a fresh Schema object from the LDAP server.
34 | *
35 | * You may want to implement a cache aging mechanism here too.
36 | *
37 | * @return Net_LDAP2_Schema|Net_LDAP2_Error|false
38 | */
39 | public function loadSchema();
40 |
41 | /**
42 | * Store a schema object in the cache
43 | *
44 | * This method will be called, if Net_LDAP2 has fetched a fresh
45 | * schema object from LDAP and wants to init or refresh the cache.
46 | *
47 | * In case of errors you may return a Net_LDAP2_Error which will
48 | * be routet to the client.
49 | * Note that doing this prevents, that the schema object fetched from LDAP
50 | * will be given back to the client, so only return errors if storing
51 | * of the cache is something crucial (e.g. for doing something else with it).
52 | * Normaly you dont want to give back errors in which case Net_LDAP2 needs to
53 | * fetch the schema once per script run and instead use the error
54 | * returned from loadSchema().
55 | *
56 | * @return true|Net_LDAP2_Error
57 | */
58 | public function storeSchema($schema);
59 | }
60 |
--------------------------------------------------------------------------------
/lib/pear-pear.php.net/net_ldap2/Net/LDAP2/Search.php:
--------------------------------------------------------------------------------
1 |
11 | * @author Benedikt Hallinger
12 | * @copyright 2009 Tarjej Huse, Benedikt Hallinger
13 | * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
14 | * @version SVN: $Id$
15 | * @link http://pear.php.net/package/Net_LDAP2/
16 | */
17 |
18 | /**
19 | * Includes
20 | */
21 | require_once 'PEAR.php';
22 |
23 | /**
24 | * Result set of an LDAP search
25 | *
26 | * @category Net
27 | * @package Net_LDAP2
28 | * @author Tarjej Huse
29 | * @author Benedikt Hallinger
30 | * @license http://www.gnu.org/copyleft/lesser.html LGPL
31 | * @link http://pear.php.net/package/Net_LDAP22/
32 | */
33 | class Net_LDAP2_Search extends PEAR implements Iterator
34 | {
35 | /**
36 | * Search result identifier
37 | *
38 | * @access protected
39 | * @var resource
40 | */
41 | protected $_search;
42 |
43 | /**
44 | * LDAP resource link
45 | *
46 | * @access protected
47 | * @var resource
48 | */
49 | protected $_link;
50 |
51 | /**
52 | * Net_LDAP2 object
53 | *
54 | * A reference of the Net_LDAP2 object for passing to Net_LDAP2_Entry
55 | *
56 | * @access protected
57 | * @var object Net_LDAP2
58 | */
59 | protected $_ldap;
60 |
61 | /**
62 | * Result entry identifier
63 | *
64 | * @access protected
65 | * @var resource
66 | */
67 | protected $_entry = null;
68 |
69 | /**
70 | * The errorcode the search got
71 | *
72 | * Some errorcodes might be of interest, but might not be best handled as errors.
73 | * examples: 4 - LDAP_SIZELIMIT_EXCEEDED - indicates a huge search.
74 | * Incomplete results are returned. If you just want to check if there's anything in the search.
75 | * than this is a point to handle.
76 | * 32 - no such object - search here returns a count of 0.
77 | *
78 | * @access protected
79 | * @var int
80 | */
81 | protected $_errorCode = 0; // if not set - sucess!
82 |
83 | /**
84 | * Cache for all entries already fetched from iterator interface
85 | *
86 | * @access protected
87 | * @var array
88 | */
89 | protected $_iteratorCache = array();
90 |
91 | /**
92 | * What attributes we searched for
93 | *
94 | * The $attributes array contains the names of the searched attributes and gets
95 | * passed from $Net_LDAP2->search() so the Net_LDAP2_Search object can tell
96 | * what attributes was searched for ({@link searchedAttrs())
97 | *
98 | * This variable gets set from the constructor and returned
99 | * from {@link searchedAttrs()}
100 | *
101 | * @access protected
102 | * @var array
103 | */
104 | protected $_searchedAttrs = array();
105 |
106 | /**
107 | * Cache variable for storing entries fetched internally
108 | *
109 | * This currently is not used by all functions and need consolidation.
110 | *
111 | * @access protected
112 | * @var array
113 | */
114 | protected $_entry_cache = false;
115 |
116 | /**
117 | * Cache variable for count()
118 | *
119 | * @see count()
120 | * @access protected
121 | * @var int
122 | */
123 | protected $_count_cache = null;
124 |
125 | /**
126 | * Constructor
127 | *
128 | * @param resource $search Search result identifier
129 | * @param Net_LDAP2|resource $ldap Net_LDAP2 object or just a LDAP-Link resource
130 | * @param array $attributes (optional) Array with searched attribute names. (see {@link $_searchedAttrs})
131 | *
132 | * @access public
133 | */
134 | public function __construct($search, $ldap, $attributes = array())
135 | {
136 | parent::__construct('Net_LDAP2_Error');
137 |
138 | $this->setSearch($search);
139 |
140 | if ($ldap instanceof Net_LDAP2) {
141 | $this->_ldap = $ldap;
142 | $this->setLink($this->_ldap->getLink());
143 | } else {
144 | $this->setLink($ldap);
145 | }
146 |
147 | $this->_errorCode = @ldap_errno($this->_link);
148 |
149 | if (is_array($attributes) && !empty($attributes)) {
150 | $this->_searchedAttrs = $attributes;
151 | }
152 | }
153 |
154 | /**
155 | * Returns an array of entry objects.
156 | *
157 | * @return array Array of entry objects.
158 | */
159 | public function entries()
160 | {
161 | $entries = array();
162 |
163 | if (false === $this->_entry_cache) {
164 | // cache is empty: fetch from LDAP
165 | while ($entry = $this->shiftEntry()) {
166 | $entries[] = $entry;
167 | }
168 | $this->_entry_cache = $entries; // store result in cache
169 | }
170 |
171 | return $this->_entry_cache;
172 | }
173 |
174 | /**
175 | * Get the next entry in the searchresult from LDAP server.
176 | *
177 | * This will return a valid Net_LDAP2_Entry object or false, so
178 | * you can use this method to easily iterate over the entries inside
179 | * a while loop.
180 | *
181 | * @return Net_LDAP2_Entry|false Reference to Net_LDAP2_Entry object or false
182 | */
183 | public function shiftEntry()
184 | {
185 | if (is_null($this->_entry)) {
186 | if(!$this->_entry = @ldap_first_entry($this->_link, $this->_search)) {
187 | $false = false;
188 | return $false;
189 | }
190 | $entry = Net_LDAP2_Entry::createConnected($this->_ldap, $this->_entry);
191 | if ($entry instanceof PEAR_Error) $entry = false;
192 |
193 | } else if ($this->_entry === false) {
194 | //no more results
195 | return false;
196 |
197 | } else {
198 | if (!$this->_entry = @ldap_next_entry($this->_link, $this->_entry)) {
199 | $false = false;
200 | return $false;
201 | }
202 | $entry = Net_LDAP2_Entry::createConnected($this->_ldap, $this->_entry);
203 | if ($entry instanceof PEAR_Error) $entry = false;
204 | }
205 | return $entry;
206 | }
207 |
208 | /**
209 | * Alias function of shiftEntry() for perl-ldap interface
210 | *
211 | * @see shiftEntry()
212 | * @return Net_LDAP2_Entry|false
213 | */
214 | public function shift_entry()
215 | {
216 | $args = func_get_args();
217 | return call_user_func_array(array( $this, 'shiftEntry' ), $args);
218 | }
219 |
220 | /**
221 | * Retrieve the next entry in the searchresult, but starting from last entry
222 | *
223 | * This is the opposite to {@link shiftEntry()} and is also very useful
224 | * to be used inside a while loop.
225 | *
226 | * @return Net_LDAP2_Entry|false
227 | */
228 | public function popEntry()
229 | {
230 | if (false === $this->_entry_cache) {
231 | // fetch entries into cache if not done so far
232 | $this->_entry_cache = $this->entries();
233 | }
234 |
235 | $return = array_pop($this->_entry_cache);
236 | return (null === $return)? false : $return;
237 | }
238 |
239 | /**
240 | * Alias function of popEntry() for perl-ldap interface
241 | *
242 | * @see popEntry()
243 | * @return Net_LDAP2_Entry|false
244 | */
245 | public function pop_entry()
246 | {
247 | $args = func_get_args();
248 | return call_user_func_array(array( $this, 'popEntry' ), $args);
249 | }
250 |
251 | /**
252 | * Return entries sorted as array
253 | *
254 | * This returns a array with sorted entries and the values.
255 | * Sorting is done with PHPs {@link array_multisort()}.
256 | * This method relies on {@link as_struct()} to fetch the raw data of the entries.
257 | *
258 | * Please note that attribute names are case sensitive!
259 | *
260 | * Usage example:
261 | *
262 | * // to sort entries first by location, then by surename, but descending:
263 | * $entries = $search->sorted_as_struct(array('locality','sn'), SORT_DESC);
264 | *
265 | *
266 | * @param array $attrs Array of attribute names to sort; order from left to right.
267 | * @param int $order Ordering direction, either constant SORT_ASC or SORT_DESC
268 | *
269 | * @return array|Net_LDAP2_Error Array with sorted entries or error
270 | * @todo what about server side sorting as specified in http://www.ietf.org/rfc/rfc2891.txt?
271 | */
272 | public function sorted_as_struct($attrs = array('cn'), $order = SORT_ASC)
273 | {
274 | /*
275 | * Old Code, suitable and fast for single valued sorting
276 | * This code should be used if we know that single valued sorting is desired,
277 | * but we need some method to get that knowledge...
278 | */
279 | /*
280 | $attrs = array_reverse($attrs);
281 | foreach ($attrs as $attribute) {
282 | if (!ldap_sort($this->_link, $this->_search, $attribute)){
283 | $this->raiseError("Sorting failed for Attribute " . $attribute);
284 | }
285 | }
286 |
287 | $results = ldap_get_entries($this->_link, $this->_search);
288 |
289 | unset($results['count']); //for tidier output
290 | if ($order) {
291 | return array_reverse($results);
292 | } else {
293 | return $results;
294 | }*/
295 |
296 | /*
297 | * New code: complete "client side" sorting
298 | */
299 | // first some parameterchecks
300 | if (!is_array($attrs)) {
301 | return PEAR::raiseError("Sorting failed: Parameterlist must be an array!");
302 | }
303 | if ($order != SORT_ASC && $order != SORT_DESC) {
304 | return PEAR::raiseError("Sorting failed: sorting direction not understood! (neither constant SORT_ASC nor SORT_DESC)");
305 | }
306 |
307 | // fetch the entries data
308 | $entries = $this->as_struct();
309 |
310 | // now sort each entries attribute values
311 | // this is neccessary because later we can only sort by one value,
312 | // so we need the highest or lowest attribute now, depending on the
313 | // selected ordering for that specific attribute
314 | foreach ($entries as $dn => $entry) {
315 | foreach ($entry as $attr_name => $attr_values) {
316 | sort($entries[$dn][$attr_name]);
317 | if ($order == SORT_DESC) {
318 | array_reverse($entries[$dn][$attr_name]);
319 | }
320 | }
321 | }
322 |
323 | // reformat entrys array for later use with array_multisort()
324 | $to_sort = array(); // <- will be a numeric array similar to ldap_get_entries
325 | foreach ($entries as $dn => $entry_attr) {
326 | $row = array();
327 | $row['dn'] = $dn;
328 | foreach ($entry_attr as $attr_name => $attr_values) {
329 | $row[$attr_name] = $attr_values;
330 | }
331 | $to_sort[] = $row;
332 | }
333 |
334 | // Build columns for array_multisort()
335 | // each requested attribute is one row
336 | $columns = array();
337 | foreach ($attrs as $attr_name) {
338 | foreach ($to_sort as $key => $row) {
339 | $columns[$attr_name][$key] =& $to_sort[$key][$attr_name][0];
340 | }
341 | }
342 |
343 | // sort the colums with array_multisort, if there is something
344 | // to sort and if we have requested sort columns
345 | if (!empty($to_sort) && !empty($columns)) {
346 | $sort_params = '';
347 | foreach ($attrs as $attr_name) {
348 | $sort_params .= '$columns[\''.$attr_name.'\'], '.$order.', ';
349 | }
350 | eval("array_multisort($sort_params \$to_sort);"); // perform sorting
351 | }
352 |
353 | return $to_sort;
354 | }
355 |
356 | /**
357 | * Return entries sorted as objects
358 | *
359 | * This returns a array with sorted Net_LDAP2_Entry objects.
360 | * The sorting is actually done with {@link sorted_as_struct()}.
361 | *
362 | * Please note that attribute names are case sensitive!
363 | * Also note, that it is (depending on server capabilitys) possible to let
364 | * the server sort your results. This happens through search controls
365 | * and is described in detail at {@link http://www.ietf.org/rfc/rfc2891.txt}
366 | *
367 | * Usage example:
368 | *
369 | * // to sort entries first by location, then by surename, but descending:
370 | * $entries = $search->sorted(array('locality','sn'), SORT_DESC);
371 | *
372 | *
373 | * @param array $attrs Array of sort attributes to sort; order from left to right.
374 | * @param int $order Ordering direction, either constant SORT_ASC or SORT_DESC
375 | *
376 | * @return array|Net_LDAP2_Error Array with sorted Net_LDAP2_Entries or error
377 | * @todo Entry object construction could be faster. Maybe we could use one of the factorys instead of fetching the entry again
378 | */
379 | public function sorted($attrs = array('cn'), $order = SORT_ASC)
380 | {
381 | $return = array();
382 | $sorted = $this->sorted_as_struct($attrs, $order);
383 | if (PEAR::isError($sorted)) {
384 | return $sorted;
385 | }
386 | foreach ($sorted as $key => $row) {
387 | $entry = $this->_ldap->getEntry($row['dn'], $this->searchedAttrs());
388 | if (!PEAR::isError($entry)) {
389 | array_push($return, $entry);
390 | } else {
391 | return $entry;
392 | }
393 | }
394 | return $return;
395 | }
396 |
397 | /**
398 | * Return entries as array
399 | *
400 | * This method returns the entries and the selected attributes values as
401 | * array.
402 | * The first array level contains all found entries where the keys are the
403 | * DNs of the entries. The second level arrays contian the entries attributes
404 | * such that the keys is the lowercased name of the attribute and the values
405 | * are stored in another indexed array. Note that the attribute values are stored
406 | * in an array even if there is no or just one value.
407 | *
408 | * The array has the following structure:
409 | *
410 | * $return = array(
411 | * 'cn=foo,dc=example,dc=com' => array(
412 | * 'sn' => array('foo'),
413 | * 'multival' => array('val1', 'val2', 'valN')
414 | * )
415 | * 'cn=bar,dc=example,dc=com' => array(
416 | * 'sn' => array('bar'),
417 | * 'multival' => array('val1', 'valN')
418 | * )
419 | * )
420 | *
421 | *
422 | * @return array associative result array as described above
423 | */
424 | public function as_struct()
425 | {
426 | $return = array();
427 | $entries = $this->entries();
428 | foreach ($entries as $entry) {
429 | $attrs = array();
430 | $entry_attributes = $entry->attributes();
431 | foreach ($entry_attributes as $attr_name) {
432 | $attr_values = $entry->getValue($attr_name, 'all');
433 | if (!is_array($attr_values)) {
434 | $attr_values = array($attr_values);
435 | }
436 | $attrs[$attr_name] = $attr_values;
437 | }
438 | $return[$entry->dn()] = $attrs;
439 | }
440 | return $return;
441 | }
442 |
443 | /**
444 | * Set the search objects resource link
445 | *
446 | * @param resource $search Search result identifier
447 | *
448 | * @access public
449 | * @return void
450 | */
451 | public function setSearch($search)
452 | {
453 | $this->_search = $search;
454 | }
455 |
456 | /**
457 | * Set the ldap ressource link
458 | *
459 | * @param resource $link Link identifier
460 | *
461 | * @access public
462 | * @return void
463 | */
464 | public function setLink($link)
465 | {
466 | $this->_link = $link;
467 | }
468 |
469 | /**
470 | * Returns the number of entries in the searchresult
471 | *
472 | * @return int Number of entries in search.
473 | */
474 | public function count()
475 | {
476 | // this catches the situation where OL returned errno 32 = no such object!
477 | if (!$this->_search) {
478 | return 0;
479 | }
480 | // ldap_count_entries is slow (see pear bug #18752) with large results,
481 | // so we cache the result internally.
482 | if ($this->_count_cache === null) {
483 | $this->_count_cache = @ldap_count_entries($this->_link, $this->_search);
484 | }
485 |
486 | return $this->_count_cache;
487 | }
488 |
489 | /**
490 | * Get the errorcode the object got in its search.
491 | *
492 | * @return int The ldap error number.
493 | */
494 | public function getErrorCode()
495 | {
496 | return $this->_errorCode;
497 | }
498 |
499 | /**
500 | * Destructor
501 | *
502 | * @access protected
503 | */
504 | public function _Net_LDAP2_Search()
505 | {
506 | if ($this->_search !== false) {
507 | @ldap_free_result($this->_search);
508 | }
509 | }
510 |
511 | /**
512 | * Closes search result
513 | *
514 | * @return void
515 | */
516 | public function done()
517 | {
518 | $this->_Net_LDAP2_Search();
519 | }
520 |
521 | /**
522 | * Return the attribute names this search selected
523 | *
524 | * @return array
525 | * @see $_searchedAttrs
526 | * @access protected
527 | */
528 | protected function searchedAttrs()
529 | {
530 | return $this->_searchedAttrs;
531 | }
532 |
533 | /**
534 | * Tells if this search exceeds a sizelimit
535 | *
536 | * @return boolean
537 | */
538 | public function sizeLimitExceeded()
539 | {
540 | return ($this->getErrorCode() == 4);
541 | }
542 |
543 |
544 | /*
545 | * SPL Iterator interface methods.
546 | * This interface allows to use Net_LDAP2_Search
547 | * objects directly inside a foreach loop!
548 | */
549 | /**
550 | * SPL Iterator interface: Return the current element.
551 | *
552 | * The SPL Iterator interface allows you to fetch entries inside
553 | * a foreach() loop: foreach ($search as $dn => $entry) { ...
554 | *
555 | * Of course, you may call {@link current()}, {@link key()}, {@link next()},
556 | * {@link rewind()} and {@link valid()} yourself.
557 | *
558 | * If the search throwed an error, it returns false.
559 | * False is also returned, if the end is reached
560 | * In case no call to next() was made, we will issue one,
561 | * thus returning the first entry.
562 | *
563 | * @return Net_LDAP2_Entry|false
564 | */
565 | public function current()
566 | {
567 | if (count($this->_iteratorCache) == 0) {
568 | $this->next();
569 | reset($this->_iteratorCache);
570 | }
571 | $entry = current($this->_iteratorCache);
572 | return ($entry instanceof Net_LDAP2_Entry)? $entry : false;
573 | }
574 |
575 | /**
576 | * SPL Iterator interface: Return the identifying key (DN) of the current entry.
577 | *
578 | * @see current()
579 | * @return string|false DN of the current entry; false in case no entry is returned by current()
580 | */
581 | public function key()
582 | {
583 | $entry = $this->current();
584 | return ($entry instanceof Net_LDAP2_Entry)? $entry->dn() :false;
585 | }
586 |
587 | /**
588 | * SPL Iterator interface: Move forward to next entry.
589 | *
590 | * After a call to {@link next()}, {@link current()} will return
591 | * the next entry in the result set.
592 | *
593 | * @see current()
594 | * @return void
595 | */
596 | public function next()
597 | {
598 | // fetch next entry.
599 | // if we have no entrys anymore, we add false (which is
600 | // returned by shiftEntry()) so current() will complain.
601 | if (count($this->_iteratorCache) - 1 <= $this->count()) {
602 | $this->_iteratorCache[] = $this->shiftEntry();
603 | }
604 |
605 | // move on array pointer to current element.
606 | // even if we have added all entries, this will
607 | // ensure proper operation in case we rewind()
608 | next($this->_iteratorCache);
609 | }
610 |
611 | /**
612 | * SPL Iterator interface: Check if there is a current element after calls to {@link rewind()} or {@link next()}.
613 | *
614 | * Used to check if we've iterated to the end of the collection.
615 | *
616 | * @see current()
617 | * @return boolean FALSE if there's nothing more to iterate over
618 | */
619 | public function valid()
620 | {
621 | return ($this->current() instanceof Net_LDAP2_Entry);
622 | }
623 |
624 | /**
625 | * SPL Iterator interface: Rewind the Iterator to the first element.
626 | *
627 | * After rewinding, {@link current()} will return the first entry in the result set.
628 | *
629 | * @see current()
630 | * @return void
631 | */
632 | public function rewind()
633 | {
634 | reset($this->_iteratorCache);
635 | }
636 | }
637 |
638 | ?>
639 |
--------------------------------------------------------------------------------
/lib/pear-pear.php.net/net_ldap2/Net/LDAP2/SimpleFileSchemaCache.php:
--------------------------------------------------------------------------------
1 |
11 | * @copyright 2009 Benedikt Hallinger
12 | * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
13 | * @version SVN: $Id$
14 | * @link http://pear.php.net/package/Net_LDAP2/
15 | */
16 |
17 | /**
18 | * A simple file based schema cacher with cache aging.
19 | *
20 | * Once the cache is too old, the loadSchema() method will return false, so
21 | * Net_LDAP2 will fetch a fresh object from the LDAP server that will
22 | * overwrite the current (outdated) old cache.
23 | */
24 | class Net_LDAP2_SimpleFileSchemaCache implements Net_LDAP2_SchemaCache
25 | {
26 | /**
27 | * Internal config of this cache
28 | *
29 | * @see Net_LDAP2_SimpleFileSchemaCache()
30 | * @var array
31 | */
32 | protected $config = array(
33 | 'path' => '/tmp/Net_LDAP_Schema.cache',
34 | 'max_age' => 1200
35 | );
36 |
37 | /**
38 | * Initialize the simple cache
39 | *
40 | * Config is as following:
41 | * path Complete path to the cache file.
42 | * max_age Maximum age of cache in seconds, 0 means "endlessly".
43 | *
44 | * @param array $cfg Config array
45 | */
46 | public function __construct($cfg)
47 | {
48 | foreach ($cfg as $key => $value) {
49 | if (array_key_exists($key, $this->config)) {
50 | if (gettype($this->config[$key]) != gettype($value)) {
51 | $this->getCore()->dropFatalError(__CLASS__.": Could not set config! Key $key does not match type ".gettype($this->config[$key])."!");
52 | }
53 | $this->config[$key] = $value;
54 | } else {
55 | $this->getCore()->dropFatalError(__CLASS__.": Could not set config! Key $key is not defined!");
56 | }
57 | }
58 | }
59 |
60 | /**
61 | * Return the schema object from the cache
62 | *
63 | * If file is existent and cache has not expired yet,
64 | * then the cache is deserialized and returned.
65 | *
66 | * @return Net_LDAP2_Schema|Net_LDAP2_Error|false
67 | */
68 | public function loadSchema()
69 | {
70 | $return = false; // Net_LDAP2 will load schema from LDAP
71 | if (file_exists($this->config['path'])) {
72 | $cache_maxage = filemtime($this->config['path']) + $this->config['max_age'];
73 | if (time() <= $cache_maxage || $this->config['max_age'] == 0) {
74 | $return = unserialize(file_get_contents($this->config['path']));
75 | }
76 | }
77 | return $return;
78 | }
79 |
80 | /**
81 | * Store a schema object in the cache
82 | *
83 | * This method will be called, if Net_LDAP2 has fetched a fresh
84 | * schema object from LDAP and wants to init or refresh the cache.
85 | *
86 | * To invalidate the cache and cause Net_LDAP2 to refresh the cache,
87 | * you can call this method with null or false as value.
88 | * The next call to $ldap->schema() will then refresh the caches object.
89 | *
90 | * @param mixed $schema The object that should be cached
91 | * @return true|Net_LDAP2_Error|false
92 | */
93 | public function storeSchema($schema) {
94 | file_put_contents($this->config['path'], serialize($schema));
95 | return true;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/storage-fs/plugin.php:
--------------------------------------------------------------------------------
1 | 'storage:fs', # notrans
5 | 'version' => '0.3',
6 | 'name' => /* trans */ 'Attachments on the filesystem',
7 | 'author' => 'Jared Hancock',
8 | 'description' => /* trans */ 'Enables storing attachments on the filesystem',
9 | 'url' => 'http://www.osticket.com/plugins/storage-fs',
10 | 'plugin' => 'storage.php:FsStoragePlugin'
11 | );
12 |
13 | ?>
14 |
--------------------------------------------------------------------------------
/storage-fs/storage.php:
--------------------------------------------------------------------------------
1 | meta->getKey();
15 | $filename = $this->getPath($hash);
16 | if (!$this->fp)
17 | $this->fp = @fopen($filename, 'rb');
18 | if (!$this->fp)
19 | throw new IOException($filename.': Unable to open for reading');
20 | if ($offset)
21 | fseek($this->fp, $offset);
22 | if (($status = @fread($this->fp, $bytes)) === false)
23 | throw new IOException($filename.': Unable to read from file');
24 | return $status;
25 | }
26 |
27 | function passthru() {
28 | $hash = $this->meta->getKey();
29 | $filename = $this->getPath($hash);
30 | // TODO: Raise IOException on failure
31 | if (($status = @readfile($filename)) === false)
32 | throw new IOException($filename.': Unable to read from file');
33 | return $status;
34 | }
35 |
36 | function write($data) {
37 | $hash = $this->meta->getKey();
38 | $filename = $this->getPath($hash);
39 | if (!$this->fp)
40 | $this->fp = @fopen($filename, 'wb');
41 | if (!$this->fp)
42 | throw new IOException($filename.':Unable to open for reading');
43 | if (($status = @fwrite($this->fp, $data)) === false)
44 | throw new IOException($filename.': Unable to write to file');
45 | return $status;
46 | }
47 |
48 | function upload($filepath) {
49 | $destination = $this->getPath($this->meta->getKey());
50 | if (!@move_uploaded_file($filepath, $destination))
51 | throw new IOException($filepath.': Unable to move file');
52 | // TODO: Consider CHMOD on the file
53 | return true;
54 | }
55 |
56 | function unlink() {
57 | $filename = $this->getPath($this->meta->getKey());
58 | if (!@unlink($filename))
59 | throw new IOException($filename.': Unable to delete file');
60 | return true;
61 | }
62 |
63 | function getPath($hash) {
64 | // TODO: Make this configurable
65 | $prefix = $hash[0];
66 | $base = static::$base;
67 | if ($base[0] != '/' && $base[1] != ':')
68 | $base = ROOT_DIR . $base;
69 | // Auto-create the subfolders
70 | $base .= '/'.$prefix;
71 | if (!is_dir($base))
72 | mkdir($base, 0751);
73 |
74 | return $base.'/'.$hash;
75 | }
76 | }
77 |
78 | class FsStoragePluginConfig extends PluginConfig {
79 |
80 | // Provide compatibility function for versions of osTicket prior to
81 | // translation support (v1.9.4)
82 | static function translate() {
83 | if (!method_exists('Plugin', 'translate')) {
84 | return array(
85 | function($x) { return $x; },
86 | function($x, $y, $n) { return $n != 1 ? $y : $x; },
87 | );
88 | }
89 | return Plugin::translate('storage-fs');
90 | }
91 |
92 | function getOptions() {
93 | list($__, $_N) = self::translate();
94 | return array(
95 | 'uploadpath' => new TextboxField(array(
96 | 'label'=>$__('Base folder for attachment files'),
97 | 'hint'=>$__('The path must already exist and be writeable by the
98 | web server. If the path starts with neither a `/` nor a
99 | drive letter, the path will be assumed to be relative to
100 | the root of osTicket'),
101 | 'configuration'=>array('size'=>60, 'length'=>255),
102 | 'required'=>true,
103 | )),
104 | );
105 | }
106 |
107 | function pre_save(&$config, &$errors) {
108 | list($__, $_N) = self::translate();
109 | $path = $config['uploadpath'];
110 | if ($path[0] != '/' && $path[1] != ':')
111 | $path = ROOT_DIR . $path;
112 |
113 | $field = $this->getForm()->getField('uploadpath');
114 | $file = md5(microtime());
115 | if (!@is_dir($path))
116 | $field->addError($__('Path does not exist'));
117 | elseif (!@opendir($path))
118 | $field->addError($__('Unable to access directory'));
119 | elseif (!@touch("$path/$file"))
120 | $field->addError($__('Unable to write to directory'));
121 | elseif (!@unlink("$path/$file"))
122 | $field->addError($__('Unable to remove files from directory'));
123 | else {
124 | touch("$path/.keep");
125 | if (!is_file("$path/.htaccess"))
126 | file_put_contents("$path/.htaccess", array('Options -Indexes', PHP_EOL, 'Deny from all'));
127 | }
128 | return true;
129 | }
130 | }
131 |
132 | class FsStoragePlugin extends Plugin {
133 | var $config_class = 'FsStoragePluginConfig';
134 |
135 | function bootstrap() {
136 | $config = $this->getConfig();
137 | $uploadpath = $config->get('uploadpath');
138 | list($__, $_N) = $config::translate();
139 | if ($uploadpath) {
140 | FileStorageBackend::register('F', 'FilesystemStorage');
141 | FilesystemStorage::$base = $uploadpath;
142 | FilesystemStorage::$desc = $__('Filesystem') .': '.$uploadpath;
143 | }
144 | }
145 | }
146 |
147 |
--------------------------------------------------------------------------------
/storage-s3/config.php:
--------------------------------------------------------------------------------
1 | new TextboxField(array(
23 | 'label' => $__('S3 Bucket'),
24 | 'configuration' => array('size'=>40),
25 | )),
26 | 'folder' => new TextboxField(array(
27 | 'label' => $__('S3 Folder Path'),
28 | 'configuration' => array('size'=>40),
29 | )),
30 | 'aws-region' => new ChoiceField(array(
31 | 'label' => $__('AWS Region'),
32 | 'choices' => array(
33 | '' => 'US Standard',
34 | 'us-east-1' => 'US East (N. Virginia)',
35 | 'us-east-2' => 'US East (Ohio)',
36 | 'us-west-1' => 'US West (N. California)',
37 | 'us-west-2' => 'US West (Oregon)',
38 | 'af-south-1' => 'Africa (Cape Town)',
39 | 'ap-east-1' => 'Asia Pacific (Hong Kong)',
40 | 'ap-south-1' => 'Asia Pacific (Mumbai)',
41 | 'ap-south-2' => 'Asia Pacific (Hyderabad)',
42 | 'ap-southeast-3' => 'Asia Pacific (Jakarta)',
43 | 'ap-southeast-4' => 'Asia Pacific (Melbourne)',
44 | 'ap-northeast-3' => 'Asia Pacific (Osaka)',
45 | 'ap-northeast-2' => 'Asia Pacific (Seoul)',
46 | 'ap-southeast-1' => 'Asia Pacific (Singapore)',
47 | 'ap-southeast-2' => 'Asia Pacific (Sydney)',
48 | 'ap-northeast-1' => 'Asia Pacific (Tokyo)',
49 | 'ca-central-1' => 'Canada (Central)',
50 | 'ca-west-1' => 'Canada West (Calgary)',
51 | 'cn-north-1' => 'China (Beijing)',
52 | 'cn-northwest-1' => 'China (Ningxia)',
53 | 'eu-central-1' => 'Europe (Frankfurt)',
54 | 'eu-west-1' => 'Europe (Ireland)',
55 | 'eu-west-2' => 'Europe (London)',
56 | 'eu-south-1' => 'Europe (Milan)',
57 | 'eu-west-3' => 'Europe (Paris)',
58 | 'eu-south-2' => 'Europe (Spain)',
59 | 'eu-north-1' => 'Europe (Stockholm)',
60 | 'eu-central-2' => 'Europe (Zurich)',
61 | 'il-central-1' => 'Israel (Tel Aviv)',
62 | 'sa-east-1' => 'South America (São Paulo)',
63 | 'me-south-1' => 'Middle East (Bahrain)',
64 | 'me-central-1' => 'Middle East (UAE)',
65 | 'us-gov-east-1' => 'AWS GovCloud (US-East)',
66 | 'us-gov-west-1' => 'AWS GovCloud (US-West)',
67 | ),
68 | 'default' => '',
69 | )),
70 | 'acl' => new ChoiceField(array(
71 | 'label' => $__('Default ACL for Attachments'),
72 | 'choices' => array(
73 | '' => $__('Use Bucket Default'),
74 | 'private' => $__('Private'),
75 | 'public-read' => $__('Public Read'),
76 | 'public-read-write' => $__('Public Read and Write'),
77 | 'authenticated-read' => $__('Read for AWS authenticated Users'),
78 | 'bucket-owner-read' => $__('Read for Bucket Owners'),
79 | 'bucket-owner-full-control' => $__('Full Control for Bucket Owners'),
80 | ),
81 | 'default' => '',
82 | )),
83 |
84 | 'access-info' => new SectionBreakField(array(
85 | 'label' => $__('Access Information'),
86 | )),
87 | 'aws-key-id' => new TextboxField(array(
88 | 'required' => true,
89 | 'configuration'=>array('length'=>64, 'size'=>40),
90 | 'label' => $__('AWS Access Key ID'),
91 | )),
92 | 'secret-access-key' => new TextboxField(array(
93 | 'widget' => 'PasswordWidget',
94 | 'required' => false,
95 | 'configuration'=>array('length'=>64, 'size'=>40),
96 | 'label' => $__('AWS Secret Access Key'),
97 | )),
98 | );
99 | }
100 |
101 | function pre_save(&$config, &$errors) {
102 | list($__, $_N) = self::translate();
103 | $credentials['credentials'] = array(
104 | 'key' => $config['aws-key-id'],
105 | 'secret' => $config['secret-access-key']
106 | ?: Crypto::decrypt($this->get('secret-access-key'), SECRET_SALT,
107 | $this->getNamespace()),
108 | );
109 | if ($config['aws-region'])
110 | $credentials['region'] = $config['aws-region'];
111 |
112 | if (!$credentials['credentials']['secret'])
113 | $this->getForm()->getField('secret-access-key')->addError(
114 | $__('Secret access key is required'));
115 |
116 | $credentials['version'] = '2006-03-01';
117 | $credentials['signature_version'] = 'v4';
118 |
119 | $s3 = new Aws\S3\S3Client($credentials);
120 |
121 | try {
122 | $s3->headBucket(array('Bucket'=>$config['bucket']));
123 | }
124 | catch (Aws\S3\Exception\AccessDeniedException $e) {
125 | $errors['err'] = sprintf(
126 | /* The %s token will become an upstream error message */
127 | $__('User does not have access to this bucket: %s'), (string)$e);
128 | }
129 | catch (Aws\S3\Exception\NoSuchBucketException $e) {
130 | $this->getForm()->getField('bucket')->addError(
131 | $__('Bucket does not exist'));
132 | }
133 |
134 | if (!$errors && $config['secret-access-key'])
135 | $config['secret-access-key'] = Crypto::encrypt($config['secret-access-key'],
136 | SECRET_SALT, $this->getNamespace());
137 | else
138 | $config['secret-access-key'] = $this->get('secret-access-key');
139 |
140 | return true;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/storage-s3/plugin.php:
--------------------------------------------------------------------------------
1 | 'storage:s3',
5 | 'version' => '0.5',
6 | 'ost_version' => '1.17', # Require osTicket v1.17+
7 | 'name' => /* trans */ 'Attachments hosted in Amazon S3',
8 | 'author' => 'Jared Hancock, Kevin Thorne',
9 | 'description' => /* trans */ 'Enables storing attachments in Amazon S3',
10 | 'url' => 'http://www.osticket.com/plugins/storage-s3',
11 | 'requires' => array(
12 | "aws/aws-sdk-php" => array(
13 | 'version' => "3.*",
14 | 'map' => array(
15 | 'aws/aws-sdk-php/src' => 'lib/Aws',
16 | 'guzzlehttp/guzzle/src' => 'lib/GuzzleHttp',
17 | 'guzzlehttp/promises/src' => 'lib/GuzzleHttp/Promise',
18 | 'guzzlehttp/psr7/src/' => 'lib/GuzzleHttp/Psr7',
19 | 'mtdowling/jmespath.php/src' => 'lib/JmesPath',
20 | 'psr/http-client/src' => 'lib/Psr/Http/Client',
21 | 'psr/http-factory/src' => 'lib/Psr/Http/Factory',
22 | 'psr/http-message/src' => 'lib/Psr/Http/Message',
23 | ),
24 | ),
25 | ),
26 | 'scripts' => array(
27 | 'pre-autoload-dump' => 'Aws\\Script\\Composer\\Composer::removeUnusedServices',
28 | ),
29 | 'extra' => array(
30 | 'aws/aws-sdk-php' => ['S3'],
31 | ),
32 | 'plugin' => 'storage.php:S3StoragePlugin'
33 | );
34 |
35 | ?>
36 |
--------------------------------------------------------------------------------
/storage-s3/storage.php:
--------------------------------------------------------------------------------
1 | getInfo();
26 | static::$__config = $config;
27 | }
28 | function getConfig() {
29 | return static::$__config;
30 | }
31 |
32 | function __construct($meta) {
33 | parent::__construct($meta);
34 | $credentials['credentials'] = array(
35 | 'key' => static::$config['aws-key-id'],
36 | 'secret' => Crypto::decrypt(static::$config['secret-access-key'],
37 | SECRET_SALT, $this->getConfig()->getNamespace())
38 | );
39 | if (static::$config['aws-region'])
40 | $credentials['region'] = static::$config['aws-region'];
41 |
42 | $credentials['version'] = self::$version;
43 | $credentials['signature_version'] = self::$sig_vers;
44 |
45 | $this->client = new S3Client($credentials);
46 | }
47 |
48 | function read($bytes=false, $offset=0) {
49 | try {
50 | if (!$this->body)
51 | $this->openReadStream();
52 | // Reads may be cut short to 8k. Try to read $bytes if at all
53 | // possible.
54 | $chunk = '';
55 | $bytes = $bytes ?: self::getBlockSize();
56 | while (strlen($chunk) < $bytes) {
57 | $buf = $this->body->read($bytes - strlen($chunk));
58 | if (!$buf) break;
59 | $chunk .= $buf;
60 | }
61 | return $chunk;
62 | }
63 | catch (Aws\S3\Exception\NoSuchKeyException $e) {
64 | throw new IOException(self::getKey()
65 | .': Unable to locate file: '.(string)$e);
66 | }
67 | }
68 |
69 | function passthru() {
70 | try {
71 | while ($block = $this->read())
72 | print $block;
73 | }
74 | catch (Aws\S3\Exception\NoSuchKeyException $e) {
75 | throw new IOException(self::getKey()
76 | .': Unable to locate file: '.(string)$e);
77 | }
78 | }
79 |
80 | function write($block) {
81 | if (!$this->body)
82 | $this->openWriteStream();
83 | if (!isset($this->upload_hash))
84 | $this->upload_hash = hash_init('md5');
85 | hash_update($this->upload_hash, $block);
86 | return $this->body->write($block);
87 | }
88 |
89 | function flush() {
90 | return $this->upload($this->body);
91 | }
92 |
93 | function upload($filepath) {
94 | if ($filepath instanceof Stream) {
95 | $filepath->rewind();
96 | // Hashing already performed in the ::write() method
97 | }
98 | elseif (is_string($filepath)) {
99 | $this->upload_hash = hash_init('md5');
100 | hash_update_file($this->upload_hash, $filepath);
101 | $filepath = fopen($filepath, 'r');
102 | rewind($filepath);
103 | }
104 |
105 | try {
106 | $params = array(
107 | 'ContentType' => $this->meta->getType(),
108 | 'CacheControl' => 'private, max-age=86400',
109 | );
110 | if (isset($this->upload_hash))
111 | $params['Content-MD5'] =
112 | $this->upload_hash_final = hash_final($this->upload_hash);
113 |
114 | $info = $this->client->upload(
115 | static::$config['bucket'],
116 | self::getKey(true),
117 | $filepath,
118 | static::$config['acl'] ?: 'private',
119 | array('params' => $params)
120 | );
121 | return true;
122 | }
123 | catch (S3Exception $e) {
124 | throw new IOException('Unable to upload to S3: '.(string)$e);
125 | }
126 | return false;
127 | }
128 |
129 | // Support MD5 hash via the returned ETag header;
130 | function getNativeHashAlgos() {
131 | return array('md5');
132 | }
133 |
134 | function getHashDigest($algo) {
135 | if ($algo == 'md5' && isset($this->upload_hash_final))
136 | return $this->upload_hash_final;
137 |
138 | // Return nothing. The migrater will compute the hash by downloading
139 | // the object contents
140 | }
141 |
142 | // Send a redirect when the file is requested locally
143 | function sendRedirectUrl($disposition='inline', $ttl = false) {
144 | // expire based on ttl (if given), otherwise expire at midnight
145 | $now = time();
146 | $ttl = $ttl ? $now + $ttl : ($now + 86400 - ($now % 86400));
147 | Http::redirect($this->getSignedRequest(
148 | $this->client->getCommand('GetObject', [
149 | 'Bucket' => static::$config['bucket'],
150 | 'Key' => self::getKey(),
151 | 'ResponseContentDisposition' => sprintf("%s; %s;",
152 | $disposition,
153 | Http::getDispositionFilename($this->meta->getName())),
154 | ]), $ttl)->getUri());
155 | return true;
156 | }
157 |
158 | function unlink() {
159 | try {
160 | $this->client->deleteObject(array(
161 | 'Bucket' => static::$config['bucket'],
162 | 'Key' => self::getKey()
163 | ));
164 | return true;
165 | }
166 | catch (S3Exception $e) {
167 | throw new IOException('Unable to remove object: '
168 | . (string) $e);
169 | }
170 | }
171 |
172 | // Adapted from Aws\S3\StreamWrapper
173 | /**
174 | * Create a pre-signed Request for the given S3 command object.
175 | *
176 | * @param Aws\CommandInterface $command Command to create a pre-signed
177 | * URL for.
178 | * @param int|string|\DateTimeInterface $expires The time at which the URL should
179 | * expire. This can be a Unix
180 | * timestamp, a PHP DateTime object,
181 | * or a string that can be evaluated
182 | * by strtotime().
183 | *
184 | * @return RequestInterface
185 | */
186 | protected function getSignedRequest($command, $expires=0)
187 | {
188 | return $this->client->createPresignedRequest($command, $expires ?: '+30 minutes');
189 | }
190 |
191 | /**
192 | * Initialize the stream wrapper for a read only stream
193 | *
194 | * @return bool
195 | */
196 | protected function openReadStream() {
197 | $this->getBody(true);
198 | return true;
199 | }
200 |
201 | /**
202 | * Initialize the stream wrapper for a read/write stream
203 | */
204 | protected function openWriteStream() {
205 | $this->body = new Stream(fopen('php://temp', 'r+'));
206 | }
207 |
208 | protected function getBody($stream=false) {
209 | $params = array(
210 | 'Bucket' => static::$config['bucket'],
211 | 'Key' => self::getKey(),
212 | );
213 |
214 | $command = $this->client->getCommand('GetObject', $params);
215 | $command['@http']['stream'] = $stream;
216 | $result = $this->client->execute($command);
217 | $this->body = $result['Body'];
218 |
219 | // Wrap the body in a caching entity body if seeking is allowed
220 | //if ($this->getOption('seekable') && !$this->body->isSeekable()) {
221 | // $this->body = new CachingStream($this->body);
222 | //}
223 | return $this->body;
224 | }
225 |
226 | function getKey($create=false) {
227 | $attrs = $create ? self::getAttrs() : $this->meta->getAttrs();
228 | $attrs = JsonDataParser::parse($attrs);
229 |
230 | $key = ($attrs && $attrs['folder']) ?
231 | sprintf('%s/%s', $attrs['folder'], $this->meta->getKey()) :
232 | $this->meta->getKey();
233 |
234 | return $key;
235 | }
236 |
237 | function getAttrs() {
238 | $bucket = static::$config['bucket'];
239 | $folder = (static::$config['folder'] ? static::$config['folder'] : '');
240 | $attr = JsonDataEncoder::encode(array('bucket' => $bucket, 'folder' => $folder));
241 |
242 | return $attr;
243 | }
244 | }
245 |
246 | require_once 'config.php';
247 |
248 | class S3StoragePlugin extends Plugin {
249 | var $config_class = 'S3StoragePluginConfig';
250 |
251 | function isMultiInstance() {
252 | return false;
253 | }
254 |
255 | function bootstrap() {
256 | require_once 'storage.php';
257 |
258 | //TODO: This needs to target a specific instance
259 | $bucketPath = sprintf('%s%s', $this->getConfig()->get('bucket'),
260 | $this->getConfig()->get('folder') ? '/'. $this->getConfig()->get('folder') : '');
261 | S3StorageBackend::setConfig($this->getConfig());
262 | S3StorageBackend::$desc = sprintf('S3 (%s)', $bucketPath);
263 | FileStorageBackend::register('3', 'S3StorageBackend');
264 | }
265 | }
266 |
267 | require_once INCLUDE_DIR . 'UniversalClassLoader.php';
268 | use Symfony\Component\ClassLoader\UniversalClassLoader_osTicket;
269 | $loader = new UniversalClassLoader_osTicket();
270 | $loader->registerNamespaceFallbacks(array(
271 | dirname(__file__).'/lib'));
272 | $loader->register();
273 |
--------------------------------------------------------------------------------