├── .gitignore ├── LICENSE ├── README.md ├── audit ├── audit.php ├── class.audit.php ├── config.php ├── plugin.php └── templates │ ├── agent-audit.tmpl.php │ ├── auditlogs.tmpl.php │ ├── ticket-audit.tmpl.php │ └── user-audit.tmpl.php ├── auth-2fa ├── auth2fa.php ├── class.auth2fa.php ├── config.php └── plugin.php ├── auth-cas ├── authentication.php ├── cas.php ├── config.php └── plugin.php ├── auth-ldap ├── authentication.php ├── config.php └── plugin.php ├── auth-oauth2 ├── auth.php ├── config.php ├── oauth2.php └── plugin.php ├── auth-passthru ├── authenticate.php ├── config.php └── plugin.php ├── auth-password-policy ├── auth.php └── plugin.php ├── doc ├── auth-oauth.md └── i18n.md ├── lib ├── .keep └── pear-pear.php.net │ └── net_ldap2 │ └── Net │ ├── LDAP2.php │ └── LDAP2 │ ├── Entry.php │ ├── Filter.php │ ├── LDIF.php │ ├── RootDSE.php │ ├── Schema.php │ ├── SchemaCache.interface.php │ ├── Search.php │ ├── SimpleFileSchemaCache.php │ └── Util.php ├── make.php ├── storage-fs ├── plugin.php └── storage.php └── storage-s3 ├── config.php ├── plugin.php └── storage.php /.gitignore: -------------------------------------------------------------------------------- 1 | .htaccess 2 | php53.cgi 3 | include/ost-config.php 4 | *.sw[a-z] 5 | .DS_Store 6 | .vagrant 7 | Vagrantfile 8 | 9 | # Staging directory used for packaging script 10 | stage 11 | 12 | # Ignore compiled phar files 13 | *.phar 14 | 15 | # Ignore hydrated libs & include dir 16 | **/lib 17 | **/include 18 | 19 | # Ignore installed dependencies and composer if placed here 20 | lib/* 21 | !/lib/pear-pear.php.net/ 22 | composer.phar 23 | composer.lock 24 | composer.json 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Core plugins for osTicket 2 | ========================= 3 | 4 | Core plugins for osTicket-1.8 and onward 5 | 6 | Installing 7 | ========== 8 | 9 | Clone this repo or download the zip file and place the contents into your 10 | `include/plugins` folder 11 | 12 | After cloning, `hydrate` the repo by downloading the third-party library 13 | dependencies. 14 | 15 | php make.php hydrate 16 | 17 | Building Plugins 18 | ================ 19 | Make any necessary additions or edits to plugins and build PHAR files with 20 | the `make.php` command 21 | 22 | php -dphar.readonly=0 make.php build 23 | 24 | This will compile a PHAR file for the plugin directory. The PHAR will be 25 | named `plugin.phar` and can be dropped into the osTicket `plugins/` folder 26 | directly. 27 | 28 | Translating 29 | =========== 30 | 31 | [![Crowdin](https://d322cqt584bo4o.cloudfront.net/osticket-plugins/localized.png)](http://i18n.osticket.com/project/osticket-plugins) 32 | 33 | Translation service is being performed via the Crowdin translation 34 | management software. The project page for the plugins is located at 35 | 36 | https://crowdin.com/project/osticket-plugins 37 | -------------------------------------------------------------------------------- /audit/audit.php: -------------------------------------------------------------------------------- 1 | getConfig(); 11 | if ($config->get('show_view_audits')) 12 | AuditEntry::$show_view_audits = $config->get('show_view_audits'); 13 | 14 | // Ticket audit 15 | Signal::connect('ticket.view.more', function($ticket, &$extras) { 16 | global $thisstaff; 17 | if (!$thisstaff || !$thisstaff->isAdmin()) 18 | return; 19 | 20 | echo sprintf('
  • getId() . '/view'); 21 | echo 'onclick="javascript: $.dialog($(this).attr(\'href\').substr(1), 201); return false;"'; 22 | echo sprintf('>', 'icon-book' ?: 'icon-cogs'); 23 | echo __('View Audit Log'); 24 | echo '
  • '; 25 | }); 26 | 27 | // User audit tab 28 | Signal::connect('usertab.audit', function($user, &$extras) { 29 | global $thisstaff; 30 | if (!$thisstaff || !$thisstaff->isAdmin()) 31 | return; 32 | 33 | $tabTitle = str_replace('-', ' ', __('audits')); 34 | echo sprintf('
  • %s
  • ', __('audits'), __(ucwords($tabTitle))); 35 | }); 36 | 37 | // User audit body 38 | Signal::connect('user.audit', function($user, &$extras) { 39 | global $thisstaff; 40 | if (!$thisstaff || !$thisstaff->isAdmin()) 41 | return; 42 | 43 | echo ''; 46 | }); 47 | 48 | // Agent audit tab 49 | Signal::connect('agenttab.audit', function($staff, &$extras) { 50 | global $thisstaff; 51 | if (!$thisstaff || !$thisstaff->isAdmin()) 52 | return; 53 | 54 | echo '
  • Audits
  • '; 55 | }); 56 | 57 | // Agent audit tab body 58 | Signal::connect('agent.audit', function($staff, &$extras) { 59 | global $thisstaff; 60 | if (!$thisstaff || !$thisstaff->isAdmin()) 61 | return; 62 | 63 | echo ''; 66 | }); 67 | 68 | // Ajax View Ticket Audit 69 | Signal::connect('ajax.scp', function($dispatcher) { 70 | $dispatcher->append( 71 | url_get('^/audit/ticket/(?P\d+)/view$', function($ticketId) { 72 | global $thisstaff; 73 | 74 | $row = Ticket::objects()->filter(array('ticket_id'=>$ticketId))->values_flat('number')->first(); 75 | if (!$row) 76 | Http::response(404, 'No such ticket'); 77 | if (!$thisstaff || !$thisstaff->isAdmin()) 78 | Http::response(403, 'Contact your administrator'); 79 | 80 | include 'templates/ticket-audit.tmpl.php'; 81 | }) 82 | ); 83 | }); 84 | 85 | // Ajax Audit Export 86 | Signal::connect('ajax.scp', function($dispatcher) { 87 | $dispatcher->append( 88 | url('^/audit/export/(?P\w+)/(?P\w+)|uid,(?P\d+)|sid,(?P\d+)|tid,(?P\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 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 53 | 54 | 55 | 56 |
    DescriptionTimestampIP Address
    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 |
    92 |
    93 |
    94 |
    95 |
    96 |

    97 | 98 |

    99 |
    100 |
    101 |
    102 |
    103 | 104 |
    105 | '.$pageNav->showing().''; 108 | else 109 | echo __('No audits found'); 110 | ?> 111 |
    112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 128 | 129 | 132 | 133 | 134 | 135 | 136 | 139 | 140 | 141 | 142 |
    Description href="audits.php?&sort=timestamp">IP Address
    137 | 138 |
    143 | '; 145 | if ($total) { //Show options.. 146 | echo ' '.__('Page').':'.$pageNav->getPageLinks().' '; 147 | } 148 | echo sprintf('%s', 149 | $type, 150 | $state, 151 | 'audit-export', 152 | __('Export')); 153 | ?> 154 |
    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 |
    32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
    Description>IP Address
    53 |
    54 | getPageLinks('audits'); 56 | $links = str_replace(''; 60 | echo ' '.__('Page').':'.$links.' '; 61 | echo sprintf('%s', 62 | $ticketId, 63 | 'audit-export', 64 | __('Export')); 65 | echo '
    '; 66 | ?> 67 |

    68 | 69 | 71 | 72 |

    73 | 74 |
    75 | 76 | 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 | QR Code 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 | --------------------------------------------------------------------------------